Knowing which users are logged into a system is very useful information for an adversary. If an adversary gains access to a host where an administrative user is logged in, they may be able to impersonate that user either through access token theft, or duplicating the users Kerberos tickets.
In the past, we could use the NetSessionEnum API to determine which users were logged into a host. Unfortunately, this now requires administrative privileges over a target host.
However, it’s possible to determine logged in user by querying the remote registry service. The remote registry service is set to automatically start on server operating systems. On clients, it’s disabled by default but may still be enabled in some environments.
When a user is logged into a host, the HKEY_USERS hive will be populated with their SID. Using regedit.exe, and selecting File > Connect Network Registry, we can see a user with the SID S-1-5-21-56914125-514874789-4251139759-500 is logged in:
We can then use PowerShell to convert the SID to a username:
$SID = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-21-569141525-514874789-4251139759-500")
$User = $SID.Translate([System.Security.Principal.NTAccount])
$User.Value
BORDERGATE\Administrator
This article will look at automating username enumeration in a domain using C#.
Enumerating Machines in the Domain
First, we need to determine the current domain, and use LDAP queries to find the hostnames for systems registered in Active Directory. This can be done fairly easily using the System.DirectoryServices namespace.
static string GetCurrentDomainName()
{
try
{
using (DirectoryEntry root = new DirectoryEntry("LDAP://RootDSE"))
{
return root.Properties["defaultNamingContext"].Value.ToString();
}
}
catch (Exception ex)
{
Console.WriteLine("Error getting domain name: " + ex.Message);
return null;
}
}
static void enumerateHosts()
{
string domainName = GetCurrentDomainName();
if (domainName == null)
{
Console.WriteLine("Unable to determine the domain name.");
return;
}
string ldapPath = $"LDAP://{domainName}";
Console.WriteLine("Querying " + ldapPath);
DirectoryEntry entry = new DirectoryEntry(ldapPath);
DirectorySearcher searcher = new DirectorySearcher(entry)
{
Filter = "(objectClass=computer)"
};
searcher.PropertiesToLoad.Add("name");
try
{
SearchResultCollection results = searcher.FindAll();
foreach (SearchResult result in results)
{
queryRegistry(result.Properties["name"][0].ToString());
}
}
catch (Exception ex)
{
Console.WriteLine("Error querying Active Directory: " + ex.Message);
}
}
Querying the Remote Registry Service
Next, we connect to the remote system and query the keys available under the HKEY_USERS hive. A regex is used to ensure that only domain users (those with a SID starting with S-1-5-21) are reported.
static void queryRegistry(string systemName)
{
string remoteMachine = @"\\" + systemName;
try
{
using (RegistryKey remoteRegistry = RegistryKey.OpenRemoteBaseKey(RegistryHive.Users, remoteMachine))
{
using (RegistryKey baseKey = remoteRegistry)
{
if (baseKey != null)
{
string[] subKeyNames = baseKey.GetSubKeyNames();
Console.WriteLine("*****************************");
Console.WriteLine(systemName + " logged in users");
foreach (string subKeyName in subKeyNames)
{
// Regex domain users
string sidPattern = @"^S-1-5-21-\d{1,}-\d{1,}-\d{1,}(?:-\d+)?$";
Regex sidRegex = new Regex(sidPattern);
bool containsSid = sidRegex.IsMatch(subKeyName);
if (containsSid == true)
{
ResolveSID(subKeyName);
}
}
}
else
{
Console.WriteLine("Failed to open base key.");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(systemName + " Error accessing remote registry: " + ex.Message);
}
}
static string ResolveSID(string sidString)
{
try
{
SecurityIdentifier sid = new SecurityIdentifier(sidString);
NTAccount account = (NTAccount)sid.Translate(typeof(NTAccount));
string username = account.ToString();
Console.Write($"SID: {sidString} ");
Console.WriteLine($"Username: {username}");
return username;
}
catch (Exception ex)
{
Console.WriteLine("Error resolving SID to username: " + ex.Message);
return "unknown";
}
The Result
Running the application shows the logged in domain users.
Full Code Listing
using Microsoft.Win32;
using System;
using System.DirectoryServices;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text.RegularExpressions;
namespace EnumerateUsers
{
internal class Program
{
static void Main(string[] args)
{
enumerateHosts();
Console.ReadLine();
}
static string GetCurrentDomainName()
{
try
{
using (DirectoryEntry root = new DirectoryEntry("LDAP://RootDSE"))
{
return root.Properties["defaultNamingContext"].Value.ToString();
}
}
catch (Exception ex)
{
Console.WriteLine("Error getting domain name: " + ex.Message);
return null;
}
}
static void enumerateHosts()
{
string domainName = GetCurrentDomainName();
if (domainName == null)
{
Console.WriteLine("Unable to determine the domain name.");
return;
}
string ldapPath = $"LDAP://{domainName}";
Console.WriteLine("Querying " + ldapPath);
DirectoryEntry entry = new DirectoryEntry(ldapPath);
DirectorySearcher searcher = new DirectorySearcher(entry)
{
Filter = "(objectClass=computer)"
};
searcher.PropertiesToLoad.Add("name");
try
{
SearchResultCollection results = searcher.FindAll();
foreach (SearchResult result in results)
{
queryRegistry(result.Properties["name"][0].ToString());
}
}
catch (Exception ex)
{
Console.WriteLine("Error querying Active Directory: " + ex.Message);
}
}
static string ResolveSID(string sidString)
{
try
{
SecurityIdentifier sid = new SecurityIdentifier(sidString);
NTAccount account = (NTAccount)sid.Translate(typeof(NTAccount));
string username = account.ToString();
Console.Write($"SID: {sidString} ");
Console.WriteLine($"Username: {username}");
return username;
}
catch (Exception ex)
{
Console.WriteLine("Error resolving SID to username: " + ex.Message);
return "unknown";
}
}
static void queryRegistry(string systemName)
{
string remoteMachine = @"\\" + systemName;
try
{
using (RegistryKey remoteRegistry = RegistryKey.OpenRemoteBaseKey(RegistryHive.Users, remoteMachine))
{
using (RegistryKey baseKey = remoteRegistry)
{
if (baseKey != null)
{
string[] subKeyNames = baseKey.GetSubKeyNames();
Console.WriteLine("*****************************");
Console.WriteLine(systemName + " logged in users");
foreach (string subKeyName in subKeyNames)
{
// Regex domain users
string sidPattern = @"^S-1-5-21-\d{1,}-\d{1,}-\d{1,}(?:-\d+)?$";
Regex sidRegex = new Regex(sidPattern);
bool containsSid = sidRegex.IsMatch(subKeyName);
if (containsSid == true)
{
ResolveSID(subKeyName);
}
}
}
else
{
Console.WriteLine("Failed to open base key.");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(systemName + " Error accessing remote registry: " + ex.Message);
}
}
}
}
In Conclusion
The remote registry service provides a useful alternative to the NetSessionEnum API. It should be noted that false positives may occur with this method. If a user does not log out correctly, the registry entries can be left in place indicating they are logging in when that is not the case.