ADSelfService Plus, histoire d’une 0-day

Avant de rentrer dans le vif du sujet, quelques mots sur ADSelfService Plus.

ADSelfService Plus est une solution de gestion de mot de passe en libre-service dans un environnement Active Directory.

Ce logiciel aide les utilisateurs du domaine à effectuer en libre-service la réinitialisation de leur mot de passe ainsi que le déverrouillage de leur compte.

Ce logiciel est réputé et utilisé par un très grand nombre de clients de part le monde :

https://www.manageengine.com/products/self-service-password/customers.html?topMenu

Il était une fois…

un matin de septembre 2019 où j’ai voulu réinitialiser mon mot de passe à l’aide de ADSelfService Plus. Je l’avais déjà fait une fois et cela avait fonctionné sauf que ce jour là le serveur hébergeant la solution était down….

Au lieu d’obtenir une erreur Windows classique, j’avais eu une fenêtre d’erreur HTTP.

Tiens ? Cela m’a surpris, je pensais qu’il s’agissait d’une application avec une GUI Windows mais non je venais de comprendre qu’il s’agissait d’une application de type mini-browser web présentant des pages HTML. Cela m’a d’autant plus intrigué qu’en scrutant les processus Windows, j’avais noté qu’ADSelfService Plus tournait sous l’utilisateur Local System, intéressant.

Je me suis aussitôt dit que s’il existait une vulnérabilité sur ce logiciel, cela pourrait servir pour de l’élévation de privilèges.

Du coup j’ai regardé côté réseau quel nom de domaine et protocole cherchait à joindre le client ADSelfservice Plus. Un coup de netstat et c’était réglé, j’avais le FQDN avec le protocole, du HTTPS.

Avec ces éléments je me suis demandé comment allait réagir le client ADSelfService Plus si au lieu de lui présenter son véritable serveur je lui présentais un « fake » serveur ?

Rien de plus simple sous Linux et Python que de lancer un mini serveur Web. En revanche je me suis dit que j’allais avoir du mal à générer un certificat SSL valide identique à celui du vrai serveur… il détecterait forcément une anomalie.

Je me suis dit, tant pis, faisons un certificat SSL autosigné et on verra. Bien m’en a pris, vous allez comprendre pourquoi 🙂

Je passe sous silence le fait qu’il m’a aussi fallu lancé un fake DNS serveur zappant toutes les requêtes DNS sauf celle correspondant au serveur hébergeant ADSelfservice Plus ; le but étant de rediriger les « bonnes » requêtes DNS vers mon fake ADSelfService Web serveur.

Voila j’avais tout ce qui me fallait pour lancer un test.

Le pré-requis pour cette attaque était simplement d’avoir un accès physique à un PC équipé d’ADSelfService Plus.

  • Je débranche le câble réseau du PC victime
  • Je place un câble croisé entre ce PC et une bécane sous Kali
  • Sous Kali je lance mon fake DNS serveur et mon fake ADSelfService Plus Web Serveur
  • Sur le PC victime je demande à ADSelfService Plus de réinitialiser mon mot de passe
  • Sur Kali je vois les requêtes DNS passées et celles pour ADSelfService Plus partent bien vers mon fake Web serveur, super !
  • Paf, sur le PC victime le client ADSelfService Plus est berné mais me toque une erreur de certificat SSL. Normal me direz-vous, mince…

Et oui c’est normal cette erreur de certificat SSL car justement j’ai généré un certificat SSL auto-signé.

Et là je me suis dit…vu que le client ADSelfService tourne sous Local System, cette fenêtre d’alerte de certificat SSL, elle tourne aussi sous Local System, y aurait pas moyen de lancer un shell ?

Alors j’ai cliqué sur « Afficher le certificat », puis dans l’onglet « Détails » :

Qu’est-ce qu’on voit en bas de cet onglet « Détails » ?

« Copier dans un fichier… » et voila le tour est joué, il n’y avait plus qu’à lancer un invite de commandes dans la fenêtre « Enregistrer sous… » pour se retrouver dans un shell avec les droits « Local System » !


C’est dingue je me suis dit que non seulement je pouvais élever mes privilèges mais qu’au final c’était bien plus grave.

En effet cette vulnérabilité permettait de tout faire : créer une backdoor, insérer un malware, exfiltrer des données utilisateurs le tout sans même avoir besoin de s’authentifier sur le poste de travail.

Et oui le bonus avec ADSelfService Plus c’est qu’il est accessible directement sur la mire d’authentification !

Wahou !

C’était si facile que je me suis dit que cette vulnérabilité devait déjà être connue de l’éditeur, référencée en tant que CVE quelque part, que je n’avais pas la dernière version du logiciel, etc… et bien que nenni !

Je me suis donc rapproché de l’éditeur et comme c’est de plus en plus souvent le cas, il dispose d’un programme de Bug Bounty et j’ai ainsi déclaré un nouveau « bug » :

Au bout de quelques mois (heum…), l’éditeur m’a répondu, le 16 mars 2020 exactement, en me remerciant et en publiant une mise à jour, la build 5814 avec une petite mention sur cette vulnérabilité, « A vulnerability issue in the ADSelfService Plus login agent has been fixed » :

https://www.manageengine.com/products/self-service-password/release-notes.html

Moralité, je passe souvent un temps considérable à déterrer de nouvelles vulnérabilités mais parfois c’est si simple, il n’y a qu’à se pencher pour en trouver.

Fin

Windows – [TUTO] Exploitation d’un driver vulnérable [1]

On a vu dans les précédents posts [1] [2] que depuis l’espace Kernel il est possible (sous certaines conditions) d’élever ses privilèges utilisateurs en modifiant son propre Access Token. Dans la démo c’était facile car on disposait d’une VM en mode debug Kernel et on avait le contrôle total de la VM utilisateur mais dans la vrai vie c’est différent. Il va falloir trouver un autre moyen de se retrouver dans l’espace Kernel depuis l’espace utilisateur. Et même si (par bonheur) on y arrive il va falloir faire la même chose que ce qu’on avait fait sous WinDbg [1] mais cette-fois en programmation.

Première étape (et non des moindres), passer de l’espace utilisateur à l’espace Kernel

C’est un vaste sujet et après réflexion je me suis dis que le plus simple est de montrer l’exploitation d’un driver volontairement vulnérable. Je m’explique. Comme vous le savez les drivers sont chargés dans l’espace Kernel donc si on tombe sur un driver un peu buggé et qu’on arrive à le faire planter via un débordement mémoire (un beau Stack Overflow) et à y exécuter notre propre code alors on aura atteint notre premier objectif. Parfait mais plutôt que de prendre un vrai driver du commerce vulnérable (on verra ça dans un prochain post), on va commencer plus simple, par un driver volontairement buggé dont on dispose du code source. Cela tombe bien y a un gars qui a fait ça, il est disponible ici : https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

Je vous passe les détails de l’installation du driver, interressons-nous directement à la programmation. Quel est notre objectif ? Charger le driver, faire quelques appels et si possible le faire planter. Pour charger le driver, cela paraît bête mais il faut connaître son nom, attention on ne parle pas du nom du fichier mais de son petit nom connu par le système. Y a plusieurs façon de le récupérer mais un moyen simple est d’utiliser un outil de Sysinternals bien connu, WinObj. Voici par exemple le nom du driver vulnérable que l’on cherche à charger :

On obtient le même résultat avec un autre outil, Dos Device Inspector :

Bon maintenant qu’on sait que le driver s’appelle HackSysExtremeVulnerableDriver il va falloir qu’on s’interesse aux IOCTL. Hein ? Ben oui il faut bien qu’on puisse interagir avec le driver, faire des appels pour qu’il fasse des choses, et bien cela passe justement par les IOCTL. En fait c’est simple. Côté driver, il y a un point d’entrée responsable de la réception des appels clients et du dispatch en fonction du code de contrôle envoyé par le client. C’est la fonction Windows DeviceIoControl qui nous permet de communiquer avec le driver, https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol

Ok, on a compris qu’avec cette fonction on va pouvoir faire des appels au driver mais comment on sait quel « Control Code » on pourra envoyer au driver ? C’est là où ça se corse, dans la vrai vie on n’a pas accès au code source. Il faudra donc faire autrement via du reverse engineering, via un monitoring des appels, et j’en passe mais dans notre cas d’école c’est plus simple car on dispose du code source. Tous les codes de contrôle sont définis dans le fichier hevd_common.h, ils commencent à 0x800 et se terminent à 0x80D.

Avec le nom du driver et les codes de contrôle on a ce qui faut pour démarrer la programmation. On va charger le driver puis s’amuser à lui envoyer des chaînes de caractères de plus en longues pour finalement le faire planter (au passage ça s’appelle du fuzzing).

Vu qu’on sait que le driver est vulnérable, à un moment il va planter, mais comme on est dans l’espace Kernel on aura droit à un bel écran bleu. Le mieux est de repartir sur la plateforme vue dans le premier post avec les 2 VM, l’une étant le débugger et la seconde le debuggee. Ce n’est pas anodin, on aura accès à la mémoire, à la stack et aux registres AVANT le crash.

Ok, allons-y pour le code. Commençons par charger le driver. Je vous renvoie à la doc Microsoft https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-ms-dos-device-names qui dit ceci :

To access the DosDevices namespace from user mode, specify \\.\ when you open a file name. You can open a corresponding device in user mode by calling CreateFile().

OK mais comme on est en langage C et qu’il faut doubler les antislashs « \ » il faut coder quelque chose comme ceci :

HANDLE device = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
         GENERIC_READ | GENERIC_WRITE,
         NULL,
         NULL,
         OPEN_EXISTING,
         NULL,
         NULL
     );

Ok maintenant on va écrire la fonction qui envoie des buffers de données au driver. Comme dit plus haut on va utiliser la fonction DeviceIoControl. Regardons d’un peu plus près les paramètres de cette fonction, https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol

 BOOL DeviceIoControl(
  HANDLE       hDevice,
  DWORD        dwIoControlCode,
  LPVOID       lpInBuffer,
  DWORD        nInBufferSize,
  LPVOID       lpOutBuffer,
  DWORD        nOutBufferSize,
  LPDWORD      lpBytesReturned,
  LPOVERLAPPED lpOverlapped
); 
  • hDevice : c’est facile, on passe le handle récupéré lors du chargement du driver
  • dwIoControlCode : c’est le code de contrôle à envoyer. On a vu que pour notre driver vulnérable ils démarrent à 0x800, prenons celui-là il correspond à HACKSYS_EVD_IOCTL_STACK_OVERFLOW ( hevd_common.h )
  • lpInBuffer : c’est le fameux buffer de données à envoyer au driver. Traditionnellement on envoie une longue chaîne de ‘A’ pour voir lors du plantage si on trouve bien plein de ‘A’ sur la stack et sur le registre d’instruction.
  • nInBufferSize : la taille du buffer d’entrée
  • lpOutBuffer : inutilisé dans notre exemple de fuzzing
  • nOutBufferSize : inutilisé dans notre exemple de fuzzing
  • lpBytesReturned : inutilisé dans notre exemple de fuzzing
  • lpOverlapped : inutilisé dans notre exemple de fuzzing

Bon on a tout ce qu’il faut pour écrire un code qui charge le driver et envoie un buffer remplit de ‘A’. Ce qui est intéressant c’est de faire varier la taille du buffer jusqu’au plantage. Bon sans plus tarder voici le code source. Plutôt que de réinventer la roue, je vous met le code de la déesse du reverse engineering, j’ai nommé hasherezade sans qui j’en saurai beaucoup moins sur les malwares, le kernel et un tas d’autres sujets, voila c’est dit.

Pour les pressés, la vidéo de démonstration est juste après le code source.

#include <stdio.h>
#include <windows.h>
#include "hevd_comm.h"
#include "util.h"

HANDLE open_device(const char* device_name)
{
    HANDLE device = CreateFileA(device_name,
        GENERIC_READ | GENERIC_WRITE,
        NULL,
        NULL,
        OPEN_EXISTING,
        NULL,
        NULL
    );
    return device;
}

void close_device(HANDLE device)
{
    CloseHandle(device);
}

BOOL send_ioctl(HANDLE device, DWORD ioctl_code, DWORD bufSize)
{
    //prepare input buffer:
    PUCHAR inBuffer = (PUCHAR) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);

    if (!inBuffer) {
        printf("[-] Alloc failed!\n");
        return FALSE;
    }
    //fill the buffer with some content:
    RtlFillMemory(inBuffer, bufSize, 'A');

    DWORD size_returned = 0;

    printf("Sending IOCTL: %#x\n", ioctl_code);
    BOOL is_ok = DeviceIoControl(device,
        ioctl_code,
        inBuffer,
        bufSize,
        NULL, //outBuffer -> None
        0, //outBuffer size -> 0
        &size_returned,
        NULL
    );
    //release the input bufffer:
    HeapFree(GetProcessHeap(), 0, (LPVOID)inBuffer);
    return is_ok;
}

int main(int argc, char *argv[])
{
    HANDLE dev = open_device(kDevName);
    if (dev == INVALID_HANDLE_VALUE) {
        printf("Cannot open the device! Is the HEVD installed?\n");
        system("pause");
        return -1;
    }
    printf("Device opened!\n");
    DWORD index = 0;
    print_info();

    while (true) {
        printf("Choose IOCTL index: ");
        scanf("%d", &index);
        DWORD ioctl_code = index_to_ioctl_code(index);
        if (ioctl_code == -1) {
            print_info();
            continue;
        }
        printf("Supply buffer size (hex): ");
        DWORD bufSize = 0;
        scanf("%X", &bufSize);
        BOOL status = send_ioctl(dev, ioctl_code, bufSize);
        printf("IOCTL returned status: %x\n", status);
        printf("***\n");
    }
    close_device(dev);
    system("pause");
    return 0;
}

Bon allons tester ce code et faisons crasher le driver. Plutôt qu’un long discours, voici une vidéo de démonstration avec des petits commentaires :

Ceci conclut la première partie de ce tuto. Nous verrons dans le prochain post comment exploiter le crash en codant un exploit qui nous donnera les privilèges système.

Windows et les privilèges – Structure d’un Access Token

On a vu dans le précédent post qu’il est possible d’élever ses privilèges et de devenir administrateur local en copiant depuis l’espace Kernel le pointeur du Token System vers le pointeur du Token d’un invite de commandes. Alors oui c’était facile car nous étions en mode debug Kernel et tout se faisait à la main en Windbg, néanmoins cela a posé certaines bases. Nous verrons dans un prochain post comment exploiter un driver vulnérable à de l’écriture arbitraire pour élever ses privilèges selon une technique assez proche.

En attendant attardons-nous un peu sur ces Token, plus précisément les Windows Access Token, que sont-ils ?

Un Windows Access Token est un objet qui décrit le contexte de sécurité d’un processus ou d’une thread. Il est généré par le système lors du logon. Il est utilisé lorsqu’un processus ou une thread interagit avec un objet qui dispose d’un descripteur de sécurité, un « securable object ».

Un Windows Access Token contient pas mal d’attributs, dont :

  • le SID du propriétaire
  • les SID des groupes dont il est membre
  • la liste de ses privilèges
  • le type de Token (Primary ou Impersonation)

Une fois que le Token est créé il n’est pas possible d’ajouter de nouveaux privilèges, il est seulement possible d’activer ou de désactiver les privilèges déjà existant.

Mais qu’est-ce qu’un privilège ?

Un privilège est un droit attribué à un utilisateur ou un à un groupe afin d’effectuer diverses opérations sur le système comme éteindre la machine, chargé des drivers, ou changer l’heure.

Certains utilisateurs et groupes ont des privilèges prédéfinis comme les administrateurs locaux, les membres du groupe Backup ou Printer Operators.

Les privilèges sont aussi administrables à travers les stratégies locales (gpedit.msc) dans la section « Attribution des droits utilisateurs ».

La commande whoami /priv liste vos privilèges.

Pour observer la structure mémoire d’un Acces Token et des privilèges, retournons dans Windbg.

Examinons un processus cmd.exe avec la commande !process. On voit que son Access Token est référencé à l’adresse 972c0a28 :

Affichons maintenant le contenu du Token avec la commande !token. On trouve le SID de l’utilisateur ainsi que les SID des groupes dont il est membre. Enfin plus bas on trouve bien la liste des privilèges, ceux que l’on retrouve avec la commande whoami /priv

Si on regarde en mémoire, les privilèges sont représentés par une valeur « Present » où chaque bit correspond à un privilège. Noté également la valeur « Enabled » qui est la liste des privilèges activés (bit à 1) ou pas (bit à 0).