Pragyan CTF 2022
I spend my Sunday doing Pragyan CTF 2022. I’m very grateful that I got 17th place even though I play it solo. Below is my writeup for challenges that I solved and don’t forget to follow me on Twitter 🙂
Pwn
Database
Intro
We were given a binary, let’s decompile it. Below is the main function.
|
|
insert_item
|
|
delete_item
|
|
update_item
|
|
leave
|
|
There is also secret function which will print the flag
|
|
Let’s checksec the binary:
|
|
Okay, PIE is enabled
Analysis
main
|
|
Reading through the code, I notice that the binary leak us the main address, which mean the PIE mitigation isn’t matter at all. We can calculate the address of available functions, plt, got, etc.
insert_item
Observation on the insert_item
method, we can control the malloc chunk size.
delete_item
Observation on the delete_item
method, the pointer was nulled after free, so we couldn’t do UAF or Double Free.
update_item
|
|
Observation on the update_item
method, we can see that there is heap-overflow. Notice that there isn’t any check on the item’s size during update. If we put larger size during update than the allocated chunk size, we can overwrite the other chunks value.
For example, let say that we insert_item
three times with size 0x10
. The chunk will be like below.
|
|
What if we set bigger size during update_item
? Then we can overwrite the other chunks which located next to each other.
What happen if we free the chunks? It will be put to tcache
bins, and then it will maintained a linked-list of the cache. Let say that we free the second and third chunk, the heap will be like below.
|
|
To give a better visualization, below is the current state of the tcache linked_list
|
|
As you can see on address 0x5555556022c0
, the tcache stored a pointer to the next cache, so that when we do malloc(0x10)
, tcache will remove its curent head and move the head to its next pointer
|
|
What happen if we use the heap-overflow to overwrite the tcache pointer? For example, imagine that the current heap chunk is like below
|
|
With update_item
, let say we want to read 0x28
string, and we input 0x6161616161616161+0x6161616161616161+0x0000000000000000+0x0000000000000021+0xdeadbeefdeadbabe
The chunk after that update will be like below:
|
|
As you can see, we can overwrite the tcache next pointer. That means we can control the allocated chunk address during calling the malloc. Take the above example, if we call malloc(0x10)
twice, the second malloc chunk address will be stored at 0xdeadbeefdeadbabe
This is the bug that we can use to solve the challenge.
Solution
My idea is to overwrite the puts
GOT value to secret
address with the heap-overflow bug, so that when we call leave
, the puts
will print the flag. To achieve that, my plan is:
- Create three items with size 0x10 via
insert_item
- Free the second and third element via
delete_item
- Heap overflow via
update_item
on the first item, with goals to replace the tcache next pointer toputs
GOT address - Create one item with size 0x10 via
insert_item
. - Create one more item with size 0x10 and the stored string is the
secret
address viainsert_item
. This item will be stored atputs
GOT address, and because the string issecret
address, now, whenever the binary callputs
, it will be resolved tosecret
instead. - Call
leave
and it will print the flag.
Below is the solver:
|
|
|
|
Flag: p_ctf{Ch4Ng3_1T_t0_M4x1Mum}
Poly-Flow
Intro
We were given a binary, let’s decompile it. Below is the main function
|
|
Check function
|
|
input function
|
|
Analysis
Okay, so the flow is:
- Need to pass
check
function, where we need to pass a string, where if it get splitted into 4 chunks where a chunk consist of 4 bytes, the sum of the 4 chunks is0xdeadbeef
. - After that we will go to
input
method. Notice that there is a buffer overflow, where we can replace the return value ofinput
Solution
So, below is the solver script, where we simply:
- Pass a string that the 4 chunks sum is
0xdeadbeef
- And then with BOF, we can ret to the input 5 times, so that the binary will cat the flag.
|
|
Flag: p_ctf{mUlT1Pl3_BuFf3R_Ov3rF|0w}
Portal
Intro
We were given a binary. Below is the main decompiled:
|
|
see_balance:
|
|
init_pack:
|
|
upgrade_pack
|
|
Analysis
Reading through the decompiled code, some notes that we could take:
- Reading through the
see_balance
method, we notice bug where format string attack is applicable. - In order to call
upgrade_pack
method, in theinit_pack
method, we need to make the global variable ofb
value to0xf9
. - Reading through the
upgrade_pack
method, we notice bug where format string attack is applicable. - Flag was stored in the
upgrade_pack
method stack.
Let’s do checksec to check further mitigation on the binary.
|
|
Okay so the PIE is enabled, which mean we might need to leak the binary base address first.
Solution
Based on those notes, my idea to solve this is:
- With the format string bug on
see_balance
, I will try to leak themain
address, so that I can retrieve the binary base address - After that, with the same bug, I will overwrite the value
b
with0xf9
, so that I can go to theupgrade_pack
method. - With the format string bug on
upgrade_pack
method, because theflag
was stored in the stack, I can simply leak the flag.
Below is the full script of that
|
|
Flag: p_ctf{W3ll_1t_W4s_3aSy_0n1y}
Web
PHP Train
We were given a website with the below source code
|
|
So we need to bypass all this check to retrieve the full flag.
Flag 1
To bypass strcmp, we can simply pass param1[]=a
. We got p_ctf{ech0_
.
Flag 2
To bypass sha1 collision, we can simply pass array to both params (param2[]=a¶m3[]=b
). We got 1f_7h3_7r41n_
.
Flag 3
We just need to pass param4=1200
becaue 1.2e3
is equals to 1200
. We got d035_n07_
.
Flag 4
We just need to pass param5=89%20
because the value checking is using loose comparison, so the whitespace will be discarded. We got 5t0p_1n_y0ur_
.
Flag 5
We just need to pass magic hash as param6=gH0nAdHk
, where the hash result prefix is 0e
because of the loose comparison. We got 5t4t10n_7h3n_
.
Flag 6
We just need to pass param7=hellohelloworldworld
, so that after the preg_replace
, the param7
value will be helloworld
. We got 1t5_n07_
.
Flag 7
We just need to pass param8=1.env
because in_array
use loose comparison, so as long as our first string start with 1
, in_array
will return true. We got y0ur_7r41n}
.
Flag: p_ctf{ech0_1f_7h3_7r41n_d035_n07_5t0p_1n_y0ur_5t4t10n_7h3n_1t5_n07_y0ur_7r41n}
Inception
We were given a website. Checking the source code, we got obfuscated javascript. Now, we just need to examine properly the obfuscated. The first part is this code
|
|
Basically, we don’t need to understand this code at all. We just need to run it in the javascript console, so that we can get the _0x31e3x2
value. Running it in the console, we can see that the value of _0x31e3x2
is p_ctf{INfjnity5
.
Moving to the other code,
|
|
Running it in the js console, we can see that the hint
consist of base64 string. Try to decode each of it will gave us the part 2 of the flag. We got _b3g1n5_w1th_4n_
Now, the last part, if we check _0xd4d0[304]
value, we will get another javascript code.
|
|
Basically, to get the last part of the flag, we just need to calculate user[_0xfd39[2]](i)-(i* 10)-DontChange[i]
.
We got the flag :D
Flag: p_ctf{INfjnity5_b3g1n5_w1th_4n_1nc3pt10n}
Code of Chaos
We were given a website, where we need to bypass the login page. Checking the robots.txt, we found the source code of the website.
|
|
Okay, so the requirement is, somehow, we need to login with user michael and pass whatever, but we aren’t allowed to pass michael
. And then, the uppercase of our user username should be MICHAEL
.
How to achieve that? The answer is to use UNICODE. Some unicode will be converted to not-UNICODE during converting it to uppercase. In this case, I use ı
unicode, so that the username is mıchael
. The upcase result will be MICHAEL
, so we can safely login to our website.
Okay now we need to get the admin privilege to find the rest of the flag. Checking the cookie, it contains jwt token, which the decrypted result is like below
We can try to bypass this by changing the alg to none
and change the user
value to admin
. Generating the forged jwt token, we get the second flag.
Forged jwt: eyJ0eXAiOiAiSldTIiwgImFsZyI6ICJub25lIn0.eyJ1c2VyIjogImFkbWluIn0.
Flag: p_ctf{un1c0de_4nd_j3t_m4kes_fu7}
Crypto
One Try
We were given a file like below
|
|
Reading through the file, we know that we need to know the public exponent value of the RSA Encryption (which is k
). We can derive it from the encrypt
method. Reading through it, we can simply solve it with z3
to retrieve our k
. After getting the k
, we can simply do RSA decryption to retrieve the flag. I use sagemath
to solve it.
|
|
Flag: p_ctf{0ne_T1m3_Pads_are_1ns3cur5}
Blind Scout
We were given base64 of encrypted text, and 5 pem file. I try to check whether there is common factor between the modulus of each file, and I found a common modulus between the second and the fourth pem, which mean now we can factor the second and the fourth pem. Trying to decrypt the encrypted with the second pem gave me the result.
|
|
Because the challenge name is Blind, I assume that the binary is braille. We will get the correct flag after decrypting the binary with braille
Flag: p_ctf{I_AM_DAREDEVIL}
Rev
Oak
We were given a file Oak.class
. We can decompile it and it will give us this result
|
|
Basically, what it do is it will compare our converted flag value with Oak.data ^ t(i*i)
. t(i*i
value can be easily generated (Just rewrite the t
function on python), so we know the value of Oak.data ^ t(i*i)
. Now, let’s move to the conv
function.
Basically, what it do is only flag[i] << 8 + flag[i+1]
. And because we know the first char should be p
(Because the flag is started with p_ctf
), we can iteratively recover the flag. Below is the solver script
|
|
Flag: p_ctf{0r1g1n4|_n@M3-0f_J4vA_Wa5()/|<}
Social Media
Follow me on twitter