Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use swagger as the source for targets #4833

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions lib/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 33 additions & 4 deletions lib/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand All @@ -2871,6 +2899,7 @@ def init():
_doSearch()
_setStdinPipeTargets()
_setBulkMultipleTargets()
_setSwaggerMultipleTargets()
_checkTor()
_setCrawler()
_findPageForms()
Expand Down
2 changes: 2 additions & 0 deletions lib/core/optiondict.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"requestFile": "string",
"sessionFile": "string",
"googleDork": "string",
"swaggerFile": "string",
"swaggerTags": "string",
"configFile": "string",
},

Expand Down
218 changes: 218 additions & 0 deletions lib/core/swagger.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions lib/parse/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
4 changes: 2 additions & 2 deletions lib/parse/configfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
6 changes: 6 additions & 0 deletions sqlmap.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down