cdp_captcha_extract.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. """Extract _captcha_ from the LOGIN PAGE's localStorage (pc.hzwlan.com origin —
  2. it was seen in the earlier dump) and re-trigger login with network capture
  3. to see if the client auto-attaches it and what the server says.
  4. Also screenshots the captcha widget to identify its type (slide vs icon).
  5. """
  6. import asyncio
  7. import base64
  8. import json
  9. import sys
  10. import httpx
  11. import websockets
  12. CDP = "http://127.0.0.1:9222"
  13. async def get_targets():
  14. async with httpx.AsyncClient(timeout=5) as c:
  15. r = await c.get(f"{CDP}/json/list")
  16. return r.json()
  17. class Page:
  18. def __init__(self, ws):
  19. self.ws = ws
  20. self.mid = 0
  21. self.events = []
  22. async def call(self, method, params=None):
  23. self.mid += 1
  24. await self.ws.send(json.dumps({"id": self.mid, "method": method, "params": params or {}}))
  25. while True:
  26. msg = json.loads(await self.ws.recv())
  27. if msg.get("id") == self.mid:
  28. return msg.get("result", {})
  29. elif "method" in msg:
  30. self.events.append(msg)
  31. async def eval(self, expr):
  32. r = await self.call("Runtime.evaluate", {"expression": expr, "returnByValue": True})
  33. return r.get("result", {}).get("value")
  34. async def main():
  35. targets = await get_targets()
  36. login = next((t for t in targets if "login" in t.get("url", "") and t["type"] == "page"), None)
  37. if not login:
  38. print("no login page target. Current targets:")
  39. for t in targets:
  40. print(f" [{t['type']}] {t['url'][:70]}")
  41. return 1
  42. print(f"target: {login['url'][:60]}")
  43. async with websockets.connect(login["webSocketDebuggerUrl"], max_size=10**7) as ws:
  44. page = Page(ws)
  45. # 1. Read _captcha_ from THIS origin's localStorage
  46. cap = await page.eval("localStorage.getItem('_captcha_')")
  47. print(f"\n=== _captcha_ on pc.hzwlan.com ===")
  48. if cap:
  49. print(f"raw (first 80): {cap[:80]}")
  50. try:
  51. padded = cap + "=" * (-len(cap) % 4)
  52. decoded = base64.urlsafe_b64decode(padded).decode("utf-8", errors="replace")
  53. d = json.loads(decoded)
  54. print(f"decoded fields: {list(d.keys())}")
  55. print(f" captchaId: {d.get('captchaId','?')}")
  56. print(f" genTime: {d.get('genTime','?')} ({'stale' if (int(d.get('genTime',0) or 0) < 1789500000) else 'fresh'})")
  57. print(f" lotNumber: {str(d.get('lotNumber','?'))[:30]}")
  58. print(f" passToken: {str(d.get('passToken','?'))[:20]}...")
  59. except Exception as e:
  60. print(f"decode err: {e}")
  61. else:
  62. print("(empty on this origin)")
  63. # 2. Screenshot the page as-is (captcha state?)
  64. shot = await page.call("Page.captureScreenshot", {"format": "png"})
  65. if shot.get("data"):
  66. with open("cdp_captcha_state.png", "wb") as f:
  67. f.write(base64.b64decode(shot["data"]))
  68. print(f"\nscreenshot: cdp_captcha_state.png")
  69. # 3. Inspect the geetest widget structure (what TYPE of captcha?)
  70. gt_info = await page.eval("""
  71. (function() {
  72. const g = document.querySelector('.geetest_box, [class*=geetest_panel]');
  73. if (!g) return JSON.stringify({present: false});
  74. const r = g.getBoundingClientRect();
  75. // v4 widget internals
  76. const hasSlider = !!g.querySelector('[class*=slider_button], .geetest_btn_drag, [class*=btn_drag]');
  77. const hasIcons = !!g.querySelector('[class*=icon], .geetest_icon');
  78. const hasImage = !!g.querySelector('img, canvas, [class*=bg_img], [class*=puzzle]');
  79. const texts = Array.from(g.querySelectorAll('*')).filter(e => e.children.length===0 && (e.innerText||'').trim()).slice(0,8).map(e => e.innerText.trim().slice(0,30));
  80. return JSON.stringify({present: true, x: r.x, y: r.y, w: r.width, h: r.height, hasSlider, hasIcons, hasImage, texts});
  81. })()
  82. """)
  83. print(f"\n=== geetest widget ===\n{gt_info}")
  84. # 4. Enable network + click login again, watch for byPwd WITH _captcha_
  85. await page.call("Network.enable")
  86. page.events.clear()
  87. # ensure credentials still filled
  88. filled = await page.eval("""
  89. (function() {
  90. const u = document.querySelector('#username');
  91. const p = document.querySelector('#password');
  92. return JSON.stringify({u: u ? (u.value||'').length : -1, p: p ? (p.value||'').length : -1});
  93. })()
  94. """)
  95. fs = json.loads(filled or "{}")
  96. if fs.get("u", 0) < 3:
  97. await page.eval(f"""
  98. (function() {{
  99. const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
  100. const u = document.querySelector('#username');
  101. const p = document.querySelector('#password');
  102. set.call(u, 'longwu988'); u.dispatchEvent(new Event('input', {{bubbles:true}}));
  103. set.call(p, 'Longwu1988'); p.dispatchEvent(new Event('input', {{bubbles:true}}));
  104. return 'filled';
  105. }})()
  106. """)
  107. print("\nclicking login (stored _captcha_ should auto-attach)...")
  108. await page.eval("""
  109. (function() {
  110. const btn = document.querySelector('button.ant-btn.login-btn, button.login-btn, button.ant-btn');
  111. if (btn) { btn.click(); return 'ok'; }
  112. return 'no btn';
  113. })()
  114. """)
  115. # monitor 20s for the byPwd request + response
  116. print("monitoring 20s...")
  117. await asyncio.sleep(20)
  118. reqs = {}
  119. bodies = {}
  120. for e in page.events:
  121. m = e.get("method", "")
  122. p = e.get("params", {})
  123. if m == "Network.requestWillBeSent":
  124. url = p.get("request", {}).get("url", "")
  125. if "byPwd" in url:
  126. reqs[p["requestId"]] = {
  127. "url": url[:80],
  128. "postData": (p["request"].get("postData") or "")[:400],
  129. }
  130. elif m == "Network.responseReceived":
  131. if p.get("requestId") in reqs:
  132. reqs[p["requestId"]]["status"] = p["response"].get("status")
  133. print(f"\n=== byPwd requests this round: {len(reqs)} ===")
  134. for rid, r in reqs.items():
  135. print(f" -> {r['url']}")
  136. print(f" status={r.get('status','?')}")
  137. pd = r.get("postData", "")
  138. if "_captcha_" in pd:
  139. print(f" *** _captcha_ ATTACHED ***")
  140. # show structure (redact values)
  141. try:
  142. j = json.loads(pd)
  143. keys = list(j.keys())
  144. print(f" body keys: {keys}")
  145. except Exception:
  146. pass
  147. else:
  148. print(f" no _captcha_ in body")
  149. try:
  150. b = await page.call("Network.getResponseBody", {"requestId": rid})
  151. txt = b.get("body", "")
  152. if b.get("base64Encoded"):
  153. txt = base64.b64decode(txt).decode("utf-8", errors="replace")
  154. print(f" resp: {txt[:250]}")
  155. except Exception as ex:
  156. print(f" body err: {type(ex).__name__}")
  157. # 5. final page state
  158. final = await page.eval("location.href.slice(0,70)")
  159. print(f"\nfinal url: {final}")
  160. return 0
  161. sys.exit(asyncio.run(main()))