CVE-2019-7160

前言

审计iCMS 7.0.13的时候发现一天前刚刚爆出了一个新的CVE,CVE-2019-7160后台getshell漏洞。

于是跟进分析一下这个漏洞。

实验环境:osx+apache2+php7+mysql5.7
icms官网:https://www.icmsdev.com/

漏洞分析

大致流程

此漏洞需要先登录后台,利用do_IO()上传ZIP文件至根目录,再利用do_local_app()对ZIP文件进行解压生成shell文件。

上传文件

首先定位到do_IO()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function do_IO(){
files::$watermark_enable = $_GET['watermark'];
$udir = iSecurity::escapeStr($_GET['udir']);
$name = iSecurity::escapeStr($_GET['name']);
$ext = iSecurity::escapeStr($_GET['ext']);
iFS::check_ext($ext,0) OR iUI::json(array('state'=>'ERROR','msg'=>'不允许的文件类型'));
iFS::$ERROR_TYPE = true;
$F = iFS::IO($name,$udir,$ext);
$F ===false && iUI::json(iFS::$ERROR);
iUI::json(array(
"value" => $F["path"],
"url" => iFS::fp($F['path'],'+http'),
"fid" => $F["fid"],
"fileType" => $F["ext"],
"image" => in_array($F["ext"],files::$IMG_EXT)?1:0,
"original" => $F["oname"],
"state" => ($F['code']?'SUCCESS':$F['state'])
));
}

它处理通过流数据上传的文件,并且我们可控路径(udir),文件名(name),文件类型(ext)。

继续跟进$F = iFS::IO($name,$udir,​$ext);,定位到IFS类中的IO函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static function IO($FileName = '', $udir = '', $FileExt = 'jpg',$type='3',$filedata=null) {
$filedata===null && $filedata = file_get_contents('php://input');
if (empty($filedata)) {
return false;
}

$fileMd5 = md5($filedata);
$FileName OR $FileName = $fileMd5;
$FileSize = strlen($filedata);
$FileExt = self::valid_ext($FileName . "." . $FileExt); //判断文件类型
if ($FileExt === false) {
return false;
}

list($RootPath, $FileDir) = self::mk_udir($udir,$fileMd5,$FileExt); // 文件保存目录方式
$FilePath = $FileDir . $FileName . "." . $FileExt;
$FileRootPath = $RootPath . $FileName . "." . $FileExt;
self::write($FileRootPath, $filedata);
$fid = self::insert_filedata(array($FileName,'',$FileDir,'',$FileExt,$FileSize), $type);
self::hook('upload',array($FileRootPath,$FileExt));
$value = array(
1,$fid,$fileMd5,$FileSize,
'',$FileName,$FileName.".".$FileExt,
$FileDir,$FileExt,
$FileRootPath,$FilePath,$RootPath
);
return self::_data($value);
}

这个函数以php://input读取数据,之后通过mk_udir函数创建文件存储路径。

我们可以通过控制udir和name两个变量使文件存放在任意位置。

例如

1
/icms/admincp.php?app=files&do=IO&frame=iPHP&ext=zip&udir=../&name=../app/test&watermark=fals

此时会在app目录下生成一个test.zip

解压文件

利用后台的安装本地应用功能。

定位到do_local_app()

1
2
3
4
5
6
7
8
9
10
11
12
public function do_local_app(){
$zipfile = trim($_POST['zipfile']);
echo $zipfile;
if(preg_match("/^iCMS\.APP\.(\w+)\-v\d+\.\d+\.\d+\.zip$/", $zipfile,$match)){
apps_store::$zip_file = iPATH.$zipfile;
apps_store::$msg_mode = 'alert';
apps_store::install_app($match[1]);
iUI::success('应用安装完成','js:1');
}else{
iUI::alert('What the fuck!!');
}
}

首先会接受一个POST的数据包,如果符合正则匹配则进入install_app(),否则输出What the fuck!!。

跟进install_app()函数

1
2
3
4
5
6
7
public static function install_app($app=null) {
self::$success = false;

$archive_files = self::setup_zip();
$msg = null;
//安装应用数据
$setup_msg = self::setup_app_data($archive_files,$app);

看到set_zip(),跟进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static function setup_zip() {
$zip_file = self::$zip_file;
if(!file_exists($zip_file)){
return self::msg("安装包不存在",false);
}

iPHP::vendor('PclZip');
$zip = new PclZip($zip_file);
if (false == ($archive_files = $zip->extract(PCLZIP_OPT_EXTRACT_AS_STRING))) {
iFS::rm($zip_file);
return self::msg("ZIP包错误",false);
}

if (0 == count($archive_files)) {
iFS::rm($zip_file);
return self::msg("空的ZIP文件",false);
}
return $archive_files;
}

看到对文件进行解压操作,这就足够利用了。

再回到do_local_app()函数入,构造一个符合正则的文件名例如

1
iCMS.APP.1-v1.1.1.zip

GETSHELL

只要成功解压我们上传的ZIP文件,释放出php文件即可GETSHELL。

POC

构造文件

利用phpinfo()测试,把php文件压缩成一个ZIP

上传

发包上传文件,构造name和udir使得文件上传至根目录。

解压

getshell