Skip to content

Commit dc38f6a

Browse files
authored
Merge pull request #21012 from adfoster-r7/improve-graphql-introspection-detection-scanner
Improve graphql introspection detection scanner
2 parents c1caf8a + 03f28eb commit dc38f6a

File tree

1 file changed

+45
-13
lines changed

1 file changed

+45
-13
lines changed

modules/auxiliary/scanner/http/graphql_introspection_scanner.rb

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class MetasploitModule < Msf::Auxiliary
88
include Msf::Exploit::Remote::HttpClient
99
include Msf::Auxiliary::Report
1010

11+
prepend Msf::Exploit::Remote::AutoCheck
12+
1113
def initialize(info = {})
1214
super(
1315
update_info(
@@ -214,7 +216,9 @@ def report_graphql_web_vuln(service, query, response)
214216
name: 'GraphQL Introspection',
215217
description: 'GraphQL endpoint has enabled introspection. This can lead to information disclosure',
216218
owner: self,
217-
category: 'Information Disclosure'
219+
category: 'Information Disclosure',
220+
confidence: 100,
221+
risk: 4
218222
}
219223
)
220224
end
@@ -236,9 +240,9 @@ def send_graphql_request(query)
236240

237241
# Process the errors array into a nice human-readable and formatted string.
238242
# @param errors An array of errors.
239-
# @return [String] A string with formatted error messages
243+
# @return [String, nil] A string with formatted error messages
240244
def process_errors(errors)
241-
return '' if errors&.empty?
245+
return nil if errors&.empty?
242246

243247
# APIs aren't consistent. Some have an error message, some have title & detail.
244248
# Match all the known cases so far, otherwise return the inspected value.
@@ -248,6 +252,12 @@ def process_errors(errors)
248252
end.join("\n") || ''
249253
end
250254

255+
def valid_graphql_response?(hash)
256+
return false if hash.nil?
257+
258+
hash.try(:[], 'data').try(:[], '__schema').try(:[], 'queryType').try(:[], 'name').is_a?(String)
259+
end
260+
251261
# Check if the current endpoint is vulnerable to GraphQL Introspection information disclosure.
252262
# @return [Exploit::CheckCode]
253263
def check
@@ -260,14 +270,26 @@ def check
260270

261271
case res.code
262272
when 200
263-
graphql_service = report_graphql_service
264-
report_graphql_vuln
265-
report_graphql_web_vuln(graphql_service, query, res)
273+
json = res.get_json_document
274+
275+
# validate the JSON response has the expected structure of an introspection response, to avoid false positives.
276+
if valid_graphql_response?(json)
277+
graphql_service = report_graphql_service
278+
report_graphql_vuln
279+
report_graphql_web_vuln(graphql_service, query, res)
266280

267-
return Exploit::CheckCode::Vulnerable('The server has introspection enabled.')
281+
return Exploit::CheckCode::Vulnerable('The server has introspection enabled.')
282+
else
283+
return Exploit::CheckCode::Safe('The server responded with a 200 status code, but the response did not have the expected structure of an introspection response.')
284+
end
268285
when 400
269-
parsed_body = JSON.parse!(res.body)
270-
error_messages = process_errors(parsed_body['errors'] || Array.wrap(parsed_body['error']))
286+
json = res.get_json_document
287+
error_messages = process_errors(json['errors'] || Array.wrap(json['error']))
288+
289+
if error_messages.nil?
290+
return Exploit::CheckCode::Safe('The server responded with an error status code, but did not provide any error messages in the response.')
291+
end
292+
271293
safe_message = "The server responded with an error status code and the following error(s) to the introspection request:\n#{error_messages}"
272294
return Exploit::CheckCode::Safe(safe_message)
273295
when 403
@@ -279,7 +301,7 @@ def check
279301
return Exploit::CheckCode::Unknown('The server required a CSRF token.')
280302
else
281303
# We are not 100% sure that the service is a GraphQL endpoint. It could be a generic 403 Access Denied.
282-
return Exploit::CheckCode::Unknown('The server is online, but returned an unexpected response code.')
304+
return Exploit::CheckCode::Unknown("The server is online, but returned an unexpected response code: '#{res.code}'.")
283305
end
284306
end
285307

@@ -296,14 +318,24 @@ def run
296318

297319
if res.code == 200
298320
print_good("#{rhost}:#{rport} - Server responded with introspected data. Reporting a vulnerability, and storing it as loot.")
321+
# validate the JSON response has the expected structure of an introspection response, to avoid false positives.
322+
unless valid_graphql_response?(res.get_json_document)
323+
print_error("#{rhost}:#{rport} - Server responded with a 200 status code, but the response did not have the expected structure of an introspection response")
324+
return
325+
end
299326
graphql_service = report_graphql_service
300327
report_graphql_vuln
301328
report_graphql_web_vuln(graphql_service, query, res)
302329
store_loot('graphql.schema', 'json', rhost, res.body, 'graphql-schema.json', 'GraphQL Schema Dump', graphql_service)
303330
else
304-
parsed_body = JSON.parse!(res.body)
305-
if parsed_body.include?('errors') || parsed_body.include?('error')
306-
print_error("#{rhost}:#{rport} - Server encountered the following error(s) (code: '#{res.code}'):\n#{process_errors(parsed_body['errors'] || Array.wrap(parsed_body['error']))}")
331+
json = res.get_json_document
332+
if json.nil?
333+
print_error("#{rhost}:#{rport} - Server replied with an unexpected status code: '#{res.code}', and the response was not a valid JSON document.")
334+
return
335+
end
336+
337+
if json.key?('errors') || json.key?('error')
338+
print_error("#{rhost}:#{rport} - Server encountered the following error(s) (code: '#{res.code}'):\n#{process_errors(json['errors'] || Array.wrap(json['error']))}")
307339
else
308340
print_error("#{rhost}:#{rport} - Server replied with an unexpected status code: '#{res.code}'")
309341
end

0 commit comments

Comments
 (0)