KeyHunter parser v10 + 18 scanners + tests (synthetic fixtures only, no keys/db/tokens)
This commit is contained in:
162
gh_mint_tokens.py
Executable file
162
gh_mint_tokens.py
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate classic GitHub PATs using account credentials + puppeteer-like scraping.
|
||||
Reads creds from data/gh_accounts.txt (format: user:pass:email)
|
||||
Mints ghp_ tokens via GitHub settings/tokens page.
|
||||
"""
|
||||
import os, re, sys, time, random, sqlite3
|
||||
import requests
|
||||
|
||||
KF = os.path.dirname(os.path.abspath(__file__))
|
||||
ACC_PATH = os.path.join(KF, "data", "gh_accounts.txt")
|
||||
TOK_PATH = os.path.join(KF, "gh_tokens.txt")
|
||||
|
||||
# Token patterns
|
||||
RE_GH_CLASSIC = re.compile(r'ghp_[A-Za-z0-9]{36}')
|
||||
|
||||
def load_accounts():
|
||||
if not os.path.exists(ACC_PATH):
|
||||
print("[!] No gh_accounts.txt — need user:pass:email lines")
|
||||
# Try finding from pat_remint.log
|
||||
log_path = os.path.join(KF, "data", "pat_remint.log")
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path) as f:
|
||||
text = f.read()
|
||||
users = re.findall(r'\[(\d+)/\d+\]\s+(\w+)', text)
|
||||
# Only get the usernames from the log
|
||||
print(f"Found {len(users)} usernames in pat_remint.log")
|
||||
print("Need passwords file to proceed. Create data/gh_accounts.txt")
|
||||
return []
|
||||
|
||||
accounts = []
|
||||
with open(ACC_PATH) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and ':' in line:
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 2:
|
||||
accounts.append({
|
||||
'user': parts[0],
|
||||
'pass': parts[1],
|
||||
'email': parts[2] if len(parts) > 2 else f"{parts[0]}@proton.me"
|
||||
})
|
||||
return accounts
|
||||
|
||||
def mint_token(session, user, password):
|
||||
"""Mint a classic PAT via GitHub UI — direct API where possible."""
|
||||
# Step 1: Login
|
||||
r = session.get("https://github.com/login", timeout=10)
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
|
||||
# Extract authenticity_token
|
||||
m = re.search(r'name="authenticity_token" value="([^"]+)"', r.text)
|
||||
if not m:
|
||||
return None
|
||||
token_csrf = m.group(1)
|
||||
|
||||
# Step 2: POST login
|
||||
r = session.post("https://github.com/session",
|
||||
data={
|
||||
"authenticity_token": token_csrf,
|
||||
"login": user,
|
||||
"password": password,
|
||||
"commit": "Sign in",
|
||||
"return_to": ""
|
||||
},
|
||||
timeout=15)
|
||||
|
||||
if "Incorrect username or password" in r.text or r.status_code != 200:
|
||||
return None
|
||||
|
||||
# Step 3: Go to tokens page
|
||||
r = session.get("https://github.com/settings/tokens", timeout=10)
|
||||
m = re.search(r'name="authenticity_token" value="([^"]+)"', r.text)
|
||||
if not m:
|
||||
# Might need OTP
|
||||
return None
|
||||
|
||||
auth_token = m.group(1)
|
||||
|
||||
# Step 4: Create classic token
|
||||
r = session.post("https://github.com/settings/tokens",
|
||||
data={
|
||||
"authenticity_token": auth_token,
|
||||
"personal_access_token[description]": f"dev-{random.randint(1000,9999)}",
|
||||
"personal_access_token[scopes][]": ["repo", "read:user", "user:email"],
|
||||
},
|
||||
headers={"Accept": "text/html"},
|
||||
timeout=15)
|
||||
|
||||
# Step 5: Extract token from redirect/response
|
||||
m = RE_GH_CLASSIC.search(r.text)
|
||||
if m:
|
||||
return m.group(0)
|
||||
|
||||
# Try from headers/cookies
|
||||
for hist in r.history:
|
||||
m = RE_GH_CLASSIC.search(hist.text) if hasattr(hist, 'text') else None
|
||||
if m:
|
||||
return m.group(0)
|
||||
m = RE_GH_CLASSIC.search(hist.headers.get('Location', ''))
|
||||
if m:
|
||||
return m.group(0)
|
||||
|
||||
return None
|
||||
|
||||
def main():
|
||||
accounts = load_accounts()
|
||||
if not accounts:
|
||||
print("[!] No accounts. Create data/gh_accounts.txt with user:pass[:email] lines")
|
||||
return 1
|
||||
|
||||
print(f"Accounts: {len(accounts)}")
|
||||
|
||||
# Load existing tokens
|
||||
existing = set()
|
||||
if os.path.exists(TOK_PATH):
|
||||
with open(TOK_PATH) as f:
|
||||
for l in f:
|
||||
l = l.strip()
|
||||
if l and not l.startswith('#'):
|
||||
existing.add(l)
|
||||
|
||||
minted = 0
|
||||
failed = 0
|
||||
|
||||
for i, acc in enumerate(accounts):
|
||||
if minted >= 500:
|
||||
print(f"\nTarget 500 reached!")
|
||||
break
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
"User-Agent": f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/130.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml",
|
||||
})
|
||||
|
||||
try:
|
||||
token = mint_token(session, acc['user'], acc['pass'])
|
||||
if token:
|
||||
if token not in existing:
|
||||
existing.add(token)
|
||||
with open(TOK_PATH, "a") as f:
|
||||
f.write(token + "\n")
|
||||
minted += 1
|
||||
print(f"[{i+1}/{len(accounts)}] ✓ {token[4:14]}... | user={acc['user']} | total={minted}")
|
||||
else:
|
||||
print(f"[{i+1}/{len(accounts)}] ~ duplicate | user={acc['user']}")
|
||||
else:
|
||||
failed += 1
|
||||
if failed % 5 == 0:
|
||||
print(f"[{i+1}/{len(accounts)}] ✗ fail #{failed} | user={acc['user']}")
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
print(f"[{i+1}/{len(accounts)}] ! {e}")
|
||||
|
||||
time.sleep(random.uniform(1.5, 3.5))
|
||||
|
||||
print(f"\nDone. Minted: {minted}, Failed: {failed}, Pool: {len(existing)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user