Hacking GraphQL: Attacking APIs
Disclaimer:
The content presented in this lab is intended solely for educational purposes and informational use. The purpose of this lab is to provide a practical understanding of API attacks, specifically as related to GraphQL, and their potential impact on web security. It is not intended to promote or encourage any malicious activities or harmful actions against real-world websites or individuals.
Participants are advised to conduct the lab exercises within a controlled environment and refrain from using the knowledge gained to engage in any unauthorized or illegal activities. Any attempt to perform attacks on live websites or systems without explicit authorization is strictly prohibited and may lead to severe legal consequences.
The lab materials, including code snippets and demonstrations, are provided as educational resources only. The instructors and creators of this lab are not responsible for any misuse or unintended application of the knowledge acquired during the learning process.
By accessing and utilizing the lab materials, participants agree to use the information responsibly and ethically, respecting the principles of cybersecurity and online safety. If you have any concerns or questions about the content or exercises, please seek guidance from authorized instructors or cybersecurity professionals.
Remember, responsible learning and ethical conduct are essential for fostering a safe and secure digital ecosystem. Let's use this lab to enhance our knowledge, promote cybersecurity awareness, and contribute to a safer online environment for everyone.
Introduction
There are several application programming interfaces (APIs), each serving a different purpose and catering to a specific need. This post specifically focuses on the GraphQL API. GraphQL is a query language and server-side runtime for APIs that give clients exactly the data they requested. As an alternative to REST, GraphQL allows developers to make requests to fetch data from multiple data sources with a single API call. GraphQL is designed to make APIs fast, flexible, and developer-friendly. The strength of GraphQL lies in its flexibility; a single endpoint can handle many different queries. However, more often than not, flexibility comes with the price of poor security.
Throughout this lab, we'll be using the Damn Vulnerable GraphQL App (DVGA).
Finding GraphQL endpoints
Before you can test a GraphQL API, you must find its endpoint. As GraphQL APIs use the same endpoint for all requests, this is a valuable piece of information. Here are some of the most common GraphQL endpoints discovered during directory enumeration.
• /graphql
• /api
• /api/graphql
• /graphql/api
• /Graphql/graphql
These are not all the paths, but they are the most common. Once a possible endpoint is found, universal queries for GraphQL can be sent in the format {__typename} to any GraphQL endpoint. If the response includes a string similar to: {"data": {"__typename": "query"}} somewhere in its response, it confirms that it is a GraphQL endpoint.
Different types of GraphQL attacks
Like any tech, GraphQL is not immune to security threats and vulnerabilities. GraphQL is vulnerable to several different forms of attack, including intrusion attacks, excessive errors/field suggestions, Denial-of-Service attacks, injection attacks such as SQL Injection and OS Command Injection, cross-site scripting, and more. For this post, we'll cover examples of Introspection Attacks, Excess Errors/Field Suggestions, SQL injection, and OS command injection.
Introspection Attacks
This type of attack is extremely useful in enumerating the types of fields that can be displayed. Now, to be able to perform this attack, you will need to run the following command in either the console or the URL bar: {__schema{types{name,fields{name}}}}. There are quite a few interesting names in the screenshot below. However, the two that stand out are “users” and “id.”
Figure 2 - Introspection attack
At this point we switched to the Altair GraphQL Client browser plugin. It’s a user friendly plugin that allows users to more seamlessly debug and test GraphQL queries without having to constantly change the URL, and makes manual testing significantly cleaner and easier. After enumerating the ID field, it didn't yield many results; however, the username field returned a couple of interesting results that are worth looking into deeper.
Figure 3 – User Enumeration
Now, that is nice. However, can we also view the passwords by adding another subsection discovered in the schema dump? The screenshot below does show a password field, however, the data is obfuscated by asterisks. We must find another attack to obtain the password, but that will be discussed later in the post.
Figure 4 – Password Enumeration
Excess errors / field
suggestions
This type of attack can be useful because if you do not have the schema dump, it can help the attacker or user enumerate fields and subsections to piece together a dangerous query that could expose sensitive information. The query submitted was intentionally incorrect syntax to determine if the application will expose excess errors and field suggestions. Now, in this case the server that we’re interacting with is extremely verbose and provides field suggestions such as “message” and “locations,” as shown in the screenshot below. An attacker can then use those suggestions to formulate queries and conduct fuzzing to disclose additional sensitive information.
Figure 5 - Field Suggestions
GraphQL SQL INJECTION TESTING
When testing for SQL injection, we must find the right parameter(s) vulnerable to SQL injection. The screenshot below shows what users see when accessing public pastes in the DVGA app.
Figure 6 - Public Paste
Now, we must identify which parts of the webpage are parameters. To obtain the parameter names, we used the web developer tools that can be accessed in any web browser. For FireFox, as in our case, you first need to click on the hamburger icon in the top right corner of the browser, then scoll down to “More Tools,” click on “Web Developer Tools,” choose the “network” tab, click on the “XHR” tab as well, and then click on the response tab when you are viewing all the public pastes. In the response you will see id, title, content, ipaddr, etc. These parameters are related to the public pastes.
Figure 7 - Parameters
After attempting to inject a couple of different parameters that didn’t bear any results, we attempted to inject the parameter filter shown in the screenshot below with a single quotation character (‘) to hopefully break the application’s syntax and generate an error message to point us in the right direction.
Figure 8 - SQL Error
Success! This shows the SQLite3 error message, confirming SQL syntax error, which can mean a possible SQL injection vulnerability. Now that we know the SQL database type, let's manually test to see if we can pick up any pastes. The screenshot below shows that by submitting 'OR 1=1—, all the pastes are shown. The submitted SQL statement evaluates to true in all cases (because 1 always equals 1), which confirms that it is indeed vulnerable to SQL injection.
Figure 9 - SQL True Statement
Now that we have that information, let's go ahead and do some more manual testing to see what other type of information can be displayed. The next step is to determine the number of columns required by the table. To do that, we will use the Order By clause. By using the clause “ORDER BY 1—” and incrementing the number (ORDER BY 1--, ORDER BY 2--, etc), we'll eventually get the error message below stating that the query has exceeded the number of available columns. Based on the output, we now know there are a total of 8 columns.
Figure 10 - SQL column discovery
Now that we know there are only eight columns in this database let's try to extract some interesting information from it. First, we must identify which columns can display the information we're looking for. For instance, some columns may only be able to display integers and if we try to display a string in that column, nothing will be returned. To identify the columns we need, we can insert a string or a letter and see if it gets returned, as in the screenshot below.
Figure 11 - Column can hold Strings
Now, if we can see the input in the response, in this case the letter 'a,' it means that the column can hold string data types. In this case, we found the first and second columns can hold strings.
Now that we know which columns can display strings, let's try to get some usernames and passwords. To do this, we'll have to use the proper SQLite syntax; PayloadsAllTheThings, by Swisskyrepo, is an excellent resource for that information. First, we are going to extract the schema of this database.
Figure 12 - Database Table Disclosure
The query in the screenshot above dumps all of the tables in the database, along with their corresponding columns. This information allows attackers to quickly choose which tables may be the most relevant to the test and extract that information. In this case, the table an attacker would be most interested in is the users table, and the columns tell us that the table holds usernames and passwords.
Now that we know this table holds username and password data, we can create an SQL query to extract the username and password from that database, as shown in the screenshot below.
Figure 13 - Username and Password Disclosed
Command injection attack To RCE
This next section will talk about getting command injection to obtain remote code execution (RCE). To do this we will first need to get all the parameters, which can quickly be obtained by intercepting a web request with BurpSuite or another proxying tool. In our situation, we’ll be using the following parameters from the screenshot below in our attack query: “ImportPaste” to grab the pastes from the database; “host” to define our victim; “path” to specify the file or folder to view; and “scheme” to specify HTTP or HTTPS.
Figure 14 - Discovery of Parameters
In this case, the parameter that stands out the most is “path.” In cases where an application does not properly sanitize input, the path the user enters will be appended to the end of the command. For instance, if the application uses the cat command to display the file and the user enters the path “/etc/hosts,” the application may interpret the final command as “cat /etc/hosts” and display the contents of the hosts file. However, if the application doesn’t properly sanitize the input, the user can enter an additional command after the file path and have both commands executed. To test this, we can add a semicolon (;) after the path to end the first command and enter “ls -al” as well and see if the application returns a directory listing as shown in the screenshot below. With command injection verified, it may be possible to obtain RCE.
Figure 15 - OS Command Injection confirmed
Before attempting RCE, we must confirm if netcat, python, or other binaries that may allow a reverse shell are installed on the system. It just so happens that we are lucky, and netcat is on this system.
Figure 16 - Discovery of Binaries
Then, after trying a few netcat payloads, the mkfifo payload successfully returned a reverse shell.
Figure 17 - Reverse shell payload
A listener was listening on port 443, and the payload was sent and bam! We got a reverse shell. Happy Hacking, everyone.
Figure 18 - Reverse shell
Mitigation/Best Practices
The vulnerabilities demonstrated above can be remedied by following a series of Best Practices. These strategies come from OWASP, details for which can be found here: https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html.
There are 5 general practices to help prevent injection attacks:
Implement input validation. Use good regular expressions to ensure the type of data being input by a user is valid
Define a specific GRAPHQL data type, such as scalars or enums
Define specific schemas for mutation input
Use an allowed character list, and never use a denylist. A good rule of thumb is that the stricter the character allow list, the better
Finally, gracefully reject invalid input and be careful not to reveal too much information that could help an attacker. This would prevent excess and field suggestion disclosure
Next are 3 options for preventing SQL injection specifically:
Prepare statements with parameterized queries
Use properly constructed stored procedures
Utilize input validation
We have 3 primary options and 2 secondary options for preventing OS Command Injection:
Avoid calling OS commands directly
Escape values added to OS commands specific to each OS
Utilize parameterization in conjunction with input validation
Implement least privilege by ensuring the applications run using the lowest privileges that are required to accomplish the necessary tasks
If possible, create isolated accounts with limited privileges that are only used for a single task
Finally, while introspection is useful in a development environment for identifying issues, it should be disabled in production environments to prevent sensitive information from getting leaked. If introspection must be enabled in a production environment, ensure authentication is enforced with the proper access controls to verify the requestor is authorized to view the requested information.