Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions lib/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,14 @@ def getDbms(versions=None):
if isListLike(versions) and UNKNOWN_DBMS_VERSION in versions:
versions = None

return Backend.getDbms() if versions is None else "%s %s" % (Backend.getDbms(), " and ".join(filterNone(versions)))
dbms = Backend.getDbms() or Backend.getIdentifiedDbms()

if versions is None:
return dbms

version = " and ".join(filterNone(versions))

return "%s %s" % (dbms, version) if dbms else version

@staticmethod
def getErrorParsedDBMSes():
Expand Down Expand Up @@ -491,7 +498,12 @@ def getForcedDbms():

@staticmethod
def getDbms():
return aliasToDbmsEnum(kb.get("dbms"))
retVal = aliasToDbmsEnum(kb.get("dbms"))

if retVal is None and conf.get("dbms"):
retVal = aliasToDbmsEnum(conf.get("dbms"))

return retVal

@staticmethod
def getErrorParsedDBMSes():
Expand Down Expand Up @@ -1529,8 +1541,6 @@ def cleanQuery(query):

>>> cleanQuery("select id from users")
'SELECT id FROM users'
>>> cleanQuery("select a from selected where b='from'")
"SELECT a FROM selected WHERE b='from'"
"""

retVal = query
Expand All @@ -1546,11 +1556,10 @@ def cleanQuery(query):
if not candidate or candidate.lower() not in queryLower:
continue

if "sys_exec" not in query:
# NOTE: the leading branch consumes whole quoted parts (hence keeping keyword-alike data
# and case sensitive quoted identifiers intact), while the keyword itself is switched only
# at word boundaries (e.g. 'selected' must not turn into 'SELECTed')
retVal = re.sub(r"(?i)('[^']*'|\"[^\"]*\")|\b%s\b" % candidate, lambda match: match.group(1) or candidate.upper(), retVal)
queryMatch = re.search(r"(?i)\b(%s)\b" % candidate, query)

if queryMatch and "sys_exec" not in query:
retVal = retVal.replace(queryMatch.group(1), candidate.upper())

return retVal

Expand Down
51 changes: 15 additions & 36 deletions lib/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@
from lib.core.settings import PRECONNECT_CANDIDATE_TIMEOUT
from lib.core.settings import PROXY_ENVIRONMENT_VARIABLES
from lib.core.settings import SOCKET_PRE_CONNECT_QUEUE_SIZE
from lib.core.settings import NONSQL_TECHNIQUES
from lib.core.settings import SQLMAP_ENVIRONMENT_PREFIX
from lib.core.settings import SUPPORTED_DBMS
from lib.core.settings import SUPPORTED_OS
Expand Down Expand Up @@ -423,10 +422,7 @@ def retrieve():
conf.googlePage += 1

def _setStdinPipeTargets():
# Note: an explicit target source takes precedence. Without this, any non-interactive run (CI,
# cron, subprocess) would reroute '-m/-l/-r/-g' targets through the STDIN container, losing both
# their count and their order
if any((conf.url, conf.direct, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.openApiFile)):
if conf.url:
return

if isinstance(conf.stdinPipe, _collections.Iterable):
Expand Down Expand Up @@ -830,6 +826,8 @@ def _setDBMS():

break

Backend.setDbms(conf.dbms)

def _listTamperingFunctions():
"""
Lists available tamper functions
Expand Down Expand Up @@ -905,13 +903,6 @@ def _setTamperingFunctions():
priority = PRIORITY.NORMAL if not hasattr(module, "__priority__") else module.__priority__
priority = priority if priority is not None else PRIORITY.LOWEST

if not isinstance(priority, int):
warnMsg = "tamper module '%s' has an invalid value for '__priority__' " % filename[:-3]
warnMsg += "(assuming '%d')" % PRIORITY.NORMAL
logger.warning(warnMsg)

priority = PRIORITY.NORMAL

for name, function in inspect.getmembers(module, inspect.isfunction):
if name == "tamper" and (hasattr(inspect, "signature") and all(_ in inspect.signature(function).parameters for _ in ("payload", "kwargs")) or inspect.getargspec(function).args and inspect.getargspec(function).keywords == "kwargs"):
found = True
Expand Down Expand Up @@ -955,14 +946,11 @@ def _setTamperingFunctions():
warnMsg += "a good idea"
logger.warning(warnMsg)

# tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines do not run
# payloads through the tampering hook, so warn instead of silently ignoring the user's
# '--tamper'. One tuple drives both the test and the name lookup - keeping two lists in step is
# exactly how this raised StopIteration, and leaving an engine OUT (as '--hql' was) is how the
# warning silently stops covering one.
_nonSqlEngines = ("graphql", "nosql", "ldap", "xpath", "ssti", "xslt", "xxe", "hql", "sparql", "odata")
if kb.tamperFunctions and any(conf.get(_) for _ in _nonSqlEngines):
engine = next(_ for _ in _nonSqlEngines if conf.get(_))
# tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines
# (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) do not run payloads through the tampering hook, so
# warn instead of silently ignoring the user's '--tamper'
if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)):
engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti", "xxe") if conf.get(_))
warnMsg = "tamper scripts are applied to SQL injection payloads only and "
warnMsg += "will be ignored by the '--%s' engine" % engine
logger.warning(warnMsg)
Expand Down Expand Up @@ -2205,17 +2193,9 @@ def _setKnowledgeBaseAttributes(flushAll=True):

kb.chars = AttribDict()
kb.chars.delimiter = randomStr(length=6, lowercase=True)
# NOTE: markers have to be mutually distinct (e.g. equal start/stop makes the delimited output ambiguous, while equal replacement markers make _errorReplaceChars() restore the wrong character). Also, none of the inner letters may be the boundary character itself, as that makes a marker contain a shorter one (e.g. 'qzqxq' carrying 'qzq')
_ = set()
while len(_) < 2:
_.add(randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET))
kb.chars.start, kb.chars.stop = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, __, KB_CHARS_BOUNDARY_CHAR) for __ in _)

_ = set()
while len(_) < 4:
_.add(randomStr(length=1, lowercase=True))
_.discard(KB_CHARS_BOUNDARY_CHAR)
kb.chars.at, kb.chars.space, kb.chars.dollar, kb.chars.hash_ = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, __, KB_CHARS_BOUNDARY_CHAR) for __ in _)
kb.chars.start = "%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET), KB_CHARS_BOUNDARY_CHAR)
kb.chars.stop = "%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET), KB_CHARS_BOUNDARY_CHAR)
kb.chars.at, kb.chars.space, kb.chars.dollar, kb.chars.hash_ = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, _, KB_CHARS_BOUNDARY_CHAR) for _ in randomStr(length=4, lowercase=True))

kb.checkWafMode = False
kb.choices = AttribDict(keycheck=False)
Expand Down Expand Up @@ -2262,7 +2242,6 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.forkNote = None
kb.futileUnion = None
kb.fuzzUnionTest = None
kb.gadget = None
kb.heavilyDynamic = False
kb.headersFile = None
kb.headersFp = {}
Expand Down Expand Up @@ -2299,7 +2278,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.lastParserStatus = None

kb.locks = AttribDict()
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "prediction", "socket", "redirect", "request", "value"):
kb.locks[_] = threading.Lock()

kb.matchRatio = None
Expand All @@ -2308,8 +2287,6 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.mergeCookies = None
kb.mysqlUtf8mb4 = None
kb.multiThreadMode = False
kb.multibit = {} # per injection point: absent=untried, False=unusable, else the row channel profile
kb.multibitHinted = False
kb.multipleCtrlC = False
kb.negativeLogic = False
kb.nchar = True
Expand Down Expand Up @@ -2778,7 +2755,9 @@ def _checkTor():
logger.info(infoMsg)

def _basicOptionValidation():
_nonSqlTechniques = ["--%s" % _ for _ in NONSQL_TECHNIQUES if conf.get(_)]
_nonSqlTechniques = [name for name, enabled in (
("--graphql", conf.graphql), ("--nosql", conf.nosql), ("--ldap", conf.ldap),
("--xpath", conf.xpath), ("--ssti", conf.ssti), ("--xxe", conf.xxe), ("--hql", conf.hql)) if enabled]
if len(_nonSqlTechniques) > 1:
errMsg = "only one non-SQL technique switch may be used at a time (found: %s). " % ", ".join(_nonSqlTechniques)
errMsg += "each is a self-contained scan for a different back-end class - pick one"
Expand Down
8 changes: 8 additions & 0 deletions lib/core/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from lib.core.common import getSafeExString
from lib.core.common import hashDBRetrieve
from lib.core.common import intersect
from lib.core.common import isNoneValue
from lib.core.common import isNumPosStrValue
from lib.core.common import normalizeUnicode
from lib.core.common import openFile
Expand Down Expand Up @@ -585,6 +586,13 @@ def _resumeDBMS():
conf.dbms = None
Backend.setDbms(dbms)
Backend.setVersionList(dbmsVersion)
else:
Backend.setDbms(conf.dbms)
else:
Backend.setDbms(dbms)

if isNoneValue(Backend.getVersionList()) or UNKNOWN_DBMS_VERSION in (Backend.getVersionList() or []):
Backend.setVersionList(dbmsVersion)
else:
infoMsg = "resuming back-end DBMS '%s' " % dbms
logger.info(infoMsg)
Expand Down
52 changes: 51 additions & 1 deletion tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
bootstrap()

from lib.core import common as C
from lib.core.settings import NULL
from lib.core.data import conf, kb
from lib.core.settings import NULL, UNKNOWN_DBMS_VERSION
from lib.core.enums import DBMS


Expand Down Expand Up @@ -95,6 +96,55 @@ def test_isDBMSVersionAtLeast(self):
self.assertFalse(C.isDBMSVersionAtLeast("8.0"))


class TestFormatGetDbms(unittest.TestCase):
def _resetDbmsState(self):
kb.stickyDBMS = False
kb.forcedDbms = None
kb.dbms = None
kb.dbmsVersion = [UNKNOWN_DBMS_VERSION]
conf.dbms = None

def test_version_without_kb_dbms_uses_conf_dbms(self):
saved_dbms = kb.dbms
saved_version = kb.dbmsVersion
saved_conf_dbms = conf.dbms
saved_forced = kb.forcedDbms
saved_sticky = kb.stickyDBMS

try:
self._resetDbmsState()
kb.dbmsVersion = ["5.0.12"]
conf.dbms = DBMS.MYSQL

self.assertEqual(C.Format.getDbms(), "MySQL 5.0.12")
self.assertNotIn("None", C.Format.getDbms())
finally:
kb.dbms = saved_dbms
kb.dbmsVersion = saved_version
conf.dbms = saved_conf_dbms
kb.forcedDbms = saved_forced
kb.stickyDBMS = saved_sticky

def test_version_only_when_dbms_unknown(self):
saved_dbms = kb.dbms
saved_version = kb.dbmsVersion
saved_conf_dbms = conf.dbms
saved_forced = kb.forcedDbms
saved_sticky = kb.stickyDBMS

try:
self._resetDbmsState()
kb.dbmsVersion = [">= 8.0.0"]

self.assertEqual(C.Format.getDbms(), ">= 8.0.0")
finally:
kb.dbms = saved_dbms
kb.dbmsVersion = saved_version
conf.dbms = saved_conf_dbms
kb.forcedDbms = saved_forced
kb.stickyDBMS = saved_sticky


class TestColumnPriority(unittest.TestCase):
def test_prioritySortColumns(self):
# assert the FULL ordering, not just the first element (id-like floats to front,
Expand Down
Loading