最近日站的时候遇到了某个cms,于是花时间审计了一发,收获颇丰,这里先放一个比较严重的问题出来。
(本文同步发表在先知社区——https://xianzhi.aliyun.com/forum/topic/1982

审计入口

全局变量$_GET、$_POST、$_COOKIE作了转义处理。一般全局作了转义以后程序员在后面写逻辑的时候就会比较放心大胆地进行各种拼接了。因此我们在审计代码的时候可以着重关注$_SERVER、$_FILE、$_REQUEST等变量。

IMG20180123_112451.png

Lang::get() 用于获取指定键的语言项。举个例子,传入'i_want_open_store',返回'我要开店'。
IMG20180123_123639.png

$vkey是个由$key经过一些处理得到的字符串,字符串的内容是$GLOBALS数组变量,需要用eval函数来使它生效。

IMG20180123_112855.png

在eval之前会调用strtokey()方法,作用就是传入$key, $owner, 返回字符串"$owner['$key的值']"。如果这里的$key包含单引号且没被转义呢?比如这里传入 $key="'xor(phpinfo())or'" 生成的$vkey就是字符串"$GLOBALS['ECLANG'][''xor(phpinfo())or'']",这个字符串会最终进入eval造成代码注入。

IMG20180123_122013.png

也就是说,如果传入Lang::get()的参数包含没被转义的单引号就存在问题,而这个Lang::get()应用是相当之广的。。。

举个栗子。

漏洞复现

进入用户中心-个人资料,随意选择一张图片上传,保存修改。
http://localhost/cms/xxx/index.php?app=member&act=profile

IMG20180123_103502.png

修改filename字段的值为 1.png'xor(phpinfo())or',成功执行phpinfo()。

IMG20180123_103701.png

漏洞分析

根据上述poc定位到member.app.php profile()方法313行,上传资料时如果存在图片文件则调用_upload_portrait()对图片进行上传处理

IMG20180123_104932.png

跟进到_upload_portrait()中。ecmall所有的图片上传都由Uploader这个图片上传辅助类来完成,这里也不例外。首先设置了allowed_type为图片类型,然后调用addFile()方法进行上传处理。

IMG20180123_105537.png

跟进addFile()

IMG20180123_105943.png

跟进_get_uploaded_info()。可以看到由于上传的后缀png'xor(phpinfo())or'没有在allowed_type里面,这里进行了错误存储。错误消息'not_allowed_type'和错误的后缀png'xor(phpinfo())or'被存储到了Uploader类的_errors[]数组里面,键名分别为msg和obj。注意,这里因为后缀是从$_FILE里获取的,不会受到全局转义的影响。

IMG20180123_110137.png

IMG20180123_110539.png

跳出addFile()以后,由于上传出错,调用show_warning()进行错误处理和错误警告。

IMG20180123_111048.png

IMG20180123_111247.png

IMG20180123_111324.png

可以看到在_trigger_message()方法中多处调用了Lang::get(),且其中一处$err['obj']被作为了参数,本文最初时就提到

如果传入Lang::get()的参数包含没被转义的单引号就存在问题

而$err['obj']恰恰是错误的后缀名png'xor(phpinfo())or',最终导致了php代码注入。

IMG20180123_111837.png

修复建议

在strtokey()函数对传入的$str进行转义处理。参考代码

function strtokey($str, $owner = '')
{
    $str = addslashes($str);
    if (!$str)
    {
        return '';
    }
    if ($owner)
    {
        return $owner . '[\'' . str_replace('.', '\'][\'', $str) . '\']';
    }
    else
    {
        $parts = explode('.', $str);
        $owner = '$' . $parts[0];
        unset($parts[0]);
        return strtokey(implode('.', $parts), $owner);
    }
}