群里总有人问反序列漏洞是啥啊,学不明白啊,从来没听明白过, 面试有人问,闲聊也有人问;1个月前有人问,1个月后还有人在问
今天就来整理一下个人对反序列化漏洞理解和这个洞涉及的一些知识点
各种语言都有反序列化漏洞
序列化即将对象转化为字节流,便于保存在文件,内存,数据库中;反序列化即将字节流转化为对象
也就是把数据转化为一种可逆的数据结构,再把这种可逆的数据结构转化回数据,这就是序列化与反序列化
看到过一个通用的例子(可以应用到很多场景中,比如网络数据包传递接收过程之类的),买一个柜子,从北京运到上海,由于柜子形状怪异,不方便运输,先把它拆成板子,再装到箱子里,顺丰邮到买家手里,把板子拼装回柜子
0x01PHP反序列化漏洞(PHP对象注入漏洞)
PHP通过serialize() 与 unserialize()实现序列化与反序列化
常见的反序列化漏洞中出现的魔术方法及其触发条件
__construct()当一个对象创建时被调用
__destruct()当一个对象销毁时被调用
__toString()当一个对象被当作一个字符串时使用
__sleep() 在对象在被序列化之前运行
__wakeup() 如果有,在反序列化之前调用
友情提示:
private属性序列化的时候格式是 %00类名%00成员名 如testname (test->类名name->成员名)
protected属性序列化的时候格式是 %00*%00成员名 如*name (name->成员名)
序列化只序列化属性,不序列化方法,因此
反序列化的时候要保证在当前的作用域环境下有该类存在,类属性就是唯一的攻击突破口
(上文提到的__toString()触发条件非常多)
<?php class test{ public $name = 'lcx'; function __construct(){ echo "__construct()"; echo "<br><br>"; } function __destruct(){ echo "__destruct()"; echo "<br><br>"; } function __wakeup(){ echo "__wakeup()"; echo "<br><br>"; } function __toString(){ return "__toString()"."<br><br>"; } function __sleep(){ echo "__sleep()"; echo "<br><br>"; return array("name"); } } $test1 = new test(); $test2 = serialize($test1); print($test2); $test3 = unserialize($test2); print($test3); ?>
判断一下魔术方法运行顺序
关于PHP序列化输出结果举例
$a = array('a' => 'Apple' ,'b' => 'banana' , 'c' => 'Coconut');
//序列化数组
$s = serialize($a);
echo $s;
?>
运行结果为a:3:{s:1:"a";s:5:"Apple";s:1:"b";s:6:"banana";s:1:"c";s:7:"Coconut";}
数组:3个对象:{1个字符串类型a;5个字符串类型Apple;以此类推}
反序列化与之相反,不举例了
那么什么情况下会出现反序列化漏洞呢?
借鉴了大佬的一个经典案例来说明,经典永流传 (文件起名为1.php, PHP中var关键字在类内部等同于public)
<?php class Test{ var $test = "123"; function __wakeup(){ $fp = fopen("test.php", 'w'); fwrite($fp, $this -> test); fclose($fp); } } $test1 = $_GET['test']; print_r($test1); echo "<br />"; $seri = unserialize($test1); require "test.php"; ?>
构造payload
O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
1.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
传了Test类有一个test参数 test="<?php phpinfo();?>";
O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
如图所示
$test1是我可控的没有任何过滤,我传了一个Test类,里面有一个test参数等于"<?php phpinfo();?>"
由于代码中写了__wakeup()
上文提到,有__wakeup(),在反序列化之前一定会调用此方法,创建了一个test.php文件,并把Test类中的test变量的值即"<?php phpinfo();?>"
写进了test.php文件,require进行文件包含
(想起来一个事,关于__wakeup(),以前遇到CTF的一个题用到了这个知识点
有当序列化字符串中,表示对象属性个数的值大于实际属性个数时,那么就会跳过wakeup方法的执行,即:
O:4:"Test":2:{s:4:"test";s:18:"<?php%20phpinfo();?>";}这种 你就会绕过__wakeup(),__wakeup()内部全都不会执行啦 不信你可以试试
)
这就是一个非常典型的反序列化漏洞
其他魔术方法同理
<?php class Test1{ function __construct($test){ $fp = fopen("shell.php", "w"); fwrite($fp, $test); fclose($fp); } } class Test2{ var $test = "123"; function __wakeup(){ $obj = new Test1($this -> test); } } $test = $_GET['test']; unserialize($test); require "shell.php"; ?>
再比如上边这个__construct()
与第一个例子用 一样的payload 一样的感觉
其实真正造成危险的代码在魔术方法中,上面两个例子用了新建文件操作,后面还有文件包含操作,由于魔术方法往往伴随某些条件自动调用,所以。。。
那么是不是说一些执行操作的代码片段不放在魔术方法中就安全了,也不一定
一些方法互相调用,可能调用来调用去,还是会执行某些操作
总结一下PHP反序列化漏洞出现的原因:
1.unserialize()传入参数可控
2.存在某些魔术方法可用
3.没有过滤或者过滤不完善
一般有反序列化函数内部都有 正则过滤(泪目)或者啥过滤,没有就会给构造payload绕过以可乘之机
0x02 Java反序列化漏洞
有两个类:
Java.io.ObjectOutputStream
Java.io.ObjectInputStream
这俩是Java中实现序列化与反序列化的关键类
序列化用到了ObjectOutputStream
类中的writeObject()
反序列化用到了ObjectInputStream
类中的readObject()
借大佬的代码举个例子
import java.io.*; /* import java.io.ObjectOutputStream; import java.io.ObjectInputStream; import java.io.FileOutputStream; import java.io.FileInputStream; */ public class Test{ public static void main(String args[]) throws Exception { String obj = "helloworld"; // 将序列化对象写入文件中 FileOutputStream fos = new FileOutputStream("lcx.ser"); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(obj); os.close(); // 从文件中读取数据 FileInputStream fis = new FileInputStream("lcx.ser"); ObjectInputStream ois = new ObjectInputStream(fis); // 通过反序列化恢复对象 String obj2 = (String)ois.readObject(); System.out.println(obj2); ois.close(); } }
创建文件,把序列化数据写入文件,读取文件,反序列化数据,打印数据
(实现Serializable和Externalizable接口的类的对象才能被序列化)
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象
更进一步,把文件变成类
import java.io.*; public class test{ public static void main(String args[]) throws Exception{ UnsafeClass Unsafe = new UnsafeClass(); Unsafe.name = "my name is lcx"; FileOutputStream fos = new FileOutputStream("object"); ObjectOutputStream os = new ObjectOutputStream(fos); //writeObject()方法将Unsafe对象写入object文件 os.writeObject(Unsafe); os.close(); //从文件中反序列化obj对象 FileInputStream fis = new FileInputStream("object"); ObjectInputStream ois = new ObjectInputStream(fis); //恢复对象 UnsafeClass objectFromDisk = (UnsafeClass)ois.readObject(); System.out.println(objectFromDisk.name); ois.close(); } } class UnsafeClass implements Serializable{ public String name; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{ //执行默认的readObject()方法 in.defaultReadObject(); //执行命令 Runtime.getRuntime().exec("calc.exe"); } }
UnsafeClass实例化的对象被序列化进object文件
再被读取并反序列化
执行了重写后的readObject方法,显示 my name is lcx 并弹出了计算器
Java反序列化漏洞和所有反序列化漏洞一样,都是由于对用户提供的、不可信的数据进行了反序列化处理,没做任何处理产生了非预期对象,带来了某些代码执行
有几个经典的例子:Apache Commons Collections序列化RCE漏洞、Spring框架反序列化漏洞、Fastjson反序列化漏洞、 Apache Shiro Java 反序列化漏洞(具体的不展开了,网上某某社区也有不少大佬写的poc很好很全面,Java玩的不好就不班门弄斧误人子弟了)
反序列化操作一般在导入模版文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘或DB存储等业务场景,在代码审计时可重点关注一些反序列化操作函数并判断输入是否可控,如
readObject()
方法是否重写,重写中是否有设计不合理,可以被利用之处),看是不是有危险库,有的话利用相关方法搞一搞
)开头。 Java RMI 的传输 100% 基于反序列化,Java RMI 的默认端口是1099
端口,有大佬写的脚本对1099端口进行扫描攻击2020网鼎杯 朱雀组Web 之 think java