从 SEACMS 漏洞浅谈变量覆盖

2019-09-06 约 402 字 预计阅读 2 分钟

声明:本文 【从 SEACMS 漏洞浅谈变量覆盖】 由作者 这是一个睿智 于 2019-09-06 09:10:00 首发 先知社区 曾经 浏览数 129 次

感谢 这是一个睿智 的辛苦付出!

最近看到 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

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now