[misc] DEFCON CTF Qualifier 2026 - rfc1149a Write-up
1. 개요
1.1 문제 설명
## 1.2 문제 배포 파일 구조
rfc1149a/
├── img01.jpg
├── img02.jpg
├── img03.jpg
├── img04.jpg
├── img05.jpg
├── img06.jpg
├── img07.jpg
├── img08.jpg
├── img09.jpg
├── img10.jpg
├── img11.jpg
├── img12.jpg
├── img13.jpg
└── img14.jpg
2. 요약
- 주어진 이미지 파일들 내에 패킷 조각들은 IPv4/TCP 패킷이었으며, TCP sequence number를 통해 각 패킷들을 흐름에 맞게 정렬하여 패킷 조각들을 맞춘 후, 일부 패킷들의 chunked gzip HTTP response 데이터들을 압축 해제함
- 압축 해제된 데이터들로부터 문제 서버의 도메인
supersecretflagserverdonttellanyoneaboutit.rfc1149.ctfwithbirds.com과 302 Found 패킷을 찾음
302 Found의 timestamp, session_id를 포함한 뒷부분이 손실되었으나 직접 로그인하여 얻은 원본 302 Found 패킷 + ipv4의 'Total Length' 필드를 참고해서 Date와 session_id를 제외한 Payload 전체를 복구함
Date는 TSval과 비교하여 알아낼 수 있으므로 TCP Checksum을 통해 session_id 후보를 줄일 수 있음
3. 초기 분석
3.1 이미지 파일(img01.jpg~img14.jpg) 분석
-
여러 장의 종이 띠가 존재하며, 각 종이 띠에는 45 00 ... 형태의 hex byte가 인쇄되어 있음
(0x45 = IPv4 version 4 + IHL 5, 0x06 = TCP Packet) → IPv4/TCP Packet data
-
각각 이미지 파일을 하나로 합쳐서 분석을 진행함 (with Kredsya, kimdy)

3.2 IPv4/TCP 패킷 분석
3.2.1 공통점
- 서버 응답 방향 패킷들만 존재함
- 34.16.111.14:80 → 10.13.37.1:*
3.2.2 seq 010b94cf 계열 (Home)
| Start seq |
확인된 단서 |
010b94cf |
200 OK, text/html, gzip, chunk 15d |
010b96fa |
200 OK, text/css, style.css |
010b9846 |
style.css gzip continuation, chunk 227 |
010b9992 |
style.css gzip/deflate continuation |
010b9a8e |
200 OK, text/css, theme-light.css |
010b9bda |
theme-light.css gzip body, chunk a9 |
3.2.3 seq 96670169 계열
| Start seq |
확인된 단서 |
96670169 |
SYN,ACK, no payload |
9667016a |
200 OK, text/css |
966702b6 |
gzip body, chunk ab |
3.2.4 seq b132103e 계열 (Login/Flag)
| Start seq |
확인된 단서 |
b132103e |
200 OK, text/html, gzip, chunk 1b6 |
b132118a |
HTML gzip/deflate continuation |
b13212c2 |
304 NOT MODIFIED, style.css |
b13214b3 |
304 NOT MODIFIED, theme-dark.css |
b13215ad |
200 OK, text/html, gzip, chunk 18d |
3.2.5 seq f4f56c96 계열
| Start seq |
확인된 단서 |
f4f56c96 |
302 FOUND |
f4f56de2 |
redirect body tail, <a href="/">/</a> |
f4f56e40 |
200 OK, text/html, gzip, chunk 14e |
f4f56f8c |
HTML gzip/deflate continuation |
f4f5705c |
304 NOT MODIFIED, style.css |
f4f57152 |
304 NOT MODIFIED, theme-light.css |
f4f5724d |
304 NOT MODIFIED, theme-dark.css |
3.3 gzip decompress 결과
style.css에서 서버 도메인 http://supersecretflagserverdonttellanyoneaboutit.rfc1149.ctfwithbirds.com 확인
@import url('http://supersecretflagserverdonttellanyoneaboutit.rfc1149.ctfwithbirds.com/static/theme-light.css') (prefers-color-scheme: light);
@import url('http://supersecretflagserverdonttellanyoneaboutit.rfc1149.ctfwithbirds.com/static/theme-dark.css') (prefers-color-scheme: dark);
HTML 응답 일부(Home, Login, Flag) 확인
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Home</title>
<link rel="icon" href="data:image
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="icon" href="data:image/svg+xml,<svg
xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🏁</text></svg>">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/login">Login</a>
<a href="/register">Register</a>
<a href="/flag">Flag</a>
</nav>
<h1>Login</h1>
<form method="POST" action="/login">
<label>Username <input name="username"></label>
<label>Password <input name="password" type="password"></label>
<button type="submit">Login</button>
</form>
<p>Don't have an account? <a href="/register">Register</a></p>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Flag
이외에도 theme-light.css, theme-dark.css 복원
4. Key Insight / Reconstruction Primitive
-
문제 유형
- TCP checksum을 제약 조건으로 활용해 손실된 HTTP 응답 데이터를 복원하는 패킷 분석 문제임
-
핵심 기법
- TCP checksum 제약을 이용한 누락 HTTP 응답 헤더 후보 필터링
-
활용 원리
- TCP checksum은 IP pseudo-header, TCP header 및 해당 TCP segment의 payload를 대상으로 계산됨
- TCP payload에 HTTP 응답 헤더가 포함되어 있으므로, 누락된 헤더에 후보 값을 대입한 뒤 원본 TCP checksum과 일치하는지 검사할 수 있음
- checksum은 16비트이므로 원본 값을 유일하게 결정하지는 못하지만, 가능한
session_id 후보군을 크게 줄일 수 있음
- 같은 TCP segment에 포함된 나머지 payload와 정확한 HTTP 헤더 구조를 알고 있어야 후보별 checksum을 계산할 수 있음
-
후보 공간에 대한 가정
- 같은 서버에서 테스트 계정으로 로그인해 받은 참조 응답을 통해
session_id가 8자리 lowercase hex 형식임을 확인함
- 참조 응답을 바탕으로
Date와 session_id를 제외한 HTTP 응답 헤더의 순서와 값을 복원함
-
근거
- 첫 번째 TCP segment에서
HTTP/1.1 302 FOUND와 응답 헤더의 prefix를 확인함
- 다음 TCP segment에서 redirect body의 뒷부분을 확인함
- IPv4 Total Length, IP/TCP header 길이 및 다음 segment의 sequence number를 통해 첫 번째 TCP payload 길이가 332바이트임을 확인함
- 손실된
Date와 session_id 후보를 완성된 332바이트 payload에 대입하여 원본 TCP checksum과 비교할 수 있음
-
왜 이것이 복원 프리미티브였나
- checksum 조건을 만족하지 않는 대부분의
session_id 후보를 로컬에서 제거할 수 있음
- checksum을 만족하는 후보가 반드시 원본 세션이라는 의미는 아니므로, 남은 후보를 쿠키로 설정해
/flag 접근 가능 여부를 서버에서 검증함
5. 풀이 흐름
5.1 HTTP response 구조 복원
-
IPv4/TCP 조각을 sequence 기준으로 정렬하고, gzip body를 가능한 범위까지 inflate하고 이를 gzip decompress함
-
style.css 에서 서버 도메인을 확인함
-
서버 접속 시, /etc/hosts에 다음과 같이 설정해야 함
34.16.111.14 supersecretflagserverdonttellanyoneaboutit.rfc1149.ctfwithbirds.com
-
Login 계열 페이지 내 /login, /register, /flag 링크를 확인함
-
Flag 계열 HTML prefix에서 <title>Flag도 확인함
-
HTTP/1.1 302 FOUND 시작 부분과 /로 redirect되는 body의 끝 부분을 확인함
- 웹 서버 응답을 분석했을 때,
/로 redirect되는 것은 login 성공 이후 응답이였고, response에 session_id도 반환되는 것을 확인함
-
해당 과정이 필요한 이유: /flag가 실제 사이트 구조 안에 있고, 인증 세션을 획득하면 접근할 대상이 있다는 것을 먼저 확인해야 했기 때문임
5.2 복원 대상 선정
302 FOUND redirect response를 핵심 복원 대상으로 잡음
- 이 응답에서 인증 관련 header가 있을 가능성을 두고, 보이지 않는 header 부분을 unknown으로 모델링함
- 해당 과정이 필요한 이유: flag 본문은 패킷에서 직접 복원되지 않았으므로,
/flag 접근에 사용할 권한을 만드는 세션 값을 찾아야 했기 때문임
5.3 HTTP Date 후보 계산
Date: header도 TCP payload에 포함되므로 checksum 계산에 영향을 줌
- 따라서
session_id만 unknown으로 둘 수 없고, 302 Found response의 Date 문자열 후보도 좁혀야 함
302 Found response의 server TCP timestamp:
302 Found response의 TSval: 0x00b34712 = 11,749,138
- 같은 timestamp clock을 쓰는 주변 HTTP response들에서
Date와 TSval을 비교해 tick rate를 계산함
23:16:01, TSval 12,991,738
23:44:56, TSval 13,636,919
00:44:09, TSval 14,958,892
03:01:41, TSval 18,028,619
- 주변 구간의 증가율은 대략
372 ticks/sec로 수렴함
645,181 / 1,735s ≈ 371.86
1,321,973 / 3,553s ≈ 372.07
3,069,727 / 8,252s ≈ 372.00
302 Found response와 23:16:01 의 TSval 차이:
12,991,738 - 11,749,138 = 1,242,600 ticks
1,242,600 / 372 ≈ 3,340.3s
23:16:01 GMT에서 약 3,340s를 빼면 22:20:21 GMT 근처가 됨
- 따라서
22:20:21 GMT를 중심으로, 초 단위 오차를 감안해 22:20:19-22:20:23 GMT를 Date 후보로 잡음
- 해당 과정이 필요한 이유:
Date가 1초만 달라도 TCP checksum 결과가 달라져 session_id 후보군이 바뀌기 때문임
5.4 TCP checksum으로 session_id 후보 생성
- 각 Date 후보마다
302 Found response header template을 구성함
- 확인되는
302 Found 데이터와 redirect body에 충돌하지 않는 후보만 유지함
session_id는 8자리 hex unknown으로 두고, TCP pseudo-header + TCP segment checksum이 맞는 후보만 유지함
- 후보 탐색은 앞 4자리/뒤 4자리 checksum 기여값을 나누는 meet-in-the-middle 방식으로 줄임
- 해당 과정이 필요한 이유:
16^8 전체를 직접 검증하기에는 시간이 오래 걸리기 때문에, 패킷 자체의 checksum으로 후보를 줄여야 했기 때문임
5.5 후보 세션 검증
- 생성된 후보를
Cookie: session_id=...로 넣어 /flag 접근 여부를 확인함
HEAD /flag로 status를 확인하고, status == 200인 후보에 대해서만 GET /flag를 수행해 body를 확인함
- 해당 과정이 필요한 이유: checksum은 원본 패킷과 양립 가능한 후보만 알려주므로, 실제
/flag 권한이 있는지는 추가 검증이 필요했기 때문임
5.6 플래그 획득
HEAD /flag에서 status == 200이 나온 세션 후보로 GET /flag body를 확인함
6. Flag
bbb{dear_bob_got_any_hot_goss_on_mallory_love_carol_ps_u_up?}
7. Pitfalls & Rabbit Holes
- 시도 1
- 서버 도메인 획득 이후 웹해킹 기법(SQL Injection, SSTI, XSS 등)을 통해 세션 탈취를 시도했으나 실패함
- 시도 2
- 보이지 않는 gzip body 부분을 완벽하게 복호화하려고 시도했으나 실패함
- 시도 3
- 302 직후에 온 것으로 추정되는 200 패킷은 HTTP response header가 보이므로 이 패킷의 Date에서 0~5초 뒤의 Date로 시도했으나 실제로는 20분 이상 차이나서 실패함