Introduction
I participated in the DeadFace CTF 2024 with two colleagues Marono and Seadyot. In this post, I will share our solutions for two challenges: one focused on reverse engineering, where we analyzed a binary to retrieve a flag, and the other centered on PHP-type juggling and magic hashes.
Marono also wrote some write-ups referenced at the end of this post.
Cereal Killer 03
In this challenge we were given an unstripped binary file that still contains debugging information which makes it easier to analyze and understand how it works.
┌──(kali㉿kali)-[~/Desktop/deadface/cerealkiller03/lin]
└─$ file ck-2024-re06
ck-2024-re06: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=3a584e566d651a96b054087f3ba2f4d9393df43d, for GNU/Linux 3.2.0, not stripped
┌──(kali㉿kali)-[~/Desktop/deadface/cerealkiller03/lin]
└─$ ./ck-2024-re06
This year, America's politicians are weighing in on the IMPORTANT issues...!
As in, which spooky cereal is best?
President Biden has a favorite spooky cereal. Tear apart this
binary and see if you can figure out what it is!
(Those of you without Reverse Engineering skills will just
have to GUESS the password, which you won't do in
100 million years!!!)
Please enter the password: thisisatest
ACCESS DENIED!!!
When we open the file in Ghidra, we immediately see the main()
function. Here, we notice several hardcoded hexadecimal values, along with the messages that appear on the screen when the binary is executed. Additionally, there are references to MD5 and RC4 algorithms, as shown in the following image.
The code is pretty straightforward, and we can begin renaming variables to make it more understandable.
First, the function takes user input and trims it. It then initializes an MD5 context
, with memory allocated at the beginning. Next, the input is copied into the allocated memory buffer within the MD5 context
. Afterwards, the MD5_Final
function is called, and the resulting hash gets stored in a variable named password_hash
. This hash is then compared to a hardcoded key_hash
. The memcmp()
compares 16 bytes, which corresponds to the size of an MD5 hash. If the comparison is successful, the code proceeds to an RC4 operation using a Hardcoded RC4 key
and the password_hash
.
In the end, the flag gets printed.
Looking at it, we notice that the RC4 function uses the password hash derived from our input, meaning that at the time of the RC4 call, this hash will match the key hash. We can patch the binary to bypass the conditional if statement and directly use the hardcoded key_hash in the RC4 operation.
Starting by patching the if statement, it originally looked something like this:
We modify it to this:
Before patching the RC4 call, we need to confirm the address of the key_hash in the assembly code. It can be identified by examining the memcmp()
function, which references the key_hash for comparison.
So, our RC4 function originally looked like this:
We adjusted it to this:
We can now export this patched file from Ghidra and execute it to retrieve the flag: flag{0Bama-C0unts-in-the-shad0wz!}
.
┌──(kali㉿kali)-[~/Desktop/deadface/cerealkiller03/lin]
└─$ ./ck-2024-re06-patched
This year, America's politicians are weighing in on the IMPORTANT issues...!
As in, which spooky cereal is best?
President Biden has a favorite spooky cereal. Tear apart this
binary and see if you can figure out what it is!
(Those of you without Reverse Engineering skills will just
have to GUESS the password, which you won't do in
100 million years!!!)
Please enter the password: doesthisneedstobecorrect?
TOP SEEKWET ACCESS GRANTED, MR. PRESIDENT!!!
Here is your NUKULAR CODE! (Note: It only works ONCE!) Have a nice breakfast, sir!
*********** KABOOM!!! ***********
flag{0Bama-C0unts-in-the-shad0wz!}
*********** KABOOM!!! ***********
Juggling Too Many Tasks
To save space, some code snippets may not include the full original code, but they should provide enough context for understanding.
For this challenge, we begin with the GitHub repository containing the source code for the API we interact with. We know the API runs on PHP version 7.4.33
[Can be verified when interacting with the API or through Wappalyzer].
The API code first retrieves two parameters we control, codeA
and codeB
. It then reads the contents of the secrets.txt
file and appends this data to codeA
, generating an MD5 hash
from the resulting string.
$str = isset($_GET['codeA']) ? $_GET['codeA'] : '';
$b = isset($_GET['codeB']) ? $_GET['codeB'] : '';
$secretFile = '/var/www/secret.txt';
$secretContents = trim(file_get_contents($secretFile));
$str .= $secretContents;
$a = md5($str);
Next, the code undergoes three regex checks:
- The first check verifies whether the beginning of the concatenated string [codeA + secret] corresponds to a hexadecimal string with 32 characters.
- The second check confirms if the beginning of codeB is a hexadecimal string with 32 characters.
- The third check ensures that the MD5 hash starts with a digit.
if (!(preg_match('/^[0-9a-fA-F]{32}/',$str)) || !(preg_match('/[0-9a-fA-F]{32}/', $b)) || preg_match('/^[a-fA-f]/', $a))
{
print "Sorry, Charlie... no GOLDEN TICKET for YOU!!! :( <br/>";
exit();
}
Finally, the code converts codeB
from hexadecimal to decimal and performs a loose comparison between the values of a
and b
. This immediately reveals two security issues: Type Juggling and Magic Hashes.
Type Juggling refers to the dynamic conversion of data types during comparisons, which can lead to different scenarios(e.g., “-1” equals -1).
Magic Hashes are hashes that usually start with a 0 in PHP, which the language interprets as representing the floating-point number 0 raised to the power of X.
$b = hexdec($_GET['codeB']);
if ($a == $b)
{
$goldenTicketFile = '/var/www/goldenticket.txt';
$goldenTicketFileContents = trim(file_get_contents($goldenTicketFile));
print "<H1>YOU WIN!!!!</H1><br/>\n";
print "<img src='charlie.jpeg'/><br/>\n";
print "Matched Hash == $a<br/>\n";
print "I got a GOLDEN TICKET... NAH NAH NAH NAH NAH NUH NAH NAH!!!</br>\n";
print "<pre>$goldenTicketFileContents</pre><br/>";
}
else
{
print "Sorry, Charlie... no GOLDEN TICKET for YOU!!! :( Better luck next time!<br/>";
}
Knowing this, we can conclude that our codeB must be 00000000000000000000000000000000
, as we want our hash to convert to 0 and match this value. Next, we need to brute-force a value for codeA that, when concatenated with the contents of the secret, starts with 0. This leads us to the following value for codeA: 0000000000000000000000000000000000000000024518873
.
With this, we can send the following request:
GET /lytton-labs-sweepstakes/goldenticketcheck.php?
codeA=0000000000000000000000000000000000000000024518873
&codeB=00000000000000000000000000000000
And we get our flag flag{CK06c_Thats-What-You-Get-For-Juggling-Too-Many-PHP-Tasks!!!}
as seen in following image.
It’s worth noting that we did not create a brute-force script because we were testing some suppositions on the request and found a valid hash without needing to create a script for this.
Additional Writeups
On this GitHub page, Marono published write-ups for the following challenges:
- Logical Left and Rational Right
- Social Pressure
- Discrete Logging
- Drink up!
- Sleeping (Marble) Beauty
- Let me in
- Yolanda
- Compromised Data
Conclusion
This was a fun and unique CTF, with storylines that made it both enjoyable and occasionally unpredictable.
Best regards, Diogo