最近看到 seacms
一连更新了好几个安全问题,出于好奇看了看,问题都是出在通用文件的变量覆盖上,这里拿出来简单分析下为什么修了好几个版本,并稍微的延申思考一下。
首先我们看最早的版本:
//检查和注册外部提交的变量
foreach($_REQUEST as $_k=>$_v)
{
if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS)',$_k) && !isset($_COOKIE[$_k]) )
{
exit('Request var not allow!');
}
}
...
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}
很简单,GLOBALS
也很容易覆盖,因为上面没有过滤 _POST
,所以我们可以传入一个 GET
成这样的值:?_POST[GLOBALS]=1
这样第一次循环 GET
的时候 _POST
就会变成 Array(GLOBALS=>1)
,然后第二次循环 POST
时就会将 GLOBALS
覆盖。
接下来看看更新之后的 9.91:
//检查和注册外部提交的变量
foreach($_REQUEST as $_k=>$_v)
{
if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS|_)',$_k) && !isset($_COOKIE[$_k]) )
{
exit('Request var not allow!');
}
}
这里的检查代码新增了一个 _
,意思是带有 _
的都不允许注册,但不知道是不是官方觉得这么做稍有不妥,在之后的9.93 版本变成了:
foreach($_REQUEST as $_k=>$_v)
{
if(
strlen($_k)>0 &&
m_eregi('^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_REQUEST|_SERVER|_FILES|_SESSION)',$_k) &&
!isset($_COOKIE[$_k])
)
{
exit('Request var not allow!');
}
}
这样自然是没什么问题,但是可以看到这个 if
是有三个条件的,第三个条件的值还是从 _COOKIE
中直接获取的,这里的意思就仿佛在说:如果 _COOKIE 存在这个 key,就不过滤
。我们可以在本地试试:
测试代码:
<?php
$GLOBALS['test']='';
var_dump("test:".$GLOBALS['test']);
function chgreg($reg)
{
$nreg=str_replace("/","\\/",$reg);
return "/".$nreg."/";
}
function m_eregi($reg,$p)
{
$nreg=chgreg($reg)."i";
return preg_match(chgreg($reg),$p);
}
var_dump("COOKIE 是否存在 _POST 键".isset($_COOKIE['_POST']));
foreach($_REQUEST as $_k=>$_v)
{
if(
strlen($_k)>0 &&
m_eregi('^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_REQUEST|_SERVER|_FILES|_SESSION)',$_k) &&
!isset($_COOKIE[$_k])
)
{
exit('Request var not allow!');
}
}
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = ($_v);
}
var_dump("test:".$GLOBALS['test']);
看图:
9.93
和 9.94
基本一样,修复后是 9.95
,直接看看 9.95
,9.95
的检测:
//此处使用 $_REQUEST 检测
foreach($_REQUEST as $_k=>$_v)
{
if( strlen($_k)>0 && m_eregi('^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_REQUEST|_SERVER|_FILES|_SESSION)',$_k))
{
Header("Location:$jpurl");
exit('err');
}
}
这次把第三个条件删掉了,记住这里用的是 $_REQUEST
检测的
再来覆盖变量的代码是这样的:
foreach(Array('_GET','_POST','_COOKIE','_SERVER') as $_request){ // 新增了一个 _SERVER ,但不影响
// 覆盖操作。。。。
}
这里是有 _COOKIE
的,可是,仔细看看 php.ini
,看看关于 $_REQUEST
变量的属性 request_order
:
没错,这里的 GP 指的是 GET
和 _POST
,少了 _COOKIE
。
因为我印象中 _REQUEST
是包含了 _COOKIE
的。
看官方文档(https://www.php.net/manual/zh/reserved.variables.request.php):
但是在下面一些:
其实从 5.3
以后就移除了。
经过了上面两次,开发终于被逼疯了:
把所有常见的变量都过滤了个遍。至此 seacms
暂时没有新的安全更新了(期待大佬后续
当然,讨论覆盖不止这一个 CMS
的问题,还可以延申讨论一下,比如当他没有过滤 _FILES
时,我们是否可以利用。
如果程序文件上传都做得很安全,但是变量覆盖时唯独没检测 _FILES
时我们可以做些什么呢?
这里举个例子,在最近审计某 CMS
时发现全局文件:
foreach(array('_GET','_POST') as $_request)
{
foreach($$_request as $_k => $_v){
if(strlen($_k)>0 && preg_match('#^(GLOBALS|_GET|_POST|_SESSION|_COOKIE)#',$_k))
{
exit('不允许请求的变量名!');
}
${$_k} = _RunMagicQuotes($_v);
}
}
过滤的倒是很全,但是这里唯独没有过滤 _FILES
,在头像的上传处的代码:
// 获取文件后缀
$imgext = strrchr('.',($_FILES['file']['name']));
$imgtype = ['jpg','png'];
//判断文件后缀是不是图片
if(!in_array($imgext, $imgtype)) {
//删除临时文件
unlink($_FILES['file']['tmp_name']);
exit("文件后缀不允许~");
}
这里检测如果文件后缀不是图片的话就删掉 tmp_name
。通过覆盖变量,我们是可以制造一个 _FILES
变量的。
简单的拼接一下上面的代码,访问:
?_FILES[file][tmp_name]=test.txt&_FILES[file][name]=1.php
test.txt
就是要删除的文件,访问后会发现删除成功了。
接下来就可以考虑删除安装文件,然后重新安装 getshell
。
当然,一般来说不允许的,但是如果能覆盖 GLOBALS
呢。这时候我们得看看 GLOBALS
内是否有敏感信息给我们覆盖。
我们可以想到 MYSQL
的信息,如果 GLOBALS
里存在 数据库信息,我们就可以让服务器连接到我们的数据库,只要从数据库里提出得信息我们都可以控制,在某些操作下是可以 getshell
。之前版本中 seacms
中存在这样的操作。
最后我们可以讨论一下防御的方法,可以从不同的 CMS
学习一下
CMS
比较变态,他可能直接把整个 GLOBALS
直接变成空,覆盖了也没什么用处。if(isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) exit('Request Denied');
foreach(array('_POST', '_GET') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(substr($__k, 0, 1) == '_') if($__R == '_POST') { unset($_POST[$__k]); } else { unset($_GET[$__k]); }
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
检测如果变量中有 下划线
就直接 unset
掉,然后在下面的 extract
中也使用了 SKIP
直接跳过已存在变量。
这里再另外推荐两个实例: