[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.
I think it’s time for a
- 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)
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)
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.
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)
We’ve had one rotation, yes, but what about another one?
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)
What’s a database? Why don’t you store actual information? Why do I need to guess?
- A running instance of the site, printing its own source code
flag.dbshould be read-only)
We can run an arbitrary script on an ephemeral, in-memory sqlite
database. Furthermore, we know the flag exists in the
column of the
flag table of the
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;
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.
- vnpack binary
- Later hint:
the corruption has even spread to the title of the challenge
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)
My RSA’s been tasting a bit bland, lately.
We see that the value for \(e\) is extremely big. We might assume that the decryption exponent \(d\) 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, \(e = \lambda(N) + 1\), resulting in \(m^e \equiv m \pmod N\).
Alright, this time I’ll give you access to print. But I’ll be imposing some other arbitrary restrictions…
- network connection running
minijail.pywith 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]
print.__self__. With this, a short exploit would consist of
exec(input()), but both of these require access to
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:=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)
I promise you it’s not a crypto challenge.
- A hosted version of
flag.php, rendering the source code of
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
"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
?a=1&b=2 for example would also give the
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
JSON, but not notation (2021-04-29)
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?
- A running instance of the site
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
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
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.
It can do computations, not just money, did you know that?
- The address on the ropsten testnet where the
CTF.solcontract is deployed:
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
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 :)