Ciberseg’s 2024 CTF Writeup
This post provides a basic writeup of the different challenges we managed to overcome at Ciberseg’s 2024 CTF. In order to make everything a bit (or much) more interesting and fun we partnered up with David Carrascal to spend quite a pleasant evening :P
In this CTF flags will be presented as flag{foo}
, were foo
can be any arbitrary set of characters. It’s also possible to come across so called
‘flase’ flags which are always flag{}
. As the name implies, these flags are not the ones we’re actually looking for…
A little note on manpages
We believe one of the best (if not the best altogether) source of information regarding *NIX commands are manpages. You’ll usually find references
to a command’s (i.e. foo
) manpage as foo(N)
, where N
denotes the manual section we are referring to. We’ll follow that very same convention.
You can consult these manpages locally through man
(i.e. by running man foo
, for instance) or by browsing through the
man-pages project. You’ll usually get the manual section you want right out of
the box, but you can explicitly state the section by invoking man
as man N foo
, where N
is a section number as explained
in man man
.
A note on the output of commands
We’re running all the commands detailed in the following sections on macOS. Unlike Linux-based machines, macOS’ kernel is Darwin, and the userland lies closer to *BSD than traditional Linux distributions. This explains why command outputs may differ slightly, but the main idea will always be the same no matter the system you’re running on.
Finally, note that commands you’re to run at a prompt will be introduced by a dollar sign ($
). If you need to run something with elevated
privileges the prompt will be #
instead.
Enough conventions! Let’s get to the fin part 😼
Labyrinth
In this challenge we needed to extract the flag from a file called maze.zip
. As you never know what to expect on CTFs, we first
ran the file through, well, file(1)
:
$ file maze.zip
maze.zip: Zip archive data, at least v2.0 to extract, compression method=store
Everything looks okay for now, so the next step would be to unzip the file with, well, unzip(1)
(as you can see command names do not tend to be
very original):
$unzip maze.zip
Archive: maze.zip
creating: maze/
creating: maze/1riqsmU2/
creating: maze/1riqsmU2/9eblzuuc/
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/4C8NvkZG/
extracting: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/4C8NvkZG/flag.txt
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/6JuNhVWR/
extracting: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/6JuNhVWR/flag.txt
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/AcElgR02/
extracting: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/AcElgR02/flag.txt
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/EGaE7LQT/
extracting: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/EGaE7LQT/flag.txt
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/HgpQ0rBV/
extracting: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/HgpQ0rBV/flag.txt
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/J2B5MzHS/
extracting: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/J2B5MzHS/flag.txt
creating: maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/QkXTcwS3/
[...]
The output’s been truncated at [...]
because it’s quite long… The bottom line is the file is generating a convoluted directory tree with some
text files named flag.txt
at the leaves. As you can imagine, all of these flag.txt
files but one contain false flags. You can check that
by running `cat maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/4C8NvkZG/flag.txt
$ cat maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/4C8NvkZG/flag.txt
flag{}
We then need to somehow search for the valid flag across the tree: this is where find(1)
comes in extra handy! Find will look for a file recursively
based on a series of parameters supplied to it. As all the false flags are contained in files called flag.txt
we can’t really search based on
the filename: we need some other criteria to find the actual valid flag. We can work with the fact that false flags will always be flag{}
: it means
valid ones will be always longer! Let’s see how many characters make up a false flag (we could count them but hey, we like wc(1)
okay?):
$ echo "flag{}" | wc -c
7
The above is actually not true: false flags don’t include the trailing newline (i.e. \n
), so the actual character count is 6
. We could
have also use echo(1)
’s -n
flag to avoid including the trailing newline, but who wants to skip a learning opportunity? Anyway, you can also
check the actual size with ls(1)
(look for the 6
corresponding to the actual size in bytes):
$ ls -l maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/4C8NvkZG/flag.txt
-rw-r--r--@ 1 collado staff 6 23 Jan 13:02 maze/1riqsmU2/9eblzuuc/0RhP5SDi/84svsjey/4C8NvkZG/flag.txt
Anyway, given it’s likely the flag won’t be a single character long we can just assume it’ll be 8
bytes long at least. This let’s us find the flag
with find
:
$ find maze -name "flag.txt" -size +8c
maze/K8gbEW40/Y6LmHlWV/gaQUqalY/7Jeqsnkm/QRgEbX9H/flag.txt
The above will basically begin looking recursively from the maze
directory for files named flag.txt
that are at least 8
characters
(i.e. bytes) long. It returns, as expected, a single match. We can just cat(1)
it to get our first flag:
$ cat maze/K8gbEW40/Y6LmHlWV/gaQUqalY/7Jeqsnkm/QRgEbX9H/flag.txt
flag{m1n0t4ur}
Bonus solution
We actually didn’t solve this challenge following the precedure detailed above. To be honest, we’re running a bit low on disk sapce, so we didn’t want
to unzip the entire thing (the maze
directory takes up about 700 MiB
). As the flags don’t look to be encrypted, chances are we can just awk(1)
the raw contents of the file interpreted as ASCII characters. We can use xxd(1)
to display the raw contents of the files as hexadecimal values together
with the derived textual representation. We won’t get probably get the flag on the first try, but we might spot something to begin out search with.
$ xxd maze.zip | awk '/.*flag{[^}].*/'
00567fc0: 7478 7466 6c61 677b 6d31 6e30 7434 7572 txtflag{m1n0t4ur
The above shows how at index 0x567fc0
we find the text flag{m1n0t4ur
. To get that we just run the raw output of xxd
through awk
, where we match
on lines adhering to a regular expression that can roughly be translated into ’look for lines containing flag{
and where the next character is not
}
’ (i.e. ignore false flags). You can run over to Regex 101 to play around with the regexp if you like! Anyway, we can just check
to see if we’re missing a part of the flag by grep(1)
ping for the file index:
$ xxd maze.zip | grep -C 1 00567fc0
00567fb0: 6d2f 5152 6745 6258 3948 2f66 6c61 672e m/QRgEbX9H/flag.
00567fc0: 7478 7466 6c61 677b 6d31 6e30 7434 7572 txtflag{m1n0t4ur
00567fd0: 7d0a 504b 0304 1403 0000 0000 4168 3758 }.PK........Ah7X
We can see how we were lucky enough to get the entire flag on a single line! Bottom line: we got the flag with no need for decompressing the file.
Agent
In this challenge we were given a URI for an HTTP server. The server’s down already, so we’ll just assume it’s IPv4 address was 1.2.3.4
as an example.
At any rate, the challenge pointed you to http://1.2.3.4:9090
. When accessing the site you just got the picture of a cartoon duck dressed as a hard
boiled detective and/or a spy: depends on your preferences.
Given the name of the challenge and the fact that a duck picture was being returned David was quick to discover that the key of the challenge lied on
the HTTP client’s announced User-Agent
and that the user agent one had
to use was DuckDuckGo’s.
Somewhat recently, DuckDuckGo’s made a browser available to the public. One can browser around for its user agent: the
user-agents.net site has a complete list. The only things that’s left for us to do is to
somehow embed the user agent into the request: that’s a piece of cake for curl(1)
:
$ curl -v -A 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.211 Mobile DuckDuckGo/5 Safari/537.36' \
-L http://1.2.3.4:9090
flag{DuckdUCKGo}
The above not only embeds the user agent, it’ll also log the request details (-v
) and follow any redirections returned by the server -L
. You can see the
server replies with the flag right away! Just for the sake of trying, we probed the server to see what it was looking for in the request when deciding whether
to serve the flag or not. It turned out you just needed to configure the user agent as DuckDuckGo/5
:
$ curl -v -A 'DuckDuckGo/5' -L http://1.2.3.4:9090
flag{DuckdUCKGo}
One less to go!
Captain Crunch
In a similar fashion to Labyrinth, we’re given a ZIP file which is now password protected together with the password’s hash. However, we’re told
the password is rather insecure and that it’s related to the world’s most famous mouse, which os no other than Mickey Mouse, of course. The password-protected
ZIP file is zipdivertido.zip
, and the passwords hash is:
3eb4e968a6b408ebf49eeac9a98e3f8a
The first thing we need to do is find out the password’s hashing algorithm. It’s length is 128
bits, so chances are it’s an MD5
hash:
$ echo $(( $(echo -n "3eb4e968a6b408ebf49eeac9a98e3f8a" | wc -c) * 4 ))
128
The above leverages bash(1)
’s arithmetic expansion in case you want to look it up. The idea is we count the number of characters in the hash with wc -c
to then multiply that number by 4
given each hex digit amounts to 4
bits. This is just fancy stuff to check the actual size of the output hash. If you
check this list of hashes you can see how only MD5 sports 128-bit long hashes.
Now that we know we’re dealing with MD5 we can begin thinking about cracking it: that’s basically trying out different password candidates, by hasing them
and then comparing the result with the provided hash. Despite the nightmare it is to install it, we’ve worked with hashcat
in the past, so that’s what we’ll do now:
$ hashcat -m 0 -a 3 --increment -1 mickey 3eb4e968a6b408ebf49eeac9a98e3f8a '?1?1?1?1?1?1?1?1?1?1?1'
3eb4e968a6b408ebf49eeac9a98e3f8a:ecemk
Let’s take a quick tour through the options:
-m 0
: We’ll be using MD5 hashes.-a 3
: We’ll perform a mask attack (i.e. a ‘fancier’ brute force attack).--increment
: We’ll begin trying out 1-character long passwords to then move on to longer ones up to the mask’s length.-1 mickey
: We’ll generate candidate passwords by combining the characters that make up the wordmickey
. This is where one of the challenge’s clues comes in handy!3eb4e968a6b408ebf49eeac9a98e3f8a
: The hash we want to crack. It can also be the path to a file containing the target hashes.'?1?1?1?1?1?1?1?1?1?1?1'
: The password mask. You can check the documentation for more information, but this mask basically represents all the possible 10-character passwords one can create by combining the characters making up the workmickey
as specified through-1
. Given we use the--increment
flag,hashcat
will try out all possible 1-character passwords, then 2-character passwords and so on until it tries out all 10-character passwords. Given the challenge states the password is rather weak we expect it to be from 1 to 10 characters long.
It’s worth mentioning the entire hashcat
invocation was David’s doing! Thanks a ton :P
If you check the output of the command, you can see how the hashed password is the 5-character long ecemk
! You can verify that
by computing it’s MD5 hash with md5sum(1)
:
$ echo -n ecemk | md5sum
3eb4e968a6b408ebf49eeac9a98e3f8a -
We got the password! After all this work it might come as a surprise that there’s a much easier way to get the password. Just head over to crackstation.net and enter the hash: it’ll be cracked in a second provided you don’t fail the *Completely Automated Public Turing test to tell Computers and Humans Apart" :P
No matter how you got it, we can now unzip(1)
the provided file:
$ unzip -P ecemk zipdivertido.zip
Archive: zipdivertido.zip
inflating: notOswald.jpeg
As seen on the output, this creates a file named notOswald.jpeg
showing a screenshot of the Steamboat Willie
shot film. However, the statement hints at how the key to the challenge is inside the image. This is a perfect prompt for using
strings(1)
. We’ll just grep(1)
for a flag candidate:
$ strings notOswald.jpeg | grep flag
flag{StEAmBoAtWiLLieIsNowPUBLIC}
Jackpot! That about does it for the challenge :P
POW
In this reversing challenge we’re provided with an executable (which will only run on Linux-based systems, by the way): pow
.
We can try to run the executable: we’ll be presented with a prompt to enter a password (which we, of course, have no idea about).
$ ./pow
++++++++++++++++++++
+ +
+ Introduce la +
+ clave: +
+ +
++++++++++++++++++++
> foo
¡Clave incorrecta!
We can try keys until we get bored, but the next step would be disassembling the whole thing. What we usually use is
cutter
, the graphical frontend for rizin
. Given the controversy that gripped
the original project (i.e. radare2
) we decided to switch some time ago…
At any rate, after opening up the binary in cutter
the first thing we have to look for is the program’s entrypoint, main()
:
|
|
It might be a bit easier to look at the decompilation:
|
|
It looks like if we get the password right function fcn_00001165()
will output the flag: otherwise the message Clave incorrecta!
gets
printed… One can look at the entry_init1()
function. We’ll stick to decompilation for now: it’s much easier to read!
|
|
Judging by the contents of main()
the flag will only be printed if we reach the main(1, argv)
call. If the argument
is 3
, the Clave incorrecta!
message will be shown… At this point we can take one of two paths:
We can try to find the password satisfying all the checks so that we reach
main(1, argv)
. This way we can just invoke the program and supply the correct password to get the flag.We can run
fcn_00001165()
whilst debugging it so that we can reconstruct the flag itself. That’s what we’ll be doing!
Cutter
lets you emulate the program: we can also set up a couple of breakpoints to make our task a bit easier! When
execution stops we can inspect the values of any registers. However, before diving into the code let’s take a look at
the function in question:
|
|
Now that’s barely readable is it? Let’s inspect the decompiled version now:
|
|
The function basically iterates 7
times to then call printf
to print the flag. What’s more, the char*
being
passed to printf()
is var_12h
which, according to the disassembled function is initialized to rbp - 0x12
.
Now, the key to the whole thing are both *((rbp + rax - 0x12)) = dl
lines. What’s going on there is that the
contents of register rax
are indexing the string pointed to by var_12h
(remember it’s initial address is
rbp - 0x12
) so that the contents of dl
are inserted at index rax
. We could work out all the values of dl
and rax
at each iteration, but we’d rather make use of cutter
’s emulator :P Just set up a breakpoint on each
of these lines before moving on and you should be good to go.
Just a note on register names
Modern 64
bit machines use, well, 64
bit registers. The different ways of addressing the registers allow us
to access different subsets of those 64
bits. In the case of the A
register so to speak we can refer to:
- The full
64
bits asRAX
. - The lower
32
bits asEAX
. - The lower
16
bits asAX
. - The lower
8
bits asAL
. - Bits
8
through15
(i.e. the top half ofAX
) asAH
.
Please refer to this StackOverflow post for
more info! The bottom line is whenever you see RAX
, EAX
, AX
, AL
or AH
you can just think A
register.
The following table illustrates the values of RAX
and RDX
at each breakpoint. Remember RAX
is the index into the
string and RDX
provides each inserted byte!
Breakpoint Stop | New Character Index (RAX ) | New Character (RDX ) |
---|---|---|
00 | 0x00 | 0x74 = t |
01 | 0x07 | 0x79 = y |
02 | 0x01 | 0x30 = 0 |
03 | 0x08 | 0x5f = _ |
04 | 0x02 | 0x30 = 0 |
05 | 0x09 | 0x63 = c |
06 | 0x03 | 0x5f = _ |
07 | 0x0a | 0x68 = h |
08 | 0x04 | 0x6d = m |
09 | 0x0b | 0x33 = 3 |
10 | 0x05 | 0x34 = 4 |
11 | 0x0c | 0x6b = k |
12 | 0x06 | 0x6e = n |
13 | 0x0d | 0x73 = s |
If we reorder them based on the index we get: t00_m4ny_ch3ks
. Remember the printf()
call: this string is enclosed
by flag{}
, which means the challenges flag is flag{t00_m4ny_ch3ks}
.
Slow Mobius
In this case we’re handed another binary: justaprintf
. The strategy we followed is similar: we began by running it.
$ ./justaprintf
flag?: VInJTkxTLMq|q\rNU}Qqpiun
If you run it several times you get different flags, which hints at some time-based random number generation… The next
step is firing cutter
up again and looking at main()
’s (well, its beginning) disassembly:
|
|
One thing that struck us is that the flag is practically readable as is: flag{hItMEwiThthECLocKBeAM}
. You can
check the binary at the addresses passed to the movabs
instructions and find these strings. Given the name
of the challenge we found out Slow Mobius is actually a
character in Rick and Morty, so the flag’s contents made sense… Anyway, it was the solution, so we didn’t
give it any more thought!
Token Vault
In this challenge we are pointed to an Ethereum contract in two different test nets. We’ll look at the one
on Sepolia, which we can inspect on Etherscan
.
Whose address is 0x1776645C7f4995c83249e16D7a626Bc10a3c905c
.
The key piece of information can be obtained by going through the contract code.
Aside from all the additional code, the most important bit is the constructor of the ERC1155
token included below
for convenience:
|
|
Two things immediatly pop out:
- The constructor receives some kind of object stored on IPFS whose URI is
ipfs://QmSdnK8U7BgdVrVL1r8wKxgVwvF55edJdwEMWzQ6J7A3bZ
. - When invoked, a password must be supplied which is then stored on the
PASSWORD
variable.
Let’s look at the seconf bullet point. If you browse to the bottom of the contract code. You can see the arguments passed to the constructro upon contract creation:
-----Decoded View---------------
Arg [0] : _password (bytes32): 0x4164614c6f76656c616365403230323400000000000000000000000000000000
We can run the raw _password
through CyberChef
(thanks @elserio
for that!) to
find that, when interpreted as ASCII characters, the password is AdaLovelace@2024
. In case it’s your first time
using CyberChef
you can just use the From Hex operation to decode the data.
Okay, we now have a password but really nothing to use it with. Let’s focus on the first bullet point: we need to
grab the content of the IPFS URI. To do so we can use ipget
:
$ ipget QmSdnK8U7BgdVrVL1r8wKxgVwvF55edJdwEMWzQ6J7A3bZ -o token.init
$ cat.init
{
"name": "CTF Byron 2024 Reward",
"description": "Reward for completing the token vault challenge.",
"flag": "TMgzlqsOfgaWVulLOmWrLg==",
"algorithm": "AES 256 CBC",
"image": "ipfs://QmRZSKCco2Xja3bNVYr8FybuqSXnZw8jQg9HdqNjVYYyAi",
"external_url": "https://byronlabs.io"
}
Well well, it looks like we have our flag already! But… it’s encrypted! The good news is we have a password to try out.
We can leverage CyberChef
once again! Bear in mind the trailing ==
in the content of flag
is indicative of a Base64
encoding. We then need to decode the content and then run it through AES decryption. The recipe then becomes:
- From Base64 with alphabet
A-Za-z0-9+/=
. - AES Decrypt with key
AdaLovelace@2024
in Latin 1 format, IV0000000000000000000000000000000
(i.e. 16 bytes for AES 128) modeCBC
and aRaw
intput and output.
It’s crucial to note the JSON document retrieved from IPFS specifies the encryption algorithm is AES 256 CBC
, but given the
input to CyberChef
it looks like the used algorithm was AES 128 CBC
instead. We initially tried to leverage openssl(1)
for
the decryption and trying to get AES 256 CBC
to work was impossible…
We almost forgot! The decryption gives us the flag right away: flag{v1t4l1k}
.
Unlike what we initially assumed, there’s no need to run any transactions whatsoever! If you went that route we hope you found Automata’s Sepolia Faucet: its the one that worked out best for us…
And the rest?
Well, that’s all the challenges we did… If you want to add the writeup for a new one feel more than free to do so! You can even open a PR to this site’s repository!
If you have any comments, questions or suggestions, feel free to drop me an email!
Thanks for your time! Hope you found this useful 😸