Insecure deserialization

在本节中,我们将介绍什么是不安全的反序列化,并描述它是如何使网站遭受高危害性攻击的。我们将重点介绍典型的场景,并演示一些 PHP、Ruby 和 Java 反序列化的具体示例。最后也会介绍一些避免不安全的反序列化漏洞的方法。

Web 安全 之 Insecure deserialization_Web 安全

利用不安全的反序列化通常比较困难。然而,它有时比你想象的要简单得多。如果您不熟悉反序列化,那么本节将包含一些重要的背景信息,您应该首先熟悉这些信息。如果您已经了解反序列化的基础知识,那么可以直接跳到学习如何利用它。

什么是序列化

序列化是将复杂的数据结构(如对象及其字段)转换为“更扁平”的格式的过程,该格式的数据可以作为字节流序列发送和接收。序列化的数据让以下过程更简单:

  • 将复杂数据写入进程间内存、文件或数据库
  • 发送复杂数据,例如,通过网络或API调用,在应用程序的不同组件之间传递复杂数据

关键的是,当序列化一个对象时,其状态也将保持不变。换句话说,对象的属性及其赋值都会被保留。

序列化 vs 反序列化

反序列化是将字节流还原为与原始对象完全相同的副本的过程。然后,网站的逻辑就可以与这个反序列化的对象进行交互,就像与任何其他对象进行交互一样。

Web 安全 之 Insecure deserialization_Web 安全_02

许多编程语言提供对序列化的本地支持。具体如何序列化对象取决于具体语言。一些语言将对象序列化为二进制格式,而另一些语言则会序列化为具有不同程度的可读性的字符串格式。请注意,原始对象的所有属性都存储在序列化数据流中,包括所有私有字段。为了防止字段被序列化,必须在类声明中将其显式标记为"transient" 。

请注意,当使用不同的编程语言时,序列化可能被称为 marshalling(Ruby)或 pickling(Python),这些术语与“序列化”同义。

什么是不安全的反序列化

不安全的反序列化是指用户可控制的数据被网站反序列化。这会使攻击者能够操纵序列化的对象,以便将有害数据传递到应用程序代码中。

甚至可以用完全不同类的对象替换序列化对象。令人担忧的是,网站上任何可用的类的对象都将被反序列化和实例化,而不管这个类是不是预期的类。因此,不安全的反序列化有时被称为 "object injection" 对象注入漏洞。

一个意外类的对象可能会导致异常。不过,在此之前,损害可能已经造成。许多基于反序列化的攻击在反序列化结束之前就已经完成。这意味着可以攻击反序列化的过程本身,即使网站的功能不直接与恶意对象交互。因此,其逻辑基于强类型语言的网站也容易受到这些技术的攻击。

不安全的反序列化漏洞是如何出现的

不安全的反序列化出现通常是因为人们普遍缺乏对用户可控数据进行反序列化的危险程度的了解。理想情况下,根本不应该对用户输入进行反序列化。

有些网站所有者认为他们很安全,因为其会对反序列化的数据进行某种形式的附加检查。然而,这种方法通常是无效的,因为几乎不可能验证或预料到所有可能发生的情况。这些检查在根本上也是有缺陷的,因为它们依赖于在数据被反序列化后对其进行检查,在许多情况下,这对于防止攻击来说已经太晚了。

漏洞产生也可能是因为反序列化的对象通常被认为是可信的。特别是在使用二进制序列化格式的语言时,开发人员可能会认为用户无法有效地读取或操作数据。然而,尽管这可能需要更多努力,但攻击者利用二进制序列化对象的可能性与利用基于字符串的格式的可能性是一样的。

由于现代网站中存在大量依赖项,因此基于反序列化的攻击也成为可能。一个站点可能使用许多不同的库,每个库也都有自己的依赖项,这就产生了一个难以安全管理的存在大量类和方法的池子。由于攻击者可以创建这些类中的任何一个实例,因此很难预测可以对恶意数据调用哪些方法。如果攻击者能够将一长串意外的方法调用链接在一起,并将数据传递到与初始源完全无关的接收器中,则尤其如此。因此,几乎不可能预测到恶意数据的流动并堵塞每个潜在的漏洞。

简而言之,安全地反序列化不受信任的输入是不可能的。

不安全的反序列化会造成什么影响

不安全的反序列化的影响可能非常严重,因为它提供了一个切入点,从而导致攻击面大幅增加。它允许攻击者以有害的方式重用现有的应用程序代码,从而导致许多其他漏洞,比如远程代码执行.

即使在无法执行远程代码的情况下,不安全的反序列化也可能导致权限提升、访问任意文件和拒绝服务攻击。

如何利用不安全的反序列化漏洞

下文会有详细说明。

如何防止不安全的反序列化漏洞

一般来说,除非绝对必要,否则应该避免用户输入的反序列化。在许多情况下,防御其潜在的高危漏洞的难度超过了其带来的好处。

如果确实需要反序列化来自不受信任的源的数据,请采取强大的措施以确保数据未被篡改。例如,您可以实现一个数字签名来检查数据的完整性。但是,请记住,任何检查都必须在开始反序列化之前进行。否则,检查就没什么用处了。

如果可能,您应该避免使用通用的反序列化功能。这些方法的序列化数据包含了原始对象的所有属性,以及可能包含敏感信息的私有字段。相反,你应该创建自己特定类的序列化方法,以控制公开字段。

最后,请记住,该漏洞是用户输入的反序列化,而不是随后处理数据的工具链的存在。不要依赖于试图消除测试过程中识别的工具链,由于跨库依赖的存在,这是不切实际的。在任何给定的时间,公开记录的内存损坏漏洞也意味着应用程序可能会受到攻击。


利用 insecure deserialization 漏洞

在本节中,我们将通过 PHP、Ruby 和 Java 反序列化的示例来教你如何利用一些常见漏洞场景。我们希望证明利用不安全的反序列化实际上比许多人认为的要容易得多。如果你能够使用预先构建的工具链,那么即使在黑盒测试期间也是如此。

我们还将指导你创建基于反序列化高危漏洞的攻击。尽管这些通常需要访问源代码,但是一旦理解了基本概念,它们也比你想象的更容易学习。我们将讨论以下主题:

  • 如何识别不安全的反序列化
  • 修改网站所需的序列化对象
  • 将恶意数据传递到危险的网站功能中
  • 注入任意对象类型
  • 链式方法调用以控制数据流入危险的接收器中
  • 手动创建自己的高级的漏洞利用
  • PHAR 反序列化

注意:尽管许多实验和示例都基于PHP,但大多数开发技术对其他语言也同样有效。

如何识别不安全的反序列化

识别不安全的反序列化相对来说比较简单,无论你使用白盒测试还是黑盒测试。

在审核过程中,你应该查看网站所有传入数据,并尝试识别出任何类似于序列化的数据。如果你知道不同语言使用的格式,则可以相对容易地识别序列化的数据。在本节中,我们将展示 PHP 和 Java 序列化的示例。一旦确定了序列化的数据,就可以测试是否能够控制它。

PHP 序列化格式

PHP 使用了一种几乎可读的字符串格式,字母表示数据类型,数字表示每个部分的长度。例如,假设一个 User 对象具有以下属性:

$user->name = "carlos";
$user->isLoggedIn = true;

序列化之后,此对象可能如下所示:

O:4:"User":2:{s:4:"name":s:6:"carlos"; s:10:"isLoggedIn":b:1;}

其含义是:

  • O:4:"User" - 一个对象,类名是 4 个字符的 "User"
  • 2 - 对象有 2 个属性
  • s:4:"name" - 第一个属性的键是 4 个字符的字符串 "name"
  • s:6:"carlos" - 第一个属性的值是 6个字符的字符串 "carlos"
  • s:10:"isLoggedIn" - 第二个属性的键是 10 个字符的字符串 "isLoggedIn"
  • b:1 - 第二个属性的值是布尔值 true

PHP 序列化的本地方法是 serialize() 和 unserialize() 。如果你有源代码的访问权限,则应该首先在所有位置查找 unserialize() 并进行进一步调查。

Java 序列化格式

有些语言,如 Java ,使用二进制序列化格式。这更难阅读,但如果知道如何识别一些信号,则仍然可以识别序列化的数据。例如,序列化的 Java 对象总是以相同的字节开头,这些字节被编码为十六进制 ac ed 和 Base64 的 rO0。

实现接口 java.io.Serializable 的任何类都可以序列化和反序列化。如果你有源代码的访问权限,请注意使用 readObject() 的方法,该方法用于从 InputStream 中读取并反序列化数据。

操纵序列化对象

利用某些反序列化漏洞就像更改序列化对象中的属性一样容易。当对象状态被持久化时,你可以研究序列化数据以识别和编辑感兴趣的属性值。然后,通过反序列化过程将恶意对象传递给网站。这是基本的反序列化攻击的初始步骤。

广义地说,在操纵序列化对象时可以采用两种方法。你可以直接以字节流的形式编辑对象,也可以用相应的语言编写一个简短的脚本来自己创建和序列化新对象。使用二进制序列化格式时,后一种方法通常更容易。

修改对象属性

在篡改数据时,只要攻击者保留一个有效的序列化对象,反序列化过程将使用修改后的属性值创建一个服务器端的对象。

作为一个简单的示例,假设一个网站使用序列化对象 User 在 cookie 中存储有关用户会话的数据。如果攻击者在 HTTP 请求中发现了这个序列化对象,他们可能会对其进行解码以找到以下字节流:

O:4:"User":2:{s:8:"username";s:6:"carlos";s:7:"isAdmin";b:0;}

这个 isAdmin 属性很容易引起攻击者的兴趣。攻击者只需将这个属性的布尔值更改为 1(true),然后重新编码对象,并用此修改后的值覆盖当前 cookie 。单独来看的话这没啥用。但是,如果网站使用此 cookie 检查当前用户是否有权访问某些管理功能:

$user = unserialize($_COOKIE);
if ($user->isAdmin === true) {
// allow access to admin interface
}

上述代码将基于来自 cookie 的数据实例化 User 对象,包括攻击者修改后的 isAdmin 属性,并且不会检查序列化对象的真实性。此时,修改后的数据就直接升级了权限。

这种简单的场景并不常见。然而,以这种方式编辑属性值展示了进行攻击的第一步。

修改数据类型

我们除了修改序列化对象中的属性值之外,也可以提供意外的数据类型。

像 PHP 这种弱类型语言,使用松散的比较运算符 == 比较不同的数据类型时特别容易受到这种操作的攻击。例如,如果在整数和字符串之间执行松散比较,PHP 将尝试将字符串转换为整数,这意味着 5 == "5" 计算结果为 true.

特别的是,这也适用于任何以数字开头的字母数字字符串。PHP 会将整个字符串转换为初始数字的整数值,字符串的其余部分将被完全忽略。因此,5 == "5 of something" 实际上被视为 5 == 5 。

当将字符串与整数 0 进行比较时,这变得更加奇怪:

0 == "Example string" // true

因为字符串中没有数字,PHP 会将整个字符串视为整数 0 。

考虑这样一种情况:将这个松散的比较运算符与来自反序列化对象的用户可控数据一起使用,这可能导致危险的逻辑缺陷。

$login = unserialize($_COOKIE)
if ($login['password'] == $password) {
// log in successfully
}

假设攻击者修改了 password 属性,使其为整数 0 而不是预期的字符串。那么只要存储的密码不是以数字开头,就会导致身份验证通过。请注意,这只是一种可能性,因为反序列化可保留数据类型,如果代码直接从请求中获取密码,则 0 将转换为字符串,并且条件的评估结果为 false 。

请注意,在修改任何序列化对象格式的数据类型时,务必记住也要更新序列化数据中的任何类型标签和长度指示符。否则,序列化的对象将损坏,并且不会反序列化。

当直接使用二进制格式时,我们建议使用 Hackvertor 扩展,其可以从 BApp store 中获得。使用 Hackvertor,你可以将序列化数据修改为字符串,它将自动更新二进制数据,并相应地调整偏移量,这可以节省大量的手动操作。

使用应用程序功能

除了简单地检查属性值之外,网站的功能还可能对反序列化对象中的数据执行危险的操作。在这种情况下,你可以使用不安全的反序列化来传递意外的数据,并利用相关功能造成损害。

例如,作为网站“删除用户”功能的一部分,通过访问 $user->image_location 属性可以删除用户的个人资料图片。如果这个 $user 来源于序列化对象,则攻击者可以通过传入一个修改了 image_location 的对象将其设置为任意一个文件路径。删除他们自己的用户帐户也会删除这个任意文件。

此示例依赖于攻击者通过用户可访问的功能手动调用危险方法。然而,当你构造将数据自动传递到危险方法的漏洞利用时,不安全的反序列化将变得更加有趣。这是通过使用“魔术方法”来实现的。

魔术方法

魔术方法是不必显式调用的方法的特殊子集。相反,它们会在特定事件或场景发生时自动调用。魔术方法是各种语言中面向对象编程的一个共同特征。它们有时通过在方法名前面加上前缀或用双下划线包围来表示。

开发人员可以向类中添加魔术方法,以便预先确定在相应的事件或场景发生时应该执行哪些代码。调用魔术方法的确切时间和原因因方法而异。PHP 中最常见的例子之一是 __construct() ,其在实例化类的对象时调用,类似于 Python 的 __init__ 。 通常,像这样的构造函数魔术方法包含初始化实例属性的代码。然而,开发人员可以自定义魔术方法来执行他们想要的任何代码。

魔术方法被广泛使用,其本身并不代表漏洞。但当它们执行的代码对攻击者可控制的数据(例如,来自反序列化对象的数据)进行处理时,它们可能变得危险。攻击者可利用此漏洞在满足相应条件时自动调用反序列化数据上的方法。

在这种情况下,最重要的是,某些语言具有在反序列化过程中自动调用的魔术方法。例如,PHP 的 unserialize() 方法查找并调用对象的  __wakeup() 神奇的方法。

在 Java 反序列化中,这同样适用于 readObject() 方法,它本质上类似于“重新初始化”序列化对象的构造函数。这个 ObjectInputStream.readObject() 方法用于从初始字节流中读取数据。但是,可序列化类也可以声明自己的 readObject() 方法如下:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {...};

这使得类可以更紧密地控制自己字段的反序列化。最重要的是以这种方式声明的 readObject() 方法充当了在反序列化期间调用的魔术方法。

你应该密切关注包含这类魔术方法的任何类。它们允许你在对象完全反序列化之前,将数据从序列化对象传递到网站代码中。这是利用更高级漏洞的起点。

注入任意对象

正如我们所看到的,偶尔可以通过编辑网站提供的对象来利用不安全的反序列化。然而,注入任意对象可以带来更多的可能性。

在面向对象编程中,对象可用的方法由其类决定。因此,如果攻击者可以操纵作为序列化数据传入的对象类,则可以影响反序列化之后,甚至在反序列化期间执行的代码。

反序列化方法通常不检查反序列化的内容。这意味着你可以传入网站可用的任何可序列化类的对象,并且该对象将被反序列化。这允许了攻击者创建任意类的实例。该对象不是预期类的事实并不重要。意外的对象类型可能会导致应用程序逻辑中的异常,但是恶意对象已经实例化。

如果攻击者有权访问源代码,他们可以详细研究所有可用的类。为了构造一个简单的攻击,他们会寻找包含反序列化魔术方法的类,然后检查其中是否有任何类对可控数据执行危险操作。然后,攻击者将会传入这个类的序列化对象,以使用其魔术方法进行攻击。

包含这些反序列化魔术方法的类也可用于发起更复杂的攻击,这涉及一系列的方法调用,称为 "gadget chain" 调用链。

调用链

"gadget" 是应用程序中存在的一段代码,可以帮助攻击者实现特定目标。单个 gadget 不能直接对用户输入造成任何有害影响。然而,攻击者的目标可能只是调用一个将其输入传递到另一个 gadget 的方法。通过以这种方式将多个 gadget 链接在一起,攻击者可能会将他们的输入传递到一个危险的 "sink gadget",从而造成最大的破坏。

重要的是要了解,与其他类型的攻击不同,gadget 链不是攻击者构建的链式方法的有效负载。所有的代码都已经存在于网站上。攻击者唯一控制的是传递到 gadget 链中的数据。这通常反序列化期间调用魔术方法来完成,有时称为“启动gadget”。

许多不安全的反序列化漏洞只能通过使用gadget链来利用。这有时可能是一个简单的一步或两步链,但构建高危害性攻击可能需要更精细的对象实例化和方法调用序列。因此,能够构造 gadget 链是成功利用不安全反序列化的关键因素之一。

使用预先构建的 gadget 链

手动识别 gadget 链可能是一个相当艰巨的过程,如果没有源代码访问,几乎不可能。幸运的是,有一些方法可以用来处理预先构建的 gadget 链,你可以先尝试一下。

有几种可用的工具可以帮助你以最小的工作量构建 gadget 链。这些工具提供了一系列预先发现的 gadget 链,这些 gadget 链在其他网站上被利用过。在目标站点上发现了不安全的反序列化漏洞后,即使你无权访问源代码,也可以使用这些工具尝试并利用它。由于包含可利用 gadget 链的库的广泛使用,这种方法成为可能。例如,如果一个依赖 Java 的 ApacheCommons Collections 库的 gadget 链可以在某个网站上被利用,那么使用该库的任何其他网站也可以使用同一个链进行攻击。

其中一个用于 Java 反序列化的工具是 "ysoserial" 。你只需指定一个你认为目标应用程序正在使用的库,然后提供一个要尝试并执行的命令,该工具就会根据已知的给定库的 gadget 链创建适当的序列化对象。这仍然需要一定量的尝试,但它比手工构建自己的 gadget 链要轻松得多。

大多数经常遭受不安全反序列化攻击的语言都有匹配的 proof-of-concept 工具。例如,对于基于 PHP 的站点,可以使用 "PHP Generic Gadget Chains"(PHPGGC)。

需要注意的是,网站代码或其任何库中存在的 gadget 链并不是导致该漏洞的原因。该漏洞是用户可控制数据的反序列化,gadget 链只是在数据被注入后操纵数据流的一种手段。这也适用于依赖于非可信数据反序列化的各种内存破坏漏洞。因此,即使他们设法管理每一个可能插入的 gadget 链,网站可能仍然是脆弱的。

使用有记录的 gadget 链

你可以看看是否有任何记录在案的漏洞利用,可以拿来攻击你的目标网站。即使没有用于自动生成序列化对象的专用工具,你仍然可以为流行框架找到有记录的 gadget 链并手动调整它们。

如果你找不到一个可以使用的 gadget 链,你仍然可以获得有价值的知识,你可以利用这些知识创建自己的自定义漏洞利用程序。

创建自己的漏洞利用

当现成的 gadget 链和有记录的漏洞攻击不成功时,你需要创建自己的漏洞利用。

为了成功地构建自己的 gadget 链,你几乎肯定需要访问源代码。第一步是研究此源代码,以识别包含反序列化期间调用的魔术方法的类。评估这个魔术方法执行的代码,看看它是否直接使用用户可控制的属性做任何危险的事情。

如果魔术方法本身不可利用,它可以作为你的 gadget 链的启动点。研究启动 gadget 调用的任何方法。这些操作是否会对你控制的数据造成危险?如果不是,请仔细查看它们随后调用的每个方法,依此类推。

重复此过程,跟踪你可以访问的值,直到你到达死胡同或识别出一个危险的 sink gadget ,你的可控数据被传递到其中。

一旦解决了如何在应用程序代码中成功地构造 gadget 链,下一步就是创建一个包含有效负载的序列化对象。这只需研究源代码中的类声明并创建一个有效的序列化对象,该对象具有利用漏洞所需的适当值。正如我们在以前的实验室中看到的,使用基于字符串的序列化格式时,这一点相对简单。

使用二进制格式,例如在构建 Java 反序列化漏洞时,可能会特别麻烦。在对现有对象进行小的更改时,直接使用字节可能会很舒服。但是,当进行更重要的更改时,例如传入一个全新的对象,这很快就变得不切实际了。为了自己生成和序列化数据,用目标语言编写自己的代码通常要简单得多。

在创建自己的 gadget 链时,要注意利用这个额外的攻击面触发次要漏洞的机会。

通过仔细研究源代码,你可以发现更长的 gadget 链,这些 gadget 链可能允许你构建高危险性攻击,通常包括远程代码执行.

PHAR 反序列化

到目前为止,我们主要研究了如何利用反序列化漏洞,即网站显式地反序列化用户输入。然而,在 PHP 中,有时即使没有明显使用 unserialize() 方法,也有可能可以利用反序列化漏洞。

当你访问不同的文件时,PHP 提供了不同的处理方式。其中之一是 phar:// ,它提供了一个流式接口来访问 PHP Archive (.phar) 文件。

PHP 文档揭示了 PHAR 清单文件包含序列化的元数据。至关重要的是,如果你对 phar:// 流执行文件系统操作,其元数据会被隐式的反序列化。这意味着 phar:// 流可能是利用不安全的反序列化的潜在点,前提是可以将此流传递到文件系统方法中。

对于明显危险的文件系统方法,例如 include() 或 fopen(),网站很可能已经实施了反制措施,以减少它们被恶意使用的可能性。然而,诸如 file_exists() 这类看起来没有明显危险的方法可能没有得到很好的保护。

此技术要求你通过某种方式将 PHAR 上传到服务器。例如,一种方法是使用图像上传功能。如果你能够将 PHAR 伪装成一个简单的 JPG 文件,你有时可以绕过网站的验证检查。如果你能强迫网站加载这个伪装成 JPG 的 PHAR 流,则任何通过 PHAR 元数据注入的有害数据都将被反序列化。由于 PHP 读取流时不检查文件扩展名,因此文件是否使用图像扩展名并不重要。

只要对象的类是由网站支持的,则 __wakeup()  和 __destruct() 魔术方法可以用这种方式调用,从而允许你使用这种技术启动一个 gadget 链。

通过内存破坏利用反序列化

即使不使用 gadget 链,也有可能利用不安全的反序列化。如果所有其他方法都失败,通常会有公开记录的内存损坏漏洞,可以通过不安全的反序列化来利用这些漏洞。这些通常会导致远程代码执行。

反序列化方法,例如 PHP 的 unserialize()  很少对这类攻击进行强化,暴露出大量的攻击面。其本身并不总会被认为是一个漏洞,因为这些方法一开始并不打算处理用户可控制的输入。