abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · cve-2026-19553-wrap-bio
CPython ssl - Python 软件基金会
当设置了 check_hostname 时,ssl.SSLContext.wrap_bio() 并不要求 server_hostname 为非 None。SSLObject 会静默跳过主机名验证。程序看起来像是在 check_hostname=True 的情况下成功完成了。没有 ValueError。证书链得到了验证。但对端的名称没有。wrap_socket() 已经会抛出异常。asyncio 的 SSLProtocol / start_tls / open_connection 会把 "" 变成 None,然后调用 wrap_bio。
一个持有针对不同主机名的 CA 有效证书的 MITM,可以针对一个忘记传入 server_hostname 的 wrap_bio / asyncio 客户端完成握手。
| ID | CVE-2026-19553 |
| CWE | CWE-297 |
| CVSS | High: 7.6 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N |
| 产品 | CPython ssl |
| 受影响版本 | < 3.12.15、3.13.0-3.13.15、3.14.0-3.14.7、3.15.0a1(3.15.0 之前) |
| 认证 | MITM / 攻击者 TLS 服务器;受害者是使用 wrap_bio 或 asyncio 且未提供主机名的 Python TLS 客户端 |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1 |
位于通信路径上(或者本身就是客户端本想连接的那台服务器)。出示一张客户端所信任、但名称并非客户端预期名称的证书。如果客户端使用了 wrap_bio(..., server_hostname=None),或者使用 asyncio 并传入 server_hostname="",同时设置了 check_hostname=True 和 CERT_REQUIRED,那么握手就会成功。身份从未被检查。在同一个上下文上,wrap_socket 会在任何字节传输之前抛出 ValueError。向 wrap_bio 传入非空主机名仍然会拒绝不匹配的情况。
同一产品,另一处遗留问题:SNI SSLContext UAF。位于 TLS 的另一侧。二者不能组合利用。
PSF 发布了 CVE-2026-19553。Issue python/cpython#156793、PR 158503、提交 6dc0069a。_check_sslobject_params 已经对 wrap_socket 运行过。SSLObject._create 跳过了它。NEWS:完成了一次验证了证书链但未验证对端身份的握手,且没有任何迹象表明该检查被跳过。打补丁后,在 check_hostname=True 且 server_hostname=None/"" 的情况下,wrap_bio 会抛出 ValueError("check_hostname requires server_hostname")。3.12 的向后移植版本则改为抛出 DeprecationWarning。
我搭建了 python:3.14.7-slim-bookworm。同一个实验 CA,两张叶子证书:victim.lab 和 evil.lab。客户端设置 check_hostname=True、CERT_REQUIRED。
INJECT:针对 evil.lab 的 wrap_bio(server_hostname=None) 被接受。对端 SAN 为 evil.lab。SNI 为 None。TLS_AES_256_GCM_SHA384。CONTROL A:wrap_socket(server_hostname=None) 抛出 ValueError: check_hostname requires server_hostname。CONTROL B:针对 evil.lab 的 wrap_bio(server_hostname="victim.lab") 抛出 SSLCertVerificationError 主机名不匹配。NEGATIVE:针对 victim.lab 的同一调用被接受。额外:在 127.0.0.1:18510 上 asyncio.open_connection(..., server_hostname="") 被接受("" 变成 None,然后调用 wrap_bio)。
已记录的错误转向:第一个 asyncio start_server 草稿漏掉了一个右括号;主机编译在 compose 之前就发现了它。主机 3.14.7(Clang、OpenSSL 3.6.4)复现了同样的四个 oracle;实验记录是容器固定版本(GCC、OpenSSL 3.0.22)。剧场:一个反向 shell。oracle 是 accept-wrong-name 加上 wrap_socket 仍然抛出异常。
cd lab
./run.sh
镜像 python:3.14.7-slim-bookworm。Compose 项目 cve-2026-19553。回环地址 127.0.0.1:18510 是 asyncio 附加项。MemoryBIO 不需要端口。
INJECT wrap_bio_none vs evil.lab: accept peer=evil.lab
CONTROL_A wrap_socket_none: ValueError:check_hostname requires server_hostname
CONTROL_B wrap_bio_name='victim.lab' vs evil.lab: mismatch-reject
SUCCESS CVE-2026-19553 wrap_bio_none=accept wrap_socket_none=ValueError wrap_bio_name=mismatch-reject CVE-2026-19553-WRAPBIO-HOST-WITNESS
向 wrap_bio()、asyncio.create_connection() 或 loop.start_tls() 传入非空的 server_hostname。升级至 3.12.15 / 3.13.16 / 3.14.8 / 3.15.0。该补丁仅将静默跳过改为 wrap_socket 原本就会抛出的相同 ValueError。