β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3323

Shared-key 802.11 auth accepts cleartext challenge response: full authentication bypass without the WEP key

Summary

hostap_input() decrypts an AUTH frame only when the Protected bit is set and then CLEARS the bit, so hostap_auth_shared() processes the seq-3 challenge response with no knowledge of whether it arrived encrypted. It memcmp()s the challenge IE against ni->ni_challenge - the exact challenge the AP itself transmitted IN CLEARTEXT in its seq-2 frame (frame 2 is legitimately cleartext per 802.11-2016 12.3.3, frame 3 MUST be protected). An RF-adjacent attacker sends seq1, hears seq2, echoes the challenge back in a cleartext seq3 and ieee80211_node_authorize() fires: authenticated, then associated, with ZERO knowledge of the WEP key. VERIFIED on stock guest via the fw4 software-radio KLD: AUTHORIZED=1 + associd 0xc001 under wepmode on; corrupt-challenge control correctly refused; under wepmode mixed a cleartext TODS data frame is delivered to the AP stack (Ipkts 1->2). Fix validated (reject unprotected algo=SHARED seq=RESPONSE before the bit is stripped, >=30B length guard): bypass gone, is_rx_bad_auth++, open-auth flow unaffected. Remote unauthenticated RF-adjacent attacker defeats the only authentication of a shared-key WEP AP. Protocol bypass, not memory corruption - no uid0 route.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3323 Β· 14 files
FileTypeDescriptionSize
fw4.c β€” 16.5 KB view raw
Makefile β€” 144 B ↓ download
build.sh β€” 87 B view raw
run.sh β€” 111 B view raw
build.log β€” 5.6 KB view raw
run.log β€” 6.0 KB view raw
run.2.log β€” 1.6 KB view raw
fix.log β€” 1.7 KB view raw
fixcheck.sh β€” 1.6 KB view raw
fix.diff β€” 1.2 KB view raw
env.txt β€” 198 B view raw
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
verdict.json β€” 3.8 KB view raw

DF-3323 β€” shared-key auth: cleartext challenge response accepted (auth bypass)

File: sys/netproto/802_11/wlan/ieee80211_hostap.c Severity: Medium Β· CWE-287 Β· confidence: certain Β· REPRODUCED on the stock guest kernel

What

hostap_input() only decrypts an AUTH frame when the Protected bit is set, and clears the bit before dispatch (:851-880). hostap_auth_shared() (:1122-1150) processes a seq-3 shared-key response with no knowledge of whether the frame arrived encrypted, and simply memcmp()s the challenge IE against ni->ni_challenge β€” the exact challenge the AP itself transmitted in cleartext in its seq-2 response (ieee80211_output.c: add_auth() marks frames for encryption only when arg == IEEE80211_AUTH_SHARED_RESPONSE, a seq the AP never sends; frame 2 is legitimately cleartext per 802.11-2016 12.3.3). Frame 3 must be protected. The kernel accepts it unprotected β†’ ieee80211_node_authorize() (:1149).

An RF-adjacent attacker completes shared-key authentication with zero knowledge of the WEP key: send seq1, hear seq2, echo it back in a cleartext seq3. Then associate normally (attack=4).

How to reproduce

sh findings/poc/DF-3323/build.sh    # compiles the fw4 fake-radio KLD in the guest
sh findings/poc/DF-3323/run.sh      # baseline on stock kernel

Success criterion (see run.log / run.2.log):

fw4.attack=2: ... AUTHORIZED=1 ... <<<< BYPASSED
fw4.state[after-assoc]: ... AUTHORIZED=1 associd 0xc001

Control (fw4.attack=3, corrupted challenge): AP answers seq4 status=15 (STATUS_CHALLENGE) and the node stays AUTHORIZED=0 β€” the memcmp discipline works; only the encryption gate is missing.

Impact amplifier (wepmode mixed, i.e. DROPUNENC off β€” also the state of the natural authmode shared wepkey … config without an explicit wepmode on): ONE injected cleartext TODS data frame is delivered to the AP stack β€” netstat -I wlan0 Ipkts 1β†’2 (run.2.log).

Fix validation

fix.diff adds an else if in hostap_input()'s MGT path: an AUTH frame that arrives without the Protected bit and parses as algo=SHARED/seq=RESPONSE is discarded (is_rx_bad_auth++). Validated on a rebuilt kernel (make -j6 nativekernel, kernel #1 21:14 / re-checked on the doubly-patched #1 21:29): fw4.attack=2 no longer authorizes (AUTHORIZED=0, no seq-4 TX), while fw4.attack=1 still elicits the seq-2 challenge (legit cleartext frame 1/2 path unbroken). See fix.log.

VERDICT.md
↓ download raw

DF-3323 β€” VERDICT

Status: reproduced (baseline, stock INVARIANTS kernel #0 Thu Jul 2 06:02:54 UTC 2026) Β· impact: remote RF-adjacent authentication bypass (no key material required) β†’ authorized + associated station; cleartext data delivery when DROPUNENC is off Β· fix: validated (kernel rebuilt with fix.diff).

Root cause (path:line)

  • sys/netproto/802_11/wlan/ieee80211_hostap.c:851-880 β€” hostap_input() MGT path: decryption happens iff wh->i_fc[1] & IEEE80211_FC1_PROTECTED; after ieee80211_crypto_decap() the bit is cleared (:879), so downstream code cannot tell an encrypted frame from a cleartext one.
  • sys/netproto/802_11/wlan/ieee80211_hostap.c:1122-1150 β€” hostap_auth_shared() case IEEE80211_AUTH_SHARED_RESPONSE: only memcmp(ni->ni_challenge, &challenge[2], challenge[1]) (:1138); on match ieee80211_node_authorize(ni) (:1149). No Protected-bit requirement anywhere on the seq-3 path.
  • sys/netproto/802_11/wlan/ieee80211_output.c:2380-2417 β€” add_auth(): the AP's seq-2 CHALLENGE frame carries the 128-byte challenge IE and is not marked for encryption (only arg == IEEE80211_AUTH_SHARED_RESPONSE sets IEEE80211_BPF_CRYPTO, and the AP never sends seq 3). Frame 2 is legitimately clearttext (802.11-2016 12.3.3), frame 3 must be protected β€” the kernel never checks.

Attack chain (executed on the guest via the fw4 software radio)

  1. sysctl kern.fw4_attack=1 β€” inject AUTH algo=SHARED seq=1 (30 bytes, legitimately cleartext). AP (wlanmode ap, authmode shared wepkey … wepmode on) creates the node, allocates + randomizes the challenge, transmits seq-2 with the 128-byte challenge IE protected=0; the harness's raw_xmit sniffer captures it byte-for-byte ("off the air").
  2. Control fw4_attack=3 β€” respond seq-3 with one corrupted challenge byte: AP answers seq-4 status=15 (IEEE80211_STATUS_CHALLENGE), node stays AUTHORIZED=0. (Proves the exchange is otherwise enforced.)
  3. Attack fw4_attack=2 β€” respond seq-3 with the captured challenge, Protected bit CLEAR: AP answers seq-4 status=0 and the node reports AUTHORIZED=1 <<<< BYPASSED.
  4. fw4_attack=4 β€” ASSOC_REQ: associd 0xc001, list sta FLAG A (authorized+assoc) β€” under wepmode on, the strictest WEP config.
  5. Part B (wepmode mixed, DROPUNENC off β€” also the default state of authmode shared wepkey … without explicit wepmode on): steps 1-4 repeat, then ONE cleartext TODS data frame is injected (fw4_attack=5): netstat -I wlan0 Ipkts 1β†’2, Ibytes 20β†’42 β€” the frame traversed hostap_input's privacy path (F_PRIVACY set, Protected bit clear β†’ key = NULL at :711), was decapped, and delivered to if_input.

No WEP key was ever known or used by the attacker.

Why not higher severity

No memory corruption. Requires an RF-adjacent attacker and a WEP shared-key-configured AP (legacy, non-default config). Under the recommended wepmode on (DROPUNENC set) cleartext data is still dropped (non-PAE), but 802.11 authentication, association, AID/power-save state and the EAPOL (PAE) pass-through are gained without the key.

Fix validation (kernel #1, both fixes applied)

fix.diff (in hostap_input, before the Protected-bit is stripped): discard subtype==AUTH frames that arrive unprotected and parse as algo == IEEE80211_AUTH_ALG_SHARED && seq == IEEE80211_AUTH_SHARED_RESPONSE (802.11-2016 12.3.3 requires frame 3 to be protected). Requires m_pkthdr.len >= 30 && m_len >= 30 so it never reads a fragmented header (if shorter, hostap_recv_mgmt's own VERIFY_LENGTH rejects the frame anyway).

  • baseline: attack=2 β†’ AUTHORIZED=1 + AP TX seq4 status=0
  • patched : attack=2 β†’ frame dropped (is_rx_bad_auth++), NO AP TX, node AUTHORIZED=0; attack=1 still elicits the seq-2 challenge (legit cleartext frames 1/2 unaffected).
  • regression: open-authmode vap, open auth (attack=6) β†’ authorized β†’ assoc succeeds (see DF-3326 fixcheck in findings/poc/DF-3326/fix.log).

fix_baseline_reproduced=1, fix_patched_reproduced=0 (the bug behavior is GONE on the patched kernel) β†’ fix_status=fixed.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

make -j6 nativekernel rc=0 with fix.diff (+DF-3326 fix); baseline bypass (AUTHORIZED=1) is gone: cleartext seq3 dropped with is_rx_bad_auth++, no seq-4 TX, node stays AUTHORIZED=0; regression: seq-1 request still elicits the cleartext seq-2 challenge, open-auth flow associates normally.

findings/poc/DF-3323/fix.log + findings/poc/DF-3326/fix.log (shared kernel #1 21:29)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 10 21:29:25 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

1) inject AUTH shared seq1 (30B) -> AP creates node, sends seq2 challenge cleartext; 2) capture challenge off the air in raw_xmit; 3) inject AUTH shared seq3 carrying the captured challenge with Protected bit CLEAR -> AP replies seq4 status 0, port authorized; 4) inject ASSOC_REQ -> associd 0xc001; 5) (wepmode mixed / no wepmode) inject cleartext TODS data frames -> delivered to AP stack

Evidence (decisive lines)

run.log/run.2.log: 'fw4[air]: AP TX AUTH seq=2 status=0 challenge_ie_len=128 protected=0', 'fw4.state[current]: ... AUTHORIZED=1 ... <<<< BYPASSED', 'associd 0xc001', netstat Ipkts 1->2; fix.log: patched kernel drops the cleartext seq3 (AUTHORIZED=0, no seq4 TX) while attack=1 still elicits the challenge

PoC changes

seed none (new harness): fw4.c = fw3.c pattern (DF-3282/3312/3314) + raw_xmit sniffer capturing the AP's outgoing AUTH/challenge frames + sysctl-driven injectors (attack=1 request, 2 captured-challenge response, 3 corrupt-challenge control, 4 assoc, 5 cleartext data, 6 open-auth regression). DF ifconfig spells hostap mode 'wlanmode ap' not 'wlanmode hostap'.

Verified recommended fix

In hostap_input()'s MGT path, discard AUTH frames that arrive unprotected and parse as algo=SHARED seq=RESPONSE (802.11-2016 12.3.3); see fix.diff

Verdict

hostap_input() decrypts AUTH frames only when the Protected bit is set and then clears it (:851-880); hostap_auth_shared() processes the seq-3 challenge response with no encryption requirement and memcmp()s it against the challenge the AP itself sent in cleartext (:1138). Echoing the overheard challenge in a cleartext seq-3 frame authenticates the attacker without the WEP key (ieee80211_node_authorize :1149). Reproduced on the stock INVARIANTS kernel: AUTHORIZED=1 + associd 0xc001 under wepmode on; corrupt-challenge control correctly refused (status 15); one cleartext TODS data frame delivered under wepmode mixed (netstat Ipkts 1->2).