// HackTheBox

Curling

secret.txt-leaked Joomla creds → template-edit RCE → curl -K config injection via a root cron job

HackTheBoxEasyLinux Jul 21, 2026 4 min read

Recon

Two ports, and only one of them is a real attack surface:

Port Service
22 SSH
80 Apache + Joomla

SSH gives nothing without a credential, so the Joomla site on port 80 is the whole box. Joomla is a PHP CMS whose administrator panel can edit template source directly — so if we can log in as an admin, that panel is a straight line to code execution. The first job is therefore to find an admin password.

Foothold

1. Leaked password in secret.txt

The homepage source carries an HTML comment that a normal visitor never sees but that ships in the markup:

<!-- secret.txt -->

Requesting /secret.txt returns a single base64 string, Q3VybGluZzIwMTgh. Base64 is trivially reversible, so decode it:

$ echo Q3VybGluZzIwMTgh | base64 -d
Curling2018!

The site bylines its articles to the author floris, giving us a username to pair with the recovered password Curling2018!.

2. Admin login → template-edit RCE

floris:Curling2018! logs straight into the Joomla administrator panel. Joomla’s Template Manager lets a Super User edit the PHP files of the active template from the browser, which is the classic no-CVE Joomla-to-RCE move: write PHP into a template file, then request that file over the web.

Edit the Protostar template’s error.php and inject a one-line command shell:

<?php system($_REQUEST['c']); ?>

The save is handled by the com_templates component’s template.save task, which needs the jform[extension_id] and jform[filename] fields plus a valid CSRF token alongside the edited source — Joomla silently refuses the write if any of those are missing.

The planted file now runs whatever command is passed in the c parameter. Requesting /templates/protostar/error.php?c=id executes id, and the code runs as www-data, the Apache/PHP user.

3. password_backup → floris’s SSH password

In floris’s home directory sits a file called password_backup. Its contents are a text hex dump — the printable output of xxd — rather than raw binary, so the first move is to reverse the hex dump back into bytes with xxd -r. Running file on the result shows it is bzip2-compressed, and each layer underneath turns out to be another compressor: the nesting is bzip2 → gzip → bzip2 → tar. Peel them off one at a time with the matching tool, then extract the archive:

$ xxd -r password_backup > stage1.bz2
$ bunzip2 -c stage1.bz2 > stage2.gz
$ gunzip -c stage2.gz > stage3.bz2
$ bunzip2 -c stage3.bz2 > stage4.tar
$ tar xf stage4.tar
$ cat password.txt
5d<wdCbdZu)|hChXll

5d<wdCbdZu)|hChXll is floris’s SSH password, so ssh floris@... upgrades the www-data webshell into a proper interactive session as floris and yields user.txt.

Privilege Escalation — curl -K config injection via root cron

Enumerating floris’s files turns up ~/admin-area/input, owned root:floris with mode rw-rw---- — writable by floris. It is not a data file but a curl config file (the format curl reads with its -K flag), and a root cron job runs curl -K input -o report on a short interval, writing the result to report (which floris can read). The default config just fetches the local web server:

url = "http://127.0.0.1"

Because curl config directives include url, and curl’s file:// scheme reads local files, we can point that fetch anywhere on the filesystem. curl runs as root from the cron, so it can read root-only files. Overwrite the config’s URL:

url = "file:///root/root.txt"

On the next cron tick, root’s curl reads /root/root.txt and writes it into report, which floris can then read:

$ echo 'url = "file:///root/root.txt"' > ~/admin-area/input
$ cat ~/admin-area/report

Timing. A box anti-tamper routine resets input back to the default every ~10-15 seconds, and the curl cron fires within that same window — so write input and read report within a few seconds of each other, before the reset clobbers the injected URL.

The -o report output path is hardcoded in the cron command, so the primitive is strictly read-only: we can make root read any file into report, but we cannot redirect that output to overwrite something (e.g. an authorized_keys or a SUID binary) for a full shell. root.txt is captured directly, and since root has no id_rsa to steal, that read primitive is exactly enough to finish the box.

← All writeups