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 forDoMime
), 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
inDoIPSO
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"
ALLOWED_KEYS_WITH_SPECIAL_CHARS - check
If we want to validate what we’ve deduced from our pseudocode, let’s take
ftw_login.tcl
as an example.
In this case, only thepassword
parameter will be allowed to contain special characters.
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:
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
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
- https://support.checkpoint.com/results/sk/sk182219 - [Acknowledgments]
- https://support.checkpoint.com/results/sk/sk182743 - [Mitigation]
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-24914 - [Mitre]
- https://nvd.nist.gov/vuln/detail/CVE-2024-24914 - [NIST]
- https://www.tcl-lang.org/ - [Tcl Documentation]
- https://www.tcl.tk/