Make PowerView Great Again

Yesterday’s commit to the PowerSploit dev branch is the biggest set of changes to PowerView since its inception. I’ve spent the last month or so rewriting PowerView from the ground up, squashing a number of bugs, adding a chunk of features, and standardizing the code base’s behavior. The commit message summarizes the modifications, but I wanted to spend some time detailing the massive set of changes. The previous PowerSploit Dev branch was merged into Master, and we will do a tagged release at some point next week.

Note: this new PowerView code is definitely beta, but should be usable. I guarantee there are new bugs that snuck in that I wasn’t able to catch, so let us know when you find them!

New Function Naming Scheme

PowerView functions now follow a brand new Verb-PrefixNoun naming scheme, partially to somewhat mirror the ‘real’ Active Directory cmdlets, and partially to expose more contextual information to the operator. I’m sure that this will irritate some people, but after some usage I believe the new naming system will make more sense to frequent users. There’s also a large set of aliases that map the old function names to the new, which should ease the transition. I spent serious time considering the new function names, trying to select what makes the most sense (there’s a method to the madness!) but I fully admit that my choices might not be perfect. If anyone makes a reasonably argued case, I’m open to changing specific function names.

These are now the verb selections in PowerView, with the general explanation for each:

  • Get-* : retrieve full raw data sets, such as Get-DomainUser (old Get-NetUser)
  • Find-* : finds specific data entries in a data set or execute threaded computer enumeration, e.g. Find-DomainShare (old Invoke-ShareHunter)
  • Add-* : adds a new object to a collection/destination, e.g. Add-DomainGroupMember which adds a domain user/group to an existing domain group
  • New-* : creates a new object/resource, e.g. New-DomainUser which creates a new domain user
  • Set-* : modifies an object, e.g. Set-DomainObject which sets domain object properties
  • Convert-* : converts object names/types, e.g. Convert-ADName which converts username formats

The Verb-PrefixNoun should now give an indication of the data source being queried. The idea with the new prefixes is to give operators an idea of what type of traffic they’re generating when executing enumeration, solely by function names:

  • Verb-Domain* : indicates that LDAP/.NET querying methods are being executed, e.g. Get-DomainOU which queries for domain organizational units through LDAP
  • Verb-WMI* : indicates that WMI is being used under the hood to execute enumeration, e.g. Get-WMIRegLastLoggedOn which enumerates the last logged on user for a host through WMI
  • Verb-Net* : indicates that Win32 API access is being used under the hood, e.g. Get-NetSession which utilizes the NetSessionEnum() Win32 API call under the hood

Nouns have been renamed to much more descriptive. I tried to think through “what object is this function returning” and naming accordingly. This resulted in one “gotcha” renaming situation that I couldn’t fix with aliases. Get-NetLocalGroup is a commonly used function that previously returned the members of a specified local group, not the groups themselves. Now, Get-NetLocalGroup will return local groups themselves, while Get-NetLocalGroupMember (old Get-NetLocalGroup) will return the members of a local group. This should be the only big gotcha, but hopefully will make more sense going forward.

Parameter/Output Standardization

Another big change is the new standardization of parameter sets across functions, particularly in the Verb-Domain* LDAP functions. This was also done to better match the behavior of the official AD cmdlets. Here are a few of the new parameters, with an explanation as well as any old aliases:

  • -Identity : replaces -UserName/-ComputerName/etc., more on this below
  • -LDAPFilter : old -Filter parameter, allows for the addition of custom LDAP filtering for any function (i.e. Get-DomainUser -LDAPFilter ‘(description=*pass*)’ )
  • -Properties : only return specific properties, more on this below
  • -SearchBase : old -ADSPath, specifies the LDAP source to search through (i.e. Get-DomainUser -SearchBase “LDAP://OU=secret,DC=testlab,DC=local” for OU queries)
  • -Server : specifies the Active Directory server to bind to, replaces the old -DomainController parameter
  • -SearchScope : new, specifies the scope to search under (Base/OneLevel/Subtree)
  • -ServerTimeLimit : new, specifies the maximum amount of time the server spends searching, useful for tuning in specific situations
  • -SecurityMasks : specifies an option for examining security information of a directory object, Dacl/Group/Owner/Sacl. Allows you to retrieve security information easily for any returned object.
  • -FindOne : returns only one result instead of all results. Useful for determining object schemas.
  • -Tombstone : specifies that the searcher should also return deleted/tombstoned objects.
  • -Credential : credential support, more on this later in the post

Also, all functions now return full objects on the pipeline. Previously (due to how PowerView evolved) some functions returned full objects (like Get-NetUser) while some returned partial data with a -FullData option for complete objects (like Get-NetComputer). This started to bug me more and more, so everything should now be standardized. If you want to retrieve specific properties, you can now do something like -Properties samaccountname,lastlogon, using -FindOne to first determine the property names you want to extract.

The great thing about this approach, as opposed to using a | Select-Object pipe at the end, is that this optimizes “towards the left”, meaning the server only returns the data fields you requested. This greatly cuts down the amount of traffic between your client and the target domain controller.

-Identity

Most LDAP/Get-Domain* functions now have an -Identity X property instead of -ComputerName/-UserName/etc. -Identity accepts not just a samAccountName, but also distinguishedName, GUID, object SID, and dnsHostName in the computer case. These can also be interspersed together, meaning you can do something like this:

This makes functions more flexible, and again mimicking the behavior of the official AD cmdlets.

-Credential

One of the biggest changes implemented is the support for -Credential for all appropriate functions. The exact behavior differs under the hood depending on function implementation (another motivator for the function renaming). I wanted to explain exactly what’s happening under the hood for each function type that accepts -Credential so you can under the traffic/events you’re producing per function executed.

First up, the Verb-Domain* functions. The functions pass through any -Credential parameter to the Get-DomainSearcher function which abstracts the alternate credential logic away. You can check out the implementation here. Basically, the code binds to the specified domain/searchBase by creating a new DirectoryServices.DirectoryEntry object with alternate credentials, which is then passed to DirectoryServices.DirectorySearcher in order to perform LDAP/AD searches.

Convert-ADName is a weird one, as it utilizes the NameTranslate COM object in the backend. Luckily Pasquale Lantella has a great Script Center code example based on Bill Stewart’s code/article that accepts an alternate credential. By invoking the ‘InitEx’ method on the COM object we can initialize everything correctly.

Verb-WMI* functions use WMI methods on the backend for remote enumeration. Specifically, Get-WmiObject is used, often with the StdRegProv class, in order to gather specific information from remote systems (for example Get-WMIRegLastLoggedOn to return the last user who logged onto a remote machine machine). Because built-in WMI methods are used, we can easily pass a -Credential object through without any issue.

Verb-Net* functions are the most painful. Many of PowerView’s Win32 API functions (like NetSessionEnum()) don’t accept alternate credential specifications. Instead, we have to use token impersonation. The newly minted Invoke-UserImpersonation will execute LogonUser() with the logonType set to LOGON32_LOGON_NEW_CREDENTIALS, which “allows the caller to clone its current token and specify new credentials for outbound connections“. This is the equivalent of executing a “runas /netonly” type of logon. However, instead of spawning a new process, ImpersonateLoggedOnUser() called on the token handle from LogonUser() in order to impersonate the newly logged on user, and then the logon handle is returned. Invoke-RevertToSelf can then revert any token impersonation, and if the logon handle from Invoke-UserImpersonation is passed, that will be closed out as well. This process lets us temporarily impersonate the user from a -Credential, execute the enumeration, and revert transparently. However, keep in mind every time you execute the LogonUser() call in Invoke-UserImpersonation, a logon event occurs.

Now, here’s the rub: Version 2 of powershell.exe starts in a multi-threaded apartment state, while Version 3+ starts in a single-threaded apartment state. Without diving into the specifics, the LogonUser() call works in both situations, but when you try to call ImpersonateLoggedOnUser() to impersonate the loggedon user token in Version 2, the call succeeds but the newly-impersonated token isn’t applied to newly spawned actions and we don’t get out alternate user context. PowerShell v3 and up is fine, and you can force PowerShell Version 2 to launch in a single-threaded apartment state with powershell.exe -sta, but it’s still potentially problematic. I’ve tried to handle this in two ways in PowerView:

  • Invoke-UserImpersonation will check if the current apartment state is STA and display a warning. So at least if you try to run something like Get-NetSession -ComputerName X -Credential $Cred from PowerShell v2, you’ll be alerted that it likely won’t work.
  • For any ‘meta’ functions (i.e. Find-DomainUserLocation/Invoke-UserHunter) that use threading, the abstracted New-ThreadedFunction helper now manually sets the apartment state of the new runspaces to be STA/single-threaded apartment. Invoke-UserImpersonation is executed once during the Begin{} block and the logon handle is passed to the script block being threaded. This prevents multiple logon events from being executed, and allows for the threaded enumeration to run from Version 2+ of PowerShell.

Also, as all of these functions accept a proper [Management.Automation.PSCredential] object for -Credential, it helps to know how to create these credential objects non-interactively (i.e. with Get-Credential). Here’s how you can do so:

$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword)
Get-DomainUser -Credential $Cred

Also,  most functions that accept -Credential now have an example (through Get-Help -Detailed/-Examples) that demonstrates this functionality, in case you forget the syntax.

Misc. Changes

The following miscellaneous changes were made:

  • Lots of function cleanup/code rot removal and standardization:
    • Additional options added to Get-DomainSearcher in order to support new param sets
    • Expanded parameter validation
    • XML help format standardized and expanded for every function
    • PSScriptAnalyzer fixups- passes PS script analyzer now!
    • Nearly all functions should tag custom types to output objects
  • -Identity supported by all appropriate functions
  • Transformed all filters to functions
  • Expanded the formats for Convert-ADName
  • Get-SPNTicket returns encrypted ticket part automatically now, and Hashcat output format added
  • Write-Verbose/Write-Warning/Throw messages now have the function name tagged in the message. This will make debugging SIGNIFICANTLY easier.
  • Verb-Domain* functions now all include a -FindOne function to return one result
  • Get-DomainUserEvent now uses -XPathFilter for a massive speedup
  • Lots of bug fixes (and new bug additions :)
  • “Required Dependencies” for each function completed
  • Fixed logic bugs for -ComputerIdentity in Get-DomainGPO, now enumerates domain-linked GPOs as well
  • Added -UserIdentity to Get-DomainGPO to enumerate GPOs applied to a given user identity
  • Now passes PSScriptAnalyzer

The following functions were removed:

  • Get-ComputerProperty, Get-UserProperty, Find-ComputerField, Find-UserField
  • Get-NameField (translated to ValueFromPipelineByPropertyName calls)
  • Invoke-DowngradeAccount – not really used, more PoC
  • Add-NetUser – split into New-DomainUser/others
  • Add-NetGroupUser – split into Add-DomainGroupMember/others
  • New-GPOImmediateTask – inconsistent and better done manually
  • Invoke-StealthUserHunter – combined into Find-DomainUserLocation
  • Get-ExploitableSystem – not really used, difficult to update

The following exported functions were added:

  • Add-RemoteConnection – ‘mounts’ a remote UNC path using WNetAddConnection2W
  • Remove-RemoteConnection – ‘unmounts’ a remote UNC path using WNetCancelConnection2
  • Invoke-UserImpersonation – creates a new “runas /netonly” type logon and impersonates the token in the current thread
  • Invoke-RevertToSelf – reverts any token impersonation
  • Invoke-Kerberoast – automates Kerberoasting
  • Find-DomainObjectPropertyOutlier – finds user/group/computer objects in AD that have ‘outlier’ properties sets
  • New-DomainUser – creates a new domain user
  • New-DomainGroup – creates a new domain group
  • Add-DomainGroupMember – adds a domain user (or group) to an existing domain group
  • Get-NetLocalGroup – now returns local groups themselves
  • Get-NetLocalGroupMember – returns local group members (old Get-NetLocalGroup)

The following functions were renamed. Aliases were made for each to ease the transition:

  • Get-IPAddress -> Resolve-IPAddress
  • Convert-NameToSid -> ConvertTo-SID
  • Convert-SidToName -> ConvertFrom-SID
  • Request-SPNTicket -> Get-DomainSPNTicket
  • Get-DNSZone -> Get-DomainDNSZone
  • Get-DNSRecord -> Get-DomainDNSRecord
  • Get-NetDomain -> Get-Domain
  • Get-NetDomainController -> Get-DomainController
  • Get-NetForest -> Get-Forest
  • Get-NetForestDomain -> Get-ForestDomain
  • Get-NetForestCatalog -> Get-ForestGlobalCatalog
  • Get-NetUser -> Get-DomainUser
  • Get-UserEvent -> Get-DomainUserEvent
  • Get-NetComputer -> Get-DomainComputer
  • Get-ADObject -> Get-DomainObject
  • Set-ADObject -> Set-DomainObject
  • Get-ObjectAcl -> Get-DomainObjectAcl
  • Add-ObjectAcl -> Add-DomainObjectAcl
  • Invoke-ACLScanner -> Find-InterestingDomainAcl
  • Get-GUIDMap -> Get-DomainGUIDMap
  • Get-NetOU -> Get-DomainOU
  • Get-NetSite -> Get-DomainSite
  • Get-NetSubnet -> Get-DomainSubnet
  • Get-NetGroup -> Get-DomainGroup
  • Find-ManagedSecurityGroups -> Get-DomainManagedSecurityGroup
  • Get-NetGroupMember -> Get-DomainGroupMember
  • Get-NetFileServer -> Get-DomainFileServer
  • Get-DFSshare -> Get-DomainDFSShare
  • Get-NetGPO -> Get-DomainGPO
  • Get-NetGPOGroup -> Get-DomainGPOLocalGroup
  • Find-GPOLocation -> Get-DomainGPOUserLocalGroupMapping
  • Find-GPOComputerAdmin -> Get-DomainGPOComputerLocalGroupMapping
  • Get-LoggedOnLocal -> Get-RegLoggedOn
  • Invoke-CheckLocalAdminAccess -> Test-AdminAccess
  • Get-SiteName -> Get-NetComputerSiteName
  • Get-Proxy -> Get-WMIRegProxy
  • Get-LastLoggedOn -> Get-WMIRegLastLoggedOn
  • Get-CachedRDPConnection -> Get-WMIRegCachedRDPConnection
  • Get-RegistryMountedDrive -> Get-WMIRegMountedDrive
  • Get-NetProcess -> Get-WMIProcess
  • Invoke-ThreadedFunction -> New-ThreadedFunction
  • Invoke-UserHunter -> Find-DomainUserLocation
  • Invoke-ProcessHunter -> Find-DomainProcess
  • Invoke-EventHunter -> Find-DomainUserEvent
  • Invoke-ShareFinder -> Find-DomainShare
  • Invoke-FileFinder -> Find-InterestingDomainShareFile
  • Invoke-EnumerateLocalAdmin -> Find-DomainLocalGroupMember
  • Get-NetDomainTrust -> Get-DomainTrust
  • Get-NetForestTrust -> Get-ForestTrust
  • Find-ForeignUser -> Get-DomainForeignUser
  • Find-ForeignGroup -> Get-DomainForeignGroupMember
  • Invoke-MapDomainTrust -> Get-DomainTrustMapping

Docs, Docs, Docs Docs Docs!

Following in @jaredcatkinson‘s documentation efforts for PowerForensics, I also started the generation of documentation for PowerView (and soon all of PowerSploit) by using platyPS. This awesome project can generate markdown docs purely from your PowerShell project’s existing XML help. For now doc files will be in ./docs/ of the dev branch and we will do our best to keep them updated.

With markdown docs generated easily, we also started integration with readthedocs.org, an external documentation hoster that integrates nicely with GitHub. The docs and formatting have a ways to go, but you can see a start of how things will look at: http://powersploit.readthedocs.io/en/latest/Recon/.

Wrap-Up

As mentioned, this is by far the biggest overhaul PowerView has ever had, so there are guaranteed to be unintended bugs and other issues. I’m intending on updating the PowerView cheat sheet soon with the new syntax as well. We’re actively field-testing the code now and actively pushing changes to the Dev branch of PowerSploit, so if you have any issues let us know and we will try for a reasonable turnaround. We’re quite excited for all the changes, and hope that everyone else is as well!

3 thoughts on “Make PowerView Great Again”

  1. Wow,congratulations for your efforts, much appreciated all the work you did!
    I’m no red-teamer but as a sysadmin the Powerview tools have helped already a lot of times. Sometimes I wonder for which reason I still need to use the standard AD cmdlets, do you have any..? ;-)
    Keep on your great work and I will report the bugs if I encounter them

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.