您当前的位置: 首页 > 

合天网安实验室

暂无认证

  • 1浏览

    0关注

    748博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

老赛棍寒假复习计划——反序列化篇

合天网安实验室 发布时间:2021-02-19 15:30:00 ,浏览量:1

 

本篇主要讲解过去一年来各大比赛中出现的比较典型的几个反序列化题目

尝试一些反序列化漏洞,去了解成因,从实践中去明白什么是反序列化漏洞。

实操知识点指路(PC端复制链接开始操作):

https://www.hetianlab.com/expc.do?ec=ECID172.19.104.182016010714511600001&pk_campaign=csdn-wemedia   

xnuca个人赛题目解析

复现环境:

链接:https://pan.baidu.com/s/1U_uDvgtzfFV165158xGE9A 
提取码:7ryd 
复制这段内容后打开百度网盘手机App,操作更方便哦--来自百度网盘超级会员V3的分享

寒假难得有时间把这一年的比赛题目都好好整理一下,首先来的是xnuca个人赛的一道题目,比较新颖,属于中等难度的web phar写入和反序列化题目,貌似在其之后的DASCTF也考察了类似的知识点,因为时间实在久远,加上xnuca当时的一小部分源码实在是找不到了,就借用了DASCTF的部分代码来进行讲解,解题方式是一样的。

这道题目首先需要通过变量覆盖来利用file_get_contents读取template.php,然后通过template.php写入phar进行反序列化。

考点一:变量覆盖

首先是一个index.php




extract变量覆盖。原理是:extract() 函数从数组中将变量导入到当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。

正常的用法通常用于把数组的值转化为变量,就好像把数组一个个解压出来成为变量一样,是不是很像extract的意思:



这样就会把数组 $my_array里面的键值和对应键名组合成为一个变量,等同于再次赋值

以前ctf考察的点基本都是如下形式的变量覆盖:

extract($_GET);

关于这个地方变量覆盖的原理,就要提到一个很关键的基础知识点,$GET,$POST,$REQUEST这三个全局变量的类型是数组(不信的话自己var_dump一下),实际上我们通过get输入的变量名会成为$GET数组里的键名,输入的变量值会成为$GET里的键值,因此extract函数才会由我们的get输入接收到了$GET这个数组,从而产生了变量覆盖。

本题目的写法为:

extract($_GET['var'], EXTR_OVERWRITE);

EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。

这个地方乍一看好像是说 $_GET['var']这个变量,而不是数组,但是之前也有考察过如果通过get或者post方式输入一个数组的ctf题(没错,就是绕过md5比较的php黑魔法),只要我们在get或者post输入的变量的后面加上[],就代表我们输入的是一个数组。

例如下面就代表我们输入了一个数组

http://IP?var[]=a

把 $_GET变量全dump出来为:

array(1) { ["var"]=> array(1) { [0]=> string(1) "a" } }

说白了,就是把 $_GET这个数组变量里键名为var的这个元组的键值设置为了一个数组,这个数组是:

array(1) { [0]=> string(1) "a" } 

所以实际上我们还是可以通过题目中的

extract($_GET['var'], EXTR_OVERWRITE);

来进行变量覆盖。例如:

http://IP/?var[template][tp1]=aaa

这样就能将已经赋值过的template变量重新赋值为一个只含有一个元组且键名为tp1的数组

之前
array(3) {
  ["tp1"]=>
  string(7) "tp1.tpl"
  ["tp2"]=>
  string(7) "tp2.tpl"
  ["tp3"]=>
  string(7) "tp3.tpl"
}

之后
array(1) { 
    ["tp1"]=> 
    string(3) "aaa" 
    }

这里很多人有个误区:为啥不是单独覆盖template数组里的一个tp1,而是覆盖了全部呢?

因为?var[template][tp1]=aaa

等同于输入了

$template=array('tp1'=>'aaa');

而不是

$template = array('tp1'=>'aaa','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');

所以是全部覆盖

我们看到第一个文件index.php里面还有一个file_get_contents,想到可以文件读取。

if(isset($_GET['tp'])) {
    $tp = $_GET['tp'];
    if (array_key_exists($tp, $template) === FALSE) {
        echo "No! You only have 3 template to reader";
        die();
    }
    $content = file_get_contents($template[$tp]);
    $temp = new Temp($content);
} else {
    echo "Please choice one template to reader";
}

思路:

  1. tp]为我们要读取的文件名

  2. tp可控

  3. array_key_exists判断 在template数组中是否存在

  4. 存在则读取 tp]指向的文件

所以我们

  1. ?var[template][a]=文件名&tp=a

这样template数组就剩一个a,然后他的值为我们要读取的文件名,然后tp等于a,读取 tp]所指向的文件,也就是 $template['a'],即我们变量覆盖进去的文件名。

访问得到

u can see ur html file in f187b1e39a106780507c0f5c399da8c1/594f803b380a41396ed63dca39503542.html

访问一下路径看到template.php源码,这里file_get_content读取到的并不是直接显示,而是被template.php写入到了某个地方,但是这个算是第一步的提示,直接访问就看到了template.php的源码,读完以后也会更理解整个过程。

  

最关键的方法是我们的render(因为另外两个一个是构造方法用来给三个属性赋值,一个是析构方法用来触发render)

他做了两件事情

模板变量替换
while (True) { 
  if(preg_match($this->pattern, $this->content, $matches)!==1) 
   break; 
  global ${$matches[1]}; 
  if(isset(${$matches[1]})) { 
   $this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content); 
  } 
  else { 
   break; 
  } 
 } 

这一步的工作用一句话概括为:"用$content里匹配到的字符串的同名变量,来替换$content本身的内容"

可能乍一看看不懂,没事我们来分析:

也就是说,当你输入的内容里面含有{{([a-z]+)}}的时候,他会提取{{}}里面的字符串,然后去判断他是否为一个已经声明的全局变量,如果是的话则导入到方法中,并且用这个全局变量的值去替换 $content的值。

例如搭建一个本地环境

当你输入http://ip?content={{a}},则返回如下结果

  1. 匹配输入,含有{{([a-z]+)}},其中 $matches为

Array ( [0] => {{a}} [1] => a ) 

2.global用于将函数外部的一个全局变量导入函数内,题目中这句代码在render方法内,所以为了使用方法外的全局变量,得加一个global

global ${$matches[1]};
#探测外部是否有需要名字为 $matches[1]的变量,

  1. 然后preg_replace将content里的 $matches[1]给替换为那个变量的值

实际上这是个啥呢,就是我们很常见的模板变量替换,比如说你的前端有一个{{a}},然后你后端检测前端代码的时候,就拿后端的a变量的值替换这个{{a}}里面a所在的位置。类似flask那种模板变量替换。

说白了就是,这段代码或者这道题应该是某个真实的cms上的代码阉割的,然后出成题目,并保留了当时的部分冗余代码。所以才留下了这个模板替换。(就是没啥用的意思,逃:)

写入文件

render做的第二件事情就是写入文件

首先给出了一个限制:

if(strlen($this->suffix)>5) { 
  echo "error suffix"; 
  die(); 
 } 

这段代码保证了你写入的后缀不能超过5个字符,虽然没什么用。

真正写文件的的代码在这里:

$filename = '/var/www/html/upload/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix; 
 file_put_contents($filename, $this->content); 
 echo "u can see ur html file in " . $filename; 

这里思路

  1. 自 PHP 5.2.0 起 data:(» RFC 2397)数据流封装器开始有效。data://text/plain;base64,加上文件内容的base64编码

  2. 变量覆盖,然后file_get_content读取我们输入的data流,然后被写入

  3. file_get_contents触发phar

phar文件:

            
关注
打赏
1665306545
查看更多评论
0.0794s