@April 14, 2022 7:00 AM (EDT)
I’ve been on a quest to add SCShell functionality to OffensiveNotion.
I’ve failed spectacularly at this task so far 💀
But the experiment hasn’t been a total wash. In my quest for SCShell, I’ve learned a metric frick-tonne about how Windows handles tokens at the API level.
Maybe you’re familiar with tokens in passing. Maybe you’ve used Metasploit’s Incognito module or done some make_token
and steal_token
shenanigans in Cobalt Strike. But maybe you’re also curious about the underlying process at play and what exactly “token manipulation” entails. If so, read on!
Let’s set it off right:
Frickin Tokens, How Do They Work?
I have correlated sources for you. Read these blog posts:
Tokens identify who you are on a host and in a domain.
They mark what you can access, which privileges you have (”privileges” in this sense meaning Windows process privs, like the infamous SeImpersonatePrivilege
), and if you’re running in a high-integrity context or not.
Token manipulation occurs low in the Windows OS. With a set of credentials in hand, an attacker can perform what amounts to the runas
command to create an access token for their current process context.
Practically speaking, this means an attacker that has recovered a set of plain text credentials can make the required token for this user, impersonate that user, and access this user’s resources without needing an interactive logon session.
Tokens are identified by their HANDLES
. You may be familiar with the term in a general sense. More precisely, a HANDLE
is a context-specific unique identifier that can inform the OS about some value. There’s no one single form of data for all HANDLES
; some can be a simple integer value. Or it could be a pointer to a memory structure.
But at the end of the day, a token’s handle simply allows you to interact with the token itself.
How Do You Make One?
There’s a set of Windows API calls that center on token manipulation. A simple example of this is LogonUserA
:
BOOL LogonUserA(
[in] LPCSTR lpszUsername,
[in, optional] LPCSTR lpszDomain,
[in, optional] LPCSTR lpszPassword,
[in] DWORD dwLogonType,
[in] DWORD dwLogonProvider,
[out] PHANDLE phToken
);
In goes a three set of pointers to C string values (username, domain, and password) plus a couple of other configuration parameters, out comes the handle to a token that identifies your security context in a given domain.
So for a simple Rust POC:
// Note the null byte termination here ;) I malded over that for a loooong time
let username_test: &str = "administrator\0";
let domain_test: &str = "slothco.lan\0";
let password_test: &str = "MYpassword123#\0";
unsafe {
let mut hToken: HANDLE = HANDLE(0);
let bResult: BOOL = LogonUserA(
PCSTR(username_test.as_ptr()),
PCSTR(domain_test.as_ptr()),
PCSTR(password_test.as_ptr()),
LOGON32_LOGON_NEW_CREDENTIALS,
LOGON32_PROVIDER_DEFAULT,
&mut hToken,
);
println!("[+] Token handle value: {:?}", hToken);
The hToken value is initialized to nothing as a placeholder. The API call completes and sets the handle value in the hToken variable.
You can print this value to the console, but it’s not terribly useful to us humans:
But at least we know it worked! Printing the bResult variable is also a good practice here. According to the docs, a nonzero result means it was successful.
The dwLogonType
parameter, which in this POC is set to the value of LOGON32_LOGON_NEW_CREDENTIALS
, is extremely important here. It sets your session up to use the same local context but use the new token for outbound connections.
The Cobalt Strike documentation explains this quite well:
If you pass this handle value to the ImpersonateLoggedOnUser
API, the current process thread can take on the characteristics of this token. In theory, this means that your current running process should be able to access networked resources for the given user.
In practice, it’s a bit more complicated. I tried adding this functionality to the OffensiveNotion agent and...
Even with no error in GetLastError, no dice. And I’m still working out the kinks. I put the call out to the community and a few outstanding red teamers that I look up to had some sage wisdom. Here’s the thread (hah) if you want to catch up:
The OffensiveNotion agent is a multi-threaded application, so it makes a lot of sense.
So I’m still working on implementing this method of token manipulation. More to follow!
Shout out to Ne0nd0g, RastaMouse, JFMaes, trickster0, and everyone else who gave me a few PTRs 😀
Failure is a Ladder
But it’s not a complete wash! Among all of the frustration, I did make a breakthrough:
🎙️ Introducing! The lean mean process making machine! The champion of the API, weighing in at a hefty 32 bytes, often imitated but never (token) duplicated, CreateProcessWithLogonW!!!!
This is a start. It’s not exactly how I envision the OffensiveNotion agent to do token duplication but it’s progress, damnit!
The function definition is a bit more complicated that LogonUserA:
BOOL CreateProcessWithLogonW(
[in] LPCWSTR lpUsername,
[in, optional] LPCWSTR lpDomain,
[in] LPCWSTR lpPassword,
[in] DWORD dwLogonFlags,
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
Notice that it never returns a handle to a new token. Instead, it makes a new process with the token of the provided credentials. And it spawns this process just as if you were interactively running it from the host.
So given a username, domain, and password, we can do the following:
println!("[*] Making token with the following credentials: {}\\{}:{}", domain, username, password);
let mut username_str: String = String::new();
let mut domain_str: String = String::new();
let mut password_str: String = String::new();
let mut command_str: String = String::new();
// Prep strings for UTF-16
username_str.push_str(username);
username_str.push_str("\0");
domain_str.push_str(domain);
domain_str.push_str("\0");
password_str.push_str(password);
password_str.push_str("\0");
command_str.push_str(command);
command_str.push_str("\0");
println!("[*] Encoding into UTF-16...");
let mut username_wstr: Vec<u16> = username_str.encode_utf16().collect();
let mut domain_wstr: Vec<u16> = domain_str.encode_utf16().collect();
let mut pass_wstr: Vec<u16> = password_str.encode_utf16().collect();
let mut cmd_ptr: Vec<u16> = command_str.encode_utf16().collect();
let bResult = CreateProcessWithLogonW(
PCWSTR(username_wstr.as_mut_ptr()),
PCWSTR(domain_wstr.as_mut_ptr()),
PCWSTR(pass_wstr.as_mut_ptr()),
LOGON_NETCREDENTIALS_ONLY,
PCWSTR(null_mut()),
PWSTR(cmd_ptr.as_mut_ptr()),
0,
null_mut(),
PCWSTR(null_mut()),
null_mut(),
null_mut(),
);
So more simply, we pass in the username values for the process and it spawns with our token:
I have a long way to go but this felt like a huge win. 😄
Here’s the code if you wanna take a look:
-Husky
— — — — — — > Back to Notes
🌐 Where You Can Find Me
🐦 Twitter | 📡 Main Blog | 👽 GitHub | 📺 YouTube
📒Recent Notes
8/30/22 Content Creators, I Will Teach You Cyber Jiu-Jitsu
8/12/22 The Responsible Red Teamer’s Manifesto
7/30/22 On Patching Binaries
7/16/22 MS-Interloper: On the Subject of Malicious MSIs
4/22/22 Failing All The Way To Token Manipulation, Part 1
4/16/22 COM Hijacking Creative Cloud