A customer involved in a penetration test reached out to us recently about a suspicious email message that one of their employees received. The attachment consisted of a Microsoft Excel workbook (.xlsm) that contained a Visual Basic (VBA) macro—I know… we shuddered as well.
Just like any phishing campaign involving Microsoft Office files, the user had to be enticed into opening it. How did the sender entice the user, you ask? They used a classic phishing lure in the subject line: “Annual Employee Evaluation Report.” I mean, how could you not open it and see how you did last year?! Even we were intrigued, so we popped open the attachment to see how the evaluation went.
The contents of the spreadsheet were immediately disappointing. There were no results, no new salary, no bonus information (I was going to buy a boat!), no nothing.
Well, almost no nothing. The first things that stood out were:
- Microsoft’s bright yellow notification: “SECURITY WARNING Macros have been disabled”
- A bunch of red text in cells exclaiming “SECURE CELL: ENABLE MACROS TO RETRIEVE DATA”
Enable macros or not? that was the question… I really wanted that boat. What’s the worst that could happen? It’s not as if the macro was going to enumerate local system information or Active Directory data and ship it back to the red team within this workbook… right?
With the script running, it was the perfect time to poke around the workbook.
Interestingly, the “Secure Cells” now displayed an error message: “ERROR 8415E1337: TIMEOUT OCCURED RETRIEVING DATA” (yes, “occurred” was spelled wrong). Only one way to figure out what went wrong: look at the macro itself.
There’s more than one way to analyze a macro, but my favorite method involves OLE Tools. OLEVBA parses OLE and OpenXML and extracts the VBA macro code in clear text, which was specifically useful in this case because, fun fact: Microsoft Office documents are just specialized XML files.
Let’s see what comes out of the workbook from OLEVBA:
At first glance, we could see the initial subroutine:
Sub_AutoOpen(). It tells the macro to execute once the file is opened (or once macros are enabled). If an error occurs, it jumps to a function called
Oops—otherwise, it declares a bunch of variables.
The most interesting pieces in this first section are the following variables, along with some handy comments:
'Gather User Information
strUname = Environ$("username")
strCname = Environ$("computername")
strDomain = Environ$("userdomain")
strDnsDomain = Environ$("userdnsdomain")
strDomainDC = Environ$("logonServer")
strOS = Environ$("os")
strComputer = "."
'Populate target sheet
Sheets("HostInfo").Cells(2, 2).Value = strUname
Sheets("HostInfo").Cells(3, 2).Value = strCname
Sheets("HostInfo").Cells(4, 2).Value = strDomain
Sheets("HostInfo").Cells(5, 2).Value = strDnsDomain
Sheets("HostInfo").Cells(6, 2).Value = strDomainDC
Sheets("HostInfo").Cells(7, 2).Value = strOS
Six variables are set to values that are returned by the Environ function, which is a VBA function that returns the string associated with a specific operating system environment variable. Those values are then stored within a sheet called “HostInfo,” specifically cells starting in Row 2, Column 2.
Weirdly, none of this macro code seemed to have anything to do with an annual employee evaluation. To that point, when we first opened the workbook, we only saw one sheet called “Evaluation,” so where is this HostInfo sheet even located? There’s no call to create the sheet either, so something funny was going on here. Maybe we shouldn’t have executed this macro after all!
Let’s go back and take a look.
Are we sure there aren’t more sheets? We weren’t sure, and, when we unhid the sheets at the bottom, there were in fact 10 additional sheets hidden from view. Much to my surprise, none of them had anything to do with an employee evaluation:
Based on the sheet names, it appeared as if local system and Active Directory information was supposed to be stored here. Naturally, we then needed to figure out what specifically was going to be stored in these sheets and how was this macro going to enumerate that data from the host.
We can answer this by continuing our analysis. So let’s get back to it!
A little help from WMI
As we scrolled through the macro, the “Gather Running Process Information” section included some interesting Windows Management Instrumentation (WMI) queries, specifically ones that gathered Running Processes, Local Users, Local Admins, and Shares. But how did this all work? Let’s break one down.
First, for those unfamiliar with it, WMI is the implementation of a native functionality for managing data and other operations that go into running and maintaining Windows. It can be used to manage remote Windows systems across an environment.
In order to make useful WMI calls to query the necessary information from the system, a namespace and system must be specified—in this case, the local system and its default namespace. However, If a namespace is not specified, WMI will use the value specified in the Registry key located at
The first command set the objServices to a WMI object, returned by the GetObject function.
winmgmt is the WMI service within the
SVCHOST process running under the “LocalSystem” account.
strComputer is the name of the host, and the majority of the WMI classes for management are in the
root\cimv2 namespace. Next, using the specified WMI object, a WMI query is executed, which gathers the name and process ID of each process on the operating system, storing the results in the variable
Now it’s time to populate the sheet with the relevant data. The red team used a For loop to iterate through the list of processes and store the ProcessID and ProcessName in the “HostInfo” sheet. The variable
IntProcStartRow was set to 11, which specified the starting row number where the results will be stored. In this case, we started with Row 11, Column 1. From there, the row number was incremented by one until it had stored all the results from the query.
So far the macro had collected local system information and stored that data in the appropriate sheet. Not too complicated, since you can point to the local system. However, if I am a threat actor and I don’t know where my script is going to land, I need to figure out a way to enumerate Active Directory information with no child processes or suspect network connections. I don’t want to tip off any defenders, after all. You’re probably wondering the same thing I was: how do you query a domain without knowing anything about it?
The answer? RootDSE.
The initial variable was set by calling the
GetObject function against
LDAP://RootDSE. RootDSE is part of the Active Directory Service Interface (ADSI), which is a set of COM interfaces used to access the features of directory services from different network providers. The script was using RootDSE, which is a unique entry in a directory server that provides data about that server, including Lightweight Directory Access Protocol (LDAP) version and naming context, to identify the context in which it’s running. In other words, by retrieving the
DefaultNamingContext from RootDSE, you can bind to the current domain without explicitly knowing it!
After setting RootDSE,
strAttribUsers were set. These were strings that would be used in the query to return specific domain user objects and specific attributes of those objects.
Next, an ADODB connection is created. ActiveX Data Objects (ADO) allow connections to OLE DB data sources, in our case ADSI. The next part is to create the command using information previously defined.
Now it was time to execute the command and return the results. Once it was executed, the results were returned, iterated through, and stored in the “ADUsers” sheet within the workbook. Notice the
objRs.Fields attributes: they are the same attributes that are being filtered in the
This continued to loop through and store results until it had reached the end, then it closed the connection. The same process and setup was repeated to gather information on: