最近看到 seacms 一连更新了好几个安全问题,出于好奇看了看,问题都是出在通用文件的变量覆盖上,这里拿出来简单分析下为什么修了好几个版本,并稍微的延申思考一下。

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.939.94 基本一样,修复后是 9.95,直接看看 9.959.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 暂时没有新的安全更新了(期待大佬后续

利用方法

FILES 变量

当然,讨论覆盖不止这一个 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 呢。这时候我们得看看 GLOBALS 内是否有敏感信息给我们覆盖。

我们可以想到 MYSQL 的信息,如果 GLOBALS 里存在 数据库信息,我们就可以让服务器连接到我们的数据库,只要从数据库里提出得信息我们都可以控制,在某些操作下是可以 getshell。之前版本中 seacms 中存在这样的操作。

总结

最后我们可以讨论一下防御的方法,可以从不同的 CMS 学习一下

  1. 首先是比较正常的,就是尽量过滤的全一些。
  2. 第二的话有些 CMS 比较变态,他可能直接把整个 GLOBALS 直接变成空,覆盖了也没什么用处。
  3. 然后就是第三种检测的方式:
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 直接跳过已存在变量。

这里再另外推荐两个实例:

Discuz! 6.x/7.x 全局变量防御绕过导致命令执行
2015通达oa-从前台注入到后台getshell

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖