When I first started working with PowerShell, commands like this were confusing:
PS C:\temp> (Get-Date -Date ((Get-Date).ToUniversalTime()) -UFormat %s)
1657915725.84934
That's ... more parentheses than I expected.
PowerShell uses parentheses for a few different functions, well-documented online. In this article we'll look at the grouping operator ().
Many languages use parentheses to specify operator precedence in expressions:
PS C:\temp> (3 + 5) * 2
16
When PowerShell sees something inside (), it evaluates that first, then it evaluates the remainder of the expression. This is pretty intuitive, since we use it for lots of math equations too. The grouping operator works the same way: statements inside the parentheses are executed first (within the current statement in the pipeline).
Let's look at a practical example.
A Grouping Operator Example
I have two image files with similar file names. File size and date/time are a match too, but I want to know if the files are exactly the same. This is a job for Get-FileHash:
PS C:\temp> Get-ChildItem
Directory: C:\temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 7/15/2022 8:01 PM 453684 IMG_3306 copy.jpg
-a---- 7/15/2022 8:01 PM 453684 IMG_3306.jpg
PS C:\temp> Get-FileHash .\IMG_3306.jpg
Algorithm Hash Path
--------- ---- ----
SHA256 41F6DDF12CA703FCB428ED8FF702130613A905DE297FB5EBB577A06CC394C440 C:...
PS C:\temp> Get-FileHash '.\IMG_3306 copy.jpg'
Algorithm Hash Path
--------- ---- ----
SHA256 9A239588772945419F4D6BBC3DDBF870A60A30C028269B6FF5D20EF4E3A8D357 C:...
We see that the different SHA256 hashes indicates that the files are not the same, but what if we wanted to test this condition in a script? We could do something like this:
PS C:\temp> $hash1 = Get-FileHash .\IMG_3306.jpg
PS C:\temp> $hash2 = Get-FileHash '.\IMG_3306 copy.jpg'
PS C:\temp> $hash1.Hash -EQ $hash2.Hash
False
By declaring the Get-FileHash output as variables, we can access the Hash property for each file and test if they are equal using the PowerShell -Eq operator. The hash values are different, so we get the False result.
This approach is fine, and in a script it's perfectly legible, but it declared two variables, $hash1 and $hash2. This is not necessary, since we can use the grouping operator instead:
PS C:\temp> (Get-FileHash IMG_3306).Hash -EQ (Get-FileHash '.\IMG_3306 copy.jpg').Hash
False
In this example we surround the Get-FileHash steps in parentheses, and access the Hash property using dot notation. PowerShell processes the expressions from left to right, executing the statements within the parentheses first before accessing the Hash property and comparing the two values with -EQ.
Statements inside the parentheses are executed first (within the current statement in the pipeline).
Variable Declaration Unnecessary
Once you get the hang of using the grouping operator, you start to realize you don't need to declare variables to access the properties or methods of a PowerShell command. For example, all PowerShell arrays have a property called Count which is the number of elements in the array. You can access this value with any command that returns an array type:
PS C:\temp> Get-ChildItem
Directory: C:\temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 7/15/2022 8:01 PM 453684 IMG_3306 copy.jpg
-a---- 7/15/2022 8:01 PM 453684 IMG_3306.jpg
PS C:\temp> (Get-ChildItem).Count
2
In my part 4 article on working with the event log I used an example where I counted the number of event logs available on Windows:
PS C:\temp> (Get-WinEvent -ListLog *).Count
442
Using the grouping operator we can execute the specified command (Get-WinEvent -Listlog *), then access a member property using dot notation. Neat!
When we work with dates, we use a special object type for date and time data. In PowerShell, it's common to identify the object type using the grouping operator and the GetType() method:
PS C:\Temp> Get-Date
Saturday, July 16, 2022 10:42:03 AM
PS C:\Temp> (Get-Date).GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True DateTime System.ValueType
Here we see the type is DateType. We can use Get-Member to identify the properties and methods available for a DateType object:
PS C:\Temp> Get-Date | Get-Member
TypeName: System.DateTime
Name MemberType Definition
---- ---------- ----------
Add Method datetime Add(timespan value)
AddDays Method datetime AddDays(double value)
AddHours Method datetime AddHours(double value)
AddMilliseconds Method datetime AddMilliseconds(double value)
AddMinutes Method datetime AddMinutes(double value)
AddMonths Method datetime AddMonths(int months)
AddSeconds Method datetime AddSeconds(double value)
AddTicks Method datetime AddTicks(long value)
AddYears Method datetime AddYears(int value)
CompareTo Method int CompareTo(System.Object value), int CompareTo(dat...
Equals Method bool Equals(System.Object value), bool Equals(datetim...
GetDateTimeFormats Method string[] GetDateTimeFormats(), string[] GetDateTimeFo...
GetHashCode Method int GetHashCode()
...
Let's say you want to get a date/time for 2 days from now. You can use the grouping operator to invoke Get-Date, then call the AddHours() method:
PS C:\temp> (Get-Date).AddHours(48)
Monday, July 18, 2022 10:47:19 AM
Remember, statements inside the parentheses are executed first. Here, Get-Date is executed first, which returns a DateType object. The 48 inside of the next set of parentheses is also evaluated, but since it's a literal and not an expression nothing is executed. Then the result of calling Get-Date (the DateType object) is used to invoke the AddHours() method.
You could have accomplished this same task (getting the date/time 2 days from now) using variables instead:
PS C:\temp> $now = Get-Date
PS C:\temp> $twodaysfromnow = $now.AddHours(48)
PS C:\temp> $twodaysfromnow
Monday, July 18, 2022 10:51:48 AM
This is is also fine. PowerShell purists may point out that this solution declares an unnecessary variable, but it is a perfectly acceptable way of solving the problem at hand.
It's not always necessary to declare a variable, but it can improve legibility. Do what looks best for you.
Simpler Differential Analysis
In my article on threat hunting with PowerShell differential analysis I showed how you can build a baseline of configuration data for a system in a known-good state, then compare the current state of a system under investigation to identify a threat actor. In my SEC504 class we go a bit further in our analysis of how a Command & Control (C2) framework looks when it migrates into an existing process to hide its presence on the system, but we apply the same differential analysis process.
Let's take a look. First, on Windows I'll get a list of the DLLs associated with the Windows Explorer process, saving the output to explorer-modules-baseline.txt:
PS C:\Temp> Get-Process -name explorer -Module | Select-Object ModuleName | Out-File explorer-modules-baseline.txt
PS C:\Temp> Get-Content .\explorer-modules-baseline.txt -First 10
ModuleName
----------
Explorer.EXE
ntdll.dll
KERNEL32.DLL
KERNELBASE.dll
msvcp_win.dll
ucrtbase.dll
combase.dll
This file has a list of all the DLLs normally associated with the Explorer process. Next, as an attacker I'll use Metasploit Meterpreter as my C2 to migrate to the Explorer process, leaving the initial exploit process and blending in to an otherwise legitimate process:
meterpreter > migrate -N explorer.exe
[*] Migrating from 8268 to 4332...
[*] Migration completed successfully.
Now, I want to use differential analysis to identify if the normal DLL list associated with the Explorer process has changed using Compare-Object. I'll create a new file called explorer-modules-current.txt with the current DLL information:
PS C:\Temp> Get-Process -Name explorer -Module | Select-Object Modulename | Out-File explorer-modules-current.txt
PS C:\Temp>
To use Compare-Object, I need to supply -ReferenceObject and -DifferenceObject arguments. Compare-Object won't read the files directly, but I can use the grouping operator with Get-Content to read the files and return the object data:
PS C:\Temp> Compare-Object -ReferenceObject (Get-Content .\explorer-modules-baseline.txt) -DifferenceObject (Get-Content .\explorer-modules-current.txt)
InputObject SideIndicator
----------- -------------
PSAPI.DLL =>
cdprt.dll =>
Windows.Globalization.dll =>
execmodelclient.dll =>
execmodelproxy.dll =>
These 5 DLLs are added by Metasploit Meterpreter during the migrate operation, a valuable Indicator of Compromise (IoC).
Conclusion
In this article we looked at how the PowerShell grouping operator works. We can use the grouping operator instead of declaring a temporary variable to access an object's properties and methods (e.g., (Get-Date).AddHours(12)), and as a way to eliminate temporary variables (e.g., (Get-FileHash IMG_3306).Hash -EQ (Get-FileHash '.\IMG_3306 copy.jpg').Hash).
Remember: statements inside the parentheses are executed first (within the current statement in the pipeline). Next time you see a confusing PowerShell statement with lots of parentheses, just look to the inner-most parentheses first and work your way out to understand the order of execution. Once you grasp that process, complex PowerShell statements become a lot easier to break down.
-Joshua Wright
Return to Getting Started With PowerShell
Joshua Wright is the author of SANS SEC504: Hacker Tools, Techniques, and Incident Handling, a faculty fellow for the SANS Institute, and a senior technical director at Counter Hack.