安全性分析

系统采用的是单入口模式,即只能通过index.php访问/inc/module/下的模块文件

因为常量in_mx定义在了index.php中

而其他文件都包含了下面的一条语句,若没有定义常量直接退出

if (!defined('in_mx')) {exit('Access Denied');}

先分析下index.php是如何加载模块文件的

<?php
define('in_mx', TRUE);
$p=isset($_GET['p']) ? addslashes($_GET['p']) : '';
$p=(trim($p)=='') ? 'index' : trim($p);
require("./inc/function/global.php");
switch ($p){
    case 'admin':
        include("./inc/function/global_admin".Ext);
        exit();
    break;
    case 'install':
        require("./install/index".Ext);
        exit();
    break;
    default:
        if(strpos($p, "n-")===0 || $ym_url_path[0] === 'news'){
            include("./inc/function/global_news".Ext);
        }
        else{
            include("./inc/function/global_page".Ext);
        }
    break;
}

首先访问index.php会加载全局文件global.php,下面先分析下全局文件

foreach($_GET as $key=>$value){
    StopAttack($key,$value,$getfilter);
}
if ($_GET["p"]!=='admin'){
    foreach($_POST as $key=>$value){ 
        StopAttack($key,$value,$postfilter);
    }
}
foreach($_COOKIE as $key=>$value){ 
    StopAttack($key,$value,$cookiefilter);
}
unset($_GET['_SESSION']);
unset($_POST['_SESSION']);
unset($_COOKIE['_SESSION']);

require('./inc/function/common.php');
if (!empty($_GET)){ foreach($_GET AS $key => $value) $$key = addslashes_yec($value); }
if (!empty($_POST)){ foreach($_POST AS $key => $value) $$key = addslashes_yec($value); }

GPC参数会经过StopAttack方法过滤,用黑名单匹配的方式

$getfilter="\b(and|or)\b.+?(>|<|=|in|like)|\/\.+?\\/|<\s*script\b|\bEXEC\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\s+(TABLE|DATABASE)";

$postfilter="\b(and|or)\b.{1,6}?(=|>|<|\bin\b|\blike\b)|\/\.+?\\/|<\s*script\b|\bEXEC\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\s+(TABLE|DATABASE)";

$cookiefilter="\b(and|or)\b.{1,6}?(=|>|<|\bin\b|\blike\b)|\/\.+?\\/|<\s*script\b|\bEXEC\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\s+(TABLE|DATABASE)";

function StopAttack($StrFiltKey,$StrFiltValue,$ArrFiltReq){  
    if(is_array($StrFiltValue))
    {
        $StrFiltValue=implode($StrFiltValue);
    }       
    if (preg_match("/".$ArrFiltReq."/is",urldecode($StrFiltValue))){
            print "网址有误~";
            exit();
    }      
}

经过过滤后,将GET和POST中传入的参数注册为变量,用的是可变变量的方式来注册,即$$,然后再经过addslashes_yec过滤,其实调用的就是addslashes函数

function addslashes_yec($val)
{
    if (empty($val) || get_magic_quotes_gpc())
    {
        return $val;
    }
    else
    {
        return is_array($val) ? array_map('addslashes_yec', $val) : addslashes($val);
    }
}

global.php分析完了,回到index.php中

如果单单访问index.php,不传入其他参数的话,最后会进入default中的else分支,包含global_page.php文件,跟进下该文件

$ym_module = array('index','cart', 'user','apidata');
if(in_array($p, $ym_module)) 
{
    if(trim($_REQUEST["action"])!=''){ 
        @include("./inc/lib/".trim($_REQUEST["action"]).Ext);exit();
    }
    require("./inc/module/".$p.Ext); 
    @include template($p, $ym_tpl."/");exit();
}

$p是从index.php传入的部分,默认为index

参数action的值是我们可控的,然后将传入参数action与前面部分拼接成文件路径

再通过include包含文件来加载模块(这里常量Ext的值为php,他这里其实是想加载库函数的)

分析到这里就结束了,总结一下这个系统的大概情况

1.系统为单入口模式,通过index.php加载其他模块,但是模块的路径我们可以控制,可控参数action没有过滤,可以通过../回溯到其他目录,包含其他存在漏洞的文件,从而绕开单入口模式

的限制

2.通过传入参数注册变量,如果有其他文件的变量未初始化,那我们就可以通过传入参数来控制

该变量的值

3.系统对SQL注入有黑名单+单引号保护(对于这个漏洞无关紧要)

漏洞文件分析

/static/ueditor/ueapi/action_upload.php

default:
        $config = array(
            "pathFormat" => $CONFIG['filePathFormat'],
            "maxSize" => $CONFIG['fileMaxSize'],
            "allowFiles" => $CONFIG['fileAllowFiles']
        );
        $fieldName = $CONFIG['fileFieldName'];
        break;
}
/* 生成上传实例对象并完成上传 */
$up = new Uploader($fieldName, $config, $base64);

这个文件原本是没有任何问题的,但是结合上面的分析就有问题了

这是ue编辑器用来上传文件的操作,ue编辑器默认是不能上传php文件的

1.该文件默认也是不能直接访问的,但是可以通过上面分析出的第1点来包含这个文件,从而间接访问

2.其中$CONFIG定义在同目录下的config.json中,但这里没有包含config.json,,即这里的$CONFIG是没有初始化的,而能上传什么文件是由config.json的配置决定的,所以可以利用上面分析出的第2点,通过传入参数来注册$CONFIG,进而控制$config的值,从而上传php文件

跟进下定义Uploader类的文件

public function __construct($fileField, $config, $type = "upload")
{
    $this->fileField = $fileField;
    $this->config = $config;
    $this->type = $type;
    if ($type == "remote") {
        $this->saveRemote();
    } else if($type == "base64") {
        $this->upBase64();
    } else {
        $this->upFile();
    }

其中我们调用的是upFile方法

private function upFile()
{
    $file = $this->file = $_FILES[$this->fileField];
    if (!$file) {
        $this->stateInfo = $this->getStateInfo("ERROR_FILE_NOT_FOUND");
        return;
    }
    if ($this->file['error']) {
        $this->stateInfo = $this->getStateInfo($file['error']);
        return;
    } else if (!file_exists($file['tmp_name'])) {
        $this->stateInfo = $this->getStateInfo("ERROR_TMP_FILE_NOT_FOUND");
        return;
    } else if (!is_uploaded_file($file['tmp_name'])) {
        $this->stateInfo = $this->getStateInfo("ERROR_TMPFILE");
        return;
    }
    $this->oriName = $file['name'];
    $this->fileSize = $file['size'];
    $this->fileType = $this->getFileExt();
    $this->fullName = $this->getFullName();
    $this->filePath = $this->getFilePath();
    $this->fileName = $this->getFileName();
    $dirname = dirname($this->filePath);
    //检查文件大小是否超出限制
    if (!$this->checkSize()) {
        $this->stateInfo = $this->getStateInfo("ERROR_SIZE_EXCEED");
        return;
    }
    //检查是否不允许的文件格式
    if (!$this->checkType()) {
        $this->stateInfo = $this->getStateInfo("ERROR_TYPE_NOT_ALLOWED");
        return;
    }
    //创建目录失败
    if (!file_exists($dirname) && !mkdir($dirname, 0777, true)) {
        $this->stateInfo = $this->getStateInfo("ERROR_CREATE_DIR");
        return;
    } else if (!is_writeable($dirname)) {
        $this->stateInfo = $this->getStateInfo("ERROR_DIR_NOT_WRITEABLE");
        return;
    }
    //移动文件
    if (!(move_uploaded_file($file["tmp_name"], $this->filePath) && file_exists($this->filePath))) { //移动失败
        $this->stateInfo = $this->getStateInfo("ERROR_FILE_MOVE");
    } else { //移动成功
        $this->stateInfo = $this->stateMap[0];
    }
}

可以看到最后调用了move_uploaded_file函数上传

漏洞复现


poc中传入的数组CONFIG为config.json中的配置字段

/* 上传文件配置 */
    "fileActionName": "uploadfile", /* controller里,执行上传视频的action名称 */
    "fileFieldName": "upfile", /* 提交的文件表单名称 */
    "filePathFormat": "/upload/file/{catname}{yyyy}{mm}{dd}/{fname}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
    "fileUrlPrefix": "", /* 文件访问路径前缀 */
    "fileMaxSize": 51200000, /* 上传大小限制,单位B,默认50MB */
    "fileAllowFiles": [
        ".png", ".jpg", ".jpeg", ".gif", ".bmp",
        ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
        ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
        ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
        ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
    ]

还有一点要注意,因为这个系统自带了.htaccess文件

如果apache开启了mod_rewrite模块或者nginx.conf引入了.htaccess文件

那么生成的shell不能访问,需要利用第1点的文件包含来访问

RewriteEngine On
RewriteBase /
#RewriteCond %{REQUEST_FILENAME} !-f
#RewriteCond %{REQUEST_FILENAME} !-d 
RewriteRule ^(.*)\.html$ /index.php?p=$1&%{QUERY_STRING} [L]

http://127.0.0.1/index.html?action=../../upload/info

源链接

Hacking more

...