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!
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.
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 SonarCloud:
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.
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
It is then surprising to see this behavior when running
git blame --output=foo; notice the presence of a new file named
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
--output because its implementation uses other sub-commands that do support
--output: command-line arguments are parsed several times by these components.
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
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.
To solve the challenge, we created a Git repository with the following structure:
worktree/: empty folders to comply with the expected structure of a Git repository
HEAD: non-empty file to fake a valid reference
config: 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 bare
worktree = worktree: the working tree directory under which checked-out are files
fsmonitor = $(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.
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!