Token Impersonation in C#

This post was inspired by a question posted by kevin in my Discord server, about how token impersonation can be applied to threads in C#. Before delving into that particular facet, let’s do a quick recap of token impersonation as a whole.

What is Token Impersonation?

This is a practice by which a calling thread can impersonate the security context of another user. This is used quite extensively in penetration testing et al to assume the identity of another user (useful for lateral movement, etc). The impersonation itself is achieved using the ImpersonateLoggedOnUser API. As the documentation states, the user to impersonate is represented by a token handle, which can be obtained in a variety of ways.

The two most common methods are known colloquially as “make token” and “steal token” (popularised by the make_token and steal_token commands in Cobalt Strike).

Make Token

This token is one we can make ourselves by passing the known plaintext credentials of a user to the LogonUserW API, usually with the LOGON32_LOGON_NEW_CREDENTIALS logon type. The resulting token has the same local identifier as the current user, but uses the alternate credentials when making network connections.

LogonUserW(
    "Administrator",    // username
    "LAB",              // domain
    "Passw0rd!",        // password
    LogonType.Logon32LogonNewCredentials,
    LogonProvider.Logon32ProviderWinnt50,
    out var hToken);

The resulting token, hToken, can then be passed to ImpersonateLoggedOnUser. We can prove it works by attempting to access a network resource before and after.

const string targetPath = @"\\WIN-UPVRP0CV3CV\c$";
IEnumerable<string> entries;

using var identity = WindowsIdentity.GetCurrent();
Console.Write($"Accessing share as {identity.Name}... ");

try
{
    // this will throw an unauthorized access exception
    entries = Directory.EnumerateFileSystemEntries(targetPath);
}
catch (UnauthorizedAccessException e) 
{
    Console.WriteLine("Failed.");
    Console.WriteLine(e.Message);
    Console.WriteLine();
}

Console.Write(@"Making token for LAB\Administrator... ");

// make token
var success = LogonUserW(
    "Administrator",
    "LAB",
    "Passw0rd!",
    LogonType.Logon32LogonNewCredentials,
    LogonProvider.Logon32ProviderWinnt50,
    out var hToken);

if (success) Console.WriteLine("Success.");
else throw new Win32Exception(Marshal.GetLastWin32Error());

Console.Write(@"Impersonating token... ");

// impersonate token
success = ImpersonateLoggedOnUser(hToken);

if (success) Console.WriteLine("Success.");
else throw new Win32Exception(Marshal.GetLastWin32Error());

Console.WriteLine("Accessing share again:");
Console.WriteLine();

// list again and it should work
entries = Directory.EnumerateFileSystemEntries(targetPath);

foreach (var entry in entries)
    Console.WriteLine(entry);
Accessing share as LAB\rasta... Failed.
Access to the path '\\WIN-UPVRP0CV3CV\c$' is denied.

Making token for LAB\Administrator... Success.
Impersonating token... Success.
Accessing share again:

\\WIN-UPVRP0CV3CV\c$\$Recycle.Bin
\\WIN-UPVRP0CV3CV\c$\$WinREAgent
\\WIN-UPVRP0CV3CV\c$\artifacts
\\WIN-UPVRP0CV3CV\c$\Documents and Settings
\\WIN-UPVRP0CV3CV\c$\DumpStack.log.tmp
\\WIN-UPVRP0CV3CV\c$\pagefile.sys
\\WIN-UPVRP0CV3CV\c$\PerfLogs
\\WIN-UPVRP0CV3CV\c$\Program Files
\\WIN-UPVRP0CV3CV\c$\Program Files (x86)
\\WIN-UPVRP0CV3CV\c$\ProgramData
\\WIN-UPVRP0CV3CV\c$\Recovery
\\WIN-UPVRP0CV3CV\c$\System Volume Information
\\WIN-UPVRP0CV3CV\c$\Users
\\WIN-UPVRP0CV3CV\c$\Windows

Steal Token

Rather than making a token, we have the potential to “steal” one from a process already running on the local system. For instance, my user identity is LAB\rasta but the user LAB\Administrator is running a Notepad process.

These steps are slightly longer. We first call OpenProcess to obtain a handle to the target process. We need at least PROCESS_QUERY_INFORMATION and PROCESS_DUP_HANDLE privileges.

var hProcess = OpenProcess(
    ProcessAccess.QueryInformation | ProcessAccess.DuplicateHandle,
    false,
    3552);

Then use OpenProcessToken to get a handle to the process primary access token with TOKEN_DUPLICATE and TOKEN_IMPERSONATE privileges.

OpenProcessToken(
    hProcess,
    TokenAccess.TokenDuplicate | TokenAccess.TokenImpersonate,
    out var hToken);

We then need to duplicate that token using DuplicateTokenEx, with an ImpersonationLevel of SecurityImpersonation and a TokenType of TokenImpersonation.

DuplicateTokenEx(
    hToken,
    TokenAccess.TokenAllAccess,
    IntPtr.Zero,
    SecurityImpersonationLevel.SecurityImpersonation,
    TokenType.TokenImpersonation,
    out var hTokenDup);

This final hTokenDup is the one that we could now pass to ImpersonateLoggedOnUser. I won’t provide another full code example as above, but trust me, it works 🙂

Alternate Impersonation Methods

If you didn’t know, a great deal of the .NET runtime is actually built on top of native APIs. For example, if you did Process.GetProcessById(3552).Handle; the runtime will call OpenProcess from the Microsoft.Win32.NativeMethods namespace.

There are also some managed wrappers around ImpersonateLoggedOnUser which exists in the System.Security.Principal.Win32 namespace. The two that I’m going to cover here are exposed via the WindowsIdentity class.

RunImpersonated

The WindowsIdentity class has a static method called RunImpersonated, which takes a SafeAccessTokenHandle and an Action. This provides a very simple method of automatically impersonating the given token and executing a pre-defined routine.

WindowsIdentity.RunImpersonated(new SafeAccessTokenHandle(hToken), DoWorkAsUser);

private static void DoWorkAsUser()
{
    var entries = Directory.EnumerateFileSystemEntries(targetPath);

    foreach (var entry in entries)
        Console.WriteLine(entry);
}

ImpersonationContext

WindowsIdentity.Impersonate provides a disposable context called a WindowsImpersonationContext. This method will also impersonate the given token and any work executed within the scope of this context will be as the impersonated user.

using var context = WindowsIdentity.Impersonate(hTokenDup);
DoWorkAsUser();

Threads

The original question posed by kevin was how to combine impersonation with new threads. The issue with threads can be demonstrated quite simply:

LogonUserW(
    "Administrator",
    "LAB",
    "Passw0rd!",
    LogonType.Logon32LogonNewCredentials,
    LogonProvider.Logon32ProviderWinnt50,
    out var hToken);

ImpersonateLoggedOnUser(hToken);

var thread = new Thread(DoWorkAsUser);
thread.Start();

The above code will throw an UnauthorizedAccessException because the new thread will not inherit the impersonated token. This is expected given that the documentation for ImpersonateLoggedOnUser specifies that only the “calling thread” will impersonate the token.

The WindowsImpersonationContext is probably the easiest means of cascading an impersonation token to a new thread:

LogonUserW(
    "Administrator",
    "LAB",
    "Passw0rd!",
    LogonType.Logon32LogonNewCredentials,
    LogonProvider.Logon32ProviderWinnt50,
    out var hToken);

using var context = WindowsIdentity.Impersonate(hToken);
var thread = new Thread(DoWorkAsUser);
thread.Start();

Related posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...

Latest posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...