# [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)

## 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 $$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$$.

# 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 and flag.php, rendering the source code of index.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 :)