[ictf Apr 2021] My challenges
Another month, another round of ImaginaryCTF. This month I was even more involved in organization, and contributed a good amount of challenges, of which I present an overview – including writeups – here. All relevant files, which include reference exploit scripts where applicable, can be found here.
pyrev (2021-04-07)
Description
I think it’s time for a dis.dis()
track…
Provided:
out.txt
- later on, the redacted source code below was also provided to illustrate how the output was generated and how it corresponded to a function
def f(n):
# redacted stuff here
import dis
dis.dis(f)
Solution
The bytecode is fairly easy to read, we see a loop being setup over a constant list that’s embedded in the disassembly. The reconstructed code is:
def f(n):
for x in [0, 6, -17, 14, -21, 25, -23, 5, 15, 2, -12, 11, -1, 6, -4, -12, -6, 9, 8, 5, -3, -3, 6, -6, 4, -18, -6, 26, -2, -18, 20, -17, -9, -4]:
n -= x print(chr(n), end="")
print()
We can either simulate it ourselves, or reconstruct it, then knowing
the flag format, simply pass in ord("i")
as argument and
read off the flag.
Optimal RSA (2021-04-10)
Description
You’re aware that textbook RSA is actually insecure, right? So anyway, I applied some padding. For even more security, I’m also using SHA512.
Provided
output.txt
Solution
We’re given all the parameters (the modulus is prime so no factoring needed), so it’s not hard to decrypt. The only tricky thing are:
- The modulus is prime, so several tools (including RsaCtfTool) won’t like that
- The encryption is not textbook RSA but OAEP (as hinted by the “optimal” in the title)
Rotations of a different kind (2021-04-13)
Description
We’ve had one rotation, yes, but what about another one?
Provided
69c6d133b72d9bb172cab52be68e5a3767beb12b668ed7396fe885a396ed9bb97d
Solution
Each individual byte is the result of a left-rotation by one more bit than the previous one (so byte i is rotated left by i bits, starting at 0). Easily undone, we can make some observations to determine what’s happening by seeing that the first and last are both as expected in the flag format, and have a distance divisible by 8.
def rol(x, i):
return ((x << i) | (x >> (8 - i))) & 0xff
print(bytes(rol(x, (8 - i) % 8) for i, x in enumerate(bytes.fromhex("69c6d133b72d9bb172cab52be68e5a3767beb12b668ed7396fe885a396ed9bb97d"))))
What’s a database (2021-04-17)
Description
What’s a database? Why don’t you store actual information? Why do I need to guess?
Provided
- A running instance of the site, printing its own source code
(
flag.db
should be read-only)
Solution
We can run an arbitrary script on an ephemeral, in-memory sqlite
database. Furthermore, we know the flag exists in the flag
column of the flag
table of the flag.db
database. If we can get the flag value in a table in our own database
that matches the real flag, it will be echoed back to us. Now, to
achieve this without actually knowing the flag up front, we’ll have to
make use of an sqlite feature: ATTACH DATABASE
. This allows
us to create a new table, and simply insert the flag we can get from
flag.db
into it.
The full exploit will then look like this:
/db?script=ATTACH 'flag.db' AS f; CREATE TABLE whatsthis (flag VARCHAR(80) PRIMARY KEY); INSERT INTO whatsthis SELECT flag FROM f.flag;
vnpack (2021-04-20)
Description
I tried to make my flag printer smaller, but I think something got corrupted. Now it’s not only my physical printer that’s possessed anymore.
Provided
- vnpack binary
- Later hint:
the corruption has even spread to the title of the challenge
Solution
The binary has been packed with UPX, but all occurences of
UPX!
have been replaced with VPX!
, causing the
binary not to run properly. This can be seen by looking at the strings
in the binary (occuring both at the start and the end), and the
challenge title helps a bit too. Fixing this allows us to just run it to
obtain the flag.
Bland RSA (2021-04-21)
Description
My RSA’s been tasting a bit bland, lately.
Provided
out.txt
Solution
We see that the value for is extremely big. We might assume that the decryption exponent is not very large in this case, and that as such Wiener’s attack or the attack of Boneh and Durfee applies. This might work, but instead, upon further inspection, we observe that the ciphertext is in fact not encrypted at all. As it turns out, , resulting in .
Minijail (2021-04-23)
Description
Alright, this time I’ll give you access to print. But I’ll be imposing some other arbitrary restrictions…
Provided
minijail.py
- network connection running
minijail.py
with access to flag.txt (python 3.8+) - Later hint:
>>> import sys
>>> print(sys.version)
3.9.1 (default, Feb 9 2021, 07:42:03)
[GCC 8.3.0]
Solution
From print
, we can get to __builtins__
via
print.__self__
. With this, a short exploit would consist of
running exec(input())
, but both of these require access to
__builtins__
.
Luckily, we appear to be running a modern python version, so we can
use the walrus operator. This means we can assign
__builtins__
to a short variable name and use it in the
same expression (e.g. a tuple or a list).
[b:=print.__self__,b.exec(b.input())]
is exactly the
length we need.
Later on, a shorter solution of length 35 was found by one of the
participants, by eliminating one of the usages of b.
:
(b:=print.__self__).exec(b.input())
. Should you have found
an even shorter solution, or another interesting approach to this, you
can always reach out in the ImaginaryCTF discord.
Fake crypto (2021-04-27)
Description
I promise you it’s not a crypto challenge.
Provided
- A hosted version of
index.php
andflag.php
, rendering the source code ofindex.php
Solution
PHP is a weird language. When you use ==
instead of
===
the comparison will often behave in a strange
way. In this particular case, since we’re generating string hashes
in hex, we’re interested in strings that could be interpreted as
integers. For example when we compare "0e12345" == 0
or
even "0e12345" == "0e54321"
, these will evaluate to true,
as the loose comparison first tries to interprete the strings as
numbers, and successfully does so, thinking they represent
0 * 10^x
. With some brute force to search for md5 hashes of
this form, we can fairly quickly find a “collision”. This phenomenon is
also known as a magic hash in PHP.
Unintended solution: I thought I checked it, but
apparently not properly. You can hash arrays in PHP without crashing,
and all arrays hash to the same thing, while comparing differently. So
using ?a[]=1&b[]=2
for example would also give the
flag.
Not really intended solution, but I don’t mind as
much: you can approach it as a crypto challenge and generate a
chosen-prefix collision on MD5 with e.g. the
fastcoll
tool.
JSON, but not notation (2021-04-29)
Description
Given the amount of rickrolling, I’m fairly sure this site is broken in some way. So that means it should be easy to become an admin, right?
Provided
index.js
package.json
- A running instance of the site
Solution
We can notice that the hash that is checked is actually the binary inverse of a fake flag, so it’s very unlikely this is a real hash, and we certainly couldn’t extract the flag from it.
What we see instead, is that the whatsThis
function
allows us to do prototype pollution. By sending the body
__proto__.is_admin=whatever
, and because there’s an obvious
typo in the GET
handler, we can pass the condition and get
the flag.
Most of the trickery is involved in making sure that all sessions have an individual prototype, since otherwise a single solver would make the flag appear for everyone.
Ropsten (2021-04-30)
Description
It can do computations, not just money, did you know that?
Provided
- The address on the ropsten testnet where the
CTF.sol
contract is deployed:0xb623E940215925Ede9745DCd07950E912895fAcB
Solution
From the title and the category, we can deduce this challenge is about the ethereum blockchain, and its ropsten testnet. We use the address provided to locate a smart contract in the etherscan explorer. There, we can try to decompile the bytecode, or simply read the verified source code that’s provided there.
Reading that, we see that function named xorme
that
takes an input, checks it and gives an output. Xoring the correct input
with the output together gives us the flag.
Unintended solution: as it turns out, I had made the challenge a bit harder than my first thought, but only after deploying a smart contract for the easier version. By looking at the other transactions of the creating address, you could find the creation transaction of the previous version, which contained the flag in plaintext in the transaction data. Lesson for next time: use a different address :)