Try   HackMD

TetCTF 2023 (Writeups of some challenges)

CTFtime: https://ctftime.org/event/1842
Team: ⚔️TSJ⚔️
Rank: 5

NewYearBot

Here is a part of the source code:

debug = request.args.get("debug")
if request.method == 'POST':
    greetType = request.form["type"]
    greetNumber = request.form["number"]
    if greetType == "greeting_all":
        greeting = random_greet(random.choice(NewYearCategoryList))
    else:
        try:
            if greetType != None and greetNumber != None:
                greetNumber = re.sub(r'\s+', '', greetNumber)
                if greetType.isidentifier() == True and botValidator(greetNumber) == True:
                    if len("%s[%s]" % (greetType, greetNumber)) > 20:
                        greeting = fail
                    else:
                        greeting = eval("%s[%s]" % (greetType, greetNumber))

The goal is to find the value of the variable FL4G. The code evals greetType[greetNumber] if:

  • greetType is an identifier
  • greetNumber passes botValidator, which does the following checks:
    • no ASCII characters between 58 and 122
    • int(all the 0-9 characters combined) is at most 5
  • greetType[greetNumber] is at most 20 characters

We can set greetType to FL4G and greetNumber to a number to get one character of the flag at a time. Since botValidator only checks for ASCII characters in the range [58, 122], we can use special Unicode characters like int(debug) to make it use the value of debug as the index instead. Therefore, querying /?debug=0 with type=FL4G&number=int(debug) gives the 0-th character of the flag, etc.

Alternatively, we can use exec(debug) to make it run arbitrary code.

Gift

This is a sourceless a.k.a. guessing web challenge. We are given the database schema and nothing else.

send_pic.php

There is a comment in the HTML about send_pic.php. This endpoint takes two parameters url and id, which correspond to the pictures table. After a bit of testing, we can find out that it takes the row with the same id and sends it to the url we give. There is a SQL injection in the id parameter, and after some testing we know that it blocks the following keywords:

  • ,
  • (
  • )
  • key_cc

Using the payload

curl 'http://172.105.127.104/send_pic.php' \
    --data "url=http://" \
    --data "id=3 union select KEY_CC from access_key--"

we were able to obtain the secret access key, messi_is_goat__!!!!.

get_img.php

We can use the access key from the previous step to log in. We can now access the page get_img.php, which includes the page in the file parameter. We can achieve LFI using this page, but some substrings are blocked:

  • sess
  • ../..
  • pear

The second one can be circumvented by using .././.., so we can include any file outside the directory. Unfortunately, we cannot find any useful files to read, and usual LFI to RCE techniques like php://filter, /tmp/sess, pearcmd.php, /proc/self/fd, /proc/self/environ, etc. don't seem to work.

After being stuck on this step for a while, a teammate suggested that peclcmd.php can be used to achieve RCE and fing the flag.

Casino 1

This is the unintended version of casino 2.

Challenge

There are 3 main functions in this challenge:

  1. We can bet any amount of money (but not more than our balance) in the casino. If we guess a random number below 2023 correctly, we win 2023 times the bet amount, otherwise the money is lost.
  2. We can get our current balance with a Merkle tree hash proving we have that much money.
  3. We can show our balance and proof to buy the first log256(balance) bytes of the flag.

Unintended Solution 1

It is possible to bet a negative amount of money, so we can win enough money by losing a negative bet.

Casino 2

It's the same as casino 1, except we cannot bet negative amounts.

Unintended Solution 2

The intended solution is probably to break the hash, which I don't know how to do. However, I found another unintended solution.

Let's look at how the casino's random numbers are generated:

  1. At the start, it reads 64 bits from a secure random source as the seed.
  2. The number is used to seed the default PRNG from Go's math/rand module.
  3. Numbers are generated with rand.Intn(2023).

After reading the source code of Go's math/rand, we can see that the PRNG is actually seeded with seed % int32max, which means that there are only 2312 possible seeds[1]. We can recover the state of the PRNG from its output (which is shown to us) by brute-forcing all seed values. Enumerating the seed only takes a few hours of CPU time. Since there is a timeout, we can preprocess a portion of possible seeds first, and then connect multiple times until we get a known seed.


  1. The seed 0 is replaced by 89482311 so there is 1 less possibility. ↩︎