Cubic

Contents

Cubic#

Helpers#

import numpy as np
import os
import requests
import time
from dotenv import load_dotenv
from string import ascii_uppercase as CHARS

load_dotenv()
API_KEY = os.environ["API_KEY"]
URL_QUERY = "https://%s.crucible.dreadnode.io/%s"
URL_FLAG = "https://crucible.dreadnode.io/api/challenges/%s/submit-flag"
CHALLENGE = "cubic"

def query(data, route="score"):
    response = requests.post(
        URL_QUERY % (CHALLENGE, route),
        headers={ "X-API-Key": API_KEY },
        json={ "data": data }
    )
    return response.json()

def submit(flag):
    response = requests.post(
        URL_FLAG % CHALLENGE,
        headers={ "X-API-Key": API_KEY },
        json={ "challenge": CHALLENGE, "flag": flag }
    )
    return False if response.status_code != 200 else response.json().get("correct")

Solution#

If we submit some random values to the endpoint we can see that it returns 12 logits.

data = np.zeros((64, 1))
query(data.tolist(), route="predict")["prediction"]
[[0.27251669404844703],
 [0.26096517762029814],
 [0.2632274650925554],
 [0.31077285026202944],
 [0.31527584262764974],
 [0.2507438205101342],
 [0.2499624217505734],
 [0.27631836345169647],
 [0.27471431660350865],
 [0.2710597178020978],
 [0.31458430128313064],
 [0.3137506504889416]]

Similar to Genigma, for each of the 12 logits we can submit 64 different queries where each query contains 63 zeros and a single large value. If we take the index which maximizes the logit and then reverse index into the alphabet we can find the flag.

Note: for convenience, the below code only submits 26 different queries as it turns out that the logit is always maximized within the first 26 values.

flag_chars = []
for logit in range(12):
    max_value = 0.0
    max_option = 0
    for option in range(26):
        data = np.zeros((64, 1))
        data[option][0] = 100
        response = query(data.tolist(), route="predict")
        value = response["prediction"][logit][0]
        if value > max_value:
            max_value = value
            max_option = option
        time.sleep(1)
    flag_chars.append(CHARS[25 - max_option])

flag = "".join(flag_chars)
print(flag)
print("Flag accepted:", submit(query(flag)["flag"]))
THREEBYTHREE
Flag accepted: True