Update of tests

This commit is contained in:
Miroslav Štampar 2026-06-28 18:27:59 +02:00
parent cb20a446ae
commit 2297c81309
32 changed files with 7177 additions and 7304 deletions

View file

@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
5cbf5f4bc21f21873df79babd91da8f7fea5ec3c1999f108f005ca6fb4d453b6 lib/core/settings.py
bb908144ffaf055c67bb06da7f914d77cad00f84839c63ae2f83fea62cdacfe2 lib/core/settings.py
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
@ -579,31 +579,25 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional
0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py
ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py
d2c27dff782dbe119a4cb5041f374d87b67e3da523ee3a7ad584d34721b6c564 tests/test_agent_dialects.py
bfb553602eb5d20b4ab5928dbcf8e6a3e7e5ff69f7d30d1f53ef6d323c237f6c tests/test_agent.py
d16977d057c28888aa41500f79a19789cadef693cb8b7d9a3bca55b983ce2266 tests/test_agent.py
138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py
feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py
36bcb68483d824db5d05870fab62f1907221bf256826b734302fbc15a9231c42 tests/test_brute.py
27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py
c99b77cc5d85334f147a1a6d4b2867af396f70e9f2609f8587344e084910e893 tests/test_checks.py
9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py
2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py
c6338f74230b758cb41adacf4f04593e70b4b11e054ea0b35712607a781e0d55 tests/test_common_parsers.py
b1540c5f2be80ee3d870d7c373adfca23f33adb06724db00335adbd79bea4272 tests/test_common_utils.py
058c6b13f2f9ce7798f4106eb4f2a0eaf290eab6b3f9aff2f46553f80c872d29 tests/test_common.py
899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py
7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py
a0a29231acbbe6bec11400e28b39b76eaf812c03bf79d5f0dbdd68cd54a052f8 tests/test_compat.py
a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py
75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py
d2c52b1c9b0f31e2d30e1fc3942986692a815e76fa8e39903c3824d6d6d0ee71 tests/test_core_extra.py
7c6d542bf96e8962ecdf8607f93e84babe4820045533bded170955e95727d630 tests/test_core_final.py
e42f6dd46fa7f2d1e666116e2244fa02e7b9d930a005e2bbeea89cfe3f2215b6 tests/test_core_more.py
951822c0d6ea62dc91cc4a7614059788b256cac06167f4767721f2ad5d54a78b tests/test_databases_enum.py
2bd0faeaf7db1d73dd0caab3bde9900fdaa1f38fd736a6e238cd56ff9bc67b66 tests/test_databases_enum.py
c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
c9f7c5219e379b0242914f79f1e5d3b8b7d1a4c5e9f77cd05d0ec382d4fbed88 tests/test_dbms_enum_a.py
866978b7d5d0270a54465897932fe645c7e0360d73b0e4086540558c107e680d tests/test_dbms_enum_b.py
a3628b7f22dcc0ff4cd9ed8a1e70519a340f40fa4d73e9220c7d11f5088d9c01 tests/test_dbms_enum.py
8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
8e469e4e29319bcb718803a9e109e742965875c985fa8e8d3bb5b18c922ec597 tests/test_deps.py
cf480e241746fdbbe071c2dfd25ec5fd186a79dcb4522f034f661b9f55a2c4ea tests/test_deps.py
b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
@ -612,11 +606,11 @@ ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns
4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py
9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
fe1211ce43a51cd8ec7dd3395aafda8d7313ff60e2ef013072ce9fa49ca4a242 tests/test_entries.py
bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py
31354d3cff0d26ecf3b42e949a2780ae3d286cdf206b59404e18a96e7a2cddd2 tests/test_filesystem.py
26730151abea598f193131c5d64ef92b531941972f3d6236f9951c3116030b1c tests/test_filesystem.py
6a9d95f64c7892957742534a14e8f094c6ed9ebc91b7059f4f1665049228a5a6 tests/test_fingerprint.py
4f3cfb830b323a3423b0f80985b9a0bbbe4ef77350b762f103dcd8936cca67c6 tests/test_generic_enum_more.py
9874920d18fc30736630df6b14a70b230504d2e4d0c035971a9aa285ee623839 tests/test_generic_more.py
de477d585396596556f7020d39bad3577f4d73336c19c1ee14e4158c45dbd924 tests/test_generic_takeover.py
bde97a4781c4ee84e0fe86f7a33206f114167eb14b704013ecf1c26b838193d7 tests/test_graphql.py
50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py
92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py
@ -625,39 +619,37 @@ da2efd1b7457ff619d98a2ae5045f072fdd34be2aa1c18f17d74d7518eeb6707 tests/test_has
c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
280afe64cabac3a737d2574f4e2873760c3883eaac1b7ba0f8fed4b82b91c9c2 tests/test_inference.py
0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py
88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py
647d782395fe88dcda775808b9988a0809b208d1df9412d89dc8b6809bd15de6 tests/test_option_more.py
a5743989442de51b3689b30c27118249502bb462788abeeb1ddb27cd176cd363 tests/test_option_setup.py
6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py
064617c6a3d28ecd75136318b4f515ab1adefbf830da17667f105337b419c184 tests/test_payload_marking.py
57fa7b742aa0859ae166cea49dd6daac36d21aae05fe3ba6c73f42c4c56d7a3c tests/test_payload_marking.py
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py
a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py
c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py
2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py
67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py
f7478deabe9d117c60d597859510a168c81e71f60981503dcd798ef2311b30a1 tests/test_request_basic.py
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
d4f6e60c23db67430cf68dc2d90317d69391a19feff0f842c08ae2443b481857 tests/test_search_enum.py
5b6ce95dddbd07d0126224f4f066643938476e536e18b700ea5d916e1052a715 tests/test_search_enum.py
a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py
d6bcba7232fff834737c094679c92e7a69cab5721bc87cb10bcab868c6a8115f tests/test_sgmllib.py
d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py
8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py
8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py
44954b916f1e4a4bb217516a65cf330fca922600d484f732525e0e4a2a553167 tests/test_target_parsing.py
67472bd71c20782cc0f738e2c2e674c29d6985669e14d15b69baef7d0e33de62 tests/test_target_parsing.py
b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py
d070a72ae9529182d6dfc0884f7720d42a5f0cd8cd865dd4c2d209389c3ade85 tests/test_techniques_more.py
f2e8b5b9799f4e591462f53a97bb643c6399acf703f33e119c03d991971274ab tests/test_techniques.py
0e644bb7b25c183d0d689ea7be542d7a2ce780cc68067f89afb2ee095a79f762 tests/test_techniques.py
639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py
f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py
708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py
48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py
4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py
e7793907ce4dad9034d61f2a3cdfec8af33b96f8e6f67138b09daf81a825c13f tests/test_users_enum.py
eca021208e388b4d14c53f1e9f8a6e7d685e54ba572fb2a8487e6b620a20bcb5 tests/test_users_enum.py
23ffd75b5aec33066e6d6aad01ab2c9c1b12ee20c1a0990f8f1be81f1ad16161 tests/_testutils.py
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py

View file

@ -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.186"
VERSION = "1.10.6.187"
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)

View file

@ -4,15 +4,32 @@
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Payload assembly helpers in lib/core/agent.py.
Consolidated unit coverage for lib/core/agent.py.
These are the (mostly) DBMS-independent string transforms that wrap, fold and
clean a payload on its way to the wire: prefix/suffix, payload delimiters,
field extraction, CONCAT folding, and RAND-marker cleanup. All values below
were probed from real output, not assumed.
This file merges the agent.py tests previously spread across
test_agent.py, test_agent_dialects.py, test_core_more.py and
test_core_extra.py:
* Payload assembly helpers (DBMS-independent string transforms that wrap,
fold and clean a payload on its way to the wire): prefix/suffix, payload
delimiters, field extraction, CONCAT folding, RAND-marker cleanup.
* Cross-dialect exercise of the 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.
* Argument-combination / shape coverage for forgeUnionQuery, limitQuery,
whereQuery, getComment, concatQuery(unpack=False), cleanupPayload markers,
adjustLateValues, getFields shapes, prefix/suffix args, nullAndCastField
noCast, plus the pure agent helpers (extractPayload/replacePayload, ...).
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import os
import re
import sys
import unittest
@ -21,9 +38,122 @@ from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.agent import agent
from lib.core.data import conf, kb, queries
from lib.core.enums import DBMS
from lib.core.settings import PAYLOAD_DELIMITER
from lib.core.settings import (
PAYLOAD_DELIMITER,
SLEEP_TIME_MARKER,
BOUNDED_BASE64_MARKER,
)
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 DbmsStateMixin(object):
"""Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak."""
def setUp(self):
self._forcedDbms = kb.forcedDbms
self._sticky = kb.stickyDBMS
self._batch = conf.batch
conf.batch = True
def tearDown(self):
kb.forcedDbms = self._forcedDbms
kb.stickyDBMS = self._sticky
conf.batch = self._batch
# --------------------------------------------------------------------------- #
# Single-DBMS payload-assembly helpers (formerly test_agent.py)
# --------------------------------------------------------------------------- #
class TestPayloadDelimiters(unittest.TestCase):
def test_add(self):
@ -82,5 +212,557 @@ class TestCleanupPayload(unittest.TestCase):
self.assertTrue(out.split()[-1].isdigit(), msg=out) # ...and replaced with a concrete number
# --------------------------------------------------------------------------- #
# Cross-dialect smoke coverage (formerly test_agent_dialects.py)
# --------------------------------------------------------------------------- #
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 TestConcatQueryDialects(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 TestForgeUnionQueryDialects(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 TestLimitQueryDialects(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)
# --------------------------------------------------------------------------- #
# Argument-combination / shape coverage (formerly test_core_more.py)
# --------------------------------------------------------------------------- #
class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase):
"""forgeUnionQuery arg combinations not reached by the dialect smoke test."""
def test_limited_subselect_wraps_query(self):
set_dbms(DBMS.MYSQL)
# limited=True wraps the payload as (SELECT ...) at `position`, fills the
# rest with `char`, and appends the FROM/comment/suffix
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None,
None, None, "NULL", None, limited=True)
self.assertIn("(SELECT user FROM mysql.user)", out)
self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out)
# position 1 of 3 => NULL,<payload>,NULL
self.assertEqual(out.count("NULL"), 2, msg=out)
def test_multiple_unions_appends_second_select(self):
set_dbms(DBMS.MYSQL)
out = agent.forgeUnionQuery("SELECT a FROM t", 0, 2, None, None, None,
"NULL", None, multipleUnions="b")
# the multipleUnions payload produces a *second* UNION ALL SELECT
self.assertEqual(out.upper().count("UNION ALL SELECT"), 2, msg=out)
self.assertIn("b", out)
def test_from_table_override(self):
set_dbms(DBMS.MYSQL)
out = agent.forgeUnionQuery("SELECT 1", 0, 1, None, None, None, "NULL",
None, fromTable=" FROM dummytable")
self.assertIn("FROM dummytable", out, msg=out)
def test_into_outfile_forces_null_position(self):
set_dbms(DBMS.MYSQL)
# an INTO OUTFILE clause forces position 0 / char NULL and re-appends the file part
out = agent.forgeUnionQuery("SELECT a INTO OUTFILE '/tmp/o.txt' FROM t",
1, 2, None, None, None, "NULL", None)
self.assertIn("INTO OUTFILE '/tmp/o.txt'", out, msg=out)
def test_collate_clause_on_mysql(self):
set_dbms(DBMS.MYSQL)
# collate=True on MySQL wraps a non-NULL, non-numeric value in the
# MYSQL_UNION_VALUE_CAST collation wrapper
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 0, 1, None,
None, None, "NULL", None, collate=True)
self.assertIn("CONVERT", out.upper(), msg=out)
class TestLimitQuery(DbmsStateMixin, unittest.TestCase):
"""limitQuery dialect shapes beyond the single limitQuery(0,...) smoke test."""
def test_no_from_returns_unchanged(self):
set_dbms(DBMS.MYSQL)
self.assertEqual(agent.limitQuery(5, "SELECT 1", "1"), "SELECT 1")
def test_mysql_appends_limit_offset_one(self):
set_dbms(DBMS.MYSQL)
out = agent.limitQuery(7, "SELECT user FROM mysql.user", "user")
self.assertTrue(out.endswith("LIMIT 7,1"), msg=out)
def test_pgsql_offset_form(self):
set_dbms(DBMS.PGSQL)
out = agent.limitQuery(4, "SELECT usename FROM pg_shadow", "usename")
self.assertIn("OFFSET 4 LIMIT 1", out, msg=out)
def test_oracle_rownum_wrap(self):
set_dbms(DBMS.ORACLE)
out = agent.limitQuery(2, "SELECT banner FROM v$version", ["banner"])
# Oracle wraps in a ROWNUM-bounded subselect ending with =<num+1>
self.assertIn("ROWNUM", out.upper(), msg=out)
self.assertTrue(out.rstrip().endswith("=3"), msg=out)
def test_firebird_first_skip(self):
set_dbms(DBMS.FIREBIRD)
out = agent.limitQuery(3, "SELECT foo FROM bar", "foo")
self.assertIsInstance(out, str)
self.assertIn("foo", out)
# Firebird uses ROWS <num+1> TO <num+1> (the FIRST/SKIP emulation); pin
# the exact shape so a broken offset arithmetic is caught.
self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out)
def test_mssql_top_not_in(self):
set_dbms(DBMS.MSSQL)
out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name")
# MSSQL emulates LIMIT via TOP + NOT IN
self.assertIn("TOP", out.upper(), msg=out)
self.assertIn("NOT IN", out.upper(), msg=out)
class TestWhereQuery(DbmsStateMixin, unittest.TestCase):
"""whereQuery only acts when conf.dumpWhere is set."""
def setUp(self):
DbmsStateMixin.setUp(self)
self._dumpWhere = conf.dumpWhere
self._tbl = conf.tbl
def tearDown(self):
conf.dumpWhere = self._dumpWhere
conf.tbl = self._tbl
DbmsStateMixin.tearDown(self)
def test_no_dumpwhere_is_identity(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = None
self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t")
def test_appends_where_clause(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>10"
conf.tbl = None
out = agent.whereQuery("SELECT a FROM t")
self.assertIn("WHERE id>10", out, msg=out)
def test_existing_where_gets_anded(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>10"
conf.tbl = None
out = agent.whereQuery("SELECT a FROM t WHERE b=1")
self.assertIn("AND id>10", out, msg=out)
def test_order_by_suffix_preserved(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>10"
conf.tbl = None
out = agent.whereQuery("SELECT a FROM t ORDER BY a")
# the genuine trailing ORDER BY is kept after the spliced WHERE
self.assertIn("WHERE id>10", out, msg=out)
# the ORDER BY must survive *after* the spliced WHERE clause; the
# substring check alone could pass even if the suffix were dropped.
self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out)
class TestGetComment(unittest.TestCase):
def test_present(self):
from lib.core.datatype import AttribDict
self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x")
def test_absent_returns_empty(self):
from lib.core.datatype import AttribDict
self.assertEqual(agent.getComment(AttribDict()), "")
class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase):
def test_unpack_false_returns_input_unchanged(self):
set_dbms(DBMS.MYSQL)
self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False),
"SELECT a FROM t")
def test_pgsql_unpack_uses_pipe_concat(self):
set_dbms(DBMS.PGSQL)
out = agent.concatQuery("SELECT usename FROM pg_shadow")
self.assertIn("||", out, msg=out)
self.assertIn(kb.chars.start, out, msg=out)
self.assertIn(kb.chars.stop, out, msg=out)
class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase):
def test_origvalue_digit_inlined(self):
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42")
self.assertEqual(out, "x=42")
def test_origvalue_nondigit_quoted(self):
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc")
self.assertIn("'abc'", out, msg=out)
def test_original_marker_raw_substitution(self):
out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw")
self.assertEqual(out, "p=raw")
def test_space_replace_marker(self):
out = agent.cleanupPayload("a[SPACE_REPLACE]b")
self.assertEqual(out, "a%sb" % kb.chars.space)
def test_non_string_returns_none(self):
self.assertIsNone(agent.cleanupPayload(None))
class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase):
def test_sleeptime_replaced_with_timesec(self):
out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER)
self.assertEqual(out, "SLEEP(%s)" % conf.timeSec)
self.assertNotIn(SLEEP_TIME_MARKER, out)
def test_randnum_marker_substituted(self):
out = agent.adjustLateValues("v=[RANDNUM]")
self.assertNotIn("[RANDNUM]", out)
self.assertTrue(out.split("=")[1].isdigit(), msg=out)
def test_bounded_base64_marker_encoded(self):
payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER)
out = agent.adjustLateValues(payload)
# the marked region is base64-encoded and the markers are consumed
self.assertNotIn(BOUNDED_BASE64_MARKER, out)
self.assertEqual(out, "QUI=")
def test_empty_payload_passthrough(self):
self.assertEqual(agent.adjustLateValues(""), "")
class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase):
def test_select_top(self):
set_dbms(DBMS.MSSQL)
res = agent.getFields("SELECT TOP 1 name FROM sysobjects")
self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched")
self.assertEqual(res[6], "name")
def test_distinct(self):
set_dbms(DBMS.MYSQL)
res = agent.getFields("SELECT DISTINCT(name) FROM t")
self.assertEqual(res[6], "name")
def test_function_is_single_element(self):
set_dbms(DBMS.MYSQL)
res = agent.getFields("SELECT COUNT(*) FROM t")
self.assertEqual(res[5], ["COUNT(*)"])
def test_no_from_keeps_whole_select_list(self):
set_dbms(DBMS.MYSQL)
res = agent.getFields("SELECT a,b,c")
self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM")
self.assertEqual(res[5], ["a", "b", "c"])
class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase):
def test_prefix_with_explicit_prefix(self):
set_dbms(DBMS.MYSQL)
out = agent.prefixQuery("1=1", prefix="')")
self.assertIn("')", out, msg=out)
self.assertTrue(out.endswith("1=1"), msg=out)
def test_prefix_group_by_clause_uses_prefix_verbatim(self):
set_dbms(DBMS.MYSQL)
# clause == [2] (GROUP BY / ORDER BY) => no trailing space added
out = agent.prefixQuery("1=1", prefix="X", clause=[2])
self.assertEqual(out, "X1=1")
def test_suffix_appends_comment(self):
set_dbms(DBMS.MYSQL)
out = agent.suffixQuery("1=1", comment="-- -")
self.assertTrue(out.startswith("1=1"), msg=out)
self.assertIn("-", out)
def test_suffix_appends_suffix_no_comment(self):
set_dbms(DBMS.MYSQL)
out = agent.suffixQuery("1=1", suffix="')")
self.assertIn("')", out, msg=out)
class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase):
def setUp(self):
DbmsStateMixin.setUp(self)
self._noCast = conf.noCast
def tearDown(self):
conf.noCast = self._noCast
DbmsStateMixin.tearDown(self)
def test_nocast_returns_field_unchanged(self):
set_dbms(DBMS.MYSQL)
conf.noCast = True
self.assertEqual(agent.nullAndCastField("colname"), "colname")
def test_cast_present_when_nocast_off(self):
set_dbms(DBMS.MYSQL)
conf.noCast = False
out = agent.nullAndCastField("colname")
self.assertIn("CAST", out.upper(), msg=out)
self.assertIn("colname", out)
# --------------------------------------------------------------------------- #
# Pure agent helpers (formerly test_core_extra.py)
# --------------------------------------------------------------------------- #
class TestAgentPure(unittest.TestCase):
"""Pure agent.py methods independent of full injection state."""
@classmethod
def setUpClass(cls):
from lib.core.agent import agent
cls.agent = agent
def tearDown(self):
set_dbms(None)
def test_get_comment_present(self):
from lib.core.datatype import AttribDict
request = AttribDict()
request.comment = "-- foo"
self.assertEqual(self.agent.getComment(request), "-- foo")
def test_get_comment_absent(self):
from lib.core.datatype import AttribDict
request = AttribDict()
self.assertEqual(self.agent.getComment(request), "")
def test_add_payload_delimiters(self):
from lib.core.settings import PAYLOAD_DELIMITER
value = "1 AND 1=1"
result = self.agent.addPayloadDelimiters(value)
self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER))
# falsy value returned unchanged
self.assertEqual(self.agent.addPayloadDelimiters(""), "")
def test_remove_payload_delimiters_roundtrip(self):
self.assertEqual(
self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")),
"1 AND 1=1",
)
def test_extract_payload(self):
wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix"
self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1")
def test_replace_payload(self):
wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix"
replaced = self.agent.replacePayload(wrapped, "NEW")
self.assertEqual(self.agent.extractPayload(replaced), "NEW")
# surrounding text preserved
self.assertTrue(replaced.startswith("prefix"))
self.assertTrue(replaced.endswith("suffix"))
def test_simple_concatenate_mysql(self):
set_dbms(DBMS.MYSQL)
# MySQL concatenate query template is 'CONCAT(%s,%s)'
self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)")
def test_hex_convert_field_mysql(self):
set_dbms(DBMS.MYSQL)
# MySQL hex template is 'HEX(%s)'
self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)")
def test_get_fields_select_from(self):
set_dbms(DBMS.MYSQL)
result = self.agent.getFields("SELECT a, b FROM users")
fieldsToCastList = result[5]
fieldsToCastStr = result[6]
self.assertEqual(fieldsToCastStr, "a, b")
self.assertEqual(fieldsToCastList, ["a", "b"])
def test_get_fields_no_from(self):
set_dbms(DBMS.MYSQL)
# a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list
result = self.agent.getFields("SELECT 1")
fieldsSelectFrom = result[0]
self.assertIsNone(fieldsSelectFrom)
self.assertEqual(result[6], "1")
class TestAgentWhereQuery(unittest.TestCase):
@classmethod
def setUpClass(cls):
from lib.core.agent import agent
cls.agent = agent
def setUp(self):
self._old_dumpWhere = conf.dumpWhere
self._old_tbl = conf.tbl
conf.tbl = None
def tearDown(self):
conf.dumpWhere = self._old_dumpWhere
conf.tbl = self._old_tbl
set_dbms(None)
def test_no_dumpwhere_passthrough(self):
conf.dumpWhere = None
query = "SELECT a FROM t"
self.assertEqual(self.agent.whereQuery(query), query)
def test_appends_where_clause(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>0"
# no existing WHERE -> appends ' WHERE id>0'
self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0")
def test_and_when_where_present(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>0"
# existing WHERE -> appended with AND
self.assertEqual(
self.agent.whereQuery("SELECT a FROM t WHERE x=1"),
"SELECT a FROM t WHERE x=1 AND id>0",
)
def test_splices_before_order_by(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>0"
# WHERE must be spliced before the trailing ORDER BY suffix
self.assertEqual(
self.agent.whereQuery("SELECT a FROM t ORDER BY a"),
"SELECT a FROM t WHERE id>0 ORDER BY a",
)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -1,274 +0,0 @@
#!/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()

198
tests/test_brute.py Normal file
View file

@ -0,0 +1,198 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit coverage for lib/utils/brute.py.
tableExists / columnExists are driven with conf.direct=True and the external
collaborators (inject.checkBooleanExpression, getFileItems, runThreads,
getPageWordSet) monkeypatched so the check runs synchronously, deterministically
and offline; plus _addPageTextWords.
Any global conf/kb/Backend state that a call reads or writes is snapshotted in
setUp and restored in tearDown so test ordering is irrelevant.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import os
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.data import conf, kb
from lib.core.enums import DBMS
import lib.utils.brute as brute
from lib.request import inject
class DbmsStateMixin(object):
"""Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak."""
def setUp(self):
self._forcedDbms = kb.forcedDbms
self._sticky = kb.stickyDBMS
self._batch = conf.batch
conf.batch = True
def tearDown(self):
kb.forcedDbms = self._forcedDbms
kb.stickyDBMS = self._sticky
conf.batch = self._batch
class TestBrute(DbmsStateMixin, unittest.TestCase):
"""Drive tableExists / columnExists with all external collaborators stubbed.
conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression,
getFileItems and runThreads are monkeypatched so the check runs synchronously,
deterministically and offline. getPageWordSet is neutralized so the wordlist is
just what the stub returns.
"""
def setUp(self):
DbmsStateMixin.setUp(self)
self._saved_conf = {k: conf.get(k) for k in
("direct", "db", "tbl", "threads", "api", "verbose")}
self._choices = kb.choices
self._cachedTables = kb.data.get("cachedTables")
self._cachedColumns = kb.data.get("cachedColumns")
self._brute = kb.brute
self._origPage = kb.originalPage
# stub the collaborators
self._orig_cbe = inject.checkBooleanExpression
self._orig_brute_cbe = brute.inject.checkBooleanExpression
self._orig_getFileItems = brute.getFileItems
self._orig_runThreads = brute.runThreads
self._orig_getPageWordSet = brute.getPageWordSet
from lib.core.datatype import AttribDict
kb.choices = AttribDict(keycheck=False)
kb.choices.tableExists = None
kb.choices.columnExists = None
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.brute = AttribDict({"tables": [], "columns": []})
kb.originalPage = None
conf.direct = True
conf.db = None
conf.threads = 1
conf.api = False
conf.verbose = 0
# runThreads -> just call the worker once synchronously
def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs):
kb.threadContinue = True
threadFunction()
brute.runThreads = _fakeRunThreads
# no page words injected into the wordlist
brute.getPageWordSet = lambda page: set()
# wordlist file -> small fixed list
brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"]
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
kb.choices = self._choices
if self._cachedTables is None:
kb.data.pop("cachedTables", None)
else:
kb.data.cachedTables = self._cachedTables
if self._cachedColumns is None:
kb.data.pop("cachedColumns", None)
else:
kb.data.cachedColumns = self._cachedColumns
kb.brute = self._brute
kb.originalPage = self._origPage
brute.inject.checkBooleanExpression = self._orig_brute_cbe
brute.getFileItems = self._orig_getFileItems
brute.runThreads = self._orig_runThreads
brute.getPageWordSet = self._orig_getPageWordSet
DbmsStateMixin.tearDown(self)
def test_table_exists_collects_true_results(self):
set_dbms(DBMS.MYSQL)
def _cbe(expression, expectingNone=True):
# initial sanity probe (random table) -> must be False, otherwise the
# function raises SqlmapDataException; then only "users" exists.
return "users" in expression
brute.inject.checkBooleanExpression = _cbe
result = brute.tableExists("/nonexistent/tables.txt")
# cachedTables keyed by conf.db (None here) holds the discovered table
self.assertIn(None, result)
self.assertIn("users", result[None])
self.assertNotIn("logs", result.get(None, []))
# also recorded in kb.brute.tables as (db, table)
self.assertIn((None, "users"), kb.brute.tables)
def test_table_exists_invalid_results_raises(self):
from lib.core.exception import SqlmapDataException
set_dbms(DBMS.MYSQL)
# the initial random-table probe returns True -> "invalid results" guard
brute.inject.checkBooleanExpression = lambda *a, **k: True
with self.assertRaises(SqlmapDataException):
brute.tableExists("/nonexistent/tables.txt")
def test_column_exists_requires_table(self):
from lib.core.exception import SqlmapMissingMandatoryOptionException
set_dbms(DBMS.MYSQL)
conf.tbl = None
# the sanity probe is False so we reach the missing-table guard
brute.inject.checkBooleanExpression = lambda *a, **k: False
with self.assertRaises(SqlmapMissingMandatoryOptionException):
brute.columnExists("/nonexistent/columns.txt")
def test_column_exists_collects_and_types(self):
set_dbms(DBMS.MYSQL)
conf.tbl = "users"
brute.getFileItems = lambda *a, **k: ["id", "name"]
calls = {"n": 0}
def _cbe(expression, expectingNone=True):
calls["n"] += 1
# initial sanity probe uses two random strings (no real column name)
if "id" not in expression and "name" not in expression:
return False
# MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`.
# 'id' is numeric (no non-digit chars => probe False => numeric);
# 'name' is non-numeric (has non-digit chars => probe True => non-numeric).
if "REGEXP" in expression:
return "name" in expression
# plain existence check (EXISTS(SELECT <col> FROM <tbl>)) => both columns exist
return True
brute.inject.checkBooleanExpression = _cbe
result = brute.columnExists("/nonexistent/columns.txt")
self.assertIn(None, result)
cols = result[None]["users"]
# column names are run through safeSQLIdentificatorNaming, so the MySQL
# reserved word "name" comes back backtick-quoted
from lib.core.common import safeSQLIdentificatorNaming, getText
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("id"))), "numeric")
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("name"))), "non-numeric")
def test_add_page_text_words_filters(self):
# restore the real getPageWordSet for this one and drive it directly
brute.getPageWordSet = self._orig_getPageWordSet
kb.originalPage = u"<html>admin password 1abc xy verylongword</html>"
words = brute._addPageTextWords()
# words <= 2 chars or starting with a digit are dropped
self.assertIn("admin", words)
self.assertIn("password", words)
self.assertNotIn("xy", words)
self.assertNotIn("1abc", words)
if __name__ == "__main__":
unittest.main(verbosity=2)

1715
tests/test_common.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,466 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Pure / near-pure parsers and state helpers in lib/core/common.py that are NOT
already exercised by tests/test_common_utils.py.
Covered here:
* proxy-log parsers reached through parseRequestFile()
(_parseBurpLog plain log, _parseBurpLog Burp XML history, _parseWebScarabLog)
* parseTargetDirect() non-smoke branch (driver resolution for SQLite)
* removeReflectiveValues() reflected-payload masking
* findPageForms() HTML <form> and inline JS POST discovery
* saveConfig() .ini serialization
* getSQLSnippet() proc-file loading + variable substitution
* checkSystemEncoding() (no-op on a normal default encoding)
* Format.getOs() fingerprint humanizer
* Backend setters/getters (setOs/getOs, setOsVersion, setOsServicePack,
setVersion/getVersion/setVersionList)
* urlencode() extra branches (LIKE percent-encoding, convall, limit, direct)
* safeStringFormat() extra branches (PAYLOAD_DELIMITER region, scalar percent)
Everything is run in isolation (no network, no DBMS). Any function that
reads/writes global conf/kb/Backend state has that state saved and restored
around the call so test ordering stays irrelevant. Temp files go to the
session scratchpad and are removed.
"""
import os
import sys
import base64
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import (
parseRequestFile,
parseTargetDirect,
removeReflectiveValues,
findPageForms,
saveConfig,
getSQLSnippet,
checkSystemEncoding,
urlencode,
safeStringFormat,
Format,
Backend,
)
from lib.core.data import kb, conf
from lib.core.enums import DBMS, HTTPMETHOD
from lib.core.settings import REFLECTED_VALUE_MARKER, PAYLOAD_DELIMITER
SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad"
def _write_temp(content, suffix):
"""Write `content` (str) to a scratchpad temp file, return its path."""
if not os.path.isdir(SCRATCH):
os.makedirs(SCRATCH)
handle, path = tempfile.mkstemp(suffix=suffix, dir=SCRATCH)
os.write(handle, content.encode("utf-8") if isinstance(content, str) else content)
os.close(handle)
return path
class TestParseRequestFileBurp(unittest.TestCase):
"""_parseBurpLog via parseRequestFile (plain '=====' log + Burp XML history)."""
def setUp(self):
self._scope = conf.scope
self._method = conf.method
self._headers = conf.headers
conf.scope = None
def tearDown(self):
conf.scope = self._scope
conf.method = self._method
conf.headers = self._headers
def test_plain_burp_log_get(self):
content = (
"======================================================\n"
"GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n"
"Host: www.target.com\n"
"Cookie: PHPSESSID=abc\n"
"======================================================\n"
)
path = _write_temp(content, ".log")
try:
targets = list(parseRequestFile(path))
finally:
os.unlink(path)
self.assertEqual(len(targets), 1)
url, method, data, cookie, headers = targets[0]
self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1")
self.assertEqual(method, HTTPMETHOD.GET)
self.assertIsNone(data)
self.assertEqual(cookie, "PHPSESSID=abc")
self.assertIn(("Host", "www.target.com"), headers)
def test_burp_xml_history_base64_request(self):
req = "GET /vuln.php?id=1 HTTP/1.1\r\nHost: www.target.com\r\nCookie: SID=xyz\r\n\r\n"
b64 = base64.b64encode(req.encode()).decode()
xml = ('<items><item><port>80</port>'
'<request base64="true"><![CDATA[%s]]></request>'
'</item></items>' % b64)
path = _write_temp(xml, ".xml")
try:
targets = list(parseRequestFile(path))
finally:
os.unlink(path)
self.assertEqual(len(targets), 1)
url, method, data, cookie, headers = targets[0]
self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1")
self.assertEqual(method, HTTPMETHOD.GET)
self.assertEqual(cookie, "SID=xyz")
def test_post_body_captured(self):
content = (
"======================================================\n"
"POST http://www.target.com:80/login HTTP/1.1\n"
"Host: www.target.com\n"
"Content-Length: 17\n"
"\n"
"user=admin&pw=1\n"
"======================================================\n"
)
path = _write_temp(content, ".log")
try:
targets = list(parseRequestFile(path))
finally:
os.unlink(path)
self.assertEqual(len(targets), 1)
url, method, data, cookie, headers = targets[0]
self.assertEqual(method, HTTPMETHOD.POST)
self.assertEqual(data, "user=admin&pw=1")
def test_scope_filters_out_nonmatching(self):
content = (
"======================================================\n"
"GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n"
"Host: www.target.com\n"
"======================================================\n"
)
path = _write_temp(content, ".log")
try:
conf.scope = r"example\.org" # does not match target.com
targets = list(parseRequestFile(path))
finally:
os.unlink(path)
self.assertEqual(targets, [])
class TestParseRequestFileWebScarab(unittest.TestCase):
"""_parseWebScarabLog via parseRequestFile."""
def setUp(self):
self._scope = conf.scope
conf.scope = None
def tearDown(self):
conf.scope = self._scope
def test_get_conversation(self):
content = (
"### Conversation : 1\n"
"URL: http://www.target.com/vuln.php?id=1\n"
"METHOD: GET\n"
"COOKIE: SID=abc\n"
)
path = _write_temp(content, ".log")
try:
targets = list(parseRequestFile(path))
finally:
os.unlink(path)
self.assertEqual(len(targets), 1)
url, method, data, cookie, headers = targets[0]
self.assertEqual(url, "http://www.target.com/vuln.php?id=1")
self.assertEqual(method, "GET")
self.assertIsNone(data)
self.assertEqual(cookie, "SID=abc")
self.assertEqual(headers, tuple())
def test_post_conversation_skipped(self):
# POST bodies live in separate files -> WebScarab POSTs are skipped
content = (
"### Conversation : 1\n"
"URL: http://www.target.com/login\n"
"METHOD: POST\n"
)
path = _write_temp(content, ".log")
try:
targets = list(parseRequestFile(path))
finally:
os.unlink(path)
self.assertEqual(targets, [])
class TestParseTargetDirectNonSmoke(unittest.TestCase):
"""parseTargetDirect() non-smoke branch: resolves the canonical DBMS name.
Uses SQLite because its driver (stdlib sqlite3) is always importable.
"""
_KEYS = ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")
def setUp(self):
self._saved = {k: conf.get(k) for k in self._KEYS}
self._smoke = kb.smokeMode
self._params_none = conf.parameters.get(None)
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
kb.smokeMode = self._smoke
if self._params_none is None:
conf.parameters.pop(None, None)
else:
conf.parameters[None] = self._params_none
def test_sqlite_local_dsn(self):
kb.smokeMode = False
conf.direct = "sqlite://%s" % os.path.join(SCRATCH, "test.db")
parseTargetDirect()
# non-smoke path canonicalizes the DBMS name via DBMS_DICT
self.assertEqual(conf.dbms, DBMS.SQLITE)
# local file DBMS: hostname forced to localhost, port 0
self.assertEqual(conf.hostname, "localhost")
self.assertEqual(conf.port, 0)
self.assertEqual(conf.parameters[None], "direct connection")
class TestRemoveReflectiveValues(unittest.TestCase):
def setUp(self):
self._mech = kb.reflectiveMechanism
self._heur = kb.heuristicMode
kb.reflectiveMechanism = True
kb.heuristicMode = False
def tearDown(self):
kb.reflectiveMechanism = self._mech
kb.heuristicMode = self._heur
def test_reflected_payload_masked(self):
content = u"<html>You searched for 1 AND 1=2 here</html>"
out = removeReflectiveValues(content, "1 AND 1=2")
self.assertIn(REFLECTED_VALUE_MARKER, out)
self.assertNotIn("AND 1=2", out)
def test_no_reflection_returns_content_unchanged(self):
content = u"<html>nothing interesting</html>"
out = removeReflectiveValues(content, "1 AND 1=2")
self.assertEqual(out, content)
def test_none_payload_returns_content(self):
content = u"<html>x</html>"
self.assertEqual(removeReflectiveValues(content, None), content)
def test_bytes_content_returned_as_is(self):
# non-text content short-circuits (isinstance text_type check)
content = b"<html>1 AND 1=2</html>"
self.assertEqual(removeReflectiveValues(content, "1 AND 1=2"), content)
class TestFindPageForms(unittest.TestCase):
def setUp(self):
self._scope = conf.scope
self._crawlExclude = conf.crawlExclude
self._cookie = conf.cookie
conf.scope = None
conf.crawlExclude = None
conf.cookie = None
def tearDown(self):
conf.scope = self._scope
conf.crawlExclude = self._crawlExclude
conf.cookie = self._cookie
def test_post_form_discovered(self):
html = ('<html><form action="/input.php" method="POST">'
'<input type="text" name="id" value="1">'
'<input type="submit" value="Go"></form></html>')
forms = findPageForms(html, "http://www.site.com")
self.assertEqual(forms, set([("http://www.site.com/input.php", "POST", "id=1", None, None)]))
def test_get_form_discovered(self):
html = ('<html><form action="/search" method="GET">'
'<input type="text" name="q" value="x">'
'<input type="submit" value="Go"></form></html>')
forms = findPageForms(html, "http://www.site.com")
self.assertEqual(len(forms), 1)
url, method, data, _cookie, _ = list(forms)[0]
self.assertEqual(method, "GET")
self.assertIn("q=x", url)
def test_inline_js_post_discovered(self):
# the `.post('url', {k: v})` regex branch (independent of HTML form parsing)
html = "<script>$.post('/api/save', {name: 'foo', id: '1'});</script>"
forms = findPageForms(html, "http://www.site.com")
self.assertTrue(any(m == HTTPMETHOD.POST and u.endswith("/api/save") for (u, m, d, c, e) in forms))
def test_blank_content_returns_empty_set(self):
self.assertEqual(findPageForms("", "http://www.site.com"), set())
class TestSaveConfig(unittest.TestCase):
def test_writes_ini_with_sections(self):
path = _write_temp("", ".ini")
try:
saveConfig(conf, path)
with open(path) as f:
data = f.read()
finally:
os.unlink(path)
# optDict families become [Section] headers
self.assertIn("[Target]", data)
self.assertIn("[Request]", data)
self.assertIn("[Enumeration]", data)
self.assertTrue(len(data) > 0)
class TestGetSQLSnippet(unittest.TestCase):
def test_mssql_proc_loaded(self):
snippet = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate")
self.assertIn("RECONFIGURE", snippet)
def test_variable_substitution(self):
# %VAR% placeholders are substituted from kwargs (here %ENABLE%);
# supplying it avoids the interactive "provide substitution values" prompt.
snippet = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE="1")
self.assertIn("xp_cmdshell", snippet)
self.assertIn("RECONFIGURE", snippet)
# comments (#...) are stripped and the placeholder is fully resolved
self.assertNotIn("#", snippet)
self.assertNotIn("%ENABLE%", snippet)
class TestCheckSystemEncoding(unittest.TestCase):
def test_noop_on_normal_encoding(self):
# On a normal default encoding this is a no-op and must not raise.
self.assertIsNone(checkSystemEncoding())
class TestFormatGetOs(unittest.TestCase):
def setUp(self):
self._api = conf.api
conf.api = False
def tearDown(self):
conf.api = self._api
def test_humanizes_type_and_technology(self):
info = {
"type": set(["Linux"]),
"distrib": set(["Ubuntu"]),
"release": set(["8.10"]),
"technology": set(["PHP 5.2.6", "Apache 2.2.9"]),
}
out = Format.getOs("back-end DBMS", info)
self.assertTrue(out.startswith("back-end DBMS operating system: Linux"))
self.assertIn("Ubuntu", out)
self.assertIn("8.10", out)
self.assertIn("web application technology:", out)
def test_api_mode_returns_dict(self):
orig = conf.api
try:
conf.api = True
info = {"type": set(["Windows"]), "technology": set(["IIS"])}
out = Format.getOs("back-end DBMS", info)
self.assertIsInstance(out, dict)
self.assertIn("web application technology", out)
finally:
conf.api = orig
class TestBackendSetters(unittest.TestCase):
"""Backend OS/version setters write kb state; save and restore it."""
_KEYS = ("os", "osVersion", "osSP", "dbmsVersion")
def setUp(self):
self._saved = {k: kb.get(k) for k in self._KEYS}
def tearDown(self):
for k, v in self._saved.items():
kb[k] = v
def test_set_get_os(self):
kb.os = None
self.assertEqual(Backend.setOs("windows"), "Windows") # capitalized
self.assertEqual(Backend.getOs(), "Windows")
def test_set_os_none_returns_none(self):
self.assertIsNone(Backend.setOs(None))
def test_set_os_version(self):
kb.osVersion = None
Backend.setOsVersion("2008")
self.assertEqual(Backend.getOsVersion(), "2008")
def test_set_os_service_pack(self):
kb.osSP = None
Backend.setOsServicePack(3)
self.assertEqual(Backend.getOsServicePack(), 3)
def test_set_get_version(self):
kb.dbmsVersion = []
self.assertEqual(Backend.setVersion("5.7"), ["5.7"])
self.assertEqual(Backend.getVersion(), "5.7")
def test_set_version_list(self):
kb.dbmsVersion = []
Backend.setVersionList(["8.0", "8.1"])
self.assertEqual(Backend.getVersionList(), ["8.0", "8.1"])
class TestUrlencodeExtraBranches(unittest.TestCase):
def test_like_percent_encoded(self):
# '%' inside a LIKE '...' literal is encoded to %25
self.assertEqual(urlencode("AND name LIKE '%DBA%'"),
"AND%20name%20LIKE%20%27%25DBA%25%27")
def test_convall_drops_safe_set(self):
self.assertEqual(urlencode("a&b", convall=True), "a%26b")
def test_limit_does_not_crash_on_long_input(self):
out = urlencode("x " * 4000, limit=True)
self.assertTrue(len(out) > 0)
def test_direct_mode_returns_value_unchanged(self):
orig = conf.direct
try:
conf.direct = "mysql://u:p@h:3306/d"
self.assertEqual(urlencode("a b"), "a b")
finally:
conf.direct = orig
class TestSafeStringFormatExtraBranches(unittest.TestCase):
def test_percent_d_in_payload_region_becomes_string(self):
fmt = "SELECT %s" + PAYLOAD_DELIMITER + " AND %d " + PAYLOAD_DELIMITER
self.assertEqual(
safeStringFormat(fmt, ("a", "5")),
"SELECT a" + PAYLOAD_DELIMITER + " AND 5 " + PAYLOAD_DELIMITER)
def test_scalar_string_percent_preserved(self):
# single-string param path: plain replace, embedded '%' survives
self.assertEqual(safeStringFormat("LIKE %s", "100%done"), "LIKE 100%done")
def test_two_params_list(self):
self.assertEqual(safeStringFormat("%s/%s", ("a", "b")), "a/b")
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -1,340 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Pure / near-pure helpers in lib/core/common.py.
These cover the request/parameter parsing, charset construction, limit-range
generation, safe string formatting, URL encoding, UNION page parsing, target
URL/direct-connection parsing and SQL identifier quoting. They are exercised
in isolation (no network, no DBMS, no filesystem mutation); any function that
reads/writes global conf/kb state has that state saved and restored around the
call so test ordering stays irrelevant.
"""
import os
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.common import (
paramToDict,
getCharset,
getLimitRange,
parseUnionPage,
safeStringFormat,
urlencode,
parseTargetUrl,
parseTargetDirect,
safeSQLIdentificatorNaming,
getPartRun,
getText,
)
from lib.core.data import kb, conf
from lib.core.enums import PLACE, CHARSET_TYPE, DBMS
class TestParamToDict(unittest.TestCase):
"""Parameter string -> OrderedDict for the various injection places."""
def test_get_two_params(self):
result = paramToDict(PLACE.GET, "id=1&name=foo")
self.assertEqual(list(result.items()), [("id", "1"), ("name", "foo")])
def test_get_preserves_order(self):
result = paramToDict(PLACE.GET, "c=3&a=1&b=2")
self.assertEqual(list(result.keys()), ["c", "a", "b"])
def test_post_place(self):
result = paramToDict(PLACE.POST, "user=admin&pass=secret")
self.assertEqual(result["user"], "admin")
self.assertEqual(result["pass"], "secret")
def test_empty_value(self):
result = paramToDict(PLACE.GET, "id=&name=x")
self.assertEqual(result["id"], "")
self.assertEqual(result["name"], "x")
def test_value_with_equal_signs(self):
# value is re-joined on '=' so embedded '=' survives
result = paramToDict(PLACE.GET, "token=a=b=c")
self.assertEqual(result["token"], "a=b=c")
def test_cookie_delimiter(self):
# COOKIE place splits on ';' rather than '&'
result = paramToDict(PLACE.COOKIE, "foo=bar;baz=qux")
self.assertEqual(list(result.items()), [("foo", "bar"), ("baz", "qux")])
def test_param_without_equals_ignored(self):
# an element with no '=' has len(parts) < 2 and is skipped
result = paramToDict(PLACE.GET, "lonely&id=1")
self.assertEqual(list(result.items()), [("id", "1")])
class TestGetCharset(unittest.TestCase):
"""Inference charsets are fixed integer tables."""
def test_binary(self):
self.assertEqual(getCharset(CHARSET_TYPE.BINARY), [0, 1, 47, 48, 49])
def test_default_is_full_ascii(self):
self.assertEqual(getCharset(None), list(range(0, 128)))
def test_digits(self):
result = getCharset(CHARSET_TYPE.DIGITS)
self.assertEqual(result, list(range(0, 10)) + list(range(47, 58)))
def test_alpha_has_no_digits(self):
result = getCharset(CHARSET_TYPE.ALPHA)
# ASCII codes for '0'..'9' are 48..57; ALPHA must exclude them
self.assertFalse(any(48 <= _ <= 57 for _ in result))
self.assertIn(ord("A"), result)
self.assertIn(ord("z"), result)
def test_alphanum_superset_of_alpha(self):
alpha = set(getCharset(CHARSET_TYPE.ALPHA))
alphanum = set(getCharset(CHARSET_TYPE.ALPHANUM))
self.assertTrue(alpha.issubset(alphanum))
self.assertIn(ord("5"), alphanum)
def test_hexadecimal_contains_hex_letters(self):
result = getCharset(CHARSET_TYPE.HEXADECIMAL)
for ch in "0123456789abcdefABCDEF":
self.assertIn(ord(ch), result, msg="missing %r" % ch)
class TestGetLimitRange(unittest.TestCase):
def test_basic(self):
self.assertEqual(list(getLimitRange(10)), list(range(0, 10)))
def test_plus_one(self):
self.assertEqual(list(getLimitRange(3, plusOne=True)), [1, 2, 3])
def test_string_count_coerced(self):
# count is int()-coerced internally
self.assertEqual(list(getLimitRange("4")), [0, 1, 2, 3])
def test_length(self):
self.assertEqual(len(getLimitRange(7)), 7)
class TestParseUnionPage(unittest.TestCase):
def test_none(self):
self.assertIsNone(parseUnionPage(None))
def test_two_entries(self):
page = "%sfoo%s%sbar%s" % (kb.chars.start, kb.chars.stop, kb.chars.start, kb.chars.stop)
# returns a BigArray; compare element-wise
self.assertEqual(list(parseUnionPage(page)), ["foo", "bar"])
def test_single_entry_unwrapped(self):
# a lone wrapped string is returned as the bare string, not a 1-element list
page = "%shello%s" % (kb.chars.start, kb.chars.stop)
self.assertEqual(parseUnionPage(page), "hello")
def test_multi_column_row(self):
# a single row whose values are joined by kb.chars.delimiter becomes one
# nested list entry
page = "%sa%sb%s" % (kb.chars.start, kb.chars.delimiter, kb.chars.stop)
self.assertEqual(list(parseUnionPage(page)), [["a", "b"]])
def test_unmarked_page_returned_verbatim(self):
self.assertEqual(parseUnionPage("no markers here"), "no markers here")
class TestSafeStringFormat(unittest.TestCase):
def test_basic_tuple(self):
self.assertEqual(safeStringFormat("SELECT foo FROM %s LIMIT %d", ("bar", "1")),
"SELECT foo FROM bar LIMIT 1")
def test_literal_percent_preserved(self):
self.assertEqual(
safeStringFormat("SELECT foo FROM %s WHERE name LIKE '%susan%' LIMIT %d", ("bar", "1")),
"SELECT foo FROM bar WHERE name LIKE '%susan%' LIMIT 1")
def test_single_string_param(self):
self.assertEqual(safeStringFormat("a %s b", "X"), "a X b")
def test_scalar_non_string(self):
self.assertEqual(safeStringFormat("n=%d", 5), "n=5")
class TestUrlencode(unittest.TestCase):
def test_basic(self):
self.assertEqual(urlencode("AND 1>(2+3)#"), "AND%201%3E%282%2B3%29%23")
def test_none(self):
self.assertIsNone(urlencode(None))
def test_spaceplus(self):
self.assertEqual(urlencode("a b", spaceplus=True), "a+b")
def test_convall_encodes_safe_chars(self):
# with convall the explicit 'safe' set is dropped, so '/' gets encoded
self.assertEqual(urlencode("a/b", convall=True), "a%2Fb")
def test_safe_char_default_kept(self):
# by default '-' and '_' are in the safe set
self.assertEqual(urlencode("a-b_c"), "a-b_c")
class TestParseTargetUrl(unittest.TestCase):
"""parseTargetUrl mutates conf.* in place; save and restore everything touched."""
def _save(self):
return {k: conf.get(k) for k in
("url", "scheme", "path", "hostname", "port", "ipv6")}
def _restore(self, saved):
for k, v in saved.items():
conf[k] = v
def test_https_url(self):
saved = self._save()
orig_params = conf.parameters.get(PLACE.GET)
try:
conf.url = "https://www.test.com/?id=1"
parseTargetUrl()
self.assertEqual(conf.hostname, "www.test.com")
self.assertEqual(conf.scheme, "https")
self.assertEqual(conf.port, 443)
self.assertEqual(conf.parameters[PLACE.GET], "id=1")
finally:
self._restore(saved)
if orig_params is None:
conf.parameters.pop(PLACE.GET, None)
else:
conf.parameters[PLACE.GET] = orig_params
def test_scheme_defaulted_and_port(self):
saved = self._save()
try:
conf.url = "example.org:8080/app"
parseTargetUrl()
self.assertEqual(conf.hostname, "example.org")
self.assertEqual(conf.scheme, "http")
self.assertEqual(conf.port, 8080)
finally:
self._restore(saved)
def test_empty_url_returns_none(self):
saved = self._save()
try:
conf.url = ""
self.assertIsNone(parseTargetUrl())
finally:
self._restore(saved)
class TestParseTargetDirect(unittest.TestCase):
"""parseTargetDirect under smokeMode (early-returns before driver imports)."""
def _save(self):
return {k: conf.get(k) for k in
("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")}
def _restore(self, saved):
for k, v in saved.items():
conf[k] = v
def test_full_mysql_dsn(self):
saved = self._save()
orig_smoke = kb.smokeMode
orig_none = conf.parameters.get(None)
try:
kb.smokeMode = True
conf.direct = "mysql://root:testpass@127.0.0.1:3306/testdb"
parseTargetDirect()
self.assertEqual(conf.dbms, "mysql")
self.assertEqual(conf.dbmsUser, "root")
self.assertEqual(conf.dbmsPass, "testpass")
self.assertEqual(conf.dbmsDb, "testdb")
self.assertEqual(conf.hostname, "127.0.0.1")
self.assertEqual(conf.port, 3306)
finally:
self._restore(saved)
kb.smokeMode = orig_smoke
if orig_none is None:
conf.parameters.pop(None, None)
else:
conf.parameters[None] = orig_none
def test_quoted_password(self):
saved = self._save()
orig_smoke = kb.smokeMode
orig_none = conf.parameters.get(None)
try:
kb.smokeMode = True
conf.direct = "mysql://user:'P@ssw0rd'@127.0.0.1:3306/test"
parseTargetDirect()
self.assertEqual(conf.dbmsPass, "P@ssw0rd")
self.assertEqual(conf.hostname, "127.0.0.1")
finally:
self._restore(saved)
kb.smokeMode = orig_smoke
if orig_none is None:
conf.parameters.pop(None, None)
else:
conf.parameters[None] = orig_none
def test_empty_direct_returns_none(self):
saved = self._save()
try:
conf.direct = None
self.assertIsNone(parseTargetDirect())
finally:
self._restore(saved)
class TestSafeSQLIdentificatorNaming(unittest.TestCase):
"""Quoting of identifiers is DBMS-specific; drive it via kb.forcedDbms."""
def _run(self, dbms, name, **kw):
orig = kb.forcedDbms
try:
kb.forcedDbms = dbms
return getText(safeSQLIdentificatorNaming(name, **kw))
finally:
kb.forcedDbms = orig
def test_mssql_keyword_bracketed(self):
self.assertEqual(self._run(DBMS.MSSQL, "begin"), "[begin]")
def test_plain_name_unquoted(self):
self.assertEqual(self._run(DBMS.MSSQL, "foobar"), "foobar")
def test_firebird_name_with_space_double_quoted(self):
self.assertEqual(self._run(DBMS.FIREBIRD, "foo bar"), '"foo bar"')
def test_mysql_keyword_backticked(self):
self.assertEqual(self._run(DBMS.MYSQL, "select"), "`select`")
def test_oracle_keyword_uppercased(self):
# Oracle quotes AND uppercases reserved words
self.assertEqual(self._run(DBMS.ORACLE, "table"), '"TABLE"')
def test_unsafe_naming_passthrough(self):
orig = conf.unsafeNaming
try:
conf.unsafeNaming = True
self.assertEqual(self._run(DBMS.MYSQL, "select"), "select")
finally:
conf.unsafeNaming = orig
class TestGetPartRun(unittest.TestCase):
def test_no_dbms_handler_in_stack(self):
# called from a test (no conf.dbmsHandler.* on the stack) -> None
self.assertIsNone(getPartRun())
def test_non_alias_form_also_none(self):
self.assertIsNone(getPartRun(alias=False))
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -21,8 +21,7 @@ bootstrap()
from lib.core.compat import (WichmannHill, patchHeaders, cmp, choose_boundary,
round, cmp_to_key, LooseVersion, _is_write_mode,
MixedWriteTextIO, _codecs_open, codecs_open)
from lib.core.compat import xrange
MixedWriteTextIO, _codecs_open)
class TestWichmannHill(unittest.TestCase):

View file

@ -1,676 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional REAL unit coverage for genuinely-uncovered PURE functions in:
* lib/core/common.py
* lib/core/option.py
* lib/core/agent.py
* lib/request/basic.py
Every test asserts a concrete, independently-reasoned known-correct value that
would FAIL if the function under test regressed. No isinstance-only checks, no
tautologies, no swallowed exceptions.
Functions targeted here are deliberately DIFFERENT from those already exercised
by tests/test_common_utils.py, test_common_parsers.py, test_core_more.py,
test_core_final.py, test_option_setup.py, test_option_more.py, test_agent.py,
test_agent_dialects.py, test_decodepage.py and test_charset.py.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from tests._testutils import bootstrap, set_dbms
bootstrap()
from lib.core.data import conf, kb
from lib.core.defaults import defaults
from lib.core.common import Backend
from lib.core.enums import DBMS
class TestCommonStringHelpers(unittest.TestCase):
"""Small pure string/list/regex/encoding helpers in lib/core/common.py."""
def test_posix_to_nt_slashes(self):
from lib.core.common import posixToNtSlashes
self.assertEqual(posixToNtSlashes("C:/Windows"), "C:\\Windows")
self.assertEqual(posixToNtSlashes("a/b/c"), "a\\b\\c")
# falsy input returned unchanged
self.assertEqual(posixToNtSlashes(""), "")
self.assertIsNone(posixToNtSlashes(None))
def test_nt_to_posix_slashes(self):
from lib.core.common import ntToPosixSlashes
self.assertEqual(ntToPosixSlashes("C:\\Windows"), "C:/Windows")
self.assertEqual(ntToPosixSlashes("a\\b\\c"), "a/b/c")
self.assertEqual(ntToPosixSlashes(""), "")
def test_is_hex_encoded_string(self):
from lib.core.common import isHexEncodedString
self.assertTrue(isHexEncodedString("DEADBEEF"))
self.assertTrue(isHexEncodedString("0x1234")) # 'x' is allowed by the regex
self.assertFalse(isHexEncodedString("test"))
self.assertFalse(isHexEncodedString("12 34")) # space breaks it
def test_is_digit(self):
from lib.core.common import isDigit
self.assertTrue(isDigit("123456"))
self.assertFalse(isDigit("3b3"))
self.assertFalse(isDigit(u"\xb2")) # superscript-2: str.isdigit() True, isDigit False
self.assertFalse(isDigit("")) # empty -> no match
self.assertFalse(isDigit(None))
def test_sanitize_str(self):
from lib.core.common import sanitizeStr
self.assertEqual(sanitizeStr("foo\n\rbar"), "foo bar")
self.assertEqual(sanitizeStr("a\r\nb"), "a b")
self.assertEqual(sanitizeStr(None), "None")
def test_filter_control_chars(self):
from lib.core.common import filterControlChars
self.assertEqual(filterControlChars("AND 1>(2+3)\n--"), "AND 1>(2+3) --")
# custom replacement character
self.assertEqual(filterControlChars("a\tb", replacement="_"), "a_b")
def test_normalize_path(self):
from lib.core.common import normalizePath
self.assertEqual(normalizePath("//var///log/apache.log"), "/var/log/apache.log")
self.assertEqual(normalizePath("/a/b/../c"), "/a/c")
def test_directory_path(self):
from lib.core.common import directoryPath
self.assertEqual(directoryPath("/var/log/apache.log"), "/var/log")
# no extension -> returned unchanged
self.assertEqual(directoryPath("/var/log"), "/var/log")
def test_longest_common_prefix(self):
from lib.core.common import longestCommonPrefix
self.assertEqual(longestCommonPrefix("foobar", "fobar"), "fo")
self.assertEqual(longestCommonPrefix("abc", "abd", "abe"), "ab")
# single sequence returned verbatim
self.assertEqual(longestCommonPrefix("only"), "only")
def test_first_not_none(self):
from lib.core.common import firstNotNone
self.assertEqual(firstNotNone(None, None, 1, 2, 3), 1)
self.assertEqual(firstNotNone(None, 0), 0) # 0 is not None
self.assertIsNone(firstNotNone(None, None))
def test_decode_string_escape(self):
from lib.core.common import decodeStringEscape
self.assertEqual(decodeStringEscape("a\\tb"), "a\tb")
self.assertEqual(decodeStringEscape("a\\nb"), "a\nb")
# no backslash -> unchanged
self.assertEqual(decodeStringEscape("plain"), "plain")
def test_encode_string_escape(self):
from lib.core.common import encodeStringEscape
self.assertEqual(encodeStringEscape("a\tb"), "a\\tb")
self.assertEqual(encodeStringEscape("a\nb"), "a\\nb")
self.assertEqual(encodeStringEscape("plain"), "plain")
def test_decode_encode_string_escape_roundtrip(self):
from lib.core.common import decodeStringEscape, encodeStringEscape
self.assertEqual(decodeStringEscape(encodeStringEscape("x\ty\nz")), "x\ty\nz")
def test_escape_json_value(self):
from lib.core.common import escapeJsonValue
# newline gets escaped (literal '\n' becomes the two chars backslash+n)
self.assertNotIn("\n", escapeJsonValue("foo\nbar"))
self.assertIn("\\n", escapeJsonValue("foo\nbar"))
# tab gets escaped to '\t'
self.assertIn("\\t", escapeJsonValue("foo\tbar"))
# quote and backslash escaped
self.assertEqual(escapeJsonValue('a"b'), 'a\\"b')
self.assertEqual(escapeJsonValue("a\\b"), "a\\\\b")
# ordinary characters untouched
self.assertEqual(escapeJsonValue("plain text"), "plain text")
def test_clean_query(self):
from lib.core.common import cleanQuery
self.assertEqual(cleanQuery("select id from users"), "SELECT id FROM users")
# already-uppercase keywords stay; identifiers untouched
self.assertEqual(cleanQuery("SELECT a FROM t"), "SELECT a FROM t")
def test_json_minimize_canonical(self):
from lib.core.common import jsonMinimize
# key order / whitespace independence
self.assertEqual(jsonMinimize('{"b": 2, "a": 1}'), jsonMinimize('{"a":1, "b":2}'))
# nested leaf path
self.assertEqual(jsonMinimize('{"a": {"b": 1}}'), ".a.b=1")
# empty object
self.assertEqual(jsonMinimize("{}"), "")
# not parseable -> None (and only None)
self.assertIsNone(jsonMinimize("not json"))
def test_json_minimize_array_length_registers(self):
from lib.core.common import jsonMinimize
# array length change must perturb the projection
self.assertNotEqual(jsonMinimize('{"a": [1, 2]}'), jsonMinimize('{"a": [1, 2, 3]}'))
def test_list_to_str_value(self):
from lib.core.common import listToStrValue
self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3")
# set/tuple/generator normalized via list first
self.assertEqual(listToStrValue((1, 2)), "1, 2")
# non-list passes through
self.assertEqual(listToStrValue("abc"), "abc")
def test_intersect(self):
from lib.core.common import intersect
self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3])
# order follows containerA
self.assertEqual(intersect([3, 2, 1], [1, 2]), [2, 1])
# case-insensitive option
self.assertEqual(intersect(["FOO", "bar"], ["foo"], lowerCase=True), ["foo"])
def test_priority_sort_columns(self):
from lib.core.common import prioritySortColumns
# 'id'-containing columns first, then by ascending length
self.assertEqual(
prioritySortColumns(["password", "userid", "name", "id"]),
["id", "userid", "name", "password"],
)
def test_safe_variable_naming(self):
from lib.core.common import safeVariableNaming
self.assertEqual(safeVariableNaming("class.id"), "EVAL_636c6173732e6964")
# plain identifier left untouched
self.assertEqual(safeVariableNaming("foobar"), "foobar")
def test_unsafe_variable_naming(self):
from lib.core.common import unsafeVariableNaming
self.assertEqual(unsafeVariableNaming("EVAL_636c6173732e6964"), "class.id")
self.assertEqual(unsafeVariableNaming("foobar"), "foobar")
def test_variable_naming_roundtrip(self):
from lib.core.common import safeVariableNaming, unsafeVariableNaming
self.assertEqual(unsafeVariableNaming(safeVariableNaming("a-b")), "a-b")
def test_average(self):
from lib.core.common import average
self.assertAlmostEqual(average([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), 0.9, places=6)
self.assertEqual(average([2, 4]), 3.0)
self.assertIsNone(average([]))
def test_stdev(self):
from lib.core.common import stdev
self.assertEqual("%.3f" % stdev([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), "0.063")
# fewer than 2 values -> None
self.assertIsNone(stdev([1.0]))
self.assertIsNone(stdev([]))
class TestCommonSafeCompare(unittest.TestCase):
"""Constant-time / checksum helpers."""
def test_safe_compare_strings(self):
from lib.core.common import safeCompareStrings
self.assertTrue(safeCompareStrings("test", "test"))
self.assertFalse(safeCompareStrings("test1", "test2"))
self.assertFalse(safeCompareStrings("test", None))
# both None compares equal (a == b path)
self.assertTrue(safeCompareStrings(None, None))
def test_safe_cs_value(self):
from lib.core.common import safeCSValue
# ensure deterministic delimiter
old = conf.get("csvDel")
conf.csvDel = defaults.csvDel
try:
self.assertEqual(safeCSValue("foo, bar"), '"foo, bar"')
self.assertEqual(safeCSValue("foobar"), "foobar")
self.assertEqual(safeCSValue("foo\rbar"), '"foo\rbar"')
self.assertEqual(safeCSValue('foo"bar'), '"foo""bar"')
finally:
conf.csvDel = old
class TestCommonSafeExString(unittest.TestCase):
def test_sqlmap_exception_message(self):
from lib.core.common import getSafeExString
from lib.core.exception import SqlmapBaseException
self.assertEqual(getSafeExString(SqlmapBaseException("foobar")), "foobar")
def test_oserror_prefixed_with_type(self):
from lib.core.common import getSafeExString
self.assertEqual(getSafeExString(OSError(0, "foobar")), "OSError: foobar")
def test_generic_value_error(self):
from lib.core.common import getSafeExString
self.assertEqual(getSafeExString(ValueError("bad input")), "ValueError: bad input")
class TestCommonHostHeader(unittest.TestCase):
def test_plain_host(self):
from lib.core.common import getHostHeader
self.assertEqual(getHostHeader("http://www.target.com/vuln.php?id=1"), "www.target.com")
def test_default_port_stripped(self):
from lib.core.common import getHostHeader
self.assertEqual(getHostHeader("http://www.target.com:80/x"), "www.target.com")
self.assertEqual(getHostHeader("https://www.target.com:443/x"), "www.target.com")
def test_nondefault_port_kept(self):
from lib.core.common import getHostHeader
self.assertEqual(getHostHeader("http://www.target.com:8080/x"), "www.target.com:8080")
def test_ipv6_brackets(self):
from lib.core.common import getHostHeader
self.assertEqual(getHostHeader("http://[::1]:8080/vuln.php?id=1"), "[::1]:8080")
self.assertEqual(getHostHeader("http://[::1]/vuln.php?id=1"), "[::1]")
class TestCommonCheckSameHost(unittest.TestCase):
def test_same_host(self):
from lib.core.common import checkSameHost
self.assertTrue(checkSameHost(
"http://www.target.com/page1.php?id=1",
"http://www.target.com/images/page2.php",
))
def test_different_host(self):
from lib.core.common import checkSameHost
self.assertFalse(checkSameHost(
"http://www.target.com/page1.php?id=1",
"http://www.target2.com/images/page2.php",
))
def test_www_prefix_ignored(self):
from lib.core.common import checkSameHost
# leading 'www.' is stripped before comparison
self.assertTrue(checkSameHost("http://www.target.com/a", "http://target.com/b"))
def test_single_url_true_and_empty_none(self):
from lib.core.common import checkSameHost
self.assertTrue(checkSameHost("http://only.com/a"))
self.assertIsNone(checkSameHost())
class TestCommonUrldecode(unittest.TestCase):
def test_convall_true(self):
from lib.core.common import urldecode
self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=True), "AND 1>(2+3)#")
def test_convall_false_keeps_unsafe(self):
from lib.core.common import urldecode
# %2B (plus) is in the default 'unsafe' set so it stays encoded when convall=False
self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#")
def test_bytes_input(self):
from lib.core.common import urldecode
self.assertEqual(urldecode(b"AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#")
def test_spaceplus(self):
from lib.core.common import urldecode
# with spaceplus the '+' becomes a space
self.assertEqual(urldecode("a+b", convall=False, spaceplus=True), "a b")
# without spaceplus the '+' stays
self.assertEqual(urldecode("a+b", convall=False, spaceplus=False), "a+b")
class TestCommonChunkSplit(unittest.TestCase):
def test_chunk_split_post_data(self):
import random
from lib.core.common import chunkSplitPostData
from lib.core.patch import unisonRandom
# The pinned docstring value is produced under sqlmap's cross-version PRNG; install it
# (then restore the stdlib functions) so the expectation is deterministic here too.
_saved = (random.choice, random.randint, random.sample, random.seed)
unisonRandom()
try:
random.seed(0)
expected = ('5;4Xe90\r\nSELEC\r\n3;irWlc\r\nT u\r\n1;eT4zO\r\ns\r\n'
'5;YB4hM\r\nernam\r\n9;2pUD8\r\ne,passwor\r\n3;mp07y\r\nd F\r\n'
'5;8RKXi\r\nROM u\r\n4;MvMhO\r\nsers\r\n0\r\n\r\n')
self.assertEqual(chunkSplitPostData("SELECT username,password FROM users"), expected)
finally:
random.choice, random.randint, random.sample, random.seed = _saved
def test_chunk_split_terminator(self):
import random
from lib.core.common import chunkSplitPostData
random.seed(123)
# regardless of content, the chunked stream must end with the zero-length terminator
self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n"))
class TestCommonDecodeIntToUnicode(unittest.TestCase):
def tearDown(self):
set_dbms(None)
def test_basic_ascii(self):
from lib.core.common import decodeIntToUnicode
self.assertEqual(decodeIntToUnicode(35), "#")
self.assertEqual(decodeIntToUnicode(64), "@")
self.assertEqual(decodeIntToUnicode(65), "A")
def test_non_int_passthrough(self):
from lib.core.common import decodeIntToUnicode
# non-int is returned unchanged
self.assertEqual(decodeIntToUnicode("x"), "x")
def test_pgsql_high_codepoint(self):
from lib.core.common import decodeIntToUnicode
set_dbms(DBMS.PGSQL)
# value > 255 on PGSQL takes the _unichr(value) branch
self.assertEqual(decodeIntToUnicode(0x2122), u"")
class TestCommonDecodeDbmsHex(unittest.TestCase):
def setUp(self):
self._old_binary = kb.binaryField
kb.binaryField = False
def tearDown(self):
kb.binaryField = self._old_binary
set_dbms(None)
def test_plain_hex(self):
from lib.core.common import decodeDbmsHexValue
self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1")
def test_odd_length_appends_question_mark(self):
from lib.core.common import decodeDbmsHexValue
self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?")
def test_list_input(self):
from lib.core.common import decodeDbmsHexValue
self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"])
def test_non_hex_passthrough(self):
from lib.core.common import decodeDbmsHexValue
self.assertEqual(decodeDbmsHexValue("5.1.41"), u"5.1.41")
class TestCommonUnsafeSQLIdentificator(unittest.TestCase):
def tearDown(self):
set_dbms(None)
def test_mssql_brackets(self):
from lib.core.common import unsafeSQLIdentificatorNaming
from lib.core.common import getText
set_dbms(DBMS.MSSQL)
self.assertEqual(getText(unsafeSQLIdentificatorNaming("[begin]")), "begin")
self.assertEqual(getText(unsafeSQLIdentificatorNaming("foobar")), "foobar")
def test_mysql_backticks(self):
from lib.core.common import unsafeSQLIdentificatorNaming, getText
set_dbms(DBMS.MYSQL)
self.assertEqual(getText(unsafeSQLIdentificatorNaming("`col`")), "col")
def test_oracle_uppercases(self):
from lib.core.common import unsafeSQLIdentificatorNaming, getText
set_dbms(DBMS.ORACLE)
# Oracle strips double quotes and uppercases
self.assertEqual(getText(unsafeSQLIdentificatorNaming('"name"')), "NAME")
class TestCommonParseSqliteSchema(unittest.TestCase):
def setUp(self):
self._old_cached = kb.data.get("cachedColumns")
self._old_db = conf.db
self._old_tbl = conf.tbl
kb.data.cachedColumns = {}
conf.db = "SQLITE_MASTER"
conf.tbl = "users"
def tearDown(self):
kb.data.cachedColumns = self._old_cached
conf.db = self._old_db
conf.tbl = self._old_tbl
def test_simple_schema(self):
from lib.core.common import parseSqliteTableSchema
self.assertTrue(parseSqliteTableSchema(
"CREATE TABLE users(\n\t\tid INTEGER,\n\t\tname TEXT\n);"))
cols = kb.data.cachedColumns[conf.db][conf.tbl]
self.assertEqual(tuple(cols.items()), (("id", "INTEGER"), ("name", "TEXT")))
def test_constraints_skipped(self):
from lib.core.common import parseSqliteTableSchema
self.assertTrue(parseSqliteTableSchema(
"CREATE TABLE suppliers(\n\tsupplier_id INTEGER PRIMARY KEY DESC,\n\tname TEXT NOT NULL\n);"))
cols = kb.data.cachedColumns[conf.db][conf.tbl]
self.assertEqual(tuple(cols.items()), (("supplier_id", "INTEGER"), ("name", "TEXT")))
class TestAgentPure(unittest.TestCase):
"""Pure agent.py methods independent of full injection state."""
@classmethod
def setUpClass(cls):
from lib.core.agent import agent
cls.agent = agent
def tearDown(self):
set_dbms(None)
def test_get_comment_present(self):
from lib.core.datatype import AttribDict
request = AttribDict()
request.comment = "-- foo"
self.assertEqual(self.agent.getComment(request), "-- foo")
def test_get_comment_absent(self):
from lib.core.datatype import AttribDict
request = AttribDict()
self.assertEqual(self.agent.getComment(request), "")
def test_add_payload_delimiters(self):
from lib.core.settings import PAYLOAD_DELIMITER
value = "1 AND 1=1"
result = self.agent.addPayloadDelimiters(value)
self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER))
# falsy value returned unchanged
self.assertEqual(self.agent.addPayloadDelimiters(""), "")
def test_remove_payload_delimiters_roundtrip(self):
self.assertEqual(
self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")),
"1 AND 1=1",
)
def test_extract_payload(self):
wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix"
self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1")
def test_replace_payload(self):
wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix"
replaced = self.agent.replacePayload(wrapped, "NEW")
self.assertEqual(self.agent.extractPayload(replaced), "NEW")
# surrounding text preserved
self.assertTrue(replaced.startswith("prefix"))
self.assertTrue(replaced.endswith("suffix"))
def test_simple_concatenate_mysql(self):
set_dbms(DBMS.MYSQL)
# MySQL concatenate query template is 'CONCAT(%s,%s)'
self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)")
def test_hex_convert_field_mysql(self):
set_dbms(DBMS.MYSQL)
# MySQL hex template is 'HEX(%s)'
self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)")
def test_get_fields_select_from(self):
set_dbms(DBMS.MYSQL)
result = self.agent.getFields("SELECT a, b FROM users")
fieldsToCastList = result[5]
fieldsToCastStr = result[6]
self.assertEqual(fieldsToCastStr, "a, b")
self.assertEqual(fieldsToCastList, ["a", "b"])
def test_get_fields_no_from(self):
set_dbms(DBMS.MYSQL)
# a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list
result = self.agent.getFields("SELECT 1")
fieldsSelectFrom = result[0]
self.assertIsNone(fieldsSelectFrom)
self.assertEqual(result[6], "1")
class TestAgentWhereQuery(unittest.TestCase):
@classmethod
def setUpClass(cls):
from lib.core.agent import agent
cls.agent = agent
def setUp(self):
self._old_dumpWhere = conf.dumpWhere
self._old_tbl = conf.tbl
conf.tbl = None
def tearDown(self):
conf.dumpWhere = self._old_dumpWhere
conf.tbl = self._old_tbl
set_dbms(None)
def test_no_dumpwhere_passthrough(self):
conf.dumpWhere = None
query = "SELECT a FROM t"
self.assertEqual(self.agent.whereQuery(query), query)
def test_appends_where_clause(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>0"
# no existing WHERE -> appends ' WHERE id>0'
self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0")
def test_and_when_where_present(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>0"
# existing WHERE -> appended with AND
self.assertEqual(
self.agent.whereQuery("SELECT a FROM t WHERE x=1"),
"SELECT a FROM t WHERE x=1 AND id>0",
)
def test_splices_before_order_by(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>0"
# WHERE must be spliced before the trailing ORDER BY suffix
self.assertEqual(
self.agent.whereQuery("SELECT a FROM t ORDER BY a"),
"SELECT a FROM t WHERE id>0 ORDER BY a",
)
class TestBasicHeuristicCharEncoding(unittest.TestCase):
def test_ascii(self):
from lib.request.basic import getHeuristicCharEncoding
self.assertEqual(getHeuristicCharEncoding(b"<html></html>"), "ascii")
def test_cache_hit_returns_same(self):
from lib.request.basic import getHeuristicCharEncoding
page = b"<html>hello world</html>"
first = getHeuristicCharEncoding(page)
# second call for identical page must come back identical (and from cache)
self.assertEqual(getHeuristicCharEncoding(page), first)
key = (len(page), hash(page))
self.assertEqual(kb.cache.encoding.get(key), first)
class TestBasicDecodePage(unittest.TestCase):
"""decodePage charset + HTML-entity decoding branches."""
def setUp(self):
self._old_encoding = conf.encoding
self._old_null = conf.nullConnection
conf.nullConnection = False
def tearDown(self):
conf.encoding = self._old_encoding
conf.nullConnection = self._old_null
def test_html_entity_amp(self):
from lib.request.basic import decodePage
from lib.core.common import getText
conf.encoding = None
self.assertEqual(
getText(decodePage(b"<html>foo&amp;bar</html>", None, "text/html; charset=utf-8")),
"<html>foo&bar</html>",
)
def test_numeric_hex_entity_tab(self):
from lib.request.basic import decodePage
from lib.core.common import getText
conf.encoding = None
self.assertEqual(getText(decodePage(b"&#x9;", None, "text/html; charset=utf-8")), "\t")
def test_numeric_hex_entity_letter(self):
from lib.request.basic import decodePage
from lib.core.common import getText
conf.encoding = None
self.assertEqual(getText(decodePage(b"&#x4A;", None, "text/html; charset=utf-8")), "J")
def test_unicode_entity(self):
from lib.request.basic import decodePage
conf.encoding = None
self.assertEqual(decodePage(b"&#x2122;", None, "text/html; charset=utf-8"), u"")
def test_empty_page(self):
from lib.request.basic import decodePage
from lib.core.common import getText
# empty page short-circuits to getUnicode(page)
self.assertEqual(getText(decodePage(b"", None, "text/html")), "")
class TestOptionSetPrefixSuffix(unittest.TestCase):
"""_setPrefixSuffix boundary construction (pure conf-mutation, no I/O)."""
def setUp(self):
self._saved = {k: conf.get(k) for k in ("prefix", "suffix", "boundaries")}
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
def _run(self, prefix, suffix):
from lib.core.option import _setPrefixSuffix
conf.prefix = prefix
conf.suffix = suffix
conf.boundaries = None
_setPrefixSuffix()
return conf.boundaries
def test_none_no_boundary(self):
# when either prefix or suffix is None, no boundary is created
self.assertIsNone(self._run(None, None))
def test_single_quote_ptype(self):
boundaries = self._run("' AND ", "'")
self.assertEqual(len(boundaries), 1)
b = boundaries[0]
self.assertEqual(b.prefix, "' AND ")
self.assertEqual(b.suffix, "'")
self.assertEqual(b.ptype, 2) # single-quote, no LIKE
self.assertEqual(b.level, 1)
self.assertEqual(b.clause, [0])
def test_double_quote_ptype(self):
boundaries = self._run('" AND ', '"')
self.assertEqual(boundaries[0].ptype, 4) # double-quote, no LIKE
def test_numeric_ptype(self):
boundaries = self._run(" AND ", "")
self.assertEqual(boundaries[0].ptype, 1) # no quoting
def test_like_single_quote_ptype(self):
boundaries = self._run("' AND ", "' like '%")
self.assertEqual(boundaries[0].ptype, 3) # LIKE with single quote
if __name__ == "__main__":
unittest.main()

View file

@ -1,605 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional unit coverage for lib/core/common.py, lib/core/option.py and
lib/core/target.py, targeting *pure* (or near-pure) functions and branches NOT
already exercised by the existing test modules:
* tests/test_common_utils.py / test_common_parsers.py / test_core_more.py
* tests/test_option_setup.py / test_option_more.py
* tests/test_target_parsing.py
This file instead covers (common.py):
boldifyMessage, calculateDeltaSeconds, commonFinderOnly,
enumValueToNameLookup, extractErrorMessage, filePathToSafeString,
isWindowsDriveLetterPath, cleanReplaceUnicode, trimAlphaNum,
removePostHintPrefix, safeExpandUser, safeFilepathEncode,
serializeObject/unserializeObject, applyFunctionRecursively,
extractExpectedValue, getHeader, getRequestHeader, parseJson,
parsePasswordHash, findMultipartPostBoundary, setTechnique/getTechnique,
extractRegexResult, extractTextTagContent, getFilteredPageContent,
checkFile, listToStrValue, intersect, isZipFile, checkOldOptions.
(option.py):
_setHTTPAuthentication (basic/ntlm/bearer/pki + error branches),
_setWriteFile, _setHTTPTimeout, _setAuthCred.
Everything runs in isolation: no network, no DBMS, no persistent filesystem
mutation. All mutated conf/kb/Backend/socket state is snapshotted and restored.
"""
import os
import socket
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
import lib.core.option as option
from lib.core.data import conf, kb, paths
from lib.core.enums import (
AUTH_TYPE,
DBMS,
EXPECTED,
HTTP_HEADER,
SORT_ORDER,
)
from lib.core.exception import (
SqlmapFilePathException,
SqlmapMissingMandatoryOptionException,
SqlmapMissingDependence,
SqlmapSyntaxException,
SqlmapSystemException,
)
from lib.core.settings import NULL
from lib.core.common import (
applyFunctionRecursively,
boldifyMessage,
calculateDeltaSeconds,
checkFile,
checkOldOptions,
cleanReplaceUnicode,
commonFinderOnly,
enumValueToNameLookup,
extractErrorMessage,
extractExpectedValue,
extractRegexResult,
extractTextTagContent,
filePathToSafeString,
findMultipartPostBoundary,
getFilteredPageContent,
getHeader,
getRequestHeader,
getText,
getTechnique,
intersect,
isWindowsDriveLetterPath,
isZipFile,
listToStrValue,
parseJson,
parsePasswordHash,
removePostHintPrefix,
safeExpandUser,
safeFilepathEncode,
serializeObject,
setTechnique,
trimAlphaNum,
unserializeObject,
)
from thirdparty.six.moves import urllib as _urllib
class _FakeRequest(object):
"""Minimal stand-in for urllib2.Request used by getRequestHeader()."""
def __init__(self, headers):
self.headers = headers
def header_items(self):
return self.headers.items()
class TestCommonPureHelpers(unittest.TestCase):
"""Pure string/encoding/list/regex helpers from lib/core/common.py."""
def test_boldify_message_marks_known_pattern(self):
self.assertEqual(
boldifyMessage("GET parameter id is not injectable", istty=True),
"\x1b[1mGET parameter id is not injectable\x1b[0m",
)
def test_boldify_message_leaves_plain_unchanged(self):
self.assertEqual(boldifyMessage("just a plain message", istty=True), "just a plain message")
def test_calculate_delta_seconds_from_epoch(self):
self.assertGreater(calculateDeltaSeconds(0), 1151721660)
def test_calculate_delta_seconds_nonnegative(self):
import time as _time
self.assertGreaterEqual(calculateDeltaSeconds(_time.time()), 0.0)
def test_common_finder_only_returns_longest_common_prefix(self):
self.assertEqual(commonFinderOnly("abcd", ["abcdefg", "foobar", "abcde"]), "abcde")
def test_enum_value_to_name_lookup_hit(self):
self.assertEqual(enumValueToNameLookup(SORT_ORDER, SORT_ORDER.LAST), "LAST")
def test_enum_value_to_name_lookup_miss(self):
self.assertIsNone(enumValueToNameLookup(SORT_ORDER, -987654321))
def test_file_path_to_safe_string(self):
self.assertEqual(filePathToSafeString("C:/Windows/system32"), "C__Windows_system32")
def test_file_path_to_safe_string_spaces_backslashes(self):
self.assertEqual(filePathToSafeString("a b\\c:d"), "a_b_c_d")
def test_is_windows_drive_letter_path_true(self):
self.assertTrue(isWindowsDriveLetterPath("C:\\boot.ini"))
def test_is_windows_drive_letter_path_false(self):
self.assertFalse(isWindowsDriveLetterPath("/var/log/apache.log"))
def test_clean_replace_unicode_list(self):
self.assertEqual(cleanReplaceUnicode(["a", "b"]), ["a", "b"])
def test_clean_replace_unicode_scalar(self):
self.assertEqual(cleanReplaceUnicode(u"plain"), u"plain")
def test_trim_alpha_num(self):
self.assertEqual(trimAlphaNum("AND 1>(2+3)-- foobar"), " 1>(2+3)-- ")
def test_trim_alpha_num_all_alnum(self):
self.assertEqual(trimAlphaNum("abc123"), "")
def test_trim_alpha_num_empty(self):
self.assertEqual(trimAlphaNum(""), "")
def test_list_to_str_value_list(self):
self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3")
def test_list_to_str_value_tuple(self):
self.assertEqual(listToStrValue((4, 5)), "4, 5")
def test_list_to_str_value_scalar(self):
self.assertEqual(listToStrValue("foo"), "foo")
def test_intersect_lists(self):
self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3])
def test_intersect_lowercase(self):
self.assertEqual(intersect(["A", "B"], ["a"], lowerCase=True), ["a"])
def test_intersect_empty(self):
self.assertEqual(intersect([], [1, 2]), [])
def test_apply_function_recursively(self):
self.assertEqual(
applyFunctionRecursively([1, 2, [3, -9]], lambda _: _ > 0),
[True, True, [True, False]],
)
def test_apply_function_recursively_scalar(self):
self.assertEqual(applyFunctionRecursively(5, lambda _: _ + 1), 6)
class TestCommonRegexAndPage(unittest.TestCase):
"""Regex / page-content extraction helpers."""
def test_extract_regex_result_hit(self):
self.assertEqual(extractRegexResult(r"a(?P<result>[^g]+)g", "abcdefg"), "bcdef")
def test_extract_regex_result_no_match(self):
self.assertIsNone(extractRegexResult(r"a(?P<result>[^g]+)g", "xyz"))
def test_extract_regex_result_no_result_group(self):
self.assertIsNone(extractRegexResult(r"plain", "plain"))
def test_extract_regex_result_empty_content(self):
self.assertIsNone(extractRegexResult(r"a(?P<result>.)b", ""))
def test_extract_text_tag_content(self):
self.assertEqual(
extractTextTagContent("<html><head><title>Title</title></head><body><pre>foobar</pre></body></html>"),
["Title", "foobar"],
)
def test_extract_text_tag_content_empty(self):
self.assertEqual(extractTextTagContent(""), [])
def test_get_filtered_page_content(self):
self.assertEqual(
getFilteredPageContent(u"<html><title>foobar</title><body>test</body></html>"),
"foobar test",
)
def test_get_filtered_page_content_drops_script(self):
page = u"<html><script>var x=1;</script><body>hello</body></html>"
self.assertNotIn("var x", getFilteredPageContent(page))
self.assertIn("hello", getFilteredPageContent(page))
def test_get_filtered_page_content_nonstring_passthrough(self):
self.assertEqual(getFilteredPageContent(None), None)
def test_extract_error_message_oracle(self):
page = (u"<html><title>Test</title>\n<b>Warning</b>: oci_parse() "
u"[function.oci-parse]: ORA-01756: quoted string not properly "
u"terminated<br><p>Only a test page</p></html>")
self.assertEqual(
getText(extractErrorMessage(page)),
"oci_parse() [function.oci-parse]: ORA-01756: quoted string not properly terminated",
)
def test_extract_error_message_none_for_plain(self):
self.assertIsNone(extractErrorMessage("Warning: This is only a dummy foobar test"))
def test_extract_error_message_non_string(self):
self.assertIsNone(extractErrorMessage(None))
def test_find_multipart_post_boundary(self):
post = ("-----------------------------9051914041544843365972754266\n"
"Content-Disposition: form-data; name=text\n\ndefault")
self.assertEqual(findMultipartPostBoundary(post), "9051914041544843365972754266")
def test_find_multipart_post_boundary_none(self):
self.assertIsNone(findMultipartPostBoundary(""))
class TestCommonHeadersAndExpected(unittest.TestCase):
def test_get_header_case_insensitive(self):
self.assertEqual(getHeader({"Foo": "bar"}, "foo"), "bar")
def test_get_header_missing(self):
self.assertIsNone(getHeader({"Foo": "bar"}, "x"))
def test_get_header_empty_dict(self):
self.assertIsNone(getHeader({}, "anything"))
def test_get_request_header_hit(self):
self.assertEqual(getText(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "foo")), "BAR")
def test_get_request_header_miss(self):
self.assertIsNone(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "missing"))
def test_extract_expected_value_bool_true(self):
self.assertIs(extractExpectedValue(["1"], EXPECTED.BOOL), True)
def test_extract_expected_value_bool_false(self):
self.assertIs(extractExpectedValue(["0"], EXPECTED.BOOL), False)
def test_extract_expected_value_bool_word(self):
self.assertIs(extractExpectedValue(["true"], EXPECTED.BOOL), True)
self.assertIs(extractExpectedValue(["false"], EXPECTED.BOOL), False)
def test_extract_expected_value_int(self):
self.assertEqual(extractExpectedValue("5", EXPECTED.INT), 5)
def test_extract_expected_value_int_invalid(self):
self.assertIsNone(extractExpectedValue(u"7\xb9645", EXPECTED.INT))
def test_extract_expected_value_no_expected(self):
self.assertEqual(extractExpectedValue("foo", None), "foo")
class TestParseJsonAndHash(unittest.TestCase):
def test_parse_json_double_quotes(self):
self.assertEqual(parseJson('{"id":1}')["id"], 1)
def test_parse_json_single_quotes(self):
self.assertEqual(parseJson("{'id':1, 'foo':[2,3,4]}")["id"], 1)
def test_parse_json_not_json(self):
self.assertIsNone(parseJson("this is not json"))
def test_parse_password_hash_mssql(self):
saved = kb.forcedDbms
try:
kb.forcedDbms = DBMS.MSSQL
result = parsePasswordHash("0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a")
self.assertIn("salt: 4086ceb6", result)
self.assertIn("header: 0x0100", result)
finally:
kb.forcedDbms = saved
def test_parse_password_hash_none(self):
self.assertEqual(parsePasswordHash(None), NULL)
def test_parse_password_hash_blank(self):
self.assertEqual(parsePasswordHash(" "), NULL)
class TestSerializeAndTechnique(unittest.TestCase):
def test_serialize_roundtrip(self):
self.assertEqual(unserializeObject(serializeObject([1, 2, 3])), [1, 2, 3])
def test_serialize_object_is_str(self):
self.assertIsInstance(serializeObject([1, 2, ("a", "b")]), str)
def test_unserialize_none(self):
self.assertIsNone(unserializeObject(None))
def test_set_get_technique_thread_local(self):
saved = getTechnique()
try:
setTechnique(5)
self.assertEqual(getTechnique(), 5)
finally:
setTechnique(saved)
def test_get_technique_falls_back_to_kb(self):
saved_thread = getTechnique()
saved_kb = kb.get("technique")
try:
setTechnique(None)
kb.technique = 7
self.assertEqual(getTechnique(), 7)
finally:
setTechnique(saved_thread)
kb.technique = saved_kb
class TestRemovePostHint(unittest.TestCase):
def test_removes_known_prefix(self):
self.assertEqual(removePostHintPrefix("JSON id"), "id")
def test_no_prefix_unchanged(self):
self.assertEqual(removePostHintPrefix("id"), "id")
class TestFileHelpers(unittest.TestCase):
def test_check_file_existing(self):
self.assertTrue(checkFile(__file__))
def test_check_file_missing_no_raise(self):
self.assertFalse(checkFile("/no/such/path_xyz_123", raiseOnError=False))
def test_check_file_missing_raises(self):
with self.assertRaises(SqlmapSystemException):
checkFile("/no/such/path_xyz_123", raiseOnError=True)
def test_is_zip_file_wordlist(self):
# paths.WORDLIST is a zip-compressed wordlist shipped with sqlmap
self.assertTrue(isZipFile(paths.WORDLIST))
def test_is_zip_file_plain_text(self):
self.assertFalse(isZipFile(paths.SQL_KEYWORDS))
def test_safe_filepath_encode_ascii_passthrough(self):
# On Python 3 the function returns the value unchanged for str input
self.assertEqual(safeFilepathEncode("/tmp/x"), "/tmp/x")
def test_safe_expand_user_basename_preserved(self):
self.assertIn(os.path.basename(__file__), safeExpandUser(__file__))
class TestCheckOldOptions(unittest.TestCase):
def test_no_old_options_is_noop(self):
# Returns None and does not raise when no deprecated options are present
self.assertIsNone(checkOldOptions(["-u", "http://test.invalid/?id=1", "--banner"]))
class TestOptionSetWriteFile(unittest.TestCase):
def setUp(self):
self._saved = (conf.fileWrite, conf.fileDest, conf.get("fileWriteType"))
def tearDown(self):
conf.fileWrite, conf.fileDest, conf.fileWriteType = self._saved
def test_noop_when_no_filewrite(self):
conf.fileWrite = None
self.assertIsNone(option._setWriteFile())
def test_raises_on_missing_local_file(self):
conf.fileWrite = "/no/such/local_file_xyz"
conf.fileDest = "/var/www/x"
with self.assertRaises(SqlmapFilePathException):
option._setWriteFile()
def test_raises_on_missing_dest(self):
fd, path = tempfile.mkstemp()
os.close(fd)
try:
conf.fileWrite = path
conf.fileDest = None
with self.assertRaises(SqlmapMissingMandatoryOptionException):
option._setWriteFile()
finally:
os.unlink(path)
def test_sets_file_write_type(self):
fd, path = tempfile.mkstemp()
os.close(fd)
try:
conf.fileWrite = path
conf.fileDest = "/var/www/x"
option._setWriteFile()
self.assertIn(conf.fileWriteType, ("text", "binary"))
finally:
os.unlink(path)
class TestOptionSetHTTPTimeout(unittest.TestCase):
def setUp(self):
self._savedTimeout = conf.timeout
self._savedSocket = socket.getdefaulttimeout()
def tearDown(self):
conf.timeout = self._savedTimeout
socket.setdefaulttimeout(self._savedSocket)
def test_explicit_timeout(self):
conf.timeout = 10
option._setHTTPTimeout()
self.assertEqual(conf.timeout, 10.0)
def test_below_minimum_is_clamped(self):
conf.timeout = 1
option._setHTTPTimeout()
self.assertEqual(conf.timeout, 3.0)
def test_default_when_unset(self):
conf.timeout = None
option._setHTTPTimeout()
self.assertEqual(conf.timeout, 30.0)
class TestOptionSetHTTPAuthentication(unittest.TestCase):
def setUp(self):
self._saved = {
"authType": conf.authType,
"authCred": conf.authCred,
"authFile": conf.authFile,
"authUsername": conf.authUsername,
"authPassword": conf.authPassword,
"httpHeaders": list(conf.httpHeaders),
"passwordMgr": kb.passwordMgr,
}
# provide a real password manager so the basic/digest branches work
kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm()
def tearDown(self):
conf.authType = self._saved["authType"]
conf.authCred = self._saved["authCred"]
conf.authFile = self._saved["authFile"]
conf.authUsername = self._saved["authUsername"]
conf.authPassword = self._saved["authPassword"]
conf.httpHeaders = self._saved["httpHeaders"]
kb.passwordMgr = self._saved["passwordMgr"]
def test_noop_when_nothing_set(self):
conf.authType = None
conf.authCred = None
conf.authFile = None
self.assertIsNone(option._setHTTPAuthentication())
def test_basic_credentials_parsed(self):
conf.authType = "basic"
conf.authCred = "admin:secret"
conf.authFile = None
option._setHTTPAuthentication()
self.assertEqual(conf.authUsername, "admin")
self.assertEqual(conf.authPassword, "secret")
def test_ntlm_credentials_parsed(self):
conf.authType = "ntlm"
conf.authCred = "DOMAIN\\user:pa:ss"
conf.authFile = None
conf.authUsername = None
conf.authPassword = None
# The python-ntlm handler module is optional; credential parsing happens
# before the handler import, so the parsed creds are set regardless.
try:
option._setHTTPAuthentication()
except SqlmapMissingDependence:
pass
self.assertEqual(conf.authUsername, "DOMAIN\\user")
self.assertEqual(conf.authPassword, "pa:ss")
def test_ntlm_bad_format_raises(self):
conf.authType = "ntlm"
conf.authCred = "nobackslash:pass"
conf.authFile = None
with self.assertRaises(SqlmapSyntaxException):
option._setHTTPAuthentication()
def test_bearer_appends_authorization_header(self):
conf.authType = "bearer"
conf.authCred = "tok123"
conf.authFile = None
conf.httpHeaders = []
option._setHTTPAuthentication()
self.assertIn((HTTP_HEADER.AUTHORIZATION, "Bearer tok123"), conf.httpHeaders)
def test_unsupported_type_raises(self):
conf.authType = "wrongtype"
conf.authCred = "a:b"
conf.authFile = None
with self.assertRaises(SqlmapSyntaxException):
option._setHTTPAuthentication()
def test_type_without_credentials_raises(self):
conf.authType = "basic"
conf.authCred = None
conf.authFile = None
with self.assertRaises(SqlmapSyntaxException):
option._setHTTPAuthentication()
def test_credentials_without_type_raises(self):
conf.authType = None
conf.authCred = "a:b"
conf.authFile = None
with self.assertRaises(SqlmapSyntaxException):
option._setHTTPAuthentication()
def test_authfile_without_type_defaults_to_pki(self):
conf.authType = None
conf.authCred = None
conf.authFile = __file__ # exists, so checkFile() inside PKI branch passes
option._setHTTPAuthentication()
self.assertEqual(conf.authType, AUTH_TYPE.PKI)
def test_pki_type_without_authfile_raises(self):
conf.authType = "pki"
conf.authCred = "x"
conf.authFile = None
with self.assertRaises(SqlmapSyntaxException):
option._setHTTPAuthentication()
class TestOptionSetAuthCred(unittest.TestCase):
def setUp(self):
self._saved = {
"scheme": conf.scheme,
"hostname": conf.hostname,
"port": conf.port,
"authUsername": conf.authUsername,
"authPassword": conf.authPassword,
"passwordMgr": kb.passwordMgr,
}
def tearDown(self):
conf.scheme = self._saved["scheme"]
conf.hostname = self._saved["hostname"]
conf.port = self._saved["port"]
conf.authUsername = self._saved["authUsername"]
conf.authPassword = self._saved["authPassword"]
kb.passwordMgr = self._saved["passwordMgr"]
def test_noop_without_password_manager(self):
kb.passwordMgr = None
# Must not raise when there is no password manager configured
self.assertIsNone(option._setAuthCred())
def test_adds_credentials_to_manager(self):
kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm()
conf.scheme = "http"
conf.hostname = "host"
conf.port = 80
conf.authUsername = "u"
conf.authPassword = "p"
option._setAuthCred()
self.assertEqual(
kb.passwordMgr.find_user_password(None, "http://host:80"),
("u", "p"),
)
if __name__ == "__main__":
unittest.main()

View file

@ -1,706 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional unit coverage for lib/core/agent.py, lib/core/common.py and
lib/utils/brute.py, targeting functions/branches NOT already exercised by:
* tests/test_agent.py (payload delimiters, prefix/suffix defaults,
getFields(SELECT a,b), one MySQL concatQuery,
cleanupPayload RANDNUM)
* tests/test_agent_dialects.py (null/cast/concat, hexConvertField,
nullAndCastField, simpleConcatenate,
forgeUnionQuery(-1,3,...), limitQuery(0,...),
forgeCaseStatement, runAsDBMSUser-noop)
* tests/test_common_utils.py (paramToDict, getCharset, getLimitRange,
parseUnionPage, safeStringFormat, urlencode,
parseTargetUrl/Direct, safeSQLIdentificatorNaming)
* tests/test_common_parsers.py (request-file parsers, reflective masking,
findPageForms, saveConfig, getSQLSnippet,
Backend setters, urlencode/safeStringFormat extras)
This file instead covers:
agent.py: forgeUnionQuery (limited / multipleUnions / fromTable / collate /
INTO OUTFILE), limitQuery across several DBMS shapes (TOP/ROWNUM/
OFFSET dialects + the " FROM "-less early return), whereQuery
(dumpWhere splicing), getComment, concatQuery(unpack=False),
cleanupPayload([ORIGVALUE]/[ORIGINAL]/[SPACE_REPLACE]),
adjustLateValues (SLEEPTIME/base64/RANDNUM), getFields on TOP /
DISTINCT / function / no-FROM shapes, prefixQuery/suffixQuery with
explicit prefix/suffix/clause/comment args, nullAndCastField noCast.
common.py: isNoneValue, isNullValue, isNumPosStrValue, isNumber, isListLike,
filterPairValues, filterListValue, filterNone, filterStringValue,
zeroDepthSearch, splitFields, unArrayizeValue, flattenValue,
arrayizeValue, joinValue, aliasToDbmsEnum, getPageWordSet,
resetCookieJar (clear branch), normalizeUnicode.
brute.py: tableExists / columnExists driven with conf.direct=True and the
external collaborators (inject.checkBooleanExpression, getFileItems,
runThreads) monkeypatched, plus _addPageTextWords.
Everything runs in isolation (no network, no DBMS, no filesystem mutation of
the project). Any global conf/kb/Backend state that a call reads or writes is
snapshotted in setUp and restored in tearDown so test ordering is irrelevant.
"""
import os
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 conf, kb, queries
from lib.core.enums import DBMS
from lib.core.settings import (
PAYLOAD_DELIMITER,
SLEEP_TIME_MARKER,
BOUNDED_BASE64_MARKER,
NULL,
)
from lib.core.common import (
Backend,
isNoneValue,
isNullValue,
isNumPosStrValue,
isNumber,
isListLike,
filterPairValues,
filterListValue,
filterNone,
filterStringValue,
zeroDepthSearch,
splitFields,
unArrayizeValue,
flattenValue,
arrayizeValue,
joinValue,
aliasToDbmsEnum,
getPageWordSet,
resetCookieJar,
normalizeUnicode,
)
class DbmsStateMixin(object):
"""Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak."""
def setUp(self):
self._forcedDbms = kb.forcedDbms
self._sticky = kb.stickyDBMS
self._batch = conf.batch
conf.batch = True
def tearDown(self):
kb.forcedDbms = self._forcedDbms
kb.stickyDBMS = self._sticky
conf.batch = self._batch
# --------------------------------------------------------------------------- #
# lib/core/agent.py
# --------------------------------------------------------------------------- #
class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase):
"""forgeUnionQuery arg combinations not reached by the dialect smoke test."""
def test_limited_subselect_wraps_query(self):
set_dbms(DBMS.MYSQL)
# limited=True wraps the payload as (SELECT ...) at `position`, fills the
# rest with `char`, and appends the FROM/comment/suffix
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None,
None, None, "NULL", None, limited=True)
self.assertIn("(SELECT user FROM mysql.user)", out)
self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out)
# position 1 of 3 => NULL,<payload>,NULL
self.assertEqual(out.count("NULL"), 2, msg=out)
def test_multiple_unions_appends_second_select(self):
set_dbms(DBMS.MYSQL)
out = agent.forgeUnionQuery("SELECT a FROM t", 0, 2, None, None, None,
"NULL", None, multipleUnions="b")
# the multipleUnions payload produces a *second* UNION ALL SELECT
self.assertEqual(out.upper().count("UNION ALL SELECT"), 2, msg=out)
self.assertIn("b", out)
def test_from_table_override(self):
set_dbms(DBMS.MYSQL)
out = agent.forgeUnionQuery("SELECT 1", 0, 1, None, None, None, "NULL",
None, fromTable=" FROM dummytable")
self.assertIn("FROM dummytable", out, msg=out)
def test_into_outfile_forces_null_position(self):
set_dbms(DBMS.MYSQL)
# an INTO OUTFILE clause forces position 0 / char NULL and re-appends the file part
out = agent.forgeUnionQuery("SELECT a INTO OUTFILE '/tmp/o.txt' FROM t",
1, 2, None, None, None, "NULL", None)
self.assertIn("INTO OUTFILE '/tmp/o.txt'", out, msg=out)
def test_collate_clause_on_mysql(self):
set_dbms(DBMS.MYSQL)
# collate=True on MySQL wraps a non-NULL, non-numeric value in the
# MYSQL_UNION_VALUE_CAST collation wrapper
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 0, 1, None,
None, None, "NULL", None, collate=True)
self.assertIn("CONVERT", out.upper(), msg=out)
class TestLimitQuery(DbmsStateMixin, unittest.TestCase):
"""limitQuery dialect shapes beyond the single limitQuery(0,...) smoke test."""
def test_no_from_returns_unchanged(self):
set_dbms(DBMS.MYSQL)
self.assertEqual(agent.limitQuery(5, "SELECT 1", "1"), "SELECT 1")
def test_mysql_appends_limit_offset_one(self):
set_dbms(DBMS.MYSQL)
out = agent.limitQuery(7, "SELECT user FROM mysql.user", "user")
self.assertTrue(out.endswith("LIMIT 7,1"), msg=out)
def test_pgsql_offset_form(self):
set_dbms(DBMS.PGSQL)
out = agent.limitQuery(4, "SELECT usename FROM pg_shadow", "usename")
self.assertIn("OFFSET 4 LIMIT 1", out, msg=out)
def test_oracle_rownum_wrap(self):
set_dbms(DBMS.ORACLE)
out = agent.limitQuery(2, "SELECT banner FROM v$version", ["banner"])
# Oracle wraps in a ROWNUM-bounded subselect ending with =<num+1>
self.assertIn("ROWNUM", out.upper(), msg=out)
self.assertTrue(out.rstrip().endswith("=3"), msg=out)
def test_firebird_first_skip(self):
set_dbms(DBMS.FIREBIRD)
out = agent.limitQuery(3, "SELECT foo FROM bar", "foo")
self.assertIsInstance(out, str)
self.assertIn("foo", out)
# Firebird uses ROWS <num+1> TO <num+1> (the FIRST/SKIP emulation); pin
# the exact shape so a broken offset arithmetic is caught.
self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out)
def test_mssql_top_not_in(self):
set_dbms(DBMS.MSSQL)
out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name")
# MSSQL emulates LIMIT via TOP + NOT IN
self.assertIn("TOP", out.upper(), msg=out)
self.assertIn("NOT IN", out.upper(), msg=out)
class TestWhereQuery(DbmsStateMixin, unittest.TestCase):
"""whereQuery only acts when conf.dumpWhere is set."""
def setUp(self):
DbmsStateMixin.setUp(self)
self._dumpWhere = conf.dumpWhere
self._tbl = conf.tbl
def tearDown(self):
conf.dumpWhere = self._dumpWhere
conf.tbl = self._tbl
DbmsStateMixin.tearDown(self)
def test_no_dumpwhere_is_identity(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = None
self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t")
def test_appends_where_clause(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>10"
conf.tbl = None
out = agent.whereQuery("SELECT a FROM t")
self.assertIn("WHERE id>10", out, msg=out)
def test_existing_where_gets_anded(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>10"
conf.tbl = None
out = agent.whereQuery("SELECT a FROM t WHERE b=1")
self.assertIn("AND id>10", out, msg=out)
def test_order_by_suffix_preserved(self):
set_dbms(DBMS.MYSQL)
conf.dumpWhere = "id>10"
conf.tbl = None
out = agent.whereQuery("SELECT a FROM t ORDER BY a")
# the genuine trailing ORDER BY is kept after the spliced WHERE
self.assertIn("WHERE id>10", out, msg=out)
# the ORDER BY must survive *after* the spliced WHERE clause; the
# substring check alone could pass even if the suffix were dropped.
self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out)
class TestGetComment(unittest.TestCase):
def test_present(self):
from lib.core.datatype import AttribDict
self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x")
def test_absent_returns_empty(self):
from lib.core.datatype import AttribDict
self.assertEqual(agent.getComment(AttribDict()), "")
class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase):
def test_unpack_false_returns_input_unchanged(self):
set_dbms(DBMS.MYSQL)
self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False),
"SELECT a FROM t")
def test_pgsql_unpack_uses_pipe_concat(self):
set_dbms(DBMS.PGSQL)
out = agent.concatQuery("SELECT usename FROM pg_shadow")
self.assertIn("||", out, msg=out)
self.assertIn(kb.chars.start, out, msg=out)
self.assertIn(kb.chars.stop, out, msg=out)
class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase):
def test_origvalue_digit_inlined(self):
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42")
self.assertEqual(out, "x=42")
def test_origvalue_nondigit_quoted(self):
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc")
self.assertIn("'abc'", out, msg=out)
def test_original_marker_raw_substitution(self):
out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw")
self.assertEqual(out, "p=raw")
def test_space_replace_marker(self):
out = agent.cleanupPayload("a[SPACE_REPLACE]b")
self.assertEqual(out, "a%sb" % kb.chars.space)
def test_non_string_returns_none(self):
self.assertIsNone(agent.cleanupPayload(None))
class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase):
def test_sleeptime_replaced_with_timesec(self):
out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER)
self.assertEqual(out, "SLEEP(%s)" % conf.timeSec)
self.assertNotIn(SLEEP_TIME_MARKER, out)
def test_randnum_marker_substituted(self):
out = agent.adjustLateValues("v=[RANDNUM]")
self.assertNotIn("[RANDNUM]", out)
self.assertTrue(out.split("=")[1].isdigit(), msg=out)
def test_bounded_base64_marker_encoded(self):
payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER)
out = agent.adjustLateValues(payload)
# the marked region is base64-encoded and the markers are consumed
self.assertNotIn(BOUNDED_BASE64_MARKER, out)
self.assertEqual(out, "QUI=")
def test_empty_payload_passthrough(self):
self.assertEqual(agent.adjustLateValues(""), "")
class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase):
def test_select_top(self):
set_dbms(DBMS.MSSQL)
res = agent.getFields("SELECT TOP 1 name FROM sysobjects")
self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched")
self.assertEqual(res[6], "name")
def test_distinct(self):
set_dbms(DBMS.MYSQL)
res = agent.getFields("SELECT DISTINCT(name) FROM t")
self.assertEqual(res[6], "name")
def test_function_is_single_element(self):
set_dbms(DBMS.MYSQL)
res = agent.getFields("SELECT COUNT(*) FROM t")
self.assertEqual(res[5], ["COUNT(*)"])
def test_no_from_keeps_whole_select_list(self):
set_dbms(DBMS.MYSQL)
res = agent.getFields("SELECT a,b,c")
self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM")
self.assertEqual(res[5], ["a", "b", "c"])
class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase):
def test_prefix_with_explicit_prefix(self):
set_dbms(DBMS.MYSQL)
out = agent.prefixQuery("1=1", prefix="')")
self.assertIn("')", out, msg=out)
self.assertTrue(out.endswith("1=1"), msg=out)
def test_prefix_group_by_clause_uses_prefix_verbatim(self):
set_dbms(DBMS.MYSQL)
# clause == [2] (GROUP BY / ORDER BY) => no trailing space added
out = agent.prefixQuery("1=1", prefix="X", clause=[2])
self.assertEqual(out, "X1=1")
def test_suffix_appends_comment(self):
set_dbms(DBMS.MYSQL)
out = agent.suffixQuery("1=1", comment="-- -")
self.assertTrue(out.startswith("1=1"), msg=out)
self.assertIn("-", out)
def test_suffix_appends_suffix_no_comment(self):
set_dbms(DBMS.MYSQL)
out = agent.suffixQuery("1=1", suffix="')")
self.assertIn("')", out, msg=out)
class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase):
def setUp(self):
DbmsStateMixin.setUp(self)
self._noCast = conf.noCast
def tearDown(self):
conf.noCast = self._noCast
DbmsStateMixin.tearDown(self)
def test_nocast_returns_field_unchanged(self):
set_dbms(DBMS.MYSQL)
conf.noCast = True
self.assertEqual(agent.nullAndCastField("colname"), "colname")
def test_cast_present_when_nocast_off(self):
set_dbms(DBMS.MYSQL)
conf.noCast = False
out = agent.nullAndCastField("colname")
self.assertIn("CAST", out.upper(), msg=out)
self.assertIn("colname", out)
# --------------------------------------------------------------------------- #
# lib/core/common.py
# --------------------------------------------------------------------------- #
class TestSmallPredicates(unittest.TestCase):
def test_is_none_value(self):
self.assertTrue(isNoneValue(None))
self.assertTrue(isNoneValue("None"))
self.assertTrue(isNoneValue(""))
self.assertTrue(isNoneValue([]))
self.assertTrue(isNoneValue(["None", ""]))
self.assertTrue(isNoneValue({}))
self.assertFalse(isNoneValue([2]))
self.assertFalse(isNoneValue("x"))
def test_is_null_value(self):
self.assertTrue(isNullValue(u"NULL"))
self.assertTrue(isNullValue(u"null"))
self.assertFalse(isNullValue(u"foobar"))
self.assertFalse(isNullValue(5))
def test_is_num_pos_str_value(self):
self.assertTrue(isNumPosStrValue(1))
self.assertTrue(isNumPosStrValue("1"))
self.assertFalse(isNumPosStrValue(0))
self.assertFalse(isNumPosStrValue("-2"))
self.assertFalse(isNumPosStrValue("100000000000000000000"))
self.assertFalse(isNumPosStrValue("abc"))
def test_is_number(self):
self.assertTrue(isNumber(1))
self.assertTrue(isNumber("0"))
self.assertTrue(isNumber("3.14"))
self.assertFalse(isNumber("foobar"))
self.assertFalse(isNumber(None))
def test_is_list_like(self):
self.assertTrue(isListLike([1]))
self.assertTrue(isListLike((1,)))
self.assertTrue(isListLike(set([1])))
self.assertFalse(isListLike("x"))
self.assertFalse(isListLike(5))
class TestValueShaping(unittest.TestCase):
def test_filter_pair_values(self):
self.assertEqual(filterPairValues([[1, 2], [3], 1, [4, 5]]), [[1, 2], [4, 5]])
self.assertEqual(filterPairValues(None), [])
def test_filter_list_value(self):
self.assertEqual(filterListValue(["users", "admins", "logs"], r"(users|admins)"),
["users", "admins"])
# non-list input returned unchanged
self.assertEqual(filterListValue("notlist", r"x"), "notlist")
# no regex returns input
self.assertEqual(filterListValue(["a"], None), ["a"])
def test_filter_none(self):
self.assertEqual(filterNone([1, 2, "", None, 3, 0]), [1, 2, 3, 0])
def test_filter_string_value(self):
self.assertEqual(filterStringValue("wzydeadbeef0123#", r"[0-9a-f]"), "deadbeef0123")
def test_un_arrayize_value(self):
self.assertEqual(unArrayizeValue(["1"]), "1")
self.assertEqual(unArrayizeValue("1"), "1")
self.assertEqual(unArrayizeValue(["1", "2"]), "1")
self.assertEqual(unArrayizeValue([["a", "b"], "c"]), "a")
self.assertIsNone(unArrayizeValue([]))
def test_flatten_value(self):
self.assertEqual(list(flattenValue([["1"], [["2"], "3"]])), ["1", "2", "3"])
def test_arrayize_value(self):
self.assertEqual(arrayizeValue("1"), ["1"])
self.assertEqual(arrayizeValue(["1"]), ["1"])
def test_join_value(self):
self.assertEqual(joinValue(["1", "2"]), "1,2")
self.assertEqual(joinValue("1"), "1")
self.assertEqual(joinValue(["1", None]), "1,None")
class TestZeroDepthAndSplit(unittest.TestCase):
def test_zero_depth_search_skips_parens(self):
expr = "SELECT (SELECT id FROM users WHERE 2>1) AS r FROM DUAL"
idx = zeroDepthSearch(expr, " FROM ")
# only the outer top-level FROM is found, not the one inside the subselect
self.assertEqual(len(idx), 1)
self.assertTrue(expr[idx[0]:].startswith(" FROM DUAL"))
def test_zero_depth_search_ignores_quoted(self):
expr = "a , 'b , c' , d"
# commas inside the quoted literal are not reported
self.assertEqual(len(zeroDepthSearch(expr, ",")), 2)
def test_split_fields_basic(self):
self.assertEqual(splitFields("foo, bar, max(foo, bar)"),
["foo", "bar", "max(foo,bar)"])
def test_split_fields_quoted(self):
self.assertEqual(splitFields("a, 'b, c', d"), ["a", "'b, c'", "d"])
def test_split_fields_custom_delimiter(self):
self.assertEqual(splitFields("a; b; max(c; d)", delimiter=";"),
["a", "b", "max(c;d)"])
class TestAliasToDbmsEnum(unittest.TestCase):
def test_known_aliases(self):
self.assertEqual(aliasToDbmsEnum("mssql"), DBMS.MSSQL)
self.assertEqual(aliasToDbmsEnum("mysql"), DBMS.MYSQL)
self.assertEqual(aliasToDbmsEnum("postgres"), DBMS.PGSQL)
def test_unknown_alias_returns_none(self):
self.assertIsNone(aliasToDbmsEnum("definitely_not_a_dbms"))
def test_empty_returns_none(self):
self.assertIsNone(aliasToDbmsEnum(""))
class TestGetPageWordSet(unittest.TestCase):
def test_word_extraction(self):
words = getPageWordSet(u"<html><title>foobar</title><body>test</body></html>")
self.assertEqual(sorted(words), [u"foobar", u"test"])
def test_non_string_returns_empty(self):
self.assertEqual(getPageWordSet(None), set())
class TestNormalizeUnicode(unittest.TestCase):
def test_accents_stripped(self):
# normalizeUnicode collapses accented chars to their ASCII base
self.assertEqual(normalizeUnicode(u"éè"), "ee")
def test_plain_ascii_unchanged(self):
self.assertEqual(normalizeUnicode(u"abc123"), "abc123")
def test_none_returns_none(self):
self.assertIsNone(normalizeUnicode(None))
class TestResetCookieJar(unittest.TestCase):
"""resetCookieJar's clear branch (conf.loadCookies falsy)."""
def setUp(self):
self._loadCookies = conf.loadCookies
conf.loadCookies = None
def tearDown(self):
conf.loadCookies = self._loadCookies
def test_clear_branch(self):
try:
from http.cookiejar import CookieJar
except ImportError: # Python 2
from cookielib import CookieJar
jar = CookieJar()
cleared = {"called": False}
class _Jar(object):
def clear(self):
cleared["called"] = True
resetCookieJar(_Jar())
self.assertTrue(cleared["called"])
# also accepts a real jar without raising
self.assertIsNone(resetCookieJar(jar))
# --------------------------------------------------------------------------- #
# lib/utils/brute.py
# --------------------------------------------------------------------------- #
import lib.utils.brute as brute
from lib.request import inject
import lib.core.threads as threads_mod
import lib.core.common as common_mod
class TestBrute(DbmsStateMixin, unittest.TestCase):
"""Drive tableExists / columnExists with all external collaborators stubbed.
conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression,
getFileItems and runThreads are monkeypatched so the check runs synchronously,
deterministically and offline. getPageWordSet is neutralized so the wordlist is
just what the stub returns.
"""
def setUp(self):
DbmsStateMixin.setUp(self)
self._saved_conf = {k: conf.get(k) for k in
("direct", "db", "tbl", "threads", "api", "verbose")}
self._choices = kb.choices
self._cachedTables = kb.data.get("cachedTables")
self._cachedColumns = kb.data.get("cachedColumns")
self._brute = kb.brute
self._origPage = kb.originalPage
# stub the collaborators
self._orig_cbe = inject.checkBooleanExpression
self._orig_brute_cbe = brute.inject.checkBooleanExpression
self._orig_getFileItems = brute.getFileItems
self._orig_runThreads = brute.runThreads
self._orig_getPageWordSet = brute.getPageWordSet
from lib.core.datatype import AttribDict
kb.choices = AttribDict(keycheck=False)
kb.choices.tableExists = None
kb.choices.columnExists = None
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.brute = AttribDict({"tables": [], "columns": []})
kb.originalPage = None
conf.direct = True
conf.db = None
conf.threads = 1
conf.api = False
conf.verbose = 0
# runThreads -> just call the worker once synchronously
def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs):
kb.threadContinue = True
threadFunction()
brute.runThreads = _fakeRunThreads
# no page words injected into the wordlist
brute.getPageWordSet = lambda page: set()
# wordlist file -> small fixed list
brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"]
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
kb.choices = self._choices
if self._cachedTables is None:
kb.data.pop("cachedTables", None)
else:
kb.data.cachedTables = self._cachedTables
if self._cachedColumns is None:
kb.data.pop("cachedColumns", None)
else:
kb.data.cachedColumns = self._cachedColumns
kb.brute = self._brute
kb.originalPage = self._origPage
brute.inject.checkBooleanExpression = self._orig_brute_cbe
brute.getFileItems = self._orig_getFileItems
brute.runThreads = self._orig_runThreads
brute.getPageWordSet = self._orig_getPageWordSet
DbmsStateMixin.tearDown(self)
def test_table_exists_collects_true_results(self):
set_dbms(DBMS.MYSQL)
def _cbe(expression, expectingNone=True):
# initial sanity probe (random table) -> must be False, otherwise the
# function raises SqlmapDataException; then only "users" exists.
return "users" in expression
brute.inject.checkBooleanExpression = _cbe
result = brute.tableExists("/nonexistent/tables.txt")
# cachedTables keyed by conf.db (None here) holds the discovered table
self.assertIn(None, result)
self.assertIn("users", result[None])
self.assertNotIn("logs", result.get(None, []))
# also recorded in kb.brute.tables as (db, table)
self.assertIn((None, "users"), kb.brute.tables)
def test_table_exists_invalid_results_raises(self):
from lib.core.exception import SqlmapDataException
set_dbms(DBMS.MYSQL)
# the initial random-table probe returns True -> "invalid results" guard
brute.inject.checkBooleanExpression = lambda *a, **k: True
with self.assertRaises(SqlmapDataException):
brute.tableExists("/nonexistent/tables.txt")
def test_column_exists_requires_table(self):
from lib.core.exception import SqlmapMissingMandatoryOptionException
set_dbms(DBMS.MYSQL)
conf.tbl = None
# the sanity probe is False so we reach the missing-table guard
brute.inject.checkBooleanExpression = lambda *a, **k: False
with self.assertRaises(SqlmapMissingMandatoryOptionException):
brute.columnExists("/nonexistent/columns.txt")
def test_column_exists_collects_and_types(self):
set_dbms(DBMS.MYSQL)
conf.tbl = "users"
brute.getFileItems = lambda *a, **k: ["id", "name"]
calls = {"n": 0}
def _cbe(expression, expectingNone=True):
calls["n"] += 1
# initial sanity probe uses two random strings (no real column name)
if "id" not in expression and "name" not in expression:
return False
# MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`.
# 'id' is numeric (no non-digit chars => probe False => numeric);
# 'name' is non-numeric (has non-digit chars => probe True => non-numeric).
if "REGEXP" in expression:
return "name" in expression
# plain existence check (EXISTS(SELECT <col> FROM <tbl>)) => both columns exist
return True
brute.inject.checkBooleanExpression = _cbe
result = brute.columnExists("/nonexistent/columns.txt")
self.assertIn(None, result)
cols = result[None]["users"]
# column names are run through safeSQLIdentificatorNaming, so the MySQL
# reserved word "name" comes back backtick-quoted
from lib.core.common import safeSQLIdentificatorNaming, getText
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("id"))), "numeric")
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("name"))), "non-numeric")
def test_add_page_text_words_filters(self):
# restore the real getPageWordSet for this one and drive it directly
brute.getPageWordSet = self._orig_getPageWordSet
kb.originalPage = u"<html>admin password 1abc xy verylongword</html>"
words = brute._addPageTextWords()
# words <= 2 chars or starting with a digit are dropped
self.assertIn("admin", words)
self.assertIn("password", words)
self.assertNotIn("xy", words)
self.assertNotIn("1abc", words)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -36,6 +36,26 @@ from plugins.generic.databases import Databases
_NOOP = lambda self: None
def _inference_gv(count, sequence):
"""Build an inject.getValue stub for blind inference branches.
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
yields the next item from `sequence` wrapped as a single-cell row ([value]),
cycling if exhausted. This mirrors the count-then-per-row contract of every
isInferenceAvailable() branch.
"""
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(count)
val = sequence[state["i"] % len(sequence)]
state["i"] += 1
return [val]
return gv
class _BaseEnumTest(unittest.TestCase):
"""Shared setup/teardown that snapshots and restores all touched global state."""
@ -507,5 +527,241 @@ class TestGetProcedures(_BaseEnumTest):
self.assertEqual(sorted(result), sorted(procs))
# --------------------------------------------------------------------------- #
# Inference / brute-force branches (relocated from test_generic_enum_more.py)
# --------------------------------------------------------------------------- #
class _DbBase(unittest.TestCase):
_CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude",
"getComments", "excludeSysDbs", "search", "freshQueries")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_getValue = dbmod.inject.getValue
self._saved_checkBool = dbmod.inject.checkBooleanExpression
self._saved_injection_data = kb.injection.data
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_hintValue = kb.get("hintValue")
self._saved_choices = dict(kb.choices)
self._saved_readInput = dbmod.readInput
self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None)
Databases.forceDbmsEnum = _NOOP
conf.getComments = False
conf.excludeSysDbs = False
conf.exclude = None
conf.search = False
conf.freshQueries = False
conf.col = None
kb.data.has_information_schema = True
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
dbmod.inject.getValue = self._saved_getValue
dbmod.inject.checkBooleanExpression = self._saved_checkBool
dbmod.readInput = self._saved_readInput
kb.injection.data = self._saved_injection_data
kb.data.has_information_schema = self._saved_has_is
kb.hintValue = self._saved_hintValue
kb.choices.clear()
kb.choices.update(self._saved_choices)
if self._saved_forceDbmsEnum is not None:
Databases.forceDbmsEnum = self._saved_forceDbmsEnum
else:
try:
del Databases.forceDbmsEnum
except AttributeError:
pass
def _fresh(self):
d = Databases()
kb.data.currentDb = ""
kb.data.cachedDbs = []
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.data.cachedCounts = {}
kb.data.cachedStatements = []
kb.data.cachedProcedures = []
return d
def _inference(self):
conf.direct = False
conf.technique = None
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
class TestDatabasesInference(_DbBase):
def test_get_columns_inference_pgsql_types(self):
# Blind column enumeration on PostgreSQL: a count, then for each index a
# column name followed by its type. Assert the {db:{tbl:{col:type}}} parse.
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
conf.db = "public"
conf.tbl = "users"
names = ["id", "email"]
state = {"i": 0, "name": True}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(names))
if state["name"]:
val = names[state["i"] % len(names)]
state["i"] += 1
state["name"] = False
return [val]
state["name"] = True
return ["integer"]
dbmod.inject.getValue = gv
result = d.getColumns()
cols = result["public"]["users"]
self.assertEqual(len(cols), 2)
self.assertEqual(cols.get("id"), "integer")
def test_get_columns_inference_dump_mode_collist(self):
# dumpMode with an explicit conf.col list: in the inference branch the
# columns are taken straight from colList (no count/type queries at all)
# and stored with value None. Asserting no getValue ran proves the
# dump-mode shortcut, not a network round-trip.
set_dbms("MySQL")
self._inference()
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
conf.col = "id,name"
def boom(*a, **k):
raise AssertionError("dumpMode+colList must not query in inference branch")
dbmod.inject.getValue = boom
result = d.getColumns(dumpMode=True)
cols = result["testdb"]["users"]
# "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it;
# both columns must be present (count, since exact key varies by quoting).
self.assertEqual(len(cols), 2)
self.assertIn("id", cols)
self.assertIsNone(cols.get("id"))
def test_get_count_over_cached_tables_inference(self):
# getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount.
# Drive the inband table fetch + per-table count and assert the
# {db:{count:[tables]}} grouping (tables sharing a count are grouped).
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = None
kb.data.cachedTables = {"testdb": ["users", "posts"]}
counts = {"users": "5", "posts": "5"}
def gv(query, *a, **k):
for t, c in counts.items():
if t in query:
return c
return "0"
dbmod.inject.getValue = gv
result = d.getCount()
# both tables have count 5 -> grouped under the same key
self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"])
def test_get_statements_count_zero_returns_empty(self):
# Inference path: a zero count short-circuits to the (empty) cache.
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
# getStatements compares the count with the int literal 0 (count == 0), so
# the count stub must return an int 0 (not "0") to take the empty branch.
dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0")
result = d.getStatements()
self.assertEqual(result, [])
def test_get_procedures_inference(self):
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"])
result = d.getProcedures()
self.assertEqual(sorted(result), ["sp_a", "sp_b"])
def test_get_dbs_mssql_inband_paging(self):
# MSSQL with no rows from the primary query falls into the query2 paging
# loop (one indexed query per db until a blank value stops it).
set_dbms("Microsoft SQL Server")
conf.direct = True
d = self._fresh()
dbs = ["master", "model"]
def gv(query, *a, **k):
# The primary inband query is 'SELECT name FROM master..sysdatabases'
# (no DB_NAME); make it return nothing so getDbs falls into the
# 'SELECT DB_NAME(<index>)' paging loop (query2).
if "DB_NAME" not in query:
return None
import re as _re
idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0])
return dbs[idx] if idx < len(dbs) else ""
dbmod.inject.getValue = gv
result = d.getDbs()
self.assertEqual(sorted(result), ["master", "model"])
def test_get_tables_inference_grouped_per_db(self):
# Blind table enumeration: count for the db, then one table name per index.
set_dbms("MySQL")
self._inference()
d = self._fresh()
conf.db = "shop"
conf.tbl = None
dbmod.inject.getValue = _inference_gv(2, ["orders", "items"])
result = d.getTables()
self.assertIn("shop", result)
self.assertEqual(sorted(result["shop"]), ["items", "orders"])
class TestDatabasesBruteForce(_DbBase):
def test_get_columns_mysql_lt5_bruteforce_decline(self):
# MySQL < 5 (no information_schema) forces bruteForce in getColumns; with
# the common-column-existence prompt answered 'N' it returns None without
# issuing any column query.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
kb.data.has_information_schema = False
kb.choices.columnExists = None
dbmod.readInput = lambda *a, **k: "N"
def boom(*a, **k):
raise AssertionError("bruteForce decline must not query columns")
dbmod.inject.getValue = boom
result = d.getColumns()
self.assertIsNone(result)
def test_get_columns_bruteforce_dumpmode_collist_on_decline(self):
# bruteForce + decline + dumpMode + colList: the columns from colList are
# stored with None type (the dump-mode salvage branch), not dropped.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
conf.col = "a,b"
kb.data.has_information_schema = False
kb.choices.columnExists = None
dbmod.readInput = lambda *a, **k: "N"
dbmod.inject.getValue = lambda *a, **k: None
result = d.getColumns(dumpMode=True)
cols = result["testdb"]["users"]
self.assertEqual(sorted(cols.keys()), ["a", "b"])
self.assertIsNone(cols.get("a"))
if __name__ == "__main__":
unittest.main()

View file

@ -6,11 +6,19 @@ See the file 'LICENSE' for copying permission
DBMS-specific enumeration overrides (plugins/dbms/<dbms>/enumeration.py),
driven through each full DBMS handler with the injection layer mocked, so the
dialect-specific table/column discovery paths run without a live target. The
in-band (UNION/error/direct) branch is taken via conf.direct=True and
inject.getValue is stubbed with canned result rows.
dialect-specific table/column/user/privilege discovery paths run without a live
target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via
conf.direct=True and inject.getValue is stubbed with canned result rows;
conf.batch=True avoids interactive prompts.
Consolidated from former tests/test_dbms_enum.py (Microsoft SQL Server),
tests/test_dbms_enum_a.py (Oracle/PostgreSQL/MySQL/SQLite) and
tests/test_dbms_enum_b.py (Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB).
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import importlib
import os
import sys
import unittest
@ -19,11 +27,18 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.common import Backend
from lib.core.data import conf, kb
from lib.core.enums import EXPECTED
from lib.core.exception import SqlmapUnsupportedFeatureException
from lib.request import inject
class _EnumBase(unittest.TestCase):
# ---------------------------------------------------------------------------
# Base for Microsoft SQL Server getTables (former test_dbms_enum.py)
# ---------------------------------------------------------------------------
class _EnumBaseMSSQL(unittest.TestCase):
"""Snapshot/restore the global state these enumerators mutate."""
module = None # the enumeration module whose inject.getValue we patch
@ -45,7 +60,7 @@ class _EnumBase(unittest.TestCase):
kb.data.cachedColumns = self._cachedColumns
class TestMSSQLServerEnum(_EnumBase):
class TestMSSQLServerEnum(_EnumBaseMSSQL):
import plugins.dbms.mssqlserver.enumeration as module
def _handler(self):
@ -94,5 +109,614 @@ class TestMSSQLServerEnum(_EnumBase):
self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"])
# ---------------------------------------------------------------------------
# Base for Oracle/PostgreSQL/MySQL/SQLite (former test_dbms_enum_a.py)
# ---------------------------------------------------------------------------
class _EnumBaseA(unittest.TestCase):
"""Snapshot/restore the global state these enumerators mutate.
Other tests in the suite depend on clean globals (a leaked kb.hintValue
breaks test_inference_engine; a leaked forced DBMS breaks others), so every
knob touched here is captured in setUp and put back in tearDown.
"""
# the enumeration module whose inject.getValue we patch (overridden per DBMS)
module = None
def setUp(self):
# conf knobs
self._direct = conf.direct
self._batch = conf.batch
self._user = conf.user
self._db = conf.get("db")
self._tbl = conf.get("tbl")
self._exclude = conf.get("exclude")
# injection layer (some override modules - e.g. SQLite/PostgreSQL - do not
# import inject because their overrides return constants without querying)
self._has_inject = hasattr(self.module, "inject")
if self._has_inject:
self._gv = self.module.inject.getValue
# kb.data cached* containers
self._cachedTables = kb.data.get("cachedTables")
self._cachedColumns = kb.data.get("cachedColumns")
self._cachedDbs = kb.data.get("cachedDbs")
self._cachedUsers = kb.data.get("cachedUsers")
self._cachedUsersRoles = kb.data.get("cachedUsersRoles")
self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges")
self._has_information_schema = kb.data.get("has_information_schema")
# state other tests are sensitive to
self._hintValue = kb.hintValue
self._injectionData = kb.injection.data
self._forcedDbms = Backend.getForcedDbms()
self._stickyDBMS = kb.stickyDBMS
# avoid readInput EOFError flakiness and interactive prompts
conf.direct = True
conf.batch = True
def tearDown(self):
conf.direct = self._direct
conf.batch = self._batch
conf.user = self._user
conf.db = self._db
conf.tbl = self._tbl
conf.exclude = self._exclude
if self._has_inject:
self.module.inject.getValue = self._gv
kb.data.cachedTables = self._cachedTables
kb.data.cachedColumns = self._cachedColumns
kb.data.cachedDbs = self._cachedDbs
kb.data.cachedUsers = self._cachedUsers
kb.data.cachedUsersRoles = self._cachedUsersRoles
kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges
kb.data.has_information_schema = self._has_information_schema
kb.hintValue = self._hintValue
kb.injection.data = self._injectionData
kb.stickyDBMS = self._stickyDBMS
if self._forcedDbms is not None:
Backend.forceDbms(self._forcedDbms)
else:
kb.forcedDbms = None
class TestOracleEnum(_EnumBaseA):
module = importlib.import_module("plugins.dbms.oracle.enumeration")
def _handler(self):
from plugins.dbms.oracle import OracleMap
set_dbms("Oracle")
return OracleMap()
def test_get_roles(self):
# rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles
conf.user = None
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [
["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"]
]
roles, areAdmins = self._handler().getRoles()
self.assertIn("SYS", roles)
self.assertIn("SCOTT", roles)
self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"})
# DBA implies administrator
self.assertIn("SYS", areAdmins)
def test_get_roles_filtered_by_user(self):
# conf.user populates a WHERE clause; canned rows still drive the parse
conf.user = "SCOTT"
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]]
roles, _ = self._handler().getRoles()
self.assertEqual(list(roles.keys()), ["SCOTT"])
self.assertEqual(roles["SCOTT"], ["RESOURCE"])
def test_get_roles_multiple_roles_per_user(self):
# a user appearing across several rows accumulates all granted roles
conf.user = None
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [
["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"]
]
roles, _ = self._handler().getRoles()
self.assertEqual(
set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"}
)
class TestPostgreSQLEnum(_EnumBaseA):
module = importlib.import_module("plugins.dbms.postgresql.enumeration")
def _handler(self):
from plugins.dbms.postgresql import PostgreSQLMap
set_dbms("PostgreSQL")
return PostgreSQLMap()
def test_get_hostname_unsupported(self):
# PostgreSQL overrides getHostname purely to warn; it returns None
self.assertIsNone(self._handler().getHostname())
class TestMySQLEnum(_EnumBaseA):
# MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the
# generic discovery path through the full MySQL handler instead.
module = importlib.import_module("plugins.generic.enumeration")
def _handler(self):
from plugins.dbms.mysql import MySQLMap
set_dbms("MySQL")
return MySQLMap()
def test_get_dbs(self):
conf.db = None
kb.data.cachedDbs = []
kb.data.has_information_schema = True
self.module.inject.getValue = lambda q, *a, **k: (
3 if k.get("expected") == EXPECTED.INT
else [["information_schema"], ["testdb"], ["mysql"]]
)
dbs = self._handler().getDbs()
self.assertIn("testdb", dbs)
self.assertEqual(set(kb.data.cachedDbs), set(dbs))
class TestSQLiteEnum(_EnumBaseA):
module = importlib.import_module("plugins.dbms.sqlite.enumeration")
def _handler(self):
from plugins.dbms.sqlite import SQLiteMap
set_dbms("SQLite")
return SQLiteMap()
def test_unsupported_simple_overrides(self):
# SQLite overrides these to a warning + an empty/neutral return value
h = self._handler()
self.assertIsNone(h.getCurrentUser())
self.assertIsNone(h.getCurrentDb())
self.assertIsNone(h.getHostname())
self.assertEqual(h.getUsers(), [])
self.assertEqual(h.getDbs(), [])
self.assertEqual(h.searchDb(), [])
self.assertEqual(h.getStatements(), [])
self.assertEqual(h.getPasswordHashes(), {})
self.assertEqual(h.getPrivileges(), {})
def test_is_dba_always_true(self):
# on SQLite the current user is treated as having all privileges
self.assertTrue(self._handler().isDba())
def test_search_column_raises(self):
with self.assertRaises(SqlmapUnsupportedFeatureException):
self._handler().searchColumn()
# ---------------------------------------------------------------------------
# Base + helpers for Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB
# (former test_dbms_enum_b.py)
# ---------------------------------------------------------------------------
def _fresh_cached():
kb.data.cachedDbs = []
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.data.cachedUsers = []
kb.data.cachedUsersPrivileges = {}
kb.data.cachedCounts = {}
kb.data.cachedStatements = []
kb.data.banner = None
class _NoOpDumper(object):
"""Swallow every dumper call so search methods don't emit/prompt."""
def __getattr__(self, name):
return lambda *a, **k: None
def _handler(display_name, dirname):
"""Instantiate the full *Map handler for the given DBMS."""
set_dbms(display_name)
main = importlib.import_module("plugins.dbms.%s" % dirname)
cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0]
return cls()
class _EnumBaseB(unittest.TestCase):
"""Snapshot/restore every global these enumerators mutate."""
# subclasses set these
display_name = None
dirname = None
def setUp(self):
# config snapshot
self._direct = conf.direct
self._batch = conf.batch
self._db = conf.db
self._tbl = conf.tbl
self._col = conf.col
self._user = conf.user
self._exclude = conf.exclude
self._search = conf.search
self._getBanner = conf.getBanner
self._excludeSysDbs = conf.excludeSysDbs
self._dumper = conf.get("dumper")
# kb snapshot
self._cached = {k: kb.data.get(k) for k in (
"cachedDbs", "cachedTables", "cachedColumns", "cachedUsers",
"cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner",
)}
self._hintValue = kb.hintValue
self._injectionData = kb.injection.data
self._currentDb = kb.data.get("currentDb")
self._hasIS = kb.data.get("has_information_schema")
# injection layer snapshot
self._gv = inject.getValue
self._cbe = getattr(inject, "checkBooleanExpression", None)
# baseline config the in-band/non-interactive paths need
conf.direct = True
conf.batch = True
kb.data.has_information_schema = True
_fresh_cached()
# restore the chosen DBMS for every test
self.handler = _handler(self.display_name, self.dirname)
# the enumeration module whose pivotDumpTable some tests stub
self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname)
def tearDown(self):
conf.direct = self._direct
conf.batch = self._batch
conf.db = self._db
conf.tbl = self._tbl
conf.col = self._col
conf.user = self._user
conf.exclude = self._exclude
conf.search = self._search
conf.getBanner = self._getBanner
conf.excludeSysDbs = self._excludeSysDbs
conf.dumper = self._dumper
for k, v in self._cached.items():
kb.data[k] = v
kb.hintValue = self._hintValue
kb.injection.data = self._injectionData
kb.data.currentDb = self._currentDb
kb.data.has_information_schema = self._hasIS
inject.getValue = self._gv
if self._cbe is not None:
inject.checkBooleanExpression = self._cbe
if hasattr(self.em, "pivotDumpTable"):
# restore the pristine reference from the wrapper module
import lib.utils.pivotdumptable as _pdt
self.em.pivotDumpTable = _pdt.pivotDumpTable
# ---------------------------------------------------------------------------
# Sybase
# ---------------------------------------------------------------------------
class TestSybaseEnum(_EnumBaseB):
display_name = "Sybase"
dirname = "sybase"
def _pivot(self, *value_lists):
"""Make em.pivotDumpTable return canned (entries, lengths) per call.
Each successive call pops the next mapping of {colName: [values]}.
"""
calls = list(value_lists)
def fake(table, colList, count=None, blind=True, alias=None):
mapping = calls.pop(0) if calls else {}
entries = {}
lengths = {}
for col in colList:
vals = mapping.get(col.split(".")[-1], [])
entries[col] = list(vals)
lengths[col] = 0
return entries, lengths
self.em.pivotDumpTable = fake
def test_get_users(self):
self._pivot({"name": ["sa", "guest"]})
users = self.handler.getUsers()
self.assertIn("sa", users)
self.assertIn("guest", users)
def test_get_dbs(self):
self._pivot({"name": ["master", "model"]})
dbs = self.handler.getDbs()
self.assertEqual(sorted(dbs), ["master", "model"])
def test_get_tables(self):
conf.db = "testdb"
self._pivot({"name": ["users", "logs"]})
tables = self.handler.getTables()
self.assertIn("testdb", tables)
self.assertEqual(sorted(tables["testdb"]), ["logs", "users"])
def test_get_columns(self):
conf.db = "testdb"
conf.tbl = "users"
# column pivot returns name + usertype: REAL Sybase numeric type ids that
# getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar").
from lib.core.dicts import SYBASE_TYPES
self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]})
cols = self.handler.getColumns()
self.assertIn("testdb", cols)
# table key is identifier-normalized (may be schema-qualified)
tbls = cols["testdb"]
self.assertTrue(any("users" in t for t in tbls))
colset = list(tbls.values())[0]
# the VALUE is the resolved type name, not the raw usertype number:
# proves the SYBASE_TYPES numeric->name mapping actually ran.
self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int"
self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar"
def test_get_privileges(self):
# getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression).
# Drive the admin-set branch BOTH ways via the isDba oracle so the result
# is not forced by a constant-True stub.
conf.user = None
# oracle True: every user is flagged DBA -> admins == all users
self._pivot({"name": ["sa", "guest"]})
inject.checkBooleanExpression = lambda *a, **k: True
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs) # users still enumerated as privilege keys
self.assertIn("guest", privs)
self.assertEqual(admins, set(["sa", "guest"]))
# oracle False: nobody is a DBA -> admins is empty, but users still listed
_fresh_cached()
self._pivot({"name": ["sa", "guest"]})
inject.checkBooleanExpression = lambda *a, **k: False
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set())
def test_search_not_implemented(self):
# these intentionally return [] with a warning on Sybase
self.assertEqual(self.handler.searchDb(), [])
self.assertEqual(self.handler.searchTable(), [])
self.assertEqual(self.handler.searchColumn(), [])
def test_get_hostname(self):
# not possible on Sybase; just must not raise
self.assertIsNone(self.handler.getHostname())
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# SAP MaxDB
# ---------------------------------------------------------------------------
class TestMaxDBEnum(_EnumBaseB):
display_name = "SAP MaxDB"
dirname = "maxdb"
def _pivot(self, *value_lists):
calls = list(value_lists)
def fake(table, colList, count=None, blind=True, alias=None):
mapping = calls.pop(0) if calls else {}
entries = {}
lengths = {}
for col in colList:
vals = mapping.get(col.split(".")[-1], [])
entries[col] = list(vals)
lengths[col] = 0
return entries, lengths
self.em.pivotDumpTable = fake
def test_get_dbs(self):
self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]})
dbs = self.handler.getDbs()
self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"])
def test_get_tables(self):
conf.db = "SYSTEM"
self._pivot({"tablename": ["USERS", "TABLES"]})
tables = self.handler.getTables()
# db key is identifier-normalized (uppercase names get quoted)
self.assertEqual(len(tables), 1)
tbls = list(tables.values())[0]
self.assertEqual(sorted(tbls), ["TABLES", "USERS"])
def test_get_columns(self):
conf.db = "SYSTEM"
conf.tbl = "USERS"
self._pivot({
"columnname": ["ID", "NAME"],
"datatype": ["INTEGER", "CHAR"],
"len": ["4", "32"],
})
cols = self.handler.getColumns()
self.assertEqual(len(cols), 1)
tbls = list(cols.values())[0]
self.assertIn("USERS", tbls)
self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)")
def test_get_privileges_empty(self):
self.assertEqual(self.handler.getPrivileges(), {})
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Microsoft SQL Server (methods NOT covered by TestMSSQLServerEnum above)
# ---------------------------------------------------------------------------
class TestMSSQLServerExtraEnum(_EnumBaseB):
display_name = "Microsoft SQL Server"
dirname = "mssqlserver"
def test_get_privileges(self):
# getPrivileges -> getUsers (generic, inject.getValue) then isDba.
# Exercise the admin-set branch BOTH ways via the isDba oracle.
conf.user = None
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
# oracle True: all users flagged DBA
inject.checkBooleanExpression = lambda *a, **k: True
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"]))
# oracle False: none are DBA -> empty admin set, users still enumerated
_fresh_cached()
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
inject.checkBooleanExpression = lambda *a, **k: False
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set())
def test_search_table(self):
conf.db = "testdb"
conf.tbl = "users"
# in-band branch: getValue returns matching table name(s)
inject.getValue = lambda q, *a, **k: ["users"]
# capture the discovered tables instead of dumping them
captured = {}
conf.dumper = _NoOpDumper()
self.handler.dumpFoundTables = lambda tables: captured.update(tables)
self.handler.searchTable()
# at least one database mapped to the matched table
flat = set()
for tbls in captured.values():
flat.update(tbls)
self.assertTrue(any("users" in t for t in flat))
def test_search_column(self):
conf.db = "testdb"
conf.tbl = None
conf.col = "password"
# exact match (no wildcard) so no recursive getColumns call;
# getValue returns the tables that contain the column
inject.getValue = lambda q, *a, **k: ["users"]
captured = {}
conf.dumper = _NoOpDumper()
self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs)
self.handler.searchColumn()
# the searched column was located in at least one table
flat = set()
for tbls in captured.values():
flat.update(tbls)
self.assertTrue(any("users" in t for t in flat))
# ---------------------------------------------------------------------------
# IBM DB2
# ---------------------------------------------------------------------------
class TestDB2Enum(_EnumBaseB):
display_name = "IBM DB2"
dirname = "db2"
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Informix
# ---------------------------------------------------------------------------
class TestInformixEnum(_EnumBaseB):
display_name = "Informix"
dirname = "informix"
def test_search_db(self):
self.assertEqual(self.handler.searchDb(), [])
def test_search_table(self):
self.assertEqual(self.handler.searchTable(), [])
def test_search_column(self):
self.assertEqual(self.handler.searchColumn(), [])
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Firebird
# ---------------------------------------------------------------------------
class TestFirebirdEnum(_EnumBaseB):
display_name = "Firebird"
dirname = "firebird"
def test_get_dbs_empty(self):
self.assertEqual(self.handler.getDbs(), [])
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_search_db_empty(self):
self.assertEqual(self.handler.searchDb(), [])
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# HSQLDB
# ---------------------------------------------------------------------------
class TestHSQLDBEnum(_EnumBaseB):
display_name = "HSQLDB"
dirname = "hsqldb"
def test_get_banner(self):
conf.getBanner = True
kb.data.banner = None
# getValue returns a single-element LIST; getBanner pipes it through
# unArrayizeValue, which must unwrap it to the scalar banner string.
inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"]
banner = self.handler.getBanner()
self.assertEqual(banner, "HSQLDB 2.5.1")
def test_get_privileges_empty(self):
self.assertEqual(self.handler.getPrivileges(), {})
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
def test_get_current_db_default_schema(self):
from lib.core.settings import HSQLDB_DEFAULT_SCHEMA
self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA)
if __name__ == "__main__":
unittest.main()

View file

@ -1,215 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
DBMS-specific enumeration overrides for Oracle, PostgreSQL, MySQL and SQLite
(plugins/dbms/<dbms>/enumeration.py), driven through each full DBMS handler with
the injection layer mocked, so the dialect-specific discovery paths run without a
live target. The in-band (UNION/error/direct) branch is taken via conf.direct=True
and inject.getValue is stubbed with canned result rows.
Companion to tests/test_dbms_enum.py (which covers Microsoft SQL Server).
"""
import importlib
import os
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.common import Backend
from lib.core.data import conf, kb
from lib.core.enums import EXPECTED
from lib.core.exception import SqlmapUnsupportedFeatureException
class _EnumBase(unittest.TestCase):
"""Snapshot/restore the global state these enumerators mutate.
Other tests in the suite depend on clean globals (a leaked kb.hintValue
breaks test_inference_engine; a leaked forced DBMS breaks others), so every
knob touched here is captured in setUp and put back in tearDown.
"""
# the enumeration module whose inject.getValue we patch (overridden per DBMS)
module = None
def setUp(self):
# conf knobs
self._direct = conf.direct
self._batch = conf.batch
self._user = conf.user
self._db = conf.get("db")
self._tbl = conf.get("tbl")
self._exclude = conf.get("exclude")
# injection layer (some override modules - e.g. SQLite/PostgreSQL - do not
# import inject because their overrides return constants without querying)
self._has_inject = hasattr(self.module, "inject")
if self._has_inject:
self._gv = self.module.inject.getValue
# kb.data cached* containers
self._cachedTables = kb.data.get("cachedTables")
self._cachedColumns = kb.data.get("cachedColumns")
self._cachedDbs = kb.data.get("cachedDbs")
self._cachedUsers = kb.data.get("cachedUsers")
self._cachedUsersRoles = kb.data.get("cachedUsersRoles")
self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges")
self._has_information_schema = kb.data.get("has_information_schema")
# state other tests are sensitive to
self._hintValue = kb.hintValue
self._injectionData = kb.injection.data
self._forcedDbms = Backend.getForcedDbms()
self._stickyDBMS = kb.stickyDBMS
# avoid readInput EOFError flakiness and interactive prompts
conf.direct = True
conf.batch = True
def tearDown(self):
conf.direct = self._direct
conf.batch = self._batch
conf.user = self._user
conf.db = self._db
conf.tbl = self._tbl
conf.exclude = self._exclude
if self._has_inject:
self.module.inject.getValue = self._gv
kb.data.cachedTables = self._cachedTables
kb.data.cachedColumns = self._cachedColumns
kb.data.cachedDbs = self._cachedDbs
kb.data.cachedUsers = self._cachedUsers
kb.data.cachedUsersRoles = self._cachedUsersRoles
kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges
kb.data.has_information_schema = self._has_information_schema
kb.hintValue = self._hintValue
kb.injection.data = self._injectionData
kb.stickyDBMS = self._stickyDBMS
if self._forcedDbms is not None:
Backend.forceDbms(self._forcedDbms)
else:
kb.forcedDbms = None
class TestOracleEnum(_EnumBase):
module = importlib.import_module("plugins.dbms.oracle.enumeration")
def _handler(self):
from plugins.dbms.oracle import OracleMap
set_dbms("Oracle")
return OracleMap()
def test_get_roles(self):
# rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles
conf.user = None
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [
["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"]
]
roles, areAdmins = self._handler().getRoles()
self.assertIn("SYS", roles)
self.assertIn("SCOTT", roles)
self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"})
# DBA implies administrator
self.assertIn("SYS", areAdmins)
def test_get_roles_filtered_by_user(self):
# conf.user populates a WHERE clause; canned rows still drive the parse
conf.user = "SCOTT"
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]]
roles, _ = self._handler().getRoles()
self.assertEqual(list(roles.keys()), ["SCOTT"])
self.assertEqual(roles["SCOTT"], ["RESOURCE"])
def test_get_roles_multiple_roles_per_user(self):
# a user appearing across several rows accumulates all granted roles
conf.user = None
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [
["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"]
]
roles, _ = self._handler().getRoles()
self.assertEqual(
set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"}
)
class TestPostgreSQLEnum(_EnumBase):
module = importlib.import_module("plugins.dbms.postgresql.enumeration")
def _handler(self):
from plugins.dbms.postgresql import PostgreSQLMap
set_dbms("PostgreSQL")
return PostgreSQLMap()
def test_get_hostname_unsupported(self):
# PostgreSQL overrides getHostname purely to warn; it returns None
self.assertIsNone(self._handler().getHostname())
class TestMySQLEnum(_EnumBase):
# MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the
# generic discovery path through the full MySQL handler instead.
module = importlib.import_module("plugins.generic.enumeration")
def _handler(self):
from plugins.dbms.mysql import MySQLMap
set_dbms("MySQL")
return MySQLMap()
def test_get_dbs(self):
conf.db = None
kb.data.cachedDbs = []
kb.data.has_information_schema = True
self.module.inject.getValue = lambda q, *a, **k: (
3 if k.get("expected") == EXPECTED.INT
else [["information_schema"], ["testdb"], ["mysql"]]
)
dbs = self._handler().getDbs()
self.assertIn("testdb", dbs)
self.assertEqual(set(kb.data.cachedDbs), set(dbs))
class TestSQLiteEnum(_EnumBase):
module = importlib.import_module("plugins.dbms.sqlite.enumeration")
def _handler(self):
from plugins.dbms.sqlite import SQLiteMap
set_dbms("SQLite")
return SQLiteMap()
def test_unsupported_simple_overrides(self):
# SQLite overrides these to a warning + an empty/neutral return value
h = self._handler()
self.assertIsNone(h.getCurrentUser())
self.assertIsNone(h.getCurrentDb())
self.assertIsNone(h.getHostname())
self.assertEqual(h.getUsers(), [])
self.assertEqual(h.getDbs(), [])
self.assertEqual(h.searchDb(), [])
self.assertEqual(h.getStatements(), [])
self.assertEqual(h.getPasswordHashes(), {})
self.assertEqual(h.getPrivileges(), {})
def test_is_dba_always_true(self):
# on SQLite the current user is treated as having all privileges
self.assertTrue(self._handler().isDba())
def test_search_column_raises(self):
with self.assertRaises(SqlmapUnsupportedFeatureException):
self._handler().searchColumn()
if __name__ == "__main__":
unittest.main()

View file

@ -1,469 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Second batch of DBMS-specific enumeration override tests (companion to
tests/test_dbms_enum.py, which covers Microsoft SQL Server getTables).
Each test drives a FULL per-DBMS handler (the *Map class in
plugins/dbms/<dbms>/__init__.py) with the injection layer mocked, so the
dialect-specific table/column/user/privilege discovery paths run without a live
target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via
conf.direct=True; conf.batch=True avoids interactive prompts.
Covered here:
* Sybase - getUsers, getDbs, getTables, getColumns, getPrivileges,
searchDb/searchTable/searchColumn, getHostname, getStatements
* SAP MaxDB - getDbs, getTables, getColumns, getPrivileges,
getPasswordHashes, getHostname, getStatements
* Microsoft SQL Server - getPrivileges, searchTable, searchColumn
(getTables already covered by test_dbms_enum.py)
* IBM DB2 - getPasswordHashes, getStatements
* Informix - searchDb, searchTable, searchColumn, getStatements
* Firebird - getDbs, getPasswordHashes, searchDb, getHostname, getStatements
* HSQLDB - getBanner, getPrivileges, getHostname, getStatements,
getCurrentDb
Sybase/MaxDB enumeration goes through lib.utils.pivotdumptable.pivotDumpTable
(imported into the module namespace), so for those we mock that wrapper - it is
part of the same data-retrieval layer - and mock inject.getValue elsewhere.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import importlib
import os
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.data import conf, kb
from lib.core.common import Backend
from lib.core.enums import EXPECTED
from lib.request import inject
def _fresh_cached():
kb.data.cachedDbs = []
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.data.cachedUsers = []
kb.data.cachedUsersPrivileges = {}
kb.data.cachedCounts = {}
kb.data.cachedStatements = []
kb.data.banner = None
class _NoOpDumper(object):
"""Swallow every dumper call so search methods don't emit/prompt."""
def __getattr__(self, name):
return lambda *a, **k: None
def _handler(display_name, dirname):
"""Instantiate the full *Map handler for the given DBMS."""
set_dbms(display_name)
main = importlib.import_module("plugins.dbms.%s" % dirname)
cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0]
return cls()
class _EnumBase(unittest.TestCase):
"""Snapshot/restore every global these enumerators mutate."""
# subclasses set these
display_name = None
dirname = None
def setUp(self):
# config snapshot
self._direct = conf.direct
self._batch = conf.batch
self._db = conf.db
self._tbl = conf.tbl
self._col = conf.col
self._user = conf.user
self._exclude = conf.exclude
self._search = conf.search
self._getBanner = conf.getBanner
self._excludeSysDbs = conf.excludeSysDbs
self._dumper = conf.get("dumper")
# kb snapshot
self._cached = {k: kb.data.get(k) for k in (
"cachedDbs", "cachedTables", "cachedColumns", "cachedUsers",
"cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner",
)}
self._hintValue = kb.hintValue
self._injectionData = kb.injection.data
self._currentDb = kb.data.get("currentDb")
self._hasIS = kb.data.get("has_information_schema")
# injection layer snapshot
self._gv = inject.getValue
self._cbe = getattr(inject, "checkBooleanExpression", None)
# baseline config the in-band/non-interactive paths need
conf.direct = True
conf.batch = True
kb.data.has_information_schema = True
_fresh_cached()
# restore the chosen DBMS for every test
self.handler = _handler(self.display_name, self.dirname)
# the enumeration module whose pivotDumpTable some tests stub
self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname)
def tearDown(self):
conf.direct = self._direct
conf.batch = self._batch
conf.db = self._db
conf.tbl = self._tbl
conf.col = self._col
conf.user = self._user
conf.exclude = self._exclude
conf.search = self._search
conf.getBanner = self._getBanner
conf.excludeSysDbs = self._excludeSysDbs
conf.dumper = self._dumper
for k, v in self._cached.items():
kb.data[k] = v
kb.hintValue = self._hintValue
kb.injection.data = self._injectionData
kb.data.currentDb = self._currentDb
kb.data.has_information_schema = self._hasIS
inject.getValue = self._gv
if self._cbe is not None:
inject.checkBooleanExpression = self._cbe
if hasattr(self.em, "pivotDumpTable"):
# restore the pristine reference from the wrapper module
import lib.utils.pivotdumptable as _pdt
self.em.pivotDumpTable = _pdt.pivotDumpTable
# ---------------------------------------------------------------------------
# Sybase
# ---------------------------------------------------------------------------
class TestSybaseEnum(_EnumBase):
display_name = "Sybase"
dirname = "sybase"
def _pivot(self, *value_lists):
"""Make em.pivotDumpTable return canned (entries, lengths) per call.
Each successive call pops the next mapping of {colName: [values]}.
"""
calls = list(value_lists)
def fake(table, colList, count=None, blind=True, alias=None):
mapping = calls.pop(0) if calls else {}
entries = {}
lengths = {}
for col in colList:
vals = mapping.get(col.split(".")[-1], [])
entries[col] = list(vals)
lengths[col] = 0
return entries, lengths
self.em.pivotDumpTable = fake
def test_get_users(self):
self._pivot({"name": ["sa", "guest"]})
users = self.handler.getUsers()
self.assertIn("sa", users)
self.assertIn("guest", users)
def test_get_dbs(self):
self._pivot({"name": ["master", "model"]})
dbs = self.handler.getDbs()
self.assertEqual(sorted(dbs), ["master", "model"])
def test_get_tables(self):
conf.db = "testdb"
self._pivot({"name": ["users", "logs"]})
tables = self.handler.getTables()
self.assertIn("testdb", tables)
self.assertEqual(sorted(tables["testdb"]), ["logs", "users"])
def test_get_columns(self):
conf.db = "testdb"
conf.tbl = "users"
# column pivot returns name + usertype: REAL Sybase numeric type ids that
# getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar").
from lib.core.dicts import SYBASE_TYPES
self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]})
cols = self.handler.getColumns()
self.assertIn("testdb", cols)
# table key is identifier-normalized (may be schema-qualified)
tbls = cols["testdb"]
self.assertTrue(any("users" in t for t in tbls))
colset = list(tbls.values())[0]
# the VALUE is the resolved type name, not the raw usertype number:
# proves the SYBASE_TYPES numeric->name mapping actually ran.
self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int"
self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar"
def test_get_privileges(self):
# getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression).
# Drive the admin-set branch BOTH ways via the isDba oracle so the result
# is not forced by a constant-True stub.
conf.user = None
# oracle True: every user is flagged DBA -> admins == all users
self._pivot({"name": ["sa", "guest"]})
inject.checkBooleanExpression = lambda *a, **k: True
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs) # users still enumerated as privilege keys
self.assertIn("guest", privs)
self.assertEqual(admins, set(["sa", "guest"]))
# oracle False: nobody is a DBA -> admins is empty, but users still listed
_fresh_cached()
self._pivot({"name": ["sa", "guest"]})
inject.checkBooleanExpression = lambda *a, **k: False
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set())
def test_search_not_implemented(self):
# these intentionally return [] with a warning on Sybase
self.assertEqual(self.handler.searchDb(), [])
self.assertEqual(self.handler.searchTable(), [])
self.assertEqual(self.handler.searchColumn(), [])
def test_get_hostname(self):
# not possible on Sybase; just must not raise
self.assertIsNone(self.handler.getHostname())
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# SAP MaxDB
# ---------------------------------------------------------------------------
class TestMaxDBEnum(_EnumBase):
display_name = "SAP MaxDB"
dirname = "maxdb"
def _pivot(self, *value_lists):
calls = list(value_lists)
def fake(table, colList, count=None, blind=True, alias=None):
mapping = calls.pop(0) if calls else {}
entries = {}
lengths = {}
for col in colList:
vals = mapping.get(col.split(".")[-1], [])
entries[col] = list(vals)
lengths[col] = 0
return entries, lengths
self.em.pivotDumpTable = fake
def test_get_dbs(self):
self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]})
dbs = self.handler.getDbs()
self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"])
def test_get_tables(self):
conf.db = "SYSTEM"
self._pivot({"tablename": ["USERS", "TABLES"]})
tables = self.handler.getTables()
# db key is identifier-normalized (uppercase names get quoted)
self.assertEqual(len(tables), 1)
tbls = list(tables.values())[0]
self.assertEqual(sorted(tbls), ["TABLES", "USERS"])
def test_get_columns(self):
conf.db = "SYSTEM"
conf.tbl = "USERS"
self._pivot({
"columnname": ["ID", "NAME"],
"datatype": ["INTEGER", "CHAR"],
"len": ["4", "32"],
})
cols = self.handler.getColumns()
self.assertEqual(len(cols), 1)
tbls = list(cols.values())[0]
self.assertIn("USERS", tbls)
self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)")
def test_get_privileges_empty(self):
self.assertEqual(self.handler.getPrivileges(), {})
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Microsoft SQL Server (methods NOT covered by test_dbms_enum.py)
# ---------------------------------------------------------------------------
class TestMSSQLServerExtraEnum(_EnumBase):
display_name = "Microsoft SQL Server"
dirname = "mssqlserver"
def test_get_privileges(self):
# getPrivileges -> getUsers (generic, inject.getValue) then isDba.
# Exercise the admin-set branch BOTH ways via the isDba oracle.
conf.user = None
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
# oracle True: all users flagged DBA
inject.checkBooleanExpression = lambda *a, **k: True
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"]))
# oracle False: none are DBA -> empty admin set, users still enumerated
_fresh_cached()
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
inject.checkBooleanExpression = lambda *a, **k: False
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set())
def test_search_table(self):
conf.db = "testdb"
conf.tbl = "users"
# in-band branch: getValue returns matching table name(s)
inject.getValue = lambda q, *a, **k: ["users"]
# capture the discovered tables instead of dumping them
captured = {}
conf.dumper = _NoOpDumper()
self.handler.dumpFoundTables = lambda tables: captured.update(tables)
self.handler.searchTable()
# at least one database mapped to the matched table
flat = set()
for tbls in captured.values():
flat.update(tbls)
self.assertTrue(any("users" in t for t in flat))
def test_search_column(self):
conf.db = "testdb"
conf.tbl = None
conf.col = "password"
# exact match (no wildcard) so no recursive getColumns call;
# getValue returns the tables that contain the column
inject.getValue = lambda q, *a, **k: ["users"]
captured = {}
conf.dumper = _NoOpDumper()
self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs)
self.handler.searchColumn()
# the searched column was located in at least one table
flat = set()
for tbls in captured.values():
flat.update(tbls)
self.assertTrue(any("users" in t for t in flat))
# ---------------------------------------------------------------------------
# IBM DB2
# ---------------------------------------------------------------------------
class TestDB2Enum(_EnumBase):
display_name = "IBM DB2"
dirname = "db2"
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Informix
# ---------------------------------------------------------------------------
class TestInformixEnum(_EnumBase):
display_name = "Informix"
dirname = "informix"
def test_search_db(self):
self.assertEqual(self.handler.searchDb(), [])
def test_search_table(self):
self.assertEqual(self.handler.searchTable(), [])
def test_search_column(self):
self.assertEqual(self.handler.searchColumn(), [])
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Firebird
# ---------------------------------------------------------------------------
class TestFirebirdEnum(_EnumBase):
display_name = "Firebird"
dirname = "firebird"
def test_get_dbs_empty(self):
self.assertEqual(self.handler.getDbs(), [])
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_search_db_empty(self):
self.assertEqual(self.handler.searchDb(), [])
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# HSQLDB
# ---------------------------------------------------------------------------
class TestHSQLDBEnum(_EnumBase):
display_name = "HSQLDB"
dirname = "hsqldb"
def test_get_banner(self):
conf.getBanner = True
kb.data.banner = None
# getValue returns a single-element LIST; getBanner pipes it through
# unArrayizeValue, which must unwrap it to the scalar banner string.
inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"]
banner = self.handler.getBanner()
self.assertEqual(banner, "HSQLDB 2.5.1")
def test_get_privileges_empty(self):
self.assertEqual(self.handler.getPrivileges(), {})
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
def test_get_current_db_default_schema(self):
from lib.core.settings import HSQLDB_DEFAULT_SCHEMA
self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA)
if __name__ == "__main__":
unittest.main()

View file

@ -56,7 +56,7 @@ class TestCheckDependencies(unittest.TestCase):
# 'kinterbasdb' (Firebird driver) is essentially never installed, so the
# probe must hit the except branch and emit a warning naming the library.
try:
import kinterbasdb # noqa: F401
__import__("kinterbasdb")
self.skipTest("kinterbasdb is unexpectedly installed")
except ImportError:
pass

802
tests/test_entries.py Normal file
View file

@ -0,0 +1,802 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests for plugins/generic/entries.py (Entries), exercising dumpTable /
dumpAll / dumpFoundTables / dumpFoundColumn by MOCKING the injection layer
(lib.request.inject.getValue) and the dumper.
No network and no DBMS are involved: conf.direct=True selects the simple inband
branches, or conf.direct=False with a BOOLEAN injection state selects the
inference (blind) branches; inject.getValue is patched to return canned rows in
the exact shape the methods parse, and conf.dumper is replaced with a recording
stub so we can assert on what each method produced (kb.data caches / returned
dicts). Every test restores all touched conf.* / kb.* / patched module attributes
in tearDown so nothing leaks.
"""
import os
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.common import Backend
from lib.core.data import conf, kb
from lib.core.enums import EXPECTED, PAYLOAD
import plugins.generic.search as smod
import plugins.generic.entries as emod
import plugins.generic.custom as cmod
import plugins.generic.misc as mmod
from plugins.generic.entries import Entries
# --------------------------------------------------------------------------- #
# Helpers/base from tests/test_search_enum.py (inband TestEntries)
# --------------------------------------------------------------------------- #
class _RecordingDumperSE(object):
"""Minimal stand-in for conf.dumper that records calls instead of printing/writing."""
def __init__(self):
self.reset()
def reset(self):
self.listed = [] # (header, elements)
self.dbTablesArg = None
self.dbColumnsArg = None
self.dbTableColumnsArg = None
self.tableValues = []
def lister(self, header, elements, content_type=None, sort=True):
self.listed.append((header, list(elements) if elements else []))
def dbTables(self, dbTables):
self.dbTablesArg = dbTables
def dbColumns(self, dbColumnsDict, colConsider, dbs):
self.dbColumnsArg = (dbColumnsDict, colConsider, dbs)
def dbTableColumns(self, tableColumns, content_type=None):
self.dbTableColumnsArg = tableColumns
def dbTableValues(self, tableValues):
self.tableValues.append(tableValues)
class _TestEntriesSE(Entries):
"""Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables)."""
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {} # {db: {tbl: {col: type}}}
self.getTablesResult = {} # value assigned to kb.data.cachedTables
self.getColumnsCalls = []
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
self.getColumnsCalls.append((conf.db, conf.tbl))
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
kb.data.cachedTables = dict(self.getTablesResult)
class _SearchEnumBase(unittest.TestCase):
def setUp(self):
# Save mutated globals
self._saved_conf = {k: conf.get(k) for k in (
"db", "tbl", "col", "direct", "excludeSysDbs", "exclude", "search",
"disableHashing", "noKeyset", "keyset", "forcePivoting",
)}
self._saved_dumper = conf.get("dumper")
self._search_getValue = smod.inject.getValue
self._entries_getValue = emod.inject.getValue
self._search_readInput = smod.readInput
self._entries_readInput = emod.readInput
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_cachedColumns = kb.data.get("cachedColumns")
self._saved_cachedTables = kb.data.get("cachedTables")
self._saved_dumpedTable = kb.data.get("dumpedTable")
self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt")
self._saved_permissionFlag = kb.get("permissionFlag")
set_dbms("MySQL")
conf.direct = True
conf.excludeSysDbs = False
conf.exclude = None
conf.search = True
conf.disableHashing = True
conf.noKeyset = True
conf.keyset = False
conf.forcePivoting = False
conf.dumper = _RecordingDumperSE()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.data.cachedTables = {}
kb.data.dumpedTable = {}
kb.dumpKeyboardInterrupt = False
kb.permissionFlag = False
# Non-interactive prompts: collapse readInput to its default.
def _readInput(message, default=None, checkBatch=True, boolean=False):
if boolean:
return True if (default in (None, 'Y', 'y', True)) else False
return default
smod.readInput = _readInput
emod.readInput = _readInput
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
smod.inject.getValue = self._search_getValue
emod.inject.getValue = self._entries_getValue
smod.readInput = self._search_readInput
emod.readInput = self._entries_readInput
kb.data.has_information_schema = self._saved_has_is
kb.data.cachedColumns = self._saved_cachedColumns
kb.data.cachedTables = self._saved_cachedTables
kb.data.dumpedTable = self._saved_dumpedTable
kb.dumpKeyboardInterrupt = self._saved_dumpKbInt
kb.permissionFlag = self._saved_permissionFlag
class TestEntries(_SearchEnumBase):
def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntriesSE()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
# --- dumpTable: inband (conf.direct) ------------------------------------
def test_dump_table_inband_rows(self):
e = self._entries_with_cols(cols=("id", "name"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
# MySQL inband dump returns a list of [colVal, colVal] rows.
emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]]
e.dumpTable()
dumped = conf.dumper.tableValues[-1]
self.assertEqual(dumped["__infos__"]["count"], 2)
self.assertEqual(dumped["__infos__"]["table"], "users")
self.assertEqual(dumped["__infos__"]["db"], "testdb")
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
def test_dump_table_uses_foundData(self):
e = _TestEntriesSE()
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["x"]]
foundData = {"testdb": {"users": {"id": "int"}}}
e.dumpTable(foundData=foundData)
# foundData short-circuits column discovery: getColumns must not run.
self.assertEqual(e.getColumnsCalls, [])
self.assertIn("id", conf.dumper.tableValues[-1])
def test_dump_table_no_columns_skips(self):
e = _TestEntriesSE()
e.getColumnsResult = {} # discovery yields nothing
conf.db = "testdb"
conf.tbl = "ghost"
conf.col = None
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
e.dumpTable()
# No columns => no values dumped.
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_empty_entries(self):
e = self._entries_with_cols(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: None # no rows
e.dumpTable()
# Nothing retrieved => dumpedTable empty => dbTableValues not called.
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_current_db(self):
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
conf.db = None # triggers getCurrentDb() -> "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["7"]]
e.dumpTable()
self.assertEqual(conf.db, "testdb")
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"])
def test_dump_table_multiple_db_error(self):
e = _TestEntriesSE()
conf.db = "a,b"
conf.tbl = "users"
conf.col = None
from lib.core.exception import SqlmapMissingMandatoryOptionException
self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable)
def test_dump_table_get_tables_when_no_tbl(self):
e = _TestEntriesSE()
e.getTablesResult = {"testdb": ["users"]}
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
conf.db = "testdb"
conf.tbl = None
conf.col = None
emod.inject.getValue = lambda *a, **k: [["42"]]
e.dumpTable()
# Tables were discovered via getTables, then the row dumped.
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"])
# --- dumpAll: single-db delegation --------------------------------------
def test_dump_all_single_db_delegates(self):
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
# dumpAll with db set & tbl None must delegate straight to dumpTable.
conf.db = "testdb"
conf.tbl = None
conf.col = None
e.getTablesResult = {"testdb": ["users"]}
emod.inject.getValue = lambda *a, **k: [["9"]]
e.dumpAll()
self.assertTrue(conf.dumper.tableValues)
# --------------------------------------------------------------------------- #
# Helpers/base from tests/test_generic_more.py (inband dump branches)
# --------------------------------------------------------------------------- #
class _RecordingDumperGM(object):
"""Recording stand-in for conf.dumper (no printing / file writing)."""
def __init__(self):
self.tableValues = []
self.sqlQueries = []
def dbTableValues(self, tableValues):
self.tableValues.append(tableValues)
def sqlQuery(self, query, queryRes):
self.sqlQueries.append((query, queryRes))
class _TestEntriesGM(Entries):
"""Entries with cross-mixin collaborators stubbed.
forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by
sibling mixins; we emulate column/table discovery by populating kb.data.cached*
from canned attributes, exactly as the production plugins do.
"""
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {} # assigned to kb.data.cachedColumns
self.getTablesResult = {} # assigned to kb.data.cachedTables
self.getColumnsCalls = []
self.getTablesCalls = 0
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
self.getColumnsCalls.append((conf.db, conf.tbl))
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
self.getTablesCalls += 1
kb.data.cachedTables = dict(self.getTablesResult)
class _GenericBase(unittest.TestCase):
"""Snapshot/restore for everything the generic mixins touch."""
_CONF_KEYS = (
"db", "tbl", "col", "direct", "batch", "exclude", "search",
"disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere",
"tmpPath", "sqlQuery", "sqlFile", "regKey", "regVal", "regData",
"regType", "osPwn", "osShell", "cleanup", "privEsc",
)
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_dumper = conf.get("dumper")
self._saved_getValue = {
emod: emod.inject.getValue,
cmod: cmod.inject.getValue,
mmod: mmod.inject.getValue,
}
self._saved_goStacked = {
cmod: cmod.inject.goStacked,
mmod: mmod.inject.goStacked,
}
self._saved_emod_readInput = emod.readInput
self._saved_mmod_readInput = mmod.readInput
self._saved_kb = {
"cachedColumns": kb.data.get("cachedColumns"),
"cachedTables": kb.data.get("cachedTables"),
"dumpedTable": kb.data.get("dumpedTable"),
"has_information_schema": kb.data.get("has_information_schema"),
"dumpKeyboardInterrupt": kb.get("dumpKeyboardInterrupt"),
"permissionFlag": kb.get("permissionFlag"),
"hintValue": kb.get("hintValue"),
"injection_data": kb.injection.data,
"bannerFp": kb.get("bannerFp"),
"os": kb.get("os"),
}
self._saved_forceDbms = kb.get("forcedDbms")
conf.direct = True
conf.batch = True
conf.exclude = None
conf.search = False
conf.disableHashing = True
conf.noKeyset = True
conf.keyset = False
conf.forcePivoting = False
conf.dumpWhere = None
conf.dumper = _RecordingDumperGM()
kb.data.cachedColumns = {}
kb.data.cachedTables = {}
kb.data.dumpedTable = {}
kb.data.has_information_schema = True
kb.dumpKeyboardInterrupt = False
kb.permissionFlag = False
def _readInput(message, default=None, checkBatch=True, boolean=False):
if boolean:
return default in (None, 'Y', 'y', True)
return default
emod.readInput = _readInput
mmod.readInput = _readInput
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
for mod, fn in self._saved_getValue.items():
mod.inject.getValue = fn
for mod, fn in self._saved_goStacked.items():
mod.inject.goStacked = fn
emod.readInput = self._saved_emod_readInput
mmod.readInput = self._saved_mmod_readInput
kb.data.cachedColumns = self._saved_kb["cachedColumns"]
kb.data.cachedTables = self._saved_kb["cachedTables"]
kb.data.dumpedTable = self._saved_kb["dumpedTable"]
kb.data.has_information_schema = self._saved_kb["has_information_schema"]
kb.dumpKeyboardInterrupt = self._saved_kb["dumpKeyboardInterrupt"]
kb.permissionFlag = self._saved_kb["permissionFlag"]
kb.hintValue = self._saved_kb["hintValue"]
kb.injection.data = self._saved_kb["injection_data"]
kb.bannerFp = self._saved_kb["bannerFp"]
kb.os = self._saved_kb["os"]
kb.forcedDbms = self._saved_forceDbms
@staticmethod
def _force_os(os_name):
# Backend.setOs only assigns when kb.os is currently None; reset first so
# tests can deterministically pin the back-end OS.
kb.os = None
Backend.setOs(os_name)
class TestEntriesDumpTable(_GenericBase):
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntriesGM()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
def test_exclude_filters_columns(self):
set_dbms("MySQL")
e = self._entries(cols=("id", "secret"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.exclude = "secret"
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpTable()
dumped = conf.dumper.tableValues[-1]
self.assertIn("id", dumped)
self.assertNotIn("secret", dumped)
def test_exclude_all_columns_skips(self):
set_dbms("MySQL")
e = self._entries(cols=("secret",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.exclude = "secret"
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
e.dumpTable()
# all columns excluded => "no usable column names" => nothing dumped
self.assertEqual(conf.dumper.tableValues, [])
def test_dumpwhere_rewrites_query(self):
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.dumpWhere = "id>5"
captured = {}
def gv(query, *a, **k):
captured["query"] = query
return [["9"]]
emod.inject.getValue = gv
e.dumpTable()
# agent.whereQuery folds conf.dumpWhere into the dump query
self.assertIn("id>5", captured["query"])
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"])
def test_disablehashing_false_path(self):
# conf.disableHashing False => attackDumpedTable() is invoked; with no
# hashes present it must complete without raising and still emit values.
set_dbms("MySQL")
e = self._entries(cols=("id", "name"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.disableHashing = False
emod.inject.getValue = lambda *a, **k: [["1", "alice"]]
# Spy on attackDumpedTable: with disableHashing False it MUST be invoked
# after the values are dumped. A recorder replaces it so we can assert the
# call happened (and no real dictionary attack runs).
saved_attack = emod.attackDumpedTable
calls = {"n": 0}
emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1)
try:
e.dumpTable()
finally:
emod.attackDumpedTable = saved_attack
self.assertEqual(calls["n"], 1)
self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1)
def test_missing_columns_skips_table(self):
# getColumns yields nothing for the targeted table => skip without fetching.
set_dbms("MySQL")
e = _TestEntriesGM()
e.getColumnsResult = {"testdb": {"other": {"id": "int"}}}
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
e.dumpTable()
self.assertEqual(conf.dumper.tableValues, [])
def test_multiple_tables_one_dumped(self):
set_dbms("MySQL")
e = _TestEntriesGM()
e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}}
conf.db = "testdb"
conf.tbl = "users,posts"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpTable()
# both tables share the same cachedColumns dict => both dumped
tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues]
self.assertIn("users", tables)
self.assertIn("posts", tables)
def test_metadb_suffix_db(self):
# A db whose name carries the METADB_SUFFIX must not get a "db" prefix in
# kb.dumpTable, and dumping still succeeds.
from lib.core.settings import METADB_SUFFIX
set_dbms("MySQL")
metadb = "x%s" % METADB_SUFFIX
e = self._entries(db=metadb, tbl="t", cols=("c",))
conf.db = metadb
conf.tbl = "t"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["v"]]
e.dumpTable()
self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"])
class TestEntriesDumpAll(_GenericBase):
def test_dumpall_multiple_dbs_tables(self):
set_dbms("MySQL")
e = _TestEntriesGM()
conf.db = None
conf.tbl = None
conf.col = None
e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]}
# dumpTable re-discovers columns per (db, tbl); supply both.
e.getColumnsResult = {
"db1": {"t1": {"a": "int"}},
"db2": {"t2": {"b": "int"}},
}
emod.inject.getValue = lambda *a, **k: [["x"]]
e.dumpAll()
# Every table contributed a values batch.
self.assertEqual(len(conf.dumper.tableValues), 2)
def test_dumpall_list_cached_tables(self):
# cachedTables as a bare list => wrapped under {None: [...]}.
set_dbms("MySQL")
e = _TestEntriesGM()
conf.db = None
conf.tbl = None
conf.col = None
# getTables sets cachedTables; emulate the list shape directly.
class _ListTables(_TestEntriesGM):
def getTables(self_inner, bruteForce=None):
kb.data.cachedTables = ["users"]
e = _ListTables()
# dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the
# None db via getCurrentDb() -> "testdb", so columns live under "testdb".
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpAll()
self.assertTrue(conf.dumper.tableValues)
# The bare-list None db must be resolved via getCurrentDb() -> "testdb"
# before the dump; assert the dumped __infos__ carries the real db (not
# None) for the requested "users" table.
infos = conf.dumper.tableValues[-1]["__infos__"]
self.assertEqual(infos["db"], "testdb")
self.assertEqual(infos["table"], "users")
def test_dumpall_exclude_skips_table(self):
set_dbms("MySQL")
e = _TestEntriesGM()
conf.db = None
conf.tbl = None
conf.col = None
conf.exclude = "secret"
e.getTablesResult = {"db1": ["secret", "users"]}
e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}}
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpAll()
tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues]
self.assertIn("users", tables)
self.assertNotIn("secret", tables)
class TestEntriesDumpFound(_GenericBase):
def _entries(self):
e = _TestEntriesGM()
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
return e
def test_dump_found_tables_yes_all(self):
set_dbms("MySQL")
e = self._entries()
emod.inject.getValue = lambda *a, **k: [["1"]]
# batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices.
e.dumpFoundTables({"testdb": ["users"]})
self.assertTrue(conf.dumper.tableValues)
# The interactive selection must dump the REQUESTED db/table, not just
# "something": assert the dumped __infos__ maps to testdb.users.
infos = conf.dumper.tableValues[-1]["__infos__"]
self.assertEqual(infos["db"], "testdb")
self.assertEqual(infos["table"], "users")
def test_dump_found_tables_declined(self):
set_dbms("MySQL")
e = self._entries()
def _no(message, default=None, checkBatch=True, boolean=False):
if boolean:
return False
return default
emod.readInput = _no
emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined")
e.dumpFoundTables({"testdb": ["users"]})
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_found_column_yes_all(self):
set_dbms("MySQL")
e = self._entries()
emod.inject.getValue = lambda *a, **k: [["1"]]
dbs = {"testdb": {"users": {"id": "int"}}}
e.dumpFoundColumn(dbs, foundCols=None, colConsider='1')
self.assertTrue(conf.dumper.tableValues)
# The selection must dump the REQUESTED db/table mapping, not just
# "something": assert the dumped __infos__ maps to testdb.users.
infos = conf.dumper.tableValues[-1]["__infos__"]
self.assertEqual(infos["db"], "testdb")
self.assertEqual(infos["table"], "users")
# --------------------------------------------------------------------------- #
# Helpers/base from tests/test_generic_enum_more.py (inference branches)
# --------------------------------------------------------------------------- #
class _RecordingDumperInf(object):
def __init__(self):
self.tableValues = []
def dbTableValues(self, tableValues):
self.tableValues.append(tableValues)
class _TestEntriesInf(Entries):
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {}
self.getTablesResult = {}
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
kb.data.cachedTables = dict(self.getTablesResult)
class _EntriesBase(unittest.TestCase):
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search",
"disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_dumper = conf.get("dumper")
self._gv = emod.inject.getValue
self._cbe = emod.inject.checkBooleanExpression
self._readInput = emod.readInput
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_cachedColumns = kb.data.get("cachedColumns")
self._saved_cachedTables = kb.data.get("cachedTables")
self._saved_dumpedTable = kb.data.get("dumpedTable")
self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt")
self._saved_permissionFlag = kb.get("permissionFlag")
self._saved_injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = False
conf.technique = None
conf.exclude = None
conf.search = False
conf.disableHashing = True
conf.noKeyset = True
conf.keyset = False
conf.forcePivoting = False
conf.dumpWhere = None
conf.dumper = _RecordingDumperInf()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.data.cachedTables = {}
kb.data.dumpedTable = {}
kb.dumpKeyboardInterrupt = False
kb.permissionFlag = False
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None))
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
emod.inject.getValue = self._gv
emod.inject.checkBooleanExpression = self._cbe
emod.readInput = self._readInput
kb.data.has_information_schema = self._saved_has_is
kb.data.cachedColumns = self._saved_cachedColumns
kb.data.cachedTables = self._saved_cachedTables
kb.data.dumpedTable = self._saved_dumpedTable
kb.dumpKeyboardInterrupt = self._saved_dumpKbInt
kb.permissionFlag = self._saved_permissionFlag
kb.injection.data = self._saved_injection_data
class TestEntriesInference(_EntriesBase):
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntriesInf()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
def test_dump_table_inference_column_pivot(self):
# Blind dump (conf.direct=False, BOOLEAN available): a row count, then one
# value per (index, column). Assert the per-column pivoted values match.
set_dbms("MySQL")
e = self._entries(cols=("id", "name"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
# data[index][column] -> value. 2 rows, columns id/name.
data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return "2" # row count
# MySQL blind cell query: 'SELECT <col> FROM testdb.users ORDER BY ...
# LIMIT <index>,1'. The row index is the LIMIT offset; the column is the
# SELECT projection.
import re as _re
idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1))
proj = query.split(" FROM ", 1)[0]
col = "name" if "name" in proj else "id"
return data[idx][col]
emod.inject.getValue = gv
e.dumpTable()
dumped = conf.dumper.tableValues[-1]
self.assertEqual(dumped["__infos__"]["count"], 2)
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
def test_dump_table_inference_empty_table(self):
# A zero row count in the inference path yields empty per-column value
# lists and no dbTableValues emission (dumpedTable stays effectively empty).
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table"))
e.dumpTable()
# count 0 => empty entries => nothing dumped
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_inference_count_failure_skips(self):
# A non-numeric count in the inference path => the table is skipped with a
# warning, no values dumped.
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return None # count failed
self.fail("must not fetch cells when count failed")
emod.inject.getValue = gv
e.dumpTable()
self.assertEqual(conf.dumper.tableValues, [])
if __name__ == "__main__":
unittest.main()

View file

@ -33,7 +33,6 @@ bootstrap()
from lib.core.data import conf, kb
from lib.core.convert import encodeHex, encodeBase64, getText
from lib.core.enums import PAYLOAD
# --------------------------------------------------------------------------- #

View file

@ -1,865 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional unit tests for the generic enumeration mixins, deliberately targeting
branches NOT already exercised by tests/test_databases_enum.py,
tests/test_users_enum.py, tests/test_search_enum.py and tests/test_generic_more.py
(which cover the conf.direct INBAND happy paths).
This file drives the OTHER branches:
* plugins/generic/databases.py - the INFERENCE paths (conf.direct=False +
isInferenceAvailable via kb.injection BOOLEAN state: count -> per-row getValue),
the MSSQL inband-paging fallback in getDbs(), getColumns onlyColNames / dumpMode,
the getColumns MySQL<5 / ACCESS bruteforce fallback, getCount over cachedTables,
and getStatements/getProcedures empty/none branches.
* plugins/generic/users.py - getPrivileges role/grant parsing per DBMS in BOTH the
inband path (PGSQL digit columns, MySQL<5 Y/N, Firebird letters, DB2 grant codes)
and the INFERENCE path (count then per-index privilege), getPasswordHashes
grouping/dedup in the inference path, getUsers inference, isDba MSSQL.
* plugins/generic/entries.py - dumpTable INFERENCE path (count -> column-pivot via
per-(index,column) getValue), the empty-table branch, the count-failure skip,
and the resolveKeysetCursor disabling via conf.noKeyset.
* plugins/generic/search.py - searchDb / searchTable / searchColumn INFERENCE
paths (count then per-index getValue), and the MySQL<5 bruteforce branch of
searchTable / searchColumn.
Recipe (proven in tests/test_databases_enum.py): patch the module's inject.getValue
with canned rows in the EXACT shape the branch parses; for inference branches return
a positive int for EXPECTED.INT count calls then the per-row/per-index values; set the
needed kb.data flags; assert the exact resulting structure (sorted lists,
{db:{tbl:{col:type}}} dicts, privilege sets, dumpedTable values).
CRITICAL STATE HYGIENE: every test snapshots and restores conf.*, the patched
inject.getValue (per module), kb.data.cached*, kb.hintValue, kb.injection.data,
Backend/forcedDbms in tearDown so nothing leaks into the rest of the suite.
"""
import os
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.data import conf, kb
from lib.core.enums import EXPECTED, PAYLOAD
import plugins.generic.databases as dbmod
import plugins.generic.users as umod
import plugins.generic.search as smod
import plugins.generic.entries as emod
from plugins.generic.databases import Databases
from plugins.generic.users import Users
from plugins.generic.search import Search
from plugins.generic.entries import Entries
_NOOP = lambda self: None
def _inference_gv(count, sequence):
"""Build an inject.getValue stub for blind inference branches.
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
yields the next item from `sequence` wrapped as a single-cell row ([value]),
cycling if exhausted. This mirrors the count-then-per-row contract of every
isInferenceAvailable() branch.
"""
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(count)
val = sequence[state["i"] % len(sequence)]
state["i"] += 1
return [val]
return gv
# --------------------------------------------------------------------------- #
# databases.py
# --------------------------------------------------------------------------- #
class _DbBase(unittest.TestCase):
_CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude",
"getComments", "excludeSysDbs", "search", "freshQueries")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_getValue = dbmod.inject.getValue
self._saved_checkBool = dbmod.inject.checkBooleanExpression
self._saved_injection_data = kb.injection.data
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_hintValue = kb.get("hintValue")
self._saved_choices = dict(kb.choices)
self._saved_readInput = dbmod.readInput
self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None)
Databases.forceDbmsEnum = _NOOP
conf.getComments = False
conf.excludeSysDbs = False
conf.exclude = None
conf.search = False
conf.freshQueries = False
conf.col = None
kb.data.has_information_schema = True
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
dbmod.inject.getValue = self._saved_getValue
dbmod.inject.checkBooleanExpression = self._saved_checkBool
dbmod.readInput = self._saved_readInput
kb.injection.data = self._saved_injection_data
kb.data.has_information_schema = self._saved_has_is
kb.hintValue = self._saved_hintValue
kb.choices.clear()
kb.choices.update(self._saved_choices)
if self._saved_forceDbmsEnum is not None:
Databases.forceDbmsEnum = self._saved_forceDbmsEnum
else:
try:
del Databases.forceDbmsEnum
except AttributeError:
pass
def _fresh(self):
d = Databases()
kb.data.currentDb = ""
kb.data.cachedDbs = []
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.data.cachedCounts = {}
kb.data.cachedStatements = []
kb.data.cachedProcedures = []
return d
def _inference(self):
conf.direct = False
conf.technique = None
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
class TestDatabasesInference(_DbBase):
def test_get_columns_inference_pgsql_types(self):
# Blind column enumeration on PostgreSQL: a count, then for each index a
# column name followed by its type. Assert the {db:{tbl:{col:type}}} parse.
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
conf.db = "public"
conf.tbl = "users"
names = ["id", "email"]
state = {"i": 0, "name": True}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(names))
if state["name"]:
val = names[state["i"] % len(names)]
state["i"] += 1
state["name"] = False
return [val]
state["name"] = True
return ["integer"]
dbmod.inject.getValue = gv
result = d.getColumns()
cols = result["public"]["users"]
self.assertEqual(len(cols), 2)
self.assertEqual(cols.get("id"), "integer")
def test_get_columns_inference_dump_mode_collist(self):
# dumpMode with an explicit conf.col list: in the inference branch the
# columns are taken straight from colList (no count/type queries at all)
# and stored with value None. Asserting no getValue ran proves the
# dump-mode shortcut, not a network round-trip.
set_dbms("MySQL")
self._inference()
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
conf.col = "id,name"
def boom(*a, **k):
raise AssertionError("dumpMode+colList must not query in inference branch")
dbmod.inject.getValue = boom
result = d.getColumns(dumpMode=True)
cols = result["testdb"]["users"]
# "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it;
# both columns must be present (count, since exact key varies by quoting).
self.assertEqual(len(cols), 2)
self.assertIn("id", cols)
self.assertIsNone(cols.get("id"))
def test_get_count_over_cached_tables_inference(self):
# getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount.
# Drive the inband table fetch + per-table count and assert the
# {db:{count:[tables]}} grouping (tables sharing a count are grouped).
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = None
kb.data.cachedTables = {"testdb": ["users", "posts"]}
counts = {"users": "5", "posts": "5"}
def gv(query, *a, **k):
for t, c in counts.items():
if t in query:
return c
return "0"
dbmod.inject.getValue = gv
result = d.getCount()
# both tables have count 5 -> grouped under the same key
self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"])
def test_get_statements_count_zero_returns_empty(self):
# Inference path: a zero count short-circuits to the (empty) cache.
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
# getStatements compares the count with the int literal 0 (count == 0), so
# the count stub must return an int 0 (not "0") to take the empty branch.
dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0")
result = d.getStatements()
self.assertEqual(result, [])
def test_get_procedures_inference(self):
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"])
result = d.getProcedures()
self.assertEqual(sorted(result), ["sp_a", "sp_b"])
def test_get_dbs_mssql_inband_paging(self):
# MSSQL with no rows from the primary query falls into the query2 paging
# loop (one indexed query per db until a blank value stops it).
set_dbms("Microsoft SQL Server")
conf.direct = True
d = self._fresh()
dbs = ["master", "model"]
def gv(query, *a, **k):
# The primary inband query is 'SELECT name FROM master..sysdatabases'
# (no DB_NAME); make it return nothing so getDbs falls into the
# 'SELECT DB_NAME(<index>)' paging loop (query2).
if "DB_NAME" not in query:
return None
import re as _re
idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0])
return dbs[idx] if idx < len(dbs) else ""
dbmod.inject.getValue = gv
result = d.getDbs()
self.assertEqual(sorted(result), ["master", "model"])
def test_get_tables_inference_grouped_per_db(self):
# Blind table enumeration: count for the db, then one table name per index.
set_dbms("MySQL")
self._inference()
d = self._fresh()
conf.db = "shop"
conf.tbl = None
dbmod.inject.getValue = _inference_gv(2, ["orders", "items"])
result = d.getTables()
self.assertIn("shop", result)
self.assertEqual(sorted(result["shop"]), ["items", "orders"])
class TestDatabasesBruteForce(_DbBase):
def test_get_columns_mysql_lt5_bruteforce_decline(self):
# MySQL < 5 (no information_schema) forces bruteForce in getColumns; with
# the common-column-existence prompt answered 'N' it returns None without
# issuing any column query.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
kb.data.has_information_schema = False
kb.choices.columnExists = None
dbmod.readInput = lambda *a, **k: "N"
def boom(*a, **k):
raise AssertionError("bruteForce decline must not query columns")
dbmod.inject.getValue = boom
result = d.getColumns()
self.assertIsNone(result)
def test_get_columns_bruteforce_dumpmode_collist_on_decline(self):
# bruteForce + decline + dumpMode + colList: the columns from colList are
# stored with None type (the dump-mode salvage branch), not dropped.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
conf.col = "a,b"
kb.data.has_information_schema = False
kb.choices.columnExists = None
dbmod.readInput = lambda *a, **k: "N"
dbmod.inject.getValue = lambda *a, **k: None
result = d.getColumns(dumpMode=True)
cols = result["testdb"]["users"]
self.assertEqual(sorted(cols.keys()), ["a", "b"])
self.assertIsNone(cols.get("a"))
# --------------------------------------------------------------------------- #
# users.py
# --------------------------------------------------------------------------- #
class _UsersBase(unittest.TestCase):
def setUp(self):
self._direct = conf.direct
self._technique = conf.technique
self._user = conf.user
self._gv = umod.inject.getValue
self._cbe = umod.inject.checkBooleanExpression
self._store = umod.storeHashesToFile
self._attack = umod.attackCachedUsersPasswords
self._readInput = umod.readInput
self._his = kb.data.get("has_information_schema")
self._injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = True
conf.user = None
kb.data.has_information_schema = True
umod.storeHashesToFile = lambda *a, **k: None
umod.attackCachedUsersPasswords = lambda *a, **k: None
umod.readInput = lambda *a, **k: "N"
def tearDown(self):
conf.direct = self._direct
conf.technique = self._technique
conf.user = self._user
umod.inject.getValue = self._gv
umod.inject.checkBooleanExpression = self._cbe
umod.storeHashesToFile = self._store
umod.attackCachedUsersPasswords = self._attack
umod.readInput = self._readInput
kb.injection.data = self._injection_data
if self._his is None:
kb.data.pop("has_information_schema", None)
else:
kb.data.has_information_schema = self._his
def _inference(self):
conf.direct = False
conf.technique = None
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
class TestUsersPrivilegesInband(_UsersBase):
def test_privileges_pgsql_multiple_digit_columns(self):
# PostgreSQL: privilege columns are digit flags; a column index maps to
# PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1,
# leave the rest 0; assert exactly those two privileges are parsed and that
# "super" makes the user an admin.
set_dbms("PostgreSQL")
from lib.core.dicts import PGSQL_PRIVS
ncols = max(PGSQL_PRIVS.keys())
row = ["pguser"] + ["0"] * ncols
row[1] = "1" # createdb
row[2] = "1" # super
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]})
self.assertIn("pguser", areAdmins)
def test_privileges_mysql_lt5_yn_flags(self):
# MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags
# mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv.
set_dbms("MySQL")
from lib.core.dicts import MYSQL_PRIVS
kb.data.has_information_schema = False
ncols = max(MYSQL_PRIVS.keys())
row = ["root"] + ["N"] * ncols
row[1] = "Y" # select_priv
row[3] = "Y" # update_priv
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn(MYSQL_PRIVS[1], privileges["root"])
self.assertIn(MYSQL_PRIVS[3], privileges["root"])
self.assertNotIn(MYSQL_PRIVS[2], privileges["root"])
def test_privileges_firebird_letter_codes(self):
# Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS.
set_dbms("Firebird")
from lib.core.dicts import FIREBIRD_PRIVS
umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertEqual(set(privileges["fbuser"]),
{FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]})
def test_privileges_db2_grant_codes(self):
# DB2: privilege string is "<name>,<grant-letters>"; each 'Y'/'G' letter at
# position i appends the DB2_PRIVS[i] name to the privilege.
set_dbms("DB2")
from lib.core.dicts import DB2_PRIVS
conf.user = "db2admin"
# "DBADM" plus a grant string whose first letter (position 1) is 'Y' ->
# DB2_PRIVS[1] ("CONTROLAUTH") is appended.
umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
joined = " ".join(privileges["DB2ADMIN"])
self.assertIn("DBADM", joined)
self.assertIn(DB2_PRIVS[1], joined)
class TestUsersPrivilegesInference(_UsersBase):
def test_privileges_inference_mysql(self):
# Blind privilege enumeration for a named user: count, then one privilege
# string per index. MySQL >= 5 adds each verbatim.
set_dbms("MySQL")
self._inference()
conf.user = "root"
privs = ["SELECT", "SUPER"]
umod.inject.getValue = _inference_gv(2, privs)
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
# the user key is wildcard-wrapped for the MySQL information_schema LIKE
key = [k for k in privileges if "root" in k][0]
self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"})
self.assertTrue(areAdmins) # SUPER => admin
def test_privileges_inference_oracle(self):
set_dbms("Oracle")
self._inference()
conf.user = "system"
umod.inject.getValue = _inference_gv(1, ["DBA"])
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("SYSTEM", privileges)
self.assertEqual(privileges["SYSTEM"], ["DBA"])
self.assertIn("SYSTEM", areAdmins)
class TestUsersPasswordHashesInference(_UsersBase):
def test_password_hashes_inference_grouping(self):
# Blind password-hash enumeration for two users: per-user count, then one
# hash per index. Assert each user maps to its own hash list.
set_dbms("MySQL")
self._inference()
conf.user = "root,guest"
# per-user single hash; count is 1 for every user
hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return "1"
for u, h in hashes.items():
if u in query:
return [h]
return [None]
umod.inject.getValue = gv
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertEqual(res["root"], ["*ROOTHASH"])
self.assertEqual(res["guest"], ["*GUESTHASH"])
def test_password_hashes_inference_dedup(self):
# The same hash returned twice for a user must be de-duplicated at the end
# (kb.data.cachedUsersPasswords[user] = list(set(...))).
set_dbms("MySQL")
self._inference()
conf.user = "root"
umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"])
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertEqual(res["root"], ["*DUP"])
class TestUsersGetUsersInference(_UsersBase):
def test_get_users_inference(self):
set_dbms("MySQL")
self._inference()
umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"])
users = Users()
kb.data.cachedUsers = []
res = users.getUsers()
self.assertEqual(sorted(res), ["guest@%", "root@localhost"])
def test_is_dba_mssql(self):
# MSSQL isDba goes through the generic checkBooleanExpression branch.
set_dbms("Microsoft SQL Server")
umod.inject.checkBooleanExpression = lambda query, *a, **k: True
users = Users()
kb.data.isDba = None
self.assertTrue(users.isDba())
# --------------------------------------------------------------------------- #
# entries.py - inference (blind) dump path
# --------------------------------------------------------------------------- #
class _RecordingDumper(object):
def __init__(self):
self.tableValues = []
def dbTableValues(self, tableValues):
self.tableValues.append(tableValues)
class _TestEntries(Entries):
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {}
self.getTablesResult = {}
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
kb.data.cachedTables = dict(self.getTablesResult)
class _EntriesBase(unittest.TestCase):
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search",
"disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_dumper = conf.get("dumper")
self._gv = emod.inject.getValue
self._cbe = emod.inject.checkBooleanExpression
self._readInput = emod.readInput
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_cachedColumns = kb.data.get("cachedColumns")
self._saved_cachedTables = kb.data.get("cachedTables")
self._saved_dumpedTable = kb.data.get("dumpedTable")
self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt")
self._saved_permissionFlag = kb.get("permissionFlag")
self._saved_injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = False
conf.technique = None
conf.exclude = None
conf.search = False
conf.disableHashing = True
conf.noKeyset = True
conf.keyset = False
conf.forcePivoting = False
conf.dumpWhere = None
conf.dumper = _RecordingDumper()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.data.cachedTables = {}
kb.data.dumpedTable = {}
kb.dumpKeyboardInterrupt = False
kb.permissionFlag = False
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None))
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
emod.inject.getValue = self._gv
emod.inject.checkBooleanExpression = self._cbe
emod.readInput = self._readInput
kb.data.has_information_schema = self._saved_has_is
kb.data.cachedColumns = self._saved_cachedColumns
kb.data.cachedTables = self._saved_cachedTables
kb.data.dumpedTable = self._saved_dumpedTable
kb.dumpKeyboardInterrupt = self._saved_dumpKbInt
kb.permissionFlag = self._saved_permissionFlag
kb.injection.data = self._saved_injection_data
class TestEntriesInference(_EntriesBase):
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntries()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
def test_dump_table_inference_column_pivot(self):
# Blind dump (conf.direct=False, BOOLEAN available): a row count, then one
# value per (index, column). Assert the per-column pivoted values match.
set_dbms("MySQL")
e = self._entries(cols=("id", "name"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
# data[index][column] -> value. 2 rows, columns id/name.
data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return "2" # row count
# MySQL blind cell query: 'SELECT <col> FROM testdb.users ORDER BY ...
# LIMIT <index>,1'. The row index is the LIMIT offset; the column is the
# SELECT projection.
import re as _re
idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1))
proj = query.split(" FROM ", 1)[0]
col = "name" if "name" in proj else "id"
return data[idx][col]
emod.inject.getValue = gv
e.dumpTable()
dumped = conf.dumper.tableValues[-1]
self.assertEqual(dumped["__infos__"]["count"], 2)
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
def test_dump_table_inference_empty_table(self):
# A zero row count in the inference path yields empty per-column value
# lists and no dbTableValues emission (dumpedTable stays effectively empty).
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table"))
e.dumpTable()
# count 0 => empty entries => nothing dumped
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_inference_count_failure_skips(self):
# A non-numeric count in the inference path => the table is skipped with a
# warning, no values dumped.
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return None # count failed
self.fail("must not fetch cells when count failed")
emod.inject.getValue = gv
e.dumpTable()
self.assertEqual(conf.dumper.tableValues, [])
# --------------------------------------------------------------------------- #
# search.py - inference (blind) paths
# --------------------------------------------------------------------------- #
class _TestSearch(Search):
excludeDbsList = ["information_schema", "mysql"]
def __init__(self):
Search.__init__(self)
self.like = ('2', "='%s'") # exact match (colConsider '2')
self.dumpFoundTablesCalls = []
self.dumpFoundColumnCalls = []
def likeOrExact(self, what):
return self.like
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def dumpFoundTables(self, tables):
self.dumpFoundTablesCalls.append(tables)
def dumpFoundColumn(self, dbs, foundCols, colConsider):
self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider))
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
db, tbl, col = conf.db, conf.tbl, conf.col
if db and tbl:
kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {})
kb.data.cachedColumns[db][tbl][col] = "varchar"
class _RecDumper(object):
def __init__(self):
self.listed = []
self.dbTablesArg = None
self.dbColumnsArg = None
def lister(self, header, elements, content_type=None, sort=True):
self.listed.append((header, list(elements) if elements else []))
def dbTables(self, dbTables):
self.dbTablesArg = dbTables
def dbColumns(self, dbColumnsDict, colConsider, dbs):
self.dbColumnsArg = (dbColumnsDict, colConsider, dbs)
class _SearchBase(unittest.TestCase):
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs",
"exclude", "search")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_dumper = conf.get("dumper")
self._gv = smod.inject.getValue
self._readInput = smod.readInput
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_cachedColumns = kb.data.get("cachedColumns")
self._saved_hintValue = kb.get("hintValue")
self._saved_injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = False
conf.technique = None
conf.excludeSysDbs = False
conf.exclude = None
conf.search = True
conf.dumper = _RecDumper()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
smod.inject.getValue = self._gv
smod.readInput = self._readInput
kb.data.has_information_schema = self._saved_has_is
kb.data.cachedColumns = self._saved_cachedColumns
kb.hintValue = self._saved_hintValue
kb.injection.data = self._saved_injection_data
class TestSearchInference(_SearchBase):
def test_search_db_inference(self):
# Blind searchDb: count of matching dbs, then one db name per index.
s = _TestSearch()
conf.db = "testdb"
smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"])
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][0], "found databases")
self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"])
def test_search_db_inference_no_match(self):
# Count fails (non-numeric) => no databases appended, empty listing.
s = _TestSearch()
conf.db = "ghost"
smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails"))
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][1], [])
def test_search_table_inference_grouped(self):
# Blind searchTable, no conf.db: outer count of dbs holding the table, then
# per-db a name, then per-db a count of matching tables, then table names.
s = _TestSearch()
conf.tbl = "users"
conf.db = None
# Sequencing by the EXPECTED.INT counts + the per-index string results.
# 1st count: number of databases with the table -> 1
# 1st db name -> "testdb"
# 2nd count: number of tables in testdb -> 1
# table name -> "users"
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
v = seq["counts"][seq["ci"] % len(seq["counts"])]
seq["ci"] += 1
return v
v = seq["vals"][seq["vi"] % len(seq["vals"])]
seq["vi"] += 1
return [v]
smod.inject.getValue = gv
s.searchTable()
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]})
self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]})
def test_search_table_mysql_lt5_bruteforce_decline(self):
# MySQL < 5 forces the bruteforce path; declining the prompt returns None
# without any injection.
s = _TestSearch()
conf.tbl = "users"
conf.db = None
kb.data.has_information_schema = False
smod.readInput = lambda *a, **k: "N"
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
self.assertIsNone(s.searchTable())
def test_search_column_inference(self):
# Blind searchColumn, no db/tbl: count of dbs with the column, then db name;
# then per-db count of tables with the column, then table name -> getColumns
# folds the column into dbs.
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
v = seq["counts"][seq["ci"] % len(seq["counts"])]
seq["ci"] += 1
return v
v = seq["vals"][seq["vi"] % len(seq["vals"])]
seq["vi"] += 1
return [v]
smod.inject.getValue = gv
s.searchColumn()
dbs = conf.dumper.dbColumnsArg[2]
self.assertIn("testdb", dbs)
self.assertIn("users", dbs["testdb"])
self.assertIn("password", dbs["testdb"]["users"])
def test_search_column_mysql_lt5_bruteforce_decline(self):
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
kb.data.has_information_schema = False
smod.readInput = lambda *a, **k: "N"
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
# Declining returns None and never reaches dbColumns.
self.assertIsNone(s.searchColumn())
self.assertIsNone(conf.dumper.dbColumnsArg)
if __name__ == "__main__":
unittest.main()

View file

@ -4,14 +4,8 @@
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional unit tests for the generic plugin mixins, driving branches NOT already
covered by tests/test_search_enum.py / tests/test_databases_enum.py:
Unit tests for the generic plugin mixins covering:
* plugins/generic/entries.py - dumpTable column/table --exclude filtering, the
--where (conf.dumpWhere) query rewrite, disableHashing toggle, METADB suffix
db handling, the "no usable columns" / "missing columns" skip branches, and
dumpAll over multiple dbs/tables (dict and list shapes) plus dumpFoundTables /
dumpFoundColumn interactive flows.
* plugins/generic/custom.py - sqlQuery SELECT/non-query/stacked branches, the
MSSQL FROM rewrite, METADB suffix stripping, SqlmapNoneDataException handling,
and sqlFile.
@ -38,14 +32,13 @@ bootstrap()
from lib.core.common import Backend
from lib.core.data import conf, kb
from lib.core.enums import DBMS, OS
from lib.core.enums import OS
from lib.core.settings import NULL
import plugins.generic.entries as emod
import plugins.generic.custom as cmod
import plugins.generic.misc as mmod
import plugins.generic.takeover as tmod
from plugins.generic.entries import Entries
from plugins.generic.custom import Custom
from plugins.generic.misc import Miscellaneous
@ -64,40 +57,6 @@ class _RecordingDumper(object):
self.sqlQueries.append((query, queryRes))
# --------------------------------------------------------------------------- #
# entries.py
# --------------------------------------------------------------------------- #
class _TestEntries(Entries):
"""Entries with cross-mixin collaborators stubbed.
forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by
sibling mixins; we emulate column/table discovery by populating kb.data.cached*
from canned attributes, exactly as the production plugins do.
"""
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {} # assigned to kb.data.cachedColumns
self.getTablesResult = {} # assigned to kb.data.cachedTables
self.getColumnsCalls = []
self.getTablesCalls = 0
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
self.getColumnsCalls.append((conf.db, conf.tbl))
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
self.getTablesCalls += 1
kb.data.cachedTables = dict(self.getTablesResult)
class _GenericBase(unittest.TestCase):
"""Snapshot/restore for everything the generic mixins touch."""
@ -196,238 +155,6 @@ class _GenericBase(unittest.TestCase):
Backend.setOs(os_name)
class TestEntriesDumpTable(_GenericBase):
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntries()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
def test_exclude_filters_columns(self):
set_dbms("MySQL")
e = self._entries(cols=("id", "secret"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.exclude = "secret"
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpTable()
dumped = conf.dumper.tableValues[-1]
self.assertIn("id", dumped)
self.assertNotIn("secret", dumped)
def test_exclude_all_columns_skips(self):
set_dbms("MySQL")
e = self._entries(cols=("secret",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.exclude = "secret"
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
e.dumpTable()
# all columns excluded => "no usable column names" => nothing dumped
self.assertEqual(conf.dumper.tableValues, [])
def test_dumpwhere_rewrites_query(self):
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.dumpWhere = "id>5"
captured = {}
def gv(query, *a, **k):
captured["query"] = query
return [["9"]]
emod.inject.getValue = gv
e.dumpTable()
# agent.whereQuery folds conf.dumpWhere into the dump query
self.assertIn("id>5", captured["query"])
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"])
def test_disablehashing_false_path(self):
# conf.disableHashing False => attackDumpedTable() is invoked; with no
# hashes present it must complete without raising and still emit values.
set_dbms("MySQL")
e = self._entries(cols=("id", "name"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
conf.disableHashing = False
emod.inject.getValue = lambda *a, **k: [["1", "alice"]]
# Spy on attackDumpedTable: with disableHashing False it MUST be invoked
# after the values are dumped. A recorder replaces it so we can assert the
# call happened (and no real dictionary attack runs).
saved_attack = emod.attackDumpedTable
calls = {"n": 0}
emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1)
try:
e.dumpTable()
finally:
emod.attackDumpedTable = saved_attack
self.assertEqual(calls["n"], 1)
self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1)
def test_missing_columns_skips_table(self):
# getColumns yields nothing for the targeted table => skip without fetching.
set_dbms("MySQL")
e = _TestEntries()
e.getColumnsResult = {"testdb": {"other": {"id": "int"}}}
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
e.dumpTable()
self.assertEqual(conf.dumper.tableValues, [])
def test_multiple_tables_one_dumped(self):
set_dbms("MySQL")
e = _TestEntries()
e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}}
conf.db = "testdb"
conf.tbl = "users,posts"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpTable()
# both tables share the same cachedColumns dict => both dumped
tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues]
self.assertIn("users", tables)
self.assertIn("posts", tables)
def test_metadb_suffix_db(self):
# A db whose name carries the METADB_SUFFIX must not get a "db" prefix in
# kb.dumpTable, and dumping still succeeds.
from lib.core.settings import METADB_SUFFIX
set_dbms("MySQL")
metadb = "x%s" % METADB_SUFFIX
e = self._entries(db=metadb, tbl="t", cols=("c",))
conf.db = metadb
conf.tbl = "t"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["v"]]
e.dumpTable()
self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"])
class TestEntriesDumpAll(_GenericBase):
def test_dumpall_multiple_dbs_tables(self):
set_dbms("MySQL")
e = _TestEntries()
conf.db = None
conf.tbl = None
conf.col = None
e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]}
# dumpTable re-discovers columns per (db, tbl); supply both.
e.getColumnsResult = {
"db1": {"t1": {"a": "int"}},
"db2": {"t2": {"b": "int"}},
}
emod.inject.getValue = lambda *a, **k: [["x"]]
e.dumpAll()
# Every table contributed a values batch.
self.assertEqual(len(conf.dumper.tableValues), 2)
def test_dumpall_list_cached_tables(self):
# cachedTables as a bare list => wrapped under {None: [...]}.
set_dbms("MySQL")
e = _TestEntries()
conf.db = None
conf.tbl = None
conf.col = None
# getTables sets cachedTables; emulate the list shape directly.
class _ListTables(_TestEntries):
def getTables(self_inner, bruteForce=None):
kb.data.cachedTables = ["users"]
e = _ListTables()
# dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the
# None db via getCurrentDb() -> "testdb", so columns live under "testdb".
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpAll()
self.assertTrue(conf.dumper.tableValues)
# The bare-list None db must be resolved via getCurrentDb() -> "testdb"
# before the dump; assert the dumped __infos__ carries the real db (not
# None) for the requested "users" table.
infos = conf.dumper.tableValues[-1]["__infos__"]
self.assertEqual(infos["db"], "testdb")
self.assertEqual(infos["table"], "users")
def test_dumpall_exclude_skips_table(self):
set_dbms("MySQL")
e = _TestEntries()
conf.db = None
conf.tbl = None
conf.col = None
conf.exclude = "secret"
e.getTablesResult = {"db1": ["secret", "users"]}
e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}}
emod.inject.getValue = lambda *a, **k: [["1"]]
e.dumpAll()
tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues]
self.assertIn("users", tables)
self.assertNotIn("secret", tables)
class TestEntriesDumpFound(_GenericBase):
def _entries(self):
e = _TestEntries()
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
return e
def test_dump_found_tables_yes_all(self):
set_dbms("MySQL")
e = self._entries()
emod.inject.getValue = lambda *a, **k: [["1"]]
# batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices.
e.dumpFoundTables({"testdb": ["users"]})
self.assertTrue(conf.dumper.tableValues)
# The interactive selection must dump the REQUESTED db/table, not just
# "something": assert the dumped __infos__ maps to testdb.users.
infos = conf.dumper.tableValues[-1]["__infos__"]
self.assertEqual(infos["db"], "testdb")
self.assertEqual(infos["table"], "users")
def test_dump_found_tables_declined(self):
set_dbms("MySQL")
e = self._entries()
def _no(message, default=None, checkBatch=True, boolean=False):
if boolean:
return False
return default
emod.readInput = _no
emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined")
e.dumpFoundTables({"testdb": ["users"]})
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_found_column_yes_all(self):
set_dbms("MySQL")
e = self._entries()
emod.inject.getValue = lambda *a, **k: [["1"]]
dbs = {"testdb": {"users": {"id": "int"}}}
e.dumpFoundColumn(dbs, foundCols=None, colConsider='1')
self.assertTrue(conf.dumper.tableValues)
# The selection must dump the REQUESTED db/table mapping, not just
# "something": assert the dumped __infos__ maps to testdb.users.
infos = conf.dumper.tableValues[-1]["__infos__"]
self.assertEqual(infos["db"], "testdb")
self.assertEqual(infos["table"], "users")
# --------------------------------------------------------------------------- #
# custom.py
# --------------------------------------------------------------------------- #

View file

@ -1,293 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Edge cases / control-flow branches of the blind-SQLi inference engine
(lib/techniques/blind/inference.py) plus the pure UNION configuration helper
(lib/techniques/union/use.py configUnion).
Complements tests/test_inference_engine.py (which covers the happy-path char-by-char
extraction). Here we drive the REAL bisection() / queryOutputLength() against a mock
oracle (Request.queryPage replaced by a parser of our own parseable payload template)
to exercise the branches the engine test does not reach:
* trivial returns: payload is None, length == 0
* --first-char / --last-char range limiting (both via the function args and via
conf.firstChar / conf.lastChar)
* --hex output decoding of the assembled value
* kb.data.processChar post-processing hook
* session resume from HashDB: a fully cached value, and a PARTIAL_VALUE_MARKER
partial value that bisection continues from (against a REAL temp SQLite HashDB)
* queryOutputLength() forging + DIGITS-charset length retrieval
No network, no live target, no real DBMS - exactly like the sibling engine test.
"""
import os
import re
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.data import conf, kb
from lib.core.common import decodeDbmsHexValue
from lib.core.common import getCurrentThreadData
from lib.core.common import hashDBWrite
from lib.core.enums import CHARSET_TYPE
from lib.core.exception import SqlmapSyntaxException
from lib.core.settings import PARTIAL_VALUE_MARKER
from lib.request.connect import Connect
from lib.utils.hashdb import HashDB
import lib.techniques.blind.inference as inf
import lib.techniques.union.use as uu
# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
# greater-char marker (swapped to '=' on the final equality check). A parseable template
# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret.
TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path
_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False,
"charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False,
"repair": False, "flushSession": None, "freshQueries": None, "hashDB": None}
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False,
"resumeValues": True, "inferenceMode": False}
class _InferenceCase(unittest.TestCase):
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in _CONF}
self._saved_kb = {k: kb.get(k) for k in _KB}
self._saved_qp = Connect.queryPage
self._saved_processChar = kb.data.get("processChar")
for k, v in _CONF.items():
conf[k] = v
for k, v in _KB.items():
kb[k] = v
kb.data.processChar = None
set_dbms("MySQL")
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
for k, v in self._saved_kb.items():
kb[k] = v
kb.data.processChar = self._saved_processChar
Connect.queryPage = self._saved_qp
inf.Request.queryPage = self._saved_qp
def _install_oracle(self, secret):
def oracle(payload=None, *args, **kwargs):
m = _PARSE.search(payload)
idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3))
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
return (ch > threshold) if op == ">" else (ch == threshold)
Connect.queryPage = staticmethod(oracle)
inf.Request.queryPage = staticmethod(oracle)
@staticmethod
def _reset_thread():
td = getCurrentThreadData()
td.shared.value = ""
td.shared.index = [0]
td.shared.start = 0
td.shared.count = 0
def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs):
self._install_oracle(secret)
self._reset_thread()
if length is None:
length = len(secret)
return inf.bisection(TEMPLATE, expression, length=length, **kwargs)
class TestTrivialReturns(_InferenceCase):
def test_none_payload(self):
# payload is None -> (0, None) without ever touching the oracle
self.assertEqual(inf.bisection(None, "SELECT x"), (0, None))
def test_zero_length(self):
# length == 0 -> (0, "") short-circuit
self._install_oracle("ignored")
self._reset_thread()
self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, ""))
class TestRangeLimiting(_InferenceCase):
SECRET = "ABCDEFGH"
def test_first_char_arg(self):
# firstChar=3 -> start from the 3rd character (1-based) -> drop "AB"
_, value = self._bisect(self.SECRET, firstChar=3)
self.assertEqual(value, "CDEFGH")
def test_last_char_arg(self):
# lastChar=4 -> stop after the 4th character
_, value = self._bisect(self.SECRET, lastChar=4)
self.assertEqual(value, "ABCD")
def test_conf_first_char(self):
conf.firstChar = 4
_, value = self._bisect(self.SECRET)
self.assertEqual(value, "DEFGH")
def test_conf_last_char(self):
conf.lastChar = 3
_, value = self._bisect(self.SECRET)
self.assertEqual(value, "ABC")
def test_first_and_last_window(self):
# combined window: chars 3..6 inclusive -> "CDEF"
_, value = self._bisect(self.SECRET, firstChar=3, lastChar=6)
self.assertEqual(value, "CDEF")
class TestHexConvert(_InferenceCase):
def test_hex_output_decoded(self):
# --hex: the retrieved value is a hex string the engine decodes on the way out
conf.hexConvert = True
hexed = "48656C6C6F" # "Hello"
_, value = self._bisect(hexed)
self.assertEqual(value, "Hello")
self.assertEqual(value, decodeDbmsHexValue(hexed))
class TestProcessCharHook(_InferenceCase):
def test_process_char_applied_to_each_char(self):
# kb.data.processChar transforms every assembled character
kb.data.processChar = lambda c: c.upper()
_, value = self._bisect("abcde")
self.assertEqual(value, "ABCDE")
class TestResumeFromHashDB(_InferenceCase):
"""bisection() consults the session store first (hashDBRetrieve(checkConf=True)).
Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py)."""
def setUp(self):
_InferenceCase.setUp(self)
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
os.close(fd)
os.remove(self.path) # HashDB creates it lazily
conf.hashDB = HashDB(self.path)
# hashDBRetrieve/Write key off these
self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port"))
conf.hostname = "test.invalid"
conf.path = "/"
conf.port = 80
def tearDown(self):
conf.hostname, conf.path, conf.port = self._saved_loc
try:
conf.hashDB.closeAll()
except Exception:
pass
if os.path.exists(self.path):
os.remove(self.path)
_InferenceCase.tearDown(self)
def test_full_value_resumed(self):
# a complete cached value short-circuits the whole bisection (0 queries)
hashDBWrite("SELECT cached", "RESUMED")
conf.hashDB.flush()
count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7)
self.assertEqual(value, "RESUMED")
self.assertEqual(count, 0)
def test_partial_value_continued(self):
# a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix
# and extracts only the remaining characters
kb.inferenceMode = True # partial markers are honored only in inference mode
hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER)
conf.hashDB.flush()
count, value = self._bisect("ABCDE", expression="SELECT partial", length=5)
self.assertEqual(value, "ABCDE")
self.assertGreater(count, 0) # it did real work for "CDE"
class TestQueryOutputLength(_InferenceCase):
def test_length_retrieved(self):
# queryOutputLength forges a LENGTH() expression and runs bisection with the
# DIGITS charset; the mock "secret" is the textual length itself
self._install_oracle("42")
self._reset_thread()
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42)
def test_length_single_digit(self):
self._install_oracle("7")
self._reset_thread()
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7)
def test_digits_charset_extracts_number(self):
# direct bisection with the DIGITS charset (queryOutputLength's inner call)
_, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS)
self.assertEqual(value, "2026")
class TestConfigUnion(unittest.TestCase):
"""lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols."""
_CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50}
def setUp(self):
self._saved = {k: conf.get(k) for k in self._CONF}
self._saved_uchar = kb.get("uChar")
for k, v in self._CONF.items():
conf[k] = v
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
kb.uChar = self._saved_uchar
def test_char_and_range(self):
uu.configUnion(char="NULL", columns="2-6")
self.assertEqual(kb.uChar, "NULL")
self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6))
def test_single_column(self):
uu.configUnion(char="NULL", columns="4")
self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4))
def test_uchar_substitution_quoted(self):
# conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template
conf.uChar = "test"
uu.configUnion(char="x[CHAR]x", columns="1")
self.assertEqual(kb.uChar, "x'test'x")
def test_uchar_substitution_digit(self):
# a digit conf.uChar is substituted unquoted
conf.uChar = "88"
uu.configUnion(char="[CHAR]", columns="1")
self.assertEqual(kb.uChar, "88")
def test_conf_ucols_overrides_columns_arg(self):
# conf.uCols takes precedence over the columns argument
conf.uCols = "3-9"
uu.configUnion(char="NULL", columns="1-2")
self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9))
def test_non_integer_range_raises(self):
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc")
def test_inverted_range_raises(self):
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2")
def test_non_string_char_ignored(self):
# a non-string char leaves kb.uChar untouched (early return)
kb.uChar = "SENTINEL"
uu.configUnion(char=None, columns="1")
self.assertEqual(kb.uChar, "SENTINEL")
if __name__ == "__main__":
unittest.main(verbosity=2)

1594
tests/test_option.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,663 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional coverage for option setup / normalization helpers in
lib/core/option.py, targeting functions and branches NOT already exercised by
tests/test_option_setup.py:
* _setTamperingFunctions (loads real tamper modules into kb.tamperFunctions)
* _setPreprocessFunctions (loads a preprocess(req) script into kb.preprocessFunctions)
* _setPostprocessFunctions (loads a postprocess(page, headers, code) script)
* _setSafeVisit (parses a safe request file into kb.safeReq)
* _cleanupOptions (additional normalization branches: delay cast,
csvDel/paramDel escape, col/binaryFields split,
torType upper, abortCode, getAll, dummy->batch)
* _basicOptionValidation (additional illegal option combinations / branches)
* _normalizeOptions (string + boolean option coercion)
* setVerbosity (eta clamp + high verbose)
As in test_option_setup.py, option.py mutates the global conf/kb singletons
aggressively, so every test saves and restores the conf/kb fields it touches via
the _preserve() context manager so the shared state stays pristine for the rest
of the suite.
"""
import contextlib
import logging
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.data import conf, kb, logger, paths
from lib.core.exception import SqlmapSyntaxException
from lib.core.exception import SqlmapSystemException
from lib.core.exception import SqlmapGenericException
from lib.core.exception import SqlmapFilePathException
from lib.core.exception import SqlmapValueException
from lib.core.settings import MAX_CONNECT_RETRIES
import lib.core.option as option
_SENTINEL = object()
# scratchpad for the preprocess/postprocess/safe-req fixture files
_SCRATCH = os.environ.get("CLAUDE_SCRATCH") or os.path.join(os.path.dirname(os.path.abspath(__file__)), "_option_more_tmp")
def tearDownModule():
"""Remove the scratch fixture directory so it never lingers on disk (and so a
stray __init__.py there can't shadow imports in a subsequent run)."""
import shutil
if os.path.isdir(_SCRATCH):
shutil.rmtree(_SCRATCH, ignore_errors=True)
@contextlib.contextmanager
def _preserve(target, *keys):
"""Save the given keys of an AttribDict (conf/kb), then restore on exit.
Missing keys are restored to absent so a test can't leak a brand-new field.
"""
saved = {}
for key in keys:
saved[key] = target[key] if key in target else _SENTINEL
try:
yield
finally:
for key in keys:
if saved[key] is _SENTINEL:
try:
del target[key]
except KeyError:
pass
else:
target[key] = saved[key]
class _ImportSandboxMixin(object):
"""Loaders in option.py (tamper/preprocess/postprocess) permanently
`sys.path.insert(0, <script dir>)` and import the script module, which would
otherwise leak the scratch directory onto sys.path (shadowing later imports)
and leave stray modules in sys.modules for the rest of the suite. Snapshot
both around the test class and restore them so the shared interpreter state
stays pristine for the other ~900 tests.
"""
@classmethod
def setUpClass(cls):
cls._saved_path = list(sys.path)
cls._saved_modules = set(sys.modules)
@classmethod
def tearDownClass(cls):
sys.path[:] = cls._saved_path
for name in list(sys.modules):
if name not in cls._saved_modules:
del sys.modules[name]
class TestSetTamperingFunctions(_ImportSandboxMixin, unittest.TestCase):
"""_setTamperingFunctions imports the named tamper modules and appends their
tamper() callables to kb.tamperFunctions."""
def test_none_noop(self):
with _preserve(conf, "tamper"), _preserve(kb, "tamperFunctions"):
kb.tamperFunctions = []
conf.tamper = None
option._setTamperingFunctions()
self.assertEqual(kb.tamperFunctions, [])
def test_loads_named_scripts(self):
# 'between' (HIGHEST) before 'space2comment' (LOW) keeps priorities
# non-increasing, so the interactive "mixed order" prompt is not triggered.
with _preserve(conf, "tamper"), _preserve(kb, "tamperFunctions"):
kb.tamperFunctions = []
conf.tamper = "between,space2comment"
option._setTamperingFunctions()
self.assertEqual(len(kb.tamperFunctions), 2)
names = sorted(f.__name__ for f in kb.tamperFunctions)
self.assertEqual(names, ["between", "space2comment"])
# each loaded entry is a callable tamper function
self.assertTrue(all(callable(f) for f in kb.tamperFunctions))
def test_mixed_order_auto_resolved_in_batch(self):
# 'space2comment' (LOW) before 'between' (HIGHEST) trips the priority
# mixup; in batch mode readInput uses the 'Y' default and auto-resolves,
# sorting kb.tamperFunctions by priority (descending).
with _preserve(conf, "tamper", "batch"), _preserve(kb, "tamperFunctions"):
kb.tamperFunctions = []
conf.batch = True
conf.tamper = "space2comment,between"
option._setTamperingFunctions()
self.assertEqual(len(kb.tamperFunctions), 2)
# after auto-resolve, 'between' (HIGHEST) comes first
self.assertEqual(kb.tamperFunctions[0].__name__, "between")
def test_missing_script_raises(self):
with _preserve(conf, "tamper"), _preserve(kb, "tamperFunctions"):
kb.tamperFunctions = []
conf.tamper = "definitely_not_a_tamper_script_xyz"
self.assertRaises(SqlmapFilePathException, option._setTamperingFunctions)
class TestSetPreprocessFunctions(_ImportSandboxMixin, unittest.TestCase):
"""_setPreprocessFunctions imports a preprocess(req) script and appends it to
kb.preprocessFunctions (after a successful test-run against a dummy Request)."""
@classmethod
def setUpClass(cls):
super(TestSetPreprocessFunctions, cls).setUpClass()
if not os.path.isdir(_SCRATCH):
os.makedirs(_SCRATCH)
# an empty __init__.py is required next to the script
with open(os.path.join(_SCRATCH, "__init__.py"), "w") as f:
f.write("")
cls.script = os.path.join(_SCRATCH, "pre_ok.py")
with open(cls.script, "w") as f:
f.write("#!/usr/bin/env\n\ndef preprocess(req):\n pass\n")
cls.bad = os.path.join(_SCRATCH, "pre_no_func.py")
with open(cls.bad, "w") as f:
f.write("#!/usr/bin/env\n\ndef notpreprocess(req):\n pass\n")
def test_none_noop(self):
with _preserve(conf, "preprocess"), _preserve(kb, "preprocessFunctions"):
kb.preprocessFunctions = []
conf.preprocess = None
option._setPreprocessFunctions()
self.assertEqual(kb.preprocessFunctions, [])
def test_loads_script(self):
with _preserve(conf, "preprocess", "debug"), _preserve(kb, "preprocessFunctions"):
kb.preprocessFunctions = []
conf.debug = False
conf.preprocess = self.script
option._setPreprocessFunctions()
self.assertEqual(len(kb.preprocessFunctions), 1)
self.assertTrue(callable(kb.preprocessFunctions[0]))
def test_missing_function_raises(self):
with _preserve(conf, "preprocess", "debug"), _preserve(kb, "preprocessFunctions"):
kb.preprocessFunctions = []
conf.debug = False
conf.preprocess = self.bad
self.assertRaises(SqlmapGenericException, option._setPreprocessFunctions)
def test_missing_file_raises(self):
with _preserve(conf, "preprocess", "debug"), _preserve(kb, "preprocessFunctions"):
kb.preprocessFunctions = []
conf.debug = False
conf.preprocess = os.path.join(_SCRATCH, "nope.py")
self.assertRaises(SqlmapFilePathException, option._setPreprocessFunctions)
class TestSetPostprocessFunctions(_ImportSandboxMixin, unittest.TestCase):
"""_setPostprocessFunctions imports a postprocess(page, headers, code) script
that must return a (page, headers, code) tuple."""
@classmethod
def setUpClass(cls):
super(TestSetPostprocessFunctions, cls).setUpClass()
if not os.path.isdir(_SCRATCH):
os.makedirs(_SCRATCH)
with open(os.path.join(_SCRATCH, "__init__.py"), "w") as f:
f.write("")
cls.script = os.path.join(_SCRATCH, "post_ok.py")
with open(cls.script, "w") as f:
f.write("#!/usr/bin/env\n\ndef postprocess(page, headers=None, code=None):\n return page, headers, code\n")
cls.bad = os.path.join(_SCRATCH, "post_no_func.py")
with open(cls.bad, "w") as f:
f.write("#!/usr/bin/env\n\ndef other(page, headers=None, code=None):\n return page, headers, code\n")
def test_none_noop(self):
with _preserve(conf, "postprocess"), _preserve(kb, "postprocessFunctions"):
kb.postprocessFunctions = []
conf.postprocess = None
option._setPostprocessFunctions()
self.assertEqual(kb.postprocessFunctions, [])
def test_loads_script(self):
with _preserve(conf, "postprocess"), _preserve(kb, "postprocessFunctions"):
kb.postprocessFunctions = []
conf.postprocess = self.script
option._setPostprocessFunctions()
self.assertEqual(len(kb.postprocessFunctions), 1)
self.assertTrue(callable(kb.postprocessFunctions[0]))
def test_missing_function_raises(self):
with _preserve(conf, "postprocess"), _preserve(kb, "postprocessFunctions"):
kb.postprocessFunctions = []
conf.postprocess = self.bad
self.assertRaises(SqlmapGenericException, option._setPostprocessFunctions)
class TestSetSafeVisit(unittest.TestCase):
"""_setSafeVisit parses a raw HTTP request file into kb.safeReq, or normalizes
a bare safeUrl, and enforces safeFreq > 0."""
@classmethod
def setUpClass(cls):
if not os.path.isdir(_SCRATCH):
os.makedirs(_SCRATCH)
cls.reqfile = os.path.join(_SCRATCH, "safe_req.txt")
with open(cls.reqfile, "w") as f:
f.write("GET /safe?ping=1 HTTP/1.1\nHost: victim.example\nUser-Agent: t\n\n")
cls.badfile = os.path.join(_SCRATCH, "safe_req_bad.txt")
with open(cls.badfile, "w") as f:
f.write("this is not a valid request line\n")
def _keys(self):
return ("safeUrl", "safeReqFile", "safeFreq", "safePost")
def test_noop_when_unset(self):
with _preserve(conf, *self._keys()):
conf.safeUrl = None
conf.safeReqFile = None
conf.safeFreq = 0
option._setSafeVisit() # must not raise
def test_safe_url_scheme_prepended(self):
with _preserve(conf, *self._keys()):
conf.safeUrl = "victim.example/keepalive"
conf.safeReqFile = None
conf.safeFreq = 5
option._setSafeVisit()
self.assertTrue(conf.safeUrl.startswith("http://"), msg=conf.safeUrl)
def test_safe_url_requires_positive_freq(self):
with _preserve(conf, *self._keys()):
conf.safeUrl = "http://victim.example/k"
conf.safeReqFile = None
conf.safeFreq = 0
self.assertRaises(SqlmapSyntaxException, option._setSafeVisit)
def test_safe_req_file_parsed(self):
with _preserve(conf, *self._keys()), _preserve(kb, "safeReq"):
conf.safeUrl = None
conf.safePost = None
conf.safeReqFile = self.reqfile
conf.safeFreq = 3
option._setSafeVisit()
self.assertEqual(kb.safeReq.method, "GET")
self.assertIn("victim.example", kb.safeReq.url)
self.assertEqual(kb.safeReq.headers.get("User-Agent"), "t")
def test_safe_req_file_invalid_format_raises(self):
with _preserve(conf, *self._keys()), _preserve(kb, "safeReq"):
conf.safeUrl = None
conf.safePost = None
conf.safeReqFile = self.badfile
conf.safeFreq = 3
self.assertRaises(SqlmapSyntaxException, option._setSafeVisit)
class TestCleanupOptionsExtra(unittest.TestCase):
"""Additional _cleanupOptions normalization branches not covered by
test_option_setup.py."""
_KEYS = (
"encoding", "eta", "testParameter", "ignoreCode", "abortCode",
"paramFilter", "base64Parameter", "agent", "user", "rParam",
"paramDel", "skip", "cookie", "delay", "url", "fileRead",
"fileWrite", "fileDest", "msfPath", "tmpPath", "googleDork",
"logFile", "bulkFile", "forms", "crawlDepth", "stdinPipe",
"multipleTargets", "optimize", "os", "forceDbms", "dbms",
"uValues", "uCols", "testFilter", "csrfToken", "testSkip",
"tor", "timeSec", "retries", "code", "csvDel", "torPort",
"torType", "outputDir", "string", "getAll", "noCast",
"dumpFormat", "col", "exclude", "binaryFields", "proxy",
"proxyFile", "dummy", "batch", "scope",
)
def _base(self):
for key in self._KEYS:
conf[key] = None
conf.eta = False
conf.optimize = False
conf.tor = False
conf.getAll = False
conf.noCast = False
conf.dummy = False
conf.batch = False
conf.timeSec = 5
conf.retries = 3
conf.multipleTargets = False
@contextlib.contextmanager
def _ctx(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
yield
def test_delay_cast_to_float(self):
with self._ctx():
conf.delay = "2"
option._cleanupOptions()
self.assertEqual(conf.delay, 2.0)
self.assertIsInstance(conf.delay, float)
def test_csv_del_escape_decoded(self):
with self._ctx():
conf.csvDel = "\\t"
option._cleanupOptions()
self.assertEqual(conf.csvDel, "\t")
def test_param_del_escape_decoded(self):
with self._ctx():
conf.paramDel = "\\n"
option._cleanupOptions()
self.assertEqual(conf.paramDel, "\n")
def test_col_whitespace_normalized(self):
with self._ctx():
conf.col = "id , name , pass"
option._cleanupOptions()
self.assertEqual(conf.col, "id,name,pass")
def test_binary_fields_split(self):
with self._ctx():
conf.binaryFields = "data, blob"
option._cleanupOptions()
self.assertEqual(conf.binaryFields, ["data", "blob"])
def test_tor_type_uppercased(self):
with self._ctx():
conf.torType = "socks5"
option._cleanupOptions()
self.assertEqual(conf.torType, "SOCKS5")
def test_abort_code_empty_becomes_list(self):
with self._ctx():
conf.abortCode = None
option._cleanupOptions()
self.assertEqual(conf.abortCode, [])
def test_abort_code_invalid_raises(self):
with self._ctx():
conf.abortCode = "notanumber"
self.assertRaises(SqlmapSyntaxException, option._cleanupOptions)
def test_user_spaces_stripped(self):
with self._ctx():
conf.user = "ad min"
option._cleanupOptions()
self.assertEqual(conf.user, "admin")
def test_dummy_forces_batch(self):
with self._ctx():
conf.dummy = True
option._cleanupOptions()
self.assertTrue(conf.batch)
def test_string_escape_decoded(self):
with self._ctx():
conf.string = "a\\tb"
option._cleanupOptions()
self.assertEqual(conf.string, "a\tb")
def test_retries_clamped(self):
with self._ctx():
conf.retries = 9999
option._cleanupOptions()
# clamped to exactly MAX_CONNECT_RETRIES, not merely "less than 9999"
self.assertEqual(conf.retries, MAX_CONNECT_RETRIES)
def test_unknown_encoding_raises(self):
with self._ctx():
conf.encoding = "definitely-not-an-encoding"
self.assertRaises(SqlmapValueException, option._cleanupOptions)
class TestBasicOptionValidationExtra(unittest.TestCase):
"""Additional illegal option combinations / validation branches in
_basicOptionValidation not covered by test_option_setup.py."""
_KEYS = (
"limitStart", "limitStop", "level", "risk", "firstChar", "lastChar",
"textOnly", "nullConnection", "uValues", "uChar", "base64Parameter",
"tamper", "eta", "verbose", "direct", "url", "dbms", "tor", "proxy",
"ignoreProxy", "regexp", "timeSec", "torPort", "torType", "dumpFormat",
"technique", "threads", "predictOutput", "optimize", "csrfToken",
"csrfUrl", "csrfMethod", "csrfData", "string", "notString", "noCast",
"hexConvert", "titles", "dumpTable", "search", "dumpAll", "data",
"requestFile", "forms", "googleDork", "bulkFile", "chunked",
"cookieDel", "dbmsCred", "mobile", "agent", "crawlExclude",
"crawlDepth", "safePost", "safeUrl", "safeReqFile", "safeFreq",
"proxyFile", "proxyFreq", "checkTor", "alert", "secondUrl",
"secondReq", "http2", "osPwn",
)
def _base(self):
for key in self._KEYS:
conf[key] = None
conf.textOnly = False
conf.nullConnection = False
conf.eta = False
conf.direct = False
conf.tor = False
conf.ignoreProxy = False
conf.predictOutput = False
conf.optimize = False
conf.noCast = False
conf.hexConvert = False
conf.titles = False
conf.mobile = False
conf.chunked = False
conf.checkTor = False
conf.http2 = False
conf.osPwn = False
conf.verbose = 1
conf.timeSec = 5
conf.torPort = None
conf.torType = "SOCKS5"
conf.dumpFormat = "CSV"
conf.technique = [1, 2, 6, 4, 5]
conf.threads = 1
@contextlib.contextmanager
def _ctx(self):
with _preserve(conf, *self._KEYS):
self._base()
yield
def test_bad_limit_start_raises(self):
with self._ctx():
conf.limitStart = -1
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_bad_limit_stop_raises(self):
with self._ctx():
conf.limitStop = 0
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_first_char_gt_last_char_raises(self):
with self._ctx():
conf.firstChar = 5
conf.lastChar = 2
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_base64_tamper_incompatible(self):
with self._ctx():
conf.base64Parameter = "id"
conf.tamper = "space2comment"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_direct_dbms_incompatible(self):
with self._ctx():
conf.direct = "mysql://u:p@h/db"
conf.dbms = "MySQL"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_titles_nullconnection_incompatible(self):
with self._ctx():
conf.titles = True
conf.nullConnection = True
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_dump_search_incompatible(self):
with self._ctx():
conf.dumpTable = True
conf.search = True
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_string_notstring_incompatible(self):
with self._ctx():
conf.string = "ok"
conf.notString = "bad"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_chunked_requires_post(self):
with self._ctx():
conf.chunked = True
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_cookie_del_single_char(self):
with self._ctx():
conf.cookieDel = ";;"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_dbms_cred_format(self):
with self._ctx():
conf.dbmsCred = "rootnopassword"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_mobile_agent_incompatible(self):
with self._ctx():
conf.mobile = True
conf.agent = "UA/1.0"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_proxy_ignoreproxy_incompatible(self):
with self._ctx():
conf.proxy = "http://127.0.0.1:8080"
conf.ignoreProxy = True
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_csrf_url_requires_token(self):
with self._ctx():
conf.csrfUrl = "http://x/token"
conf.csrfToken = None
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_csrf_token_threads_incompatible(self):
with self._ctx():
conf.csrfToken = "tok"
conf.threads = 4
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_time_sec_must_be_positive(self):
with self._ctx():
conf.timeSec = 0
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_forms_requires_target(self):
with self._ctx():
conf.forms = True
conf.url = None
conf.googleDork = None
conf.bulkFile = None
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_safe_post_requires_safe_url(self):
with self._ctx():
conf.safePost = "x=1"
conf.safeUrl = None
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_proxy_freq_requires_proxy_file(self):
with self._ctx():
conf.proxyFreq = 5
conf.proxyFile = None
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_check_tor_requires_tor_or_proxy(self):
with self._ctx():
conf.checkTor = True
conf.tor = False
conf.proxy = None
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_second_url_req_incompatible(self):
with self._ctx():
conf.secondUrl = "http://x/2"
conf.secondReq = "/path/req.txt"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_alert_unsafe_requires_env(self):
# _basicOptionValidation raises SqlmapSystemException for --alert without env
with self._ctx():
saved = os.environ.pop("SQLMAP_UNSAFE_ALERT", None)
try:
conf.alert = "echo hi"
self.assertRaises(SqlmapSystemException, option._basicOptionValidation)
finally:
if saved is not None:
os.environ["SQLMAP_UNSAFE_ALERT"] = saved
class TestNormalizeOptionsExtra(unittest.TestCase):
"""_normalizeOptions coerces values by option type. test_option_setup covers
INTEGER; here cover FLOAT and BOOLEAN coercion (STRING is left untouched)."""
def test_float_coercion(self):
# 'delay' is a FLOAT option; a string value is coerced to float
opts = {"delay": "2.5"}
option._normalizeOptions(opts)
self.assertEqual(opts["delay"], 2.5)
def test_bad_float_becomes_zero(self):
opts = {"delay": "notafloat"}
option._normalizeOptions(opts)
self.assertEqual(opts["delay"], 0.0)
def test_boolean_coercion(self):
# 'forms' is a BOOLEAN option; a truthy non-empty value -> True
opts = {"forms": 1}
option._normalizeOptions(opts)
self.assertIs(opts["forms"], True)
def test_boolean_empty_false(self):
opts = {"forms": ""}
option._normalizeOptions(opts)
self.assertIs(opts["forms"], False)
class TestSetVerbosityExtra(unittest.TestCase):
"""setVerbosity branches not covered by test_option_setup.py."""
def test_eta_clamps_verbose(self):
saved_level = logger.level
try:
with _preserve(conf, "verbose", "eta"):
conf.verbose = 5
conf.eta = True
option.setVerbosity()
# with eta on and verbose > 2, verbose is clamped to 2 (DEBUG)
self.assertEqual(conf.verbose, 2)
self.assertEqual(logger.level, logging.DEBUG)
finally:
logger.setLevel(saved_level)
def test_string_verbose_coerced_to_int(self):
saved_level = logger.level
try:
with _preserve(conf, "verbose", "eta"):
conf.verbose = "1"
conf.eta = False
option.setVerbosity()
self.assertEqual(conf.verbose, 1)
self.assertEqual(logger.level, logging.INFO)
finally:
logger.setLevel(saved_level)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -1,739 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Option setup / normalization helpers in lib/core/option.py.
These exercise the (mostly) pure config-massaging functions that parse, validate
and normalize user-supplied option values into the canonical conf.*/kb.* shapes
that the rest of sqlmap relies on - WITHOUT touching the network, the DBMS, the
filesystem (beyond what bootstrap already set up) or any interactive prompt.
option.py mutates the global conf/kb singletons aggressively, so every test that
writes a conf/kb field saves and restores it via the _preserve() helper so the
shared state stays pristine for the other test files in the suite.
"""
import contextlib
import logging
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.data import conf, kb, logger
from lib.core.common import Backend
from lib.core.enums import HTTP_HEADER
from lib.core.settings import DEFAULT_USER_AGENT
from lib.core.settings import IGNORE_CODE_WILDCARD
from lib.core.exception import SqlmapSyntaxException
from lib.core.exception import SqlmapUnsupportedDBMSException
import lib.core.option as option
_SENTINEL = object()
# conf/kb fields that Backend.getIdentifiedDbms()/getOs() consult; any test that
# might touch DBMS/OS forcing snapshots ALL of them so no fingerprint state leaks
# into sibling test files (e.g. test_target_parsing's resume tests).
_BACKEND_CONF_KEYS = ("dbms", "forceDbms", "os")
_BACKEND_KB_KEYS = ("dbms", "dbmsVersion", "forcedDbms", "dbmsFilter", "os", "osVersion", "osSP")
class _BackendGuard(unittest.TestCase):
"""Mixin: fully snapshot & restore Backend-relevant conf/kb state per test."""
def setUp(self):
super(_BackendGuard, self).setUp()
self._snap_conf = {k: (conf[k] if k in conf else _SENTINEL) for k in _BACKEND_CONF_KEYS}
self._snap_kb = {k: (kb[k] if k in kb else _SENTINEL) for k in _BACKEND_KB_KEYS}
def tearDown(self):
for store, snap, keys in ((conf, self._snap_conf, _BACKEND_CONF_KEYS),
(kb, self._snap_kb, _BACKEND_KB_KEYS)):
for k in keys:
if snap[k] is _SENTINEL:
try:
del store[k]
except KeyError:
pass
else:
store[k] = snap[k]
super(_BackendGuard, self).tearDown()
@contextlib.contextmanager
def _preserve(target, *keys):
"""Save the given keys of an AttribDict (conf/kb), then restore on exit.
Missing keys are restored to absent so a test can't leak a brand-new field.
"""
saved = {}
for key in keys:
saved[key] = target[key] if key in target else _SENTINEL
try:
yield
finally:
for key in keys:
if saved[key] is _SENTINEL:
try:
del target[key]
except KeyError:
pass
else:
target[key] = saved[key]
class TestSetTechnique(unittest.TestCase):
def test_letters_to_ints(self):
# BEUST(Q) letters map to the PAYLOAD.TECHNIQUE ints (B=1,E=2,U=6,S=4,T=5)
with _preserve(conf, "technique"):
conf.technique = "BEUST"
option._setTechnique()
self.assertEqual(conf.technique, [1, 2, 6, 4, 5])
def test_lowercase_accepted(self):
with _preserve(conf, "technique"):
conf.technique = "bt"
option._setTechnique()
self.assertEqual(conf.technique, [1, 5])
def test_invalid_letter_raises(self):
with _preserve(conf, "technique"):
conf.technique = "X"
self.assertRaises(SqlmapSyntaxException, option._setTechnique)
def test_already_list_left_alone(self):
# non-string (already-normalized) value is a no-op
with _preserve(conf, "technique"):
conf.technique = [1, 6]
option._setTechnique()
self.assertEqual(conf.technique, [1, 6])
class TestSetDBMS(_BackendGuard):
def test_none_noop(self):
with _preserve(conf, "dbms"):
conf.dbms = None
option._setDBMS()
self.assertIsNone(conf.dbms)
def test_plain_canonicalized(self):
# input is lowercased then mapped to the canonical DBMS name via DBMS_ALIASES
with _preserve(conf, "dbms"):
conf.dbms = "MySQL"
option._setDBMS()
self.assertEqual(conf.dbms, "MySQL")
# non-identity case: an all-caps spelling must be lowercased and run
# through the alias map back to the canonical "MySQL" (proves the
# lower -> alias-lookup -> canonical transform actually executes, rather
# than the test passing because input already equals output)
with _preserve(conf, "dbms"):
conf.dbms = "MYSQL"
option._setDBMS()
self.assertEqual(conf.dbms, "MySQL")
def test_alias_canonicalized(self):
# "pgsql" is an alias for PostgreSQL
with _preserve(conf, "dbms"):
conf.dbms = "pgsql"
option._setDBMS()
self.assertEqual(conf.dbms.lower(), "postgresql")
def test_version_extracted_into_backend(self):
# _setDBMS calls Backend.setVersion -> mutates kb.dbmsVersion; preserve it too
with _preserve(conf, "dbms"), _preserve(kb, "dbmsVersion"):
conf.dbms = "mysql 5.7"
option._setDBMS()
self.assertEqual(conf.dbms, "MySQL")
self.assertIn("5.7", Backend.getVersion())
def test_unsupported_raises(self):
with _preserve(conf, "dbms"):
conf.dbms = "totallynotadbms"
self.assertRaises(SqlmapUnsupportedDBMSException, option._setDBMS)
class TestSetOS(_BackendGuard):
def test_none_noop(self):
with _preserve(conf, "os"):
conf.os = None
option._setOS() # must not raise
def test_valid_os_sets_backend(self):
# _setOS calls Backend.setOs -> mutates kb.os; preserve it too
with _preserve(conf, "os"), _preserve(kb, "os"):
conf.os = "Linux"
option._setOS()
self.assertEqual(Backend.getOs(), "Linux")
def test_unsupported_os_raises(self):
with _preserve(conf, "os"):
conf.os = "plan9"
self.assertRaises(SqlmapUnsupportedDBMSException, option._setOS)
class TestSetDBMSAuthentication(unittest.TestCase):
def test_none_noop(self):
with _preserve(conf, "dbmsCred", "dbmsUsername", "dbmsPassword"):
conf.dbmsCred = None
option._setDBMSAuthentication()
# nothing populated
self.assertIsNone(conf.get("dbmsUsername"))
def test_splits_user_password(self):
with _preserve(conf, "dbmsCred", "dbmsUsername", "dbmsPassword"):
conf.dbmsCred = "root:secret"
option._setDBMSAuthentication()
self.assertEqual(conf.dbmsUsername, "root")
self.assertEqual(conf.dbmsPassword, "secret")
def test_empty_password_allowed(self):
with _preserve(conf, "dbmsCred", "dbmsUsername", "dbmsPassword"):
conf.dbmsCred = "sa:"
option._setDBMSAuthentication()
self.assertEqual(conf.dbmsUsername, "sa")
self.assertEqual(conf.dbmsPassword, "")
class TestSetThreads(unittest.TestCase):
def test_zero_becomes_one(self):
with _preserve(conf, "threads"):
conf.threads = 0
option._setThreads()
self.assertEqual(conf.threads, 1)
def test_negative_becomes_one(self):
with _preserve(conf, "threads"):
conf.threads = -5
option._setThreads()
self.assertEqual(conf.threads, 1)
def test_non_int_becomes_one(self):
with _preserve(conf, "threads"):
conf.threads = None
option._setThreads()
self.assertEqual(conf.threads, 1)
def test_positive_int_preserved(self):
with _preserve(conf, "threads"):
conf.threads = 7
option._setThreads()
self.assertEqual(conf.threads, 7)
class TestSetPrefixSuffix(unittest.TestCase):
def test_no_prefix_suffix_noop(self):
with _preserve(conf, "prefix", "suffix", "boundaries"):
conf.prefix = None
conf.suffix = None
conf.boundaries = []
option._setPrefixSuffix()
self.assertEqual(conf.boundaries, [])
def test_builds_single_boundary(self):
with _preserve(conf, "prefix", "suffix", "boundaries"):
conf.prefix = "')"
conf.suffix = "-- -"
conf.boundaries = []
option._setPrefixSuffix()
self.assertEqual(len(conf.boundaries), 1)
b = conf.boundaries[0]
self.assertEqual(b.prefix, "')")
self.assertEqual(b.suffix, "-- -")
self.assertEqual(b.level, 1)
def test_ptype_single_quote(self):
with _preserve(conf, "prefix", "suffix", "boundaries"):
conf.prefix = "'"
conf.suffix = "'"
conf.boundaries = []
option._setPrefixSuffix()
self.assertEqual(conf.boundaries[0].ptype, 2)
def test_ptype_double_quote(self):
with _preserve(conf, "prefix", "suffix", "boundaries"):
conf.prefix = '"'
conf.suffix = '"'
conf.boundaries = []
option._setPrefixSuffix()
self.assertEqual(conf.boundaries[0].ptype, 4)
def test_ptype_plain(self):
with _preserve(conf, "prefix", "suffix", "boundaries"):
conf.prefix = " "
conf.suffix = ""
conf.boundaries = []
option._setPrefixSuffix()
self.assertEqual(conf.boundaries[0].ptype, 1)
class TestSetHostname(unittest.TestCase):
def test_extracts_host(self):
with _preserve(conf, "url", "hostname"):
conf.url = "http://www.example.com:8080/page?id=1"
option._setHostname()
self.assertEqual(conf.hostname, "www.example.com")
def test_no_url_noop(self):
with _preserve(conf, "url", "hostname"):
conf.url = None
conf.hostname = "preexisting"
option._setHostname()
self.assertEqual(conf.hostname, "preexisting")
class TestSetHTTPHeaderSetters(unittest.TestCase):
def test_referer_appended(self):
with _preserve(conf, "referer", "httpHeaders"):
conf.httpHeaders = []
conf.referer = "http://ref.example/"
option._setHTTPReferer()
self.assertIn((HTTP_HEADER.REFERER, "http://ref.example/"), conf.httpHeaders)
def test_referer_none_noop(self):
with _preserve(conf, "referer", "httpHeaders"):
conf.httpHeaders = []
conf.referer = None
option._setHTTPReferer()
self.assertEqual(conf.httpHeaders, [])
def test_host_appended(self):
with _preserve(conf, "host", "httpHeaders"):
conf.httpHeaders = []
conf.host = "victim.local"
option._setHTTPHost()
self.assertIn((HTTP_HEADER.HOST, "victim.local"), conf.httpHeaders)
def test_cookie_appended(self):
with _preserve(conf, "cookie", "httpHeaders"):
conf.httpHeaders = []
conf.cookie = "SESSION=abc"
option._setHTTPCookies()
self.assertIn((HTTP_HEADER.COOKIE, "SESSION=abc"), conf.httpHeaders)
class TestSetHTTPUserAgent(unittest.TestCase):
def test_explicit_agent(self):
with _preserve(conf, "agent", "mobile", "randomAgent", "httpHeaders"):
conf.httpHeaders = []
conf.mobile = False
conf.randomAgent = False
conf.agent = "MyCustomUA/1.0"
option._setHTTPUserAgent()
self.assertIn((HTTP_HEADER.USER_AGENT, "MyCustomUA/1.0"), conf.httpHeaders)
def test_default_agent_when_unset(self):
with _preserve(conf, "agent", "mobile", "randomAgent", "httpHeaders"):
conf.httpHeaders = []
conf.mobile = False
conf.randomAgent = False
conf.agent = None
option._setHTTPUserAgent()
self.assertIn((HTTP_HEADER.USER_AGENT, DEFAULT_USER_AGENT), conf.httpHeaders)
def test_existing_ua_not_duplicated(self):
with _preserve(conf, "agent", "mobile", "randomAgent", "httpHeaders"):
conf.httpHeaders = [(HTTP_HEADER.USER_AGENT, "Already/1.0")]
conf.mobile = False
conf.randomAgent = False
conf.agent = None
option._setHTTPUserAgent()
uas = [v for (h, v) in conf.httpHeaders if h.upper() == HTTP_HEADER.USER_AGENT.upper()]
self.assertEqual(uas, ["Already/1.0"])
class TestSetHTTPExtraHeaders(unittest.TestCase):
def test_parses_newline_separated(self):
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
conf.httpHeaders = []
conf.headers = "X-Foo: bar\nX-Baz: qux"
option._setHTTPExtraHeaders()
self.assertIn(("X-Foo", "bar"), conf.httpHeaders)
self.assertIn(("X-Baz", "qux"), conf.httpHeaders)
def test_escaped_newline_form(self):
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
conf.httpHeaders = []
conf.headers = "X-A: 1\\nX-B: 2"
option._setHTTPExtraHeaders()
self.assertIn(("X-A", "1"), conf.httpHeaders)
self.assertIn(("X-B", "2"), conf.httpHeaders)
def test_invalid_header_raises(self):
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
conf.httpHeaders = []
conf.headers = "no-colon-here"
self.assertRaises(SqlmapSyntaxException, option._setHTTPExtraHeaders)
def test_no_headers_adds_cache_control(self):
# with no explicit headers and no requestFile, a Cache-Control:no-cache is appended
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
conf.httpHeaders = []
conf.headers = None
conf.requestFile = None
conf.encoding = None
option._setHTTPExtraHeaders()
self.assertIn((HTTP_HEADER.CACHE_CONTROL, "no-cache"), conf.httpHeaders)
class TestNormalizeOptions(unittest.TestCase):
def test_integer_coercion(self):
# 'threads' is an INTEGER option; a string value is coerced to int in place
opts = {"threads": "5"}
option._normalizeOptions(opts)
self.assertEqual(opts["threads"], 5)
def test_bad_integer_becomes_zero(self):
opts = {"threads": "notanumber"}
option._normalizeOptions(opts)
self.assertEqual(opts["threads"], 0)
def test_none_left_alone(self):
opts = {"threads": None}
option._normalizeOptions(opts)
self.assertIsNone(opts["threads"])
def test_unknown_key_untouched(self):
opts = {"definitelyNotAnOption": "value"}
option._normalizeOptions(opts)
self.assertEqual(opts["definitelyNotAnOption"], "value")
class TestSetVerbosity(unittest.TestCase):
def _restore_logger(self):
return _preserve(conf, "verbose", "eta")
def test_none_becomes_one(self):
saved_level = logger.level
try:
with self._restore_logger():
conf.verbose = None
conf.eta = False
option.setVerbosity()
self.assertEqual(conf.verbose, 1)
self.assertEqual(logger.level, logging.INFO)
finally:
logger.setLevel(saved_level)
def test_zero_sets_error_level(self):
saved_level = logger.level
try:
with self._restore_logger():
conf.verbose = 0
conf.eta = False
option.setVerbosity()
self.assertEqual(logger.level, logging.ERROR)
finally:
logger.setLevel(saved_level)
def test_two_sets_debug_level(self):
saved_level = logger.level
try:
with self._restore_logger():
conf.verbose = 2
conf.eta = False
option.setVerbosity()
self.assertEqual(logger.level, logging.DEBUG)
finally:
logger.setLevel(saved_level)
class TestCleanupOptions(_BackendGuard):
"""_cleanupOptions touches a huge number of conf fields; preserve broadly."""
# the subset of conf keys these tests read or write
_KEYS = (
"encoding", "eta", "testParameter", "ignoreCode", "abortCode",
"paramFilter", "base64Parameter", "agent", "user", "rParam",
"paramDel", "skip", "cookie", "delay", "url", "fileRead",
"fileWrite", "fileDest", "msfPath", "tmpPath", "googleDork",
"logFile", "bulkFile", "forms", "crawlDepth", "stdinPipe",
"multipleTargets", "optimize", "os", "forceDbms", "dbms",
"uValues", "uCols", "testFilter", "csrfToken", "testSkip",
"tor", "timeSec", "retries", "code", "csvDel", "torPort",
"torType", "outputDir", "string", "getAll", "noCast",
"dumpFormat", "col", "exclude", "binaryFields", "proxy",
"proxyFile", "dummy", "batch", "scope",
)
def _base(self):
"""Set the cleanup-relevant conf fields to inert defaults, then let the
individual test override the one(s) it cares about."""
for key in self._KEYS:
conf[key] = None
conf.eta = False
conf.optimize = False
conf.tor = False
conf.getAll = False
conf.noCast = False
conf.dummy = False
conf.batch = False
conf.timeSec = 5
conf.retries = 3
conf.multipleTargets = False
def test_test_parameter_split(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.testParameter = "id,name"
option._cleanupOptions()
self.assertEqual(conf.testParameter, ["id", "name"])
def test_empty_test_parameter_becomes_list(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.testParameter = None
option._cleanupOptions()
self.assertEqual(conf.testParameter, [])
def test_ignore_code_wildcard(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.ignoreCode = IGNORE_CODE_WILDCARD
option._cleanupOptions()
self.assertIn(404, conf.ignoreCode)
self.assertIn(0, conf.ignoreCode)
def test_ignore_code_list(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.ignoreCode = "401,403"
option._cleanupOptions()
self.assertEqual(conf.ignoreCode, [401, 403])
def test_ignore_code_invalid_raises(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.ignoreCode = "abc"
self.assertRaises(SqlmapSyntaxException, option._cleanupOptions)
def test_abort_code_list(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.abortCode = "500,502"
option._cleanupOptions()
self.assertEqual(conf.abortCode, [500, 502])
def test_param_filter_uppercased_and_split(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.paramFilter = "get,post"
option._cleanupOptions()
self.assertEqual(conf.paramFilter, ["GET", "POST"])
def test_skip_split(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.skip = "a, b ,c"
option._cleanupOptions()
self.assertEqual(conf.skip, ["a", "b", "c"])
def test_url_scheme_prepended(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.url = "example.com/page?id=1"
option._cleanupOptions()
self.assertTrue(conf.url.startswith("http://"), msg=conf.url)
def test_url_credentials_extracted_to_basic_auth(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"), \
_preserve(conf, "authType", "authCred"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.authType = None
conf.authCred = None
conf.url = "http://user:pass@example.com/page?id=1"
option._cleanupOptions()
self.assertNotIn("user:pass@", conf.url)
self.assertEqual(conf.authCred, "user:pass")
def test_random_pool_from_rparam(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.rParam = "id=1,2,3"
option._cleanupOptions()
self.assertEqual(conf.rParam, ["id"])
self.assertEqual(kb.randomPool["id"], ["1", "2", "3"])
def test_code_cast_to_int(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.code = "200"
option._cleanupOptions()
self.assertEqual(conf.code, 200)
def test_dump_format_uppercased(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.dumpFormat = "csv"
option._cleanupOptions()
self.assertEqual(conf.dumpFormat, "CSV")
def test_uvalues_sets_ucols(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.uValues = "NULL,1,2"
option._cleanupOptions()
self.assertEqual(conf.uCols, "3-3")
def test_multiple_targets_flag(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.crawlDepth = 2
option._cleanupOptions()
self.assertTrue(conf.multipleTargets)
def test_proxy_disables_precon(self):
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"), \
_preserve(conf, "disablePrecon"):
kb.explicitSettings = set()
kb.randomPool = {}
self._base()
conf.disablePrecon = False
conf.proxy = "http://127.0.0.1:8080"
option._cleanupOptions()
self.assertTrue(conf.disablePrecon)
class TestBasicOptionValidation(_BackendGuard):
"""_basicOptionValidation reads a wide swathe of conf; set up a benign baseline
and flip one offending pair per test."""
_KEYS = (
"limitStart", "limitStop", "level", "risk", "firstChar", "lastChar",
"textOnly", "nullConnection", "uValues", "uChar", "base64Parameter",
"tamper", "eta", "verbose", "direct", "url", "dbms", "tor", "proxy",
"ignoreProxy", "regexp", "timeSec", "torPort", "torType", "dumpFormat",
"technique", "threads", "predictOutput", "optimize", "csrfToken",
"csrfUrl", "string", "notString", "noCast", "hexConvert",
)
def _base(self):
for key in self._KEYS:
conf[key] = None
conf.textOnly = False
conf.nullConnection = False
conf.eta = False
conf.direct = False
conf.tor = False
conf.ignoreProxy = False
conf.predictOutput = False
conf.optimize = False
conf.noCast = False
conf.hexConvert = False
conf.verbose = 1
conf.timeSec = 5
conf.torPort = None
conf.torType = "SOCKS5"
conf.dumpFormat = "CSV"
conf.technique = [1, 2, 6, 4, 5]
conf.threads = 1
def test_clean_baseline_passes(self):
with _preserve(conf, *self._KEYS):
self._base()
option._basicOptionValidation() # must not raise
def test_bad_level_raises(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.level = 99
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_bad_risk_raises(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.risk = 9
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_textonly_nullconnection_incompatible(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.textOnly = True
conf.nullConnection = True
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_direct_url_incompatible(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.direct = "mysql://u:p@h/db"
conf.url = "http://x/?id=1"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_empty_technique_raises(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.technique = []
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_bad_regexp_raises(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.regexp = "("
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_bad_dump_format_raises(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.dumpFormat = "BOGUS"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_bad_tor_port_raises(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.torPort = 70000
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_uvalues_uchar_incompatible(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.uValues = "NULL,1"
conf.uChar = "NULL"
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
def test_tor_ignoreproxy_incompatible(self):
with _preserve(conf, *self._KEYS):
self._base()
conf.tor = True
conf.ignoreProxy = True
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -20,7 +20,7 @@ from _testutils import bootstrap
bootstrap()
from lib.core.settings import (JSON_RECOGNITION_REGEX, JSON_LIKE_RECOGNITION_REGEX,
XML_RECOGNITION_REGEX, PAYLOAD_DELIMITER, DEFAULT_GET_POST_DELIMITER,
XML_RECOGNITION_REGEX, PAYLOAD_DELIMITER,
CUSTOM_INJECTION_MARK_CHAR)
# The real source marks injection points with kb.customInjectionMark, which defaults to

View file

@ -0,0 +1,88 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit coverage for PURE functions in lib/request/basic.py.
These exercise getHeuristicCharEncoding (with its kb.cache.encoding memoization)
and decodePage's charset + HTML-entity decoding branches, in isolation - WITHOUT
touching the network, the DBMS or any interactive prompt.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.data import conf, kb
class TestBasicHeuristicCharEncoding(unittest.TestCase):
def test_ascii(self):
from lib.request.basic import getHeuristicCharEncoding
self.assertEqual(getHeuristicCharEncoding(b"<html></html>"), "ascii")
def test_cache_hit_returns_same(self):
from lib.request.basic import getHeuristicCharEncoding
page = b"<html>hello world</html>"
first = getHeuristicCharEncoding(page)
# second call for identical page must come back identical (and from cache)
self.assertEqual(getHeuristicCharEncoding(page), first)
key = (len(page), hash(page))
self.assertEqual(kb.cache.encoding.get(key), first)
class TestBasicDecodePage(unittest.TestCase):
"""decodePage charset + HTML-entity decoding branches."""
def setUp(self):
self._old_encoding = conf.encoding
self._old_null = conf.nullConnection
conf.nullConnection = False
def tearDown(self):
conf.encoding = self._old_encoding
conf.nullConnection = self._old_null
def test_html_entity_amp(self):
from lib.request.basic import decodePage
from lib.core.common import getText
conf.encoding = None
self.assertEqual(
getText(decodePage(b"<html>foo&amp;bar</html>", None, "text/html; charset=utf-8")),
"<html>foo&bar</html>",
)
def test_numeric_hex_entity_tab(self):
from lib.request.basic import decodePage
from lib.core.common import getText
conf.encoding = None
self.assertEqual(getText(decodePage(b"&#x9;", None, "text/html; charset=utf-8")), "\t")
def test_numeric_hex_entity_letter(self):
from lib.request.basic import decodePage
from lib.core.common import getText
conf.encoding = None
self.assertEqual(getText(decodePage(b"&#x4A;", None, "text/html; charset=utf-8")), "J")
def test_unicode_entity(self):
from lib.request.basic import decodePage
conf.encoding = None
self.assertEqual(decodePage(b"&#x2122;", None, "text/html; charset=utf-8"), u"")
def test_empty_page(self):
from lib.request.basic import decodePage
from lib.core.common import getText
# empty page short-circuits to getUnicode(page)
self.assertEqual(getText(decodePage(b"", None, "text/html")), "")
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -4,14 +4,15 @@
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests for plugins/generic/search.py (Search) and plugins/generic/entries.py
(Entries), exercising searchDb / searchTable / searchColumn and dumpTable by
MOCKING the injection layer (lib.request.inject.getValue) and the dumper.
Unit tests for plugins/generic/search.py (Search), exercising searchDb /
searchTable / searchColumn by MOCKING the injection layer
(lib.request.inject.getValue) and the dumper.
No network and no DBMS are involved: conf.direct=True selects the simple inband
branches, inject.getValue is patched to return canned rows in the exact shape the
methods parse, and conf.dumper is replaced with a recording stub so we can assert
on what each method produced (kb.data caches / returned dicts).
branches (TestSearch), or conf.direct=False with a BOOLEAN injection state selects
the inference branches (TestSearchInference); inject.getValue is patched to return
canned rows in the exact shape the methods parse, and conf.dumper is replaced with
a recording stub so we can assert on what each method produced.
"""
import os
@ -24,10 +25,30 @@ from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.data import conf, kb
from lib.core.enums import EXPECTED, PAYLOAD
import plugins.generic.search as smod
import plugins.generic.entries as emod
from plugins.generic.search import Search
from plugins.generic.entries import Entries
def _inference_gv(count, sequence):
"""Build an inject.getValue stub for blind inference branches.
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
yields the next item from `sequence` wrapped as a single-cell row ([value]),
cycling if exhausted. This mirrors the count-then-per-row contract of every
isInferenceAvailable() branch.
"""
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(count)
val = sequence[state["i"] % len(sequence)]
state["i"] += 1
return [val]
return gv
class _RecordingDumper(object):
@ -102,29 +123,6 @@ class _TestSearch(Search):
kb.data.cachedColumns[db][tbl][col] = "varchar"
class _TestEntries(Entries):
"""Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables)."""
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {} # {db: {tbl: {col: type}}}
self.getTablesResult = {} # value assigned to kb.data.cachedTables
self.getColumnsCalls = []
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
self.getColumnsCalls.append((conf.db, conf.tbl))
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
kb.data.cachedTables = dict(self.getTablesResult)
class _SearchEnumBase(unittest.TestCase):
def setUp(self):
# Save mutated globals
@ -362,113 +360,190 @@ class TestSearch(_SearchEnumBase):
self.assertRaises(SqlmapMissingMandatoryOptionException, s.search)
class TestEntries(_SearchEnumBase):
def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntries()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
# --------------------------------------------------------------------------- #
# search.py - inference (blind) paths
# --------------------------------------------------------------------------- #
# --- dumpTable: inband (conf.direct) ------------------------------------
class _TestSearchInf(Search):
excludeDbsList = ["information_schema", "mysql"]
def test_dump_table_inband_rows(self):
e = self._entries_with_cols(cols=("id", "name"))
def __init__(self):
Search.__init__(self)
self.like = ('2', "='%s'") # exact match (colConsider '2')
self.dumpFoundTablesCalls = []
self.dumpFoundColumnCalls = []
def likeOrExact(self, what):
return self.like
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def dumpFoundTables(self, tables):
self.dumpFoundTablesCalls.append(tables)
def dumpFoundColumn(self, dbs, foundCols, colConsider):
self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider))
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
db, tbl, col = conf.db, conf.tbl, conf.col
if db and tbl:
kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {})
kb.data.cachedColumns[db][tbl][col] = "varchar"
class _RecDumper(object):
def __init__(self):
self.listed = []
self.dbTablesArg = None
self.dbColumnsArg = None
def lister(self, header, elements, content_type=None, sort=True):
self.listed.append((header, list(elements) if elements else []))
def dbTables(self, dbTables):
self.dbTablesArg = dbTables
def dbColumns(self, dbColumnsDict, colConsider, dbs):
self.dbColumnsArg = (dbColumnsDict, colConsider, dbs)
class _SearchBase(unittest.TestCase):
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs",
"exclude", "search")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_dumper = conf.get("dumper")
self._gv = smod.inject.getValue
self._readInput = smod.readInput
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_cachedColumns = kb.data.get("cachedColumns")
self._saved_hintValue = kb.get("hintValue")
self._saved_injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = False
conf.technique = None
conf.excludeSysDbs = False
conf.exclude = None
conf.search = True
conf.dumper = _RecDumper()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
smod.inject.getValue = self._gv
smod.readInput = self._readInput
kb.data.has_information_schema = self._saved_has_is
kb.data.cachedColumns = self._saved_cachedColumns
kb.hintValue = self._saved_hintValue
kb.injection.data = self._saved_injection_data
class TestSearchInference(_SearchBase):
def test_search_db_inference(self):
# Blind searchDb: count of matching dbs, then one db name per index.
s = _TestSearchInf()
conf.db = "testdb"
smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"])
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][0], "found databases")
self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"])
def test_search_db_inference_no_match(self):
# Count fails (non-numeric) => no databases appended, empty listing.
s = _TestSearchInf()
conf.db = "ghost"
smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails"))
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][1], [])
def test_search_table_inference_grouped(self):
# Blind searchTable, no conf.db: outer count of dbs holding the table, then
# per-db a name, then per-db a count of matching tables, then table names.
s = _TestSearchInf()
conf.tbl = "users"
conf.col = None
# MySQL inband dump returns a list of [colVal, colVal] rows.
emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]]
conf.db = None
e.dumpTable()
# Sequencing by the EXPECTED.INT counts + the per-index string results.
# 1st count: number of databases with the table -> 1
# 1st db name -> "testdb"
# 2nd count: number of tables in testdb -> 1
# table name -> "users"
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
dumped = conf.dumper.tableValues[-1]
self.assertEqual(dumped["__infos__"]["count"], 2)
self.assertEqual(dumped["__infos__"]["table"], "users")
self.assertEqual(dumped["__infos__"]["db"], "testdb")
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
v = seq["counts"][seq["ci"] % len(seq["counts"])]
seq["ci"] += 1
return v
v = seq["vals"][seq["vi"] % len(seq["vals"])]
seq["vi"] += 1
return [v]
def test_dump_table_uses_foundData(self):
e = _TestEntries()
conf.db = "testdb"
smod.inject.getValue = gv
s.searchTable()
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]})
self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]})
def test_search_table_mysql_lt5_bruteforce_decline(self):
# MySQL < 5 forces the bruteforce path; declining the prompt returns None
# without any injection.
s = _TestSearchInf()
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["x"]]
foundData = {"testdb": {"users": {"id": "int"}}}
conf.db = None
kb.data.has_information_schema = False
smod.readInput = lambda *a, **k: "N"
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
self.assertIsNone(s.searchTable())
e.dumpTable(foundData=foundData)
# foundData short-circuits column discovery: getColumns must not run.
self.assertEqual(e.getColumnsCalls, [])
self.assertIn("id", conf.dumper.tableValues[-1])
def test_dump_table_no_columns_skips(self):
e = _TestEntries()
e.getColumnsResult = {} # discovery yields nothing
conf.db = "testdb"
conf.tbl = "ghost"
conf.col = None
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
e.dumpTable()
# No columns => no values dumped.
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_empty_entries(self):
e = self._entries_with_cols(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: None # no rows
e.dumpTable()
# Nothing retrieved => dumpedTable empty => dbTableValues not called.
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_current_db(self):
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
conf.db = None # triggers getCurrentDb() -> "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["7"]]
e.dumpTable()
self.assertEqual(conf.db, "testdb")
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"])
def test_dump_table_multiple_db_error(self):
e = _TestEntries()
conf.db = "a,b"
conf.tbl = "users"
conf.col = None
from lib.core.exception import SqlmapMissingMandatoryOptionException
self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable)
def test_dump_table_get_tables_when_no_tbl(self):
e = _TestEntries()
e.getTablesResult = {"testdb": ["users"]}
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
conf.db = "testdb"
def test_search_column_inference(self):
# Blind searchColumn, no db/tbl: count of dbs with the column, then db name;
# then per-db count of tables with the column, then table name -> getColumns
# folds the column into dbs.
s = _TestSearchInf()
conf.col = "password"
conf.db = None
conf.tbl = None
conf.col = None
emod.inject.getValue = lambda *a, **k: [["42"]]
e.dumpTable()
# Tables were discovered via getTables, then the row dumped.
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"])
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
# --- dumpAll: single-db delegation --------------------------------------
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
v = seq["counts"][seq["ci"] % len(seq["counts"])]
seq["ci"] += 1
return v
v = seq["vals"][seq["vi"] % len(seq["vals"])]
seq["vi"] += 1
return [v]
def test_dump_all_single_db_delegates(self):
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
# dumpAll with db set & tbl None must delegate straight to dumpTable.
conf.db = "testdb"
smod.inject.getValue = gv
s.searchColumn()
dbs = conf.dumper.dbColumnsArg[2]
self.assertIn("testdb", dbs)
self.assertIn("users", dbs["testdb"])
self.assertIn("password", dbs["testdb"]["users"])
def test_search_column_mysql_lt5_bruteforce_decline(self):
s = _TestSearchInf()
conf.col = "password"
conf.db = None
conf.tbl = None
conf.col = None
e.getTablesResult = {"testdb": ["users"]}
emod.inject.getValue = lambda *a, **k: [["9"]]
e.dumpAll()
self.assertTrue(conf.dumper.tableValues)
kb.data.has_information_schema = False
smod.readInput = lambda *a, **k: "N"
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
# Declining returns None and never reaches dbColumns.
self.assertIsNone(s.searchColumn())
self.assertIsNone(conf.dumper.dbColumnsArg)
if __name__ == "__main__":

View file

@ -474,7 +474,7 @@ class TestCreateDirs(_TargetTestBase):
self.assertIn("example.com", conf.filePath)
def test_target_dir_and_target_txt(self):
out = self._outdir("t_out")
self._outdir("t_out")
conf.hostname = "example.com"
conf.url = "http://example.com/?id=1"
conf.data = None

View file

@ -4,26 +4,29 @@
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Mocked-oracle / canned-input coverage for three self-contained injection engines:
Mocked-oracle / canned-input coverage for the self-contained extraction /
inference engines under lib/techniques/*:
* lib/techniques/union/use.py - _oneShotUnionUse value extraction + unionUse
* lib/techniques/union/use.py - _oneShotUnionUse / unionUse / configUnion
* lib/techniques/error/use.py - _oneShotErrorUse / _errorFields / errorUse
* lib/techniques/ldap/inject.py - boolean-blind LDAP oracle + blind char inference
* lib/techniques/graphql/inject.py - schema walk, query building, blind-SQLi inference
* lib/techniques/blind/inference.py - bisection / queryOutputLength edge branches
The established pattern (see tests/test_inference.py, tests/test_union_engine.py) is
followed: the network seam (Request.queryPage / Request.getPage / the per-module _send /
_gqlSend) is replaced by a deterministic in-process oracle that answers against a known
secret, so the REAL extraction / parsing / bisection logic runs with no live target,
The established pattern (see tests/test_inference_engine.py,
tests/test_union_engine.py) is followed: the network seam (Request.queryPage /
Request.getPage / the per-module _send / _gqlSend) and the forge/escape chain are
replaced by a deterministic in-process oracle that answers against a known secret,
so the REAL extraction / parsing / bisection logic runs with no live target,
no network and no DBMS.
configUnion is already covered by tests/test_inference.py - not duplicated here.
stdlib unittest only; works on Python 2.7 and 3.x.
"""
import os
import re
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@ -32,14 +35,26 @@ bootstrap()
from lib.core.data import conf, kb
from lib.core.datatype import AttribDict
from lib.core.enums import PAYLOAD, PLACE
from lib.core.common import decodeDbmsHexValue
from lib.core.common import getCurrentThreadData
from lib.core.common import hashDBWrite
from lib.core.common import setTechnique
from lib.core.enums import CHARSET_TYPE
from lib.core.enums import PAYLOAD
from lib.core.enums import PLACE
from lib.core.exception import SqlmapSyntaxException
from lib.core.settings import PARTIAL_VALUE_MARKER
from lib.core.agent import agent
from lib.core.unescaper import unescaper
from lib.request.connect import Connect
from lib.request.connect import Connect as Request
from lib.utils.hashdb import HashDB
import lib.techniques.union.use as uu
import lib.techniques.error.use as eu
import lib.techniques.ldap.inject as ldap
import lib.techniques.graphql.inject as gql
import lib.techniques.blind.inference as inf
# ===========================================================================
@ -278,6 +293,497 @@ class TestUnionUse(_UnionCase):
self.assertIsNone(value)
# ===========================================================================
# UNION-based: lib/techniques/union/use.py (partial / LIMIT-loop branches)
# ===========================================================================
# Distinct from the scalar _UNION_VECTOR / _UU_CONF / _UU_KB above: these drive the
# partial / LIMIT-loop path (NEGATIVE where, forcePartial on, jsonAgg disabled).
_UNION_VECTOR_LIMIT = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.NEGATIVE, False, False, None, None)
_UU_CONF_LIMIT = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None,
"forcePartial": True, "disableJson": True, "binaryFields": None,
"reportJson": False, "api": False, "threads": 1, "verbose": 0, "eta": False,
"noTruncate": True, "uFrom": None}
_UU_KB_LIMIT = {"jsonAggMode": False, "respTruncated": False, "unionDuplicates": False,
"forcePartialUnion": False, "tableFrom": None, "unionTemplate": None,
"nchar": False, "pageEncoding": None, "bruteMode": False, "partRun": None,
"suppressResumeInfo": False, "threadContinue": True}
class _UnionLimitCase(unittest.TestCase):
"""Drive unionUse() down the partial / LIMIT-loop path (jsonAgg disabled, NEGATIVE where,
forcePartial on). The forge chain is a pass-through; concatQuery records the per-row
expression so the oracle can recover the LIMIT offset and answer from a known row set."""
def setUp(self):
self._sc = {k: conf.get(k) for k in _UU_CONF_LIMIT}
self._sk = {k: kb.get(k) for k in _UU_KB_LIMIT}
self._sqp = Request.queryPage
self._scounters = kb.get("counters")
self._sinj_data = kb.injection.data
self._shashdb = conf.get("hashDB")
self._sbatch = conf.get("batch")
self._s_forge = agent.forgeUnionQuery
self._s_concat = agent.concatQuery
self._s_payload = agent.payload
self._s_escape = unescaper.escape
self._s_lastexpr = getattr(agent, "_lastexpr", None)
self._s_initTechnique = uu.initTechnique
for k, v in _UU_CONF_LIMIT.items():
conf[k] = v
for k, v in _UU_KB_LIMIT.items():
kb[k] = v
conf.batch = True
conf.hashDB = None
kb.counters = {}
entry = AttribDict()
entry.vector = _UNION_VECTOR_LIMIT
entry.where = PAYLOAD.WHERE.NEGATIVE
kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry}
# record the expression seen by each _oneShotUnionUse so the oracle can branch on it
def rec_concat(expression, unpack=True):
agent._lastexpr = expression
return expression
agent.concatQuery = rec_concat
agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED"
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD"
unescaper.escape = lambda expression, *a, **k: expression
uu.initTechnique = lambda technique=None: None
self._s_columns = os.environ.get("COLUMNS")
os.environ["COLUMNS"] = "80"
set_dbms("MySQL")
def tearDown(self):
for k, v in self._sc.items():
conf[k] = v
for k, v in self._sk.items():
kb[k] = v
conf.batch = self._sbatch
Request.queryPage = self._sqp
uu.Request.queryPage = self._sqp
kb.counters = self._scounters
kb.injection.data = self._sinj_data
conf.hashDB = self._shashdb
agent.forgeUnionQuery = self._s_forge
agent.concatQuery = self._s_concat
agent.payload = self._s_payload
unescaper.escape = self._s_escape
agent._lastexpr = self._s_lastexpr
uu.initTechnique = self._s_initTechnique
if self._s_columns is None:
os.environ.pop("COLUMNS", None)
else:
os.environ["COLUMNS"] = self._s_columns
def _install_row_oracle(self, rows, count=None):
"""rows: list of tuples (per-row columns). Oracle answers COUNT and per-LIMIT rows
from the recorded expression (agent._lastexpr), wrapping in real start/stop markers."""
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
total = count if count is not None else len(rows)
def oracle(payload=None, content=False, raise404=False, **kwargs):
expr = getattr(agent, "_lastexpr", "") or ""
if "COUNT" in expr.upper():
body = str(total)
else:
m = re.search(r"LIMIT (\d+),1", expr)
idx = int(m.group(1)) if m else 0
row = rows[idx] if 0 <= idx < len(rows) else ("?",)
body = delim.join(row)
page = "%s%s%s" % (start, body, stop)
return (page, AttribDict(), 200) if content else page
Request.queryPage = staticmethod(oracle)
uu.Request.queryPage = staticmethod(oracle)
class TestUnionPartialDump(_UnionLimitCase):
def test_multi_row_two_columns(self):
rows = [("1", "alice"), ("2", "bob"), ("3", "carol")]
self._install_row_oracle(rows)
value = uu.unionUse("SELECT id,name FROM users")
self.assertEqual(list(value), [["1", "alice"], ["2", "bob"], ["3", "carol"]])
def test_multi_row_single_column(self):
rows = [("alice",), ("bob",)]
self._install_row_oracle(rows)
value = uu.unionUse("SELECT name FROM users")
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["alice", "bob"])
def test_query_count_matches_rows(self):
# one COUNT query + one query per row = 4 UNION requests for 3 rows
rows = [("1", "a"), ("2", "b"), ("3", "c")]
self._install_row_oracle(rows)
uu.unionUse("SELECT id,name FROM users")
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1 + len(rows))
def test_count_returns_zero_empty(self):
# COUNT yields "0" -> empty-table sentinel (the function returns []), no row queries
self._install_row_oracle([], count=0)
value = uu.unionUse("SELECT id,name FROM users")
self.assertEqual(value, [])
def test_single_row_count_one(self):
# COUNT yields "1": the multi-row thread loop is skipped, falls through to one one-shot
rows = [("solo",)]
self._install_row_oracle(rows, count=1)
value = uu.unionUse("SELECT name FROM users")
self.assertEqual(uu.unArrayizeValue(value), "solo")
def test_length_limited_window(self):
# conf.limitStart/limitStop windowing (dump=True): only rows in [start, stop) survive.
# With limitStart=2, limitStop=4 over a 5-row table the engine COUNTs then walks
# offsets 1..3 -> rows index 1,2,3 -> "b","c","d".
conf.forcePartial = False
conf.limitStart = 2
conf.limitStop = 4
rows = [("a",), ("b",), ("c",), ("d",), ("e",)]
self._install_row_oracle(rows, count=5)
value = uu.unionUse("SELECT name FROM users", dump=True)
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["b", "c", "d"])
class TestOneShotUnionUseLimited(_UnionLimitCase):
"""_oneShotUnionUse called directly with the `limited` flag set (the per-row caller's mode)."""
def test_limited_single_row(self):
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
body = delim.join(("7", "zed"))
page = "%s%s%s" % (start, body, stop)
def oracle(payload=None, content=False, raise404=False, **kwargs):
return (page, AttribDict(), 200) if content else page
Request.queryPage = staticmethod(oracle)
uu.Request.queryPage = staticmethod(oracle)
retVal = uu._oneShotUnionUse("SELECT id,name FROM t LIMIT 0,1", unpack=True, limited=True)
self.assertEqual(retVal, page)
# one wrapped multi-column entry -> one row of two columns
self.assertEqual(list(uu.parseUnionPage(retVal)), [["7", "zed"]])
# ===========================================================================
# ERROR-based: lib/techniques/error/use.py
# ===========================================================================
# An error injection vector is consumed by agent.prefixQuery/suffixQuery (here stubbed
# to a pass-through that just yields the "[QUERY]" placeholder the engine substitutes into).
_ERR_VECTOR = ("pref", "suff", 2, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None)
_ERR_CONF = {"hexConvert": False, "noEscape": None, "verbose": 0, "api": False,
"reportJson": False, "limitStart": 0, "limitStop": 0, "noTruncate": True,
"threads": 1, "eta": False}
_ERR_KB = {"testMode": True, "safeCharEncode": False, "errorChunkLength": None,
"fileReadMode": False, "bruteMode": False, "threadContinue": True,
"suppressResumeInfo": False, "dumpTable": None}
class _ErrorCase(unittest.TestCase):
"""Stub the forge/escape/transport seam so _oneShotErrorUse's OWN parsing (marker
extraction, trim repair, char restoration) is what is exercised."""
def setUp(self):
self._sc = {k: conf.get(k) for k in _ERR_CONF}
self._sk = {k: kb.get(k) for k in _ERR_KB}
self._sqp = Request.queryPage
self._scounters = kb.get("counters")
self._stechnique = kb.get("technique")
self._sinj_data = kb.injection.data
self._shashdb = conf.get("hashDB")
self._sbatch = conf.get("batch")
self._s_prefix = agent.prefixQuery
self._s_suffix = agent.suffixQuery
self._s_payload = agent.payload
self._s_nullcast = agent.nullAndCastField
self._s_escape = unescaper.escape
# restore thread state we touch
td = getCurrentThreadData()
self._s_td_uid = td.lastRequestUID
self._s_td_httperr = td.lastHTTPError
self._s_td_redirect = td.lastRedirectMsg
for k, v in _ERR_CONF.items():
conf[k] = v
for k, v in _ERR_KB.items():
kb[k] = v
conf.batch = True
conf.hashDB = None # disable session resume in these tests
kb.counters = {}
kb.technique = PAYLOAD.TECHNIQUE.ERROR
setTechnique(PAYLOAD.TECHNIQUE.ERROR)
entry = AttribDict()
entry.vector = _ERR_VECTOR
entry.where = PAYLOAD.WHERE.ORIGINAL
kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: entry}
# pass-through forge chain: the produced payload text carries the injExpression so
# the oracle can (optionally) branch on the requested field; agent.payload returns
# exactly the newValue it is handed.
agent.prefixQuery = lambda vector, *a, **k: "[QUERY]"
agent.suffixQuery = lambda query, *a, **k: query
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue
agent.nullAndCastField = lambda field: field
unescaper.escape = lambda expression, *a, **k: expression
# getConsoleWidth() in _errorFields hits curses with no tty; pin COLUMNS so it doesn't
self._s_columns = os.environ.get("COLUMNS")
os.environ["COLUMNS"] = "80"
set_dbms("MySQL")
def tearDown(self):
for k, v in self._sc.items():
conf[k] = v
for k, v in self._sk.items():
kb[k] = v
conf.batch = self._sbatch
Request.queryPage = self._sqp
eu.Request.queryPage = self._sqp
kb.counters = self._scounters
kb.technique = self._stechnique
setTechnique(None)
kb.injection.data = self._sinj_data
conf.hashDB = self._shashdb
agent.prefixQuery = self._s_prefix
agent.suffixQuery = self._s_suffix
agent.payload = self._s_payload
agent.nullAndCastField = self._s_nullcast
unescaper.escape = self._s_escape
td = getCurrentThreadData()
td.lastRequestUID = self._s_td_uid
td.lastHTTPError = self._s_td_httperr
td.lastRedirectMsg = self._s_td_redirect
if self._s_columns is None:
os.environ.pop("COLUMNS", None)
else:
os.environ["COLUMNS"] = self._s_columns
@staticmethod
def _wrap(body):
return "%s%s%s" % (kb.chars.start, body, kb.chars.stop)
def _install_page(self, page):
def oracle(payload=None, content=False, raise404=False, **kwargs):
return (page, {}, 200) if content else page
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
def _install_field_oracle(self, mapping):
"""Oracle that branches on which field name appears in the forged payload (the
injExpression is passed through agent.payload unchanged, so it is in `payload`)."""
def oracle(payload=None, content=False, raise404=False, **kwargs):
body = "?"
for field, value in mapping.items():
if field in (payload or ""):
body = value
break
page = "<html>%s</html>" % self._wrap(body)
return (page, {}, 200) if content else page
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
class TestOneShotErrorUse(_ErrorCase):
def test_single_value_extracted(self):
self._install_page("<html>%s</html>" % self._wrap("admin"))
self.assertEqual(eu._oneShotErrorUse("SELECT name"), "admin")
def test_space_char_restored(self):
# the kb.chars.space placeholder (used to survive transport) is restored to a literal
# space by _errorReplaceChars. NOTE: the other char tokens (dollar/at/hash) are random
# per-run and may collide with the space token, so only space is asserted here.
body = "hello%sworld" % kb.chars.space
self._install_page(self._wrap(body))
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hello world")
def test_no_markers_returns_none(self):
self._install_page("<html>no useful markers here</html>")
self.assertIsNone(eu._oneShotErrorUse("SELECT x"))
def test_html_entities_unescaped(self):
# retVal goes through htmlUnescape() and <br> -> newline on the way out
self._install_page(self._wrap("a &amp; b<br>c"))
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "a & b\nc")
def test_counter_incremented(self):
self._install_page(self._wrap("v"))
eu._oneShotErrorUse("SELECT x")
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.ERROR), 1)
def test_field_substituted_into_expression(self):
# field is replaced (once) by nullAndCastField(field) before forging; the oracle keys
# on the field name in the resulting payload to prove the right column was requested
self._install_field_oracle({"surname": "Smith"})
self.assertEqual(eu._oneShotErrorUse("SELECT surname FROM users", field="surname"), "Smith")
def test_recovered_from_http_error_body(self):
# page itself carries no markers; the delimited value lives in the 500-error body
td = getCurrentThreadData()
td.lastRequestUID = 4242
td.lastHTTPError = (4242, 500, "<html>%s</html>" % self._wrap("from-error-page"))
self._install_page("<html>regular page, no markers</html>")
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "from-error-page")
def test_recovered_from_response_header(self):
# neither page nor error body has it; it is carried back in a response header value
body = self._wrap("hdr-value")
page = "<html>nothing</html>"
def oracle(payload=None, content=False, raise404=False, **kwargs):
headers = {"X-Leak": body}
return (page, headers, 200) if content else page
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hdr-value")
def test_hex_convert_decoded(self):
# --hex: the delimited body is a hex string decoded by decodeDbmsHexValue
conf.hexConvert = True
self._install_page(self._wrap("48656C6C6F")) # "Hello"
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "Hello")
def test_empty_value_between_markers(self):
self._install_page(self._wrap(""))
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "")
class TestOneShotErrorUseChunking(_ErrorCase):
"""The MySQL multi-chunk reassembly loop: with kb.errorChunkLength set, output >= chunk
length triggers another request at the next offset; the engine concatenates the pieces."""
def setUp(self):
_ErrorCase.setUp(self)
kb.testMode = False # honor the chunk-offset loop
kb.errorChunkLength = 4 # pre-set so the length-probe search is skipped
conf.verbose = 0
def test_multi_chunk_reassembled(self):
# secret returned 4 chars at a time via SUBSTRING(expr, offset, 4); the loop walks offsets
secret = "abcdefghij"
def oracle(payload=None, content=False, raise404=False, **kwargs):
# MySQL substring is rendered as MID((field),offset,length)
m = re.search(r"(?:MID|SUBSTRING)\(.*?,(\d+),(\d+)\)", payload or "")
if m:
off, length = int(m.group(1)), int(m.group(2))
chunk = secret[off - 1:off - 1 + length]
else:
chunk = secret
return ("%s%s%s" % (kb.chars.start, chunk, kb.chars.stop), {}, 200) if content else None
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
# a field is required for the SUBSTRING windowing branch to engage
self.assertEqual(eu._oneShotErrorUse("SELECT data FROM t", field="data"), secret)
class TestErrorFields(_ErrorCase):
"""_errorFields iterates the field list, recovering one value per column."""
def test_multi_field_values(self):
self._install_field_oracle({"user": "alice", "pass": "s3cr3t"})
values = eu._errorFields("SELECT user,pass FROM t", "user,pass",
["user", "pass"], suppressOutput=True)
self.assertEqual(values, ["alice", "s3cr3t"])
def test_single_field_value(self):
self._install_field_oracle({"email": "root@localhost"})
values = eu._errorFields("SELECT email FROM t", "email", ["email"], suppressOutput=True)
self.assertEqual(values, ["root@localhost"])
def test_empty_field_yields_null(self):
# a field listed in emptyFields is short-circuited to the NULL sentinel (no oracle hit)
from lib.core.settings import NULL
def boom(*a, **k):
raise AssertionError("oracle must not be called for an empty field")
Request.queryPage = staticmethod(boom)
eu.Request.queryPage = staticmethod(boom)
values = eu._errorFields("SELECT col FROM t", "col", ["col"],
emptyFields=["col"], suppressOutput=True)
self.assertEqual(values, [NULL])
def test_rownum_field_skipped(self):
# a "ROWNUM " field is skipped entirely (Oracle limit artifact)
self._install_field_oracle({"name": "bob"})
values = eu._errorFields("SELECT name FROM t", "name",
["ROWNUM x", "name"], suppressOutput=True)
self.assertEqual(values, ["bob"])
class TestErrorUse(_ErrorCase):
"""errorUse() orchestration. initTechnique() needs a full injection session; stub it so
the orchestration + _errorFields extraction + result shaping is what is exercised."""
def setUp(self):
_ErrorCase.setUp(self)
self._s_initTechnique = eu.initTechnique
eu.initTechnique = lambda technique=None: None
def tearDown(self):
eu.initTechnique = self._s_initTechnique
_ErrorCase.tearDown(self)
def test_scalar_value(self):
# scalar expression (no FROM): single one-shot extraction, unwrapped from the list
self._install_page(self._wrap("5.7.40"))
self.assertEqual(eu.errorUse("SELECT VERSION()"), "5.7.40")
def test_scalar_no_output_none(self):
self._install_page("<html>no markers</html>")
self.assertIsNone(eu.errorUse("SELECT VERSION()"))
def test_multi_row_dump(self):
# dump=True over a FROM-table query: errorUse COUNTs the rows then LIMIT-walks them,
# reconstructing each row's single column value in order
conf.limitStart = 1
conf.limitStop = 3
rows = {0: "alice", 1: "bob", 2: "carol"}
def oracle(payload=None, content=False, raise404=False, **kwargs):
nv = payload or ""
if "COUNT" in nv.upper():
body = "3"
else:
m = re.search(r"LIMIT (\d+),1", nv)
idx = int(m.group(1)) if m else 0
body = rows.get(idx, "?")
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
value = eu.errorUse("SELECT name FROM users", dump=True)
self.assertEqual([eu.unArrayizeValue(v) for v in value], ["alice", "bob", "carol"])
def test_dump_zero_count_returns_empty(self):
# COUNT yields "0" (non-positive) -> the query returned no output -> None
conf.limitStart = 1
conf.limitStop = 10
def oracle(payload=None, content=False, raise404=False, **kwargs):
nv = payload or ""
body = "0" if "COUNT" in nv.upper() else "x"
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
# a "0" count is truthy-but-not-positive -> empty-table sentinel (returns [])
self.assertEqual(eu.errorUse("SELECT name FROM users", dump=True), [])
# ===========================================================================
# LDAP: lib/techniques/ldap/inject.py
# ===========================================================================
@ -371,7 +877,6 @@ class _LdapOracleCase(unittest.TestCase):
conf.cookieDel = None
directory = self.DIRECTORY
sentinel = ldap.SENTINEL
def fake_send(place, parameter, value):
assertions = re.findall(r"\((\w+)=([^()]*)", value)
@ -765,5 +1270,251 @@ class TestGraphqlParseRows(unittest.TestCase):
self.assertIn("| 1 | 22 |", out)
# ===========================================================================
# Blind inference: lib/techniques/blind/inference.py
# ===========================================================================
# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
# greater-char marker (swapped to '=' on the final equality check). A parseable template
# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret.
TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path
_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False,
"charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False,
"repair": False, "flushSession": None, "freshQueries": None, "hashDB": None}
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False,
"resumeValues": True, "inferenceMode": False}
class _InferenceCase(unittest.TestCase):
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in _CONF}
self._saved_kb = {k: kb.get(k) for k in _KB}
self._saved_qp = Connect.queryPage
self._saved_processChar = kb.data.get("processChar")
for k, v in _CONF.items():
conf[k] = v
for k, v in _KB.items():
kb[k] = v
kb.data.processChar = None
set_dbms("MySQL")
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
for k, v in self._saved_kb.items():
kb[k] = v
kb.data.processChar = self._saved_processChar
Connect.queryPage = self._saved_qp
inf.Request.queryPage = self._saved_qp
def _install_oracle(self, secret):
def oracle(payload=None, *args, **kwargs):
m = _PARSE.search(payload)
idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3))
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
return (ch > threshold) if op == ">" else (ch == threshold)
Connect.queryPage = staticmethod(oracle)
inf.Request.queryPage = staticmethod(oracle)
@staticmethod
def _reset_thread():
td = getCurrentThreadData()
td.shared.value = ""
td.shared.index = [0]
td.shared.start = 0
td.shared.count = 0
def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs):
self._install_oracle(secret)
self._reset_thread()
if length is None:
length = len(secret)
return inf.bisection(TEMPLATE, expression, length=length, **kwargs)
class TestTrivialReturns(_InferenceCase):
def test_none_payload(self):
# payload is None -> (0, None) without ever touching the oracle
self.assertEqual(inf.bisection(None, "SELECT x"), (0, None))
def test_zero_length(self):
# length == 0 -> (0, "") short-circuit
self._install_oracle("ignored")
self._reset_thread()
self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, ""))
class TestRangeLimiting(_InferenceCase):
SECRET = "ABCDEFGH"
def test_first_char_arg(self):
# firstChar=3 -> start from the 3rd character (1-based) -> drop "AB"
_, value = self._bisect(self.SECRET, firstChar=3)
self.assertEqual(value, "CDEFGH")
def test_last_char_arg(self):
# lastChar=4 -> stop after the 4th character
_, value = self._bisect(self.SECRET, lastChar=4)
self.assertEqual(value, "ABCD")
def test_conf_first_char(self):
conf.firstChar = 4
_, value = self._bisect(self.SECRET)
self.assertEqual(value, "DEFGH")
def test_conf_last_char(self):
conf.lastChar = 3
_, value = self._bisect(self.SECRET)
self.assertEqual(value, "ABC")
def test_first_and_last_window(self):
# combined window: chars 3..6 inclusive -> "CDEF"
_, value = self._bisect(self.SECRET, firstChar=3, lastChar=6)
self.assertEqual(value, "CDEF")
class TestHexConvert(_InferenceCase):
def test_hex_output_decoded(self):
# --hex: the retrieved value is a hex string the engine decodes on the way out
conf.hexConvert = True
hexed = "48656C6C6F" # "Hello"
_, value = self._bisect(hexed)
self.assertEqual(value, "Hello")
self.assertEqual(value, decodeDbmsHexValue(hexed))
class TestProcessCharHook(_InferenceCase):
def test_process_char_applied_to_each_char(self):
# kb.data.processChar transforms every assembled character
kb.data.processChar = lambda c: c.upper()
_, value = self._bisect("abcde")
self.assertEqual(value, "ABCDE")
class TestResumeFromHashDB(_InferenceCase):
"""bisection() consults the session store first (hashDBRetrieve(checkConf=True)).
Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py)."""
def setUp(self):
_InferenceCase.setUp(self)
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
os.close(fd)
os.remove(self.path) # HashDB creates it lazily
conf.hashDB = HashDB(self.path)
# hashDBRetrieve/Write key off these
self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port"))
conf.hostname = "test.invalid"
conf.path = "/"
conf.port = 80
def tearDown(self):
conf.hostname, conf.path, conf.port = self._saved_loc
try:
conf.hashDB.closeAll()
except Exception:
pass
if os.path.exists(self.path):
os.remove(self.path)
_InferenceCase.tearDown(self)
def test_full_value_resumed(self):
# a complete cached value short-circuits the whole bisection (0 queries)
hashDBWrite("SELECT cached", "RESUMED")
conf.hashDB.flush()
count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7)
self.assertEqual(value, "RESUMED")
self.assertEqual(count, 0)
def test_partial_value_continued(self):
# a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix
# and extracts only the remaining characters
kb.inferenceMode = True # partial markers are honored only in inference mode
hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER)
conf.hashDB.flush()
count, value = self._bisect("ABCDE", expression="SELECT partial", length=5)
self.assertEqual(value, "ABCDE")
self.assertGreater(count, 0) # it did real work for "CDE"
class TestQueryOutputLength(_InferenceCase):
def test_length_retrieved(self):
# queryOutputLength forges a LENGTH() expression and runs bisection with the
# DIGITS charset; the mock "secret" is the textual length itself
self._install_oracle("42")
self._reset_thread()
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42)
def test_length_single_digit(self):
self._install_oracle("7")
self._reset_thread()
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7)
def test_digits_charset_extracts_number(self):
# direct bisection with the DIGITS charset (queryOutputLength's inner call)
_, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS)
self.assertEqual(value, "2026")
class TestConfigUnion(unittest.TestCase):
"""lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols."""
_CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50}
def setUp(self):
self._saved = {k: conf.get(k) for k in self._CONF}
self._saved_uchar = kb.get("uChar")
for k, v in self._CONF.items():
conf[k] = v
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
kb.uChar = self._saved_uchar
def test_char_and_range(self):
uu.configUnion(char="NULL", columns="2-6")
self.assertEqual(kb.uChar, "NULL")
self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6))
def test_single_column(self):
uu.configUnion(char="NULL", columns="4")
self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4))
def test_uchar_substitution_quoted(self):
# conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template
conf.uChar = "test"
uu.configUnion(char="x[CHAR]x", columns="1")
self.assertEqual(kb.uChar, "x'test'x")
def test_uchar_substitution_digit(self):
# a digit conf.uChar is substituted unquoted
conf.uChar = "88"
uu.configUnion(char="[CHAR]", columns="1")
self.assertEqual(kb.uChar, "88")
def test_conf_ucols_overrides_columns_arg(self):
# conf.uCols takes precedence over the columns argument
conf.uCols = "3-9"
uu.configUnion(char="NULL", columns="1-2")
self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9))
def test_non_integer_range_raises(self):
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc")
def test_inverted_range_raises(self):
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2")
def test_non_string_char_ignored(self):
# a non-string char leaves kb.uChar untouched (early return)
kb.uChar = "SENTINEL"
uu.configUnion(char=None, columns="1")
self.assertEqual(kb.uChar, "SENTINEL")
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -1,540 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional mocked-oracle coverage for the two self-contained extraction engines
whose value-recovery branches are NOT reached by tests/test_techniques.py /
tests/test_inference.py:
* lib/techniques/error/use.py - _oneShotErrorUse / _errorFields / errorUse
* lib/techniques/union/use.py - _oneShotUnionUse / unionUse (partial/LIMIT loop)
Same established harness as the sibling files (see tests/test_techniques.py,
tests/test_inference.py): the network seam (Request.queryPage) and the forge/escape
chain (agent.prefixQuery / suffixQuery / payload / forgeUnionQuery / concatQuery /
unescaper.escape) are replaced by an in-process oracle that answers against a KNOWN
secret wrapped in the REAL kb.chars.start/stop delimiters. The function's OWN regex
extraction / multi-field iteration / counting / LIMIT windowing is what runs - no
live target, no network, no DBMS.
Every test asserts the exact reconstructed value (known-secret oracle), so it fails
if the extraction logic breaks.
stdlib unittest only; works on Python 2.7 and 3.x.
"""
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.data import conf, kb
from lib.core.datatype import AttribDict
from lib.core.enums import PAYLOAD
from lib.core.agent import agent
from lib.core.common import getCurrentThreadData
from lib.core.common import setTechnique
from lib.core.unescaper import unescaper
from lib.request.connect import Connect as Request
import lib.techniques.error.use as eu
import lib.techniques.union.use as uu
# ===========================================================================
# ERROR-based: lib/techniques/error/use.py
# ===========================================================================
# An error injection vector is consumed by agent.prefixQuery/suffixQuery (here stubbed
# to a pass-through that just yields the "[QUERY]" placeholder the engine substitutes into).
_ERR_VECTOR = ("pref", "suff", 2, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None)
_ERR_CONF = {"hexConvert": False, "noEscape": None, "verbose": 0, "api": False,
"reportJson": False, "limitStart": 0, "limitStop": 0, "noTruncate": True,
"threads": 1, "eta": False}
_ERR_KB = {"testMode": True, "safeCharEncode": False, "errorChunkLength": None,
"fileReadMode": False, "bruteMode": False, "threadContinue": True,
"suppressResumeInfo": False, "dumpTable": None}
class _ErrorCase(unittest.TestCase):
"""Stub the forge/escape/transport seam so _oneShotErrorUse's OWN parsing (marker
extraction, trim repair, char restoration) is what is exercised."""
def setUp(self):
self._sc = {k: conf.get(k) for k in _ERR_CONF}
self._sk = {k: kb.get(k) for k in _ERR_KB}
self._sqp = Request.queryPage
self._scounters = kb.get("counters")
self._stechnique = kb.get("technique")
self._sinj_data = kb.injection.data
self._shashdb = conf.get("hashDB")
self._sbatch = conf.get("batch")
self._s_prefix = agent.prefixQuery
self._s_suffix = agent.suffixQuery
self._s_payload = agent.payload
self._s_nullcast = agent.nullAndCastField
self._s_escape = unescaper.escape
# restore thread state we touch
td = getCurrentThreadData()
self._s_td_uid = td.lastRequestUID
self._s_td_httperr = td.lastHTTPError
self._s_td_redirect = td.lastRedirectMsg
for k, v in _ERR_CONF.items():
conf[k] = v
for k, v in _ERR_KB.items():
kb[k] = v
conf.batch = True
conf.hashDB = None # disable session resume in these tests
kb.counters = {}
kb.technique = PAYLOAD.TECHNIQUE.ERROR
setTechnique(PAYLOAD.TECHNIQUE.ERROR)
entry = AttribDict()
entry.vector = _ERR_VECTOR
entry.where = PAYLOAD.WHERE.ORIGINAL
kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: entry}
# pass-through forge chain: the produced payload text carries the injExpression so
# the oracle can (optionally) branch on the requested field; agent.payload returns
# exactly the newValue it is handed.
agent.prefixQuery = lambda vector, *a, **k: "[QUERY]"
agent.suffixQuery = lambda query, *a, **k: query
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue
agent.nullAndCastField = lambda field: field
unescaper.escape = lambda expression, *a, **k: expression
# getConsoleWidth() in _errorFields hits curses with no tty; pin COLUMNS so it doesn't
self._s_columns = os.environ.get("COLUMNS")
os.environ["COLUMNS"] = "80"
set_dbms("MySQL")
def tearDown(self):
for k, v in self._sc.items():
conf[k] = v
for k, v in self._sk.items():
kb[k] = v
conf.batch = self._sbatch
Request.queryPage = self._sqp
eu.Request.queryPage = self._sqp
kb.counters = self._scounters
kb.technique = self._stechnique
setTechnique(None)
kb.injection.data = self._sinj_data
conf.hashDB = self._shashdb
agent.prefixQuery = self._s_prefix
agent.suffixQuery = self._s_suffix
agent.payload = self._s_payload
agent.nullAndCastField = self._s_nullcast
unescaper.escape = self._s_escape
td = getCurrentThreadData()
td.lastRequestUID = self._s_td_uid
td.lastHTTPError = self._s_td_httperr
td.lastRedirectMsg = self._s_td_redirect
if self._s_columns is None:
os.environ.pop("COLUMNS", None)
else:
os.environ["COLUMNS"] = self._s_columns
@staticmethod
def _wrap(body):
return "%s%s%s" % (kb.chars.start, body, kb.chars.stop)
def _install_page(self, page):
def oracle(payload=None, content=False, raise404=False, **kwargs):
return (page, {}, 200) if content else page
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
def _install_field_oracle(self, mapping):
"""Oracle that branches on which field name appears in the forged payload (the
injExpression is passed through agent.payload unchanged, so it is in `payload`)."""
def oracle(payload=None, content=False, raise404=False, **kwargs):
body = "?"
for field, value in mapping.items():
if field in (payload or ""):
body = value
break
page = "<html>%s</html>" % self._wrap(body)
return (page, {}, 200) if content else page
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
class TestOneShotErrorUse(_ErrorCase):
def test_single_value_extracted(self):
self._install_page("<html>%s</html>" % self._wrap("admin"))
self.assertEqual(eu._oneShotErrorUse("SELECT name"), "admin")
def test_space_char_restored(self):
# the kb.chars.space placeholder (used to survive transport) is restored to a literal
# space by _errorReplaceChars. NOTE: the other char tokens (dollar/at/hash) are random
# per-run and may collide with the space token, so only space is asserted here.
body = "hello%sworld" % kb.chars.space
self._install_page(self._wrap(body))
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hello world")
def test_no_markers_returns_none(self):
self._install_page("<html>no useful markers here</html>")
self.assertIsNone(eu._oneShotErrorUse("SELECT x"))
def test_html_entities_unescaped(self):
# retVal goes through htmlUnescape() and <br> -> newline on the way out
self._install_page(self._wrap("a &amp; b<br>c"))
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "a & b\nc")
def test_counter_incremented(self):
self._install_page(self._wrap("v"))
eu._oneShotErrorUse("SELECT x")
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.ERROR), 1)
def test_field_substituted_into_expression(self):
# field is replaced (once) by nullAndCastField(field) before forging; the oracle keys
# on the field name in the resulting payload to prove the right column was requested
self._install_field_oracle({"surname": "Smith"})
self.assertEqual(eu._oneShotErrorUse("SELECT surname FROM users", field="surname"), "Smith")
def test_recovered_from_http_error_body(self):
# page itself carries no markers; the delimited value lives in the 500-error body
td = getCurrentThreadData()
td.lastRequestUID = 4242
td.lastHTTPError = (4242, 500, "<html>%s</html>" % self._wrap("from-error-page"))
self._install_page("<html>regular page, no markers</html>")
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "from-error-page")
def test_recovered_from_response_header(self):
# neither page nor error body has it; it is carried back in a response header value
body = self._wrap("hdr-value")
page = "<html>nothing</html>"
def oracle(payload=None, content=False, raise404=False, **kwargs):
headers = {"X-Leak": body}
return (page, headers, 200) if content else page
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hdr-value")
def test_hex_convert_decoded(self):
# --hex: the delimited body is a hex string decoded by decodeDbmsHexValue
conf.hexConvert = True
self._install_page(self._wrap("48656C6C6F")) # "Hello"
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "Hello")
def test_empty_value_between_markers(self):
self._install_page(self._wrap(""))
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "")
class TestOneShotErrorUseChunking(_ErrorCase):
"""The MySQL multi-chunk reassembly loop: with kb.errorChunkLength set, output >= chunk
length triggers another request at the next offset; the engine concatenates the pieces."""
def setUp(self):
_ErrorCase.setUp(self)
kb.testMode = False # honor the chunk-offset loop
kb.errorChunkLength = 4 # pre-set so the length-probe search is skipped
conf.verbose = 0
def test_multi_chunk_reassembled(self):
# secret returned 4 chars at a time via SUBSTRING(expr, offset, 4); the loop walks offsets
secret = "abcdefghij"
def oracle(payload=None, content=False, raise404=False, **kwargs):
# MySQL substring is rendered as MID((field),offset,length)
m = re.search(r"(?:MID|SUBSTRING)\(.*?,(\d+),(\d+)\)", payload or "")
if m:
off, length = int(m.group(1)), int(m.group(2))
chunk = secret[off - 1:off - 1 + length]
else:
chunk = secret
return ("%s%s%s" % (kb.chars.start, chunk, kb.chars.stop), {}, 200) if content else None
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
# a field is required for the SUBSTRING windowing branch to engage
self.assertEqual(eu._oneShotErrorUse("SELECT data FROM t", field="data"), secret)
class TestErrorFields(_ErrorCase):
"""_errorFields iterates the field list, recovering one value per column."""
def test_multi_field_values(self):
self._install_field_oracle({"user": "alice", "pass": "s3cr3t"})
values = eu._errorFields("SELECT user,pass FROM t", "user,pass",
["user", "pass"], suppressOutput=True)
self.assertEqual(values, ["alice", "s3cr3t"])
def test_single_field_value(self):
self._install_field_oracle({"email": "root@localhost"})
values = eu._errorFields("SELECT email FROM t", "email", ["email"], suppressOutput=True)
self.assertEqual(values, ["root@localhost"])
def test_empty_field_yields_null(self):
# a field listed in emptyFields is short-circuited to the NULL sentinel (no oracle hit)
from lib.core.settings import NULL
def boom(*a, **k):
raise AssertionError("oracle must not be called for an empty field")
Request.queryPage = staticmethod(boom)
eu.Request.queryPage = staticmethod(boom)
values = eu._errorFields("SELECT col FROM t", "col", ["col"],
emptyFields=["col"], suppressOutput=True)
self.assertEqual(values, [NULL])
def test_rownum_field_skipped(self):
# a "ROWNUM " field is skipped entirely (Oracle limit artifact)
self._install_field_oracle({"name": "bob"})
values = eu._errorFields("SELECT name FROM t", "name",
["ROWNUM x", "name"], suppressOutput=True)
self.assertEqual(values, ["bob"])
class TestErrorUse(_ErrorCase):
"""errorUse() orchestration. initTechnique() needs a full injection session; stub it so
the orchestration + _errorFields extraction + result shaping is what is exercised."""
def setUp(self):
_ErrorCase.setUp(self)
self._s_initTechnique = eu.initTechnique
eu.initTechnique = lambda technique=None: None
def tearDown(self):
eu.initTechnique = self._s_initTechnique
_ErrorCase.tearDown(self)
def test_scalar_value(self):
# scalar expression (no FROM): single one-shot extraction, unwrapped from the list
self._install_page(self._wrap("5.7.40"))
self.assertEqual(eu.errorUse("SELECT VERSION()"), "5.7.40")
def test_scalar_no_output_none(self):
self._install_page("<html>no markers</html>")
self.assertIsNone(eu.errorUse("SELECT VERSION()"))
def test_multi_row_dump(self):
# dump=True over a FROM-table query: errorUse COUNTs the rows then LIMIT-walks them,
# reconstructing each row's single column value in order
conf.limitStart = 1
conf.limitStop = 3
rows = {0: "alice", 1: "bob", 2: "carol"}
def oracle(payload=None, content=False, raise404=False, **kwargs):
nv = payload or ""
if "COUNT" in nv.upper():
body = "3"
else:
m = re.search(r"LIMIT (\d+),1", nv)
idx = int(m.group(1)) if m else 0
body = rows.get(idx, "?")
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
value = eu.errorUse("SELECT name FROM users", dump=True)
self.assertEqual([eu.unArrayizeValue(v) for v in value], ["alice", "bob", "carol"])
def test_dump_zero_count_returns_empty(self):
# COUNT yields "0" (non-positive) -> the query returned no output -> None
conf.limitStart = 1
conf.limitStop = 10
def oracle(payload=None, content=False, raise404=False, **kwargs):
nv = payload or ""
body = "0" if "COUNT" in nv.upper() else "x"
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
Request.queryPage = staticmethod(oracle)
eu.Request.queryPage = staticmethod(oracle)
# a "0" count is truthy-but-not-positive -> empty-table sentinel (returns [])
self.assertEqual(eu.errorUse("SELECT name FROM users", dump=True), [])
# ===========================================================================
# UNION-based: lib/techniques/union/use.py (branches not in test_techniques.py)
# ===========================================================================
_UNION_VECTOR = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.NEGATIVE, False, False, None, None)
_UU_CONF = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None,
"forcePartial": True, "disableJson": True, "binaryFields": None,
"reportJson": False, "api": False, "threads": 1, "verbose": 0, "eta": False,
"noTruncate": True, "uFrom": None}
_UU_KB = {"jsonAggMode": False, "respTruncated": False, "unionDuplicates": False,
"forcePartialUnion": False, "tableFrom": None, "unionTemplate": None,
"nchar": False, "pageEncoding": None, "bruteMode": False, "partRun": None,
"suppressResumeInfo": False, "threadContinue": True}
class _UnionLimitCase(unittest.TestCase):
"""Drive unionUse() down the partial / LIMIT-loop path (jsonAgg disabled, NEGATIVE where,
forcePartial on). The forge chain is a pass-through; concatQuery records the per-row
expression so the oracle can recover the LIMIT offset and answer from a known row set."""
def setUp(self):
self._sc = {k: conf.get(k) for k in _UU_CONF}
self._sk = {k: kb.get(k) for k in _UU_KB}
self._sqp = Request.queryPage
self._scounters = kb.get("counters")
self._sinj_data = kb.injection.data
self._shashdb = conf.get("hashDB")
self._sbatch = conf.get("batch")
self._s_forge = agent.forgeUnionQuery
self._s_concat = agent.concatQuery
self._s_payload = agent.payload
self._s_escape = unescaper.escape
self._s_lastexpr = getattr(agent, "_lastexpr", None)
self._s_initTechnique = uu.initTechnique
for k, v in _UU_CONF.items():
conf[k] = v
for k, v in _UU_KB.items():
kb[k] = v
conf.batch = True
conf.hashDB = None
kb.counters = {}
entry = AttribDict()
entry.vector = _UNION_VECTOR
entry.where = PAYLOAD.WHERE.NEGATIVE
kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry}
# record the expression seen by each _oneShotUnionUse so the oracle can branch on it
def rec_concat(expression, unpack=True):
agent._lastexpr = expression
return expression
agent.concatQuery = rec_concat
agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED"
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD"
unescaper.escape = lambda expression, *a, **k: expression
uu.initTechnique = lambda technique=None: None
self._s_columns = os.environ.get("COLUMNS")
os.environ["COLUMNS"] = "80"
set_dbms("MySQL")
def tearDown(self):
for k, v in self._sc.items():
conf[k] = v
for k, v in self._sk.items():
kb[k] = v
conf.batch = self._sbatch
Request.queryPage = self._sqp
uu.Request.queryPage = self._sqp
kb.counters = self._scounters
kb.injection.data = self._sinj_data
conf.hashDB = self._shashdb
agent.forgeUnionQuery = self._s_forge
agent.concatQuery = self._s_concat
agent.payload = self._s_payload
unescaper.escape = self._s_escape
agent._lastexpr = self._s_lastexpr
uu.initTechnique = self._s_initTechnique
if self._s_columns is None:
os.environ.pop("COLUMNS", None)
else:
os.environ["COLUMNS"] = self._s_columns
def _install_row_oracle(self, rows, count=None):
"""rows: list of tuples (per-row columns). Oracle answers COUNT and per-LIMIT rows
from the recorded expression (agent._lastexpr), wrapping in real start/stop markers."""
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
total = count if count is not None else len(rows)
def oracle(payload=None, content=False, raise404=False, **kwargs):
expr = getattr(agent, "_lastexpr", "") or ""
if "COUNT" in expr.upper():
body = str(total)
else:
m = re.search(r"LIMIT (\d+),1", expr)
idx = int(m.group(1)) if m else 0
row = rows[idx] if 0 <= idx < len(rows) else ("?",)
body = delim.join(row)
page = "%s%s%s" % (start, body, stop)
return (page, AttribDict(), 200) if content else page
Request.queryPage = staticmethod(oracle)
uu.Request.queryPage = staticmethod(oracle)
class TestUnionPartialDump(_UnionLimitCase):
def test_multi_row_two_columns(self):
rows = [("1", "alice"), ("2", "bob"), ("3", "carol")]
self._install_row_oracle(rows)
value = uu.unionUse("SELECT id,name FROM users")
self.assertEqual(list(value), [["1", "alice"], ["2", "bob"], ["3", "carol"]])
def test_multi_row_single_column(self):
rows = [("alice",), ("bob",)]
self._install_row_oracle(rows)
value = uu.unionUse("SELECT name FROM users")
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["alice", "bob"])
def test_query_count_matches_rows(self):
# one COUNT query + one query per row = 4 UNION requests for 3 rows
rows = [("1", "a"), ("2", "b"), ("3", "c")]
self._install_row_oracle(rows)
uu.unionUse("SELECT id,name FROM users")
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1 + len(rows))
def test_count_returns_zero_empty(self):
# COUNT yields "0" -> empty-table sentinel (the function returns []), no row queries
self._install_row_oracle([], count=0)
value = uu.unionUse("SELECT id,name FROM users")
self.assertEqual(value, [])
def test_single_row_count_one(self):
# COUNT yields "1": the multi-row thread loop is skipped, falls through to one one-shot
rows = [("solo",)]
self._install_row_oracle(rows, count=1)
value = uu.unionUse("SELECT name FROM users")
self.assertEqual(uu.unArrayizeValue(value), "solo")
def test_length_limited_window(self):
# conf.limitStart/limitStop windowing (dump=True): only rows in [start, stop) survive.
# With limitStart=2, limitStop=4 over a 5-row table the engine COUNTs then walks
# offsets 1..3 -> rows index 1,2,3 -> "b","c","d".
conf.forcePartial = False
conf.limitStart = 2
conf.limitStop = 4
rows = [("a",), ("b",), ("c",), ("d",), ("e",)]
self._install_row_oracle(rows, count=5)
value = uu.unionUse("SELECT name FROM users", dump=True)
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["b", "c", "d"])
class TestOneShotUnionUseLimited(_UnionLimitCase):
"""_oneShotUnionUse called directly with the `limited` flag set (the per-row caller's mode)."""
def test_limited_single_row(self):
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
body = delim.join(("7", "zed"))
page = "%s%s%s" % (start, body, stop)
def oracle(payload=None, content=False, raise404=False, **kwargs):
return (page, AttribDict(), 200) if content else page
Request.queryPage = staticmethod(oracle)
uu.Request.queryPage = staticmethod(oracle)
retVal = uu._oneShotUnionUse("SELECT id,name FROM t LIMIT 0,1", unpack=True, limited=True)
self.assertEqual(retVal, page)
# one wrapped multi-column entry -> one row of two columns
self.assertEqual(list(uu.parseUnionPage(retVal)), [["7", "zed"]])
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -11,7 +11,8 @@ be exercised against canned result rows without a live target, network, or DBMS.
Each test sets conf.direct = True to drive the inband (union/error/query OR
conf.direct) branch of the method under test, patches inject.getValue with rows
matching the shape the method parses, then asserts the relevant kb.data.cached*
container was populated.
container was populated. Inference (blind) branches set conf.direct = False with a
BOOLEAN technique present and follow the count-then-per-index contract.
"""
import os
@ -24,11 +25,32 @@ from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.data import conf, kb
from lib.core.enums import EXPECTED, PAYLOAD
import plugins.generic.users as umod
from plugins.generic.users import Users
from lib.core.settings import CURRENT_USER
def _inference_gv(count, sequence):
"""Build an inject.getValue stub for blind inference branches.
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
yields the next item from `sequence` wrapped as a single-cell row ([value]),
cycling if exhausted. This mirrors the count-then-per-row contract of every
isInferenceAvailable() branch.
"""
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(count)
val = sequence[state["i"] % len(sequence)]
state["i"] += 1
return [val]
return gv
class TestUsersEnum(unittest.TestCase):
def setUp(self):
# Snapshot the global state these tests mutate so tearDown can restore it
@ -252,5 +274,205 @@ class TestUsersEnum(unittest.TestCase):
self.assertIn("root", areAdmins)
# --------------------------------------------------------------------------- #
# Privilege parsing / inference branches (relocated from test_generic_enum_more.py)
# --------------------------------------------------------------------------- #
class _UsersBase(unittest.TestCase):
def setUp(self):
self._direct = conf.direct
self._technique = conf.technique
self._user = conf.user
self._gv = umod.inject.getValue
self._cbe = umod.inject.checkBooleanExpression
self._store = umod.storeHashesToFile
self._attack = umod.attackCachedUsersPasswords
self._readInput = umod.readInput
self._his = kb.data.get("has_information_schema")
self._injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = True
conf.user = None
kb.data.has_information_schema = True
umod.storeHashesToFile = lambda *a, **k: None
umod.attackCachedUsersPasswords = lambda *a, **k: None
umod.readInput = lambda *a, **k: "N"
def tearDown(self):
conf.direct = self._direct
conf.technique = self._technique
conf.user = self._user
umod.inject.getValue = self._gv
umod.inject.checkBooleanExpression = self._cbe
umod.storeHashesToFile = self._store
umod.attackCachedUsersPasswords = self._attack
umod.readInput = self._readInput
kb.injection.data = self._injection_data
if self._his is None:
kb.data.pop("has_information_schema", None)
else:
kb.data.has_information_schema = self._his
def _inference(self):
conf.direct = False
conf.technique = None
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
class TestUsersPrivilegesInband(_UsersBase):
def test_privileges_pgsql_multiple_digit_columns(self):
# PostgreSQL: privilege columns are digit flags; a column index maps to
# PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1,
# leave the rest 0; assert exactly those two privileges are parsed and that
# "super" makes the user an admin.
set_dbms("PostgreSQL")
from lib.core.dicts import PGSQL_PRIVS
ncols = max(PGSQL_PRIVS.keys())
row = ["pguser"] + ["0"] * ncols
row[1] = "1" # createdb
row[2] = "1" # super
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]})
self.assertIn("pguser", areAdmins)
def test_privileges_mysql_lt5_yn_flags(self):
# MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags
# mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv.
set_dbms("MySQL")
from lib.core.dicts import MYSQL_PRIVS
kb.data.has_information_schema = False
ncols = max(MYSQL_PRIVS.keys())
row = ["root"] + ["N"] * ncols
row[1] = "Y" # select_priv
row[3] = "Y" # update_priv
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn(MYSQL_PRIVS[1], privileges["root"])
self.assertIn(MYSQL_PRIVS[3], privileges["root"])
self.assertNotIn(MYSQL_PRIVS[2], privileges["root"])
def test_privileges_firebird_letter_codes(self):
# Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS.
set_dbms("Firebird")
from lib.core.dicts import FIREBIRD_PRIVS
umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertEqual(set(privileges["fbuser"]),
{FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]})
def test_privileges_db2_grant_codes(self):
# DB2: privilege string is "<name>,<grant-letters>"; each 'Y'/'G' letter at
# position i appends the DB2_PRIVS[i] name to the privilege.
set_dbms("DB2")
from lib.core.dicts import DB2_PRIVS
conf.user = "db2admin"
# "DBADM" plus a grant string whose first letter (position 1) is 'Y' ->
# DB2_PRIVS[1] ("CONTROLAUTH") is appended.
umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
joined = " ".join(privileges["DB2ADMIN"])
self.assertIn("DBADM", joined)
self.assertIn(DB2_PRIVS[1], joined)
class TestUsersPrivilegesInference(_UsersBase):
def test_privileges_inference_mysql(self):
# Blind privilege enumeration for a named user: count, then one privilege
# string per index. MySQL >= 5 adds each verbatim.
set_dbms("MySQL")
self._inference()
conf.user = "root"
privs = ["SELECT", "SUPER"]
umod.inject.getValue = _inference_gv(2, privs)
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
# the user key is wildcard-wrapped for the MySQL information_schema LIKE
key = [k for k in privileges if "root" in k][0]
self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"})
self.assertTrue(areAdmins) # SUPER => admin
def test_privileges_inference_oracle(self):
set_dbms("Oracle")
self._inference()
conf.user = "system"
umod.inject.getValue = _inference_gv(1, ["DBA"])
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("SYSTEM", privileges)
self.assertEqual(privileges["SYSTEM"], ["DBA"])
self.assertIn("SYSTEM", areAdmins)
class TestUsersPasswordHashesInference(_UsersBase):
def test_password_hashes_inference_grouping(self):
# Blind password-hash enumeration for two users: per-user count, then one
# hash per index. Assert each user maps to its own hash list.
set_dbms("MySQL")
self._inference()
conf.user = "root,guest"
# per-user single hash; count is 1 for every user
hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return "1"
for u, h in hashes.items():
if u in query:
return [h]
return [None]
umod.inject.getValue = gv
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertEqual(res["root"], ["*ROOTHASH"])
self.assertEqual(res["guest"], ["*GUESTHASH"])
def test_password_hashes_inference_dedup(self):
# The same hash returned twice for a user must be de-duplicated at the end
# (kb.data.cachedUsersPasswords[user] = list(set(...))).
set_dbms("MySQL")
self._inference()
conf.user = "root"
umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"])
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertEqual(res["root"], ["*DUP"])
class TestUsersGetUsersInference(_UsersBase):
def test_get_users_inference(self):
set_dbms("MySQL")
self._inference()
umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"])
users = Users()
kb.data.cachedUsers = []
res = users.getUsers()
self.assertEqual(sorted(res), ["guest@%", "root@localhost"])
def test_is_dba_mssql(self):
# MSSQL isDba goes through the generic checkBooleanExpression branch.
set_dbms("Microsoft SQL Server")
umod.inject.checkBooleanExpression = lambda query, *a, **k: True
users = Users()
kb.data.isDba = None
self.assertTrue(users.isDba())
if __name__ == "__main__":
unittest.main()