mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-30 21:41:03 +00:00
Minor improvements
This commit is contained in:
parent
820efa7a8a
commit
7b60bc8284
11 changed files with 626 additions and 104 deletions
|
|
@ -22,8 +22,10 @@ from lib.core.data import logger
|
|||
from lib.core.enums import CUSTOM_LOGGING
|
||||
from lib.core.enums import POST_HINT
|
||||
from lib.core.settings import ERROR_PARSING_REGEXES
|
||||
from lib.core.settings import GRAPHQL_ARG_WORDLIST
|
||||
from lib.core.settings import GRAPHQL_ENDPOINT_PATHS
|
||||
from lib.core.settings import GRAPHQL_ERROR_REGEX
|
||||
from lib.core.settings import GRAPHQL_FIELD_WORDLIST
|
||||
from lib.core.settings import GRAPHQL_INTROSPECTION_QUERY
|
||||
from lib.core.settings import NOSQL_ERROR_REGEX
|
||||
from lib.core.settings import UPPER_RATIO_BOUND
|
||||
|
|
@ -354,6 +356,90 @@ def _introspect(endpoint):
|
|||
return None
|
||||
|
||||
|
||||
# --- Schema recovery via field suggestions (introspection disabled) ---------
|
||||
|
||||
def _gqlErrors(page):
|
||||
# GraphQL error-envelope messages as a list of strings
|
||||
doc = _parseJSON(page)
|
||||
if not isinstance(doc, dict):
|
||||
return []
|
||||
return [getUnicode(e.get("message", "")) for e in (doc.get("errors") or []) if isinstance(e, dict)]
|
||||
|
||||
|
||||
def _harvestSuggestions(message):
|
||||
# Pull suggested identifiers out of a "Did you mean ..." GraphQL validation message,
|
||||
# handling both single- and double-quoted phrasings ('a', 'b', or 'c' / "a" or "b")
|
||||
idx = message.find("Did you mean")
|
||||
if idx < 0:
|
||||
return []
|
||||
return re.findall(r"""['"]([A-Za-z_][A-Za-z0-9_]*)['"]""", message[idx:])
|
||||
|
||||
|
||||
def _suggestFields(endpoint, op):
|
||||
# Recover root field names for an operation via suggestion harvesting: probe a random
|
||||
# (guaranteed-unknown) field to collect the closest matches, then confirm/expand using a
|
||||
# seed wordlist. A seed that does NOT come back as "Cannot query field" is itself a real field.
|
||||
prefix = "" if op == "query" else "mutation "
|
||||
found = set()
|
||||
probes = [randomStr(length=10, lowercase=True)] + list(GRAPHQL_FIELD_WORDLIST)
|
||||
|
||||
for seed in probes:
|
||||
page, _ = _gqlSend(endpoint, "%s{ %s }" % (prefix, seed))
|
||||
doc = _parseJSON(page) or {}
|
||||
for entry in (doc.get("errors") or []):
|
||||
message = getUnicode(entry.get("message", "")) if isinstance(entry, dict) else ""
|
||||
if "Did you mean" in message and "on type" in message:
|
||||
found.update(_harvestSuggestions(message))
|
||||
# a seeded name counts as a real field only if it actually resolved (appears in `data`);
|
||||
# "no unknown-field error" alone is too weak (lenient servers accept anything)
|
||||
data = doc.get("data")
|
||||
if seed in GRAPHQL_FIELD_WORDLIST and isinstance(data, dict) and seed in data:
|
||||
found.add(seed)
|
||||
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def _suggestArgs(endpoint, op, field):
|
||||
# Recover an argument name for `field` from an "Unknown argument ... Did you mean ..." message
|
||||
prefix = "" if op == "query" else "mutation "
|
||||
bogus = randomStr(length=10, lowercase=True)
|
||||
page, _ = _gqlSend(endpoint, '%s{ %s(%s: 1) }' % (prefix, field, bogus))
|
||||
found = set()
|
||||
for message in _gqlErrors(page):
|
||||
if "Unknown argument" in message:
|
||||
found.update(_harvestSuggestions(message))
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def _introspectViaSuggestions(endpoint):
|
||||
# Fallback schema recovery when introspection is disabled but the server still leaks field/argument
|
||||
# names through "Did you mean" validation errors. Builds best-effort Slots: known scalar arg types
|
||||
# are unavailable here, so we default to the 'string' strategy (the most broadly injectable) and let
|
||||
# the per-slot injection oracle confirm which (field, argument) pairs are actually vulnerable.
|
||||
|
||||
probe = randomStr(length=10, lowercase=True)
|
||||
page, _ = _gqlSend(endpoint, "{ %s }" % probe)
|
||||
if not any("Did you mean" in m for m in _gqlErrors(page)):
|
||||
return None
|
||||
|
||||
logger.info("introspection is disabled; recovering the schema from field-suggestion errors")
|
||||
|
||||
slots = []
|
||||
for op, parentName in (("query", "Query"), ("mutation", "Mutation")):
|
||||
fields = _suggestFields(endpoint, op)
|
||||
if not fields:
|
||||
continue
|
||||
logger.info("recovered %d %s field(s) via suggestions: %s" % (
|
||||
len(fields), op, ", ".join(fields)))
|
||||
for field in fields:
|
||||
args = _suggestArgs(endpoint, op, field) or list(GRAPHQL_ARG_WORDLIST)
|
||||
for arg in args:
|
||||
# returnSel="" renders as "{ __typename }" (valid on any OBJECT); strategy="string"
|
||||
slots.append(Slot(op, parentName, field, [(arg, {}, None)],
|
||||
arg, "string", "OBJECT", "", ""))
|
||||
return slots or None
|
||||
|
||||
|
||||
# --- Schema walking ---------------------------------------------------------
|
||||
|
||||
def _extractSlots(schema):
|
||||
|
|
@ -1087,11 +1173,11 @@ def graphqlScan():
|
|||
global SENTINEL
|
||||
SENTINEL = randomStr(length=10, lowercase=True)
|
||||
|
||||
infoMsg = "'--graphql' is self-contained: it discovers the GraphQL endpoint, "
|
||||
infoMsg += "enumerates the schema, and injects SQL/NoSQL payloads into reachable "
|
||||
infoMsg += "argument slots. SQL enumeration switches (e.g. --banner, --dbs, "
|
||||
infoMsg += "--tables) are ignored"
|
||||
logger.info(infoMsg)
|
||||
debugMsg = "'--graphql' is self-contained: it discovers the GraphQL endpoint, "
|
||||
debugMsg += "enumerates the schema, and injects SQL/NoSQL payloads into reachable "
|
||||
debugMsg += "argument slots. SQL enumeration switches (e.g. --banner, --dbs, "
|
||||
debugMsg += "--tables) are ignored"
|
||||
logger.debug(debugMsg)
|
||||
|
||||
url = conf.url.rstrip("/") if conf.url else ""
|
||||
|
||||
|
|
@ -1120,19 +1206,22 @@ def graphqlScan():
|
|||
# 2. Schema introspection
|
||||
logger.info("introspecting the GraphQL schema")
|
||||
schema = _introspect(endpoint)
|
||||
if not schema:
|
||||
logger.error("introspection failed (disabled or the endpoint rejected the query)")
|
||||
return
|
||||
|
||||
types = schema.get("types") or []
|
||||
logger.info("introspection returned %d types" % len(types))
|
||||
|
||||
# 3. Slot enumeration
|
||||
slots = _extractSlots(schema)
|
||||
if not slots:
|
||||
logger.warning("no injectable argument slots found in the schema")
|
||||
_dumpSchema(schema, endpoint)
|
||||
return
|
||||
if schema:
|
||||
types = schema.get("types") or []
|
||||
logger.info("introspection returned %d types" % len(types))
|
||||
slots = _extractSlots(schema)
|
||||
if not slots:
|
||||
logger.warning("no injectable argument slots found in the schema")
|
||||
_dumpSchema(schema, endpoint)
|
||||
return
|
||||
else:
|
||||
# Introspection blocked: try to recover the schema from field-suggestion errors
|
||||
logger.warning("introspection failed (disabled or rejected); trying suggestion-based recovery")
|
||||
slots = _introspectViaSuggestions(endpoint)
|
||||
if not slots:
|
||||
logger.error("could not recover the schema (introspection disabled and no field suggestions)")
|
||||
return
|
||||
|
||||
querySlots = [_ for _ in slots if _.operation == "query"]
|
||||
mutationSlots = [_ for _ in slots if _.operation == "mutation"]
|
||||
|
|
@ -1141,8 +1230,10 @@ def graphqlScan():
|
|||
len(slots), len(querySlots), len(mutationSlots)))
|
||||
|
||||
# 4. Schema dump (before detection -- matches regular sqlmap table/column
|
||||
# enumeration preceding data retrieval)
|
||||
_dumpSchema(schema, endpoint)
|
||||
# enumeration preceding data retrieval). Only when introspection succeeded; the
|
||||
# suggestion-recovered path has no full schema document to render.
|
||||
if schema:
|
||||
_dumpSchema(schema, endpoint)
|
||||
|
||||
if mutationSlots:
|
||||
names = sorted(set("%s(%s:)" % (_.fieldName, _.targetArg) for _ in mutationSlots))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue