A 27-Year-Old Authentication Bypass in OpenBSD's PPP Stack
摘要
该漏洞源于 `sppp(4)` 子系统在处理 PAP 认证时,直接使用攻击者可控的长度字段进行 `bcmp` 比较。若长度设为 0,`bcmp` 始终返回成功,从而实现无密码绕过;若长度过大则会导致内核堆越界读取。该代码最早于 1999 年从 FreeBSD 引入,直到 2026 年 6 月才被修复。攻击者可利用此漏洞在无需凭据的情况下完成 PPPoE 握手,进而通过伪造服务端劫持网络流量。
荐读理由
这是技术安全漏洞分析文章,属于反价值第2条产品发布稿/技术软文。无任何能满足寻求结果1-5的实质信息,纯属读者大概率已知的常识,无需深入。
原文
OpenBSD's sppp(4) subsystem handles synchronous PPP links, the backbone of PPPoE connectivity. When a peer connects, the PPP handshake can require PAP (Password Authentication Protocol) credentials before the link reaches STATE_OPENED. The check that decides whether to accept or reject those credentials has been broken since it was first imported into the OpenBSD source tree in July 1999.
This is a story about a one-line bug that lived for 27 years.
The bug
The PAP credential check lives in sppp_pap_input() in sys/net/if_spppsubr.c. When the OpenBSD system acts as a PAP authenticator, it compares the peer-supplied name and password against configured values using bcmp:
if (name_len > AUTHMAXLEN ||
passwd_len > AUTHMAXLEN ||
bcmp(name, sp->hisauth.name, name_len) != 0 ||
bcmp(passwd, sp->hisauth.secret, passwd_len) != 0) {
/* authentication failed */
The problem is that name_len and passwd_len come directly from the incoming PAP frame. They are attacker-controlled. And bcmp(buf, ref, 0) always returns 0, regardless of what buf and ref contain.
The > AUTHMAXLEN guard is an upper-bound check. It rejects values above 255 but happily allows zero through. So when an attacker sends a PAP Auth-Request with name_len=0 and passwd_len=0, both bcmp calls return 0, the fail-branch is never taken, and OpenBSD sends a PAP_ACK. Authentication is complete. No credentials were needed.
A secondary issue shares the same root cause. Supplying a name_len larger than the actual allocation of sp->hisauth.name causes bcmp to read past the heap object, producing a kernel heap over-read. The configured credential is dynamically allocated as malloc(strlen(configured_string) + 1), so an 8-byte credential paired with name_len=200 reads 192 bytes of adjacent kernel heap.
27 years of history
The bcmp comparison pattern was part of the original sppp code imported on July 1, 1999 in a commit described as "lmc driver; ported by [email protected]". The code originated from FreeBSD, which itself derived it from Cronyx Engineering Ltd.'s implementation written by Serge Vakulenko in 1994-1996.
The zero-length bypass worked against every version since that original import. The guard at the time used > AUTHNAMELEN (64) and > AUTHKEYLEN (16), but zero is less than both, so it passed through unchecked.
In February 2009, a commit titled "Allow username and password to be up to 255 characters in length" changed the auth fields from fixed-size struct arrays (name[64], secret[16]) to dynamically allocated malloc(strlen()+1) and replaced the two separate bounds checks with a single > AUTHMAXLEN (256). This made the heap over-read possible, since the allocation size was now decoupled from the comparison bound.
The CHAP handler in the same file already had the correct pattern, using an exact-length pre-check:
if (name_len != strlen(sp->hisauth.name)
|| bcmp(name, sp->hisauth.name, name_len) != 0) {
The PAP handler never got the same treatment. For 27 years.
Reachability and impact
Both bugs are reachable via the PPPoE data path: pppoe_data_input -> pppoeintr -> sppp_input -> sppp_pap_input. The attacker does not need to know any credentials.
The attack completes the full PPPoE handshake: discovery, LCP negotiation, PAP authentication with zero-length fields, IPCP negotiation, and finally ICMP echo through the established link. The attacker's rogue PPPoE server carries the victim's IP traffic. A rogue server in the same broadcast domain exploits the bypass to impersonate a legitimate server, and OpenBSD routes traffic through the attacker's endpoint.
We verified this against OpenBSD 7.6 (amd64) in QEMU/KVM. The proof of concept acts as a PPPoE server, completes the handshake, and sends the empty PAP Auth-Request.
The fix
The fix mirrors the exact-length pre-check already used by the CHAP handler:
if (name_len != strlen(sp->hisauth.name) ||
passwd_len != strlen(sp->hisauth.secret) ||
bcmp(name, sp->hisauth.name, name_len) != 0 ||
bcmp(passwd, sp->hisauth.secret, passwd_len) != 0) {
This simultaneously prevents the zero-length bypass and bounds the bcmp length to the exact stored credential size, eliminating the over-read. The fix was committed by mvs on June 14, 2026.
- Fix commit: openbsd/src@076e2b1
Proof of concept
The PoC script (poc-001-pap-bypass.py) acts as a PPPoE server. It completes the PPPoE discovery and LCP negotiation, then sends a PAP Auth-Request with name_len=0, passwd_len=0. OpenBSD responds with PAP_ACK, and the link reaches full network-layer operation:
PAP_ACK received with empty credentials
VM accepted name_len=0, passwd_len=0 as valid auth.
IPCP Config-Ack received — link is UP (us=10.0.0.2 peer=10.0.0.1)
ICMP echo reply from 10.0.0.1
FULL LINK ESTABLISHED
Timeline
1999-07-01 — sppp code with vulnerable
bcmpcomparison imported into OpenBSD from FreeBSD2009-02-16 — Auth fields changed to dynamic allocation with
AUTHMAXLEN, enabling heap over-read2026-06-12 — Reported to OpenBSD with proof of concept
2026-06-14 — Fix committed by mvs
这条对你有帮助吗?