[Windows] Un petit bypass simple de l’UAC

L’UAC (User Account Control), 3 lettres pour désigner le système de contrôle d’accès introduit par Microsoft depuis Windows Vista.

Le but est d’alerter l’utilisateur lorsqu’une action nécessite des changements sur le système avec des privilèges plus élevés.

Exemple, un utilisateur administrateur de son poste souhaite lancer une invite de commandes en tant qu’administrateur :

Bien qu’étant administrateur de son poste, l’UAC se déclenche :

L’UAC embête certains utilisateurs (mais c’est pour leur bien !), les pentesters mais gêne surtout les auteurs de malwares car s’ils échouent à bypasser l’UAC, ils se dévoilent tout simplement.

C’est pourquoi beaucoup de techniques ont vu le jour pour bypasser l’UAC.

Une grande partie de ces techniques sont recensées sur ce GitHub :

https://github.com/hfiref0x/UACME

On s’aperçoit que pas mal de failles ont été corrigées par Microsoft mais d’autres toujours pas…

En fait pour Microsoft ce n’est pas vraiment un problème de sécurité car il ne s’agit pas d’une élévation de privilèges mais d’un bypass d’un avertissement de sécurité.

Chacun a son avis sur le sujet mais pour ma part je pense qu’il s’agit de vulnérabilités sérieuses et Microsoft devrait les corriger car elles sont largement utilisées par les malwares.

La société Lexfo en a parlé d’ailleurs récemment dans un post lié au ransomware « Lockbit » (très intéressant au passage) :

https://blog.lexfo.fr/lockbit-malware.html

A mon tour d’ajouter une petite pierre à l’édifice, j’ai en effet trouvé un petit bypass simple de l’UAC 🙂

Avant d’entrer dans le vif du sujet, un peu d’historique.

Les alertes de l’UAC étaient si fréquentes dans Vista que depuis Windows 7 et après de nombreuses plaintes, Microsoft a fait quelques concessions.

En effet certains binaires signés par Microsoft (ainsi qu’une liste d’objets COM) ne déclenchent pas d’alerte de l’UAC, on peut considérer cela comme une sorte de whitelist.

Quels sont ces binaires ?

Il suffit de rechercher les exécutables qui ont l’attribut « autoElevate » à « true » dans leur fichier manifest.xml :

sigcheck.exe -m c:\windows\system32\*.exe

Ces binaires sont intéressant car la moindre vulnérabilité permet d’ouvrir une brèche sur l’UAC.

Alors, let’s go !

Prérequis :

  • UAC par défaut
  • Utilisateur membre du groupe « Administrateurs » local.

On lance la commande lpksetup depuis une invite de commandes utilisateur.

L’exécutable « autoelève » ses privilèges mais pour les raisons citées précédemment l’UAC ne se déclenche pas :

On confirme avec Process Hacker que ce process dispose bien des privilèges élevés (Elevated = Yes et on a bien toute une liste de privilèges) :

S’agissant d’une application graphique, je me suis dit qu’il y a peut-être moyen de lancer un shell depuis l’interface et si c’est le cas cela bypasserait automatiquement l’UAC.

Et c’est le cas, après avoir lancé lpksetup allez dans « Installer ou désintaller des langues », cliquez sur « Parcourir » , sélectionnez un dossier où vous avez des droits en écriture, puis faites bouton droit, Propriétés.

Dans l’onglet « Personnaliser », cliquez sur « Choisir un fichier »

Dans la boite de dialogue saisir « cmd.exe » à la place du chemin du dossier et cliquez sur « Entrer » :

Et voila on obtient une invite de commandes Administrateur sans avoir rencontré l’UAC !

Le bypass manuel c’est bien mais le bypass automatique c’est mieux.

En analysant l’exécution de lpksetup.exe au travers de ProcMon, on s’aperçoit qu’il est vulnérable à du « DLL Hijacking » (« NAME NOT FOUND »)

Il suffit de créer une DLL contenant un « ShellExecute cmd.exe » qu’on nomme oci.dll et le tout est joué :

Pour ce coup là je ne suis pas le premier à avoir vu ce DLL Hijacking, mais Microsoft n’a toujours pas corrigé.

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.