Class: Rex::Proto::DHCPv6::Server
- Inherits:
-
Object
- Object
- Rex::Proto::DHCPv6::Server
- 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
-
#assigned_address ⇒ Object
Returns the value of attribute assigned_address.
-
#context ⇒ Object
Returns the value of attribute context.
-
#dns_servers ⇒ Object
Returns the value of attribute dns_servers.
-
#domain_list ⇒ Object
Returns the value of attribute domain_list.
-
#interface ⇒ Object
Returns the value of attribute interface.
-
#listen_host ⇒ Object
Returns the value of attribute listen_host.
-
#preferred_lifetime ⇒ Object
Returns the value of attribute preferred_lifetime.
-
#reporter ⇒ Object
Returns the value of attribute reporter.
-
#server_duid ⇒ Object
Returns the value of attribute server_duid.
-
#sock ⇒ Object
Returns the value of attribute sock.
-
#thread ⇒ Object
Returns the value of attribute thread.
-
#valid_lifetime ⇒ Object
Returns the value of attribute valid_lifetime.
Instance Method Summary collapse
-
#handle_request(buf) ⇒ Array(Integer, String)?
Parse a raw client message and build the rogue response bytes.
-
#initialize(dns_servers: [], assigned_address: nil, domain_list: nil, server_mac: nil, listen_host: '::', interface: nil, context: {}) ⇒ Server
constructor
Lease lifetimes default to 300s/600s and can be overridden via the
preferred_lifetime/valid_lifetimeaccessors. - #interface_index ⇒ Object protected
- #ipv6_join_group_opt ⇒ Object protected
-
#join_multicast_group ⇒ Object
protected
Join the well-known DHCPv6 multicast group so the socket receives the multicast Solicit/Request messages clients send.
- #monitor_socket ⇒ Object protected
-
#on_request(&block) ⇒ Object
A block invoked with (message_type, client_host, request_packet) each time a request is answered, for logging / reporting.
- #random_mac ⇒ Object protected
-
#start ⇒ Object
Start listening and answering DHCPv6 requests.
- #stop ⇒ Object
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.
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.
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 |