序列化是将复杂的数据结构(例如对象及其字段)转换为字节序列的过程。以便在网络上传输或者保存在本地文件中。
进行序列化之后,在传递和保存对象的时候,对象的状态以及相关的描述信息依旧是完整的并且可进行传递。
序列化机制的核心作用就是对象状态的保存与重建。
反序列化是将字节流还原为原始对象的过程,反序列化之后的对象其状态与序列化时的状态完全相同。然后,网站的逻辑可以与此反序列化的对象进行交互,就像与任何其他对象进行交互一样。
许多编程语言为序列化提供本机支持。对象的确切序列化方式取决于语言。某些语言将对象序列化为二进制格式,例如JAVA;而其他语言则使用不同的字符串格式,序列化之后的字节序列都具有不同程度的可读性,并且,所有原始对象的属性都存储在字节序列中。
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对象始终以相同的字节开头,这些字节的编码方式为ac ed 十六进制和rO0Base64。
①java.io.ObjectOutputStream
:表示对象输出流;
它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;
②java.io.ObjectInputStream
:表示对象输入流;
它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回;
只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!
假定一个User类,它的对象需要序列化,可以有如下三种方法:
①若User类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化
ObjectOutputStream采用默认的序列化方式,对User对象的非transient的实例变量进行序列化。
ObjcetInputStream采用默认的反序列化方式,对对User对象的非transient的实例变量进行反序列化。
②若User类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。
ObjectOutputStream调用User对象的writeObject(ObjectOutputStream out)的方法进行序列化。
ObjectInputStream会调用User对象的readObject(ObjectInputStream in)的方法进行反序列化。
③若User类实现了Externalnalizable接口,且User类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
ObjectOutputStream调用User对象的writeExternal(ObjectOutput out))的方法进行序列化。
ObjectInputStream会调用User对象的readExternal(ObjectInput in)的方法进行反序列化。
①:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\object.out"));
②:通过对象输出流的writeObject()方法写对象:
oos.writeObject(new User("Jenny", "15"));
①:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));
②:通过对象输出流的readObject()方法读取对象:
User user = (User) ois.readObject();
为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。
为了更好地理解Java序列化与反序列化,举一个简单的示例如下:
public class TestDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User user1 = new User("Jenny", "15");
oos.writeObject(user1);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("object.out");
ObjectInputStream ois = new ObjectInputStream(fis);
User user2 = (User) ois.readObject();
System.out.println(user2.getUserName()+ " " +
user2.getPassword() + " " + user2.getSex());
//反序列化的输出结果为:Jenny 15
}
}
public class User implements Serializable {
private String userName;
private String password;
private String sex;
}
反序列化漏洞是指网站对用户可控制的数据进行反序列化时,攻击者能够操纵序列化的对象,将有害数据传递到应用程序代码中。甚至有可能用完全不同类的对象替换序列化的对象。
意外类的对象可能会导致异常。但是,到此时,攻击者对反序列化漏洞的利用可能已经完成。许多基于反序列化的攻击是在反序列化完成之前完成的。这意味着即使网站本身的功能未与恶意对象直接交互,反序列化过程本身也可以发起攻击。
无论是白盒测试还是黑盒测试,我们都需要对序列化内容和反序列化内容进行查看。不同的开发语言,序列化和反序列化都有其特点。
前面我们提到,反序列化是把字节序列转换为对象的这一过程,这个字节序列包含了对象的属性等全部内容。所以我们可以通过反序列化的过程来将我们的恶意数据传递到服务器上。
广义上讲,在处理序列化对象时可以采用两种方法。您可以直接以其字节流形式编辑该对象,也可以用相应的语言编写一个简短的脚本来自己创建和序列化新对象。使用二进制序列化格式时,后一种方法通常更容易。
当篡改数据时,只要攻击者保留有效的序列化对象,反序列化过程将创建具有修改后的属性值的服务器端对象。
例子:考虑一个使用序列化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
}
此易受攻击的代码将User基于cookie中的数据(包括攻击者修改的isAdmin属性)实例化对象。绝对不会检查序列化对象的真实性。然后将这些数据传递到条件语句中,在这种情况下,将允许轻松地进行特权升级。
基于PHP的逻辑由于==
在比较不同数据类型时,编译器会先将比较符号两端的数据转化为同一类型,这时,我们可以利用这一特性。
例如,在整数和字符串之间进行松散比较,PHP将尝试将字符串转换为整数,即结果5 == "5"为true
。
异常地,这也适用于以数字开头的任何字母数字字符串。在这种情况下,PHP将根据初始数字有效地将整个字符串转换为整数值。字符串的其余部分将被完全忽略。因此,5 == "5 of something" 在实践中被视为5 == 5
。
当将字符串与整数进行比较时,因为没有数字,所以字符串中的数字为0。PHP将整个字符串视为整数00 == "Example string" // true
在这种比较运算符与反序列化对象中用户可控制的数据一起使用时,可能会导致危险的逻辑缺陷。
$login = unserialize($_COOKIE)
if ($login['password'] == $password) {
// log in successfully
}
假设攻击者修改了password属性,使其包含整数0而不是预期的字符串。只要存储的密码不是以数字开头,该条件将始终返回true,从而绕过身份验证。如果代码直接从请求中获取了密码,则密码0将被转换为字符串,条件的计算结果为false。
以任何序列化的对象格式修改数据类型时,同事也需要更新序列化数据中的任何类型标签和长度指示符,否则,序列化的对象将被破坏,并且不会被反序列化。
除了简单地检查属性值之外,网站的功能还可能会对反序列化对象中的数据执行危险的操作。在这种情况下,可以使用反序列化漏洞来传递意外数据,并利用相关功能造成损害。
例如,作为网站“删除用户”功能的一部分,通过访问$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()
这种方式声明的方法充当了反序列化期间调用的魔术方法。
应该密切注意包含这些魔术方法类型的所有类。它们允许在完全反序列化对象之前将数据从序列化的对象传递到网站的代码中。这个是我们利用这个漏洞的起点。
有时候可以通过编辑网站提供的对象来利用反序列化漏洞。
在面向对象的编程中,对象可用的方法由其类确定。因此,如果攻击者可以操纵将哪类对象作为序列化数据传递,则他们可以影响在反序列化之后(甚至在反序列化期间)执行什么代码。
反序列化方法通常不会检查正在反序列化的内容。这意味着可以向网站传入任何可序列化类的对象,并且该对象将被反序列化。这使攻击者可以创建任意类的实例。攻击者构造的对象类型可能会导致应用程序逻辑中的异常,但是再出现异常的同时,恶意对象将已经实例化。
如果可以访问源代码,那我们可以查看所有可用的类。构造一个简单的漏洞利用程序,寻找包含反序列化魔术方法的类,然后检查它们是否可以对可控制的数据执行危险操作。然后,可以传入此类的序列化对象,以使用其魔术方法进行利用。
包含这些反序列化魔术方法的类也可以用于发起更复杂的攻击,涉及一系列的方法调用,称为“工具链”。
“工具链”是应用程序中存在的代码片段,可以帮助攻击者实现特定目标。单个工具链可能不会直接对用户输入造成任何有害影响。但是,攻击者的目标可能只是调用一种方法,该方法会将其输入传递给另一个工具链。通过以这种方式将多个工具链链接在一起,攻击者可以潜在地将其输入传递到危险的“接收器工具链”中,从而在其中造成最大的破坏。
与某些其他类型的利用不同,工具链不是攻击者构建的链式方法的有效负载。网站上已经存在所有代码。攻击者唯一控制的是传递到工具链中的数据。通常使用反序列化期间调用的魔术方法(有时称为“启动工具链”)完成此操作。
①使用预建的工具链
手动识别工具链是一个相当艰巨的过程,并且如果没有源代码访问,几乎是不可能的。但是可以首先尝试一些使用预建工具链的选项。
有几种可用的工具可以帮助我们轻松地构建工具链。这些工具提供了一系列已在其他网站上利用的预先发现的工具链。在目标站点上发现了反序列化漏洞之后,即使无权访问源代码,也可以使用这些工具来尝试利用。例如,如果可以在一个网站上利用依赖Java的Apache Commons Collections库的工具链,那么使用同一链也可以利用实现该库的任何其他网站。
②ysoserial
Java反序列化攻击的一种此类工具是“ ysoserial”。只需指定认为目标应用程序正在使用的库,然后提供要尝试执行的命令即可。该工具会根据给定库知道的工具链来创建适当的序列化对象。
③PHPGGC
大多数经常遭受反序列化漏洞困扰的语言都具有其验证工具。例如,对于基于PHP的站点,可以使用“ PHP通用小工具链”(PHPGGC)。
造成该漏洞的原因不是网站代码或其任何库中存在工具链。漏洞是用户可控制数据的反序列化过程或数据,工具链只是在注入数据后操纵该数据流的一种方式。这也适用于依赖于不信任数据的反序列化的各种内存损坏漏洞。因此,即使网站设法以某种方式设法插入每个可能的工具链,它们仍可能会受到攻击。
④创建自己的工具链
查看是否有任何已知漏洞可以用来攻击目标网站。如果没有自动生成序列化对象的专用工具,也可以找到流行框架的文档资料创建工具链,并手动进行调整。
要成功构建自己的小工具链,需要访问源代码。第一步是研究源代码,识别包含反序列化期间调用的魔术方法的类。评估此魔术方法执行的代码,以查看它是否对用户可控制的属性有直接危险。
如果不可单独使用魔术方法,则可以将其用作工具链的“启动小工具”。研究启动小工具调用的任何方法。这些操作是否会对控制的数据造成危险?如果不是,查看它们随后调用的每个方法,依此类推。
重复此过程,跟踪可以访问的值,直到达到死胡同或确定可控数据传递到的危险接收器工具为止。
一旦确定了如何在应用程序代码中成功构建工具链,下一步就是创建一个包含有效负载的序列化对象。这只是研究源代码中的类声明并创建一个有效的序列化对象的案例,该对象具有利用漏洞所需的适当值。
⑤PHAR反序列化(具体可查看https://paper.seebug.org/680/)
在PHP中,没有使用unserialize()方法,有时也可以利用反序列化。
大多数PHP文件操作允许使用各种URL协议去访问文件路径:如data://,zlib://或php://。
例如常见的:
include('php://filter/rea,d=convert.base64-encode/resource=index.php');
include('data://text/plain;base64,xxxxxxxxxxxx');
phar://也是流包装的一种,它提供了用于访问PHP Archive(.phar)文件的流接口
它提供了用于访问PHP Archive(.phar)文件的流接口,在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
⑥利用内存损坏利用反序列化
如果其他所有方法均失败,则通常存在公开记录的内存损坏漏洞,可以通过反序列化来利用该漏洞,通常会导致远程执行代码。
1、除非绝对必要,否则应避免对用户输入进行反序列化。在许多情况下,反序列化带来的危害可能大于带来的利益。
2、如果确实需要反序列化来自不受信任来源的数据,需确保数据未被篡改。例如,实施数字签名来检查数据的完整性。并且,在开始反序列化过程之前,也需要对数据进行检查。
3、应避免完全使用通用的反序列化功能。这些方法的序列化数据包含原始对象的所有属性,包括可能包含敏感信息的私有字段,键值对等。可以创建自己的特定于类的序列化方法,以便可以控制公开哪些字段。
参考:
https://portswigger.net/web-security/deserialization/exploiting
https://paper.seebug.org/680/