Upgrading PowerUp With PSReflect

PowerUp is something that I haven’t written about much in nearly two years. It recently went through a long overdue overhaul in preparation for our “Advanced PowerShell for Offensive Operations” training class, and I wanted to document the recent changes and associated development challenges. Being one of the first PowerShell scripts I ever wrote, there was a LOT to clean up and correct (it’s come a long way since its initial commit back in 2014).

The new code is in the development branch of PowerSploit and I updated the PowerUp cheat sheet to reflect the new functions and syntax. Many of these updates were only possible with @mattifestation‘s awesome PSReflect library, something we’ll be covering heavily in our class. If you need to access the Win32 API or create structs/enums in PowerShell without touching disk or resorting to complicated reflection techniques, I highly recommend checking the project out.

Removed, Renamed, and Added Functions

First, some housekeeping. The following PowerUp functions were removed as they have working equivalents in PowerShell version 2.0+: Invoke-ServiceStart (Start-Service), Invoke-ServiceStop (Stop-Service -Force), Invoke-ServiceEnable (Set-Service -StartupType Manual), Invoke-ServiceDisable (Set-Service -StartupType Disabled).

The following functions were renamed:

Get-ModifiableFile was renamed to Get-ModifiablePath as it now handles folder paths instead of just file paths.

was renamed to as it now handles folder paths instead of just file paths. Get-ServiceFilePermission was renamed to Get-ModifiableServiceFile .

was renamed to . Get-ServicePermission was renamed to Get-ModifiableService .

was renamed to . Find-DLLHijack was renamed to Find-ProcessDLLHijack to clarify how exactly it should be used.

was renamed to to clarify how exactly it should be used. Find-PathHijack was renamed to Find-PathDLLHijack for clarification as well.

was renamed to for clarification as well. Get-RegAlwaysInstallElevated was renamed to Get-RegistryAlwaysInstallElevated .

was renamed to . Get-RegAutoLogon was renamed to Get-RegistryAutoLogon .

was renamed to . Get-VulnAutoRun was renamed to Get-ModifiableRegistryAutoRun for clarification.

Any ‘AbuseFunction’ fields returned by Invoke-AllChecks should return the new function names if applicable.

Get-SiteListPassword, our implementation of Jerome Nokin‘s mcafee-sitelist-pwd-decryption.py Python script was combined into PowerUp.ps1 and implemented in Invoke-AllChecks. Get-System is being kept as a separate file in the PowerSploit ./Privesc/ folder as it’s not really an escalation ‘check’ per se. A modified version of @obscuresec‘s Get-GPPPassword was also integrated, where the code looks for any group policy preference files cached locally on the host and decrypts any found credentials. This was added into PowerUp as it is kept as a host-based check instead of one that produces network communications. Big thanks to Ben Campbell for the prodding to implement this.

The following functions are new and will be described in more detail later in this post:

@mattifestation‘s PSReflect library in order to allow in-memory Win32 API access and struct/enum construction.

Get-CurrentUserTokenGroupSid which returns all SIDs that the current user is a part of, whether they are disabled or not (the equivalent of whoami /groups ).

which returns all SIDs that the current user is a part of, whether they are disabled or not (the equivalent of ). Add-ServiceDacl which adds a DACL field to a service object returned by Get-Service.

which adds a DACL field to a service object returned by Get-Service. Set-ServiceBinPath which sets the binary path for a service to a specified value (the equivalent of sc.exe config SERVICE binPath= X ).

Modifiable Service Enumeration

One of the first tests written into PowerUp was a ‘vulnerable’ service check, meaning enumerating all services that the current user can modify the configuration of. This can sometimes happen if a third party installer accidentally grants SERVICE_CHANGE_CONFIG or SERVICE_ALL_ACCESS rights for a service to users/groups not a part of local administrators, resulting in the canonical Windows misconfiguration privesc of sc.exe config SERVICE binPath= 'net user...' . I used to think that this check was outdated until I saw this issue twice in the last year while on engagements ¯\_(ツ)_/¯

Services have ACLs associated with them just like files, but the built-in Get-Service/Get-Acl cmdlets don’t let us easily enumerate these. So to check for modification rights, Get-ModifiableService used to attempt to set the error control for each service to its current value, returning $False if a permission error was thrown. This was pretty accurate but was quite noisy with all of its attempted service modifications. A bit ago sagishahar started down the path of ACL enumeration using sc.exe sdshow SERVICE. We’ve recently heavily expanded on this to remove the dependency on sc.exe completely.

@mattifestation was able to whip up the code for Add-ServiceDacl which takes a [ServiceProcess.ServiceController] object from Get-Service, queries for the service DACL with the QueryServiceObjectSecurity() Win32 API call, and adds a .Dacl field to the passed service object based on a ServiceAccessRights enum that he created. Here’s what the output looks like:

Test-ServiceDaclPermission now incorporates this approach, allowing you to test the ACLs for specified services against different permission sets (like ‘ChangeConfig’, ‘Restart’, ‘AllAccess’, etc.). If the current user has the specified rights to a service name/object passed on the pipeline to Test-ServiceDaclPermission the service object will be returned. This means that Get-ModifiableService is now quite simple:

Get-ModifiableService.ps1 Get-Service | Test-ServiceDaclPermission -PermissionSet 'ChangeConfig' | ForEach-Object { $ServiceDetails = $_ | Get-ServiceDetail $ServiceRestart = $_ | Test-ServiceDaclPermission -PermissionSet 'Restart' if($ServiceRestart) { $CanRestart = $True } else { $CanRestart = $False } $Out = New-Object PSObject $Out | Add-Member Noteproperty 'ServiceName' $ServiceDetails.name $Out | Add-Member Noteproperty 'Path' $ServiceDetails.pathname $Out | Add-Member Noteproperty 'StartName' $ServiceDetails.startname $Out | Add-Member Noteproperty 'AbuseFunction' "Invoke-ServiceAbuse -Name '$($ServiceDetails.name)'" $Out | Add-Member Noteproperty 'CanRestart' $CanRestart $Out } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Get-Service | Test-ServiceDaclPermission -PermissionSet 'ChangeConfig' | ForEach-Object { $ServiceDetails = $_ | Get-ServiceDetail $ServiceRestart = $_ | Test-ServiceDaclPermission -PermissionSet 'Restart' if ( $ServiceRestart ) { $CanRestart = $True } else { $CanRestart = $False } $Out = New-Object PSObject $Out | Add-Member Noteproperty 'ServiceName' $ServiceDetails . name $Out | Add-Member Noteproperty 'Path' $ServiceDetails . pathname $Out | Add-Member Noteproperty 'StartName' $ServiceDetails . startname $Out | Add-Member Noteproperty 'AbuseFunction' "Invoke-ServiceAbuse -Name '$($ServiceDetails.name)'" $Out | Add-Member Noteproperty 'CanRestart' $CanRestart $Out }

So everything should be less complicated, more accurate, and no longer reliant upon sc.exe!

Modifiable File Enumeration

Several functions (Get-ModifiableServiceFile, Get-ModifiableRegistryAutoRun, Get-ModifiableScheduledTaskFile) try to check if particular file paths are writeable by the current user. To do this, any path strings discovered by these functions are run through the Get-ModifiablePath function which ‘tokenizes’ the string into likely file locations and checks each for modification rights. This used to be done with the .NET File.OpenWrite function, opening a candidate file for write access and closing it immediately, returning $False if an error is throw.

Get-ModifiablePath now performs proper file ACL enumeration to determine if the current user can modify any file candidates. All enabled group SIDs the user is currently a part of are enumerated with [System.Security.Principal.WindowsIdentity]::GetCurrent().Groups and the file ACLs for each candidate are enumerated with Get-Acl. PowerUp then filters for all ACE entries that allow for modification (‘GenericWrite’, ‘GenericAll’, ‘MaximumAllowed’, ‘WriteOwner’, ‘WriteDAC’, ‘WriteData/AddFile’ or ‘AppendData/AddSubdirectory’ rights) and translates all the IdentityReferences (SID/account names) for these entries. Finally, if there are any matches between the SID set that can modify the file and what the current user is a part of, a custom object is returned that has the file path and IdentityReference/Permission sets.

This will catch some side cases that PowerUp previously missed where the current user had the ability to modify the owner or access control of a file. It’s also a bit quieter as the file isn’t actually opened for reading.

In order to find modifiable folders in %PATH%, Find-PathDLLHijack used to create a temporary file and immediately delete it in any candidate folders. It now uses Get-ModifiablePath as well to prevent this file operation. It also takes advantage of another benefit of Get-ModifiablePath; if a file doesn’t exist, Get-ModifiablePath will check if the parent folder of the file allows modification by the current user as well (meaning you could create the missing file). Here’s how it looks for a %PATH% that includes the C:\Python27\ folder that exists and the C:\Perl\ folder which does not:

Writing Out External Binaries

As we started down the path of using PSReflect to replace service ACL enumeration, we realized we should go ahead and write out the dependency on sc.exe all together. Starting/stopping were replaced with Start-Service/Stop-Service and enabling/disabling were replaced with Set-Service -StartupType Manual. The only action not replaceable with these built in PowerShell methods was sc.exe config SERVICE binPath= '...' . For that we again need the Windows API.

The newly minted Set-ServiceBinPath function takes advantage of the ChangeServiceConfig() Win32 API call to modify the lpBinaryPathName (binPath) field of a service to whatever we specify. This is now used in the Invoke-ServiceAbuse function to create a local administrator or execute a custom command.

You can see another small modification in the above example; functions in PowerUp that interact with services can now take a service name OR a service object (from Get-Service) on the pipeline.

The last lingering binary call was more annoying to resolve. One of PowerUp’s tests is a check of whether the current user is a local administrator but the current security context is medium integrity, meaning a BypassUAC attack would be applicable. This was previously done by calling whoami /groups to enumerate all group SIDs the current user is a part of and searching for S-1-5-32-544 (the SID of the local Administrators group) – ($(whoami /groups) -like "*S-1-5-32-544*").length -eq 1 . The equivalent call in PowerShell is [System.Security.Principal.WindowsIdentity]::GetCurrent().Groups which actually wraps the GetTokenInformation() API call just like whoami.exe. However, in the case of a local administrator in medium integrity, this WON’T show the S-1-5-32-544 SID.

I sat scratching my head for a while until Lee Holmes pointed out that the Groups() call on the WindowsIdentity object filters out certain results, “Specifically, any groups which were on the token for deny-only will not be returned in the Groups collection. Similarly, a group which is the SE_GROUP_LOGON_ID will not be returned“. You can see this in the reference source here.

So it was again back to the raw Windows API, this time implementing a series of four API calls. If we use GetCurrentProcess() to get a pseudo handle to our current process, open its access token with OpenProcessToken(), and query GetTokenInformation() with the TokenGroups value from the TOKEN_INFORMATION_CLASS enumeration we can get back a TOKEN_GROUPS structure. This has ALL group SIDs the user is currently a part of, whether they’re enabled or not. We can then use ConvertSidToStringSid() to convert the SID structures to readable strings and search for ‘S-1-5-32-544’.

The new Get-CurrentUserTokenGroupSid function will return all SIDs that the current user is a part of, whether they are disabled or not, along with their attribute enumerations:

We can now check for administrative rights in medium integrity with (without calling whoami.exe) by executing (Get-CurrentUserTokenGroupSid | Select-Object -ExpandProperty SID) -contains 'S-1-5-32-544' .

Wrap-Up

So why all the effort to avoid external binary calls? The biggest reason is command line auditing and avoiding host modification in general. The previous version of PowerUp was quite ‘noisy’ from this perspective, spawning a large number of external binaries from its powershell.exe process and doing things like attempted brute-forced service modifications. We try our best to adhere to an approach of stealth and staying off of disk (even if it’s a bit more work) and these new PowerUp updates fall right in line with that philosophy. There are plenty of ways to catch our offensive PowerShell, but we don’t want to make it any easier on defenders than necessary ;)

And as a final side note, PowerUp now has a decent suite of Pester tests to validate its functionality. This should increase its stability going forward and make the codebase more resilient to unintended bugs as a result of refactoring in the future. Pester is a unit-testing framework for PowerShell and I can’t recommend highly enough that everyone get in the habit of properly designing and testing their code! We are now actually requiring associated Pester tests be submitted with any new code for PowerSploit.