diff --git a/lib/core/common.py b/lib/core/common.py index ec82ad7338b..0f918fc0311 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -183,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 @@ -5373,6 +5374,10 @@ def _parseBurpLog(content): for target in _parseWebScarabLog(content): yield target + if conf.swaggerFile: + for target in _parseSwagger(content, conf.swaggerTags): + 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..9ad636fa572 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 @@ -1768,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: @@ -1915,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() @@ -2677,7 +2705,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 +2815,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 +2883,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 +2899,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/core/swagger.py b/lib/core/swagger.py new file mode 100644 index 00000000000..700df9b89e4 --- /dev/null +++ b/lib/core/swagger.py @@ -0,0 +1,218 @@ +#!/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 +from lib.core.exception import SqlmapSkipTargetException +from typing import Dict + +class Operation: + + def __init__(self, name, method, props): + self.name = name + self.method = method + self.props = props + + def tags(self): + return self.props["tags"] + + def parameters(self): + return self.props["parameters"] + + 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 + def injectable(self, body): + return len(self.parametersForTypes(["query", "path", "header"])) > 0 or body + + def queryString(self): + queryParameters = self.parametersForTypes(["query"]) + if len(queryParameters) < 1: + 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) + + def path(self, path): + pathParameters = self.parametersForTypes(["path"]) + if len(pathParameters) < 1: + 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 + + 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 _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, objOrRefPath): + example = {} + obj = _obj(swagger, objOrRefPath) + + if "type" in obj and obj["type"] == "object" and "properties" in obj: + properties = obj["properties"] + for prop in properties: + 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 + +def parse(content, tags): + """ + 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. + """ + + try: + swagger = json.loads(content) + + openapiv3 = False + swaggerv2 = False + + # extra validations + if "openapi" in swagger and swagger["openapi"].startswith("3."): + openapiv3 = True + + if "swagger" in swagger and swagger["swagger"].startswith("2."): + swaggerv2 = True + + 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])): + errMsg = "swagger server is missing!" + raise SqlmapSyntaxException(errMsg) + + if swaggerv2 and "host" not in swagger: + errMsg = "swagger server is missing!" + raise SqlmapSyntaxException(errMsg) + + 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)) + + + for path in swagger["paths"]: + 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 + + 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) + headers = op.headers() + 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, tuple(headers)) + 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" + raise SqlmapSyntaxException(errMsg) 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]