导语:反序列化漏洞出现很久了,一直到现在都很流行,但是这类漏洞的利用方式要比其他常见的漏洞要更加复杂。在最近的一次渗透测试过程中,我利用Java反序列化在服务器上获得了权限,从这台服务器上我获得了多个跨机房的数据中心的预发和生产环
反序列化漏洞出现很久了,一直到现在都很流行,但是这类漏洞的利用方式要比其他常见的漏洞要更加复杂。在最近的一次渗透测试过程中,我利用Java反序列化在服务器上获得了权限,从这台服务器上我获得了多个跨机房的数据中心的预发和生产环境的数十台服务器的root访问权限。我发现的这个漏洞应该曾经被多个入侵者利用过,如果我以前没有接触过Java序列化/反序列化漏洞,我可能也会错过它。
在这篇博文中,我将尝试理清有关反序列化漏洞的一些疑惑,并希望通过使用现成的工具来降低漏洞利用的条件。我将专注于Java反序列化漏洞,但是同样的概念也适用于其他语言的反序列化漏洞。我也将专注于命令执行漏洞,来保持利用方式的简单化。
我今年在SteelCon上演讲过这个议题,并且还在BSides Manchester和Bides Belfast上发表过演讲(同时,我也在今年的44con峰会上演讲了一个关于Java后门的议题)!
反序列化
简单来说,序列化是将运行时变量和程序对象转换成可以存储或传输的形式的过程。反序列化是将序列化形式转换回内存变量和程序对象的相反过程。序列化形式可以是基于文本的格式,例如JSON或XML,或二进制格式。许多高级的语言(如C#,Java和PHP)都内置了对数据序列化的支持,这对于使用和保存开发人员不必自己实现这些例程是非常重要的事情。在这篇博客文章中,我将专注于Java内置的序列化格式,不过其他格式也有可能带来类似的风险(可以查看AlvaroMuñoz和Oleksandr Mirosh在“BlackHat 2017”和Def Con 25的演讲议题——Friday the 13th: JSON Attacks)。
反序列化发生了什么?
使用反序列化本身并不会产生问题。当用户(攻击者)可以控制被反序列化的数据时就出现问题了,例如,如果数据可以通过网络连接传送到反序列化例程中。如果攻击者控制的数据被反序列化,那么它们对内存中的变量和程序对象就会有一些影响。之后,如果攻击者可以影响内存中的变量和程序对象,那么它们可以影响使用这些变量和对象的代码流。让我们来看一下Java反序列化的例子:
public class Session { public String username; public boolean loggedIn; public void loadSession(byte[] sessionData) throws Exception { ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(sessionData)); this.username = ois.readUTF(); this.loggedIn = ois.readBoolean(); } } public class Session { public String username; public boolean loggedIn; public void loadSession(byte[] sessionData) throws Exception { ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(sessionData)); this.username = ois.readUTF(); this.loggedIn = ois.readBoolean(); } }
“loadSession”方法接受字节数组作为参数并且通过反序列化该字节数组得到一个字符串(username)和布尔值(loggedIn)的对象属性。如果攻击者可以控制传递给该方法的'sessionData'字节数组的内容, 那么它们就可以控制这些对象属性。以下是可以如何使用此Session对象的示例:
public class UserSettingsController { public void updatePassword(Session session, String newPassword) throws Exception { if(session.loggedIn) { UserModel.updatePassword(session.username, newPassword); } else { throw new Exception("Error: User not logged in."); } } } public class UserSettingsController { public void updatePassword(Session session, String newPassword) throws Exception { if(session.loggedIn) { UserModel.updatePassword(session.username, newPassword); } else { throw new Exception("Error: User not logged in."); } } }
如果会话已经是登录态,则会话中存储的用户名对应的用户的密码会被更新为给定的值。这是一个“POP Gadget”的简单示例,这是一段我们可以通过对象的属性进行控制的代码片段。
面向对象编程(Property-Oriented Programming)
当我们控制对象属性并使用它们以这种方式影响代码执行的流程时,我们就是在进行所谓的 “面向对象编程”。POP gadget是一个代码片段,我们可以通过控制某些对象的属性来发挥我们的优势。通常,多个gadget需要链接才能创建完整的漏洞。我们可以将其视为高级的ROP(面向回归的编程 – 用于内存损坏利用的技术),但是除了将ROP gadget推送到堆栈之外,POP gadget可能允许我们将一些数据写入到文件中。
这里有一个重要的问题,反序列化漏洞不涉及将类或代码发送到服务器来执行。我们只是发送已经知道的服务器可以识别的类的属性,以便操纵处理这些属性的现有代码。因此,漏洞的成功利用是依赖于可以通过反序列化操作的代码的知识。这是利用反序列化漏洞的一大难点。
有趣的Gadgets
POP gadget可以存在于程序中的任何位置,唯一的要求是可以使用反序列化对象的属性来操纵代码,并且攻击者可以控制反序列化的数据。然而,一些gadget则更为有趣,因为它们的执行更加可预测一些。在Java中,一个可序列化的类可以定义一个名为' readObject '的方法,用于在反序列化期间执行特殊处理(例如支持向后兼容性)。此方法也可用于响应该类的对象被反序列化的事件。该方法的一个示例使用可能是数据库管理器对象在反序列化到内存中时自动建立与数据库的连接。大多数Java序列化漏洞都会利用这些readObject方法中的代码,因为这些代码保证会在反序列化期间执行。
利用反序列化漏洞
为了利用反序列化漏洞,我们需要做两件重要的事情:
1. 允许我们将我们自己的序列化对象发送到目标进行反序列化的一个入口点。
2. 我们可以通过反序列化操作的一个或多个代码片段。
入口点
我们可以通过查看使用类“java.io.ObjectInputStream” (特别是“readObject”方法)的应用程序源代码或实现“readObject”方法的可序列化类来识别反序列化漏洞的入口点。如果攻击者可以操纵提供给ObjectInputStream的数据,那么该数据就是反序列化攻击的入口点。或者,如果Java源代码不可用,那么,我们可以查找存储在磁盘上或通过网络传输的序列化数据,只要我们知道要查找的内容!
Java序列化格式始终以双字节的魔术数字的十六进制0xAC ED作为开头。之后的两个字节是版本号。我只见过版本是5(0x00 05)的数据,但早期的版本也可能是存在的,并且将来还可能存在更高的版本。在四字节头部之后是一个或多个内容元素,每个元素的第一个字节应该在0x70到0x7E的范围内,并且描述用于推断流中以下数据的结构的内容元素的类型。有关更多的详细信息,请参阅Oracle关于对象序列化流协议的文档。
我听很多人经常说要查找四字节的序列数据0xAC ED 00 05,以便识别Java序列化,实际上有一些IDS也是通过签名寻找这个序列来进行攻击检测的。在我最近为客户进行渗透测试服务期间,我没有立即看到这四个字节,因为目标客户端应用程序在整个运行时保持与服务器的网络连接,并且四字节头只在序列化流的一开始才存在。由于这个原因,客户端的IDS没有发现我的攻击 – 我的有效载荷在流中是延迟发送的,并且与序列化头分开。
我们可以使用ASCII转储来帮助识别Java序列化数据,而不是依赖于四字节0xAC ED 00 05的头部数据。
Java序列化数据的最明显的特征是转储中存在Java类名称,例如 “java.rmi.dgc.Lease”。在某些情况下,Java类名称可能会以“L”开头的替代格式出现 ,以';'结尾 ,并使用正斜杠来分隔命名空间和类名(例如 “Ljava / rmi / dgc / VMID;”)。除了Java类名,由于序列化格式规范的约定,还有一些其他常见的字符串,例如 :表示对象(TC_OBJECT),后跟其类描述(TC_CLASSDESC)的'sr'或 可能表示没有超类(TC_NULL)的类的类注释(TC_ENDBLOCKDATA)的'xp' 。
在确定了使用序列化数据后,我们需要确定该数据中我们可以实际注入有效载荷的偏移量。目标需要调用 “ObjectInputStream.readObject”来反序列化和实例化一个对象(有效载荷)并支持面向对象的编程,但是它可以首先调用其他的ObjectInputStream方法,例如 'readInt',该方法会简单地读取一个来自流的4字节整数。readObject方法将从序列化流中读取以下内容类型:
0x70 - TC_NULL 0x71 - TC_REFERENCE 0x72 - TC_CLASSDESC 0x73 - TC_OBJECT 0x74 - TC_STRING 0x75 - TC_ARRAY 0x76 - TC_CLASS 0x7B - TC_EXCEPTION 0x7C - TC_LONGSTRING 0x7D - TC_PROXYCLASSDESC 0x7E - TC_ENUM
在最简单的情况中,一个对象将是从序列化流中读取的第一件事,我们可以直接在4字节序列化头之后插入我们的有效负载。我们可以通过查看序列化流的前五个字节来识别这些情况。如果这五个字节是四字节序列化头(0xAC ED 00 05),后跟上面列出的值之一,那么我们可以通过发送我们自己的四字节序列化头和一个有效负载对象来攻击目标。
在其他情况下,四字节序列化头部最有可能后跟TC_BLOCKDATA元素(0x77)或TC_BLOCKDATALONG元素(0x7A)。前者由单字节长度字段组成,后面是构成实际块数据的多个字节,后者由四字节长度字段组成,后面是构成数据块的许多字节。如果块数据后面是readObject所支持的元素之一,那么我们可以在块数据之后注入一个有效负载。
我写了一个工具来支持我在这方面的一些研究,即SerializationDumper,我们可以使用这个工具来识别反序列化漏洞的入口点。该工具可以解析Java序列化流,并以人类可读的形式将其转储到文件中。如果流中包含readObject支持的一个元素类型,那么我们就可以用有效负载对象来替换该元素。以下是这个工具的使用示例:
$ java -jar SerializationDumper-v1.0.jar ACED00057708af743f8c1d120cb974000441424344 STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_BLOCKDATA - 0x77 Length - 8 - 0x08 Contents - 0xaf743f8c1d120cb9 TC_STRING - 0x74 newHandle 0x00 7e 00 00 Length - 4 - 0x00 04 Value - ABCD - 0x41424344 $ java -jar SerializationDumper-v1.0.jar ACED00057708af743f8c1d120cb974000441424344 STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_BLOCKDATA - 0x77 Length - 8 - 0x08 Contents - 0xaf743f8c1d120cb9 TC_STRING - 0x74 newHandle 0x00 7e 00 00 Length - 4 - 0x00 04 Value - ABCD - 0x41424344
在此示例中,流中包含TC_BLOCKDATA,后面跟着的TC_STRING就可以用有效负载进行替换。
序列化流中的对象在加载时被实例化,而不是在整个流被解析之后被实例化。这个事实使我们能够将有效负载注入到序列化流中,而不用担心纠正流的其余部分。有效载荷将在任何类型的验证发生之前和应用程序尝试从序列化流中读取更多数据之前反序列化和执行。
POP Gadget
确定了一个入口点,我们就可以为目标提供我们自己的序列化对象以进行反序列化,接下来我们需要的是POP gadget。如果我们可以访问源代码,那么我们可以查找 'readObject'方法和调用'ObjectInputStream.readObject'之后的代码 ,以便找出潜在的gadget的存在。
通常我们无法访问应用程序的源代码,但这并不妨碍我们利用反序列化漏洞,因为有许多常用的第三方库可以被定位出来。包括Chris Frohoff和Gabriel Lawrence在内的研究人员已经在各种库中发现了POP小工具链,并发布了一个名为“ ysoserial ”的工具 ,可以生成有效载荷对象。这个工具大大简化了攻击Java反序列化漏洞的过程!
ysoserial中包含很多小工具链,所以下一步是制定出哪些可以针对目标使用的方法。应用程序使用的第三方库或已经披露的安全问题也要关注。如果我们知道目标使用了哪些第三方库,那么我们可以选择合适的ysoserial有效载荷来进行尝试。不幸的是,这些信息可能不一定随时可用,在这种情况下,我们可以谨慎地循环遍历各种各样的ysoserial小工具链,直到找到我们可以使用的信息。应该注意这种方法,因为总是有触发未处理异常和崩溃目标应用程序的风险。只不过,目标将变得特别不稳定,
如果目标应用程序使用“ClassNotFoundException”响应了ysoserial的攻击有效负载,那么可能的是因为你所选小工具链所针对的库对目标应用程序不可用。如果消息是 “无法运行程序”的 “java.io.IOException” 异常则可能意味着小工具链起作用了,但是小工具链尝试执行的操作系统命令在服务器上不可用。
Ysoserial进行命令执行的有效负载是不返回命令输出结果的也就是没有回显。这是因为使用'java.lang.Runtime.exec(String)'有一些限制 。第一个是不支持shell操作符,如输出重定向和管道符。第二个是有效载荷命令的参数不能包含空格(例如我们可以使用“nc -lp 31313 -e /bin/sh”,但是我们不能使用 “perl -e ‘use Socket;…”,因为参数perl包含一个空格)。幸运的是有一个不错的有效载荷在线编码器/生成器可以解决这些限制:
http://jackson.thuraisamy.me/runtime-exec-payloads.html。
尝试利用反序列化漏洞 – DeserLab和SerialBrute
理解序列化以及反序列化的原理对于(例如面向对象的编程)有效地利用反序列化漏洞是非常重要的事情。比起其他一些常见的漏洞,反序列化漏洞会涉及到更多的知识,所以定制一个实践目标是很有帮助的。在这篇博客文章中,我创建并发布了一个名为“ DeserLab ”的演示应用程序,它在Java序列化格式之上实现了一个自定义的网络协议。该应用程序容易受到反序列化漏洞的攻击,可以配合本博客中提供的信息进行漏洞利用。
' SerialBrute '是两个Python脚本,是我编写的并用于自动化测试任意目标的ysoserial有效负载。第一个 SerialBrute.py可以重播TCP会话或HTTP请求,并在给定的点注入一个有效负载,而第二个“SrlBrt.py”是一个辅助脚本,可以在需要特殊处理的情况下进行修改并提供有效负载。两者都尝试通过查看返回的异常来检测有效载荷是有效或无效的。这些脚本只是用于演示的攻击工具,应谨慎使用,因为有可能会导致应用程序挂掉,但我个人已经成功地重播了TCP对话并注入了ysoserial小工具链。
参考
以下大多数参考文献都是在整个博客文章中提及到的,可能有助于你更多地了解反序列化漏洞以及漏洞利用。
以下介绍涵盖了面向对象的程序设计(POP),以及对于那些对面向回归的程序设计(ROP)有兴趣的人来说是很有用的。请注意,由于与POP的相似性(即控制现有代码),这里仅提及ROP表示; ROP技术本身与反序列化漏洞无关。
· 在PHP应用程序利用中代码重用/ROP进行漏洞利用 – Stefan Esser,Black Hat USA 2010(https://www.owasp.org/images/9/9e/Utilization-Code-Reuse-Or-Return-Oriented-Programming-In- PHP-Application-Exploits.pdf)
· 返回导向编程:无代码注入的漏洞利用 – Erik Buchanan等人,Black Hat USA 2008(https://www.blackhat.com/presentations/bh-usa-08/Shacham/BH_US_08_Shacham_Return_Oriented_Programming.pdf)
以下文章和PPT讨论了PHP和Java的反序列化漏洞:
· 反序列化用户提供的数据的另类思路
(https://heine.familiedeelstra.com/security/unserialize)
· CVE-2011-2894:Spring的反序列化漏洞RCE
http://www.pwntester.com/blog/2013/12/16/cve-2011-2894-deserialization-spring-rce/
· What do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and your application have in common? This Vulnerability (https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/)
· Practical PHP Object Injection (https://www.insomniasec.com/downloads/publications/Practical%20PHP%20Object%20Injection.pdf)
以下内容讨论了Java和.NET中JSON和XML库中的反序列化漏洞:
· Friday the 13th: JSON Attacks – Alvaro Muñoz and Oleksandr Mirosh, Black Hat USA 2017 (https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-Json-Attacks.pdf)
Java文档的以下部分描述了序列化数据格式和Serializable接口:
· 对象序列化流协议(https://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html)
· java.io.Serializable接口(https://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html)
本博客文章中提到的工具可以在以下链接中找到:
· ysoserial反序列化有效载荷生成器(https://github.com/frohoff/ysoserial/)
· Runtime.exec()有效载荷编码器(http://jackson.thuraisamy.me/runtime-exec-payloads.html)
· SerializationDumper(https://github.com/NickstaDB/SerializationDumper)
· SerialBrute(https://github.com/NickstaDB/SerialBrute/)
· DeserLab(https://github.com/NickstaDB/DeserLab)
最后,以下这些大牛对Java反序列化漏洞利用也做了很多研究工作:
· Chris Frohoff(https://twitter.com/frohoff)
· Gabriel Lawrence(https://twitter.com/gebl)
· Matthias Kaiser(https://twitter.com/matthias_kaiser)
· AlvaroMuñoz(https://twitter.com/pwntester)