
Automated defect verification tool for 6 dnsmasq CVEs (CVE-2026-2291, 4890, 4891, 4892, 4893, 5172)
Automated black-box tool for verifying 6 dnsmasq vulnerabilities (May 2026). Sends attack packets to a live DUT and reports PASS/FAIL — no source code access needed.
# Set DUT DNS to your laptop's WAN IP via GUI first, then:
sudo python3 dnsmasq_cve_verify.py --laptop <YOUR_WAN_IP> --dut <DUT_LAN_IP> --dut-pass <SSH_PASS>
# Example:
sudo python3 dnsmasq_cve_verify.py --laptop 10.0.0.211 --dut 192.168.1.1 --dut-pass '12345Asdf@'
| CVE | CVSS | Type | Attack Vector | Affected Feature |
|---|---|---|---|---|
| CVE-2026-2291 | 9.2 | Heap buffer overflow | Remote | extract_name() — always active |
| CVE-2026-5172 | 7.5 | OOB read / crash | Remote | extract_addresses() — always active |
| CVE-2026-4890 | 7.5 | Infinite loop DoS | Remote | NSEC bitmap parsing (--dnssec) |
| CVE-2026-4891 | 5.3 | Heap OOB read | Remote | RRSIG validation (--dnssec) |
| CVE-2026-4892 | 8.4 | Heap overflow → root | Local/Adjacent | DHCPv6 CLID (--dhcp-script + DHCPv6) |
| CVE-2026-4893 | 5.3 | Validation bypass | Remote | ECS source check (--add-subnet) |
Root cause: union bigname declares char name[MAXDNAME] but escaped characters can expand a name to 2*MAXDNAME+1 bytes, causing heap overflow.
Test method: Sends DNS queries containing domain names with high-bit characters (0x80+) that get \DDD escaped internally (4 bytes per input byte). If dnsmasq crashes or stops responding, it's vulnerable.
Patched behavior: Rejects oversized names gracefully (FORMERR/REFUSED) or uses enlarged buffer.
Root cause: Falsified rdlen field lets extract_name() advance pointer past record end. Remaining-bytes underflow produces a huge value → massive OOB read → crash.
Test method: Sends DNS responses with CNAME records where rdlen is smaller than the actual encoded name. If dnsmasq crashes, it's vulnerable.
Patched behavior: Validates that pointer stays within declared rdlen boundary after extract_name().
Root cause: NSEC type bitmap parsing advances by p[1] instead of p[1]+2 (missing window header size). With bitmap_length=0, pointer never advances → infinite loop.
Test method: Sends a crafted NSEC record with window=0, bitmap_length=0. If dnsmasq stops responding to ALL queries (hangs, not crashes), it's vulnerable. Exploitable BEFORE RRSIG validation.
Patched behavior: Advances by p[1]+2 and skips zero-length bitmaps.
Root cause: rdlen in RRSIG not validated against minimum size (18 + signer name). Calculated signature length underflows negative → treated as huge → OOB read.
Test method: Sends RRSIG records with rdlen=10 (way below minimum 31+ bytes). Crash = vulnerable.
Patched behavior: Validates rdlen >= fixed_fields + signer_name_length before computing signature length.
Root cause: DHCPv6 CLIDs (up to 65535 bytes) get hex-encoded via sprintf("%.2x") into daemon->packet (5131 bytes). 3000-byte CLID → 6000-byte hex string → overflow. Helper process runs as root.
Test method: Sends DHCPv6 SOLICIT with 3000-byte Client Identifier. Requires IPv6 adjacency and --dhcp-script configured. Helper crash = vulnerable.
Patched behavior: Truncates or validates CLID length before hex encoding.
Note: Some builds compile with -DNO_DHCP6 and are NOT affected by this CVE.
Root cause: process_reply() passes OPT record length (~23 bytes) instead of full packet length to check_source(). All bounds checks fail → function always returns 1 (valid).
Test method: Sends DNS queries with EDNS Client Subnet option containing spoofed source prefixes. If dnsmasq echoes ECS back without validation, it's vulnerable.
Patched behavior: Passes full packet length to check_source(), enabling proper bounds checks per RFC 7871 Section 9.2.
Upgrade to dnsmasq 2.92rel2 (recommended)
dnsmasq_cve_verify.py)The primary QA tool. Runs on the testing laptop, sends attack packets to the DUT, and reports clear PASS/FAIL for each CVE. No modification of the DUT is needed beyond read-only SSH access for state inspection.
┌─────────────────────────────────────────────────────────────────────┐
│ Testing Laptop │
│ │
│ LAN interface WAN interface │
│ <LAPTOP_LAN_IP> <LAPTOP_WAN_IP> │
│ │ │ │
│ │ ┌────┴──────────────┐ │
│ │ │ Malicious DNS │ │
│ │ │ Server (port 53) │ │
│ │ └────┬──────────────┘ │
│ │ │ │
└────────┼───────────────────────────────┼────────────────────────────┘
│ LAN subnet │ WAN subnet
│ │
┌────────┼───────────────────────────────┼────────────────────────────┐
│ │ │ │
│ LAN: <DUT_LAN_IP> WAN: <DUT_WAN_IP> │
│ (LAN gateway) (WAN uplink) │
│ │
│ DUT (Linksys Router) │
│ dnsmasq (any version < 2.92rel2) │
│ │
│ resolv-file=/etc/resolv.conf │
│ → nameserver <LAPTOP_WAN_IP> ← set via GUI, forwards to us │
│ │
└─────────────────────────────────────────────────────────────────────┘
Data flow:
1. Tool sends DNS query to DUT LAN IP (port 53)
2. DUT's dnsmasq can't resolve locally → forwards upstream to LAPTOP_WAN_IP
3. Our malicious server on WAN interface replies with exploit payload
4. DUT's dnsmasq processes the malicious response → crash/hang/survive
5. Tool checks DUT state via SSH (read-only)
Example setup (your IPs will differ):
| Role | IP (example) |
|---|---|
| Laptop LAN | 192.168.1.254 |
| Laptop WAN | 10.0.0.211 |
| DUT LAN | 192.168.1.1 |
| DUT WAN | 10.0.0.214 |
The key requirement: Laptop WAN IP and DUT WAN IP must be on the same subnet, so the DUT can reach the laptop as an upstream DNS server.
┌──────────┐ ┌───────────┐ ┌──────────────────┐ ┌──────────┐
│ SETUP │ ──► │ TRIGGER │ ──► │ STATE INSPECT │ ──► │ VERDICT │
│ │ │ │ │ │ │ │
│ Start │ │ Send DNS │ │ SSH to DUT: │ │ PASS: │
│ malicious│ │ query to │ │ - pidof dnsmasq │ │ survived │
│ DNS srv │ │ DUT→DUT │ │ - PID changed? │ │ │
│ on WAN │ │ forwards │ │ - dmesg crash? │ │ FAIL: │
│ interface│ │ to us→we │ │ - /var/log/msg │ │ crashed/ │
│ (10.0.0. │ │ reply w/ │ │ │ │ hung │
│ 211:53) │ │ exploit │ │ Liveness query │ │ │
│ │ │ payload │ │ (version.bind) │ │ │
└──────────┘ └───────────┘ └──────────────────┘ └──────────┘
NOTE: Tool does NOT modify DUT settings. User must set DNS to 10.0.0.211 via GUI.
paramiko installed (pip install paramiko)Connect the testing laptop to the DUT with two cables:
| Laptop Port | Connects To | Purpose |
|---|---|---|
| LAN port | DUT LAN port | SSH access + send DNS queries to DUT |
| WAN port | DUT WAN subnet (e.g., upstream switch/modem port) | Act as upstream DNS server |
After connecting, note your laptop's IPs:
# Find your IPs
ip addr show | grep "inet "
# Example output:
# inet 192.168.1.254/24 ... ← this is your LAN IP
# inet 10.0.0.211/24 ... ← this is your WAN IP (use this for --laptop)
http://192.168.1.1 or http://myrouter.local10.0.0.211)cd /path/to/dnsmasq-cve-2026/
# Run all 6 CVE tests:
sudo python3 dnsmasq_cve_verify.py --laptop <YOUR_WAN_IP> --dut <DUT_LAN_IP> --dut-pass <SSH_PASSWORD>
# Example:
sudo python3 dnsmasq_cve_verify.py --laptop 10.0.0.211 --dut 192.168.1.1 --dut-pass '12345Asdf@'
The tool will:
All Linksys routers tested are not practically exploitable for any of the 6 CVEs in their production build configurations. The dangerous features (DNSSEC, DHCPv6-via-dnsmasq) are either not compiled or not configured. Patches are still recommended as defense-in-depth.
Pre-fix (dnsmasq 2.78, no-DNSSEC build):
Pre-fix (dnsmasq 2.90 with DNSSEC enabled):
Post-fix (dnsmasq 2.92rel2 or backport patches applied): All 6 CVEs → PASS
paramiko (pip install paramiko)test_dnsmasq_cve_remote.py)Lightweight version check only — queries version.bind to determine if dnsmasq
version is below the fix. No SSH, no setup, no exploit payloads.
python3 test_dnsmasq_cve_remote.py 192.168.1.1
test_dnsmasq_cve_on_device.sh)Runs directly on the DUT via SSH/serial. Checks binary version and compile options.
scp test_dnsmasq_cve_on_device.sh [email protected]:/tmp/
ssh [email protected] "sh /tmp/test_dnsmasq_cve_on_device.sh"
malicious_dns_server.py)Standalone exploit server for manual testing. Run it, point DUT's upstream DNS at it,
then trigger queries to crash-5172.evil.test, crash-2291.evil.test, etc.
sudo python3 malicious_dns_server.py --port 53
# Then on DUT: configure upstream → this host
# Then trigger: dig @192.168.1.1 crash-5172.evil.test
After applying patches and flashing new firmware, re-run:
sudo python3 dnsmasq_cve_verify.py --laptop <YOUR_WAN_IP> --dut <DUT_LAN_IP> --dut-pass <SSH_PASS>
# Expected: all 6 PASS
| Problem | Fix |
|---|
| "DUT not forwarding queries to us" | Verify Step 2 was done correctly. Check laptop WAN IP matches what you entered in GUI. |
| "Cannot connect to DUT" | Verify SSH credentials. Try: ssh [email protected] manually. |
| "Cannot bind port 53" | Run with sudo. Or use --dns-port 5353 (requires manual DUT config). |
| "Version: unknown" | DUT may not have dnsmasq in standard path. Tool still tests correctly. |
| CVE | Result | Reason |
|---|
| CVE-2026-2291 | PASS | DNSSEC not compiled |
| CVE-2026-4890 | PASS | DNSSEC not compiled |
| CVE-2026-4891 | PASS | DNSSEC not compiled |
| CVE-2026-4892 | PASS | dnsmasq not serving DHCPv6 (separate DHCPv6 server used) |
| CVE-2026-4893 | PASS | Logic bug only — no crash |
| CVE-2026-5172 | PASS | Survived exploit (vulnerable code path not in 2.78) |
| CVE | Result | Reason |
|---|
| CVE-2026-2291 | PASS | DNSSEC not compiled |
| CVE-2026-4890 | PASS | DNSSEC not compiled |
| CVE-2026-4891 | PASS | DNSSEC not compiled |
| CVE-2026-4892 | PASS | DHCPv6 not compiled |
| CVE-2026-4893 | PASS | Logic bug only — no crash |
| CVE-2026-5172 | PASS | Survived exploit variants |
| CVE | Result | Reason |
|---|
| CVE-2026-2291 | PASS | DNSSEC not compiled — not exploitable |
| CVE-2026-4890 | PASS | DNSSEC not compiled — not exploitable |
| CVE-2026-4891 | PASS | DNSSEC not compiled — not exploitable |
| CVE-2026-4892 | PASS/FAIL | DHCPv6 compiled + dhcp-script active |
| CVE-2026-4893 | PASS | Logic bug — no crash (version-based only) |
| CVE-2026-5172 | PASS | blockdata_expand path not in 2.78 |
| CVE | Result | Reason |
|---|
| CVE-2026-2291 | FAIL | Heap overflow via escaped names |
| CVE-2026-4890 | FAIL | Infinite loop (hangs) |
| CVE-2026-4891 | FAIL | RRSIG OOB read crash |
| CVE-2026-4892 | PASS/FAIL | Depends on DHCPv6 + script config |
| CVE-2026-4893 | PASS | Logic bug — no crash |
| CVE-2026-5172 | FAIL | OOB read via falsified rdlen |
| Flag | Default | Description |
|---|
--laptop | (required) | Laptop's WAN IP (binds malicious DNS server here) |
--dut | 192.168.1.1 | DUT's LAN IP (SSH + DNS queries sent here) |
--dut-user | root | DUT SSH username |
--dut-pass | (prompted) | DUT SSH password |
--dns-port | 53 | Port for malicious DNS server |
--cve | all 6 | Specific CVE(s) to test (repeatable) |
| Tool | Python | Root | SSH | Network |
|---|
dnsmasq_cve_verify.py | 3.6+ paramiko | Yes (port 53) | Yes (read-only) | LAN + WAN to DUT |
test_dnsmasq_cve_remote.py | 3.6+ stdlib | No | No | UDP 53 to DUT |
test_dnsmasq_cve_on_device.sh | N/A (shell) | No | Run on DUT | N/A |
malicious_dns_server.py | 3.6+ stdlib | Yes (port 53) | No | DUT forwards to us |