mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-28 12:31:00 +00:00
274 lines
11 KiB
Python
274 lines
11 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
|
|
Cross-dialect exercise of lib/core/agent.py payload-assembly helpers.
|
|
|
|
agent.py builds SQL payloads from per-DBMS dialect templates (queries.xml).
|
|
The helpers are pure given the identified back-end DBMS, so driving each one
|
|
across EVERY supported dialect walks the dialect-specific branches (CAST forms,
|
|
concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target.
|
|
|
|
These are smoke-level assertions (right type, dialect tokens present) rather than
|
|
golden strings: the goal is to traverse the dialect branches the single-DBMS
|
|
tests in test_agent.py do not reach.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from _testutils import bootstrap, set_dbms
|
|
bootstrap()
|
|
|
|
from lib.core.agent import agent
|
|
from lib.core.data import queries
|
|
|
|
DIALECTS = sorted(queries.keys())
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Per-dialect expectation maps (keyed by the DBMS display name == queries key).
|
|
#
|
|
# These were derived by inspecting the actual agent.py output for every dialect
|
|
# (the queries.xml templates drive the branches). They pin the *distinctive*
|
|
# dialect token so an assertion fails if the dialect branch collapses to the
|
|
# wrong form (e.g. concat operator swapped, null-wrapper dropped).
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
# concatQuery / simpleConcatenate join operator per dialect.
|
|
CONCAT_OPERATOR = {
|
|
"ClickHouse": "CONCAT(",
|
|
"Informix": "CONCAT(",
|
|
"MySQL": "CONCAT(",
|
|
"SAP MaxDB": "CONCAT(",
|
|
"Microsoft SQL Server": "+",
|
|
"Sybase": "+",
|
|
"Microsoft Access": "&",
|
|
}
|
|
# everything not listed above uses the SQL standard "||"
|
|
CONCAT_OPERATOR_DEFAULT = "||"
|
|
|
|
# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect.
|
|
NULL_WRAPPER = {
|
|
"Altibase": "NVL",
|
|
"Apache Derby": "COALESCE",
|
|
"ClickHouse": "ifNull",
|
|
"CrateDB": "COALESCE",
|
|
"Cubrid": "IFNULL",
|
|
"Firebird": "COALESCE",
|
|
"FrontBase": "COALESCE",
|
|
"H2": "IFNULL",
|
|
"HSQLDB": "IFNULL",
|
|
"IBM DB2": "COALESCE",
|
|
"Informix": "NVL",
|
|
"InterSystems Cache": "COALESCE",
|
|
"Mckoi": "IF(",
|
|
"Microsoft Access": "IIF",
|
|
"Microsoft SQL Server": "ISNULL",
|
|
"MimerSQL": "COALESCE",
|
|
"MonetDB": "COALESCE",
|
|
"MySQL": "IFNULL",
|
|
"Oracle": "NVL",
|
|
"PostgreSQL": "COALESCE",
|
|
"Presto": "COALESCE",
|
|
"Raima Database Manager": "IFNULL",
|
|
"SAP MaxDB": "VALUE",
|
|
"SQLite": "COALESCE",
|
|
"Snowflake": "NVL",
|
|
"Spanner": "IFNULL",
|
|
"Sybase": "ISNULL",
|
|
"Vertica": "COALESCE",
|
|
"Virtuoso": "__MAX_NOTNULL",
|
|
"eXtremeDB": "IFNULL",
|
|
}
|
|
|
|
# hexConvertField: dialects that DO have a hex function, mapped to its token.
|
|
HEX_FUNCTION = {
|
|
"Altibase": "HEX_ENCODE(",
|
|
"Cubrid": "HEX(",
|
|
"H2": "RAWTOHEX(",
|
|
"IBM DB2": "HEX(",
|
|
"Microsoft SQL Server": "fn_varbintohexstr",
|
|
"MySQL": "HEX(",
|
|
"Oracle": "RAWTOHEX(",
|
|
"PostgreSQL": "ENCODE(",
|
|
"Presto": "TO_HEX(",
|
|
"SAP MaxDB": "HEX(",
|
|
"SQLite": "HEX(",
|
|
"Spanner": "TO_HEX(",
|
|
"Sybase": "BINTOSTR",
|
|
"Vertica": "TO_HEX(",
|
|
}
|
|
# dialects that intentionally do NOT support hex conversion and return the
|
|
# field unchanged (a no-op the old "colname in out" check silently masked).
|
|
HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION)
|
|
|
|
# limitQuery: dialects whose limit template is empty so the call legitimately
|
|
# raises (no .limit.query). These are skipped by name in the limit-token test.
|
|
LIMIT_RAISES = {"Mckoi", "Raima Database Manager"}
|
|
# dialects with no special limitQuery branch: the query is returned unchanged
|
|
# (no limit token is emitted).
|
|
LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"}
|
|
# broad set of dialect limit tokens; every running, non-passthrough dialect
|
|
# emits at least one of these.
|
|
LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER")
|
|
|
|
|
|
class TestNullCastConcatFields(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
out = agent.nullCastConcatFields("user,password")
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
# both column names survive the null/cast/concat rewrite
|
|
self.assertIn("user", out, msg=dbms)
|
|
self.assertIn("password", out, msg=dbms)
|
|
# the dialect-specific NULL-wrapper must be present (the column-name
|
|
# check above is always satisfied and so cannot catch a broken
|
|
# branch); this fails if the wrapper collapses to the wrong form.
|
|
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
|
|
|
|
def test_literal_passthrough(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
# a bare quoted literal is returned untouched
|
|
self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms)
|
|
|
|
|
|
class TestNullAndCastField(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
out = agent.nullAndCastField("colname")
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
self.assertIn("colname", out, msg=dbms)
|
|
# dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...)
|
|
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
|
|
|
|
|
|
class TestHexConvertField(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
out = agent.hexConvertField("colname")
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
self.assertIn("colname", out, msg=dbms)
|
|
if dbms in HEX_FUNCTION:
|
|
# the dialect's hex function wraps the field
|
|
self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out))
|
|
else:
|
|
# intentional no-op: the field is returned verbatim. The old
|
|
# "colname in out" check masked this; pin the exact identity.
|
|
self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out))
|
|
|
|
|
|
class TestConcatQuery(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
out = agent.concatQuery("SELECT user FROM users")
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
# concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/
|
|
# SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and
|
|
# the rest use the SQL-standard ||. Assert the right operator so the
|
|
# test fails if the dialect collapses to the wrong concatenation.
|
|
expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT)
|
|
self.assertIn(expected, out, msg="%s: %s" % (dbms, out))
|
|
|
|
|
|
class TestSimpleConcatenate(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
out = agent.simpleConcatenate("a", "b")
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
self.assertIn("a", out, msg=dbms)
|
|
self.assertIn("b", out, msg=dbms)
|
|
|
|
|
|
class TestForgeUnionQuery(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
count = 3
|
|
out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None,
|
|
None, None, "NULL", None)
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
self.assertIn("UNION", out.upper(), msg=dbms)
|
|
# position -1 with char NULL fills every one of the `count` columns
|
|
# with the char, so the NULL char must appear exactly `count` times.
|
|
# (a hardcoded "UNION in out" check could not catch a wrong column
|
|
# count.) Match NULL as a whole token to avoid matching substrings.
|
|
self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count,
|
|
msg="%s expected %d NULLs: %s" % (dbms, count, out))
|
|
|
|
|
|
class TestLimitQuery(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
|
|
# Only Mckoi/Raima have an empty limit template and legitimately
|
|
# raise; skip exactly those by name rather than swallowing *any*
|
|
# exception (which would hide a real regression in another dialect).
|
|
if dbms in LIMIT_RAISES:
|
|
with self.assertRaises(Exception, msg=dbms):
|
|
agent.limitQuery(0, "SELECT user FROM users", "user")
|
|
continue
|
|
|
|
out = agent.limitQuery(0, "SELECT user FROM users", "user")
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
|
|
if dbms in LIMIT_PASSTHROUGH:
|
|
# these dialects have no dedicated limitQuery branch and return
|
|
# the query unchanged (documented no-op).
|
|
self.assertEqual(out, "SELECT user FROM users", msg=dbms)
|
|
else:
|
|
# every other running dialect emits a real limit construct
|
|
self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS),
|
|
msg="%s missing limit token: %s" % (dbms, out))
|
|
|
|
|
|
class TestForgeCaseStatement(unittest.TestCase):
|
|
def test_all_dialects(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
out = agent.forgeCaseStatement("1=1")
|
|
self.assertIsInstance(out, str, msg=dbms)
|
|
# dialects vary on the conditional form (CASE / IIF / IF); the
|
|
# condition itself is always embedded
|
|
self.assertIn("1=1", out, msg=dbms)
|
|
# ...but the conditional construct itself must also be present,
|
|
# otherwise the "1=1" check alone could pass on a degenerate output.
|
|
self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out,
|
|
msg="%s missing conditional construct: %s" % (dbms, out))
|
|
|
|
|
|
class TestPrefixSuffixAcrossDialects(unittest.TestCase):
|
|
def test_prefix_suffix(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
prefix = agent.prefixQuery("1=1")
|
|
suffix = agent.suffixQuery("1=1")
|
|
self.assertIsInstance(prefix, str, msg=dbms)
|
|
self.assertIsInstance(suffix, str, msg=dbms)
|
|
# prefixQuery pads a leading space ahead of the expression by default
|
|
self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix))
|
|
# suffixQuery returns the expression itself (no extra clause/comment)
|
|
self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix))
|
|
|
|
|
|
class TestRunAsDBMSUserAndWhere(unittest.TestCase):
|
|
def test_run_as_user_noop_without_conf(self):
|
|
for dbms in DIALECTS:
|
|
set_dbms(dbms)
|
|
# without conf.dbmsCred the query is returned unchanged
|
|
self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|