web安全101之反序列化
序列化的核心思维旨在,将A变成B,最后再从B还原回A。
总之,在一些条件苛刻或者变化无常的环境与需求中,产生了这种灵活的可逆性的B的中间体。
理解不安全反序列化的最好方法是了解不同的编程语言如何实现序列化和反序列化。 这里的序列化与反序列化指的是程序语言中自带的实施与实现。而非自创或者自定义的序列化与反序列化机制(比如:N进制形式,hashmap,树型等其他数据结构里的序列化中间体)。
总之,大多数面向对象的编程语言都具有类似的序列化和反序列化的接口或者说函数,但它们序列化对象的格式不同。 一些编程语言还允许开发人员序列化为其他标准化格式,例如 JSON , YAML,N进制形式,hashmap,树型等其他数据结构。
PHP前置知识
我们将已PHP与JAVA为例。
debug
您需要安装 PHP 和 Java。或者使用在线运行时环境(并非所有的在线版都支持序列化与反序列化功能)
PHP序列化
有时会将PHP反序列化称作PHP对象注入,但它们描述的是同一个东西。
https://extendsclass.com/php.html 在线编辑器
我们创建了类User,定义了变量,并实例化类和给它的变量赋值,最后生成序列化对象并echo。
PHP序列化的基本结构是 数据类型 : 数据
数据类型以及后面跟随的数据如下
b:THE_BOOLEAN; # 布尔值
i:THE_INTEGER; # 整数
d:THE_FLOAT; # 浮点
s:LENGTH_OF_STRING:"ACTUAL_STRING"; # 字符串
a:NUMBER_OF_ELEMENTS:{ELEMENTS} # 数组
O:LENGTH_OF_NAME:"CLASS_NAME":NUMBER_OF_PROPERTIES:{PROPERTIES} # 对象
请注意数据类型的符号:O与s
对象:名字长度:类名称:属性个数:{属性} {字符串:字符串长度:实际的字符串}
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
PHP序列化与反序列化
serialize函数将其序列化,然后unserialize再将其反序列化,var_dump函数查看反序列化的变量以及属性
调试代码
<?php
class User{
public $username;
public $status;
}
$user = new User;
$user->username = 'vickie';
$user->status = 'not admin';
$serialized_string = serialize($user);
$unserialized_data = unserialize($serialized_string);
var_dump($unserialized_data);
var_dump($unserialized_data->status);
?>
基于对象属性或值的注入类漏洞
前置知识告诉我们,原来有一个序列化函数,从攻击者可控的地方将其序列化为中间格式,然后再反序列化。如同所有注入类的漏洞一样,我们无法想象服务器究竟用了什么函数,方法,属性或者配置是否存在缺陷等。因此可控地方的payload产生的情况具体例子具体讨论。
因为在前置知识中,我们明显的发现序列化之后的字符串是人类可读的。因此,我们可以将not admin修改为admin的逻辑漏洞型,以至于和其他的修改包没有任何差距性。又或者将其他注入类payload置于属性名或者值中。
一切都因为PHP的序列化是人类可读的。但遗憾的是,并非所有的语言都是这种人类可读的。因此,我们需要在最初就赋值(准备好),然后再使用语言附带的序列化函数生成这种序列化的中间体来发送payload(bp抓到的包,人类不可读,无从修改与放置payload)
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
基于反序列化引擎的漏洞
PHP对象的创建与销毁
如果对象使用了PHP魔术方法,这些方法将具有魔术属性,它们有的会在某些执行点或满足某些条件时自动运行。
其中两个神奇的方法是:__wakeup()
和__destruct()
官方手册给出了反序列化函数的__wakeup() 解释,如果是对象则会自动调用,这是它的执行点,触发点。
__wakeup() 高频出现的缘由
unserialize() 在还原时遇到对象就会执行 __wakeup() 方法并执行其中的代码。
__wakeup() 方法通常用于重建对象可能拥有的任何资源,重新建立序列化期间丢失的任何数据库连接,以及执行其他重新初始化任务。 它在 PHP 对象注入攻击期间通常很有用,因为它为服务器的数据库或程序中的其他函数提供了一个方便的入口点。
析构函数__destruct()
:没人用这个对象时就会调用析构函数来销毁对象。
这也算是具备一个销毁,删除的功能点了。攻击者可能通过控制传递给这些函数的输入来破坏文件系统的完整性。
反序列化的RCE
当您控制传递给 unserialize() 的序列化对象时,您就控制了所创建对象的属性。 如果在这些可控的基础之上您还可以控制传递给自动执行的方法(如__wakeup()
或__destruct()
)的值。 就有可能实现 RCE。
序列化对象属性--> 自动执行的方法(魔术方法等引擎)--> 根据具体函数等功能点看看是否可控,是否能RCE。
代码示例
参考资料:https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
比如下面的对象属性hook,就有一个危险函数eval() 。如果hook可控,其值就传递到__wakeup()中的eval()中,形成了上述的反序列化的RCE条件。
class Example2
{
private $hook;
function __construct(){
// some PHP code...
}
function __wakeup(){ # 反序列化时遇到对象自动执行
if (isset($this->hook)) eval($this->hook); # eval危险方法
}
}
// some PHP code...
$user_data = unserialize($_COOKIE['data']);
要让hook可控,就需要将cookie的data设置为序列化的Example2 对象
将 hook 属性设置为您想要执行的任何 PHP 代码。将payload赋值给hook。
class Example2
{
private $hook = "phpinfo();"; # 基于对eval()的理解,直接将payload赋值给可控属性
}
print urlencode(serialize(new Example2));
1.序列化的 Example2 对象作为数据 cookie 传递到程序中,而不是特殊语义符号,因此需要对数据进行url编码urlencode()。
2.程序对数据 cookie 调用 unserialize()。
3.因为数据 cookie 是一个序列化的 Example2 对象,所以 unserialize() 会实例化一个新的 Example2 对象。
4.unserialize() 函数看到 Example2 类已经实现,所以调用了__wakeup()
。
5.__wakeup() 函数查找对象的 $hook 属性,如果它不为 NULL,则运行 eval($hook)。
6.$hook 属性不是 NULL,因为它被赋值为 phpinfo();,所以 eval("phpinfo();") 被运行。
让我们再回头看看这个总结:序列化对象属性--> 自动执行的方法(魔术方法等引擎)--> 根据具体函数等功能点看看是否可控,是否能RCE。
使用其他魔术方法
__toString()
仅当对象被视为字符串时才调用方法
当调用未定义的方法时,程序会调用__call()
方法。 例如,对 $object->undefined($args) 的调用将变成 $object->__call('undefined', $args)。
以上提及的四种魔术方法是出场率最高的,如果遇到其他需求,可翻翻官方手册
https://www.php.net/manual/en/language.oop5.magic.php
使用POP链或称为“小工具”链
当我们使用以上提及的魔术方法中没有那么直接的危险的eval()出现时,就形成了一种现象:这些属性确实可控,但几个方法又没有可执行函数。
那么我能不能用这些可控的属性拼拼凑凑的将它们形成gadgets(小玩具),串起来变成POP链(小工具)链?【太多的术语都在表达同一个意思了。。】
总之:所有的情况都在这里,不是直接利用就是间接的拼拼凑凑来利用。您可能会遇到非常多的叫法:比如小工具,链,POP,gadget,chain,甚至利用工具ysoserial等全部都可以理解为就是一个东西。都包含在这两种情况里。
这些小工具凭借的是:从代码库中借用的代码片段。 POP 链使用魔法方法作为他们的初始小工具。 然后攻击者可以使用这些方法来调用其他小工具。因为涉及代码库,那就会随着时间的变迁出现以前版本代码库中的可行变为无效,需要结合如今的代码库进行修正与比对。
实例理解:以下代码为代码片段,但是没有形成闭环。无法调用CodeSnippet类中的危险方法evaluate()
https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
class Example
{
private $obj;
function __construct()
{
// some PHP code...
}
function __wakeup()
{
if (isset($this->obj)) return $this->obj->evaluate();
}
}
class CodeSnippet
{
private $code;
function evaluate() # 无法调用CodeSnippet类中的危险方法evaluate()
{
eval($this->code);
}
}
// some PHP code...
$user_data = unserialize($_POST['data']);
// some PHP code...
我们想从以下流程让RCE变得实际可行。因为危险方法执行的变量是code,所以payload最终需要交付给$code
由于最后一行包含不安全的反序列化漏洞,攻击者可以使用以下代码生成序列化对象:
class CodeSnippet
{
private $code = "phpinfo();";
}
class Example
{
private $obj;
function __construct()
{
$this->obj = new CodeSnippet; # 生成CodeSnippet类,让危险方法变得可以调用
}
}
print urlencode(serialize(new Example));
由此可见:POP 链通过链接和重用应用程序代码库中的代码来实现 RCE。
它的利用思维与二进制的ROP同源:因为存在可执行空间保护(数据执行防护),使得传统的缓冲区溢出不切实际,它将用户可写内存区域的可执行权限割掉了,无法执行是保护的关键所在。为了绕过这个保护,ROP操作的是返回地址,就像上述的POP链一样,操作同名的存在于内存(代码库)里面的“类”,将危险函数,指令串起来。
https://en.维科.org/wiki/Return-oriented_programming
JAVA前置知识
java 在线版编译器
https://www.tutorialspoint.com/compile_java_online.php
在java环境中有许多应用程序处理序列化对象
java序列化与反序列化
读取就是还原的过程,肯定是反序列化操作函数 readObject()
要使 Java 对象可序列化,使用 java.io.Serializable 接口。 这些类还实现了特殊方法 writeObject() 和 readObject(),以分别处理该类对象的序列化和反序列化。 让我们看一个例子。以下代码在名为 SerializeTest.java 的文件中:
import java.io.ObjectInputStream; # 对象输入输出
import java.io.ObjectOutputStream;
import java.io.FileInputStream; # 文件输入输出
import java.io.FileOutputStream;
import java.io.Serializable; # 可序列化
import java.io.IOException; # IO 异常
class User implements Serializable{ # 定义 User类可序列化(解锁序列化和反序列化操作)
public String username; # 属性 username
}
public class SerializeTest{ # 文件名通常与公开类名一致
public static void main(String args[]) throws Exception{
User newUser = new User(); # 实例化
newUser.username = "vickie"; # 赋值
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(newUser); # 序列化并写入文件
os.close();
FileInputStream is = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(is);
User storedUser = (User)ois.readObject(); # 读取文件并反序列化
System.out.println(storedUser.username);
ois.close();
}
}
编译与执行
在java应用程序中,HTTP头,参数,cookie都可以被用于序列化对象
Java 序列化对象不像 PHP 序列化字符串那样的人类可读,但还算是有迹可寻。
特征:以十六进制的 AC ED 00 05 或 base64 的 rO0 开头。(您可能会在参数或者cookie的地方看见它们的存在)
特征:HTTP Content-Type 头设置为 application/x-java-serialized-object。
注意
由于 Java 序列化对象包含许多特殊字符,因此在传输之前对它们进行编码是很常见的,因此还要注意这些签名的不同编码版本。前文说过,各语言的不同版本与弃用。
因为人类不可读,您也不可能直接修改十六进制。往往是先从源码放置payload,再将其序列化为中间态,最后发送给服务器。
利用思路还是基于对象属性与值的修改或者通过POP链的思路尝试创建任何类的对象来构造RCE。
JAVA版本的POP链
这与PHP的POP链没有任何本质上的区别。查找和链接小工具以制定漏洞利用可能非常耗时,因为您也无法提前就知道具体的应用加载了哪些库,库里面哪些类可用,只能用已知来枚举未知。
流行库中的小工具创建漏洞利用链发挥了巨大的作用。
使用流行的,常用的这些命名空间来枚举未知。总不能现场直接给它挖个0day出来吧。
例如 Apache Commons-Collections、Spring Framework、Apache Groovy 和 Apache Commons FileUpload
自动化利用工具Ysoserial (理解上类似于sqlmap)
Ysoserial (https://github.com/frohoff/ysoserial/) ,使用常见的 Java 库中发现的一组小工具链来生成payload
$ java -jar ysoserial.jar gadget_chain command_to_execute // 语法格式
$ java -jar ysoserial.jar CommonsCollections1 calc.exe // 实例:弹计算器
很明显:在使用工具时,它的第一个参数确切的指定了是哪个库,您在不知道的情况下甚至可以全部怼一遍所有可选的库。。但看得明白想得清楚才是正路,尽量去识别出目标应用程序使用了哪些易受攻击的库。侦察与观察很重要。
参考资料 https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet/
防御
防御反序列化漏洞很困难。 根据所使用的编程语言、库和序列化格式,保护应用程序免受这些漏洞影响的最佳方法有很大差异。 不存在一刀切的解决方案。
输入检查
白名单反序列化允许的类
防止篡改序列化 cookie,可以跟踪服务器上的会话状态
使用简单的数据类型,如字符串和数组,能不用序列化就别用
留意补丁并确保依赖项是最新的
owasp反序列化备忘单:https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html
感谢师傅们很有耐心的阅读到了这里。我们还会再见面的。共勉。