mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-28 04:20:58 +00:00
Couple of bug fixes
This commit is contained in:
parent
1404133538
commit
002a28f7f0
8 changed files with 250 additions and 14 deletions
|
|
@ -84,7 +84,7 @@ b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/paylo
|
|||
0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml
|
||||
997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml
|
||||
40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml
|
||||
9d7dcbc6c5e368c44db851865ff49c791c3dee1ee62d8c02af8f8b15f4551aed data/xml/queries.xml
|
||||
38882b6ceb8bca59ce8ed927abe3b8840394c56b3881371c2103e229b8795040 data/xml/queries.xml
|
||||
e043101194219a2e4c8bc352f0d3a04b87e1c28b1bcd6c13f6d5d1c9e260b653 doc/ARCHITECTURE.md
|
||||
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
|
||||
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
|
||||
|
|
@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/
|
|||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py
|
||||
63657c00a046ca0fb28fd069407ab6305bd7b95c42f26a96ed083fd05b152252 extra/vulnserver/vulnserver.py
|
||||
3abecaec1a9c59645a4821463a2d761235f7a4f763a491f188a41a083bbddd98 lib/controller/action.py
|
||||
6574ed70c7fe0ac305dbc85ed7102f648b6a3f42fe2fe6b89172d69717327149 lib/controller/checks.py
|
||||
72707b5bdfc757c4e5271e156178919292b991a6e7337d3dcdeffea9df6db3ea lib/controller/checks.py
|
||||
dcd4adcd7a2447a624ca7927541941d25767a4581af2d762c3197dc93790f4df lib/controller/controller.py
|
||||
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
|
||||
|
|
@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch
|
|||
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
|
||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||
f01361d999b0cf89b8418265c4a4962924fcc03a6b87e15b39c0836788725e85 lib/core/settings.py
|
||||
997888bab1d98fb9bc2550f3ab99df966d37f38719a41a8fb767e2cd79db6c4f lib/core/settings.py
|
||||
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
|
||||
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
|
||||
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
|
||||
|
|
@ -246,6 +246,7 @@ aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.
|
|||
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
|
||||
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
|
||||
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
|
||||
0fd055877e8b21d17c11447dac7f91ef1766e0b04d470c494a6d98f5249e3186 lib/utils/dialect.py
|
||||
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
|
||||
853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py
|
||||
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
|
||||
|
|
@ -394,7 +395,7 @@ ba04af3683b9a6e29e8fa6b3bf436a57e59435cebb042414f2df82018d91599e plugins/dbms/m
|
|||
78f1ff4b82fd4af50e1fbdb81539862f1c31258cda212b39f4a8501960f1b95e plugins/dbms/monetdb/syntax.py
|
||||
236fd244f0bbc3976b389429a8176feda6c243267564c2a0eff6fc2458c1b3f9 plugins/dbms/monetdb/takeover.py
|
||||
6bdc774463ac87b1bd1b6a9d5c2346b7edbf40d9848b7870a30d1eaedde4fc51 plugins/dbms/mssqlserver/connector.py
|
||||
52c19e9067f22f5c386206943d1807af4c661500bf260930a5986e9a180e96c7 plugins/dbms/mssqlserver/enumeration.py
|
||||
69ba678efde8335efb8a167b63143b4fb65ea19802bc3ade30c87cb979c198e4 plugins/dbms/mssqlserver/enumeration.py
|
||||
67cd70b64aed27af467682ceae8e20992b6765d2374d5762efb5a4585b8a6f79 plugins/dbms/mssqlserver/filesystem.py
|
||||
38ade085f9f1b227eda8c89f78e3ce869e8f430c98bef0cc7cbd2c7dcd60c24e plugins/dbms/mssqlserver/fingerprint.py
|
||||
1ecde09e80d7b709a710281f4983a6831bc02ca3458ae0b97b28446d6db241b4 plugins/dbms/mssqlserver/__init__.py
|
||||
|
|
@ -479,7 +480,7 @@ e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/v
|
|||
2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py
|
||||
51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py
|
||||
a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py
|
||||
37351d6fb7418e3659bec5c9a6f9f181a606deae74d3bc9fb8c97f495449471f plugins/generic/databases.py
|
||||
6d037861acbbabec529e10c50840820ca7b876c29c69310a571b519c3f3b72fa plugins/generic/databases.py
|
||||
36b7319ac00f8fe1a33496364a76ff165ea2e66db0150f5366a45135366369ca plugins/generic/entries.py
|
||||
d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py
|
||||
a02ac4ebc1cc488a2aa5ae07e6d0c3d5064e99ded7fd529dfa073735692f11df plugins/generic/filesystem.py
|
||||
|
|
@ -581,6 +582,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com
|
|||
5016119bdb57094381afdca35ef29a4a6641e26e4b48a9119f1db633e6123d29 tests/test_datafiles.py
|
||||
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
|
||||
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
|
||||
9c0a0cd0b2d52a53f75c98c60f87a022354b7c3dc4baaf3fe1e272a0af5b7f0a tests/test_dialectdbms.py
|
||||
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
|
||||
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
|
||||
9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py
|
||||
|
|
|
|||
|
|
@ -1321,7 +1321,7 @@
|
|||
</dbms>
|
||||
|
||||
<dbms value="ClickHouse">
|
||||
<cast query="CAST(%s AS String)"/>
|
||||
<cast query="CAST(%s AS Nullable(String))"/>
|
||||
<length query="length(%s)"/>
|
||||
<isnull query="ifNull(%s, '')"/>
|
||||
<delimiter query="||"/>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ from lib.request.connect import Connect as Request
|
|||
from lib.request.comparison import comparison
|
||||
from lib.request.inject import checkBooleanExpression
|
||||
from lib.request.templates import getPageTemplate
|
||||
from lib.utils.dialect import dialectCheckDbms
|
||||
from lib.techniques.union.test import unionTest
|
||||
from lib.techniques.union.use import configUnion
|
||||
from thirdparty import six
|
||||
|
|
@ -149,6 +150,13 @@ def checkSqlInjection(place, parameter, value):
|
|||
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None and not kb.droppingRequests:
|
||||
kb.heuristicDbms = heuristicCheckDbms(injection)
|
||||
|
||||
# keyword-free fallback: heuristicCheckDbms() above uses SELECT/quote payloads
|
||||
# and is skipped when the WAF/IPS is dropping requests; the operator-dialect
|
||||
# probes carry no SELECT/quote/schema name, so they can still narrow the DBMS in
|
||||
# that case (or when it was inconclusive), using the now-calibrated boolean oracle
|
||||
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None:
|
||||
kb.heuristicDbms = dialectCheckDbms(injection)
|
||||
|
||||
# If the DBMS has already been fingerprinted (via DBMS-specific
|
||||
# error message, simple heuristic check or via DBMS-specific
|
||||
# payload), ask the user to limit the tests to the fingerprinted
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.6.120"
|
||||
VERSION = "1.10.6.121"
|
||||
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)
|
||||
|
|
@ -306,7 +306,7 @@ FIREBIRD_SYSTEM_DBS = ("RDB$BACKUP_HISTORY", "RDB$CHARACTER_SETS", "RDB$CHECK_CO
|
|||
MAXDB_SYSTEM_DBS = ("SYSINFO", "DOMAIN")
|
||||
SYBASE_SYSTEM_DBS = ("master", "model", "sybsystemdb", "sybsystemprocs", "tempdb")
|
||||
DB2_SYSTEM_DBS = ("NULLID", "SQLJ", "SYSCAT", "SYSFUN", "SYSIBM", "SYSIBMADM", "SYSIBMINTERNAL", "SYSIBMTS", "SYSPROC", "SYSPUBLIC", "SYSSTAT", "SYSTOOLS", "SYSDEBUG", "SYSINST")
|
||||
HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOB")
|
||||
HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOBS")
|
||||
H2_SYSTEM_DBS = ("INFORMATION_SCHEMA",) + ("IGNITE", "ignite-sys-cache")
|
||||
INFORMIX_SYSTEM_DBS = ("sysmaster", "sysutils", "sysuser", "sysadmin")
|
||||
MONETDB_SYSTEM_DBS = ("tmp", "json", "profiler")
|
||||
|
|
|
|||
120
lib/utils/dialect.py
Normal file
120
lib/utils/dialect.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.common import popValue
|
||||
from lib.core.common import pushValue
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import DBMS
|
||||
from lib.request.inject import checkBooleanExpression
|
||||
|
||||
# Operator-dialect probes for a keyword-free back-end DBMS heuristic.
|
||||
#
|
||||
# Each probe is an arithmetic identity that holds only in the dialect(s) noted, using operator
|
||||
# *semantics* alone - no SQL keywords, functions, quotes or schema names. It complements
|
||||
# heuristicCheckDbms() (which uses (SELECT 'x')='x' string round-trips): the dialect probes carry
|
||||
# no SELECT/quote, so they can narrow the back-end DBMS where those are dropped (e.g. a
|
||||
# keyword-matching WAF/IPS, or when kb.droppingRequests has it skipped entirely).
|
||||
#
|
||||
# Each probe is evaluated through checkBooleanExpression(), i.e. as an appended boolean
|
||||
# (... AND (<probe>)), which yields a clean true/false from the comparison oracle. (A value-position
|
||||
# variant - replacing the value with id=2^0 etc. - was prototyped and rejected: those probes land on
|
||||
# OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing
|
||||
# false positives. See PROVE_DESIGN.md.)
|
||||
#
|
||||
# Truth table measured on a live OWASP-CRS platform across 11 engines (MySQL, MariaDB/TiDB,
|
||||
# PostgreSQL, CockroachDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB, Derby);
|
||||
# only the zero-false-positive rules are kept (see _classify). With anchor value 2:
|
||||
#
|
||||
# * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1) vs
|
||||
# no such operator (SQLite/Oracle/... -> error, so false)
|
||||
# * 2^3=8 -> '^' is exponentiation (PostgreSQL/CockroachDB: 2^3=8) - false for XOR dialects
|
||||
# (2^3=1) and erroring dialects; a positive PostgreSQL-family marker. CAVEAT:
|
||||
# '^'=exponentiation is not strictly unique to PostgreSQL - MS Access/Jet and DuckDB
|
||||
# also use it (neither on the platform), so this can read as PostgreSQL there.
|
||||
# * 5/2=2 -> integer division (PostgreSQL/MSSQL/SQLite) vs real division (MySQL/Oracle: 2.5)
|
||||
# * 2|0=2 -> a bitwise OR operator exists (absent in Firebird/Oracle/ClickHouse/H2)
|
||||
DIALECT_PROBES = (
|
||||
("xor", "2^0=2"),
|
||||
("pgpow", "2^3=8"),
|
||||
("intdiv", "5/2=2"),
|
||||
("bitor", "2|0=2"),
|
||||
)
|
||||
|
||||
def _classify(signature):
|
||||
"""
|
||||
Maps a measured (xor, pgpow, intdiv, bitor) operator-dialect signature to a back-end
|
||||
DBMS, or returns None when the signature does not *uniquely* identify a major DBMS (so
|
||||
detection proceeds unchanged - the heuristic never wrong-foots the scan).
|
||||
|
||||
Rules below are the subset of the measured 11-engine truth table that maps with zero
|
||||
false positives. Engines whose operator profile is not distinctive enough (Oracle's
|
||||
all-false signature, which a minimal engine like ClickHouse/H2/Firebird/HSQLDB/Derby or
|
||||
a fully WAF-blocked channel also produces) deliberately fall through to None:
|
||||
|
||||
>>> _classify((True, False, False, True)) # MySQL / MariaDB / TiDB
|
||||
'MySQL'
|
||||
>>> _classify((True, False, True, True)) # Microsoft SQL Server
|
||||
'Microsoft SQL Server'
|
||||
>>> _classify((False, True, True, True)) # PostgreSQL
|
||||
'PostgreSQL'
|
||||
>>> _classify((False, True, False, True)) # CockroachDB (pgwire) -> PostgreSQL family
|
||||
'PostgreSQL'
|
||||
>>> _classify((False, False, True, True)) # SQLite
|
||||
'SQLite'
|
||||
>>> _classify((False, False, True, False)) is None # Firebird/HSQLDB/Derby/H2 -> no prior
|
||||
True
|
||||
>>> _classify((False, False, False, False)) is None # all-false (Oracle/ClickHouse/blocked) -> no prior
|
||||
True
|
||||
"""
|
||||
|
||||
xor, pgpow, intdiv, bitor = signature
|
||||
|
||||
if pgpow: # '^' is exponentiation -> PostgreSQL family
|
||||
return DBMS.PGSQL
|
||||
if xor and intdiv: # '^' is XOR AND integer division -> SQL Server
|
||||
return DBMS.MSSQL
|
||||
if xor and not intdiv: # '^' is XOR AND real division -> MySQL family
|
||||
return DBMS.MYSQL
|
||||
if not xor and intdiv and bitor: # no '^', integer division, bitwise '|' -> SQLite
|
||||
return DBMS.SQLITE
|
||||
|
||||
return None
|
||||
|
||||
def dialectCheckDbms(injection):
|
||||
"""
|
||||
Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the
|
||||
given (boolean-capable) injection. Complements heuristicCheckDbms() - which is skipped when the
|
||||
WAF/IPS is dropping requests and otherwise relies on SELECT/quote payloads - because every probe
|
||||
here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous or
|
||||
WAF-blocked channel yields None, leaving the scan unchanged.
|
||||
"""
|
||||
|
||||
retVal = None
|
||||
|
||||
if conf.skipHeuristics:
|
||||
return retVal
|
||||
|
||||
pushValue(kb.injection)
|
||||
kb.injection = injection
|
||||
|
||||
try:
|
||||
# channel sanity: a tautology must read TRUE and a contradiction FALSE, otherwise the
|
||||
# boolean oracle is unreliable and the all-false signature (Oracle-like) would be meaningless
|
||||
if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3"):
|
||||
signature = tuple(bool(checkBooleanExpression(expr)) for _, expr in DIALECT_PROBES)
|
||||
retVal = _classify(signature)
|
||||
finally:
|
||||
kb.injection = popValue()
|
||||
|
||||
if retVal and not Backend.getIdentifiedDbms():
|
||||
infoMsg = "heuristic (dialect) test shows that the back-end DBMS could be '%s'" % retVal
|
||||
logger.info(infoMsg)
|
||||
|
||||
return retVal
|
||||
|
|
@ -93,7 +93,7 @@ class Enumeration(GenericEnumeration):
|
|||
|
||||
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
|
||||
for db in dbs:
|
||||
if conf.excludeSysDbs and db in self.excludeDbsList:
|
||||
if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList:
|
||||
infoMsg = "skipping system database '%s'" % db
|
||||
singleTimeLogMessage(infoMsg)
|
||||
continue
|
||||
|
|
@ -116,7 +116,7 @@ class Enumeration(GenericEnumeration):
|
|||
|
||||
if not kb.data.cachedTables and isInferenceAvailable() and not conf.direct:
|
||||
for db in dbs:
|
||||
if conf.excludeSysDbs and db in self.excludeDbsList:
|
||||
if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList:
|
||||
infoMsg = "skipping system database '%s'" % db
|
||||
singleTimeLogMessage(infoMsg)
|
||||
continue
|
||||
|
|
@ -206,7 +206,7 @@ class Enumeration(GenericEnumeration):
|
|||
for db in foundTbls.keys():
|
||||
db = safeSQLIdentificatorNaming(db)
|
||||
|
||||
if conf.excludeSysDbs and db in self.excludeDbsList:
|
||||
if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList:
|
||||
infoMsg = "skipping system database '%s'" % db
|
||||
singleTimeLogMessage(infoMsg)
|
||||
continue
|
||||
|
|
@ -343,7 +343,7 @@ class Enumeration(GenericEnumeration):
|
|||
for db in (_ for _ in dbs if _):
|
||||
db = safeSQLIdentificatorNaming(db)
|
||||
|
||||
if conf.excludeSysDbs and db in self.excludeDbsList:
|
||||
if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList:
|
||||
continue
|
||||
|
||||
if conf.exclude and re.search(conf.exclude, db, re.I) is not None:
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ class Databases(object):
|
|||
if conf.excludeSysDbs:
|
||||
infoMsg = "skipping system database%s '%s'" % ("s" if len(self.excludeDbsList) > 1 else "", ", ".join(unsafeSQLIdentificatorNaming(db) for db in self.excludeDbsList))
|
||||
logger.info(infoMsg)
|
||||
query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs) if db not in self.excludeDbsList)
|
||||
query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs) if unsafeSQLIdentificatorNaming(db) not in self.excludeDbsList)
|
||||
else:
|
||||
query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs))
|
||||
|
||||
|
|
@ -356,7 +356,7 @@ class Databases(object):
|
|||
|
||||
if not kb.data.cachedTables and isInferenceAvailable() and not conf.direct:
|
||||
for db in dbs:
|
||||
if conf.excludeSysDbs and db in self.excludeDbsList:
|
||||
if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList:
|
||||
infoMsg = "skipping system database '%s'" % unsafeSQLIdentificatorNaming(db)
|
||||
logger.info(infoMsg)
|
||||
continue
|
||||
|
|
|
|||
106
tests/test_dialectdbms.py
Normal file
106
tests/test_dialectdbms.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth
|
||||
table: the (xor, intdiv, pgcast, bitor) operator signatures measured across 11 live engines
|
||||
on an OWASP-CRS test platform, asserting that _classify() maps each to the expected back-end
|
||||
DBMS - and, just as importantly, that the engines whose signatures collide or are ambiguous
|
||||
map to None (no prior), so the heuristic never wrong-foots detection. The end-to-end behaviour
|
||||
(the probes producing these signatures through a real boolean injection) is exercised against
|
||||
the live platform, not here.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
import lib.utils.dialect as dialect
|
||||
from lib.core.data import kb
|
||||
from lib.core.enums import DBMS
|
||||
from lib.utils.dialect import _classify
|
||||
from lib.utils.dialect import dialectCheckDbms
|
||||
|
||||
# measured 2026-06 across the sqli-platform (boolean form "id=2 AND <probe>", anchor value 2);
|
||||
# signature = (2^0=2, 2^3=8, 5/2=2, 2|0=2)
|
||||
MEASURED = {
|
||||
"mysql": ((True, False, False, True), DBMS.MYSQL),
|
||||
"tidb": ((True, False, False, True), DBMS.MYSQL), # MySQL wire-compatible
|
||||
"mssql": ((True, False, True, True), DBMS.MSSQL),
|
||||
"postgres": ((False, True, True, True), DBMS.PGSQL),
|
||||
"cockroach": ((False, True, False, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division)
|
||||
"sqlite": ((False, False, True, True), DBMS.SQLITE),
|
||||
# not distinctive enough -> deliberately no prior (operators alone can't safely separate these)
|
||||
"firebird": ((False, False, True, False), None),
|
||||
"hsqldb": ((False, False, True, False), None), # collides with firebird/derby/h2
|
||||
"derby": ((False, False, True, False), None),
|
||||
"h2": ((False, False, True, False), None),
|
||||
"clickhouse": ((False, False, False, False), None), # all-error, like Oracle/broken channel
|
||||
}
|
||||
|
||||
|
||||
class TestDialectClassification(unittest.TestCase):
|
||||
def test_measured_engines_map_as_expected(self):
|
||||
for engine, (signature, expected) in MEASURED.items():
|
||||
self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine)
|
||||
|
||||
def test_no_false_positive_across_measured_set(self):
|
||||
# ambiguous engines must not borrow a major-DBMS identity; concrete ones must stay in range
|
||||
for engine, (signature, expected) in MEASURED.items():
|
||||
result = _classify(signature)
|
||||
if expected is None:
|
||||
self.assertIsNone(result, "ambiguous engine %r leaked a DBMS prior" % engine)
|
||||
else:
|
||||
self.assertIn(result, (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.ORACLE))
|
||||
|
||||
def test_all_error_signature_yields_no_prior(self):
|
||||
# an all-error signature (Oracle, ClickHouse, or simply a WAF-blocked channel) is not
|
||||
# distinctive enough - it must NOT be guessed as any DBMS
|
||||
self.assertIsNone(_classify((False, False, False, False)))
|
||||
|
||||
def test_pgpow_dominates_as_postgres_marker(self):
|
||||
# exponentiation '^' is a positive PostgreSQL-family marker regardless of division flavour
|
||||
self.assertEqual(_classify((False, True, True, True)), DBMS.PGSQL)
|
||||
self.assertEqual(_classify((False, True, False, True)), DBMS.PGSQL)
|
||||
|
||||
|
||||
class TestDialectCheckDbmsGuard(unittest.TestCase):
|
||||
"""dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good
|
||||
channel, and None (no prior) whenever the channel is unreliable - the safety contract."""
|
||||
|
||||
def _run(self, truth):
|
||||
# truth: {expression: bool} simulating checkBooleanExpression through a confirmed injection
|
||||
orig = dialect.checkBooleanExpression
|
||||
dialect.checkBooleanExpression = lambda expr, **kwargs: bool(truth.get(expr, False))
|
||||
saved = kb.get("injection")
|
||||
try:
|
||||
return dialectCheckDbms(object()) # the injection arg is only stashed, never inspected here
|
||||
finally:
|
||||
dialect.checkBooleanExpression = orig
|
||||
kb.injection = saved
|
||||
|
||||
def test_identifies_mysql_on_good_channel(self):
|
||||
truth = {"2=2": True, "2=3": False, "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True}
|
||||
self.assertEqual(self._run(truth), DBMS.MYSQL)
|
||||
|
||||
def test_identifies_postgres_on_good_channel(self):
|
||||
truth = {"2=2": True, "2=3": False, "2^0=2": False, "2^3=8": True, "5/2=2": True, "2|0=2": True}
|
||||
self.assertEqual(self._run(truth), DBMS.PGSQL)
|
||||
|
||||
def test_none_on_blocked_channel(self):
|
||||
# everything blocked/false -> the tautology 2=2 reads False -> sanity fails -> None
|
||||
self.assertIsNone(self._run({}))
|
||||
|
||||
def test_none_on_static_channel(self):
|
||||
# a static page reads everything True, so the contradiction 2=3 is True -> sanity fails -> None
|
||||
self.assertIsNone(self._run({"2=2": True, "2=3": True, "2^0=2": True, "2^3=8": True, "5/2=2": True, "2|0=2": True}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue