During recent security research, we came up with a fun "trick" that we later shared in a Capture the Flag challenge for the Hack.lu CTF and our Code Security Advent Calendar. We received good feedback and wanted to share the details with a broader audience.
Let's say that you discovered a code vulnerability that allows you to truncate arbitrary files. It sounds like a pretty weak exploitation primitive, but if you are dealing with an application that involves operations on a Git repository under your control, you're in luck!
The vulnerable snippet
For our example, let's use the code snippet of Day 16 of this year's Code Security Advent Calendar. It implements a service that allows cloning an arbitrary Git repository and later running git blame
on specific files and lines.
challenge.py
This code suffers from an argument injection vulnerability when crafting the command line for git blame
. Argument injections are widespread code vulnerabilities identified by our static analysis technology; you can find a scan report of the above snippet on SonarQube Cloud:
Exploiting argument injection vulnerabilities depends heavily on the features offered by the invoked binary.
For instance, if a hypothetic program supports the option --output=foo
that writes the program output to the file foo
, attackers who can inject this argument could create new files or overwrite existing ones. The attacker's goal is usually to gain the ability to execute arbitrary code on the server, and such primitives are very powerful but also quite rare.
Finding an interesting argument
Let's get back to our code snippet, where we can add new arguments to the git blame
invocation.
After looking at the manual of git-blame
, we couldn't find any "interesting" option to execute arbitrary code. Most arguments alter the behavior of the blame process or the way it renders its output. Most importantly, the manual does not document the presence of the option --output
, which is usually present on other git
sub-commands.
It is then surprising to see this behavior when running git blame --output=foo
; notice the presence of a new file named foo
:
Although the command failed, an empty file named foo
was created. If a file with the same name already exists, the destination file is truncated!
This option provides attackers with an arbitrary file truncation primitive. The command git-blame
supports --output
because its implementation uses other sub-commands that do support --output
: command-line arguments are parsed several times by these components.
Putting the pieces together
As we demonstrated in Securing Developer Tools: Git Integrations, control over the Git options of a local repository is dangerous: several configuration directives allow specifying external commands to change Git's behavior. For instance, core.fsmonitor
can point to a third-party program to replace Git's built-in filesystem monitor. This process happens during most operations, including git blame
.
We could leverage this technique if we find a way to force Git operations to ignore the local repository and use one in our control instead. As you may have already guessed, the file truncation primitive was proven to be useful here.
We can trick Git into loading a configuration from an unintended location by corrupting a critical file like .git/HEAD
. In such cases, Git starts looking for repositories in the current folder, which the attacker fully controls as it is the work tree with all the files of the cloned remote repository.
Solving the challenge
To solve the challenge, we created a Git repository with the following structure:
objects/
,refs/
,worktree/
: empty folders to comply with the expected structure of a Git repositoryHEAD
: non-empty file to fake a valid referenceconfig
: malicious configuration based on what we described in Securing Developer Tools: Git Integrations and Justin Steven's advisory. Most importantly, it should contain:bare = false
: don't mark the current directory as bareworktree = worktree
: the working tree directory under which checked-out are filesfsmonitor = $(id>/pwned)#
: the custom filesystem monitor daemon to start at the next Git invocation; this is the attacker's payload
When the repository is imported for the first time, nothing happens because the local Git repository stored in .git
is constructed during the clone
operation: this repository is valid and ignores the bare repository we planted.
Then, the argument injection is triggered to truncate .git/HEAD
, corrupting the once-valid local repository. By invoking git blame
a second time, git
now uses the malicious bare repository and calls the custom filesystem monitor, effectively executing the attacker's payload.
Closing words
As we shared with our series of publications on vulnerabilities in the IT monitoring software Checkmk, seemingly minor vulnerabilities can hide a critical impact. Our Clean Code approach helps you identify these security liabilities before they are deployed to production.
We hope you enjoyed this article and learned something about argument injection bugs; we sure had fun!