系统采用的是单入口模式,即只能通过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]