the_open_door
coffee
ESEN
← Back to index

RCE via git hooks

A git hook is a script that git runs automatically around certain actions (commit, push, merge, checkout...). They live in .git/hooks/ inside each repository and are local to that repo: they aren't versioned and don't travel with a clone, which is why they're easy to overlook. If the script is executable, git runs it; otherwise it's ignored.

The most useful ones for escalation run before the action completes:

The escalation pattern. The hook runs with the privileges of the user launching the git operation, not whoever wrote the hook. The dangerous combination appears when:

  1. A privileged process (a root cron, a service) periodically runs a git commit, git pull or similar over a repository.
  2. We can write to that repository's .git/hooks/ directory (because we own the working tree, e.g. a repo in our $HOME).

Then planting the right hook executes code as that privileged process:

echo '#!/bin/bash' > /path/to/repo/.git/hooks/pre-commit
echo 'chmod u+s /bin/bash' >> /path/to/repo/.git/hooks/pre-commit
chmod +x /path/to/repo/.git/hooks/pre-commit

On the process's next cycle, root's git commit fires the pre-commit and /bin/bash gets the SUID bit. Recovering root is immediate by keeping the EUID:

bash -p     # -p doesn't drop the SUID binary's EUID 0
id          # euid=0(root)

Instead of SUID you can use any payload: a reverse shell, copying a key into /root/.ssh/authorized_keys, adding a user, etc. SUID on bash is the cleanest and most reliable.

How to spot it. The clue is usually a recurring process. Without pspy a loop diffing ps output works:

old=$(ps -eo command); while true; do new=$(ps -eo command); diff <(echo "$old") <(echo "$new"); old=$new; done

Seeing a recurring git commit/git-sync launched by root over a writable repo is the signal. It fits the general cron over a writable resource pattern: the abusable part isn't the cron itself, but that it touches a resource under our control.

Defence. Don't run privileged git operations over repositories whose working tree belongs to an unprivileged user; use core.hooksPath pointing to a read-only, controlled directory; or git -c core.hooksPath=/dev/null to disable hooks in automation.

Toolspspy

Machines where it's used