Finding Hidden Treasure on Owned Boxes: Post-Exploitation Enumeration with wmiServSessEnum
TLDR: We can use WMI queries to enumerate accounts
configured to run any service on a box (even non-started / disabled), as well
as perform live session enumeration. Info on
running the tool is in the bottom section.
Background
On a recent engagement I had gotten local admin privileges on ~20 boxes, and after querying active sessions on them got me nothing interesting I was ready to look for other potential escalation paths. I ran secretsdump against several of the systems to grab local account hashes, and found that in the process of running it, I had also obtained plaintext credentials for a domain account that was not mentioned in any of the session enumeration information I had pulled. This got me thinking about how this was possible, as well as how I could more reliably hunt for similar configurations on other systems I could remotely execute code on.First, to explain what was going on – the NetWkstaUserEnum WINAPI function used by a majority of session enumeration tools is great at what it does, but only pulls data for active sessions on the remote system (interactive, service, and batch logins). However, if a service is configured on the system but is currently not running, it will not be listed as a current session when enumerating the system. This makes sense, as a non-running service has no processes associated with it. After further investigation of the systems in question, I validated this is indeed what happened, as each of the systems was configured with a stopped service that would run using non-default credentials.
I’ve included an example below showing this in practice on a lab system using
the GetNetLoggedOnUsers() functionality of @Cobbr_io’s SharpSploit, which uses
the NetWkstaUserEnum WINAPI function to query sessions on a remote system, and
a test service I configured (TestService) to run as the local ‘admin’ user on
the box. It shows that when the service is not running, the admin user is not
enumerated (as expected):
For a bit more context on why this matters to us
at all, we have to take a look at how credentials for service accounts are
cached by Windows. When a service is configured
with a set of credentials to run as, the OS needs to store them so they don’t
have to be re-entered every reboot / every time the service is ran. Windows stores these service account
credentials within the HKEY_LOCAL_MACHINE\Security registry hive, in an
encrypted storage space known as LSA Secrets.
However, the passwords themselves, although encrypted, are stored as
plaintext values (opposed to NTLM hashes).
Items stored in this space are only readable by NT_Authority/SYSTEM by
default, but users with administrative rights on the system can create a backup
of the registry hive that can subsequently be accessed and decrypted to extract
the data contained within. As the screengrab
below shows, the credentials are sitting in LSA secrets, ready to be used
whenever next needed.
And if we dump the contents of LSA secrets, we see we can
actually retrieve the plaintext password for the account configured to run the
service:
So at this point I was a bit stumped, how could I quickly and reliably enumerate accounts configured to run services on a relatively large number of remote systems? It’s not really a best practice to start randomly secretsdumping boxes, and even if you threw opsec concerns to the wind it would still take a relatively long amount of time if you wanted to dump anything more than just a few systems. With that in mind, I wanted something that would ideally be agentless, and could be ran in a multi-threaded process to increase collection speed against multiple systems. I settled on writing something that would check these boxes in c#, primarily as that’s what I’ve been doing the majority of my development in lately.
Building the Tool
Note: This section doesn’t
have anything critical on the functionality or usage of the tool, but instead
outlines the development process and roadblocks I ran into as I built it. If this doesn’t interest you, I recommend
scrolling through to the next section.
When I first sat down to write this tool, I thought WMI
would be a good candidate to use for collection as I had some knowledge of the
Win32_Service class and figured it would be pretty easy to pull the needed
information from the remote system. As I
prepared to start coding I checked out similar projects that implemented WMI
connectivity in .net applications. From
an offensive tooling standpoint, I didn’t find too much outside of tools
designed to facilitate code execution, and overwhelmingly they appeared to use
the older System.Management namespace to build their WMI objects. In my reading
of Microsoft docs, I found that the newer Microsoft.Management.Infrastructure
namespace was recommended to use to access WMI.
As I began to build out the functionality of the tool and started exploring other WMI classes I figured it would make sense to extend the tool’s functionality to also include the optional enumeration of sessions on the system via WMI, similar to the sessionEnum functionality seen earlier through SharpSploit. To explore various WMI classes I used WMI Explorer (https://github.com/vinaypamnani/wmie2/releases) which provides a super helpful interface that allows you to browse WMI classes and get information on specific properties & methods.
Through this I found the Win32_LoggedOnUser WMI class. At first it seemed like this would be exactly
what was needed for enumerating active sessions, and my initial tests worked
great: I log in with user1, user1 shows up when I query the class, I log in
with user2, user1 & user2 now show up when I query the class. The issue came when I logged off with user2
and queried the class again; user2 still showed up as having a session on the system. I tried giving it a few minutes, thinking
that the session was temporarily caching on log-off, but still user2 appeared
to be logged in when querying the class.
This led me to a bunch of googling and the unfortunate conclusion that
the Win32_LoggedOnUser class tracks ALL login sessions since last reboot,
including ‘stale’ connections, or those that are no longer exist. This isn’t great for us, as these stale
sessions do not retain cached credentials in memory by default, potentially
leading to a plethora of false positives based on old logins. There are definitely operational uses for
this information, ex. looking for a system where there have been administrative logins at some point since last reboot –
likely within the past month – and targeting them for long-term surveillance or
persistence with the theory being that an admin may log in there again; however
those uses are outside of the scope of this tool.
The array of session objects returned when querying the
Win32_LoggedOnUser class have two properties: an antecedent, and a
dependent. The antecedent is the value
that contains the ‘human-readable’ information regarding a specific session –
the hostname, domain, etc. The dependent
contains a ‘loginID’ value, a unique int corresponding to the specific instance
of an account logging into the system.
If a single user logs in & out multiple times prior to a reboot,
each instance will receive a unique loginID and thus be tracked independently
by the Win32_LoggedOnUser class.
There wasn’t a whole lot I could do directly with the
LoggedOnUser class to filter to only live sessions, but through a bit more
exploration of WMI classes I landed on the Win32_SessionProcess class. Similarly to LoggedOnUser, this class also
only returns an antecedent and a dependent.
However, the antecedent and dependent values returned for objects of the
SessionProcess class are different, with the antecedent containing the LoginID tied to each active process on the system and the dependent containing a handle to each of these processes. Although by themselves there isn’t much that
can be done with these values, the LoginID returned by SessionProcess can be
cross-referenced against the LoginIDs associated with LoggedOnUser objects, giving
a listing of actual logins (those that have at least one running process
associated with their loginID).
Once this connection had been made, it was fairly
straightforward to get session enumeration functionality up and running. From there, everything was pretty much in its
final state as far as functionality goes.
Things were looking good until I started using Wireshark to watch
execution across the wire in real-time.
When enumerating sessions using the NetWkstaUserEnum WINAPI function,
approximately 15 packets were sent over the wire. When running session enumeration over WMI,
that number was up to ~200 packets.
Quite a bit larger, but makes sense when considering that the session
has to be set up and multiple requests have to be made (although if anyone can
further update the queries to shave this number further I would be happy to
include). However, when I ran service
enumeration, packet counts shot up to a monstrous ~1700 per host. This was just simply too high for my liking,
and I could imagine network congestion, downed boxes, etc. if this was ran over
too many hosts in parallel.
The breakthrough in getting the amount of traffic
sent over the wire down was the realization that the WQL query sent to retrieve
objects was processed server-side. WMI
connectivity using the Microsoft.Management.Infrastructure namespace involves
creating a CimSession to a remote host, which in turn is queried using a WQL
(WMI-Query-Language) query. This is a
SQL-like statement that can be used to retrieve data based on certain
criteria. I had (mistakenly) assumed
that filters applied to these queries (ex. select * from Win32_Service where startname like ‘%admin%’) would
be applied to data after it was returned to our system; or in other words all the
data would be pulled back across the wire, and then filtered using the given
rules prior to displaying. Luckily, I
found this not to be the case, and the entire query is sent to the remote host
where it is processed on their system.
From there, only results that match the given filter are sent back over
the wire to our system. Almost all
services can be filtered out, as we’re not interested in those running under ‘default’
accounts such as SYSTEM, LOCAL SERVICE, and NETWORK SERVICE. With these new
filters applied, traffic for service is down to a much more manageable ~170
packets per host (varies with # services identified).
One other interesting point that became apparent as I
analyzed traffic from both WMI and API-based enumeration methods, this method
uses solely RPC connections, whereas API methods use SMB to remotely pull
information. There are definitely
improvements that can be made to this as well, API methods would likely be
faster and may potentially be even lighter from a network traffic perspective
(depending on what filtering can be done prior to returning service information),
and the current queries could likely be further refined to likewise reduce
traffic further. Overall though, with this last hurdle overcome, I figured the
tool was in a decent enough place to release.
wmiServSessEnum Usage
Like other tools that use WMI to connect to other systems,
admin rights are required on the remote system.
An IP/ comma-separated list of IPs is required to be entered
in on the command line when executing the tool, or a reference to a file on the
local system containing one IP per line to target. I looked into incorporating CIDR notation
into the tool, but ultimately decided against it, so as of now only specific IP
addresses are supported. Ideally this
shouldn’t be a huge deal as the addresses that are being tested are ones that you
already have valid credentials for, meaning initial network enumeration has
already occurred.
By default the tool will use whatever credentials you’re
currently running your session as, but also accepts username+domain+plaintext
passwords (use a domain of ‘.’ For a local user).
WmiServSessEnum can be ran in several different modes:
·
sessions –
similar to other user enumeration methods, will return a list of active
sessions on the remote system
·
services – returns a list (if any) of
non-default accounts configured to run services on the remote system
·
all (default) – runs both
flags should be inputted in the format of –u=UserName etc.
Comments
Post a Comment