Overview
Machine author: TheHermit. Host: Ubuntu, with Docker containers (Debian 8 Jessie). Theme: Star Trek: The Next Generation.
Chain: recon, SQL injection in a custom WordPress plugin, credentials in a draft post,
Joomla admin, PHP shell upload via eXtplorer, a www-data shell on the host (outside
Docker), a SUID stack buffer overflow in /bin/lcars, root.
The key trap: there are two different www-data shells. One is in a Docker container
(WordPress/Joomla), the other is on the real host (via Apache on 443). Root only comes
from the second one.
Reconnaissance
| |
Interpreting the banners is critical here:
- 80 and 8080 are Debian 8 (Jessie), so these are services in Docker containers.
- 443 is Ubuntu 2.4.25, so this is the real host. The Apache version mismatch signals a split infrastructure.
| |
Directory enumeration on 443:
| |
Findings:
https://enterprise.htb/files/(directory listing enabled)https://enterprise.htb/files/lcars.zip, the source of the custom WordPress plugin
Unzipping lcars.zip reveals the plugin files, including lcars_db.php, the SQL
injection entry point. Reading the source shows an unparameterized query using
$_GET['query'].
Initial Access
SQL injection in the custom lcars plugin
Vulnerable endpoint:
| |
| |
The query parameter is vulnerable to three techniques: boolean-based blind, error-based
(MySQL >= 5.0, FLOOR / information_schema.PLUGINS), and time-based blind (SLEEP(5)).
Backend: MySQL >= 5.0, PHP 5.6.31, Apache 2.4.10 (Debian).
| |
| |
The joomla database (without db) is empty; the real data is in joomladb, and
WordPress data is in wordpress.
Credentials in a draft post
| |
wp_posts contains an unpublished (draft) post titled “Passwords” (ID 66) where someone
saved passwords quickly:
| |
wp_users gives the WordPress admin login william.riker (phpass hash $P$B..., does
not crack with rockyou). Working WordPress pair: william.riker : u*Z14ru0p#ttj83zS6.
Draft and trash posts in wp_posts are a classic hiding place for secrets. Dump
post_content regardless of post_status. Revisions can also contain older versions of
secrets (revision 67 here has a different set of passwords from 68).
Docker trap, WordPress does not lead to root
Logging into the WordPress panel works, but every shell attempt (theme editing, PHP in a post) lands in a container:
| |
No docker, a minimal SUID set, user.txt is a joke text about the Holodeck. This is a
dead end; move to Joomla and get out of the container.
Joomla dump, password reuse
| |
The Joomla table prefix is random (edz2g_). Users:
| id | username | password (bcrypt $2y$10$) | |
|---|---|---|---|
| 400 | geordi.la.forge | [email protected] | $2y$10$cXSgEkNQGBBUneDKXq9gU... |
| 401 | Guinan | [email protected] | $2y$10$90gyQVv7oL6CCN8lF/0LYu... |
bcrypt is slow; instead of cracking, reuse the passwords from the WordPress draft
(password spraying). Working: geordi.la.forge : ZD3YxfnSjezg67JZ gives Joomla admin on
port 8080.
From Joomla admin to a shell on the host
Architecture: WordPress (80) and Joomla (8080) are in Docker. Apache on 443 is the host,
and the /files/ directory is shared between 443 and Joomla (8080).
In the Joomla panel: Components -> eXtplorer (file manager). It allows uploading to
/files/. Upload a PHP shell, then call it via port 443 (host), not 8080 (container):
| |
This gives a shell as www-data outside Docker (hostname enterprise, not a container
ID). Real user.txt:
| |
If you execute code in the WordPress context (container), you get:
| |
get_header() is a WordPress function, available only when the file is loaded through the
WordPress bootstrap. A standalone PHP shell on port 443 (/files/) avoids this and lands
on the host.
TTY stabilisation:
| |
Privilege Escalation
SUID buffer overflow in /bin/lcars
| |
A non-standard SUID root binary:
| |
It asks for an access code:
| |
Recover the access code with ltrace, which shows the strcmp() against a hardcoded
Bridge Access Code. With the correct code the binary shows a menu:
| |
Option 4 (Security) asks for a “Security Override” and reads input into an unbounded
buffer: a classic stack-based buffer overflow. The captured input to the binary is
picarda1\n4\n..., so picarda1 is the access code, 4 selects Security, and the rest
is the overflow.
Exploit characteristics (32-bit, non-PIE, classic ret2shellcode):
- Architecture: x86 32-bit; the return address is packed as
struct.pack('<L', ...)(little-endian). - Offset to EIP:
padding = 212. - No ASLR-safe address, so a hardcoded return address on the stack:
0xffffdd60(points into a NOP sled). - NOP sled: 70 bytes of
\x90. - Shellcode: ~105 bytes, does
cp /bin/bash /tmp/writeuppluschmod 4777 /tmp/writeup./tmp/writeupinherits SUID root, so./writeup -pgives root (bash -pkeeps the effective UID).
Bad chars:
| |
That is: null, \n, \r, vertical tab, tab, form feed, space. The shellcode must be
free of them, which is why it is encoded (shikata_ga_nai; the decoder stub is visible at
the start: \xd9\xee...\xd9\x74\x24\xf4\x5f is an FPU getPC plus a decode loop).
Stack addresses depend on environment variables (they are pushed onto the stack at
process start, shifting everything). To keep 0xffffdd60 stable between gdb and a real
run:
| |
env - starts the process with an empty environment, so the stack layout in gdb matches
the real run. LINES / COLUMNS add readline/terminal state and shift offsets, which is
the most common cause of “works in gdb, not outside gdb”.
Exploit (Python 3):
| |
Rules for porting BOF exploits to Python 3:
- All byte literals prefixed with
b"...". struct.packalready returnsbytes; do not wrap it instr().- Never
print(payload)to a binary file; usesys.stdout.buffer.write()oropen('payload.txt','wb'). - If you must stay on Python 2 (as in the original), generate the payload locally on Kali
(
python2.7), because Python 2.7 is not on the target; then transferpayload.txt.
Execution on the target:
| |
Terminal proof:
| |
Without -p, ./writeup gives a nice prompt but not root (whoami = www-data), because
bash drops euid by default. ./writeup -p keeps the SUID.
Detection and Mitigation
- A version mismatch across banners maps the infrastructure; segment services by version.
- Do not store secrets in post content or revisions (draft, trash,
revision); dumppost_contentwithout status filtering during review. - A container hostname (container ID), no
docker, and a minimal SUID set indicate a container; look for shared resources to reach the host. - Do not reuse passwords across CMSes (WordPress to Joomla).
- Stabilise the environment for BOF exploitation (
env -,unset LINES/COLUMNS) because ENV shifts the stack. - SUID bash requires
-p, or euid is dropped. - Port Python 2 to Python 3 exploits with everything as
bytes, output throughsys.stdout.buffer.write().
Lessons Learned
- Banner discrepancy is an infrastructure map. Debian vs Ubuntu, 2.4.10 vs 2.4.25 immediately revealed the container/host split.
- Secrets live in post content and revisions.
- Docker is a dead end; look for shared resources (
/files/between 8080 and 443) to reach the host. - Password reuse and spraying between CMSes.
- Classic 32-bit stack BOF: offset, NOP sled, encoded shellcode (bad chars), hardcoded ret; stabilise the environment.
- SUID bash needs
-p.
Command Reference
| |