From 9a34282aa30c0c4680fb145db31c2fe77a828e3e Mon Sep 17 00:00:00 2001 From: David Donn Date: Tue, 14 Sep 2021 17:17:21 +1000 Subject: [PATCH 01/12] get operations with query parameters --- lib/core/common.py | 51 +++++++++++++++++++++++++++++++++++++++++ lib/core/option.py | 32 +++++++++++++++++++++++--- lib/core/optiondict.py | 2 ++ lib/parse/cmdline.py | 6 +++++ lib/parse/configfile.py | 4 ++-- sqlmap.conf | 6 +++++ 6 files changed, 96 insertions(+), 5 deletions(-) diff --git a/lib/core/common.py b/lib/core/common.py index ec82ad7338b..55d8e1b44f4 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -36,6 +36,7 @@ import time import types import unicodedata +import json from difflib import SequenceMatcher from math import sqrt @@ -5362,6 +5363,53 @@ def _parseBurpLog(content): if not(conf.scope and not re.search(conf.scope, url, re.I)): yield (url, conf.method or method, data, cookie, tuple(headers)) + def _parseSwagger(content): + """ + Parses Swagger OpenAPI 3.x.x JSON documents + """ + + try: + swagger = json.loads(content) + logger.debug("swagger OpenAPI version '%s'" % swagger["openapi"]) + + for path in swagger["paths"]: + for operation in swagger["paths"][path]: + op = swagger["paths"][path][operation] + + tags = conf.swaggerTags.split(",") if conf.swaggerTags is not None else None + + if ((tags is None or any(tag in op["tags"] for tag in tags)) + and operation == "get"): + + url = None + method = None + data = None + cookie = None + + url = "%s%s" % (swagger["servers"][0]["url"], path) + method = operation.upper() + q = list(filter(lambda p: (p["in"] == "query"), op["parameters"])) + qs = "" + for qp in q: + qs += "&%s=%s" %(qp["name"], qp["example"]) + qs = qs.replace('&', '?', 1) + + if op["parameters"] is not None and len(q) > 0: + url += qs + + logger.debug("swagger url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) + yield (url, method, data, cookie, None) + + else: + logger.info("excluding url '%s', method '%s' as target since there are no parameters to inject" %(url, method)) + + + except json.decoder.JSONDecodeError: + errMsg = "swagger file is not valid JSON" + raise SqlmapSyntaxException(errMsg) + + + content = readCachedFileContent(reqFile) if conf.scope: @@ -5373,6 +5421,9 @@ def _parseBurpLog(content): for target in _parseWebScarabLog(content): yield target + for target in _parseSwagger(content): + yield target + def getSafeExString(ex, encoding=None): """ Safe way how to get the proper exception represtation as a string diff --git a/lib/core/option.py b/lib/core/option.py index 3b6a1ceab43..8240f422528 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -477,6 +477,31 @@ def _setBulkMultipleTargets(): warnMsg = "no usable links found (with GET parameters)" logger.warn(warnMsg) +def _setSwaggerMultipleTargets(): + if not conf.swaggerFile: + return + + infoMsg = "parsing multiple targets from swagger '%s'" % conf.swaggerFile + logger.info(infoMsg) + + if not os.path.exists(conf.swaggerFile): + errMsg = "the specified list of targets does not exist" + raise SqlmapFilePathException(errMsg) + + if checkFile(conf.swaggerFile, False): + debugMsg = "swagger file '%s' checks out" % conf.swaggerFile + logger.debug(debugMsg) + + for target in parseRequestFile(conf.swaggerFile): + kb.targets.add(target) + + else: + errMsg = "the specified list of targets is not a file " + errMsg += "nor a directory" + raise SqlmapFilePathException(errMsg) + + + def _findPageForms(): if not conf.forms or conf.crawlDepth: return @@ -2677,7 +2702,7 @@ def _basicOptionValidation(): errMsg = "maximum number of used threads is %d avoiding potential connection issues" % MAX_NUMBER_OF_THREADS raise SqlmapSyntaxException(errMsg) - if conf.forms and not any((conf.url, conf.googleDork, conf.bulkFile)): + if conf.forms and not any((conf.url, conf.googleDork, conf.bulkFile, conf.swaggerFile)): errMsg = "switch '--forms' requires usage of option '-u' ('--url'), '-g' or '-m'" raise SqlmapSyntaxException(errMsg) @@ -2787,7 +2812,7 @@ def _basicOptionValidation(): errMsg = "value for option '--union-char' must be an alpha-numeric value (e.g. 1)" raise SqlmapSyntaxException(errMsg) - if conf.hashFile and any((conf.direct, conf.url, conf.logFile, conf.bulkFile, conf.googleDork, conf.configFile, conf.requestFile, conf.updateAll, conf.smokeTest, conf.wizard, conf.dependencies, conf.purge, conf.listTampers)): + if conf.hashFile and any((conf.direct, conf.url, conf.logFile, conf.bulkFile, conf.swaggerFile, conf.googleDork, conf.configFile, conf.requestFile, conf.updateAll, conf.smokeTest, conf.wizard, conf.dependencies, conf.purge, conf.listTampers)): errMsg = "option '--crack' should be used as a standalone" raise SqlmapSyntaxException(errMsg) @@ -2855,7 +2880,7 @@ def init(): parseTargetDirect() - if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)): + if any((conf.url, conf.logFile, conf.bulkFile, conf.swaggerFile, conf.requestFile, conf.googleDork, conf.stdinPipe)): _setHostname() _setHTTPTimeout() _setHTTPExtraHeaders() @@ -2871,6 +2896,7 @@ def init(): _doSearch() _setStdinPipeTargets() _setBulkMultipleTargets() + _setSwaggerMultipleTargets() _checkTor() _setCrawler() _findPageForms() diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index c22b9d11ee0..fada113e764 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -18,6 +18,8 @@ "requestFile": "string", "sessionFile": "string", "googleDork": "string", + "swaggerFile": "string", + "swaggerTags": "string", "configFile": "string", }, diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 5dacb84b5f2..36057fef299 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -141,6 +141,12 @@ def cmdLineParser(argv=None): target.add_argument("-g", dest="googleDork", help="Process Google dork results as target URLs") + target.add_argument("--swaggerFile", dest="swaggerFile", + help="Parse target(s) from a Swagger OpenAPI 3.x.x JSON file ") + + target.add_argument("--swaggerTags", dest="swaggerTags", + help="Only process swagger operations that include one of these tags") + target.add_argument("-c", dest="configFile", help="Load options from a configuration INI file") diff --git a/lib/parse/configfile.py b/lib/parse/configfile.py index a353ce8e0bb..b2f0fe64ec5 100644 --- a/lib/parse/configfile.py +++ b/lib/parse/configfile.py @@ -79,14 +79,14 @@ def configFileParser(configFile): mandatory = False - for option in ("direct", "url", "logFile", "bulkFile", "googleDork", "requestFile", "wizard"): + for option in ("direct", "url", "logFile", "bulkFile", "googleDork", "requestFile", "wizard", "swaggerFile"): if config.has_option("Target", option) and config.get("Target", option) or cmdLineOptions.get(option): mandatory = True break if not mandatory: errMsg = "missing a mandatory option in the configuration file " - errMsg += "(direct, url, logFile, bulkFile, googleDork, requestFile or wizard)" + errMsg += "(direct, url, logFile, bulkFile, googleDork, requestFile, wizard or swaggerFile)" raise SqlmapMissingMandatoryOptionException(errMsg) for family, optionData in optDict.items(): diff --git a/sqlmap.conf b/sqlmap.conf index a771a4e799b..e52e212fed5 100644 --- a/sqlmap.conf +++ b/sqlmap.conf @@ -32,6 +32,12 @@ requestFile = # Example: +ext:php +inurl:"&id=" +intext:"powered by " googleDork = +# Parse target(s) for a Swagger OpenAPI 3.x.x JSON file +swaggerFile = + +# Only process swagger operations that have one of these tags (e.g. tagA,tagB) +swaggerTags = + # These options can be used to specify how to connect to the target URL. [Request] From caaf7c491fc4aa11905f097ddc8205e38ef3f484 Mon Sep 17 00:00:00 2001 From: David Donn Date: Wed, 15 Sep 2021 11:01:20 +1000 Subject: [PATCH 02/12] path injections --- lib/core/common.py | 50 +++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/core/common.py b/lib/core/common.py index 55d8e1b44f4..aadb20483db 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -5363,6 +5363,29 @@ def _parseBurpLog(content): if not(conf.scope and not re.search(conf.scope, url, re.I)): yield (url, conf.method or method, data, cookie, tuple(headers)) + def _swaggerOperationParameters(parameters, types): + return list(filter(lambda p: (p["in"] in types), parameters)) + + def _swaggerOperationQueryString(parameters): + queryParameters = _swaggerOperationParameters(parameters, ["query"]) + if len(queryParameters) < 1: + return None + queryString = "" + for qp in queryParameters: + queryString += "&%s=%s" %(qp["name"], qp["example"]) + + return queryString.replace('&', '', 1) + + def _swaggerOperationPath(path, parameters): + pathParameters = _swaggerOperationParameters(parameters, ["path"]) + if len(pathParameters) < 1: + return path + parameterPath = path + for p in pathParameters: + parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) + return parameterPath + + def _parseSwagger(content): """ Parses Swagger OpenAPI 3.x.x JSON documents @@ -5381,27 +5404,26 @@ def _parseSwagger(content): if ((tags is None or any(tag in op["tags"] for tag in tags)) and operation == "get"): - url = None - method = None - data = None - cookie = None + # header injection is not currently supported + if len(_swaggerOperationParameters(op["parameters"], ["query", "path"])) > 0: + url = None + method = None + data = None + cookie = None - url = "%s%s" % (swagger["servers"][0]["url"], path) - method = operation.upper() - q = list(filter(lambda p: (p["in"] == "query"), op["parameters"])) - qs = "" - for qp in q: - qs += "&%s=%s" %(qp["name"], qp["example"]) - qs = qs.replace('&', '?', 1) + parameterPath = _swaggerOperationPath(path, op["parameters"]) + qs = _swaggerOperationQueryString(op["parameters"]) + url = "%s%s" % (swagger["servers"][0]["url"], parameterPath) + method = operation.upper() - if op["parameters"] is not None and len(q) > 0: - url += qs + if qs is not None: + url += "?" + qs logger.debug("swagger url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) yield (url, method, data, cookie, None) else: - logger.info("excluding url '%s', method '%s' as target since there are no parameters to inject" %(url, method)) + logger.info("excluding path '%s', operation '%s' as there are no parameters to inject" %(path, operation)) except json.decoder.JSONDecodeError: From f6201dae42ab5698c973f7eb3137f2e182e92cf4 Mon Sep 17 00:00:00 2001 From: David Donn Date: Wed, 15 Sep 2021 11:31:56 +1000 Subject: [PATCH 03/12] extra swagger validations --- lib/core/common.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/core/common.py b/lib/core/common.py index aadb20483db..bdc9483acd9 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -5393,7 +5393,22 @@ def _parseSwagger(content): try: swagger = json.loads(content) - logger.debug("swagger OpenAPI version '%s'" % swagger["openapi"]) + + # extra validations + if "openapi" not in swagger or not swagger["openapi"].startswith("3."): + errMsg = "swagger must be OpenAPI 3.x.x!" + raise SqlmapSyntaxException(errMsg) + + if ("servers" not in swagger or + not isinstance(swagger["servers"], list) or + len(swagger["servers"]) < 1 or + "url" not in swagger["servers"][0]): + errMsg = "swagger server is missing!" + raise SqlmapSyntaxException(errMsg) + + server = swagger["servers"][0]["url"] + + logger.info("swagger OpenAPI version '%s', server '%s'" %(swagger["openapi"], server)) for path in swagger["paths"]: for operation in swagger["paths"][path]: @@ -5413,7 +5428,7 @@ def _parseSwagger(content): parameterPath = _swaggerOperationPath(path, op["parameters"]) qs = _swaggerOperationQueryString(op["parameters"]) - url = "%s%s" % (swagger["servers"][0]["url"], parameterPath) + url = "%s%s" % (server, parameterPath) method = operation.upper() if qs is not None: From 4aeed819f712f4de151fb4ac32df00c70c2581f4 Mon Sep 17 00:00:00 2001 From: David Donn Date: Fri, 17 Sep 2021 11:36:45 +1000 Subject: [PATCH 04/12] support operations that have a request body --- lib/core/common.py | 44 ++++++++++++++++++++++++++++++++++++++------ lib/core/option.py | 5 ++++- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/core/common.py b/lib/core/common.py index bdc9483acd9..242b7044508 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -5385,6 +5385,31 @@ def _swaggerOperationPath(path, parameters): parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) return parameterPath + def _swaggerRef(swagger, refPath): + paths = refPath.replace("#/", "", 1).split('/') + r = swagger + for p in paths: + r = r[p] + return r + + def _swaggerBody(swagger, refPath): + body = {} + ref = _swaggerRef(swagger, refPath) + if "type" in ref and ref["type"] == "object" and "properties" in ref: + properties = ref["properties"] + for prop in properties: + if "example" in properties[prop]: + value = properties[prop]["example"] + #if properties[prop]["type"] in ["string", "enum"] and value[0] != '"': + # value = "\"%s\"" %value + body[prop] = value + elif "$ref" in properties[prop]: + body[prop] = _swaggerBody(swagger, properties[prop]["$ref"]) + elif properties[prop]["type"] == "array" and "$ref" in properties[prop]["items"]: + body[prop] = [ _swaggerBody(swagger, properties[prop]["items"]["$ref"]) ] + + return body + def _parseSwagger(content): """ @@ -5414,13 +5439,17 @@ def _parseSwagger(content): for operation in swagger["paths"][path]: op = swagger["paths"][path][operation] - tags = conf.swaggerTags.split(",") if conf.swaggerTags is not None else None + tags = conf.swaggerTags + + if tags is None or any(tag in op["tags"] for tag in tags): - if ((tags is None or any(tag in op["tags"] for tag in tags)) - and operation == "get"): + body = {} + if "requestBody" in op: + ref = op["requestBody"]["content"]["application/json"]["schema"]["$ref"] + body = _swaggerBody(swagger, ref) # header injection is not currently supported - if len(_swaggerOperationParameters(op["parameters"], ["query", "path"])) > 0: + if (len(_swaggerOperationParameters(op["parameters"], ["query", "path"]))) > 0 or body: url = None method = None data = None @@ -5430,6 +5459,8 @@ def _parseSwagger(content): qs = _swaggerOperationQueryString(op["parameters"]) url = "%s%s" % (server, parameterPath) method = operation.upper() + if body: + data = json.dumps(body) if qs is not None: url += "?" + qs @@ -5458,8 +5489,9 @@ def _parseSwagger(content): for target in _parseWebScarabLog(content): yield target - for target in _parseSwagger(content): - yield target + if conf.swaggerFile: + for target in _parseSwagger(content): + yield target def getSafeExString(ex, encoding=None): """ diff --git a/lib/core/option.py b/lib/core/option.py index 8240f422528..9ad636fa572 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1793,7 +1793,7 @@ def _cleanupOptions(): if conf.tmpPath: conf.tmpPath = ntToPosixSlashes(normalizePath(conf.tmpPath)) - if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe)): + if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe, conf.swaggerFile)): conf.multipleTargets = True if conf.optimize: @@ -1940,6 +1940,9 @@ class _(six.text_type): if conf.dummy: conf.batch = True + if conf.swaggerTags: + conf.swaggerTags = [_.strip() for _ in re.split(PARAMETER_SPLITTING_REGEX, conf.swaggerTags)] + threadData = getCurrentThreadData() threadData.reset() From 8f13e35f3ff798982f540634a7f011209e032d1e Mon Sep 17 00:00:00 2001 From: donnd-t Date: Fri, 17 Sep 2021 15:06:20 +1000 Subject: [PATCH 05/12] refactor swagger code to separate module --- lib/core/common.py | 119 +------------------------------------------ lib/core/swagger.py | 120 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 117 deletions(-) create mode 100644 lib/core/swagger.py diff --git a/lib/core/common.py b/lib/core/common.py index 242b7044508..0f918fc0311 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -36,7 +36,6 @@ import time import types import unicodedata -import json from difflib import SequenceMatcher from math import sqrt @@ -184,6 +183,7 @@ from lib.core.settings import VERSION_STRING from lib.core.settings import ZIP_HEADER from lib.core.settings import WEBSCARAB_SPLITTER +from lib.core.swagger import parse as _parseSwagger from lib.core.threads import getCurrentThreadData from lib.utils.safe2bin import safecharencode from lib.utils.sqlalchemy import _sqlalchemy @@ -5363,121 +5363,6 @@ def _parseBurpLog(content): if not(conf.scope and not re.search(conf.scope, url, re.I)): yield (url, conf.method or method, data, cookie, tuple(headers)) - def _swaggerOperationParameters(parameters, types): - return list(filter(lambda p: (p["in"] in types), parameters)) - - def _swaggerOperationQueryString(parameters): - queryParameters = _swaggerOperationParameters(parameters, ["query"]) - if len(queryParameters) < 1: - return None - queryString = "" - for qp in queryParameters: - queryString += "&%s=%s" %(qp["name"], qp["example"]) - - return queryString.replace('&', '', 1) - - def _swaggerOperationPath(path, parameters): - pathParameters = _swaggerOperationParameters(parameters, ["path"]) - if len(pathParameters) < 1: - return path - parameterPath = path - for p in pathParameters: - parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) - return parameterPath - - def _swaggerRef(swagger, refPath): - paths = refPath.replace("#/", "", 1).split('/') - r = swagger - for p in paths: - r = r[p] - return r - - def _swaggerBody(swagger, refPath): - body = {} - ref = _swaggerRef(swagger, refPath) - if "type" in ref and ref["type"] == "object" and "properties" in ref: - properties = ref["properties"] - for prop in properties: - if "example" in properties[prop]: - value = properties[prop]["example"] - #if properties[prop]["type"] in ["string", "enum"] and value[0] != '"': - # value = "\"%s\"" %value - body[prop] = value - elif "$ref" in properties[prop]: - body[prop] = _swaggerBody(swagger, properties[prop]["$ref"]) - elif properties[prop]["type"] == "array" and "$ref" in properties[prop]["items"]: - body[prop] = [ _swaggerBody(swagger, properties[prop]["items"]["$ref"]) ] - - return body - - - def _parseSwagger(content): - """ - Parses Swagger OpenAPI 3.x.x JSON documents - """ - - try: - swagger = json.loads(content) - - # extra validations - if "openapi" not in swagger or not swagger["openapi"].startswith("3."): - errMsg = "swagger must be OpenAPI 3.x.x!" - raise SqlmapSyntaxException(errMsg) - - if ("servers" not in swagger or - not isinstance(swagger["servers"], list) or - len(swagger["servers"]) < 1 or - "url" not in swagger["servers"][0]): - errMsg = "swagger server is missing!" - raise SqlmapSyntaxException(errMsg) - - server = swagger["servers"][0]["url"] - - logger.info("swagger OpenAPI version '%s', server '%s'" %(swagger["openapi"], server)) - - for path in swagger["paths"]: - for operation in swagger["paths"][path]: - op = swagger["paths"][path][operation] - - tags = conf.swaggerTags - - if tags is None or any(tag in op["tags"] for tag in tags): - - body = {} - if "requestBody" in op: - ref = op["requestBody"]["content"]["application/json"]["schema"]["$ref"] - body = _swaggerBody(swagger, ref) - - # header injection is not currently supported - if (len(_swaggerOperationParameters(op["parameters"], ["query", "path"]))) > 0 or body: - url = None - method = None - data = None - cookie = None - - parameterPath = _swaggerOperationPath(path, op["parameters"]) - qs = _swaggerOperationQueryString(op["parameters"]) - url = "%s%s" % (server, parameterPath) - method = operation.upper() - if body: - data = json.dumps(body) - - if qs is not None: - url += "?" + qs - - logger.debug("swagger url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) - yield (url, method, data, cookie, None) - - else: - logger.info("excluding path '%s', operation '%s' as there are no parameters to inject" %(path, operation)) - - - except json.decoder.JSONDecodeError: - errMsg = "swagger file is not valid JSON" - raise SqlmapSyntaxException(errMsg) - - - content = readCachedFileContent(reqFile) if conf.scope: @@ -5490,7 +5375,7 @@ def _parseSwagger(content): yield target if conf.swaggerFile: - for target in _parseSwagger(content): + for target in _parseSwagger(content, conf.swaggerTags): yield target def getSafeExString(ex, encoding=None): diff --git a/lib/core/swagger.py b/lib/core/swagger.py new file mode 100644 index 00000000000..1697aecbddf --- /dev/null +++ b/lib/core/swagger.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2021 sqlmap developers (https://sqlmap.org/) +See the file 'LICENSE' for copying permission +""" + +import json + +from lib.core.data import logger +from lib.core.exception import SqlmapSyntaxException + +def _operationParameters(parameters, types): + return list(filter(lambda p: (p["in"] in types), parameters)) + +def _operationQueryString(parameters): + queryParameters = _operationParameters(parameters, ["query"]) + if len(queryParameters) < 1: + return None + queryString = "" + for qp in queryParameters: + queryString += "&%s=%s" %(qp["name"], qp["example"]) + + return queryString.replace('&', '', 1) + +def _operationPath(path, parameters): + pathParameters = _operationParameters(parameters, ["path"]) + if len(pathParameters) < 1: + return path + parameterPath = path + for p in pathParameters: + parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) + return parameterPath + +def _ref(swagger, refPath): + paths = refPath.replace("#/", "", 1).split('/') + r = swagger + for p in paths: + r = r[p] + return r + +def _body(swagger, refPath): + body = {} + ref = _ref(swagger, refPath) + if "type" in ref and ref["type"] == "object" and "properties" in ref: + properties = ref["properties"] + for prop in properties: + if "example" in properties[prop]: + value = properties[prop]["example"] + body[prop] = value + elif "$ref" in properties[prop]: + body[prop] = _body(swagger, properties[prop]["$ref"]) + elif properties[prop]["type"] == "array" and "$ref" in properties[prop]["items"]: + body[prop] = [ _body(swagger, properties[prop]["items"]["$ref"]) ] + + return body + +def parse(content, tags): + """ + Parses Swagger OpenAPI 3.x.x JSON documents + """ + + try: + swagger = json.loads(content) + + # extra validations + if "openapi" not in swagger or not swagger["openapi"].startswith("3."): + errMsg = "swagger must be OpenAPI 3.x.x!" + raise SqlmapSyntaxException(errMsg) + + if ("servers" not in swagger or + not isinstance(swagger["servers"], list) or + len(swagger["servers"]) < 1 or + "url" not in swagger["servers"][0]): + errMsg = "swagger server is missing!" + raise SqlmapSyntaxException(errMsg) + + server = swagger["servers"][0]["url"] + + logger.info("swagger OpenAPI version '%s', server '%s'" %(swagger["openapi"], server)) + + for path in swagger["paths"]: + for operation in swagger["paths"][path]: + op = swagger["paths"][path][operation] + + # skip any operations without one of our tags + if tags is not None and not any(tag in op["tags"] for tag in tags): + continue + + body = {} + if "requestBody" in op: + ref = op["requestBody"]["content"]["application/json"]["schema"]["$ref"] + body = _body(swagger, ref) + + # header injection is not currently supported + if (len(_operationParameters(op["parameters"], ["query", "path"]))) < 1 and not body: + logger.info("excluding path '%s', operation '%s' as there are no parameters to inject" %(path, operation)) + continue + + url = None + method = None + data = None + cookie = None + + parameterPath = _operationPath(path, op["parameters"]) + qs = _operationQueryString(op["parameters"]) + url = "%s%s" % (server, parameterPath) + method = operation.upper() + if body: + data = json.dumps(body) + + if qs is not None: + url += "?" + qs + + logger.debug("swagger url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) + yield (url, method, data, cookie, None) + + except json.decoder.JSONDecodeError: + errMsg = "swagger file is not valid JSON" + raise SqlmapSyntaxException(errMsg) From 59e8bb94f2dffd3d668db98bced4c64229092e82 Mon Sep 17 00:00:00 2001 From: donnd-t Date: Fri, 17 Sep 2021 15:49:06 +1000 Subject: [PATCH 06/12] refactor --- lib/core/swagger.py | 87 +++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/lib/core/swagger.py b/lib/core/swagger.py index 1697aecbddf..ae9b71779bc 100644 --- a/lib/core/swagger.py +++ b/lib/core/swagger.py @@ -10,27 +10,47 @@ from lib.core.data import logger from lib.core.exception import SqlmapSyntaxException -def _operationParameters(parameters, types): - return list(filter(lambda p: (p["in"] in types), parameters)) +class Operation: -def _operationQueryString(parameters): - queryParameters = _operationParameters(parameters, ["query"]) - if len(queryParameters) < 1: - return None - queryString = "" - for qp in queryParameters: - queryString += "&%s=%s" %(qp["name"], qp["example"]) + def __init__(self, op): + self.op = op + + def tags(self): + return self.op["tags"] + + def parameters(self): + return self.op["parameters"] - return queryString.replace('&', '', 1) + def parametersForTypes(self, types): + return list(filter(lambda p: (p["in"] in types), self.parameters())) + + def bodyRef(self): + if "requestBody" in self.op: + return self.op["requestBody"]["content"]["application/json"]["schema"]["$ref"] + return None -def _operationPath(path, parameters): - pathParameters = _operationParameters(parameters, ["path"]) - if len(pathParameters) < 1: - return path - parameterPath = path - for p in pathParameters: - parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) - return parameterPath + # header injection is not currently supported + def injectable(self, body): + return len(self.parametersForTypes(["query", "path"])) > 0 or body + + def queryString(self): + queryParameters = self.parametersForTypes(["query"]) + if len(queryParameters) < 1: + return None + queryString = "" + for qp in queryParameters: + queryString += "&%s=%s" %(qp["name"], qp["example"]) + + return queryString.replace('&', '', 1) + + def path(self, path): + pathParameters = self.parametersForTypes(["path"]) + if len(pathParameters) < 1: + return path + parameterPath = path + for p in pathParameters: + parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) + return parameterPath def _ref(swagger, refPath): paths = refPath.replace("#/", "", 1).split('/') @@ -39,21 +59,21 @@ def _ref(swagger, refPath): r = r[p] return r -def _body(swagger, refPath): - body = {} +def _example(swagger, refPath): + example = {} ref = _ref(swagger, refPath) if "type" in ref and ref["type"] == "object" and "properties" in ref: properties = ref["properties"] for prop in properties: if "example" in properties[prop]: value = properties[prop]["example"] - body[prop] = value + example[prop] = value elif "$ref" in properties[prop]: - body[prop] = _body(swagger, properties[prop]["$ref"]) + example[prop] = _example(swagger, properties[prop]["$ref"]) elif properties[prop]["type"] == "array" and "$ref" in properties[prop]["items"]: - body[prop] = [ _body(swagger, properties[prop]["items"]["$ref"]) ] + example[prop] = [ _example(swagger, properties[prop]["items"]["$ref"]) ] - return body + return example def parse(content, tags): """ @@ -81,19 +101,18 @@ def parse(content, tags): for path in swagger["paths"]: for operation in swagger["paths"][path]: - op = swagger["paths"][path][operation] + op = Operation(swagger["paths"][path][operation]) # skip any operations without one of our tags - if tags is not None and not any(tag in op["tags"] for tag in tags): + if tags is not None and not any(tag in op.tags() for tag in tags): continue body = {} - if "requestBody" in op: - ref = op["requestBody"]["content"]["application/json"]["schema"]["$ref"] - body = _body(swagger, ref) + bodyRef = op.bodyRef() + if bodyRef: + body = _example(swagger, bodyRef) - # header injection is not currently supported - if (len(_operationParameters(op["parameters"], ["query", "path"]))) < 1 and not body: + if not op.injectable(body): logger.info("excluding path '%s', operation '%s' as there are no parameters to inject" %(path, operation)) continue @@ -102,8 +121,8 @@ def parse(content, tags): data = None cookie = None - parameterPath = _operationPath(path, op["parameters"]) - qs = _operationQueryString(op["parameters"]) + parameterPath = op.path(path) + qs = op.queryString() url = "%s%s" % (server, parameterPath) method = operation.upper() if body: @@ -112,7 +131,7 @@ def parse(content, tags): if qs is not None: url += "?" + qs - logger.debug("swagger url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) + logger.debug("including url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) yield (url, method, data, cookie, None) except json.decoder.JSONDecodeError: From f031e00970b39f28739b84f46a3afd7602118358 Mon Sep 17 00:00:00 2001 From: donnd-t Date: Fri, 17 Sep 2021 18:44:18 +1000 Subject: [PATCH 07/12] show warning when example is missing --- lib/core/swagger.py | 80 ++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/lib/core/swagger.py b/lib/core/swagger.py index ae9b71779bc..e573c2ea947 100644 --- a/lib/core/swagger.py +++ b/lib/core/swagger.py @@ -9,24 +9,27 @@ from lib.core.data import logger from lib.core.exception import SqlmapSyntaxException +from lib.core.exception import SqlmapSkipTargetException class Operation: - def __init__(self, op): - self.op = op + def __init__(self, name, method, props): + self.name = name + self.method = method + self.props = props def tags(self): - return self.op["tags"] + return self.props["tags"] def parameters(self): - return self.op["parameters"] + return self.props["parameters"] def parametersForTypes(self, types): return list(filter(lambda p: (p["in"] in types), self.parameters())) def bodyRef(self): - if "requestBody" in self.op: - return self.op["requestBody"]["content"]["application/json"]["schema"]["$ref"] + if "requestBody" in self.props: + return self.props["requestBody"]["content"]["application/json"]["schema"]["$ref"] return None # header injection is not currently supported @@ -39,6 +42,8 @@ def queryString(self): return None queryString = "" for qp in queryParameters: + if "example" not in qp: + raise SqlmapSkipTargetException("missing example for parameter '%s'" %qp["name"]) queryString += "&%s=%s" %(qp["name"], qp["example"]) return queryString.replace('&', '', 1) @@ -49,6 +54,8 @@ def path(self, path): return path parameterPath = path for p in pathParameters: + if "example" not in p: + raise SqlmapSkipTargetException("missing example for parameter '%s'" %p["name"]) parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) return parameterPath @@ -72,6 +79,9 @@ def _example(swagger, refPath): example[prop] = _example(swagger, properties[prop]["$ref"]) elif properties[prop]["type"] == "array" and "$ref" in properties[prop]["items"]: example[prop] = [ _example(swagger, properties[prop]["items"]["$ref"]) ] + else: + raise SqlmapSkipTargetException("missing example for parameter '%s'" %prop) + return example @@ -100,39 +110,41 @@ def parse(content, tags): logger.info("swagger OpenAPI version '%s', server '%s'" %(swagger["openapi"], server)) for path in swagger["paths"]: - for operation in swagger["paths"][path]: - op = Operation(swagger["paths"][path][operation]) + for method in swagger["paths"][path]: + op = Operation(path, method, swagger["paths"][path][method]) + method = method.upper() # skip any operations without one of our tags if tags is not None and not any(tag in op.tags() for tag in tags): continue - body = {} - bodyRef = op.bodyRef() - if bodyRef: - body = _example(swagger, bodyRef) - - if not op.injectable(body): - logger.info("excluding path '%s', operation '%s' as there are no parameters to inject" %(path, operation)) - continue - - url = None - method = None - data = None - cookie = None - - parameterPath = op.path(path) - qs = op.queryString() - url = "%s%s" % (server, parameterPath) - method = operation.upper() - if body: - data = json.dumps(body) - - if qs is not None: - url += "?" + qs - - logger.debug("including url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) - yield (url, method, data, cookie, None) + try: + body = {} + bodyRef = op.bodyRef() + if bodyRef: + body = _example(swagger, bodyRef) + + if op.injectable(body): + url = None + data = None + cookie = None + + parameterPath = op.path(path) + qs = op.queryString() + url = "%s%s" % (server, parameterPath) + if body: + data = json.dumps(body) + + if qs is not None: + url += "?" + qs + + logger.debug("including url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) + yield (url, method, data, cookie, None) + else: + logger.info("excluding path '%s', method '%s' as there are no parameters to inject" %(path, method)) + + except SqlmapSkipTargetException as e: + logger.warn("excluding path '%s', method '%s': %s" %(path, method, e)) except json.decoder.JSONDecodeError: errMsg = "swagger file is not valid JSON" From 65187decac6aaf2d246be678b3ca8b9e238fb3d9 Mon Sep 17 00:00:00 2001 From: donnd-t Date: Fri, 17 Sep 2021 18:48:56 +1000 Subject: [PATCH 08/12] show warning when example is missing --- lib/core/swagger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/core/swagger.py b/lib/core/swagger.py index e573c2ea947..ec8013345da 100644 --- a/lib/core/swagger.py +++ b/lib/core/swagger.py @@ -88,6 +88,9 @@ def _example(swagger, refPath): def parse(content, tags): """ Parses Swagger OpenAPI 3.x.x JSON documents + + Target injectable parameter values are generated from the "example" properties. + Only property-level "example" is supported. The "examples" property is not supported. """ try: From 1141f21dee24673286bed7dcb030a1b0e90a6f7c Mon Sep 17 00:00:00 2001 From: donnd-t Date: Tue, 2 Nov 2021 16:54:38 +1100 Subject: [PATCH 09/12] added support for headers --- lib/core/swagger.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/core/swagger.py b/lib/core/swagger.py index ec8013345da..65ee8708448 100644 --- a/lib/core/swagger.py +++ b/lib/core/swagger.py @@ -34,7 +34,7 @@ def bodyRef(self): # header injection is not currently supported def injectable(self, body): - return len(self.parametersForTypes(["query", "path"])) > 0 or body + return len(self.parametersForTypes(["query", "path", "header"])) > 0 or body def queryString(self): queryParameters = self.parametersForTypes(["query"]) @@ -59,6 +59,17 @@ def path(self, path): parameterPath = parameterPath.replace("{%s}" %p["name"], "%s*" %p["example"]) return parameterPath + def headers(self): + hdrs = [] + headerParameters = self.parametersForTypes(["header"]) + if len(headerParameters) < 1: + return hdrs + for hp in headerParameters: + if "example" not in hp: + raise SqlmapSkipTargetException("missing example for header '%s'" %hp["name"]) + hdrs.append((hp["name"], "%s*" %hp["example"])) + return hdrs + def _ref(swagger, refPath): paths = refPath.replace("#/", "", 1).split('/') r = swagger @@ -133,6 +144,7 @@ def parse(content, tags): cookie = None parameterPath = op.path(path) + headers = op.headers() qs = op.queryString() url = "%s%s" % (server, parameterPath) if body: @@ -142,7 +154,7 @@ def parse(content, tags): url += "?" + qs logger.debug("including url '%s', method '%s', data '%s', cookie '%s'" %(url, method, data, cookie)) - yield (url, method, data, cookie, None) + yield (url, method, data, cookie, tuple(headers)) else: logger.info("excluding path '%s', method '%s' as there are no parameters to inject" %(path, method)) From 5ab5f5811f2d1ae8c87fb1a82bed891211ee6a3f Mon Sep 17 00:00:00 2001 From: donnd-t Date: Wed, 3 Nov 2021 13:03:32 +1100 Subject: [PATCH 10/12] support for nested payloads --- lib/core/swagger.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/core/swagger.py b/lib/core/swagger.py index 65ee8708448..11b107415da 100644 --- a/lib/core/swagger.py +++ b/lib/core/swagger.py @@ -10,6 +10,7 @@ from lib.core.data import logger from lib.core.exception import SqlmapSyntaxException from lib.core.exception import SqlmapSkipTargetException +from typing import Dict class Operation: @@ -70,28 +71,39 @@ def headers(self): hdrs.append((hp["name"], "%s*" %hp["example"])) return hdrs -def _ref(swagger, refPath): - paths = refPath.replace("#/", "", 1).split('/') +def _obj(swagger, objOrRefPath): + if isinstance(objOrRefPath, Dict): + return objOrRefPath + paths = objOrRefPath.replace("#/", "", 1).split('/') r = swagger for p in paths: r = r[p] return r -def _example(swagger, refPath): +def _example(swagger, objOrRefPath): example = {} - ref = _ref(swagger, refPath) - if "type" in ref and ref["type"] == "object" and "properties" in ref: - properties = ref["properties"] + obj = _obj(swagger, objOrRefPath) + + if "type" in obj and obj["type"] == "object" and "properties" in obj: + properties = obj["properties"] for prop in properties: - if "example" in properties[prop]: - value = properties[prop]["example"] - example[prop] = value + if properties[prop]["type"] == "object": + example[prop] = {} + for objectProp in properties[prop]["properties"]: + example[prop][objectProp] = _example(swagger, properties[prop]["properties"][objectProp]) elif "$ref" in properties[prop]: example[prop] = _example(swagger, properties[prop]["$ref"]) elif properties[prop]["type"] == "array" and "$ref" in properties[prop]["items"]: example[prop] = [ _example(swagger, properties[prop]["items"]["$ref"]) ] + elif "example" in properties[prop]: + value = properties[prop]["example"] + example[prop] = value else: raise SqlmapSkipTargetException("missing example for parameter '%s'" %prop) + elif "example" in obj: + return obj["example"] + else: + raise SqlmapSkipTargetException("missing example for object '%s'" %obj) return example From 35cb10fd69b3dc644899d009f308e26f7b747564 Mon Sep 17 00:00:00 2001 From: donnd-t Date: Fri, 12 Nov 2021 13:44:35 +1100 Subject: [PATCH 11/12] support for swagger 2.0 metadata --- lib/core/swagger.py | 49 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/core/swagger.py b/lib/core/swagger.py index 11b107415da..3f0f2e5873a 100644 --- a/lib/core/swagger.py +++ b/lib/core/swagger.py @@ -110,7 +110,7 @@ def _example(swagger, objOrRefPath): def parse(content, tags): """ - Parses Swagger OpenAPI 3.x.x JSON documents + Parses Swagger 2.x and OpenAPI 3.x.x JSON documents Target injectable parameter values are generated from the "example" properties. Only property-level "example" is supported. The "examples" property is not supported. @@ -119,21 +119,54 @@ def parse(content, tags): try: swagger = json.loads(content) + openapiv3 = False + swaggerv2 = False + # extra validations - if "openapi" not in swagger or not swagger["openapi"].startswith("3."): - errMsg = "swagger must be OpenAPI 3.x.x!" - raise SqlmapSyntaxException(errMsg) + if "openapi" in swagger and swagger["openapi"].startswith("3."): + openapiv3 = True + + if "swagger" in swagger and swagger["swagger"].startswith("2."): + swaggerv2 = True - if ("servers" not in swagger or + if not (openapiv3 or swaggerv2): + errMsg = "swagger must be either Swagger 2.x or OpenAPI 3.x.x!" + raise SqlmapSyntaxException(errMsg) + + if (openapiv3 and + ("servers" not in swagger or not isinstance(swagger["servers"], list) or len(swagger["servers"]) < 1 or - "url" not in swagger["servers"][0]): + "url" not in swagger["servers"][0])): + errMsg = "swagger server is missing!" + raise SqlmapSyntaxException(errMsg) + + if swaggerv2 and "host" not in swagger: errMsg = "swagger server is missing!" raise SqlmapSyntaxException(errMsg) - server = swagger["servers"][0]["url"] + if openapiv3: + # only one server supported + server = swagger["servers"][0]["url"] + + logger.info("swagger OpenAPI version '%s', server '%s'" %(swagger["openapi"], server)) + elif swaggerv2: + logger.info("swagger version '%s'" %swagger["swagger"]) + + basePath = "" + if "basePath" in swagger: + basePath = swagger["basePath"] + + scheme = "https" + if ("schemes" in swagger and + isinstance(swagger["schemes"], list) and + len(swagger["schemes"]) > 0): + scheme = swagger["schemes"][0] + + server = "%s://%s%s" % (scheme, swagger["host"], basePath) + + logger.info("swagger version '%s', server '%s'" %(swagger["swagger"], server)) - logger.info("swagger OpenAPI version '%s', server '%s'" %(swagger["openapi"], server)) for path in swagger["paths"]: for method in swagger["paths"][path]: From 051eb12a8d77e814d6eabd59a005feed22966e24 Mon Sep 17 00:00:00 2001 From: donnd-t Date: Fri, 12 Nov 2021 14:29:08 +1100 Subject: [PATCH 12/12] support for swagger v2 body specs --- lib/core/swagger.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/core/swagger.py b/lib/core/swagger.py index 3f0f2e5873a..700df9b89e4 100644 --- a/lib/core/swagger.py +++ b/lib/core/swagger.py @@ -29,8 +29,15 @@ def parametersForTypes(self, types): return list(filter(lambda p: (p["in"] in types), self.parameters())) def bodyRef(self): + # OpenAPI v3 if "requestBody" in self.props: return self.props["requestBody"]["content"]["application/json"]["schema"]["$ref"] + # swagger v2 + elif "parameters" in self.props: + inParameters = self.parametersForTypes(["body"]) + if not isinstance(inParameters, list) or len(inParameters) < 1: + return None + return inParameters[0]["schema"]["$ref"] return None # header injection is not currently supported