pfSense OpenVPN wizard CRLF leftover of SA-26_07
pfSense CE 2.9.0-DEVELOPMENT OpenVPN wizard copies Default Domain into dns_domain with no is_domain() check. openvpn_add_dhcpopts writes it into a server conf that already has script-security 3. OpenVPN ran injected up. Sibling of SA-26_07, which named only vpn_openvpn_server.php.
- Name
- pfSense OpenVPN wizard CRLF leftover of SA-26_07
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- Netgate
- Affected
- pfSense CE 2.9.0-DEVELOPMENT (9363ac5) openvpn_wizard.inc. Privilege page-pfsensewizardsubsystem. CCD up is ignored.
- Published
- 13 Oct 2026
- Updated
- 13 Oct 2026
- Tags
- 0-day, pfsense, openvpn, rce, cwe-74
SA-26_07 named the server page. The wizard still writes the same keys.
I am @abraxas_null. The proof of concept is on GitHub: abraxas/pfsense-openvpn-wizard-crlf (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py, gadget.php. Authorized lab only. It talks to loopback.
This is pfSense CE 2.9.0-DEVELOPMENT (9363ac5b8651a1c7a333180425ce7719070f95f9). No CVE yet. CWE-74 / CWE-78. 8.8 High (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). Remote authenticated RCE. Privilege Wizard Subsystem. Not OpenVPN advanced. Not Diagnostics: Command.
SA-26_07 (announced 2026-08-13, credits @lujiefsi): dns_domain and netbios_scope on vpn_openvpn_server.php accept CR/LF and quotes, which add OpenVPN directives including up / route-up. Affects CE <= 2.8.1. Corrected 2026-06-17 on CE master 2.9.0 commit fd301381 (not in this public tree). Redmine #16899.
The wizard copies the same keys from defaultdomain / nbtscope with no is_domain() check, then openvpn_resync runs the same openvpn_add_dhcpopts() helper into a server config.ovpn that already has script-security 3. IPsec mobile already uses is_domain() for the analogous field. If 2.9.0-RELEASE escaped the helper, this is the same SA. The SA named only the server page.
Why pfSense, and this pin
PHP-FPM is root. A limited GUI user can be granted a single page without Diagnostics: Command. Public GitHub has no RELENG_2_9_0. I hunted master 9363ac5 (2.9.0-DEVELOPMENT) and dropped leftovers that match a public SA even when the fix landed later. Skip Plus. Skip packages not in this tree.
The 2026 skip list is SA-26_01 through SA-26_22, including SA-26_06 proxy crontab, SA-26_18 widget LFI, SA-26_20 Kea JSON, and SA-26_22 / CVE-2026-92182 xmlrpc uid skip (still in this tree, public). Advisories: https://docs.netgate.com/advisories/index.html
Twenty hunts. CSC dns_domain uses the same helper into CCD files. OpenVPN 2.6 CCD accepts push / iroute / ifconfig-push / config / disable. up in a CCD file is not executed. I dropped CSC as High. The wizard writes a server conf.
How the wizard lands in config.ovpn
Wizard step 10:
if (isset($pconfig['step10']['defaultdomain']))
$server['dns_domain'] = $pconfig['step10']['defaultdomain'];
...
if (isset($pconfig['step10']['nbtscope'])) {
$server['netbios_scope'] = $pconfig['step10']['nbtscope'];
}Then config_set_path('openvpn/openvpn-server/', $server); openvpn_resync('server', $server);. Helper:
if (!empty($settings['dns_domain'])) {
$conf .= "push \"dhcp-option DOMAIN {$settings['dns_domain']}\"\n";
}
...
$conf .= "script-security 3\n";A quote plus LF closes the push "dhcp-option ..." line and adds server directives. Wizard step-10 checks DNS/NTP/WINS as IPs, not the domain or scope strings. page-openvpn-server-advanced already warns as root for custom_options. The wizard does not.
The lab
Pinned wizard assign plus openvpn_add_dhcpopts. OpenVPN 2.6.14 on tun0. up points at /oracle/write-witness.sh. Loopback 127.0.0.1:18820.
CONTROL: example.com is one DOMAIN push plus script-security 3, no up. TRIGGER: quote + LF adds up /oracle/write-witness.sh. OpenVPN ran that up (up-exec=yes). Same poison in a CCD blob was not executed (ccd-up-ignored=yes).
Last run:
SUCCESS PFSENSE-OPENVPN-WIZARD-CRLF wizard-assign=yes dhcpopts-up=yes script-security-3=yes control-clean=yes up-exec=yes PFSENSE-OPENVPN-WIZARD-CRLF-WITNESSOpenVPN log: /oracle/write-witness.sh tun0 1500 0 10.8.0.1 255.255.255.0 init then Initialization Sequence Completed. Stack tears down on EXIT.
What an attacker can do
| Method | Sink | Privilege |
|---|---|---|
Complete OpenVPN Remote Access wizard; set Default Domain (or NetBIOS scope) to a value that closes the push line and adds up |
openvpn_add_dhcpopts into server config.ovpn (openvpn.inc:697-738) with script-security 3 (:905) |
Integrity (lab-proven): OpenVPN 2.6.14 ran that up on tun0 and wrote the witness (up-exec=yes). Control example.com stayed a single DOMAIN push. |
| Same host commands | OpenVPN up as the daemon user (root on pfSense) |
Recon (inferred): host filesystem, config.xml, keys. |
| Broken OpenVPN config or host commands | OpenVPN server | Availability (inferred): OpenVPN / firewall disruption. |
Lab-proven vs inferred: wizard assign, dhcpopts up, script-security 3, control-clean, and tun0 up-exec are lab-proven. Filesystem recon is inferred from the up process. CCD up stayed ignored.
Requires: WebGUI session with page-pfsensewizardsubsystem. CSRF token. Does not require page-openvpn-server or page-openvpn-server-advanced.
Adjacent surface that stays closed
| Surface | Lab |
|---|---|
Control domain example.com |
one DOMAIN push, control-clean=yes |
CCD up (OpenVPN 2.6 subset) |
ccd-up-ignored=yes |
| Unauth / CSRF without token | closed |
OpenVPN custom_options |
intended advanced privilege |
| Diagnostics: Command | stays closed unless also granted |
The fix
is_domain() (or strip CR/LF/quotes) on wizard defaultdomain / nbtscope before they become dns_domain / netbios_scope, same as IPsec mobile already does. Sanitize inside openvpn_add_dhcpopts() so every caller is covered, including the wizard.
Replay:
cd lab
./run.shLoopback 127.0.0.1:18820. Witness PFSENSE-OPENVPN-WIZARD-CRLF-WITNESS.
References
- Pack: github.com/abraxas/pfsense-openvpn-wizard-crlf
- Product: github.com/pfsense/pfsense
9363ac5b8651a1c7a333180425ce7719070f95f9 openvpn_wizard.incopenvpn.incopenvpn_add_dhcpopts- Named page (not this leftover): SA-26_07 / Redmine #16899
- CWE-74 / CWE-78
Abraxas Labs
https://abraxaslabs.tech
https://github.com/abraxas
https://x.com/abraxas_null (@abraxas_null)
abraxas.null@proton.me