The setup
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 sheet
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?
Macros enabled!
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.
The code
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:
- ADInfo
- ADUsers
- ADGroups
- ADComputers
- AV
- HostInfo
- LocalUsers
- LocalAdmins
- Shares
- Proxy
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 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WBEM\Scripting\Default Namespace
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 objProcessSet.
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, strFilterUsers
and 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 strAttribUsers
variable.
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:
AD Info | AD Groups | AD Computers |
---|---|---|
AD Info: Name | AD Groups: SAM Account Name | AD Computers : SAM Account Name |
After all the AD information was enumerated and stored within the appropriate sheets, the macro then updated the “Secure Cells” from earlier setting a string variable and then calling the sheet name and specific cell to be updated:
timeoutResp = "ERROR 8415E1337: TIMEOUT OCCURED RETRIEVING DATA" Sheets("Evaluation").Cells(8, 5).Value = timeoutResp
Now, you might be wondering “How does the attacker get all this data back?” Great question! Simple answer? The adversary saved it and sent it back via an HTTP POST request.
So while this may have looked crazy, it really wasn’t. First, a few variables were set: the full path including the file to the workbook, the file as byte data (which was obtained by calling the GetFileData
function), and then the mimetype of the file. Next, these values were passed into the PostFile
function and used to POST the data back to a previously declared URL. That’s it!
Don’t be an enabler
At this point, the biggest question our faithful readers probably have is: “How do I detect this?” In general, you might expect these WMI queries to spawn WMI as a childproc from Excel, but that is not the case here: the WMI calls are actually processed by wmiprvse.exe
, which allows WMI to interface with the system. That said, Excel does create a potentially suspicious modload, so it makes sense to look out for Microsoft Office applications loading wbemdisp.dll
—one of the related WMI COM components. While evidence of this modload is not inherently malicious, it can help an analyst to prioritize investigation of certain macro-enabled Office files that also exhibit WMI-related activity.
Now what about the HTTP POST back to the attackers? You could monitor network connections from Word and Excel, but that can get extremely noisy. Baselining and then filtering out normal connections is also possible, but what if the attacker uses a technique like Domain Fronting? In this case, that’s exactly what the red team did, and this technique could easily allow an adversary to walk this data right out of your network.
So what do you do now? We have a few recommendations:
- Block macros from running in Office files from the internet with GPO. Microsoft published a great blog post on how to implement this.
- Validate your email security gateway configuration. Do you normally deal with macro-embedded files such as .docm or .xlsm? If not, you may want to think about adding those and other macro-embedded files to your blocked attachment policy. Microsoft has a list of Office file formats, and you can use it to help determine what to add to your block policy.
- Educate everyone. While you should never expect your non-security co-workers or employees to be security experts, they can serve as a valuable detection signal when trained to identify and report suspicious behavior.