-
Notifications
You must be signed in to change notification settings - Fork 8
/
server.rb
342 lines (287 loc) · 9.82 KB
/
server.rb
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# -*- coding: binary -*-
require 'rex/socket'
require 'rex/proto/dhcp'
module Rex
module Proto
module DHCP
##
#
# DHCP Server class
# not completely configurable - written specifically for a PXE server
# - scriptjunkie
#
# extended to support testing/exploiting CVE-2011-0997
# - apconole@yahoo.com
##
class Server
include Rex::Socket
def initialize(hash, context = {})
self.listen_host = '0.0.0.0' # clients don't already have addresses. Needs to be 0.0.0.0
self.listen_port = 67 # mandatory (bootps)
self.context = context
self.sock = nil
self.myfilename = hash['FILENAME'] || ""
self.myfilename << ("\x00" * (128 - self.myfilename.length))
source = hash['SRVHOST'] || Rex::Socket.source_address
self.ipstring = Rex::Socket.addr_aton(source)
ipstart = hash['DHCPIPSTART']
if ipstart
self.start_ip = Rex::Socket.addr_atoi(ipstart)
else
# Use the first 3 octects of the server's IP to construct the
# default range of x.x.x.32-254
self.start_ip = "#{self.ipstring[0..2]}\x20".unpack("N").first
end
self.current_ip = start_ip
ipend = hash['DHCPIPEND']
if ipend
self.end_ip = Rex::Socket.addr_atoi(ipend)
else
# Use the first 3 octects of the server's IP to construct the
# default range of x.x.x.32-254
self.end_ip = "#{self.ipstring[0..2]}\xfe".unpack("N").first
end
# netmask
netmask = hash['NETMASK'] || "255.255.255.0"
self.netmaskn = Rex::Socket.addr_aton(netmask)
# router
router = hash['ROUTER'] || source
self.router = Rex::Socket.addr_aton(router)
# dns
dnsserv = hash['DNSSERVER'] || source
self.dnsserv = Rex::Socket.addr_aton(dnsserv)
# broadcast
if hash['BROADCAST']
self.broadcasta = Rex::Socket.addr_aton(hash['BROADCAST'])
else
self.broadcasta = Rex::Socket.addr_itoa( self.start_ip | (Rex::Socket.addr_ntoi(self.netmaskn) ^ 0xffffffff) )
end
self.served = {}
self.serveOnce = hash.include?('SERVEONCE')
self.servePXE = (hash.include?('PXE') or hash.include?('FILENAME') or hash.include?('PXEONLY'))
self.serveOnlyPXE = hash.include?('PXEONLY')
# Always assume we don't give out hostnames ...
self.give_hostname = false
self.served_over = 0
if (hash['HOSTNAME'])
self.give_hostname = true
self.served_hostname = hash['HOSTNAME']
if ( hash['HOSTSTART'] )
self.served_over = hash['HOSTSTART'].to_i
end
end
self.leasetime = 600
self.relayip = "\x00\x00\x00\x00" # relay ip - not currently suported
self.pxeconfigfile = "update2"
self.pxealtconfigfile = "update0"
self.pxepathprefix = ""
self.pxereboottime = 2000
self.domain_name = hash['DOMAINNAME'] || nil
self.url = hash['URL'] if hash.include?('URL')
end
def report(&block)
self.reporter = block
end
# Start the DHCP server
def start
self.sock = Rex::Socket::Udp.create(
'LocalHost' => listen_host,
'LocalPort' => listen_port,
'Context' => context
)
self.thread = Rex::ThreadFactory.spawn("DHCPServerMonitor", false) {
monitor_socket
}
end
# Stop the DHCP server
def stop
self.thread.kill
self.served = {}
self.sock.close rescue nil
end
# Set an option
def set_option(opts)
allowed_options = [
:serveOnce, :pxealtconfigfile, :servePXE, :relayip, :leasetime, :dnsserv,
:pxeconfigfile, :pxepathprefix, :pxereboottime, :router, :proxy_auto_discovery,
:give_hostname, :served_hostname, :served_over, :serveOnlyPXE, :domain_name, :url
]
opts.each_pair { |k,v|
next if not v
if allowed_options.include?(k)
self.instance_variable_set("@#{k}", v)
end
}
end
# Send a single packet to the specified host
def send_packet(ip, pkt)
port = 68 # bootpc
if ip
self.sock.sendto( pkt, ip, port )
else
if not self.sock.sendto( pkt, '255.255.255.255', port )
self.sock.sendto( pkt, self.broadcasta, port )
end
end
end
attr_accessor :listen_host, :listen_port, :context, :leasetime, :relayip, :router, :dnsserv
attr_accessor :domain_name, :proxy_auto_discovery
attr_accessor :sock, :thread, :myfilename, :ipstring, :served, :serveOnce
attr_accessor :current_ip, :start_ip, :end_ip, :broadcasta, :netmaskn
attr_accessor :servePXE, :pxeconfigfile, :pxealtconfigfile, :pxepathprefix, :pxereboottime, :serveOnlyPXE
attr_accessor :give_hostname, :served_hostname, :served_over, :reporter, :url
protected
# See if there is anything to do.. If so, dispatch it.
def monitor_socket
while true
rds = [@sock]
wds = []
eds = [@sock]
r,_,_ = ::IO.select(rds,wds,eds,1)
if (r != nil and r[0] == self.sock)
buf,host,port = self.sock.recvfrom(65535)
# Lame compatabilitiy :-/
from = [host, port]
dispatch_request(from, buf)
end
end
end
def dhcpoption(type, val = nil)
ret = ''
ret << [type].pack('C')
if val
ret << [val.length].pack('C') + val
end
ret
end
# Dispatch a packet that we received
def dispatch_request(from, buf)
type = buf.unpack('C').first
if (type != Request)
#dlog("Unknown DHCP request type: #{type}")
return
end
# parse out the members
_hwtype = buf[1,1]
hwlen = buf[2,1].unpack("C").first
_hops = buf[3,1]
_txid = buf[4..7]
_elapsed = buf[8..9]
_flags = buf[10..11]
clientip = buf[12..15]
_givenip = buf[16..19]
_nextip = buf[20..23]
_relayip = buf[24..27]
_clienthwaddr = buf[28..(27+hwlen)]
servhostname = buf[44..107]
_filename = buf[108..235]
magic = buf[236..239]
if (magic != DHCPMagic)
#dlog("Invalid DHCP request - bad magic.")
return
end
messageType = 0
pxeclient = false
# options parsing loop
spot = 240
while (spot < buf.length - 3)
optionType = buf[spot,1].unpack("C").first
break if optionType == 0xff
optionLen = buf[spot + 1,1].unpack("C").first
optionValue = buf[(spot + 2)..(spot + optionLen + 1)]
spot = spot + optionLen + 2
if optionType == 53
messageType = optionValue.unpack("C").first
elsif optionType == 150 or (optionType == 60 and optionValue.include? "PXEClient")
pxeclient = true
end
end
# don't serve if only serving PXE and not PXE request
return if pxeclient == false and self.serveOnlyPXE == true
# prepare response
pkt = [Response].pack('C')
pkt << buf[1..7] #hwtype, hwlen, hops, txid
pkt << "\x00\x00\x00\x00" #elapsed, flags
pkt << clientip
# if this is somebody we've seen before, use the saved IP
if self.served.include?( buf[28..43] )
pkt << Rex::Socket.addr_iton(self.served[buf[28..43]][0])
else # otherwise go to next ip address
self.current_ip += 1
if self.current_ip > self.end_ip
self.current_ip = self.start_ip
end
self.served.merge!( buf[28..43] => [ self.current_ip, messageType == DHCPRequest ] )
pkt << Rex::Socket.addr_iton(self.current_ip)
end
pkt << self.ipstring #next server ip
pkt << self.relayip
pkt << buf[28..43] #client hw address
pkt << servhostname
pkt << self.myfilename
pkt << magic
pkt << "\x35\x01" #Option
if messageType == DHCPDiscover #DHCP Discover - send DHCP Offer
pkt << [DHCPOffer].pack('C')
# check if already served an Ack based on hw addr (MAC address)
# if serveOnce & PXE, don't reply to another PXE request
# if serveOnce & ! PXE, don't reply to anything
if self.serveOnce == true and self.served.has_key?(buf[28..43]) and
self.served[buf[28..43]][1] and (pxeclient == false or self.servePXE == false)
return
end
elsif messageType == DHCPRequest #DHCP Request - send DHCP ACK
pkt << [DHCPAck].pack('C')
# now we ignore their discovers (but we'll respond to requests in case a packet was lost)
if ( self.served_over != 0 )
# NOTE: this is sufficient for low-traffic net
# for high-traffic, this will probably lead to
# hostname collision
self.served_over += 1
end
else
return # ignore unknown DHCP request
end
# Options!
pkt << dhcpoption(OpProxyAutodiscovery, self.proxy_auto_discovery) if self.proxy_auto_discovery
pkt << dhcpoption(OpDHCPServer, self.ipstring)
pkt << dhcpoption(OpLeaseTime, [self.leasetime].pack('N'))
pkt << dhcpoption(OpSubnetMask, self.netmaskn)
pkt << dhcpoption(OpRouter, self.router)
pkt << dhcpoption(OpDns, self.dnsserv)
pkt << dhcpoption(OpDomainName, self.domain_name)
if self.servePXE # PXE options
pkt << dhcpoption(OpPXEMagic, PXEMagic)
# We already got this one, serve localboot file
if self.serveOnce == true and self.served.has_key?(buf[28..43]) and
self.served[buf[28..43]][1] and pxeclient == true
pkt << dhcpoption(OpPXEConfigFile, self.pxealtconfigfile)
else
# We are handing out an IP and our PXE attack
if(self.reporter)
self.reporter.call(buf[28..43],self.ipstring)
end
pkt << dhcpoption(OpPXEConfigFile, self.pxeconfigfile)
end
pkt << dhcpoption(OpPXEPathPrefix, self.pxepathprefix)
pkt << dhcpoption(OpPXERebootTime, [self.pxereboottime].pack('N'))
if ( self.give_hostname == true )
send_hostname = self.served_hostname
if ( self.served_over != 0 )
# NOTE : see above comments for the 'uniqueness' of this value
send_hostname += self.served_over.to_s
end
pkt << dhcpoption(OpHostname, send_hostname)
end
end
pkt << dhcpoption(OpURL, self.url) if self.url
pkt << dhcpoption(OpEnd)
#pkt << ("\x00" * 32) #padding
# And now we mark as requested
self.served[buf[28..43]][1] = true if messageType == DHCPRequest
send_packet(nil, pkt)
end
end
end
end
end