Whodat? Enumerating Who "owns" a Workstation for IR

Published: 2020-02-20
Last Updated: 2020-02-20 16:24:54 UTC
by Rob VandenBrink (Version: 1)
1 comment(s)

Eventually in almost every incident response situation, you have to start contacting the actual people who sit at the keyboard of affected stations.  Often you'll want them to step back from the keyboard or logout, for either remote forensics data collection or for remediation.  Or in the worst case, if you don't have remote re-imaging working in your shop, to either ship their station back to home base for re-imaging or to arrange a local resource to re-image the machien the hard way.

Long story short, what this means is that you need the name of the principal user of that station, and their phone number, preferably their cell number.  Time is usually of the essence in IR, and you can't always count on or wait for email or voicemail (or VOIP phones either...).  What you will usually find is that often stations will pass from hand-to-hand, outside of IT's control.  It's pretty common for managers in remote locations to "stash" a user's laptop when they leave the company, either as a spare or just to be handy when the next new hire gets onboarded.  It's also not unheard of for the IT folks re-deploying workstations to make clerical errors as they assign devices (or just plain skip the updates).

What that means is that the hardware / OS / software inventory scripts that we've covered in the past only cover 3 or the 4 facets of the actual inventory.  We want to add Userid, Full Username, City / Address and Extension and full phone / cell phone number.  What this also implies is that you want your AD administrators to work more closely with both the telephone and cell phone admins and with HR.  If you use the methods outlined below, it also implies that you'll be populating the appropriate AD fields with accurate contact information.

First, how do we get the currently logged in user?  We've had methods to do this since forever, a few of them are:

Arguably the easiest method is to use pstools.  psloggedin.exe will get you the currently logged in user by scanning the HKEY_USERS registry hive to get logged in User's SIDs, then users NetSessionEnum API go get the user name.  And, just like psexec, psloggedin will "find a way" - it'll generally enable what it needs to get the job done, then back out the changes after the data collection is complete.

How does this look?  See below (the -l enumerates only locally logged in user accounts)

> PsLoggedon.exe -nobanner -l \\nn.nn.nn.nn
Users logged on locally:
     2/4/2020 1:41:40 PM        domain\userid

Note however that this means that you'll need to loop through each station, and that the station needs to be online to collect this information.

WMIC also does a good job, with similar (and and similarly limited) results.


"nbtstat -a <computername>" was the way to get this info back in the day, but I'm not seeing great success with this command in a modern network.

Query.exe and qwinsta both still work great though:

> query user /server:<hostname or ip>
 <userid>              console             1  Active      none   2/4/2020 1:41 PM

 > qwinsta /server:
 <userid>              console             1  Active      none   2/4/2020 1:41 PM

The problem with all of these is that it just gets us the username - we still have to look up any contact info somewhere else.  This may be simple if your AD is up (or your Exchange / email Services if the GAL serves as your phone book), but might be more difficult if your security incident is more widespread.

More importantly, these all give us the username as a string, with lots of other cruft all around the one string of interest.  This is fine for 1-2-3 hosts, but if you're collecting info for hundreds or thousands of hosts, not so much.

How about adding user collection to our previous hardware inventory script (in PowerShell)?  This is pretty easy also, and also allows us to add in our contact details.

To get a logged in user:

$a = Get-WmiObject –ComputerName <hostname or ip> –Class Win32_ComputerSystem | Select-Object UserName


(Get-CimInstance works exactly the same way)

Parsing this out to just the username string:

$a.username | ConvertFrom-String -Delimiter "\\"

P1          P2    
--          --    
DOMAIN      userid

$currentuser = ($a.username | ConvertFrom-String -Delimiter "\\").p2

What if our user isn't logged in, but the machine is powered on?  In that case, we're digging into the security event log on that machine, looking for event id 4624.  In some cases we'll see multiple users logging into a computer, so we'll go back "N" days, and pick the user with the largest count of logins as our principal user for that station.  Note that logon type 2 is a local interactive login, so that's what we're looking for in "$event.properties[8].value -eq 2".  An alternative view is that you'll want to collect all the users of that host, since the principal user might not be available when you call.

$days = -1

$filter = @{

            Logname = 'Security'

            ID = 4624

            StartTime =  [datetime]::now.AddDays($days)

            EndTime = [datetime]::now


$events += Get-WinEvent -FilterHashtable $filter -ComputerName $WorkstationNameOrIP

$b = $events | where { $_.properties[8].value -eq 3 }

Now, on the AD side, let's add in the infomation collection for phone and email info:

$userinfo = Get-ADUser -filter * -properties Mobile, TelephoneNumber | select samaccountname, name, telephonenumber, mobile, emailaddress

Finally, adding the hardware info from our existing hardware inventory script, we can join the two blocks of information by workstation name, using DNS (we'll do this in a few paragraphs).  This is more reliable than just using the IP address returned in $pcs, as workstations commonly now have both wired and wireless IPs, so that field is not our most reliable bit of info.

$pcs = get-adcomputer -filter * -property Name,dnshostname,OperatingSystem,Operatingsystemversion,LastLogonDate,IPV4Address

How about if we need to get the username for a station that's powered off or disconnected?  Remember that during a security incident that target machine might not be operable, or it might be remote.  That's a bit more time consuming, we'll need to query AD for users who have logged into the target station.  If multiple users have logged in, then most likely we'll want the person who has logged in the most, or the complete list with a count for each user.  More useful, a database of all stations and logins can be refreshed weekly, then used to verify our inventory database or just saved so that we have current information in the event of an incident.

In preparation for this, you'll want to ensure that you are logging all user logins.  The best advice is that you want both successful and failed logins (for obvious reasons), though for this application we need successful logins.  In group policy editor, these settings are at:
Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy Configuration > Audit Policies > Login/Logoff > Audit Logon (choose both Success and Failure)

And yes, for reasons unknown Microsoft has these both disabled by default (most vendors follow this same not-so-intuitive path).  Maybe some 1990's reason like "disk is expensive" or something ...

Anyway, with that enabled, we can now query the event log:


#collect a week of data (fewer days will speed up this script)

DAYS = 7

$filter = @{
    Logname = 'Security'
    ID = 4624
    StartTime =  [datetime]::now.AddDays($days)
    EndTime = [datetime]::now

$events = Get-WinEvent -FilterHashtable $filter -ComputerName $DC.Hostname

To harvest the information of interest, the fields we want are:

$events[n].Properties[5] for the userid
$events[n].Properties[8] needs to be 2 for a local login, or 3 for a remote login.  If you are querying AD logs on the DCs, you'll want Logon Type 3.
$events[n].Properties[18] for the ip address of workstation

Putting this all into one script:

$U_TO_S = @()
$events = @()

$days = -1
$filter = @{
    Logname = 'Security'
    ID = 4624
    StartTime =  [datetime]::now.AddDays($days)
    EndTime = [datetime]::now

# Get your ad information
$DomainName = (Get-ADDomain).DNSRoot
# Get all DC's in the Domain
$AllDCs = Get-ADDomainController -Filter * -Server $DomainName | Select-Object Hostname,Ipv4address,isglobalcatalog,site,forest,operatingsystem

foreach($DC in $AllDCs) {
    if (test-connection $DC.hostname -quiet) {
        # collect the events
        write-host "Collecting from server" $dc.hostname
        $events += Get-WinEvent -FilterHashtable $filter -ComputerName $DC.Hostname

# filter to network logins only (Logon Type 3), userid and ip address only
$b = $events | where { $_.properties[8].value -eq 3 } | `
     select-object @{Name ="user"; expression= {$_.properties[5].value}}, `
     @{name="ip"; expression={$_.properties[18].value} }

# filter out workstation logins (ends in $) and any other logins that won't apply (in this case "ends in ADFS")
# as we are collecting the station IP's, adding an OS filter to remove anything that includes "Server" might also be useful
# filter out duplicate username and ip combos
$c = $b | where { $_.user -notmatch 'ADFS$' } | where { $_.user -notmatch '\$$' } | sort-object -property user,ip -unique

# collect all user contact info from AD
# this assumes that these fields are populated
$userinfo = Get-ADUser -filter * -properties Mobile, TelephoneNumber | select samaccountname, name, telephonenumber, mobile, emailaddress | sort samaccountname

# combine our data into one "users to stations to contact info" variable
# any non-ip stn fields will error out - for instance "-".  This is not a problem
foreach ( $logevent in $c ) {
            $u = $userinfo | where { $_.samaccountname -eq $logevent.user }
            $tempobj = [pscustomobject]@{
                user = $logevent.user
                stn = [system.net.dns]::gethostbyaddress($logevent.ip).hostname
                mobile = $u.mobile
                telephone = $u.telephonenumber
                emailaddress = $u.emailaddress
            $U_to_S += $tempobj

# We can easily filter here to
# in this case we are filtering out Terminal Servers (or whatever) by hostname, remove duplicate entries
$user_to_stn_db = $U_TO_S | where { $_.stn -notmatch 'TS' } | sort-object -property stn, user -unique | sort-object -property user

$user_to_stn_db | Out-GridView

This will get you close to a final "userid + contacts + workstation" database.  What you'll likely want to add is any additional filters, maybe to remove any VDI station logins, named-user-admin accounts and so on.  If you are seeing ANONYMOUS logins, logins with the built-in "Administrator" accounts, service accounts and so on, you'll likely want to filter those top, but maybe investigate them first.
Again, with the hostnames and IP addresses in hand it's now easy to get the station's OS from our previous inventory scripts, and maybe filter out anything that has the word "Server" in it if the goal is to collect username - workstation information. 

At some point you'll have some manual edits to do, but a script like this can go a long way towards verifying an existing inventory and contact database, creating a new one from scratch, or just a handy "who logs into what" list with phone numbers.  Remember, for IR you're not so much concerned with who "owns" a station as to who logs into it often (so you can find those people and then physicall find that station).
So if you have 3 people sharing one station, if your list has all 3 people that's a win!

As always, I'll have this script in my github, https://github.com/robvandenbrink - look in "Critical Controls / CC01"

If you can, try a script like this in your environment, then user our comment form to let us know if you find anything "odd".

Rob VandenBrink

1 comment(s)


Nice script - but it requires some kind of additional rights to be able to run this against the DC. According to this post (https://social.technet.microsoft.com/Forums/lync/en-US/b72162d1-2c86-4d1a-9727-ec7269814cc4/getwinevent-with-nonadministrative-user?forum=winserverpowershell) you need to modify rights on the registry. Is that right? I don't want to muck around with the DC unless I really have to.

Diary Archives