连续使用过滤函数造成的安全问题总结

简介

P师傅很早在小密圈里面就提过这样的问题,刚好最近也遇到过一个类似的情况,所以就总结一下这类错误的用法。

strip_tags && preg_replace

1
$textMsg = trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/s''',$message)));

这个代码的意图很简单——去掉所有的标签和内容。首先使用preg_replace过滤掉标签、标签内容、标签属性,接着又使用strip_tags去掉其余的html和php标记。

常规的输入方式如:<head>evil</head>,得到的结果是空,即全部都被过滤了。

但是如果攻击者输入<head>evil</headend>或者<he<>ad>evil</head>之类,就会导致evil字符串逃逸,攻击者利用evil字符串再结合上下文说不定就能够造成漏洞。

漏洞实例

这个是出现在MetInfo中,在include/mail/class.phpmailer.php中的发送邮件的函数MsgHTML()中就存在这样的代码:

1
2
3
4
5
6
7
8
9
10
11
function MsgHTML($message, $basedir = '') {
$this->IsHTML(true);
$this->Body = $message;
$textMsg = trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/s''', $message)));
if (!empty($textMsg) && empty($this->AltBody)) {
$this->AltBody = html_entity_decode($textMsg);
}
if (empty($this->AltBody)) {
$this->AltBody = 'To view this email message, open the email in with HTML compatibility!' . "\n\n";
}
}

最终将$textMsg赋值给$this->AltBody,而AltBody就会作为邮件的一部分发送出去。所以如果使用不当,我们利用这个小特点能够形成存储型XSS等等漏洞。

escapeshellarg && escapeshellcmd

  • escapeshellarg 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号
  • escapeshellcmd 会对&#;|*?~<>^()[]{}$\, \x0A 和 \xFF进行转义,'"仅在不配对儿的时候被转义

需要注意的是escapeshellargescapeshellcmd在win平台和linux平台的表现是不一样的。他们两者造成的漏洞也主要是在Linux平台下。接下来主要是说明在Linux平台下的情况

1
2
3
$msg = "123'456";
echo escapeshellarg($msg) // 结果是: '123'\''456'
echo escapeshellcmd($msg) // 结果是: 123\'456

当两者混合使用时,就会出现问题。代码如下:

1
2
3
$parameter1 = escapeshellarg($parameter)
$parameter2 = escapeshellcmd($parameter1)
system("curl ".$parameter2)

假设我们传入的$parameter172.17.0.2' -v -d a=1,那么经过escapeshellarg之后变为'172.17.0.2'\'' -v -d a=1'。之后经过escapeshellcmd变为'172.17.0.2'\\'' -v -d a=1\',此时\\的存在后面得'不会被转义,所以后面的两个''变为了空白字符。那么最后实际的命令为curl 172.17.0.2\ -v -d a=1',成功地逃逸了单引号。

这两个函数联合使用之后可以造成单引号逃逸,这样就很有可能会造成漏洞,利用的方式就需要看具体的应用场景了。

关于这个漏洞的详细实例可以参考PHP escapeshellarg()+escapeshellcmd() 之殇

addslashes && basename

basename的主要用法是:

给出一个包含有指向一个文件的全路径的字符串,basename()函数返回基本的文件名。如果是在windows环境下,路径中的斜线(/)和反斜线(\)都可以用作目录分割符,在其他环境下是斜线(/)

以一个简单的例子来进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 win平台下
$mypath1 = 'C:/Users/monkey/1.txt';
$name1 = basename($mypath1);
var_dump($name1); // 1.txt
$mypath2 = 'C:\Users\monkey\2.txt';
$name2 = basename($mypath2);
var_dump($name2); // 2.txt

在Linux平台下
$mypath1 = 'C:/Users/monkey/1.txt';
$name1 = basename($mypath1);
var_dump($name1); // 1.txt
$mypath2 = 'C:\Users\monkey\2.txt';
$name2 = basename($mypath2);
var_dump($name2); // C:\Users\monkey\2.txt

需要说明的是

  1. 不一定是需要addslashes,只需要是进行了转义即可
  2. 此方式的利用需要在win平台下。因为在win平台下,\/都可以作为basename的分隔符,但是在Linux平台下只有/可以作为分隔符,而addslashes会增加一个\。所以只能在win平台下使用。

漏洞演示如下:

1
2
3
4
5
$filename = "123'456.png";
$filename = addslashes($filename);
var_dump($filename); //结果是 123\'456.png
$filename = basename($filename);
var_dump($filename); // 结果是 '456.png

通过例子可以看到,成功地逃逸了反斜线,单引号也保留了。
如果存在如下的代码:

1
2
3
4
5
6
7
8
9
10
11
// 对输入进行转义
if (!@ get_magic_quotes_gpc()) {
$_GET = $_GET ? $this->addslashes_deep($_GET) : '';
$_POST = $_POST ? $this->addslashes_deep($_POST) : '';
$_COOKIE = $this->addslashes_deep($_COOKIE);
$_REQUEST = $this->addslashes_deep($_REQUEST);
}

$imagename = basename($_POST['image']);
$sql = "UPDATE table SET image = '".$imagename."'where id=1";
query($sql);

此时,如果我们输入的image的参数为123' and if(1.sleep(3),0)#,最后的imagename的值为' and if(1.sleep(3),0)#,sql语句为UPDATE table SET image = '' or if(1.sleep(3),0)#'where id=1形成了一个盲注。

漏洞实例

在douphp1.3的版本中就存在一个这样的问题。
admin/article.php中存在如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
elseif ($rec == 'update') {
if (empty($_POST['title']))
$dou->dou_msg($_LANG['article_name'] . $_LANG['is_empty']);

// 上传图片生成
if ($_FILES['image']['name'] != "") {
// 获取图片文件名
$basename = basename($_POST['image']);
$file_name = substr($basename, 0, strrpos($basename, '.'));
$upfile = $img->upload_image('image'"$file_name"); // 上传的文件域
$file = $images_dir . $upfile;
$up_file = ", image='$file'";
}

// 格式化自定义参数
$_POST['defined'] = str_replace("\r\n"',', $_POST['defined']);

$sql = "UPDATE " . $dou->table('article') . " SET cat_id = '$_POST[cat_id]', title = '$_POST[title]', defined = '$_POST[defined]' ,content = '$_POST[content]'" . $up_file . ", keywords = '$_POST[keywords]', description = '$_POST[description]' WHERE id = '$_POST[id]'";
$dou->query($sql);

$dou->create_admin_log($_LANG['article_edit'] . ': ' . $_POST['title']);
$dou->dou_msg($_LANG['article_edit_succes'], 'article.php');
}

可以看到使用了$basename = basename($_POST['image']);,之后$basename就传递到$up_file,而SQL语句就直接使用了$up_file。如果仅仅只使用basename()不存在问题。但是在此之前调用了过滤代码,过滤代码是:

1
2
3
4
5
6
7
8
function dou_magic_quotes() {
if (!@ get_magic_quotes_gpc()) {
$_GET = $_GET ? $this->addslashes_deep($_GET) : '';
$_POST = $_POST ? $this->addslashes_deep($_POST) : '';
$_COOKIE = $this->addslashes_deep($_COOKIE);
$_REQUEST = $this->addslashes_deep($_REQUEST);
}
}

所以我们通过合理地构造$_POST['image']参数就能够进行update的盲注了,具体漏洞的过程这里不进行详细地说明了。

explode && preg_replace

严格来说,这两个函数不算是过滤函数,但是这两个函数有时一起使用时说不定就会存在漏洞,而且这种漏洞尤其是会存在在文件上传的地方。preg_replace()函数的用法相信大家都十分的熟悉。而explode()用法参照php手册上面的说明:

array explode ( string $delimiter , string $string [, int $limit ] ),此函数返回由字符串组成的数组,每个元素都是 string 的一个子串,它们被字符串 delimiter 作为边界点分割出来。

下面是一个explode使用的简单示例:

1
2
$pizza  = "piece1 piece2 piece3 piece4 piece5 piece6";
$pieces = explode(" ", $pizza); // 得到数组array("piece1","piece2","piece3","piece4","piece5","piece6")

这两个函数造成的漏洞其实就是一个任意文件上传,由于preg_replace()过滤了特殊字符,导致能够逃逸出php这种后缀,而explode()用以取文件名,最后取得的就是错误的文件后缀。

我们以dedecms中的后台文件上传漏洞为例进行说明。首先我们需要明确的是在dedecms中是禁止上传包含以下文件后缀的文件的。php|pl|cgi|asp|aspx|jsp|php3|shtm|shtml。但是由于explodepreg_replace错误使用,使得可以上传任意文件。

include/dialog/select_images_post.php中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 过滤非法字符
$imgfile_name = trim(preg_replace("#[ \r\n\t\*\%\\\/\?><\|\":]{1,}#"'', $imgfile_name));

// 校验文件类型
if(!preg_match("#\.(".$cfg_imgtype.")#i", $imgfile_name))
{
ShowMsg("你所上传的图片类型不在许可列表,请更改系统对扩展名限定的配置!""-1");
exit();
}

// 拼接文件名,得到上传文件的绝对路径
$filename_name = $cuserLogin->getUserID().'-'.dd2char(MyDate("ymdHis", $nowtme).mt_rand(100999));
$filename = $mdir.'/'.$filename_name;
$fs = explode('.', $imgfile_name);
$filename = $filename.'.'.$fs[count($fs)-1];
$fullfilename = $cfg_basedir.$activepath."/".$filename;

// 上传文件
move_uploaded_file($imgfile, $fullfilename) or die("上传文件到 $fullfilename 失败!");

  1. 其中$imgfile_name = trim(preg_replace("#[ \r\n\t\*\%\\\/\?><\|\":]{1,}#", '', $imgfile_name));,使用preg_replace()过滤了特殊的字符,那么如果上传的文件名为xxx.jpg.ph%p,这样经过过滤之后得到的就是xxx.jpg.php,不仅突破了上传限制还得到了php后缀。
  2. $fs = explode('.', $imgfile_name);$filename = $filename.'.'.$fs[count($fs)-1];,直接使用.分割文件名然后去最后一个后缀。此时xxx.jpg.php中的最后一个后缀是php,所以就能够上传一个php后缀的文件。

如此,通过preg_replace()的过滤和explode()的错误取文件名的方法,不仅顺利地绕过了上传限制还能够进行任意文件上传。我相信这种处理文件上传的做法在其他的cms系统应该也有。

总结

由于开发人员对函数的用法缺乏深刻的理解,或者想当然地认为多个过滤函数能够有很好地过滤效果,却没有意识到多个过滤函数的组合使用恰好有可能存在漏洞。

本文也只是简单地总结了一下这种情况,而漏洞实例部分只是为了说明这些漏洞是实际存在故而没有进行详细的漏洞分析,相信通过简单的代码演示,师傅们也能够了解这些漏洞的成因。

虽然每个漏洞都是比较简单的漏洞,但是这一类的问题却是十分的有趣。我认为安全研究除了研究在看似固若金汤的防护下是否还存在漏洞,另一方面是研究各种不同的利用姿势是否存在某种共性或者是规律,能够帮助安全研究人员发现更多的类似潜在漏洞,找出目前的安全盲点,就如同之前TK教主找出来的APP克隆漏洞一样。最后期待和师傅们的交流。

参考

PHP代码审计学习

PHP escapeshellarg()+escapeshellcmd() 之殇