diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5d6e0d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, graphw00f +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbb7a39 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +

+ +
+ graphw00f - GraphQL Fingerprinting +

+ +graphw00f (inspired by [wafw00f](https://github.com/EnableSecurity/wafw00f)) is a GraphQL fingerprinting tool. + +# Table of Contents +* [How does it work?](#how-does-it-work) +* [Detections](#detections) +* [GraphQL Technologies Defence Matrices](#graphql-technologies-defence-matrices) +* [Prerequisites](#prerequisites) +* [Installation](#installation) +* [Support & Issues](#support-and-issues) +* [Resources](#resources) + + +# How does it work? +graphw00f is a Python utility which attempts to send a mixture of benign and malformed queries to determine the GraphQL engine running behind the scenes. + +Different GraphQL servers respond uniquely to queries, mutations and subscriptions given the right payload, this makes it trivial to fingerprint and distinguish between the various GraphQL servers. (CWE: [CWE-200](#CWE-Reference)) + +# Detections +graphw00f currently attempts to discover the following GraphQL engines: +* Graphene +* Ariadne +* Apollo +* graphql-go +* gqlgen +* WPGraphQL +* GraphQL API for Wordpress +* Ruby GraphQL +* graphql-php +* Hasura +* HyperGraphQL +* GraphQL for Java + +# GraphQL Technologies Defence Matrices +Each fingerprinted technology (e.g. Graphene, Ariadne, ...) has an associated document ([example for graphene](https://github.com/dolevf/graphw00f/blob/main/docs/graphene.md)) which covers the security defence mechanisms the specific technology supports to give a better idea how the implementation may be attacked. + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|--------------------|------------|-----------------| +| On by Default | No Support | No Support | No Support | Enabled by Default | N/A | Off by Default | +``` + +# Prerequisites +* python3 +* requests + +# Installation +## Clone Repository +`git clone git@github.com:dolevf/graphw00f.git` + +## Run graphw00f +`python3 main.py -h` + +``` +Usage: main.py -h + +Options: + -h, --help show this help message and exit + -r, --noredirect Do not follow redirections given by 3xx responses + -t URL, --target=URL target url with the path + -o OUTPUT_FILE, --output-file=OUTPUT_FILE + Output results to a file (CSV) + -l, --list List all GraphQL technologies graphw00f is able to + detect + -v, --version Print out the current version and exit. +``` + +# Example +``` +python3 main.py -t http://127.0.0.1:5000/graphql + ++-------------------+ +--------------------+ +| GRAPHQL | | FINGERPRINT | ++-------------------+ +--------------------+ + ** ** + *** *** + ** ** + +-------------------+ + | graphw00f | + +-------------------+ + *** *** + ** *** + ** ** + +--------------+ +--------------+ + | Node X | | Node Y | + +--------------+ +--------------+ + *** *** + ** ** + ** ** + +------------+ + | Node Z | + +------------+ + + graphw00f - v1.0.0 + The fingerprinting tool for GraphQL + +[*] Checking if GraphQL is available at http://127.0.0.1:8088/graphql... +[*] Found GraphQL. +[*] Attempting to fingerprint... +[*] Discovered GraphQL Engine! +[!] The site https://www.graphql-java.com is using: graphql-java - GraphQL for Java +[!] Attack Surface Matrix: https://github.com/dolevf/graphw00f/blob/main/docs/graphql-java.md +[!] Technologies: Java +[!] Homepage: https://www.graphql-java.com +[*] Completed. +``` + +# Support and Issues +Any issues with graphw00f such as false/true positives, inaccurate detections, etc. please create a GitHub issue with environment details. + +# Resources +Want to learn more about GraphQL? head over to my other project and hack GraphQL away: [Damn Vulnerable GraphQL Application](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/) diff --git a/conf.py b/conf.py new file mode 100644 index 0000000..1138bf4 --- /dev/null +++ b/conf.py @@ -0,0 +1,7 @@ +# Custom Headers +# HEADERS = {"User-Agent":"My User Agent"} +HEADERS = {'User-Agent':'graphw00f'} + +# Custom Cookies +# COOKIES = {"PHPSESS":"DEADBEEF"} +COOKIES = {} diff --git a/docs/apollo.md b/docs/apollo.md new file mode 100644 index 0000000..a0eda0e --- /dev/null +++ b/docs/apollo.md @@ -0,0 +1,18 @@ +# Apollo + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +Apollo Server is a community-maintained open-source GraphQL server. It works with many Node.js HTTP server frameworks, or can run on its own with a built-in Express server. Apollo Server works with any GraphQL schema built with GraphQL.js--or define a schema's type definitions using schema definition language (SDL). +Apollo uses TypeScript as its language. + +# Security Features +Apollo offers the following features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|----------------------------------|----------------------------------|-----------------------------|------------------------------------------------|-------------------------------------------------------------------------------|-----------------| +| On by Default | Supported via External Libraries | Supported via External Libraries | Supported | Enabled if NODE_ENV is not set to 'production' | exception.stacktrace exists if NODE_ENV is not set to 'production' or 'test' | On by default | +``` \ No newline at end of file diff --git a/docs/ariadne.md b/docs/ariadne.md new file mode 100644 index 0000000..7b5c0a6 --- /dev/null +++ b/docs/ariadne.md @@ -0,0 +1,17 @@ +# Ariadne + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +Ariadne is a Python library for implementing GraphQL servers using a schema-first approach. + +# Security Features +Ariadne offers the following features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|--------------------|----------------|-----------------| +| On by Default | Supported | Supported | No Support | Enabled by Default | Off by Default | No Support | +``` \ No newline at end of file diff --git a/docs/gqlgen.md b/docs/gqlgen.md new file mode 100644 index 0000000..81660ee --- /dev/null +++ b/docs/gqlgen.md @@ -0,0 +1,17 @@ +# gqlgen + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +gqlgen is a Go library for building GraphQL servers without any fuss, based on schema-first approach. + +# Security Features +gqlgen provides the following security features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|---------------|----------------|-----------------| +| On by Default | No Support | Off by Default | Off by Default | On by Default | Off by Default | Off by Default | +``` \ No newline at end of file diff --git a/docs/graphene.md b/docs/graphene.md new file mode 100644 index 0000000..d361a8f --- /dev/null +++ b/docs/graphene.md @@ -0,0 +1,17 @@ +# Graphene + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +Graphene-Python is a library for building GraphQL APIs in Python easily, its main goal is to provide a simple but extendable API for making developers' lives easier. + +# Security Features +Graphene offers the following features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|--------------------|------------|-----------------| +| On by Default | No Support | No Support | No Support | Enabled by Default | N/A | Off by Default | +``` \ No newline at end of file diff --git a/docs/graphql-go.md b/docs/graphql-go.md new file mode 100644 index 0000000..b50d538 --- /dev/null +++ b/docs/graphql-go.md @@ -0,0 +1,17 @@ +# GraphQL-Go + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +An implementation of GraphQL in Go. + +# Security Features +graphql-go offers the following features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|--------------------|----------------|-----------------| +| On by Default | No Support | No Support | No Support | Enabled by Default | Off by Default | No Support | +``` diff --git a/docs/graphql-java.md b/docs/graphql-java.md new file mode 100644 index 0000000..5d6b475 --- /dev/null +++ b/docs/graphql-java.md @@ -0,0 +1,17 @@ +# GraphQL Java + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +The GraphQL Java is an implementation of the GraphQL specification for the Java language. + +# Security Features +GraphQL Java offers the following security features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|--------------------|------------|-----------------| +| On by Default | Off by Default | Off by Default | No Support | Enabled by Default | No Support | Off by Default | +``` \ No newline at end of file diff --git a/docs/graphql-php.md b/docs/graphql-php.md new file mode 100644 index 0000000..16f362e --- /dev/null +++ b/docs/graphql-php.md @@ -0,0 +1,17 @@ +# ProductName + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +graphql-php is a PHP implementation of the GraphQL specification. + +# Security Features +graphql-php offers the following features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|---------------------------------|---------------------------------|-----------------------------|--------------------|----------------|---------------------------------| +| On by Default | Supported - Disabled by Default | Supported - Disabled by Default | No Support | Enabled by Default | Off by Default | Supported - Disabled by Default | +``` \ No newline at end of file diff --git a/docs/graphqlapiforwp.md b/docs/graphqlapiforwp.md new file mode 100644 index 0000000..7cca930 --- /dev/null +++ b/docs/graphqlapiforwp.md @@ -0,0 +1,25 @@ +# GraphQL API For WordPress + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +GraphQL API For WordPress bring the most powerful GraphQL experience into your WordPress site + +# Security Features +GraphQL API For WordPress provides the followign security features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|---------------|------------|-----------------| +| On by Default | No Support | No Support | Off by Default | N/A | No Support | No Support | +``` + +While GraphQL API for Wordpress does not provide common security mechanisms out of the box, it does provide additional controls: + +* Access Control Lists +* Persisted Queries on custom endpoints +* Access granularity on schemas + +The existence of these features in practice depends on the WordPress Admin, they may or may not be enabled. \ No newline at end of file diff --git a/docs/hasura.md b/docs/hasura.md new file mode 100644 index 0000000..1798fb6 --- /dev/null +++ b/docs/hasura.md @@ -0,0 +1,19 @@ +# Hasura + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +The Hasura GraphQL engine makes your data instantly accessible over a real-time GraphQL API, so you can build and ship modern apps and APIs faster. Hasura connects to your databases, REST servers, GraphQL servers, and third party APIs to provide a unified realtime GraphQL API across all your data sources. + +# Security Features +While Hasura Cloud provides some security mechanisms, Hasura API (the non-cloud version) provides a limited set of security features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|---------------|------------|-----------------| +| On by Default | No Support | No Support | No Support | N/A | No Support | No Support | +``` + +Hasura non-cloud provides Access Control Lists options, however, they must be explicitly enabled and used. \ No newline at end of file diff --git a/docs/hypergraphql.md b/docs/hypergraphql.md new file mode 100644 index 0000000..f2f2170 --- /dev/null +++ b/docs/hypergraphql.md @@ -0,0 +1,17 @@ +# HyperGraphQL + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +An implementation of GraphQL in Java + +# Security Features +HyperGraphQL offers the following features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|--------------------|------------|-----------------| +| No Support | No Support | No Support | No Support | Enabled by Default | No Support | No Support | +``` diff --git a/docs/ruby-graphql.md b/docs/ruby-graphql.md new file mode 100644 index 0000000..cf6b180 --- /dev/null +++ b/docs/ruby-graphql.md @@ -0,0 +1,17 @@ +# Ruby GraphQL + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +ruby-graphql is a Ruby implementation of the GraphQL specification. + +# Security Features +Ruby GraphQL provides the following security features: + +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|--------------------|------------|-----------------| +| On by Default | No Support | Off by Default | Off by Default | Enabled by Default | No Support | On by Default | +``` \ No newline at end of file diff --git a/docs/templ.md b/docs/templ.md new file mode 100644 index 0000000..39bb073 --- /dev/null +++ b/docs/templ.md @@ -0,0 +1,9 @@ +# ProductName + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About + +# Security Features \ No newline at end of file diff --git a/docs/wpgraphql.md b/docs/wpgraphql.md new file mode 100644 index 0000000..928ea2e --- /dev/null +++ b/docs/wpgraphql.md @@ -0,0 +1,16 @@ +# WPGraphQL + +# Table of Contents +* [About](#About) +* [Security Features](#Security-Features) + +# About +WPGraphQL is a WordPress plugin which provides a WordPress instance with immediate GraphQL API support. + +# Security Features +WPGraphQL offers the following security features: +``` +| Field Suggestions | Query Depth Limit | Query Cost Analysis | Automatic Persisted Queries | Introspection | Debug Mode | Batch Requests | +|-------------------|-------------------|---------------------|-----------------------------|----------------|----------------|-----------------| +| On by Default | Off by Default | No Support | No Support | Off by Default | Off by Default | On by Default | +``` \ No newline at end of file diff --git a/graphw00f/helpers.py b/graphw00f/helpers.py new file mode 100644 index 0000000..6bdab62 --- /dev/null +++ b/graphw00f/helpers.py @@ -0,0 +1,133 @@ + +import datetime +from urllib.parse import urlparse +from version import VERSION + +class bcolors: + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + +def error_contains(response, word_to_match): + if isinstance(response, dict): + if response.get('errors'): + for i in response['errors']: + err_message = i.get('message', '') + if word_to_match in err_message: + return True + return False + +def get_time(): + return datetime.datetime.now().strftime('%Y-%m-%d') + +def draw_art(): + return ''' ++-------------------+ +--------------------+ +| GRAPHQL | | FINGERPRINT | ++-------------------+ +--------------------+ + ** ** + *** *** + ** ** + +-------------------+ + | graphw00f | + +-------------------+ + *** *** + ** *** + ** ** + +--------------+ +--------------+ + | Node X | | Node Y | + +--------------+ +--------------+ + *** *** + ** ** + ** ** + +------------+ + | Node Z | + +------------+ + + graphw00f - v{version} + The fingerprinting tool for GraphQL + '''.format(version=VERSION) + +def get_engines(): + return { + 'apollo':{ + 'name':'Apollo', + 'url':'https://www.apollographql.com', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/apollo.md', + 'technology':['JavaScript', 'Node.js', 'TypeScript'] + }, + 'graphene':{ + 'name':'Graphene', + 'url':'https://graphene-python.org', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/graphene.md', + 'technology':['Python'] + }, + 'hasura':{ + 'name':'Hasura', + 'url':'https://hasura.io', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/hasura.md', + 'technology':['Haskell'] + }, + 'graphql-php':{ + 'name':'GraphQL PHP', + 'url':'https://webonyx.github.io/graphql-php', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/graphql-php.md', + 'technology':['PHP'] + }, + 'ruby-graphql':{ + 'name':'Ruby GraphQL', + 'url':'https://graphql-ruby.org', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/ruby-graphql.md', + 'technology':['Ruby'] + }, + 'hypergraphql':{ + 'name':'HyperGraphQL', + 'url':'https://www.hypergraphql.org', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/hypergraphql.md', + 'technology':['Java'] + }, + 'ariadne':{ + 'name':'Ariadne', + 'url':'https://ariadnegraphql.org', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/ariadne.md', + 'technology':['Python'] + }, + 'graphql-api-for-wp':{ + 'name':'GraphQL API for Wordpress', + 'url':'https://graphql-api.com', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/graphqlapiforwp.md', + 'technology':['PHP'], + }, + 'wpgraphql':{ + 'name':'WPGraphQL WordPress Plugin', + 'url':'https://www.wpgraphql.com', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/wpgraphql.md', + 'technology':['PHP'] + }, + 'gqlgen':{ + 'name':'gqlgen - GraphQL for Go', + 'url':'https://gqlgen.com', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/gqlgen.md', + 'technology':['Go'] + }, + 'graphql-go':{ + 'name':'graphql-go -GraphQL for Go', + 'url':'https://github.com/graphql-go/graphql', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/graphql-go.md', + 'technology':['Go'] + }, + 'graphql-java':{ + 'name':'graphql-java - GraphQL for Java', + 'url':'https://www.graphql-java.com', + 'ref':'https://github.com/dolevf/graphw00f/blob/main/docs/graphql-java.md', + 'technology':['Java'] + } + } + +def user_confirmed(choice): + if choice in ('yes', 'y'): + return True + return False diff --git a/graphw00f/lib.py b/graphw00f/lib.py new file mode 100644 index 0000000..a78512a --- /dev/null +++ b/graphw00f/lib.py @@ -0,0 +1,429 @@ +import requests + +from graphw00f.helpers import error_contains + +requests.packages.urllib3.disable_warnings() + +class GraphQLDetectionFailed(Exception): + pass + +class GraphQLUnknownState(Exception): + pass + +class GraphQLError(Exception): + pass + +class GRAPHW00F: + def __init__(self, headers, + cookies, + follow_redirects=False): + self.url = 'http://example.com' + self.cookies = cookies + self.headers = headers + self.follow_redirects = follow_redirects + + def check(self, url): + query = ''' + query { + __typename + } + ''' + response = self.graph_query(url, payload=query) + if response.get('data', {}).get('__typename', '') in ('Query', 'QueryRoot', 'query_root'): + return True + elif response.get('errors') and any('locations' in i for i in response['errors']): + return True + elif response.get('data'): + return True + else: + raise GraphQLDetectionFailed + raise GraphQLUnknownState + + def execute(self, url): + self.url = url + if self.engine_graphene(): + return 'graphene' + elif self.engine_ariadne(): + return 'ariadne' + elif self.engine_apollo(): + return 'apollo' + elif self.engine_hasura(): + return 'hasura' + elif self.engine_wpgraphql(): + return 'wpgraphql' + elif self.engine_graphqlapiforwp(): + return 'graphql-api-for-wp' + elif self.engine_graphqljava(): + return 'graphql-java' + elif self.engine_hypergraphql(): + return 'hypergraphql' + elif self.engine_ruby(): + return 'ruby-graphql' + elif self.engine_graphqlphp(): + return 'graphql-php' + elif self.engine_gqlgen(): + return 'gqlgen' + elif self.engine_graphqlgo(): + return 'graphql-go' + return None + + def graph_query(self, url, operation='query', payload={}): + try: + response = requests.post(url, + headers=self.headers, + cookies=self.cookies, + verify=False, + allow_redirects=self.follow_redirects, + json={operation:payload}) + return response.json() + except GraphQLError: + return {} + except: + return {} + + def engine_apollo(self): + query = ''' + query @skip { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Directive "@skip" argument "if" of type "Boolean!" is required, but it was not provided.'): + return True + + query = ''' + query @deprecated { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Directive "@deprecated" may not be used on QUERY.'): + return True + + def engine_graphene(self): + query = '' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Must provide query string.'): + return True + + query = '''aaa''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Syntax Error GraphQL (1:1)'): + return True + + return False + + def engine_hasura(self): + query = ''' + query @cached { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if response.get('data'): + if response.get('data', {}).get('__typename', '') == 'query_root': + return True + + query = ''' + query { + __schema + } + ''' + response = self.graph_query(self.url, payload=query) + + if error_contains(response, 'missing selection set for "__Schema"'): + return True + + query = ''' + query { + aa + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'field "aaa" not found in type: \'query_root\''): + return True + + query = ''' + query @skip { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'directive "skip" is not allowed on a query'): + return True + + return False + + def engine_graphqlphp(self): + query = ''' + query ! { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Syntax Error: Cannot parse the unexpected character "?".'): + return True + + query = ''' + subscription { + s + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Schema is not configured for subscriptions.'): + return True + + return False + + def engine_ruby(self): + query = ''' + query @skip { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, '\'@skip\' can\'t be applied to queries (allowed: fields, fragment spreads, inline fragments)'): + return True + elif error_contains(response, 'Directive \'skip\' is missing required arguments: if'): + return True + + query = ''' + query @deprecated { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, '\'@deprecated\' can\'t be applied to queries'): + return True + + query = ''' + query aa@aa { + __schema { + directives { + description + } + } + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Directive @aa is not defined'): + return True + + query = ''' + query { + __schema + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Field must have selections (field \'__schema\' returns __Schema but has no selections.'): + return True + + return False + + def engine_hypergraphql(self): + query = ''' + zzz { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Validation error of type InvalidSyntax: Invalid query syntax.'): + return True + + query = ''' + query { + __schema { + directives { + descriptio + } + } + } + ''' + response = self.graph_query(self.url, payload=query) + if response.get('errors'): + for i in response['errors']: + qp = i.get('queryPath', []) + matches = ['__schema', 'directives', 'descriptio'] + if qp == matches: + return True + + return False + + def engine_graphqljava(self): + query = ''' + queryy { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Invalid Syntax : offending token \'queryy\''): + return True + + query = ''' + query @aaa@aaa { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Validation error of type DuplicateDirectiveName: Directives must be uniquely named within a location.'): + return True + + query = '' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Invalid Syntax : offending token \'\''): + return True + + return False + + def engine_ariadne(self): + query = ''' + query { + __schema + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Field \'__schema\' of type \'__Schema!\' must have a selection of subfields.'): + return True + + query = ''' + subscription { + s + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Could not connect to websocket endpoint'): + return True + + query = '' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'The query must be a string.'): + return True + + return False + + def engine_graphqlapiforwp(self): + query = ''' + query { + alias1$1:__schema + } + ''' + response = self.graph_query(self.url, payload=query) + if response.get('data'): + if response.get('data').get('alias1$1', '') == 'schema': + return True + + query = '''query aa#aa { __typename }''' + response = self.graph_query(self.url, payload=query) + + if error_contains(response, 'Unexpected token "END"'): + return True + + query = ''' + query @skip { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Argument \'if\' cannot be empty, so directive \'skip\' has been ignored'): + return True + + query = ''' + query @doesnotexist { + __typename + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'No DirectiveResolver resolves directive with name \'doesnotexist\''): + return True + + query = '' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'The query in the body is empty'): + return True + + return False + + def engine_wpgraphql(self): + query = '' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'GraphQL Request must include at least one of those two parameters: "query" or "queryId"'): + return True + + query = ''' + query { + alias1$1:__schema + } + ''' + response = self.graph_query(self.url, payload=query) + if not error_contains(response, 'Syntax Error: Expected Name, found $'): + return False + + try: + debug_msg = response['extensions']['debug'][0] + if debug_msg['type'] == 'DEBUG_LOGS_INACTIVE' or \ + debug_msg['message'] == 'GraphQL Debug logging is not active. To see debug logs, GRAPHQL_DEBUG must be enabled.': + return True + except KeyError: + pass + + return False + + def engine_gqlgen(self): + query = ''' + query @aa@aa { + __schema + } + ''' + response = self.graph_query(self.url, payload=query) + + if error_contains(response, 'The directive "aa" can only be used once at this location.'): + return True + + query = ''' + queryyy { + __schema + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Unexpected Name "queryyy"'): + return True + return False + + def engine_graphqlgo(self): + query = ''' + query @skip { + abc + } + ''' + response = self.graph_query(self.url, payload=query) + if error_contains(response, 'Directive "skip" may not be used on QUERY. Directive "@skip" argument "if" of type "Boolean!" is required but not provided'): + return True + + query = '' + response = self.graph_query(self.url, payload=query) + + if error_contains(response, 'Must provide an operation.'): + return True + + query = ''' + query ? { + __schema + } + ''' + response = self.graph_query(self.url, payload=query) + + if error_contains(response, 'Unexpected character "?"'): + return True + + query = ''' + query ? { + __schema + } + ''' + + response = self.graph_query(self.url, payload=query) + + if error_contains(response, 'Syntax Error GraphQL request'): + return True + + return False diff --git a/main.py b/main.py new file mode 100644 index 0000000..9167e48 --- /dev/null +++ b/main.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +import conf + +from graphw00f.helpers import ( + get_time, + draw_art, + get_engines, + user_confirmed, + bcolors +) +from time import sleep +from urllib.parse import urlparse +from optparse import OptionParser + +from version import VERSION +from graphw00f.lib import ( + GRAPHW00F, + GraphQLDetectionFailed, + GraphQLUnknownState +) + + +def main(): + parser = OptionParser(usage='%prog -t http://example.com/graphql') + parser.add_option('-r', '--noredirect', action='store_false', dest='followredirect', default=True, + + help='Do not follow redirections given by 3xx responses') + parser.add_option('-t', '--target', dest='url', help='target url with the path') + parser.add_option('-o', '--output-file', dest='output_file', + help='Output results to a file (CSV)', default=None) + parser.add_option('-l', '--list', dest='list', action='store_true', default=False, + help='List all GraphQL technologies graphw00f is able to detect') + parser.add_option('--version', '-v', dest='version', action='store_true', default=False, + help='Print out the current version and exit.') + options, args = parser.parse_args() + + if options.list: + print(draw_art()) + for k, v in graphw00f.helpers.get_engines().items(): + print('{key}: {name} ({language})'.format( + key=k, + name=v['name'], + language=', '.join(v['language'])) + ) + sys.exit(0) + + if options.version: + print('version:', VERSION) + sys.exit(0) + + if not options.url: + parser.print_help() + sys.exit(1) + + url = options.url + url_path = urlparse(url).path + url_scheme = urlparse(url).scheme + url_netloc = urlparse(url).netloc + + print(draw_art()) + + if url_scheme not in ('http', 'https'): + print('URL is missing a scheme (http|https)') + sys.exit(1) + + if not url_netloc: + print('url {url} does not seem right.'.format(url=url)) + sys.exit(1) + + if not url_path: + print('[*] No URL path was provided.') + print('[*[ are you sure you want to fingerprint the server without a path? [y/n]') + choice = input().lower() + if not user_confirmed(choice): + sys.exit(1) + + + print('[*] Checking if GraphQL is available at {url}...'.format(url=url)) + + g = GRAPHW00F(follow_redirects=options.followredirect, + headers=conf.HEADERS, + cookies=conf.COOKIES) + detected = None + try: + if g.check(url): + print('[*] Found GraphQL.') + except GraphQLDetectionFailed: + print(bcolors.FAIL + '[x] Could not determine existence of GraphQL (GraphQLDetectionFailed)' + bcolors.ENDC) + print('[*] Continue anyway? [y/n]'.format(url=url)) + choice = input().lower() + if not user_confirmed(choice): + print('Quitting.') + sys.exit(1) + except GraphQLUnknownState: + print('Something went wrong.') + sys.exit(1) + + print('[*] Attempting to fingerprint...') + result = g.execute(url) + + if result: + name = get_engines()[result]['name'] + url = get_engines()[result]['url'] + ref = get_engines()[result]['ref'] + technologies = ', '.join(get_engines()[result]['technology']) + detected = name + print(bcolors.OKGREEN + '[*] Discovered GraphQL Engine!') + print('[!] The site {} is using: {}'.format(url, name)) + print('[!] Attack Surface Matrix: {}'.format(ref)) + print('[!] Technologies: {}'.format(technologies)) + print('[!] Homepage: {}'.format(url)) + else: + print('[x] Nothing was found :-(') + + if options.output_file: + f = open(options.output_file, 'w') + f.write('url,detected_engine,timestamp\n') + f.write('{},{},{}\n'.format(url_netloc, detected, get_time())) + f.close() + + print('[*] Completed.') + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/static/graphw00f.png b/static/graphw00f.png new file mode 100644 index 0000000..1fd8057 Binary files /dev/null and b/static/graphw00f.png differ diff --git a/version.py b/version.py new file mode 100644 index 0000000..1dea037 --- /dev/null +++ b/version.py @@ -0,0 +1 @@ +VERSION = '1.0.1'