Add rogue IPv6 RA + RDNSS DNS-takeover coercion (CVE-2026-20929) - #21725
Add rogue IPv6 RA + RDNSS DNS-takeover coercion (CVE-2026-20929)#21725Pushpenderrathore wants to merge 11 commits into
Conversation
|
The one red check here (Verify / Ruby 3.4) is the framework-wide PacketFu bug tracked in #21721, not this change. PacketFu 2.0.0's |
21bba62 to
51d212c
Compare
There was a problem hiding this comment.
Pull request overview
This PR (stacked on #21724) adds native IPv6 DNS-takeover coercion primitives for CVE-2026-20929 by introducing a rogue Router Advertisement (RDNSS/DNSSL) module, a rogue DHCPv6 module, and the supporting DHCPv6 + IPv6 RA packet-construction library code so Metasploit can coerce clients onto an attacker-controlled DNS server without external tooling.
Changes:
- Adds
auxiliary/spoof/ipv6/ipv6_ra_dns_takeoverto coerce RFC 8106 clients via rogue RAs (RDNSS) and optionally reply immediately to Router Solicitations. - Adds native DHCPv6 parsing/server code (
Rex::Proto::DHCPv6) andauxiliary/spoof/dhcp/dhcpv6_dns_takeoverfor mitm6-style coercion. - Extracts shared selective DNS poisoning/forwarding behavior into
Msf::Exploit::Remote::DNS::NamePoisonerand expands the IPv6 mixin with RA/RDNSS/DNSSL builders, with RSpec coverage and module docs.
Impact Analysis:
- Blast radius: medium — introduces new auxiliary modules plus new library/mixin APIs (
Msf::Exploit::Remote::Ipv6,Msf::Exploit::Remote::DNS::NamePoisoner,Rex::Proto::DHCPv6) that can be reused by future modules. - Data and contract effects: adds new public methods/constants and a new autoload inflection (
dhcpv6 -> DHCPv6); no schema/storage changes. - Rollback and test focus: rollback is straightforward (new files + small mixin/autoload additions); focus testing on RA packet validity (RDNSS/DNSSL encoding), DNS poisoning behavior (scope + forwarding), and DHCPv6 server multicast receive/reply behavior.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/auxiliary/spoof/ipv6/ipv6_ra_dns_takeover.rb | New rogue RA (RDNSS/DNSSL) coercion module with optional RS handling. |
| modules/auxiliary/spoof/dhcp/dhcpv6_dns_takeover.rb | New rogue DHCPv6 DNS-takeover module using shared NamePoisoner behavior. |
| lib/msf/core/exploit/remote/ipv6.rb | Adds RDNSS/DNSSL builders and RA packet construction + RS helpers. |
| lib/msf/core/exploit/remote/dns/name_poisoner.rb | New shared selective DNS poisoning + forwarding mixin for coercion modules. |
| lib/rex/proto/dhcpv6/constants.rb | DHCPv6 constants for the new Rex DHCPv6 implementation. |
| lib/rex/proto/dhcpv6/packet.rb | DHCPv6 message/option encoding + response builder helpers. |
| lib/rex/proto/dhcpv6/server.rb | Minimal rogue DHCPv6 server with multicast join and request/response loop. |
| lib/msf_autoload.rb | Adds Zeitwerk inflection for DHCPv6. |
| spec/lib/msf/core/exploit/remote/ipv6_spec.rb | Unit tests for RA/RDNSS/DNSSL packet construction and RS logic. |
| spec/lib/rex/proto/dhcpv6/packet_spec.rb | Unit tests for DHCPv6 wire encoding and response construction. |
| spec/lib/rex/proto/dhcpv6/server_spec.rb | Unit tests for DHCPv6 server request handling behavior. |
| spec/modules/auxiliary/spoof/ipv6/ipv6_ra_dns_takeover_spec.rb | Module-level specs for RA takeover behavior and DNS poisoning integration. |
| spec/modules/auxiliary/spoof/dhcp/dhcpv6_dns_takeover_spec.rb | Module-level specs for DHCPv6 takeover DNS poisoning integration. |
| documentation/modules/auxiliary/spoof/ipv6/ipv6_ra_dns_takeover.md | New module documentation for RA/RDNSS coercion workflow. |
| documentation/modules/auxiliary/spoof/dhcp/dhcpv6_dns_takeover.md | New module documentation for DHCPv6 coercion workflow. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
name_poisoner.rb built the logged peer as a bare "#{host}:#{port}"
string, which is ambiguous for IPv6 clients (colon-separated) and
goes against the framework convention of using
Rex::Socket.to_authority, which brackets IPv6 hosts.
Copilot flagged this on rapid7#21725, which stacks on this branch and
shares this file; porting the same fix here so both branches carry
it independently of the eventual rebase.
Live-lab finding: with defaults, the IPv6 coercion here never reaches the relayTesting the full coerce-to-relay-to-cert chain against a live domain, I isolated why the same setup succeeds with one coercion name and silently fails with another. The cause is a bind-family mismatch between this coercion half and the relay half (#21709), not the resolver. The relay listens IPv4-only. This coercion hands the victim an AAAA. when 'AAAA'
[Dnsruby::RR.create(name: "#{name}.", type: 'AAAA', address: datastore['SPOOF_IP6'])]
else
# Only answer A records if an IPv4 spoof address is meaningful; otherwise
# returning nothing lets the client prefer the AAAA answer we control.
Rex::Socket.is_ipv4?(srvhost) ? [Dnsruby::RR.create(name: "#{name}.", type: 'A', address: srvhost)] : []So the victim connects to Reproduced deterministically with a listener bound exactly like the relay (
Row 3 is the tell: that name's A record points at the same IPv4 listener that accepts row 1, yet it fails, because Windows prefers the AAAA and does not fall back to the working A record. Row 4 is the fix. Why one name worked and another did not: a hand-made A-only record (for example Fix (committed to the relay stack, #21709 as |
Provide the native coercion half of the Kerberos relay via DNS (CVE-2026-20929), removing the dependency on external tooling like mitm6: - Add Rex::Proto::DHCPv6, a native DHCPv6 packet library (RFC 8415 plus the RFC 3646 DNS options): message/option encoding and helpers to build the rogue server responses that hand a client the attacker as its DNS server. - Add Rex::Proto::DHCPv6::Server, a reusable rogue DHCPv6 server that answers Solicit/Request/Renew/Rebind/Confirm/Information-Request. - Add auxiliary/spoof/dhcp/dhcpv6_dns_takeover, which runs the rogue DHCPv6 server and a paired DNS server that poisons names under a target domain (A/AAAA or a CNAME for the DNS-CNAME relay trick) while forwarding all other lookups so the victim stays functional.
Two defects found while validating the rogue DHCPv6 server against a live client on the lab network: - The multicast group join packed the interface index in network byte order, so on little-endian hosts the kernel joined on interface 0 and the server never received the multicast Solicit. Pack it in native byte order to match struct ipv6_mreq. - The client address was read assuming recvfrom always returns an address array, but older rex-socket versions return the host string directly, so the reply went to a malformed destination. Handle both shapes. With these fixes a cross-host Solicit is answered with an Advertise addressed correctly to the client on udp/546, handing out the attacker as the DNS server.
Move the selective DNS poisoning behaviour out of the DHCPv6 DNS takeover module into a reusable DNS::NamePoisoner mixin (TARGET_DOMAIN, TARGET_HOSTS, SPOOF_IP6, RELAY_CNAME plus the on_dispatch_request poison/forward logic and the IPv6 validation helper), so the upcoming Router Advertisement takeover module can share the exact same coercion behaviour instead of duplicating it. The DHCPv6 module now just includes the mixin and keeps its DHCPv6-specific options and server wiring. No behaviour change; its spec is unchanged and green.
Add the Router Advertisement equivalent of the mitm6 DHCPv6 DNS takeover, a second native coercion primitive for the Kerberos relay via DNS technique (CVE-2026-20929) with no external tooling dependency. The Ipv6 mixin gains RFC 8106 builders: ipv6_build_rdnss_option (Recursive DNS Server option), ipv6_build_dnssl_search_option (DNS search list), and ipv6_build_ra_dns_packet, which assembles an ICMPv6 RA advertising the attacker as the IPv6 resolver. Router lifetime defaults to 0 so routing is untouched and only DNS is taken over; the existing DNSSL command-injection builder is left intact. The new auxiliary/spoof/ipv6/ipv6_ra_dns_takeover module multicasts these RAs on an interval while the shared DNS::NamePoisoner server poisons names under the target domain and forwards everything else, keeping the victim functional. Adds 20 specs for the RDNSS/DNSSL/RA builders and the module wiring; loads clean in msfconsole, rubocop and msftidy clean.
Add a Router Solicitation responder so a client is coerced the moment it boots or refreshes instead of waiting for the next unsolicited advertisement. The Ipv6 mixin gains ipv6_router_solicitation? (ICMPv6 type 133 detection, reading PacketFu's icmpv6_type) and ipv6_solicited_ra_target, which applies RFC 4861 section 6.2.6: unicast the reply to the solicitor, or multicast to all-nodes when the solicitation source is the unspecified address. ipv6_build_ra_dns_packet now takes optional dst_mac/dst_addr so the same builder produces both the multicast unsolicited RA and a unicast solicited reply. The module now runs a single capture thread that multicasts the unsolicited RA on the interval while filtering for RS (BPF icmp6 and ip6[40] == 133) and replying immediately, keeping all pcap access on one handle. A RESPOND_TO_SOLICITS option (default true) gates the responder. Adds 9 specs (RS detection, solicited-target selection, unicast vs multicast reply, nil/non-solicitation handling); rubocop and msftidy clean.
The rogue DHCPv6 and Router Advertisement modules steer the victim to query the attacker's IPv6 address (SPOOF_IP6), but the paired DNS server inherited SocketServer's 0.0.0.0 SRVHOST default. That binds IPv4 only, so the IPv6 queries the victim was told to send never reached the server and no name was ever poisoned. It also made the A-record branch in NamePoisoner#poison_answers_for hand out 0.0.0.0 for in-scope names. Override the default to :: in the shared NamePoisoner mixin so both modules bind all IPv6 addresses out of the box. In-scope A queries now carry no bogus answer and are forwarded, leaving the victim functional while the AAAA answer we control wins.
- name_poisoner.rb built the logged peer as a bare "#{host}:#{port}"
string, which is ambiguous for IPv6 clients (colon-separated) and
goes against the framework convention of using
Rex::Socket.to_authority, which brackets IPv6 hosts.
- ipv6_build_rdnss_option accepted any string IPAddr can parse,
including IPv4 addresses. IPAddr#hton happily returns 4 bytes for
an IPv4 address, but the option's length field assumes every
address is a 16-byte IPv6 address, so an IPv4 entry would produce
a length-inconsistent, malformed RDNSS option instead of a clear
error. This is shared library code with no caller-side guard of
its own, so validate the address family here.
- RA_INTERVAL had no lower bound; 0 or a negative value makes
ra_service_loop's own interval check always true, injecting RAs
on almost every loop iteration instead of respecting the option.
Copilot's suggested fix placed the check inside the loop, but that
runs on a spawned background thread where fail_with would not
propagate to the console the normal way; validated it synchronously
in run instead, alongside the existing SPOOF_IP6 check, matching
how this module already validates the MAC address and pcap open
before spawning the thread.
Added spec coverage for all three.
…inistically The mixin builds PacketFu::IPv6Packet objects but never required packetfu itself; it was only pulled in lazily by Msf::Exploit::Capture#initialize. Under the CI suite the classes could load partially, leaving a StructFu dependency nil and raising "undefined method '[]' for nil" inside PacketFu::IPv6Packet.new. Requiring packetfu in the mixin, and in the two specs that construct packets directly, makes the load order deterministic.
packetfu 2.0.0 (its latest release) recovers the setter name inside StructFu#typecast by matching caller[0] against the MRI <= 3.3 backtrace format (`method='). Ruby 3.4 reformatted backtraces to 'Klass#method=', so the pattern returns nil and every PacketFu field assignment, including the ones inside PacketFu::IPv6Packet.new itself, raises "undefined method '[]' for nil". This broke the IPv6 RA specs only on the 3.4 CI job. Reopen StructFu#typecast in the IPv6 mixin with a pattern that captures the trailing setter name on both the old and new formats. Verified against Ruby 3.4.9 and 3.3.8.
f3d7743 to
1302e65
Compare
name_poisoner.rb built the logged peer as a bare "#{host}:#{port}"
string, which is ambiguous for IPv6 clients (colon-separated) and
goes against the framework convention of using
Rex::Socket.to_authority, which brackets IPv6 hosts.
Copilot flagged this on rapid7#21725, which stacks on this branch and
shares this file; porting the same fix here so both branches carry
it independently of the eventual rebase.
|
Update after rebasing onto master: this branch now carries the Ruby 3.4 PacketFu fix directly, so all Verify Ruby 3.4 legs are green. The two remaining red checks are the |
Part of #21693.
Third of a stacked series adding native Kerberos authentication relay to Metasploit. #21709 contributes the relay stack and the ESC8 target, #21724 contributes the DHCPv6 coercion half, and this PR adds the Router Advertisement coercion half.
Stacked on #21724. This branch contains that PR's commits, so the diff here shows both until #21724 lands. The RA specific work is the top commits. Review #21724 first.
Description
auxiliary/spoof/ipv6/ipv6_ra_dns_takeoverruns a rogue IPv6 router that advertises the attacker as the recursive DNS server via the RDNSS option (RFC 8106) inside Router Advertisements, paired with the same DNS poisoner used by the DHCPv6 module.It is the Router Advertisement equivalent of the mitm6 style DHCPv6 takeover. Rather than answering DHCPv6 Solicits it multicasts RAs carrying an RDNSS option, which modern Windows and other RFC 8106 clients adopt as their IPv6 resolver. Two behaviours worth calling out:
Having both coercion paths matters because they fail in different places. DHCPv6 needs the client to actually run a DHCPv6 client, and RDNSS needs the client to honour RFC 8106. Between them the coverage across Windows versions and configurations is much better than either alone.
Related Issue: #21693
Breaking Changes
None.
Reviewer Notes
Shared code with #21724:
Msf::Exploit::Remote::DNS::NamePoisonerholds the poison-under-a-domain plus forward-everything-else behaviour, and both modules use it. It was extracted in #21724 rather than duplicated here.lib/msf/core/exploit/remote/ipv6.rbgains the RA and RDNSS construction used by this module.Unit coverage is green, and the end to end coerce to relay to certificate chain has been validated in #21709 against a live domain. Correcting an earlier note: that validation does not need a two host lab with the CA separate from the KDC. The coercion introduces a new name rather than poisoning an existing one, so the victim keeps reaching the KDC and a single host is sufficient. This module has now also been validated against a live RFC 8106 Windows client: it adopted the rogue RDNSS advertisement (confirmed with
netsh interface ipv6 show dnsservers), the module answered a triggered Router Solicitation with an immediate unicast RA, and the victim's default route was unchanged (router lifetime 0, so DNS is taken over while routing is left alone).Verification Steps
bundle exec rspec spec/lib/rex/proto/dhcpv6 spec/lib/msf/core/exploit/remote/ipv6_spec.rb spec/modules/auxiliary/spoofand confirm no failures.use auxiliary/spoof/ipv6/ipv6_ra_dns_takeover, setDOMAINto a lab domain,run. Confirm the DNS server reports it is poisoning names under that domain.netsh interface ipv6 show dnsserverson Windows).Test Evidence
Environment
Pre-Submission Checklist
documentation/modules(new modules only)lib/changes)