IPv6 firewall fully controllable by unprivileged local users: IPV6_FW_ADD/DEL/FLUSH/ZERO/GET setsockopt on any IPv6 datagram socket has no privilege check
Summary
ip6_fw_ctl()'s only authorization is 'securelevel > 2' (securelevel is -1 by default), and its socket entry has no privilege gate: udp6's protosw wires pr_ctloutput so any process holding a plain socket(AF_INET6, SOCK_DGRAM) reaches ip6_ctloutput's IPV6_FW_ADD/DEL/FLUSH/ZERO and IPV6_FW_GET, which forward straight to ip6_fw_ctl without consulting the 'privileged' capability flag the function already computes and uses for five other option cases. Contrast the v4 ipfw path, reachable only through raw IP sockets gated by SYSCAP_NONET_RAW. VERIFIED on stock INVARIANTS guest: after kldload ip6fw.ko (default policy 'deny ipv6 from any to any'), uid 65534 added '00001 allow ipv6 from any to any' via a UDP6 socket (ret 0), /sbin/ip6fw show confirmed the nobody-installed rule live, ping6 ::1 flipped 100%-loss -> 0%-loss -> 100%-loss with nobody's add/del, and nobody FLUSHed/ZEROed the chain at will - complete firewall-policy bypass + unprivileged IPv6 blackout capability. PLATFORM FACT: ip6fw is dead on default config (no IPV6FIREWALL in X86_64_GENERIC; ip6_fw_ctl_ptr NULL) but ip6fw.ko + /sbin/ip6fw ship in the default install, so the surface activates the moment an admin enables IPv6 filtering. Fix validated by full in-guest nativekernel rebuild + reboot: nobody now receives EPERM on all five ops; root management unchanged.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3392 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ip6fw_ctl.c | β | 2.8 KB | view raw | |
| ip6_fw.h | β | 8.6 KB | view raw | |
| build.sh | β | 109 B | view raw | |
| run.sh | β | 163 B | view raw | |
| build.log | β | 64 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| run.2.log | β | 566 B | view raw | |
| fix.diff | β | 527 B | view raw | |
| fix.log | β | 732 B | view raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| verdict.json | β | 5.2 KB | view raw | |
| env.txt | β | 367 B | view raw |
DF-3392 VERDICT β REPRODUCED (unprivileged local firewall policy bypass)
What was run
Stock INVARIANTS guest DragonFly 6.5-DEVELOPMENT #0 (uname in env.txt).
PoC ip6fw_ctl.c built in-guest (cc -O, no special headers beyond the
kernel's own ip6_fw.h).
Reproduction chain (run.log, run.2.log β full untrimmed)
- Platform fact / default-dead: with the stock kernel,
net.inet6.ip6.fw.enabledoes not exist,kldstatshows no ip6fw, andIPV6_FW_GETas any user returnsEINVALβip6_fw_ctl_ptr == NULL(ip6_input.c:130-131 only set by ip6_fw_init(), which runs exclusively from the module/option path;X86_64_GENERIChas nooptions IPV6FIREWALL). - Admin opt-in: root runs
kldload ip6fw.ko(module shipped in /boot/kernel; userland/sbin/ip6fwshipped). Module loads with default rule 65535deny ipv6 from any to any(IPV6FIREWALL_DEFAULT_TO_ACCEPT not defined in sys/net/ip6fw/Makefile);net.inet6.ip6.fw.enable=1by default.ping6 -c 1 ::1β 100% loss (policy enforcing). - Unprivileged manipulation: uid 65534 (
nobody), plainsocket(AF_INET6, SOCK_DGRAM, 0): -IPV6_FW_ADDofallow ipv6 from any to any(#1) β ret 0 -/sbin/ip6fw show(root) confirms rule00001 allow ipv6 from any to anylive in the kernel chain above the deny rule -ping6 -c 1 ::1now β 0% loss: the admin's deny policy is bypassed by a rule an unprivileged user installed -IPV6_FW_DEL(#1) β ret 0; ping6 blocked again -IPV6_FW_ZERO,IPV6_FW_FLUSHβ ret 0 (counter wipe / full chain wipe by an unprivileged user = unpriv firewall DoS)
Root cause
ip6_ctloutput() computes privileged (ip6_output.c:1156-1158, caps-based,
used by at least five other option cases at :1198/:1424/:1429/:1712/:1720)
but the IPV6_FW_ADD/DEL/FLUSH/ZERO (:1534-1552) and IPV6_FW_GET
(:1760-1774) cases never consult it. They forward straight to
ip6_fw_ctl() whose only gate is securelevel > 2 (ip6_fw.c:1119 β
normally 0/-1). The option is reachable from ANY IPv6 socket: the udp6
protosw uses .pr_ctloutput = ip6_ctloutput_dispatch (in6_proto.c:142) β
contrast the v4 ipfw path which requires a raw IP socket, itself gated by
SYSCAP_NONET_RAW (raw_ip.c:473). No raw socket, no privilege, no jail
exception for the v6 path.
Impact
Once ip6fw is active (kldload / kernel option), every local user can: - bypass the admin's IPv6 security policy (insert accept-any rule #1), - destroy it (flush / delete arbitrary rules), - DoS all IPv6 connectivity (add deny rules; default is deny), - read accounting counters reset / rule-state changes. Not uid=0: this is a policy/privilege-boundary bypass, not memory corruption.
Fix validation (fix.diff)
if (!privileged) return EPERM; added to both IPV6_FW_ cases of
ip6_ctloutput. Kernel rebuilt in-guest (make nativekernel
KERNCONF=X86_64_GENERIC), installed, rebooted. Baseline (stock kernel):
nobody's ADD/FLUSH return 0 (run.log). Patched: nobody gets EPERM on all
IPV6_FW_ ops; root's ops still succeed (fix.log). No behavior change
otherwise.
Notes
IPV6_FW_GET (sockopt form) returns EINVAL both before and after module
load β a separate pre-existing plumbing defect in the GET path
(ip6_fw_ctl builds an mbuf chain; ip6_output.c:1771 sopt_from_mbuf on a
multi-mbuf chain), unrelated to the privilege issue and NOT required for
the bypass (write ops are the impact).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff applied to in-guest /usr/src, full nativekernel rebuild, reboot into #1: nobody now receives EPERM for IPV6_FW_ADD/DEL/FLUSH/ZERO/GET (fix.log + probe output); root management (ADD/DEL, ip6fw show, GET) unchanged; default-deny enforcement and module load/unload unchanged. Baseline (stock #0) reproduced the bypass as uid 65534.
findings/poc/DF-3392/fix.log; findings/poc/DF-3392/fix.diff; kbuild.log KERNEL_DONE marker; uname #1
Confirmed kernel references
Detail
Exploit chain
1) any local user: socket(AF_INET6, SOCK_DGRAM, 0) [in6_proto.c:142 -> ip6_ctloutput_dispatch]; 2) setsockopt(IPPROTO_IPV6, IPV6_FW_ADD, rule, 200) [ip6_output.c:1534, no priv gate] -> ip6_fw_ctl [ip6_fw.c:1119 securelevel-only gate] -> add_entry6 installs the rule; 3) '00001 allow ipv6 from any to any' shadows the admin's 65535 deny -> all IPv6 filtering bypassed; alternatively FLUSH/DEL/DENY rules -> unpriv IPv6 blackout.
Evidence (decisive lines)
["run.log: 'IPV6_FW_ADD: ACCEPT-any rule #1 added, uid=65534, ret=0' + ping6 ::1 toggling 100% loss -> 0% loss -> 100% loss with nobody's add/del", "run.2.log: /sbin/ip6fw show listing '00001 allow ipv6 from any to any' immediately after nobody's ADD (authoritative kernel-chain proof)", "fix.log: patched kernel #1 - nobody: 'Operation not permitted' on ADD/FLUSH/ZERO/DEL; root: 'IPV6_FW_ADD ... uid=0, ret=0' + ip6fw show works", "probe.c output (fix.log tail): nobody GET with valsize<=SOMAXOPT_SIZE -> EPERM (gate); the earlier GET EINVAL was sys_getsockopt's non-root valszmax cap (uipc_syscalls.c:1323-1330), not the protocol layer"]
PoC changes
seed had no runnable source; wrote ip6fw_ctl.c from struct ip6_fw in sys/net/ip6fw/ip6_fw.h. Fixed: added
Verified recommended fix
Add 'if (!privileged) return (EPERM);' to the IPV6_FW_{ADD,DEL,FLUSH,ZERO} and IPV6_FW_GET cases of ip6_ctloutput() (the variable is already computed at ip6_output.c:1156 and used by five other option cases), matching the raw-socket privilege boundary the v4 ipfw path gets via SYSCAP_NONET_RAW.
Verdict
REPRODUCED on stock INVARIANTS guest: after root loads the shipped ip6fw.ko (the only way to use DF's IPv6 firewall; option absent from X86_64_GENERIC so ip6_fw_ctl_ptr==NULL by default - verified dead path), uid 65534 on a plain socket(AF_INET6, SOCK_DGRAM) adds/deletes/flushes ip6fw rules via setsockopt(IPPROTO_IPV6, IPV6_FW_*) with no privilege check anywhere on the path (ip6_ctloutput computes 'privileged' but the IPV6_FW cases never consult it; ip6_fw_ctl's only gate is securelevel>2, and securelevel is -1 by default). Policy bypass proven behaviorally: default 'deny ipv6 from any to any' blocks ping6 ::1; nobody's IPV6_FW_ADD of 'allow from any to any' (#1) restores it (ip6fw show confirms the nobody-installed rule live in the kernel chain); nobody's DEL/FLUSH re-breaks/re-wipes the policy at will. Contrast the v4 ipfw path which requires a raw IP socket gated by SYSCAP_NONET_RAW. Impact is firewall policy bypass/manipulation + IPv6 DoS by unprivileged local users once ip6fw is enabled - NOT memory corruption and not uid=0. Fix (priv gate mirroring the file's own 'if (!privileged) return EPERM' pattern) validated by full in-guest kernel rebuild: patched kernel returns EPERM to uid 65534 for ADD/DEL/FLUSH/ZERO/GET while root operation is unchanged.
No comments yet.