# salve como paymap.py e rode: python paymap.py https://alvo/checkout
# deps: pip install requests beautifulsoup4
import sys, json, re, urllib.parse as up
import requests
from bs4 import BeautifulSoup
URL = sys.argv[1] if len(sys.argv) > 1 else input("URL alvo: ").strip()
COOKIE = input("Cookie header (enter p/ nenhum): ").strip()
AUTH = input("Authorization header (enter p/ nenhum): ").strip()
H = {"User-Agent": "Mozilla/5.0 (audit)", "Accept": "text/html,*/*"}
if COOKIE: H["Cookie"] = COOKIE
if AUTH: H["Authorization"] = AUTH
r = requests.get(URL, headers=H, timeout=20, allow_redirects=True)
soup = BeautifulSoup(r.text, "html.parser")
GW = {
"stripe": r"js\.stripe\.com|stripe-v3",
"paypal": r"paypal\.com/sdk|paypalobjects",
"mercadopago": r"mercadopago|mercadolibre",
"pagseguro": r"pagseguro|uol\.com\.br/checkout",
"adyen": r"adyen\.com",
"braintree": r"braintreegateway|braintree-web",
"cielo": r"cieloecommerce|cielo\.com\.br",
"iugu": r"iugu\.com",
"ebanx": r"ebanx",
"square": r"squareup|squarecdn",
"klarna": r"klarna\.com",
"applepay": r"apple-pay",
"googlepay": r"pay\.google\.com|googlepay",
}
scripts = [s.get("src") for s in soup.find_all("script") if s.get("src")]
gateways = [name for name, rx in GW.items() if any(re.search(rx, s or "", re.I) for s in scripts)]
forms = []
for i, f in enumerate(soup.find_all("form")):
inputs = []
for el in f.find_all(["input", "select", "textarea"]):
t = (el.get("type") or el.name).lower()
val = el.get("value")
if t == "password": val = "***"
elif t != "hidden" and val: val = "<user>"
inputs.append({
"name": el.get("name"), "id": el.get("id"), "type": t, "value": val,
"required": el.has_attr("required"), "pattern": el.get("pattern"),
"maxlength": el.get("maxlength"),
})
blob = " ".join([f.get("action") or "", " ".join(f.get("class") or []), f.get("id") or ""])
pay_like = bool(re.search(r"pay|checkout|order|cart|charge|billing|card", blob, re.I)) \
or any(re.search(r"card|cvv|cvc|cpf|amount|price|total|order", x["name"] or "", re.I) for x in inputs)
if pay_like:
forms.append({
"idx": i, "action": up.urljoin(URL, f.get("action") or ""),
"method": (f.get("method") or "GET").upper(),
"enctype": f.get("enctype"), "autocomplete": f.get("autocomplete"),
"inputs": inputs,
})
target_host = up.urlparse(URL).netloc
third = sorted({up.urlparse(s).netloc for s in scripts if s and up.urlparse(s).netloc and up.urlparse(s).netloc != target_host})
sec_headers = {k: r.headers.get(k) for k in [
"Content-Security-Policy", "Strict-Transport-Security", "X-Frame-Options",
"X-Content-Type-Options", "Referrer-Policy", "Permissions-Policy",
"Cross-Origin-Opener-Policy", "Cross-Origin-Resource-Policy",
]}
cookie_flags = []
for c in r.raw.headers.getlist("Set-Cookie") if hasattr(r.raw, "headers") else []:
name = c.split("=", 1)[0].strip()
cookie_flags.append({
"name": name,
"httpOnly": "httponly" in c.lower(),
"secure": "secure" in c.lower(),
"sameSite": (re.search(r"samesite=(\w+)", c, re.I).group(1) if re.search(r"samesite=", c, re.I) else None),
})
# ---------- coleta corpus JS (inline + externos same-origin, com limite) ----------
inline_js = "\n".join([(s.string or "") for s in soup.find_all("script") if not s.get("src")])
ext_corpus = []
MAX_EXT = 8
MAX_BYTES = 400_000
for s in scripts[:MAX_EXT]:
if not s: continue
src_abs = up.urljoin(URL, s)
if up.urlparse(src_abs).netloc != target_host:
continue
try:
rj = requests.get(src_abs, headers=H, timeout=10)
if rj.ok and len(rj.text) <= MAX_BYTES:
ext_corpus.append((src_abs, rj.text))
except Exception:
pass
JS_ALL = inline_js + "\n" + "\n".join(t for _, t in ext_corpus)
# ---------- GraphQL ----------
gql_endpoints = sorted(set(re.findall(r"[\"'`]([^\"'`\s]*?/graphql[^\"'`\s]*)[\"'`]", JS_ALL, re.I)))
gql_ops = []
for m in re.finditer(r"\b(query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)", JS_ALL):
gql_ops.append({"kind": m.group(1).lower(), "name": m.group(2)})
# dedup ops
_seen = set(); gql_ops = [o for o in gql_ops if (o["kind"], o["name"]) not in _seen and not _seen.add((o["kind"], o["name"]))]
gql_op_names = sorted(set(re.findall(r"operationName[\"']?\s*[:=]\s*[\"']([A-Za-z_][A-Za-z0-9_]*)", JS_ALL)))
gql_pay_ops = [o for o in gql_ops if re.search(r"pay|checkout|order|charge|cart|refund|capture|intent|subscri|invoice", o["name"], re.I)]
# ---------- Webhooks / IPN / callbacks ----------
WEBHOOK_RX = r"[\"'`]([^\"'`\s]*?/(?:webhook|webhooks|hooks|ipn|notify|notification|notifications|callback|callbacks|postback|listener|events)[^\"'`\s]*)[\"'`]"
webhook_urls = sorted(set(re.findall(WEBHOOK_RX, JS_ALL + " " + r.text, re.I)))
webhook_headers_hint = sorted(set(re.findall(r"[\"'`](x-[a-z0-9-]*(?:signature|hmac|hub-signature|webhook)[a-z0-9-]*)[\"'`]", JS_ALL, re.I)))
# ---------- Polling assíncrono de status ----------
poll_endpoints = sorted(set(re.findall(r"[\"'`]([^\"'`\s]*?/(?:status|poll|polling|pending|await|check|state|progress|order[-_]?status|payment[-_]?status|transaction[-_]?status)[^\"'`\s]*)[\"'`]", JS_ALL, re.I)))
uses_set_interval = bool(re.search(r"setInterval\s*\(", JS_ALL))
uses_recursive_timeout = bool(re.search(r"setTimeout\s*\([^)]*(?:poll|check|status|refresh|retry)", JS_ALL, re.I))
event_sources = sorted(set(re.findall(r"new\s+EventSource\s*\(\s*[\"'`]([^\"'`]+)[\"'`]", JS_ALL)))
websockets = sorted(set(re.findall(r"new\s+WebSocket\s*\(\s*[\"'`]([^\"'`]+)[\"'`]", JS_ALL)))
status_state_machine = sorted(set(re.findall(r"[\"'`](pending|processing|awaiting_?payment|requires_?action|succeeded|paid|captured|authorized|failed|declined|refunded|reversed|chargeback)[\"'`]", JS_ALL, re.I)))
async_flow = {
"graphql": {
"endpoints": gql_endpoints,
"operations": gql_ops[:200],
"operationNamesFromRequests": gql_op_names,
"paymentRelatedOperations": gql_pay_ops,
},
"webhooks": {
"urlsHardcoded": webhook_urls,
"signatureHeadersReferenced": webhook_headers_hint,
},
"polling": {
"statusEndpoints": poll_endpoints,
"setInterval": uses_set_interval,
"recursiveSetTimeout": uses_recursive_timeout,
"eventSource": event_sources,
"webSocket": websockets,
"statusStatesReferenced": status_state_machine,
},
"corpus": {
"inlineScriptBytes": len(inline_js),
"externalScriptsFetched": [u for u, _ in ext_corpus],
},
}
OUT = {
"url": URL, "status": r.status_code, "finalUrl": r.url,
"gateways": gateways, "forms": forms, "thirdParty": third,
"securityHeaders": sec_headers, "cookies": cookie_flags,
"scripts": scripts[:50],
"asyncFlow": async_flow,
}
PROMPT = (
"Voce eh auditor de seguranca (bug bounty AUTORIZADO). Analise o JSON abaixo — "
"fluxo de PAGAMENTO de " + URL + ".\n"
"Liste em portugues:\n"
"1) VULNERABILIDADES provaveis (price tampering em hidden inputs, IDOR em orderId, "
"CSRF ausente, cookies sem HttpOnly/Secure/SameSite, CSP frouxa, HSTS ausente, "
"gateway PCI mal integrado, scripts 3rd-party arriscados no checkout, GraphQL sem "
"persisted queries / introspection aberta / mutations de pagamento sem authz, "
"webhooks sem HMAC/replay-protection, endpoints de polling de status como oraculo "
"de enumeracao ou IDOR, EventSource/WebSocket sem authz por canal).\n"
"2) EVIDENCIAS (aponte o campo/endpoint/header/operationName no JSON, incluindo "
"asyncFlow.graphql, asyncFlow.webhooks e asyncFlow.polling).\n"
"3) PoC minima (curl) para reproduzir em lab.\n"
"4) CORRECAO recomendada.\n"
"Formato: [severidade] titulo -> evidencia -> PoC -> fix.\n\n"
"JSON:\n\`\`\`json\n" + json.dumps(OUT, indent=2, ensure_ascii=False) + "\n\`\`\`\n"
)
with open("paymap.json", "w", encoding="utf-8") as fp:
json.dump(OUT, fp, indent=2, ensure_ascii=False)
with open("paymap.prompt.txt", "w", encoding="utf-8") as fp:
fp.write(PROMPT)
print("[ok] gerados: paymap.json + paymap.prompt.txt")
print(f"gateways: {gateways} forms: {len(forms)} 3rd-party: {len(third)}")
print(f"graphql endpoints: {len(async_flow['graphql']['endpoints'])} ops: {len(async_flow['graphql']['operations'])} webhooks: {len(async_flow['webhooks']['urlsHardcoded'])} polling: {len(async_flow['polling']['statusEndpoints'])} ws/sse: {len(async_flow['polling']['webSocket']) + len(async_flow['polling']['eventSource'])}")