#!/usr/bin/env python3 """GCKey signup flow reconnaissance — read-only observation. Navigates the GCKey signup process via Playwright, captures screenshots and form field details at each step. Does NOT submit any forms. """ import asyncio import json import os import io import sys from playwright.async_api import async_playwright async def dump_page(page, label): """Capture current page state and upload screenshot.""" print(f"\n{'='*60}") print(f" STEP: {label}") print(f" URL: {page.url}") print(f" Title: {await page.title()}") # Get form inputs els = await page.evaluate("""() => { const r = []; document.querySelectorAll('input, select, textarea').forEach(el => { r.push({ tag: el.tagName, name: el.name || '', id: el.id || '', type: el.type || '', ph: el.placeholder || '', req: el.required, ml: el.maxLength > 0 ? el.maxLength : 0, vis: el.offsetParent !== null, }); }); return r; }""") visible_inputs = [e for e in els if e.get('vis') or e.get('type') == 'hidden'] if visible_inputs: print(f" Inputs ({len(visible_inputs)}):") for e in visible_inputs: hid = " [HIDDEN]" if e['type'] == 'hidden' else "" print(f" <{e['tag']}> name={e['name']} id={e['id']} type={e['type']}" f" placeholder='{e['ph']}' required={e['req']} maxlen={e['ml']}{hid}") # Get buttons btns = await page.evaluate("""() => { const r = []; document.querySelectorAll('button, input[type=submit], input[type=button]').forEach(el => { r.push({ tag: el.tagName, val: el.value || '', txt: (el.textContent || '').trim().substring(0, 60), id: el.id || '', name: el.name || '', }); }); return r; }""") if btns: print(f" Buttons ({len(btns)}):") for b in btns: print(f" <{b['tag']}> name={b['name']} id={b['id']}" f" value='{b['val'][:40]}' text='{b['txt'][:40]}'") # Get links with auth keywords links = await page.evaluate("""() => { const keywords = ['sign', 'register', 'create', 'gckey', 'enroll', 'new user', 'account']; return Array.from(document.querySelectorAll('a')).map(a => ({ t: (a.textContent || '').trim().substring(0, 80), h: a.href || '', })).filter(a => { const lower = (a.t + ' ' + a.h).toLowerCase(); return keywords.some(k => lower.includes(k)); }); }""") if links: print(f" Auth-related links ({len(links)}):") for l in links: print(f" '{l['t'][:60]}' -> {l['h'][:100]}") # Check for CAPTCHA captcha = await page.evaluate("""() => { return { recaptcha: !!document.querySelector('iframe[src*=recaptcha], .g-recaptcha, #recaptcha'), hcaptcha: !!document.querySelector('iframe[src*=hcaptcha], .h-captcha'), imgcaptcha: !!document.querySelector('img[alt*=captcha i], img[src*=captcha i], img[id*=captcha i]'), iframes: Array.from(document.querySelectorAll('iframe')).map(f => f.src).filter(s => s), }; }""") print(f" CAPTCHA: recaptcha={captcha['recaptcha']} hcaptcha={captcha['hcaptcha']} img={captcha['imgcaptcha']}") if captcha['iframes']: print(f" Iframes: {captcha['iframes']}") # Screenshot — save locally, skip MinIO for recon ss = await page.screenshot(type="png") fname = f"/tmp/gckey_{label.replace(' ', '_').lower()}.png" with open(fname, "wb") as f: f.write(ss) print(f" Screenshot saved: {fname} ({len(ss)} bytes)") return ss async def recon(): async with async_playwright() as p: browser = await p.chromium.launch( headless=True, args=["--ignore-certificate-errors", "--no-sandbox", "--disable-dev-shm-usage"], ) ctx = await browser.new_context( viewport={"width": 1280, "height": 900}, locale="en-CA", ignore_https_errors=True, ) page = await ctx.new_page() try: # ── Step 1: CRTC SAML → GCKey login page ────────────────── print("Step 1: CRTC SmartForms → GACS → GCKey login") await page.goto( "https://services.crtc.gc.ca/Pro/SmartForms/?_gc_lang=eng", wait_until="domcontentloaded", timeout=30000, ) await asyncio.sleep(2) # Click "GCKey Log In" to get through GACS to GCKey gckey_login = await page.query_selector("a:has-text('GCKey Log In')") if gckey_login: await gckey_login.click() try: await page.wait_for_load_state("domcontentloaded", timeout=30000) except Exception: pass await asyncio.sleep(5) await dump_page(page, "01_gckey_login") # ── Step 2: Navigate directly to the Sign Up page ────────── # Extract ReqID from current URL import re req_match = re.search(r'ReqID=([A-Z0-9]+)', page.url) if req_match: req_id = req_match.group(1) signup_url = f"https://clegc-gckey.gc.ca/j/eng/rg?ReqID={req_id}" print(f"\nStep 2: Navigating to signup: {signup_url}") await page.goto(signup_url, wait_until="domcontentloaded", timeout=20000) await asyncio.sleep(3) await dump_page(page, "02_gckey_signup_terms") # ── Step 3: Accept terms ─────────────────────────────── accept = await page.query_selector( "input[type=submit][value*='Accept'], " "input[type=submit][value*='accept'], " "button:has-text('Accept'), " "button:has-text('I Accept'), " "input[type=submit][value*='I Accept']" ) if accept: val = await accept.get_attribute("value") or "" print(f"\nStep 3: Accepting terms ('{val}')...") await accept.click() try: await page.wait_for_load_state("domcontentloaded", timeout=20000) except Exception: pass await asyncio.sleep(3) await dump_page(page, "03_gckey_create_username") # ── Step 4: Capture username creation page ───────── # Fill a dummy username to see what the next page looks like # (we won't submit — just fill and capture) username_field = await page.query_selector("input[name*='user'], input[name*='token'], input[id*='user']") if username_field: print("\nStep 4: Found username field — filling dummy value...") await username_field.fill("pw-recon-test-12345") # Find the submit/continue button cont = await page.query_selector( "input[type=submit]:not([value*='Cancel']):not([value*='Exit']), " "button[type=submit]:not(:has-text('Cancel'))" ) if cont: val = await cont.get_attribute("value") or await cont.inner_text() print(f" Continue button: '{val}' — clicking to see password page...") await cont.click() try: await page.wait_for_load_state("domcontentloaded", timeout=20000) except Exception: pass await asyncio.sleep(3) await dump_page(page, "04_gckey_create_password") # ── Step 5: Capture password page ───────── pwd_field = await page.query_selector("input[type=password]") if pwd_field: print("\nStep 5: Found password field — filling dummy...") await pwd_field.fill("Pw$Recon2026!x9") # Check for confirm password pwd_confirm = await page.evaluate("""() => { const pwds = document.querySelectorAll('input[type=password]'); return pwds.length; }""") print(f" Password fields count: {pwd_confirm}") if pwd_confirm >= 2: fields = await page.query_selector_all("input[type=password]") await fields[1].fill("Pw$Recon2026!x9") cont2 = await page.query_selector( "input[type=submit]:not([value*='Cancel']):not([value*='Exit']), " "button[type=submit]:not(:has-text('Cancel'))" ) if cont2: val = await cont2.get_attribute("value") or await cont2.inner_text() print(f" Continue button: '{val}' — clicking to see security questions...") await cont2.click() try: await page.wait_for_load_state("domcontentloaded", timeout=20000) except Exception: pass await asyncio.sleep(3) await dump_page(page, "05_gckey_security_questions") # ── Step 6: Capture security questions ─ # Check for select elements (question dropdowns) selects = await page.query_selector_all("select") if selects: print(f"\nStep 6: Found {len(selects)} select dropdowns") for i, sel in enumerate(selects): options = await sel.evaluate("""el => Array.from(el.options).map(o => ({v: o.value, t: o.text})) """) print(f" Select {i}: {len(options)} options") for opt in options[:10]: print(f" '{opt['t'][:60]}' (value={opt['v']})") if len(options) > 10: print(f" ... and {len(options)-10} more") else: print("\n No accept/terms button found — page might be different") else: print(" Could not extract ReqID from URL") except Exception as e: print(f"\nERROR: {e}") import traceback traceback.print_exc() await dump_page(page, "error_state") finally: await browser.close() print(f"\n{'='*60}") print("RECON COMPLETE") print("Screenshots uploaded to minio://performancewest/recon/gckey/") print("="*60) if __name__ == "__main__": asyncio.run(recon())