Update of unit tests

This commit is contained in:
Miroslav Štampar
2026-06-28 14:28:42 +02:00
parent b933ff3f63
commit cb20a446ae
48 changed files with 15116 additions and 190 deletions
+8
View File
@@ -99,6 +99,14 @@ jobs:
# 'binary' instead of 'text'. Keeping this step byte-compile-free leaves --smoke clean.
run: python -B -m unittest discover -s tests -p "test_*.py"
- name: Coverage
if: matrix.python-version != 'pypy-2.7'
run: |
python -m pip install coverage
python -m coverage run --source=lib,plugins,tamper -m unittest discover -s tests -p "test_*.py"
python -m coverage run -a --source=lib,plugins,tamper sqlmap.py --doc-test
python -m coverage report --fail-under=50
- name: Smoke test
run: python sqlmap.py --smoke-test
+1
View File
@@ -15,3 +15,4 @@ lib/.DS_Store
plugins/.DS_Store
thirdparty/.DS_Store
CLAUDE.md
.coverage
+45 -10
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
1a569b5bcd33ae45d95c140fd3bae2f12ad54640d938172de3cb99f73a549b47 lib/core/settings.py
5cbf5f4bc21f21873df79babd91da8f7fea5ec3c1999f108f005ca6fb4d453b6 lib/core/settings.py
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
@@ -200,7 +200,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
a6440d24f8d6b772221fc78a655d3df07a000ba23e7924bd51cf5068097ee1fb lib/parse/cmdline.py
8351588876a7579fa96b3ab860ef2254487de34ea624c0a7696f2428c24ceb98 lib/parse/cmdline.py
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
@@ -579,50 +579,85 @@ 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
138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py
feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.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
899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py
7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py
a0a29231acbbe6bec11400e28b39b76eaf812c03bf79d5f0dbdd68cd54a052f8 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
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
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
b6d8a4bc9c46a332a2dc7b3cf862ea67e38b5c5701cfd8eb3556021f6b611416 tests/test_dialectdbms.py
8e469e4e29319bcb718803a9e109e742965875c985fa8e8d3bb5b18c922ec597 tests/test_deps.py
b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
ed5a0e453b811dc3dcc5ca28e14a9d7552aacaa7e316e1bca1b042dc5939e204 tests/test_dns_engine.py
703faac01f38224ba85bd0fc398d939ea034f1d7fd641cdc15da4f77ec049443 tests/test_dns_server.py
7f9180a53dbf0bb3e52801fdbfffd31f365a0bff77bf90e58d2ef63a0c23026f tests/test_dns_engine.py
ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py
4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py
9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py
4a5f9392b7fec7b40c4d865b83306b58b76f3423cebc2876e6e75fb91b037202 tests/test_graphql.py
8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py
31354d3cff0d26ecf3b42e949a2780ae3d286cdf206b59404e18a96e7a2cddd2 tests/test_filesystem.py
6a9d95f64c7892957742534a14e8f094c6ed9ebc91b7059f4f1665049228a5a6 tests/test_fingerprint.py
4f3cfb830b323a3423b0f80985b9a0bbbe4ef77350b762f103dcd8936cca67c6 tests/test_generic_enum_more.py
9874920d18fc30736630df6b14a70b230504d2e4d0c035971a9aa285ee623839 tests/test_generic_more.py
bde97a4781c4ee84e0fe86f7a33206f114167eb14b704013ecf1c26b838193d7 tests/test_graphql.py
50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py
92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py
da2efd1b7457ff619d98a2ae5045f072fdd34be2aa1c18f17d74d7518eeb6707 tests/test_hash_crack.py
0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py
c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
13d0369f3fea7262f7944999f559da38e5284cbc76660fd7aeffedad78e65f5f tests/test_ldap.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
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py
7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py
064617c6a3d28ecd75136318b4f515ab1adefbf830da17667f105337b419c184 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
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
d4f6e60c23db67430cf68dc2d90317d69391a19feff0f842c08ae2443b481857 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
f3a628db8a3e05baee580c02132e95b164695e4b3ee1785707e3ea148702449a tests/test_tamper.py
8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py
44954b916f1e4a4bb217516a65cf330fca922600d484f732525e0e4a2a553167 tests/test_target_parsing.py
b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py
d070a72ae9529182d6dfc0884f7720d42a5f0cd8cd865dd4c2d209389c3ade85 tests/test_techniques_more.py
f2e8b5b9799f4e591462f53a97bb643c6399acf703f33e119c03d991971274ab 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
23ffd75b5aec33066e6d6aad01ab2c9c1b12ee20c1a0990f8f1be81f1ad16161 tests/_testutils.py
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
+1 -1
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.185"
VERSION = "1.10.6.186"
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)
+1 -1
View File
@@ -898,7 +898,7 @@ def cmdLineParser(argv=None):
parser.add_argument("--non-interactive", dest="nonInteractive", action="store_true",
help=SUPPRESS)
parser.add_argument("--smoke-test", dest="smokeTest", action="store_true",
parser.add_argument("--smoke-test", "--doc-test", dest="smokeTest", action="store_true",
help=SUPPRESS)
parser.add_argument("--vuln-test", dest="vulnTest", action="store_true",
+274
View File
@@ -0,0 +1,274 @@
#!/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()
+619
View File
@@ -0,0 +1,619 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests for the sqlmap REST API (lib/utils/api.py).
Two complementary angles:
1. Pure helpers / objects called directly (is_admin, validate_task_options,
the Database and Task classes, the StdDbOut/LogRecorder IPC writers).
2. The bottle HTTP routes driven through the WSGI app via a minimal in-process
test client (no sockets, no network, no scan subprocess) - task lifecycle,
option get/set, scan status/data/log, admin list/flush, version, auth.
The scan-data assembler/collector helpers (_storeData / _assembleData /
_sanitizeScanData / _cleanIdentifier / writeReportJson) are pinned separately in
test_report.py; here we focus on what that file does not exercise.
"""
import io
import json
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
import lib.utils.api as api
from lib.core.data import conf
from lib.core.convert import encodeBase64
from lib.core.enums import CONTENT_STATUS, CONTENT_TYPE
from thirdparty.bottle.bottle import default_app
def _wsgi_call(method, path, body=None, headers=None, remote_addr="127.0.0.1"):
"""
Drive the module's bottle routes through the WSGI interface in-process.
Returns (status_code_int, parsed_json_or_None, raw_text).
"""
app = default_app()
environ = {
"REQUEST_METHOD": method,
"PATH_INFO": path,
"SERVER_NAME": "localhost",
"SERVER_PORT": "80",
"REMOTE_ADDR": remote_addr,
"wsgi.input": io.BytesIO(),
"wsgi.errors": sys.stderr,
"wsgi.url_scheme": "http",
}
if body is not None:
data = json.dumps(body).encode("utf-8")
environ["CONTENT_TYPE"] = "application/json"
environ["CONTENT_LENGTH"] = str(len(data))
environ["wsgi.input"] = io.BytesIO(data)
for key, value in (headers or {}).items():
environ["HTTP_%s" % key.upper().replace("-", "_")] = value
captured = {}
def start_response(status, response_headers, exc_info=None):
captured["status"] = status
chunks = app(environ, start_response)
raw = b"".join(chunks).decode("utf-8", "replace")
code = int(captured["status"].split(" ", 1)[0])
try:
parsed = json.loads(raw)
except ValueError:
parsed = None
return code, parsed, raw
class _ApiServerCase(unittest.TestCase):
"""
Stands up just enough of the API server state (IPC database + DataStore globals)
to drive the routes, snapshotting and restoring every global it touches.
"""
def setUp(self):
conf.batch = True
# snapshot mutated globals
self._saved = {
"current_db": api.DataStore.current_db,
"tasks": api.DataStore.tasks,
"admin_token": api.DataStore.admin_token,
"username": api.DataStore.username,
"password": api.DataStore.password,
"filepath": api.Database.filepath,
}
# fresh in-memory IPC database (same init the server() function performs)
self.db = api.Database(":memory:")
self.db.connect()
self.db.init()
api.DataStore.current_db = self.db
api.DataStore.tasks = {}
api.DataStore.admin_token = "a" * 32
api.DataStore.username = None
api.DataStore.password = None
api.Database.filepath = ":memory:"
def tearDown(self):
try:
self.db.disconnect()
except Exception:
pass
api.DataStore.current_db = self._saved["current_db"]
api.DataStore.tasks = self._saved["tasks"]
api.DataStore.admin_token = self._saved["admin_token"]
api.DataStore.username = self._saved["username"]
api.DataStore.password = self._saved["password"]
api.Database.filepath = self._saved["filepath"]
def _new_task(self):
code, parsed, _ = _wsgi_call("GET", "/task/new")
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
return parsed["taskid"]
# ---------------------------------------------------------------------------
# Pure helpers / objects
# ---------------------------------------------------------------------------
class TestGenericHelpers(unittest.TestCase):
def setUp(self):
self._saved_token = api.DataStore.admin_token
def tearDown(self):
api.DataStore.admin_token = self._saved_token
def test_is_admin_constant_time_compare(self):
api.DataStore.admin_token = "deadbeef"
self.assertTrue(api.is_admin("deadbeef"))
self.assertFalse(api.is_admin("deadbeer"))
self.assertFalse(api.is_admin(None))
self.assertFalse(api.is_admin(""))
class TestValidateTaskOptions(unittest.TestCase):
def setUp(self):
self._saved_tasks = api.DataStore.tasks
api.DataStore.tasks = {"t1": api.Task("t1", "127.0.0.1")}
def tearDown(self):
api.DataStore.tasks = self._saved_tasks
def test_non_dict_rejected(self):
msg = api.validate_task_options("t1", ["level"], "scan_start")
self.assertEqual(msg, "Invalid JSON options")
def test_unsupported_option_rejected(self):
# reportJson is in RESTAPI_UNSUPPORTED_OPTIONS
msg = api.validate_task_options("t1", {"reportJson": "x.json"}, "scan_start")
self.assertIn("Unsupported option", msg)
self.assertIn("reportJson", msg)
def test_readonly_option_rejected(self):
# taskid is in RESTAPI_READONLY_OPTIONS
msg = api.validate_task_options("t1", {"taskid": "haxx"}, "option_set")
self.assertIn("Unsupported option", msg)
self.assertIn("taskid", msg)
def test_unknown_option_rejected(self):
msg = api.validate_task_options("t1", {"nosuchoption": 1}, "option_set")
self.assertIn("Unknown option", msg)
self.assertIn("nosuchoption", msg)
def test_valid_options_accepted(self):
# a real, supported option returns None (no error message)
self.assertIsNone(api.validate_task_options("t1", {"level": 3, "risk": 2}, "scan_start"))
class TestDatabase(unittest.TestCase):
"""The IPC Database wrapper: connect/init schema, execute SELECT vs DML, disconnect."""
def setUp(self):
self.db = api.Database(":memory:")
self.db.connect("test")
self.db.init()
def tearDown(self):
self.db.disconnect()
def test_init_creates_expected_schema(self):
names = set(row[0] for row in self.db.execute("SELECT name FROM sqlite_master WHERE type='table'"))
self.assertTrue({"logs", "data", "errors"}.issubset(names))
def test_init_is_idempotent(self):
# "CREATE TABLE IF NOT EXISTS" - running init twice must not raise
self.db.init()
def test_execute_select_returns_rows_dml_returns_none(self):
self.assertIsNone(self.db.execute("INSERT INTO errors VALUES(NULL, ?, ?)", ("t1", "boom")))
rows = self.db.execute("SELECT taskid, error FROM errors")
self.assertEqual(rows, [("t1", "boom")])
def test_disconnect_is_safe_without_connection(self):
fresh = api.Database(":memory:") # never connected
fresh.disconnect() # must not raise
class TestTask(unittest.TestCase):
"""The Task object: option defaults, set/get/reset, and the no-process engine paths."""
def test_initialize_options_sets_api_markers(self):
t = api.Task("abc123", "10.0.0.1")
self.assertEqual(t.remote_addr, "10.0.0.1")
self.assertIs(t.options.api, True)
self.assertEqual(t.options.taskid, "abc123")
self.assertIs(t.options.batch, True)
self.assertIs(t.options.disableColoring, True)
self.assertIs(t.options.eta, False)
def test_set_get_reset_options(self):
t = api.Task("abc123", "10.0.0.1")
original_level = t.get_option("level")
t.set_option("level", original_level + 4)
self.assertEqual(t.get_option("level"), original_level + 4)
t.reset_options()
self.assertEqual(t.get_option("level"), original_level)
def test_get_options_returns_attribdict(self):
t = api.Task("abc123", "10.0.0.1")
opts = t.get_options()
self.assertIs(opts, t.options)
self.assertIn("level", opts)
def test_engine_paths_without_process(self):
t = api.Task("abc123", "10.0.0.1")
self.assertIsNone(t.engine_process())
self.assertIsNone(t.engine_get_id())
self.assertIsNone(t.engine_get_returncode())
self.assertFalse(t.engine_has_terminated())
self.assertIsNone(t.engine_stop())
self.assertIsNone(t.engine_kill())
class TestStdDbOutAndLogRecorder(unittest.TestCase):
"""
StdDbOut and LogRecorder write engine output/logs into the IPC database
(conf.databaseCursor). Verify both write paths land the expected rows.
"""
def setUp(self):
self.db = api.Database(":memory:")
self.db.connect("client")
self.db.init()
self._saved = {
"stdout": sys.stdout,
"stderr": sys.stderr,
"databaseCursor": conf.get("databaseCursor"),
"taskid": conf.get("taskid"),
"partRun": getattr(__import__("lib.core.data", fromlist=["kb"]).kb, "partRun", None),
}
conf.databaseCursor = self.db
conf.taskid = "t1"
def tearDown(self):
sys.stdout = self._saved["stdout"]
sys.stderr = self._saved["stderr"]
conf.databaseCursor = self._saved["databaseCursor"]
conf.taskid = self._saved["taskid"]
self.db.disconnect()
def test_stdout_write_stores_typed_data(self):
# StdDbOut hijacks sys.stdout in __init__; restore it immediately and call write() directly
std = api.StdDbOut("t1", messagetype="stdout")
sys.stdout = self._saved["stdout"]
std.write("MySQL >= 5.0", status=CONTENT_STATUS.COMPLETE, content_type=CONTENT_TYPE.DBMS_FINGERPRINT)
rows = self.db.execute("SELECT taskid, status, content_type FROM data")
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0][0], "t1")
self.assertEqual(rows[0][2], CONTENT_TYPE.DBMS_FINGERPRINT)
# the helpers are noops but must not raise
std.flush(); std.close(); std.seek()
def test_stderr_write_stores_error(self):
std = api.StdDbOut("t1", messagetype="stderr")
sys.stderr = self._saved["stderr"]
std.write("something failed")
rows = self.db.execute("SELECT taskid, error FROM errors")
self.assertEqual(rows, [("t1", "something failed")])
def test_logrecorder_emit_stores_log(self):
import logging
rec = api.LogRecorder()
record = logging.LogRecord("sqlmap", logging.INFO, __file__, 1, "hello %s", ("world",), None)
rec.emit(record)
rows = self.db.execute("SELECT taskid, level, message FROM logs")
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0][0], "t1")
self.assertEqual(rows[0][1], "INFO")
self.assertEqual(rows[0][2], "hello world")
# ---------------------------------------------------------------------------
# HTTP routes (WSGI test client)
# ---------------------------------------------------------------------------
class TestVersionRoute(_ApiServerCase):
def test_version(self):
code, parsed, _ = _wsgi_call("GET", "/version")
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
self.assertIn("version", parsed)
self.assertEqual(parsed["api_version"], 2) # MAJOR of RESTAPI_VERSION "2.0.0"
def test_security_headers_applied(self):
app = default_app()
environ = {
"REQUEST_METHOD": "GET", "PATH_INFO": "/version",
"SERVER_NAME": "localhost", "SERVER_PORT": "80", "REMOTE_ADDR": "127.0.0.1",
"wsgi.input": io.BytesIO(), "wsgi.errors": sys.stderr, "wsgi.url_scheme": "http",
}
captured = {}
def start_response(status, response_headers, exc_info=None):
captured["headers"] = dict(response_headers)
b"".join(app(environ, start_response))
headers = captured["headers"]
self.assertEqual(headers.get("X-Frame-Options"), "DENY")
self.assertEqual(headers.get("X-Content-Type-Options"), "nosniff")
self.assertIn("application/json", headers.get("Content-Type", ""))
class TestTaskLifecycle(_ApiServerCase):
def test_new_and_delete(self):
taskid = self._new_task()
self.assertIn(taskid, api.DataStore.tasks)
code, parsed, _ = _wsgi_call("GET", "/task/%s/delete" % taskid)
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
self.assertNotIn(taskid, api.DataStore.tasks)
def test_delete_unknown_task_404(self):
code, parsed, _ = _wsgi_call("GET", "/task/deadbeef/delete")
self.assertEqual(code, 404)
self.assertFalse(parsed["success"])
self.assertEqual(parsed["message"], "Non-existing task ID")
class TestOptionRoutes(_ApiServerCase):
def test_option_list(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/option/%s/list" % taskid)
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
self.assertIn("level", parsed["options"])
def test_option_list_invalid_task(self):
code, parsed, _ = _wsgi_call("GET", "/option/nope/list")
self.assertFalse(parsed["success"])
self.assertEqual(parsed["message"], "Invalid task ID")
def test_option_set_then_get(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("POST", "/option/%s/set" % taskid, {"level": 4, "risk": 3})
self.assertTrue(parsed["success"])
code, parsed, _ = _wsgi_call("POST", "/option/%s/get" % taskid, ["level", "risk"])
self.assertTrue(parsed["success"])
self.assertEqual(parsed["options"], {"level": 4, "risk": 3})
def test_option_set_invalid_task(self):
code, parsed, _ = _wsgi_call("POST", "/option/nope/set", {"level": 1})
self.assertFalse(parsed["success"])
self.assertEqual(parsed["message"], "Invalid task ID")
def test_option_set_rejects_unsupported(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("POST", "/option/%s/set" % taskid, {"reportJson": "x"})
self.assertFalse(parsed["success"])
self.assertIn("Unsupported option", parsed["message"])
def test_option_get_unknown_option(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("POST", "/option/%s/get" % taskid, ["nosuchoption"])
self.assertFalse(parsed["success"])
self.assertIn("Unknown option", parsed["message"])
def test_option_get_invalid_task(self):
code, parsed, _ = _wsgi_call("POST", "/option/nope/get", ["level"])
self.assertFalse(parsed["success"])
self.assertEqual(parsed["message"], "Invalid task ID")
class TestScanQueryRoutes(_ApiServerCase):
"""status/data/log on a task that has never launched a subprocess (no scan started)."""
def test_status_not_running(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/scan/%s/status" % taskid)
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
self.assertEqual(parsed["status"], "not running")
self.assertIsNone(parsed["returncode"])
def test_status_invalid_task(self):
code, parsed, _ = _wsgi_call("GET", "/scan/nope/status")
self.assertFalse(parsed["success"])
self.assertEqual(parsed["message"], "Invalid task ID")
def test_data_empty(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid)
self.assertTrue(parsed["success"])
self.assertEqual(parsed["data"], [])
self.assertEqual(parsed["error"], [])
def test_data_returns_stored_rows(self):
taskid = self._new_task()
# store a result row directly into the shared IPC db, then read it back via the route
api._storeData(self.db, taskid, "MySQL >= 5.0", CONTENT_STATUS.COMPLETE, CONTENT_TYPE.DBMS_FINGERPRINT)
code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid)
self.assertTrue(parsed["success"])
self.assertEqual(len(parsed["data"]), 1)
self.assertEqual(parsed["data"][0]["type_name"], "DBMS_FINGERPRINT")
self.assertEqual(parsed["data"][0]["value"], "MySQL >= 5.0")
def test_data_invalid_task(self):
code, parsed, _ = _wsgi_call("GET", "/scan/nope/data")
self.assertFalse(parsed["success"])
def test_log_empty(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/scan/%s/log" % taskid)
self.assertTrue(parsed["success"])
self.assertEqual(parsed["log"], [])
def test_log_returns_stored_rows(self):
taskid = self._new_task()
self.db.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (taskid, "00:00:00", "INFO", "started"))
code, parsed, _ = _wsgi_call("GET", "/scan/%s/log" % taskid)
self.assertTrue(parsed["success"])
self.assertEqual(parsed["log"], [{"time": "00:00:00", "level": "INFO", "message": "started"}])
def test_log_invalid_task(self):
code, parsed, _ = _wsgi_call("GET", "/scan/nope/log")
self.assertFalse(parsed["success"])
def test_log_limited_subset(self):
taskid = self._new_task()
for i in range(1, 4):
self.db.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (taskid, "00:00:0%d" % i, "INFO", "m%d" % i))
code, parsed, _ = _wsgi_call("GET", "/scan/%s/log/1/2" % taskid)
self.assertTrue(parsed["success"])
self.assertEqual([m["message"] for m in parsed["log"]], ["m1", "m2"])
def test_log_limited_bad_range(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/scan/%s/log/5/2" % taskid)
self.assertFalse(parsed["success"])
self.assertIn("must be digits", parsed["message"])
def test_log_limited_invalid_task(self):
code, parsed, _ = _wsgi_call("GET", "/scan/nope/log/1/2")
self.assertFalse(parsed["success"])
self.assertEqual(parsed["message"], "Invalid task ID")
def test_scan_stop_invalid_when_not_running(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/scan/%s/stop" % taskid)
self.assertFalse(parsed["success"])
def test_scan_kill_invalid_when_not_running(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/scan/%s/kill" % taskid)
self.assertFalse(parsed["success"])
class TestScanStart(_ApiServerCase):
"""scan_start, with the subprocess-spawning seam (engine_start) monkeypatched."""
def test_scan_start_invalid_task(self):
code, parsed, _ = _wsgi_call("POST", "/scan/nope/start", {})
self.assertFalse(parsed["success"])
self.assertEqual(parsed["message"], "Invalid task ID")
def test_scan_start_rejects_unsupported_option(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("POST", "/scan/%s/start" % taskid, {"wizard": True})
self.assertFalse(parsed["success"])
self.assertIn("Unsupported option", parsed["message"])
def test_scan_start_launches_engine(self):
taskid = self._new_task()
task = api.DataStore.tasks[taskid]
calls = {"started": False}
class _FakeProc(object):
pid = 4242
returncode = None
def poll(self):
return None
def terminate(self):
pass
def kill(self):
pass
def wait(self):
return 0
def fake_engine_start():
calls["started"] = True
task.process = _FakeProc()
original = task.engine_start
task.engine_start = fake_engine_start
try:
code, parsed, _ = _wsgi_call("POST", "/scan/%s/start" % taskid, {"url": "http://t/?id=1"})
finally:
task.engine_start = original
self.assertTrue(calls["started"])
self.assertTrue(parsed["success"])
self.assertEqual(parsed["engineid"], 4242)
# the provided option was applied to the task
self.assertEqual(task.get_option("url"), "http://t/?id=1")
class TestAdminRoutes(_ApiServerCase):
def test_admin_list_with_token(self):
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/admin/%s/list" % api.DataStore.admin_token)
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
self.assertIn(taskid, parsed["tasks"])
self.assertEqual(parsed["tasks_num"], 1)
def test_admin_list_same_remote_addr_without_token(self):
# /admin/list (no token) sees only tasks from the requesting remote_addr
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/admin/list", remote_addr="127.0.0.1")
self.assertTrue(parsed["success"])
self.assertIn(taskid, parsed["tasks"])
def test_admin_list_other_remote_addr_excluded(self):
self._new_task() # created from 127.0.0.1
code, parsed, _ = _wsgi_call("GET", "/admin/list", remote_addr="10.9.9.9")
self.assertTrue(parsed["success"])
self.assertEqual(parsed["tasks_num"], 0)
def test_admin_flush_with_token(self):
self._new_task()
self._new_task()
self.assertEqual(len(api.DataStore.tasks), 2)
code, parsed, _ = _wsgi_call("GET", "/admin/%s/flush" % api.DataStore.admin_token)
self.assertTrue(parsed["success"])
self.assertEqual(len(api.DataStore.tasks), 0)
def test_admin_flush_only_own_remote_addr(self):
# task from .1, flush requested by .2 (no token) -> task survives
taskid = self._new_task()
code, parsed, _ = _wsgi_call("GET", "/admin/flush", remote_addr="10.0.0.2")
self.assertTrue(parsed["success"])
self.assertIn(taskid, api.DataStore.tasks)
class TestAuthentication(_ApiServerCase):
"""check_authentication before_request hook (HTTP Basic) when credentials are configured."""
def test_no_credentials_allows_access(self):
api.DataStore.username = None
api.DataStore.password = None
code, parsed, _ = _wsgi_call("GET", "/version")
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
def test_missing_auth_header_denied(self):
api.DataStore.username = "user"
api.DataStore.password = "pass"
code, _, raw = _wsgi_call("GET", "/version")
self.assertEqual(code, 401)
def test_wrong_credentials_denied(self):
api.DataStore.username = "user"
api.DataStore.password = "pass"
token = encodeBase64("user:wrong", binary=False)
code, _, raw = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token})
self.assertEqual(code, 401)
def test_correct_credentials_allowed(self):
api.DataStore.username = "user"
api.DataStore.password = "pass"
token = encodeBase64("user:pass", binary=False)
code, parsed, _ = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token})
self.assertEqual(code, 200)
self.assertTrue(parsed["success"])
def test_malformed_basic_credentials_denied(self):
# base64 of a string without ':' separator -> denied
api.DataStore.username = "user"
api.DataStore.password = "pass"
token = encodeBase64("nocolon", binary=False)
code, _, _ = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token})
self.assertEqual(code, 401)
if __name__ == "__main__":
unittest.main(verbosity=2)
+504
View File
@@ -0,0 +1,504 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests for lib/controller/checks.py driven with a MOCKED HTTP layer.
checks.py is the injection-detection controller; almost everything in it goes
through the network seam (lib.request.connect.Connect, imported into the module
as `Request`). By monkeypatching `Request.queryPage` / `Request.getPage` to
return canned (page, headers/ratio, code) tuples - and stubbing `agent.payload`
where the real payload machinery would require a fully-built target - the
decision logic of each check (the kb.*/conf.*/return-value verdict) can be
exercised offline, without a live target, DBMS, or DNS.
Every test snapshots and restores the conf/kb fields it touches AND every
module attribute it monkeypatches, so ordering between tests (and with the rest
of the suite) is irrelevant. conf.batch is forced on to avoid interactive
prompts, and readInput is stubbed per-test where a branch would prompt.
"""
import os
import re
import sys
import time
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
import lib.controller.checks as checks
from lib.core.data import conf, kb
from lib.core.datatype import AttribDict, InjectionDict
from lib.core.dicts import FROM_DUMMY_TABLE
from lib.core.enums import DBMS
from lib.core.enums import HEURISTIC_TEST
from lib.core.enums import HTTP_HEADER
from lib.core.enums import HTTPMETHOD
from lib.core.enums import NULLCONNECTION
from lib.core.enums import PLACE
from lib.core.settings import SINGLE_QUOTE_MARKER
from lib.core.common import getCurrentThreadData
from lib.parse.html import htmlParser
# conf/kb fields any of the checks read or write; snapshotted wholesale so a
# test never leaks state into another test or the rest of the suite.
_CONF_KEYS = (
"paramDict", "parameters", "url", "hostname", "method", "skipHeuristics",
"prefix", "suffix", "nosql", "graphql", "ldap", "beep", "string",
"notString", "regexp", "regex", "dummy", "offline", "skipWaf", "data",
"hashDB", "cj", "cookie", "dropSetCookie", "httpHeaders", "proxy", "tor",
"tamper", "timeout", "retries", "textOnly", "ignoreCode", "disablePrecon",
"ipv6", "multipleTargets", "level", "base64Parameter", "batch",
)
_KB_KEYS = (
"heavilyDynamic", "dynamicParameter", "originalPage", "originalPageTime",
"originalCode", "ignoreCasted", "heuristicMode", "disableHtmlDecoding",
"heuristicTest", "heuristicPage", "heuristicCode", "pageStable",
"nullConnection", "pageCompress", "matchRatio", "skipSeqMatcher",
"choices", "injection", "errorIsNone", "serverHeader", "identifiedWafs",
"tamperFunctions", "resendPostOnRedirect", "checkWafMode", "wafBypass",
"heuristicExtendedDbms", "resumeValues", "mergeCookies", "httpErrorCodes",
)
def _snapshot():
return (
dict((k, conf.get(k)) for k in _CONF_KEYS),
dict((k, kb.get(k)) for k in _KB_KEYS),
)
def _restore(snap):
confSnap, kbSnap = snap
for k, v in confSnap.items():
conf[k] = v
for k, v in kbSnap.items():
kb[k] = v
class _ChecksTestBase(unittest.TestCase):
"""Snapshots conf/kb and the patchable seams; restores them in tearDown."""
def setUp(self):
self._snap = _snapshot()
# remember the real seams so monkeypatches can't leak. agent.payload /
# addPayloadDelimiters are class methods on a shared singleton: patching
# sets an *instance* attribute, so it's restored by deleting that
# attribute (reassigning would leave a stale bound method behind).
self._origQueryPage = checks.Request.queryPage
self._origGetPage = checks.Request.getPage
self._agentHadPayload = "payload" in checks.agent.__dict__
self._agentHadAddDelims = "addPayloadDelimiters" in checks.agent.__dict__
self._origReadInput = checks.readInput
self._origDbmsErr = checks.wasLastResponseDBMSError
self._origHttpErr = checks.wasLastResponseHTTPError
self._origCBE = checks.checkBooleanExpression
# sane offline baseline shared by most checks
conf.batch = True
conf.skipHeuristics = False
conf.prefix = conf.suffix = None
conf.hashDB = None
conf.dummy = conf.offline = conf.proxy = conf.tor = None
kb.choices = AttribDict(keycheck=False)
def tearDown(self):
checks.Request.queryPage = self._origQueryPage
checks.Request.getPage = self._origGetPage
if not self._agentHadPayload and "payload" in checks.agent.__dict__:
del checks.agent.payload
if not self._agentHadAddDelims and "addPayloadDelimiters" in checks.agent.__dict__:
del checks.agent.addPayloadDelimiters
checks.readInput = self._origReadInput
checks.wasLastResponseDBMSError = self._origDbmsErr
checks.wasLastResponseHTTPError = self._origHttpErr
checks.checkBooleanExpression = self._origCBE
_restore(self._snap)
# --- helpers ---
def _patchQueryPage(self, fn):
checks.Request.queryPage = staticmethod(fn)
def _patchGetPage(self, fn):
checks.Request.getPage = staticmethod(fn)
@staticmethod
def _contentQuery(page, code=200, headers=None):
"""A queryPage that returns (page, headers/ratio, code) when content is
requested and a plain truthiness otherwise."""
def _fn(*args, **kwargs):
if kwargs.get("content"):
return (page, headers, code)
return bool(page)
return _fn
@staticmethod
def _detectingContentQuery(page, code=200, headers=None):
"""Like _contentQuery, but mirrors the real connection layer's
error-detection seam: it advances the request UID and runs the REAL
htmlParser() over the page (exactly as Connect.getPage() does), so the
page is classified by sqlmap's genuine error regexes. The unstubbed
wasLastResponseDBMSError() then reads the threadData.lastErrorPage this
leaves behind - the heuristic verdict is the detector's, not the stub's."""
def _fn(*args, **kwargs):
threadData = getCurrentThreadData()
kb.requestCounter = (kb.get("requestCounter") or 0) + 1
threadData.lastRequestUID = kb.requestCounter
htmlParser(page or "")
if kwargs.get("content"):
return (page, headers, code)
return bool(page)
return _fn
@staticmethod
def _comparingQuery(page, code=200, headers=None):
"""A queryPage that, for a non-content request, runs the REAL
comparison() engine of the injected page against kb.pageTemplate (the
same call Connect.queryPage makes for its True/False verdict). The
matchRatio/seqMatcher dynamicity logic therefore actually executes -
the verdict is computed, not hard-coded."""
def _fn(*args, **kwargs):
if kwargs.get("content"):
return (page, headers, code)
return checks.comparison(page, headers, code, getRatioValue=False)
return _fn
class TestHeuristicCheckSqlInjection(_ChecksTestBase):
def setUp(self):
super(TestHeuristicCheckSqlInjection, self).setUp()
conf.paramDict = {PLACE.GET: {"id": "1"}}
conf.parameters = {PLACE.GET: "id=1"}
conf.url = "http://test.invalid/index.php?id=1"
conf.method = None
conf.nosql = conf.graphql = conf.ldap = False
conf.beep = False
kb.heavilyDynamic = False
kb.dynamicParameter = False
kb.originalPage = ""
kb.ignoreCasted = False
# clear any error-page marker left by an earlier request so the real
# wasLastResponseDBMSError() starts from a clean slate
td = getCurrentThreadData()
td.lastErrorPage = tuple()
td.lastRequestUID = 0
# bypass the full payload-building machinery (needs a built target)
checks.agent.payload = lambda *a, **kw: "PAYLOAD"
def test_skip_heuristics_returns_none(self):
conf.skipHeuristics = True
self.assertIsNone(checks.heuristicCheckSqlInjection(PLACE.GET, "id"))
def test_positive_on_dbms_error(self):
# Feed a GENUINE MySQL error page (matches sqlmap's real error regex in
# data/xml/errors.xml) through the detecting stub and let the UNSTUBBED
# wasLastResponseDBMSError() classify it. The POSITIVE verdict is then
# the real detector's, not a hard-coded True.
page = ("<html><body>You have an error in your SQL syntax; check the "
"manual that corresponds to your MySQL server version</body></html>")
self._patchQueryPage(self._detectingContentQuery(page))
result = checks.heuristicCheckSqlInjection(PLACE.GET, "id")
self.assertEqual(result, HEURISTIC_TEST.POSITIVE)
self.assertEqual(kb.heuristicTest, HEURISTIC_TEST.POSITIVE)
def test_negative_on_clean_page(self):
# A clean page matches none of sqlmap's error regexes, so the unstubbed
# wasLastResponseDBMSError() returns false -> NEGATIVE verdict.
self._patchQueryPage(self._detectingContentQuery("a perfectly ordinary page"))
result = checks.heuristicCheckSqlInjection(PLACE.GET, "id")
self.assertEqual(result, HEURISTIC_TEST.NEGATIVE)
self.assertEqual(kb.heuristicTest, HEURISTIC_TEST.NEGATIVE)
def test_records_page_and_resets_mode(self):
self._patchQueryPage(self._detectingContentQuery("nothing special here"))
checks.heuristicCheckSqlInjection(PLACE.GET, "id")
# mode flags must be flipped back off after the check
self.assertFalse(kb.heuristicMode)
self.assertFalse(kb.disableHtmlDecoding)
class TestHeuristicCheckDbms(_ChecksTestBase):
def setUp(self):
super(TestHeuristicCheckDbms, self).setUp()
kb.injection = InjectionDict()
def test_skip_heuristics_returns_false(self):
conf.skipHeuristics = True
self.assertFalse(checks.heuristicCheckDbms(InjectionDict()))
def test_no_match_when_all_expressions_false(self):
checks.checkBooleanExpression = lambda expr: False
self.assertFalse(checks.heuristicCheckDbms(InjectionDict()))
def test_identifies_dbms_on_distinguishing_pair(self):
# An expr-AWARE oracle that recognises ONLY the predicate
# heuristicCheckDbms() builds for one CHOSEN target DBMS. The function
# iterates every DBMS, forging for each the pair
# positive: (SELECT '<r1>'<FROM dbms>)=<Q><r1><Q> -> must be True
# negative: (SELECT '<r1>'<FROM dbms>)=<Q><r2><Q> -> must be False
# (<Q> == SINGLE_QUOTE_MARKER, r1 != r2). The DBMS is reported only when
# the positive holds AND the negative fails. The oracle below returns
# True exactly for that shape - it keys off the chosen DBMS's UNIQUE
# FROM clause (so no other DBMS's predicate matches) and off the two
# quoted literals being equal (so the "must differ" negative is False).
# Firebird is chosen because its FROM clause (' FROM RDB$DATABASE') is
# unique in FROM_DUMMY_TABLE and it is not a HEURISTIC_NULL_EVAL DBMS,
# so heuristicCheckDbms() takes the SELECT-literal predicate path for it.
target = DBMS.FIREBIRD
targetFrom = FROM_DUMMY_TABLE[target]
predicate = re.compile(
r"\(SELECT '([^']*)'( FROM [^)]*)?\)="
+ re.escape(SINGLE_QUOTE_MARKER) + r"(.*?)" + re.escape(SINGLE_QUOTE_MARKER)
)
def oracle(expr):
match = predicate.search(expr)
if not match:
return False
selected, fromClause, compared = match.group(1), match.group(2) or "", match.group(3)
# True only for the target DBMS's FROM clause with matching literals
return fromClause == targetFrom and selected == compared
checks.checkBooleanExpression = oracle
result = checks.heuristicCheckDbms(InjectionDict())
# real predicate matching must single out the chosen DBMS, not whatever
# getPublicTypeMembers() happens to yield first
self.assertEqual(result, target)
self.assertEqual(kb.heuristicExtendedDbms, target)
class TestCheckDynParam(_ChecksTestBase):
# A stable baseline page that checkDynParam's injected response is compared
# against by the REAL comparison() engine. Long enough that difflib's
# quick_ratio is meaningful rather than degenerate.
_BASELINE = ("<html><head><title>Welcome</title></head><body>"
+ "the quick brown fox jumps over the lazy dog. " * 20
+ "</body></html>")
def setUp(self):
super(TestCheckDynParam, self).setUp()
conf.method = None
checks.agent.payload = lambda *a, **kw: "PAYLOAD"
# state the real comparison() engine reads
conf.string = conf.notString = conf.regexp = conf.code = None
conf.titles = conf.textOnly = False
kb.nullConnection = False
kb.heavilyDynamic = False
kb.skipSeqMatcher = False
kb.errorIsNone = False
kb.negativeLogic = False
kb.pageCompress = False
kb.matchRatio = None
kb.pageTemplate = self._BASELINE
def test_redirect_short_circuits(self):
kb.choices.redirect = "yes"
self.assertIsNone(checks.checkDynParam(PLACE.GET, "id", "1"))
def test_dynamic_when_page_differs(self):
# A response wildly different from the baseline drives the real
# comparison() ratio below LOWER_RATIO_BOUND -> queryPage returns False
# (page differs) -> parameter is dynamic.
self._patchQueryPage(self._comparingQuery("totally unrelated content " + "Z" * 200))
result = checks.checkDynParam(PLACE.GET, "id", "1")
self.assertTrue(result)
self.assertTrue(kb.dynamicParameter)
def test_not_dynamic_when_page_same(self):
# An identical response yields ratio 1.0 (> UPPER_RATIO_BOUND) from the
# real comparison() -> queryPage returns True (page same) -> not dynamic.
self._patchQueryPage(self._comparingQuery(self._BASELINE))
result = checks.checkDynParam(PLACE.GET, "id", "1")
self.assertFalse(result)
self.assertFalse(kb.dynamicParameter)
class TestCheckDynamicContent(_ChecksTestBase):
def setUp(self):
super(TestCheckDynamicContent, self).setUp()
kb.nullConnection = False
def test_null_connection_skips(self):
kb.nullConnection = NULLCONNECTION.HEAD
self.assertIsNone(checks.checkDynamicContent("a", "b"))
def test_missing_page_aborts(self):
self.assertIsNone(checks.checkDynamicContent(None, "x"))
def test_identical_pages_no_dynamicity(self):
# high ratio -> no dynamic-content engine, no further requests
self._patchQueryPage(lambda *a, **kw: self.fail("should not request"))
self.assertIsNone(checks.checkDynamicContent("identical content", "identical content"))
class TestCheckStability(_ChecksTestBase):
def setUp(self):
super(TestCheckStability, self).setUp()
kb.originalPageTime = time.time()
kb.nullConnection = False
def test_stable_when_pages_match(self):
kb.originalPage = "SAME PAGE"
self._patchQueryPage(self._contentQuery("SAME PAGE"))
self.assertTrue(checks.checkStability())
self.assertTrue(kb.pageStable)
def test_redirect_returns_none(self):
kb.originalPage = "SAME PAGE"
self._patchQueryPage(self._contentQuery("SAME PAGE"))
kb.choices.redirect = "yes"
self.assertIsNone(checks.checkStability())
def test_unstable_continue_choice(self):
kb.originalPage = "FIRST PAGE CONTENT"
conf.retries = 0
kb.heavilyDynamic = False
checks.readInput = lambda *a, **kw: "C"
def _q(*a, **kw):
if kw.get("content"):
return ("SECOND DIFFERENT PAGE", None, 200)
return True # keeps checkDynamicContent's retry loop from firing
self._patchQueryPage(_q)
result = checks.checkStability()
self.assertFalse(result)
self.assertFalse(kb.pageStable)
def test_unstable_string_choice_sets_conf_string(self):
kb.originalPage = "FIRST"
self._patchQueryPage(self._contentQuery("SECOND"))
replies = iter(["S", "MATCHME"])
checks.readInput = lambda *a, **kw: next(replies)
checks.checkStability()
self.assertEqual(conf.string, "MATCHME")
class TestCheckNullConnection(_ChecksTestBase):
def setUp(self):
super(TestCheckNullConnection, self).setUp()
conf.data = None
kb.pageCompress = False
kb.nullConnection = None
def test_post_data_disables_null_connection(self):
conf.data = "a=b"
self.assertFalse(checks.checkNullConnection())
def test_head_content_length(self):
def _getPage(*a, **kw):
if kw.get("method") == HTTPMETHOD.HEAD:
return ("", {HTTP_HEADER.CONTENT_LENGTH: "1234"}, 200)
return ("x", {}, 200)
self._patchGetPage(_getPage)
self.assertTrue(checks.checkNullConnection())
self.assertEqual(kb.nullConnection, NULLCONNECTION.HEAD)
def test_range_content_range(self):
def _getPage(*a, **kw):
if kw.get("method") == HTTPMETHOD.HEAD:
return ("", {}, 200) # no Content-Length on HEAD
if kw.get("auxHeaders"):
return ("A", {HTTP_HEADER.CONTENT_RANGE: "bytes 0-0/100"}, 206)
return ("x", {}, 200)
self._patchGetPage(_getPage)
self.assertTrue(checks.checkNullConnection())
self.assertEqual(kb.nullConnection, NULLCONNECTION.RANGE)
def test_not_supported(self):
# nothing usable on any method -> nullConnection ends up False
self._patchGetPage(lambda *a, **kw: ("xx", {}, 200))
self.assertFalse(checks.checkNullConnection())
self.assertFalse(kb.nullConnection)
class TestCheckConnection(_ChecksTestBase):
def setUp(self):
super(TestCheckConnection, self).setUp()
conf.hostname = "1.2.3.4" # dotted-quad -> no DNS resolution
conf.string = conf.regexp = None
conf.cj = None
conf.ignoreCode = None
kb.httpErrorCodes = {}
checks.wasLastResponseHTTPError = lambda: False
checks.wasLastResponseDBMSError = lambda: False
td = getCurrentThreadData()
td.lastPage = "PAGE CONTENT"
td.lastCode = 200
class _Headers(object):
headers = "Server: test\r\n"
def test_success_sets_error_is_none(self):
self._patchQueryPage(lambda *a, **kw: ("PAGE CONTENT", self._Headers(), 200))
self.assertTrue(checks.checkConnection())
self.assertTrue(kb.errorIsNone)
self.assertEqual(kb.originalPage, "PAGE CONTENT")
def test_dbms_error_clears_error_is_none(self):
self._patchQueryPage(lambda *a, **kw: ("oops SQL error", self._Headers(), 200))
checks.wasLastResponseDBMSError = lambda: True
self.assertTrue(checks.checkConnection())
self.assertFalse(kb.errorIsNone)
def test_string_not_in_response_still_continues(self):
conf.string = "NEEDLE-NOT-PRESENT"
self._patchQueryPage(lambda *a, **kw: ("haystack only", self._Headers(), 200))
# warns but carries on (returns True)
self.assertTrue(checks.checkConnection())
class TestCheckWaf(_ChecksTestBase):
def setUp(self):
super(TestCheckWaf, self).setUp()
conf.string = conf.notString = conf.regexp = None
conf.dummy = conf.offline = conf.skipWaf = None
kb.originalCode = 200
kb.originalPage = "page"
conf.parameters = {PLACE.GET: "id=1"}
kb.resendPostOnRedirect = False
conf.timeout = 30
kb.identifiedWafs = []
conf.tamper = None
kb.tamperFunctions = []
checks.agent.addPayloadDelimiters = lambda v: v
def test_skips_when_string_set(self):
conf.string = "x"
self.assertIsNone(checks.checkWaf())
def test_not_detected_on_high_ratio(self):
# queryPage()[1] is the ratio; high ratio -> not blocked
self._patchQueryPage(lambda *a, **kw: ("ok", 0.9, 200))
self.assertFalse(checks.checkWaf())
def test_detected_on_low_ratio(self):
self._patchQueryPage(lambda *a, **kw: ("blocked", 0.1, 403))
checks.readInput = lambda *a, **kw: True # continue + accept bypass
import lib.utils.wafbypass as wafbypass
orig = wafbypass.neutralizeFingerprint
wafbypass.neutralizeFingerprint = lambda: None
try:
self.assertTrue(checks.checkWaf())
finally:
wafbypass.neutralizeFingerprint = orig
class TestCheckInternet(_ChecksTestBase):
def test_internet_available(self):
self._patchGetPage(lambda *a, **kw: ("ok", None, checks.CHECK_INTERNET_CODE))
self.assertTrue(checks.checkInternet())
def test_internet_unavailable(self):
self._patchGetPage(lambda *a, **kw: ("captive portal", None, 500))
self.assertFalse(checks.checkInternet())
if __name__ == "__main__":
unittest.main(verbosity=2)
+466
View File
@@ -0,0 +1,466 @@
#!/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)
+340
View File
@@ -0,0 +1,340 @@
#!/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:[email protected]: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)
+290
View File
@@ -0,0 +1,290 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Tests for lib/core/compat.py -- cross-version compatibility utilities,
including WichmannHill RNG, patchHeaders, cmp_to_key, LooseVersion,
MixedWriteTextIO, and _codecs_open.
"""
import io
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
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
class TestWichmannHill(unittest.TestCase):
def test_seed_and_random(self):
r = WichmannHill(42)
self.assertIsInstance(r.random(), float)
self.assertGreaterEqual(r.random(), 0.0)
self.assertLess(r.random(), 1.0)
def test_deterministic_seed(self):
r1 = WichmannHill(123)
r2 = WichmannHill(123)
# First random numbers should match
self.assertEqual([r1.random() for _ in range(10)],
[r2.random() for _ in range(10)])
def test_getstate_setstate(self):
r = WichmannHill(7)
for _ in range(20):
r.random()
state = r.getstate()
saved = [r.random() for _ in range(5)]
r.setstate(state)
self.assertEqual(saved, [r.random() for _ in range(5)])
def test_jumpahead(self):
r1 = WichmannHill(99)
r2 = WichmannHill(99)
for _ in range(10):
r1.random()
r2.jumpahead(10)
self.assertEqual(r1.getstate()[1], r2.getstate()[1])
def test_jumpahead_negative_raises(self):
r = WichmannHill()
with self.assertRaises(ValueError):
r.jumpahead(-1)
def test_whseed(self):
# a fixed integer whseed must be deterministic across instances ...
r1 = WichmannHill()
r1.whseed(12345)
r2 = WichmannHill()
r2.whseed(12345)
self.assertEqual([r1.random() for _ in range(10)],
[r2.random() for _ in range(10)])
# ... and pin the known sequence (hash(int) == int, so stable across processes)
r3 = WichmannHill()
r3.whseed(12345)
self.assertEqual([round(r3.random(), 6) for _ in range(3)],
[0.600031, 0.872148, 0.039151])
def test_whseed_none(self):
r = WichmannHill()
r.whseed() # seeds from current time; must not raise
# the time-derived seed must still drive a valid in-range sequence. (Non-determinism is NOT
# asserted here: __whseed() derives its seed from int(time.time()*256) masked to 24 bits, so
# two back-to-back instances legitimately collide - that would be a timing-fragile test. The
# os.urandom-backed seed() None path IS asserted non-deterministic in test_seed_none.)
seq = [r.random() for _ in range(10)]
self.assertTrue(all(isinstance(x, float) and 0.0 <= x < 1.0 for x in seq))
# the seed must actually advance the generator (not stuck on a constant)
self.assertGreater(len(set(seq)), 1)
def test_seed_none(self):
r = WichmannHill()
r.seed() # seeds from os.urandom/time; must not raise
seq = [r.random() for _ in range(10)]
self.assertTrue(all(isinstance(x, float) and 0.0 <= x < 1.0 for x in seq))
other = WichmannHill()
other.seed()
self.assertNotEqual(seq, [other.random() for _ in range(10)])
def test_seed_hashable(self):
# a non-int hashable seed goes through hash(a); two instances seeded with the same
# object in the same process must produce the same sequence (determinism). The literal
# values are NOT pinned because hash() of a str is randomized per process.
r1 = WichmannHill("a_string_seed")
r2 = WichmannHill("a_string_seed")
seq = [r1.random() for _ in range(10)]
self.assertEqual(seq, [r2.random() for _ in range(10)])
self.assertTrue(all(0.0 <= x < 1.0 for x in seq))
# a different seed must yield a different sequence
r3 = WichmannHill("different_seed")
self.assertNotEqual(seq, [r3.random() for _ in range(10)])
def test_setstate_bad_version(self):
r = WichmannHill()
with self.assertRaises(ValueError):
r.setstate((999, (1, 1, 1), None))
class TestPatchHeaders(unittest.TestCase):
def test_patches_dict_to_header_obj(self):
h = patchHeaders({"Host": "example.com", "Content-Type": "text/html"})
self.assertEqual(h["host"], "example.com")
self.assertEqual(h["content-type"], "text/html")
self.assertEqual(h.get("HOST"), "example.com")
self.assertIsNone(h.get("missing"))
self.assertIsNotNone(h.headers)
self.assertTrue(any("Host: example.com" in _ for _ in h.headers))
def test_passthrough_none(self):
self.assertIsNone(patchHeaders(None))
def test_passthrough_existing_headers_attr(self):
d = {"A": "1"}
d["headers"] = []
result = patchHeaders(d)
self.assertEqual(result, d) # unchanged
class TestCmp(unittest.TestCase):
def test_less(self):
self.assertEqual(cmp("a", "b"), -1)
def test_greater(self):
self.assertEqual(cmp(2, 1), 1)
def test_equal(self):
self.assertEqual(cmp(5, 5), 0)
class TestRound(unittest.TestCase):
def test_positive(self):
self.assertEqual(round(2.0), 2.0)
self.assertEqual(round(2.5), 3.0)
self.assertEqual(round(2.499), 2.0)
def test_negative(self):
self.assertEqual(round(-2.5), -3.0)
self.assertEqual(round(-2.0), -2.0)
def test_with_decimals(self):
self.assertAlmostEqual(round(2.567, d=2), 2.57)
class TestCmpToKey(unittest.TestCase):
def test_sort_with_cmp(self):
items = [3, 1, 4, 1, 5]
key_func = cmp_to_key(lambda a, b: (a > b) - (a < b))
self.assertEqual(sorted(items, key=key_func), [1, 1, 3, 4, 5])
def test_reverse_sort(self):
items = [3, 1, 2]
key_func = cmp_to_key(lambda a, b: (b > a) - (b < a))
self.assertEqual(sorted(items, key=key_func), [3, 2, 1])
def test_hash_raises(self):
k = cmp_to_key(lambda a, b: 0)(5)
with self.assertRaises(TypeError):
hash(k)
class TestLooseVersion(unittest.TestCase):
def test_basic(self):
self.assertEqual(LooseVersion("1.0"), (1, 0))
self.assertEqual(LooseVersion("1.0.1"), (1, 0, 1))
def test_comparison(self):
self.assertTrue(LooseVersion("1.0.1") > LooseVersion("1.0"))
self.assertTrue(LooseVersion("8.0.22") > LooseVersion("8.0.2"))
def test_no_digits(self):
self.assertEqual(LooseVersion("alpha"), ())
self.assertEqual(LooseVersion(""), ())
self.assertEqual(LooseVersion(None), ())
def test_with_suffix(self):
self.assertEqual(LooseVersion("1.0alpha"), (1, 0))
self.assertEqual(LooseVersion("10.5.3-beta"), (10, 5, 3))
class TestIsWriteMode(unittest.TestCase):
def test_write_modes(self):
for mode in ("w", "a", "x", "w+", "a+", "x+", "w+b", "ab"):
self.assertTrue(_is_write_mode(mode), msg="mode %r" % mode)
def test_read_modes(self):
for mode in ("r", "rb", ""):
self.assertFalse(_is_write_mode(mode), msg="mode %r" % mode)
class TestMixedWriteTextIO(unittest.TestCase):
def test_text_write(self):
buf = io.StringIO()
w = MixedWriteTextIO(buf, "utf-8", "strict")
w.write(u"hello")
self.assertEqual(buf.getvalue(), "hello")
def test_bytes_write_decodes(self):
buf = io.StringIO()
w = MixedWriteTextIO(buf, "utf-8", "strict")
w.write(b"world")
self.assertEqual(buf.getvalue(), "world")
def test_writelines(self):
buf = io.StringIO()
w = MixedWriteTextIO(buf, "utf-8", "strict")
w.writelines([u"a", u"b", u"c"])
self.assertEqual(buf.getvalue(), "abc")
def test_iterator(self):
buf = io.StringIO(u"line1\nline2\n")
w = MixedWriteTextIO(buf, "utf-8", "strict")
self.assertEqual(list(w), ["line1\n", "line2\n"])
def test_enter_exit(self):
buf = io.StringIO()
w = MixedWriteTextIO(buf, "utf-8", "strict")
with w as f:
f.write(u"test")
self.assertTrue(buf.closed)
class TestCodecsOpen(unittest.TestCase):
def test_no_encoding_returns_io_open(self):
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
tmp.close()
try:
f = _codecs_open(tmp.name, "w", encoding=None)
f.write(u"test")
f.close()
with open(tmp.name) as fh:
self.assertIn("test", fh.read())
finally:
os.unlink(tmp.name)
def test_with_encoding(self):
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
tmp.close()
try:
f = _codecs_open(tmp.name, "w", encoding="utf-8")
f.write(u"caf\xe9")
f.close()
with open(tmp.name, "rb") as fh:
self.assertIn(b"caf\xc3\xa9", fh.read())
finally:
os.unlink(tmp.name)
def test_with_encoding_and_bytes(self):
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
tmp.close()
try:
f = _codecs_open(tmp.name, "w", encoding="utf-8")
# MixedWriteTextIO should accept bytes too
f.write(b"bytes_input")
f.close()
with open(tmp.name) as fh:
self.assertIn("bytes_input", fh.read())
finally:
os.unlink(tmp.name)
class TestChooseBoundary(unittest.TestCase):
def test_length(self):
self.assertEqual(len(choose_boundary()), 32)
def test_hex_chars(self):
b = choose_boundary()
self.assertTrue(all(c in "0123456789abcdef" for c in b))
if __name__ == "__main__":
unittest.main(verbosity=2)
+676
View File
@@ -0,0 +1,676 @@
#!/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()
+605
View File
@@ -0,0 +1,605 @@
#!/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()
+706
View File
@@ -0,0 +1,706 @@
#!/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)
+511
View File
@@ -0,0 +1,511 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests for the enumeration methods of plugins/generic/databases.py.
The injection layer (lib.request.inject.getValue) is mocked so no network or
live DBMS is required; each test drives a single enumeration method down a
specific branch (conf.direct "inband" path or the isInferenceAvailable() blind
path) and asserts on the returned value / kb.data.cached* state.
CRITICAL: every test restores conf.*, the patched dbmod.inject.getValue, and the
mutated kb.data flags in tearDown so global state does not leak 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
from plugins.generic.databases import Databases
# Databases.forceDbmsEnum() is supplied at runtime by the concrete dbms fingerprint
# plugin mixin (plugins/dbms/*/fingerprint.py); a bare Databases() instance lacks it,
# so neutralize it for the duration of these tests. Restored in tearDown via the saved ref.
_NOOP = lambda self: None
class _BaseEnumTest(unittest.TestCase):
"""Shared setup/teardown that snapshots and restores all touched global state."""
# conf keys every test may read/write
_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_injection_data = kb.injection.data
self._saved_has_is = kb.data.get("has_information_schema")
# the inference paths of getTables/getColumns set kb.hintValue as a side effect;
# snapshot it so we never leak a stale hint into other test files (e.g. the
# inference engine's tryHint(), whose setUp does not reset it).
self._saved_hintValue = kb.get("hintValue")
self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None)
Databases.forceDbmsEnum = _NOOP
# sane defaults shared by most tests
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
kb.injection.data = self._saved_injection_data
kb.data.has_information_schema = self._saved_has_is
kb.hintValue = self._saved_hintValue
if self._saved_forceDbmsEnum is not None:
Databases.forceDbmsEnum = self._saved_forceDbmsEnum
else:
try:
del Databases.forceDbmsEnum
except AttributeError:
pass
# helpers -----------------------------------------------------------------
def _fresh(self):
"""Return a Databases() instance with every cache reset to empty."""
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 _enable_inference(self):
"""Take the blind inference branch: conf.direct off, a BOOLEAN technique present."""
conf.direct = False
conf.technique = None
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
class TestGetCurrentDb(_BaseEnumTest):
def test_current_db_mysql(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
dbmod.inject.getValue = lambda query, *a, **k: "testdb"
self.assertEqual(d.getCurrentDb(), "testdb")
self.assertEqual(kb.data.currentDb, "testdb")
def test_current_db_cached(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
kb.data.currentDb = "already"
def _boom(*a, **k):
raise AssertionError("inject.getValue must not be called when currentDb is cached")
dbmod.inject.getValue = _boom
self.assertEqual(d.getCurrentDb(), "already")
def test_current_db_oracle_schema_warning_branch(self):
# Oracle takes the schema-name warning branch; result still returned.
set_dbms("Oracle")
conf.direct = True
d = self._fresh()
dbmod.inject.getValue = lambda query, *a, **k: "SYSTEM"
self.assertEqual(d.getCurrentDb(), "SYSTEM")
class TestGetDbs(_BaseEnumTest):
def test_get_dbs_direct_mysql(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
dbmod.inject.getValue = lambda query, *a, **k: [["information_schema"], ["mysql"], ["testdb"]]
result = d.getDbs()
self.assertEqual(sorted(result), ["information_schema", "mysql", "testdb"])
self.assertIn("testdb", kb.data.cachedDbs)
def test_get_dbs_cached_short_circuit(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
kb.data.cachedDbs = ["pre", "cached"]
def _boom(*a, **k):
raise AssertionError("must not query when cachedDbs is populated")
dbmod.inject.getValue = _boom
self.assertEqual(d.getDbs(), ["pre", "cached"])
def test_get_dbs_direct_pgsql_schema_branch(self):
set_dbms("PostgreSQL")
conf.direct = True
d = self._fresh()
dbmod.inject.getValue = lambda query, *a, **k: [["public"], ["information_schema"]]
result = d.getDbs()
self.assertEqual(sorted(result), ["information_schema", "public"])
def test_get_dbs_mysql_no_information_schema(self):
# MySQL < 5: query2 / count2 branch; still inband under conf.direct.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
kb.data.has_information_schema = False
dbmod.inject.getValue = lambda query, *a, **k: [["mysql"], ["app"]]
result = d.getDbs()
self.assertEqual(sorted(result), ["app", "mysql"])
def test_get_dbs_inference(self):
set_dbms("MySQL")
self._enable_inference()
d = self._fresh()
names = ["alpha", "beta", "gamma"]
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(names))
val = names[state["i"]]
state["i"] += 1
return [val]
dbmod.inject.getValue = gv
result = d.getDbs()
self.assertEqual(sorted(result), sorted(names))
def test_get_dbs_fallback_to_current(self):
# No dbs returned inband -> falls back to current database.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
state = {"n": 0}
def gv(query, *a, **k):
state["n"] += 1
if state["n"] == 1:
return None # getDbs inband: nothing
return "fallbackdb" # getCurrentDb
dbmod.inject.getValue = gv
result = d.getDbs()
self.assertEqual(result, ["fallbackdb"])
class TestGetTables(_BaseEnumTest):
def test_get_tables_direct_mysql(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = None
dbmod.inject.getValue = lambda query, *a, **k: [["testdb", "users"], ["testdb", "posts"]]
result = d.getTables()
self.assertIn("testdb", result)
self.assertEqual(sorted(result["testdb"]), ["posts", "users"])
def test_get_tables_cached_short_circuit(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
kb.data.cachedTables = {"db": ["t1"]}
def _boom(*a, **k):
raise AssertionError("must not query when cachedTables is populated")
dbmod.inject.getValue = _boom
self.assertEqual(d.getTables(), {"db": ["t1"]})
def test_get_tables_direct_pgsql(self):
set_dbms("PostgreSQL")
conf.direct = True
d = self._fresh()
conf.db = "public"
conf.tbl = None
dbmod.inject.getValue = lambda query, *a, **k: [["public", "accounts"]]
result = d.getTables()
self.assertEqual(result.get("public"), ["accounts"])
def test_get_tables_inference(self):
set_dbms("MySQL")
self._enable_inference()
d = self._fresh()
conf.db = "testdb"
conf.tbl = None
tables = ["t_a", "t_b"]
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(tables))
val = tables[state["i"] % len(tables)]
state["i"] += 1
return [val]
dbmod.inject.getValue = gv
result = d.getTables()
self.assertIn("testdb", result)
self.assertEqual(sorted(result["testdb"]), sorted(tables))
class TestGetColumns(_BaseEnumTest):
def _run_direct(self, dbms, db, tbl, rows):
set_dbms(dbms)
conf.direct = True
d = self._fresh()
conf.db = db
conf.tbl = tbl
dbmod.inject.getValue = lambda query, *a, **k: rows
return d.getColumns()
def test_columns_direct_mysql(self):
result = self._run_direct("MySQL", "testdb", "users", [["id", "int"], ["age", "int"]])
self.assertIn("testdb", result)
cols = result["testdb"]["users"]
self.assertEqual(cols.get("id"), "int")
self.assertEqual(cols.get("age"), "int")
def test_columns_direct_pgsql(self):
result = self._run_direct("PostgreSQL", "public", "users", [["id", "integer"]])
self.assertEqual(result["public"]["users"].get("id"), "integer")
def test_columns_direct_oracle_uppercase(self):
# Oracle is an UPPER_CASE dbms: conf.db/tbl get upcased internally.
result = self._run_direct("Oracle", "system", "users", [["ID", "NUMBER"]])
# Oracle quotes the identifier ("SYSTEM"); assert the column landed regardless.
flat = {}
for tables in result.values():
for cols in tables.values():
flat.update(cols)
self.assertEqual(flat.get("ID"), "NUMBER")
def test_columns_direct_mssql(self):
result = self._run_direct("Microsoft SQL Server", "master", "users", [["id", "int"]])
# MSSQL wraps the db identifier in [brackets]; assert the column landed.
flat = {}
for tables in result.values():
for cols in tables.values():
flat.update(cols)
self.assertEqual(flat.get("id"), "int")
def test_columns_only_names(self):
# onlyColNames is ONLY read in the inference branch (the INBAND path
# ignores it), so drive the blind inference path like
# test_columns_inference_mysql but with onlyColNames=True. The flag must
# SUPPRESS the type lookup: each column's value lands as None instead of
# the real type. Asserting cols.get("id") is None proves the flag took
# effect (otherwise the type query would run and return "int").
set_dbms("MySQL")
self._enable_inference()
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
colnames = ["id", "name"]
state = {"i": 0}
type_queries = {"n": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(colnames))
# With onlyColNames the second-stage type query (blind.query2, which
# selects column_type) must NEVER be issued.
if "column_type" in query.lower():
type_queries["n"] += 1
return ["int"]
val = colnames[state["i"] % len(colnames)]
state["i"] += 1
return [val]
dbmod.inject.getValue = gv
result = d.getColumns(onlyColNames=True)
cols = result["testdb"]["users"]
# both column names enumerated...
self.assertEqual(len(cols), len(colnames))
self.assertIn("id", cols)
# ...but their types were suppressed (None), and no type query ran.
self.assertIsNone(cols.get("id"))
self.assertEqual(type_queries["n"], 0)
def test_columns_inference_mysql(self):
set_dbms("MySQL")
self._enable_inference()
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
colnames = ["id", "name"]
state = {"i": 0, "names": True}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(colnames))
# alternate: column name then its type
if state["names"]:
val = colnames[state["i"] % len(colnames)]
state["i"] += 1
state["names"] = False
return [val]
else:
state["names"] = True
return ["int"]
dbmod.inject.getValue = gv
result = d.getColumns()
self.assertIn("testdb", result)
cols = result["testdb"]["users"]
# both columns enumerated (reserved words like "name" get quoted, so count, not exact keys)
self.assertEqual(len(cols), len(colnames))
self.assertEqual(cols.get("id"), "int")
class TestGetCount(_BaseEnumTest):
def test_count_single_table_mysql(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
dbmod.inject.getValue = lambda query, *a, **k: "42"
result = d.getCount()
self.assertEqual(result, {"testdb": {42: ["users"]}})
def test_count_dotted_table_splits_db(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = None
conf.tbl = "shop.orders"
dbmod.inject.getValue = lambda query, *a, **k: "7"
result = d.getCount()
self.assertEqual(result, {"shop": {7: ["orders"]}})
def test_count_multiple_tables(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users,posts"
counts = {"users": "3", "posts": "5"}
def gv(query, *a, **k):
# the table name appears in the FROM clause of the generated query
for t, c in counts.items():
if t in query:
return c
return "0"
dbmod.inject.getValue = gv
result = d.getCount()
self.assertIn("testdb", result)
self.assertIn("users", result["testdb"][3])
self.assertIn("posts", result["testdb"][5])
class TestGetStatements(_BaseEnumTest):
def test_statements_direct_mysql(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
dbmod.inject.getValue = lambda query, *a, **k: [["SELECT 1"], ["SELECT 2"]]
result = d.getStatements()
self.assertEqual(sorted(result), ["SELECT 1", "SELECT 2"])
def test_statements_direct_pgsql(self):
set_dbms("PostgreSQL")
conf.direct = True
d = self._fresh()
dbmod.inject.getValue = lambda query, *a, **k: [["SELECT now()"]]
result = d.getStatements()
self.assertEqual(result, ["SELECT now()"])
def test_statements_inference(self):
set_dbms("PostgreSQL")
self._enable_inference()
d = self._fresh()
stmts = ["SELECT a", "SELECT b"]
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(stmts))
val = stmts[state["i"] % len(stmts)]
state["i"] += 1
return [val]
dbmod.inject.getValue = gv
result = d.getStatements()
self.assertEqual(sorted(result), sorted(stmts))
class TestGetSchema(_BaseEnumTest):
def test_schema_mysql(self):
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = None
conf.col = None
state = {"n": 0}
def gv(query, *a, **k):
state["n"] += 1
if state["n"] == 1:
# getTables call
return [["testdb", "users"]]
# getColumns call
return [["id", "int"]]
dbmod.inject.getValue = gv
result = d.getSchema()
self.assertIn("testdb", result)
self.assertIn("users", result["testdb"])
self.assertEqual(result["testdb"]["users"].get("id"), "int")
class TestGetProcedures(_BaseEnumTest):
def test_procedures_direct_pgsql(self):
set_dbms("PostgreSQL")
conf.direct = True
d = self._fresh()
dbmod.inject.getValue = lambda query, *a, **k: [["proc_a"], ["proc_b"]]
result = d.getProcedures()
self.assertEqual(sorted(result), ["proc_a", "proc_b"])
def test_procedures_inference_mysql(self):
set_dbms("MySQL")
self._enable_inference()
d = self._fresh()
procs = ["sp_one", "sp_two"]
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(procs))
val = procs[state["i"] % len(procs)]
state["i"] += 1
return [val]
dbmod.inject.getValue = gv
result = d.getProcedures()
self.assertEqual(sorted(result), sorted(procs))
if __name__ == "__main__":
unittest.main()
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
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.
"""
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
class _EnumBase(unittest.TestCase):
"""Snapshot/restore the global state these enumerators mutate."""
module = None # the enumeration module whose inject.getValue we patch
def setUp(self):
self._direct = conf.direct
self._db = conf.db
self._gv = self.module.inject.getValue
self._cachedTables = kb.data.get("cachedTables")
self._cachedColumns = kb.data.get("cachedColumns")
conf.direct = True
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
def tearDown(self):
conf.direct = self._direct
conf.db = self._db
self.module.inject.getValue = self._gv
kb.data.cachedTables = self._cachedTables
kb.data.cachedColumns = self._cachedColumns
class TestMSSQLServerEnum(_EnumBase):
import plugins.dbms.mssqlserver.enumeration as module
def _handler(self):
from plugins.dbms.mssqlserver import MSSQLServerMap
set_dbms("Microsoft SQL Server")
return MSSQLServerMap()
def test_get_tables(self):
# one database (conf.db), single-column rows: getTables keys the cache by
# the db loop variable and stores the rows run through
# arrayize -> unArrayize -> safeSQLIdentificatorNaming -> sorted().
conf.db = "appdb"
self.module.inject.getValue = lambda q, *a, **k: (
3 if k.get("expected") == EXPECTED.INT else [["users"], ["products"], ["customers"]]
)
self._handler().getTables()
tables = kb.data.cachedTables
self.assertEqual(list(tables.keys()), ["appdb"])
stored = tables["appdb"]
# value is a real sorted list (the final sort step), not an echo of input
self.assertEqual(stored, sorted(stored))
# MSSQL qualifies bare names with the dbo schema; assert exact membership
self.assertIn("dbo.users", stored)
self.assertEqual(stored, ["dbo.customers", "dbo.products", "dbo.users"])
def test_get_tables_multiple_dbs(self):
# exercise the per-database keying with two DBs (conf.db = "a,b"): each db
# in the loop gets its OWN sorted table list. Rows are single-column;
# unArrayizeValue collapses each 1-tuple row to the scalar table name.
conf.db = "appdb,salesdb"
def getValue(q, *a, **k):
if k.get("expected") == EXPECTED.INT:
return 3
# the query carries the db name (%s substituted); route per database
if "appdb" in q:
return [["users"], ["sessions"], ["accounts"]]
return [["orders"], ["invoices"]]
self.module.inject.getValue = getValue
self._handler().getTables()
tables = kb.data.cachedTables
# exactly the two requested databases, each mapped to its own sorted list
self.assertEqual(sorted(tables.keys()), ["appdb", "salesdb"])
self.assertEqual(tables["appdb"], ["dbo.accounts", "dbo.sessions", "dbo.users"])
self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"])
if __name__ == "__main__":
unittest.main()
+215
View File
@@ -0,0 +1,215 @@
#!/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()
+469
View File
@@ -0,0 +1,469 @@
#!/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()
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Optional-dependency probe (lib/utils/deps.py, the --dependencies feature).
checkDependencies() attempts to import every supported DBMS driver and warns
on the ones missing; it must never raise regardless of what's installed.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
import lib.utils.deps as deps
from lib.utils.deps import checkDependencies
class _RecordingLogger(object):
"""Captures every (level, message) emitted while installed as deps.logger."""
def __init__(self):
self.records = []
def warning(self, msg, *args):
self.records.append(("warning", msg % args if args else msg))
def info(self, msg, *args):
self.records.append(("info", msg % args if args else msg))
def debug(self, msg, *args):
self.records.append(("debug", msg % args if args else msg))
def error(self, msg, *args):
self.records.append(("error", msg % args if args else msg))
def messages(self, level=None):
return [m for (lvl, m) in self.records if level is None or lvl == level]
class TestCheckDependencies(unittest.TestCase):
def setUp(self):
self._real_logger = deps.logger
self.rec = _RecordingLogger()
deps.logger = self.rec
def tearDown(self):
deps.logger = self._real_logger
def test_missing_driver_warns_with_library_name(self):
# '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
self.skipTest("kinterbasdb is unexpectedly installed")
except ImportError:
pass
checkDependencies()
warnings = self.rec.messages("warning")
self.assertTrue(warnings, msg="no warnings captured for a missing driver")
# the Firebird entry must name its third-party library in a warning
self.assertTrue(
any("kinterbasdb" in w for w in warnings),
msg="missing Firebird driver did not produce a library-naming warning: %r" % warnings,
)
def test_all_present_emits_all_installed_info(self):
# force every __import__ to succeed so no library is ever recorded as
# missing; the empty-missing-set branch must emit the summary info line.
import builtins
class _FakeModule(object):
__version__ = "999.0.0"
real_import = builtins.__import__
def _always_succeed(name, *args, **kwargs):
try:
return real_import(name, *args, **kwargs)
except Exception:
return _FakeModule()
builtins.__import__ = _always_succeed
try:
checkDependencies()
finally:
builtins.__import__ = real_import
infos = self.rec.messages("info")
self.assertTrue(
any("all dependencies are installed" in m for m in infos),
msg="all-present path did not emit the summary info: %r" % infos,
)
# and with nothing missing there must be no missing-library warnings
self.assertFalse(
any("third-party library" in w and "requires" in w for w in self.rec.messages("warning")),
msg="unexpected missing-library warning when all imports succeed",
)
def test_returns_none(self):
# contract: the probe is purely advisory and never returns a value
self.assertIsNone(checkDependencies())
if __name__ == "__main__":
unittest.main()
+7 -4
View File
@@ -72,13 +72,16 @@ class TestDialectClassification(unittest.TestCase):
self.assertEqual(_classify(base + (shift,)), expected, "engine %r misclassified (shift=%s)" % (engine, shift))
def test_no_false_positive_across_measured_set(self):
# non-collision property: every measured engine maps to EXACTLY its expected DBMS (or None),
# never to some other back-end. The shift flag is irrelevant for these (non-shift-sensitive)
# engines, so assert it both ways.
for engine, (base, expected) in MEASURED.items():
for shift in (False, True):
result = _classify(base + (shift,))
if expected is None:
self.assertIsNone(result, "ambiguous engine %r leaked a DBMS prior" % engine)
else:
self.assertIn(result, (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.MONETDB, DBMS.ORACLE))
self.assertEqual(result, expected, "engine %r misclassified (shift=%s): got %r, expected %r" % (engine, shift, result, expected))
# the only non-None DBMS priors the measured set can yield (sanity on the mapping itself)
produced = set(expected for _, expected in MEASURED.values() if expected is not None)
self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE})
def test_all_error_signature_yields_no_prior(self):
# an all-error signature (Oracle, ClickHouse, IRIS, or simply a WAF-blocked channel) is not
+109 -10
View File
@@ -29,6 +29,7 @@ character-based and a chunk could split a code point, need the real-DBMS run.
import binascii
import os
import re
import socket
import struct
import sys
@@ -251,16 +252,114 @@ class TestDnsExfilEngineMssql(TestDnsExfilEngine):
DBMS_NAME = "Microsoft SQL Server"
class TestDnsLabelInvariant(unittest.TestCase):
"""The exfil chunk is hex-encoded into ONE DNS label, so 2*chunk_length must never exceed the
63-octet DNS label limit - otherwise the query carries an invalid (over-long) label and exfil
silently breaks. Guards the chunk_length arithmetic in dnsUse for every supported DBMS."""
def test_hex_label_within_max_dns_label(self):
for dbms in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL):
chunk_length = MAX_DNS_LABEL // 2 if dbms in (DBMS.ORACLE, DBMS.MYSQL, DBMS.PGSQL) else MAX_DNS_LABEL // 4 - 2
self.assertGreater(chunk_length, 0, "%s: non-positive chunk_length" % dbms)
self.assertLessEqual(2 * chunk_length, MAX_DNS_LABEL,
"%s: hex label (%d) exceeds MAX_DNS_LABEL (%d)" % (dbms, 2 * chunk_length, MAX_DNS_LABEL))
class TestDnsLabelInvariant(_DnsCase):
"""The exfil chunk is hex-encoded into ONE DNS label, so the label dnsUse emits must never
exceed the 63-octet DNS label limit - otherwise the query carries an invalid (over-long) label
and exfil silently breaks.
Unlike a static formula check, this drives the REAL dnsUse() chunking through the REAL DNSServer
and asserts the invariant on the ACTUAL labels that reach the wire. The mock oracle does NOT
re-derive the chunk size: it slices each chunk to exactly the length dnsUse itself rendered into
its SUBSTRING call (captured live from agent.hexConvertField, whose input is the source's
substring expression). So if the chunk_length arithmetic in dnsUse regresses, the emitted hex
label grows past 63 octets and this test goes red - it observes the source's output, it does not
recompute it.
"""
def _drive_and_collect_labels(self, secret):
"""
Runs dnsUse for L{secret} end-to-end against the real DNS server, slicing each chunk to the
length the SOURCE asked for (parsed from the live SUBSTRING expression dnsUse builds), and
returns (every label seen in every emitted query name, list of source chunk_lengths seen).
"""
secret_bytes = secret.encode("utf-8")
boundaries = []
served = [0]
source_chunk_lengths = []
# Snapshot the names the REAL DNSServer parsed off the wire, captured the moment they land
# in _requests - dnsUse's own .pop() consumes them, so we must grab them before that.
captured_names = []
real_randomStr = self._saved_randomStr
def spy_randomStr(length=4, alphabet=None, **kw):
if alphabet == DNS_BOUNDARIES_ALPHABET and length == 3:
out = real_randomStr(length=length, alphabet=alphabet, **kw)
boundaries.append(out)
return out
return real_randomStr(length=length, alphabet=alphabet, **kw) if alphabet is not None else real_randomStr(length=length, **kw)
dnsmod.randomStr = spy_randomStr
# agent.hexConvertField receives the rendered SUBSTRING call, e.g. "MID((...),1,31)" /
# "SUBSTRING((...) FROM 1 FOR 13)"; the substring LENGTH argument (the source's real
# chunk_length) is the last integer literal in it. Capture it per iteration so the oracle
# emits a chunk of exactly that size - the source's arithmetic, not a copy of it.
saved_hexConvertField = agent.hexConvertField
def spy_hexConvertField(field):
source_chunk_lengths.append(int(re.findall(r"\d+", field)[-1]))
return saved_hexConvertField(field)
agent.hexConvertField = spy_hexConvertField
def oracle(payload=None, *args, **kwargs):
prefix, suffix = boundaries[-2], boundaries[-1]
chunk_length = source_chunk_lengths[-1]
chunk = secret_bytes[served[0]:served[0] + chunk_length]
if chunk:
host = "%s.%s.%s.%s" % (prefix, binascii.hexlify(chunk).decode(), suffix, conf.dnsDomain)
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.settimeout(3)
c.sendto(_build_query(host), ("127.0.0.1", self.server.port))
try:
c.recvfrom(512)
finally:
c.close()
served[0] += len(chunk)
for _ in range(100):
with self.server._lock:
matched = [r for r in self.server._requests if host.encode() in r]
if matched:
captured_names.extend(r.decode() if isinstance(r, bytes) else r for r in matched)
break
time.sleep(0.01)
return None
Connect.queryPage = staticmethod(oracle)
dnsmod.Request.queryPage = staticmethod(oracle)
try:
result = dnsmod.dnsUse("%s AND %d=%d", "user()")
finally:
agent.hexConvertField = saved_hexConvertField
# round-trip must still work (the source must actually reassemble what it chunked)
self.assertEqual(result, secret)
labels = []
for name in captured_names:
labels.extend(label for label in name.split(".") if label)
return labels, source_chunk_lengths
def test_emitted_dns_labels_within_max_dns_label(self):
# long enough that every supported dialect's chunk_length forces several chunks (>1 label of
# hex payload), so the chunking loop - not just a single-shot path - is what we measure
secret = ("The quick brown fox jumps over the lazy dog "
"0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz") * 3
for dbms_name in ("MySQL", "Oracle", "PostgreSQL", "Microsoft SQL Server"):
self.DBMS_NAME = dbms_name
set_dbms(dbms_name)
labels, source_chunk_lengths = self._drive_and_collect_labels(secret)
# the source must have actually chunked (multiple SUBSTRING iterations), otherwise we
# would not be testing the chunking output at all
self.assertGreater(len(source_chunk_lengths), 1,
"%s: payload did not force multiple chunks (got %d)" % (dbms_name, len(source_chunk_lengths)))
self.assertTrue(all(cl > 0 for cl in source_chunk_lengths),
"%s: non-positive chunk_length from source: %r" % (dbms_name, source_chunk_lengths))
self.assertTrue(labels, "%s: no DNS query labels were captured" % dbms_name)
for label in labels:
self.assertLessEqual(len(label), MAX_DNS_LABEL,
"%s: emitted DNS label %r is %d octets, exceeds MAX_DNS_LABEL (%d)"
% (dbms_name, label, len(label), MAX_DNS_LABEL))
class TestDnsChannelDetection(_DnsCase):
+99 -64
View File
@@ -36,18 +36,69 @@ def build_query(name, tid=b"\x12\x34", qtype=1):
class _HighPortDNSServer(DNSServer):
"""Real DNSServer logic, bound on a high port (no root, no :53 probe)"""
def __init__(self, port, sock=None, maxlen=MAX_DNS_REQUESTS):
"""Real DNSServer logic, bound on an ephemeral high port (no root, no :53 probe).
Binds to port 0 and reads the kernel-chosen port back via getsockname() (same pattern
as tests/test_dns_engine.py) so concurrent/repeated runs never collide on a hardcoded
port. The actual port is exposed as L{self.port}.
"""
def __init__(self, sock=None, maxlen=MAX_DNS_REQUESTS):
self._requests = collections.deque(maxlen=maxlen)
self._lock = threading.Lock()
if sock is None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("127.0.0.1", port))
sock.bind(("127.0.0.1", 0))
self._socket = sock
self.port = self._socket.getsockname()[1]
self._running = False
self._initialized = False
def close(self):
self._running = False
try:
self._socket.close()
except socket.error:
pass
# Maximum time (seconds) to wait for the daemon server thread to come up, or for a sent
# query to be recorded, before failing loudly instead of spinning/sleeping forever.
WAIT_TIMEOUT = 5.0
def _wait_initialized(srv, timeout=WAIT_TIMEOUT):
"""Bounded wait for the server thread to flip _initialized; fail fast if it never does."""
deadline = time.time() + timeout
while not srv._initialized:
if time.time() > deadline:
raise RuntimeError("DNS server failed to initialize within %.1fs" % timeout)
time.sleep(0.01)
def _wait_recorded(srv, token, timeout=WAIT_TIMEOUT):
"""Bounded wait until L{token} appears in a recorded request; False on timeout."""
if hasattr(token, "encode"):
token = token.encode()
deadline = time.time() + timeout
while time.time() <= deadline:
with srv._lock:
if any(token in r for r in srv._requests):
return True
time.sleep(0.01)
return False
def _wait_popped(srv, prefix, suffix, timeout=WAIT_TIMEOUT):
"""Bounded wait until pop(prefix, suffix) yields a value; returns it or None on timeout."""
deadline = time.time() + timeout
while time.time() <= deadline:
popped = srv.pop(prefix, suffix)
if popped:
return popped
time.sleep(0.01)
return None
class _SendFailOnceSocket(object):
"""Wraps a real UDP socket; first sendto() raises (simulated transient failure)"""
@@ -95,31 +146,30 @@ class TestDNSQuery(unittest.TestCase):
class TestDNSServerRoundTrip(unittest.TestCase):
PORT = 5471
@classmethod
def setUpClass(cls):
cls.srv = _HighPortDNSServer(cls.PORT)
cls.srv = _HighPortDNSServer()
cls.srv.run()
while not cls.srv._initialized:
time.sleep(0.02)
_wait_initialized(cls.srv)
@classmethod
def tearDownClass(cls):
srv = getattr(cls, "srv", None)
if srv is not None:
srv.close()
cls.srv = None
def _send(self, name):
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.settimeout(3)
c.sendto(build_query(name), ("127.0.0.1", self.PORT))
c.sendto(build_query(name), ("127.0.0.1", self.srv.port))
try:
c.recvfrom(512)
except socket.timeout:
pass
finally:
c.close()
for _ in range(100):
with self.srv._lock:
if any(name.encode() in r for r in self.srv._requests):
return True
time.sleep(0.01)
return False
return _wait_recorded(self.srv, name)
def test_roundtrip_and_pop(self):
self.assertTrue(self._send("aaa.cafe.bbb.exfil.test"))
@@ -132,49 +182,40 @@ class TestDNSServerRoundTrip(unittest.TestCase):
# labels regardless of qtype, and the server records before crafting the (A) response
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.settimeout(2)
c.sendto(build_query("ggg.beef.hhh.exfil.test", qtype=28), ("127.0.0.1", self.PORT))
c.sendto(build_query("ggg.beef.hhh.exfil.test", qtype=28), ("127.0.0.1", self.srv.port))
try:
c.recvfrom(512)
except socket.timeout:
pass
finally:
c.close()
for _ in range(200):
if self.srv.pop("ggg", "hhh"):
return
time.sleep(0.01)
self.fail("AAAA-type query was not recorded (exfil would be lost for AAAA-resolving DBMSes)")
if not _wait_popped(self.srv, "ggg", "hhh"):
self.fail("AAAA-type query was not recorded (exfil would be lost for AAAA-resolving DBMSes)")
class TestDNSServerMemoryBound(unittest.TestCase):
"""The server records every received query (it listens on :53); only matching ones are
popped. Unrelated/stray traffic and resolver retries must not grow memory without bound."""
PORT = 5475
def test_requests_are_bounded_and_recent_kept(self):
srv = _HighPortDNSServer(self.PORT, maxlen=50)
srv = _HighPortDNSServer(maxlen=50)
self.addCleanup(srv.close)
srv.run()
while not srv._initialized:
time.sleep(0.02)
_wait_initialized(srv)
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for i in range(200): # flood well past the bound
c.sendto(build_query("noise%d.unrelated.test" % i), ("127.0.0.1", self.PORT))
c.sendto(build_query("noise%d.unrelated.test" % i), ("127.0.0.1", srv.port))
c.close()
# a legit exfil query right after the flood must still be capturable
c2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); c2.settimeout(2)
c2.sendto(build_query("ppp.d00d.qqq.exfil.test"), ("127.0.0.1", self.PORT))
c2.sendto(build_query("ppp.d00d.qqq.exfil.test"), ("127.0.0.1", srv.port))
try:
c2.recvfrom(512)
except socket.timeout:
pass
finally:
c2.close()
popped = None
for _ in range(200):
popped = srv.pop("ppp", "qqq")
if popped:
break
time.sleep(0.01)
popped = _wait_popped(srv, "ppp", "qqq")
with srv._lock:
n = len(srv._requests)
self.assertLessEqual(n, 50, "request buffer exceeded its bound (%d)" % n)
@@ -182,11 +223,11 @@ class TestDNSServerMemoryBound(unittest.TestCase):
class TestDNSServerResilience(unittest.TestCase):
def _make(self, port, sock=None):
srv = _HighPortDNSServer(port, sock=sock)
def _make(self, sock=None):
srv = _HighPortDNSServer(sock=sock)
self.addCleanup(srv.close)
srv.run()
while not srv._initialized:
time.sleep(0.02)
_wait_initialized(srv)
return srv
def _query(self, port, name):
@@ -200,34 +241,28 @@ class TestDNSServerResilience(unittest.TestCase):
finally:
c.close()
def _recorded(self, srv, token, tries=120):
for _ in range(tries):
with srv._lock:
if any(token.encode() in r for r in srv._requests):
return True
time.sleep(0.01)
return False
def _recorded(self, srv, token):
return _wait_recorded(srv, token)
def test_survives_transient_send_error(self):
port = 5472
# ephemeral bind, then wrap the bound socket so its first sendto() raises
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
srv = self._make(port, sock=_SendFailOnceSocket(s))
self._query(port, "aaa.11.bbb.exfil.test") # first sendto raises
self._query(port, "ccc.22.ddd.exfil.test") # must still be served
s.bind(("127.0.0.1", 0))
srv = self._make(sock=_SendFailOnceSocket(s))
self._query(srv.port, "aaa.11.bbb.exfil.test") # first sendto raises
self._query(srv.port, "ccc.22.ddd.exfil.test") # must still be served
self.assertTrue(self._recorded(srv, "ccc.22.ddd"),
"DNS server died after one failing sendto (lost subsequent exfil)")
self.assertTrue(srv._running)
def test_survives_malformed_packets(self):
port = 5473
srv = self._make(port)
srv = self._make()
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for junk in (b"", b"\x00", b"\xff" * 7, b"\x12\x34\x01\x00\x00\x01" + b"\x20abc"):
c.sendto(junk, ("127.0.0.1", port))
c.sendto(junk, ("127.0.0.1", srv.port))
c.close()
self._query(port, "ok.33.fine.exfil.test")
self._query(srv.port, "ok.33.fine.exfil.test")
self.assertTrue(self._recorded(srv, "ok.33.fine"),
"DNS server died on a malformed packet")
@@ -235,14 +270,19 @@ class TestDNSServerResilience(unittest.TestCase):
class TestDNSServerConcurrency(unittest.TestCase):
"""Under --threads, many workers fire DNS queries and call pop() while the server thread
appends - all guarded by one lock. Each worker must get back exactly its own data."""
PORT = 5474
@classmethod
def setUpClass(cls):
cls.srv = _HighPortDNSServer(cls.PORT)
cls.srv = _HighPortDNSServer()
cls.srv.run()
while not cls.srv._initialized:
time.sleep(0.02)
_wait_initialized(cls.srv)
@classmethod
def tearDownClass(cls):
srv = getattr(cls, "srv", None)
if srv is not None:
srv.close()
cls.srv = None
def test_concurrent_send_and_pop_no_crosstalk(self):
import binascii, re
@@ -258,19 +298,14 @@ class TestDNSServerConcurrency(unittest.TestCase):
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.settimeout(2)
try:
c.sendto(build_query(host), ("127.0.0.1", self.PORT))
c.sendto(build_query(host), ("127.0.0.1", self.srv.port))
try:
c.recvfrom(512)
except socket.timeout:
pass
finally:
c.close()
got = None
for _ in range(200):
got = self.srv.pop(prefix, suffix)
if got:
break
time.sleep(0.01)
got = _wait_popped(self.srv, prefix, suffix)
if not got:
errors.append("worker %d: never popped its query" % i); return
m = re.search(r"%s\.(?P<r>.+?)\.%s" % (prefix, suffix), got, re.I)
+410
View File
@@ -0,0 +1,410 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Output formatting of the result dumper (lib/core/dump.py) and the SQLite
replication backend (lib/core/replication.py).
dump.Dump turns extracted DB structures (schemas, table/column listings, row
counts, single facts, user lists) into the human-readable ASCII tables printed
to the console, and serializes per-table row data to CSV / HTML / SQLite files.
None of that needs a live target, network or DBMS: the console renderers route
every line through Dump._write (overridden here to capture instead of print),
and the file renderers just write to a path we point at a temp dir. These tests
pin the rendered layout/escaping contracts so a formatting regression is caught
without an end-to-end scan.
"""
import io
import os
import shutil
import sys
import tempfile
import unittest
from collections import OrderedDict as _PlainOrderedDict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.common import Backend
from lib.core.data import conf, kb
from lib.core.dump import Dump
from lib.core.enums import DUMP_FORMAT
from lib.core.replication import Replication
# --- console-rendering tests (no files): capture every Dump._write line --------------------------
class _CaptureCase(unittest.TestCase):
"""Base for the console renderers: pins a neutral case-preserving DBMS, disables api/report
side channels, and replaces Dump._write with an in-memory capture so nothing hits stdout."""
_CONF_KEYS = ("api", "reportCollector", "dumpFormat", "col", "csvDel", "dumpPath", "dumpFile",
"limitStart", "limitStop", "forceDbms", "dbms")
_KB_KEYS = ("forcedDbms", "dbms")
def setUp(self):
self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS)
self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS)
conf.forceDbms = conf.dbms = None
kb.dbms = None
Backend.forceDbms("MySQL")
conf.api = False
conf.reportCollector = None
conf.col = None
conf.csvDel = ","
self.lines = []
self.d = Dump()
self.d._write = self._capture
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
for k, v in self._savedKb.items():
kb[k] = v
def _capture(self, data, newline=True, console=True, content_type=None):
# mirror Dump._write's own line-vs-space join so multi-call lines reassemble faithfully
self.lines.append("%s%s" % (data, "\n" if newline else " "))
def text(self):
return "".join(self.lines)
class TestStringAndLister(_CaptureCase):
def test_string_scalar_quoted(self):
# a plain string fact is rendered as "header: 'value'"
self.d.string("current user", "root@localhost")
self.assertIn("current user: 'root@localhost'", self.text())
def test_string_multiline_block(self):
# a value containing a newline switches to the fenced ---\n...\n--- block form
self.d.string("banner", "line1\nline2")
out = self.text()
self.assertIn("banner:\n---\nline1\nline2\n---", out)
def test_string_singleton_list_unwrapped(self):
# a one-element list is unwrapped to the scalar form (not the lister "[N]:" form)
self.d.string("current database", ["testdb"])
out = self.text()
self.assertIn("current database: 'testdb'", out)
self.assertNotIn("[1]", out)
def test_lister_sorts_and_counts(self):
# lister prints a "[count]:" header, one "[*] item" per element, sorted case-insensitively
self.d.lister("available databases", ["mysql", "Alpha", "zebra"])
out = self.text()
self.assertIn("available databases [3]:", out)
body = out[out.index("[3]:"):]
# case-insensitive ascending: Alpha, mysql, zebra
self.assertLess(body.index("[*] Alpha"), body.index("[*] mysql"))
self.assertLess(body.index("[*] mysql"), body.index("[*] zebra"))
def test_lister_dedupes(self):
# the sort path also de-duplicates (set()) before listing
self.d.lister("database management system users", ["root", "root", "guest"])
out = self.text()
self.assertIn("database management system users [2]:", out)
self.assertEqual(out.count("[*] root"), 1)
def test_lister_unsorted_preserves_order(self):
# sort=False (e.g. rFile) keeps insertion order
self.d.lister("files saved to", ["/z", "/a", "/m"], sort=False)
out = self.text()
self.assertLess(out.index("[*] /z"), out.index("[*] /a"))
self.assertLess(out.index("[*] /a"), out.index("[*] /m"))
class TestCurrentDb(_CaptureCase):
def test_label_default_dbms(self):
# MySQL is not in the schema/owner special-cased lists -> plain "current database"
self.d.currentDb("testdb")
self.assertIn("current database: 'testdb'", self.text())
def test_label_schema_dbms(self):
# Oracle is in the schema-equivalent list -> the label is annotated accordingly
Backend.forceDbms("Oracle")
self.d.currentDb("SYSTEM")
out = self.text()
self.assertIn("equivalent to schema on Oracle", out)
self.assertIn("SYSTEM", out)
class TestDbTables(_CaptureCase):
def test_table_listing_box(self):
self.d.dbTables({"testdb": ["users", "logs"]})
out = self.text()
self.assertIn("Database: testdb", out)
self.assertIn("[2 tables]", out)
self.assertIn("| users", out)
self.assertIn("| logs", out)
# box borders present
self.assertIn("+", out)
def test_single_table_singular(self):
self.d.dbTables({"testdb": ["only"]})
self.assertIn("[1 table]", self.text())
def test_no_tables(self):
self.d.dbTables({})
self.assertIn("No tables found", self.text())
def test_box_width_matches_longest_table(self):
# the border length tracks the longest table name (+2 padding)
self.d.dbTables({"testdb": ["a", "elephant"]})
out = self.text()
# "elephant" is 8 chars -> a border line of 8+2 = 10 dashes exists
self.assertIn("+%s+" % ("-" * 10), out)
class TestDbTableColumns(_CaptureCase):
def test_typed_columns_two_column_box(self):
self.d.dbTableColumns({"testdb": {"users": {"id": "int", "name": "varchar(50)"}}})
out = self.text()
self.assertIn("Database: testdb", out)
self.assertIn("Table: users", out)
self.assertIn("[2 columns]", out)
self.assertIn("| Column", out)
self.assertIn("| Type", out)
self.assertIn("int", out)
self.assertIn("varchar(50)", out)
def test_typeless_columns_single_box(self):
# when no column carries a type, only the Column box is rendered (no Type header)
self.d.dbTableColumns({"testdb": {"users": {"id": None, "name": None}}})
out = self.text()
self.assertIn("| Column", out)
self.assertNotIn("| Type", out)
def test_mixed_types_still_show_type_header(self):
# even if the alphabetically-last column is type-less, a Type column must appear
self.d.dbTableColumns({"testdb": {"t": {"aaa": "int", "zzz": None}}})
self.assertIn("| Type", self.text())
class TestDbTablesCount(_CaptureCase):
def test_count_box_sorted_desc(self):
self.d.dbTablesCount({"testdb": {5: ["small"], 100: ["big"]}})
out = self.text()
self.assertIn("Database: testdb", out)
self.assertIn("| Table", out)
self.assertIn("| Entries", out)
# higher count first (reverse sort)
self.assertLess(out.index("big"), out.index("small"))
self.assertIn("100", out)
class TestUserSettings(_CaptureCase):
def test_privileges_listed_with_admin_flag(self):
# userSettings accepts (settingsDict, adminsSet); admins get an "(administrator)" tag
settings = ({"root": ["ALL"], "guest": ["SELECT"]}, set(["root"]))
self.d.userSettings("database management system users privileges", settings, "privilege")
out = self.text()
self.assertIn("[*] root (administrator)", out)
self.assertIn("[*] guest", out)
self.assertNotIn("guest (administrator)", out)
self.assertIn("privilege: ALL", out)
self.assertIn("privilege: SELECT", out)
# --- file-rendering tests (CSV / HTML / SQLite): point output at a temp dir ----------------------
class _FileDumpCase(unittest.TestCase):
_CONF_KEYS = ("dumpFormat", "dumpPath", "dumpFile", "col", "api", "reportCollector",
"limitStart", "limitStop", "csvDel", "forceDbms", "dbms")
_KB_KEYS = ("forcedDbms", "dbms")
def setUp(self):
self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS)
self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS)
conf.forceDbms = conf.dbms = None
kb.dbms = None
Backend.forceDbms("MySQL")
self.tmp = tempfile.mkdtemp(prefix="sqlmap-dumpfmt-test")
conf.dumpPath = self.tmp
conf.dumpFile = None
conf.col = None
conf.api = False
conf.reportCollector = None
conf.limitStart = conf.limitStop = None
conf.csvDel = ","
self.d = Dump()
self.d._write = lambda *a, **k: None # silence the console table
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
for k, v in self._savedKb.items():
kb[k] = v
shutil.rmtree(self.tmp, ignore_errors=True)
def _path(self, table_values, ext):
db = table_values["__infos__"]["db"] or "All"
return os.path.join(self.tmp, db, "%s.%s" % (table_values["__infos__"]["table"], ext))
def _dump(self, table_values, fmt, ext):
conf.dumpFormat = fmt
self.d.dbTableValues(table_values)
with io.open(self._path(table_values, ext), encoding="utf-8") as f:
return f.read()
class TestCsvDump(_FileDumpCase):
def _sample(self):
return _PlainOrderedDict([
("__infos__", {"count": 2, "db": "testdb", "table": "users"}),
("id", {"length": 2, "values": ["1", "2"]}),
("name", {"length": 6, "values": ["luther", "fluffy"]}),
])
def test_header_and_rows(self):
content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv")
lines = [l for l in content.splitlines() if l.strip()]
self.assertEqual(lines[0].split(","), ["id", "name"])
self.assertEqual(lines[1].split(","), ["1", "luther"])
self.assertEqual(lines[2].split(","), ["2", "fluffy"])
def test_delimiter_in_value_is_quoted(self):
# RFC-4180: a value containing the delimiter must be wrapped in quotes
tv = _PlainOrderedDict([
("__infos__", {"count": 1, "db": "testdb", "table": "t"}),
("a", {"length": 8, "values": ["x,y"]}),
("b", {"length": 1, "values": ["z"]}),
])
content = self._dump(tv, DUMP_FORMAT.CSV, "csv")
self.assertIn('"x,y"', content)
def test_null_and_blank_markers(self):
# the display replacements apply to CSV too: DB NULL (" ") -> NULL, empty ("") -> <blank>
tv = _PlainOrderedDict([
("__infos__", {"count": 1, "db": "testdb", "table": "t"}),
("a", {"length": 4, "values": [" "]}),
("b", {"length": 7, "values": [""]}),
("c", {"length": 1, "values": ["x"]}),
])
content = self._dump(tv, DUMP_FORMAT.CSV, "csv")
row = [l for l in content.splitlines() if l.strip()][1]
self.assertEqual(row.split(","), ["NULL", "<blank>", "x"])
def test_custom_delimiter(self):
conf.csvDel = ";"
content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv")
self.assertEqual(content.splitlines()[0].split(";"), ["id", "name"])
class TestHtmlDump(_FileDumpCase):
def _sample(self):
return _PlainOrderedDict([
("__infos__", {"count": 1, "db": "testdb", "table": "users"}),
("id", {"length": 2, "values": ["1"]}),
("name", {"length": 6, "values": ["luther"]}),
])
def test_html_scaffold_and_cells(self):
content = self._dump(self._sample(), DUMP_FORMAT.HTML, "html")
self.assertIn("<!DOCTYPE html>", content)
self.assertIn("<title>testdb.users</title>", content)
self.assertIn("<th", content)
self.assertIn(">id</th>", content)
self.assertIn(">name</th>", content)
self.assertIn("<td>1</td>", content)
self.assertIn("<td>luther</td>", content)
self.assertIn("</table>", content)
self.assertIn("</html>", content)
def test_html_escapes_markup(self):
# a value with HTML metacharacters must be escaped, not emitted raw
tv = _PlainOrderedDict([
("__infos__", {"count": 1, "db": "testdb", "table": "t"}),
("payload", {"length": 16, "values": ["<script>x</script>"]}),
])
content = self._dump(tv, DUMP_FORMAT.HTML, "html")
self.assertNotIn("<td><script>x</script></td>", content)
self.assertIn("&lt;", content)
class TestSqliteDump(_FileDumpCase):
def test_rows_and_inferred_types(self):
tv = _PlainOrderedDict([
("__infos__", {"count": 2, "db": "testdb", "table": "people"}),
("id", {"length": 2, "values": ["1", "2"]}), # all ints -> INTEGER
("ratio", {"length": 4, "values": ["1.5", "2.0"]}), # floats -> REAL
("name", {"length": 6, "values": ["alice", " "]}), # text with a NULL marker
])
conf.dumpFormat = DUMP_FORMAT.SQLITE
self.d.dbTableValues(tv)
import sqlite3
dbfile = os.path.join(self.tmp, "testdb.sqlite3")
self.assertTrue(os.path.exists(dbfile))
conn = sqlite3.connect(dbfile)
try:
cur = conn.cursor()
cur.execute("SELECT id, ratio, name FROM people ORDER BY id")
rows = cur.fetchall()
self.assertEqual(rows[0], (1, 1.5, "alice"))
# the DB NULL marker (" ") was stored as a real NULL, not the "NULL" text
self.assertEqual(rows[1], (2, 2.0, None))
# column affinities inferred from the values
cur.execute("PRAGMA table_info(people)")
types = {name: ctype for (_cid, name, ctype, _nn, _dv, _pk) in cur.fetchall()}
self.assertEqual(types["id"], "INTEGER")
self.assertEqual(types["ratio"], "REAL")
self.assertEqual(types["name"], "TEXT")
finally:
conn.close()
# --- replication backend tests (pure sqlite3, no network/DBMS) -----------------------------------
class TestReplication(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="sqlmap-repl-test")
self.path = os.path.join(self.tmp, "out.sqlite3")
self.repl = Replication(self.path)
def tearDown(self):
try:
self.repl.connection.close()
except Exception:
pass
shutil.rmtree(self.tmp, ignore_errors=True)
def test_create_insert_select_roundtrip(self):
t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)])
t.beginTransaction()
t.insert(["1", "alice"])
t.insert(["2", "bob"])
t.endTransaction()
rows = sorted(t.select())
self.assertEqual(rows, [(1, "alice"), (2, "bob")])
def test_select_with_condition(self):
t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)])
t.insert(["1", "alice"])
t.insert(["2", "bob"])
self.assertEqual(t.select("name = 'bob'"), [(2, "bob")])
def test_insert_wrong_arity_raises(self):
from lib.core.exception import SqlmapValueException
t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)])
with self.assertRaises(SqlmapValueException):
t.insert(["only-one-value"])
def test_typeless_table(self):
t = self.repl.createTable("t", ["a", "b"], typeless=True)
t.insert(["x", "y"])
self.assertEqual(t.select(), [("x", "y")])
def test_datatype_str(self):
self.assertEqual(str(Replication.TEXT), "TEXT")
self.assertEqual(str(Replication.INTEGER), "INTEGER")
self.assertIn("DataType", repr(Replication.REAL))
if __name__ == "__main__":
unittest.main(verbosity=2)
+736
View File
@@ -0,0 +1,736 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit coverage for the file-read/file-write/UDF-injection SQL & command builders:
- plugins/generic/filesystem.py (encoding, INSERT/UPDATE query forging,
length probe, read/write dispatch)
- plugins/dbms/mssqlserver/filesystem.py
(debug.exe SCR script, BULK INSERT /
bin->hex extraction, PowerShell &
certutil base64 upload commands)
- lib/takeover/udf.py (sys_exec/sys_eval calls, CREATE FUNCTION
SQL for MySQL/PostgreSQL, remote-path
selection, UDF pruning)
These methods are (near-)pure string builders given conf/kb plus the injection
layer. Each test drives the real method with inject.goStacked / inject.getValue
(and, for MSSQL, xpCmdshellWriteFile/execCmd) captured, and asserts the EXACT
SQL / command / encoded payload produced -- so a regression in the assembly
logic fails the test. No live target / network / DBMS involved.
"""
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.convert import encodeHex, encodeBase64, getText
from lib.core.enums import PAYLOAD
# --------------------------------------------------------------------------- #
# shared base: snapshot/restore every global + monkeypatch these tests touch #
# --------------------------------------------------------------------------- #
class _FsBase(unittest.TestCase):
# subclasses set `target_modules` = list of modules whose inject.* we patch
target_modules = ()
# conf fields read by the methods under test
_CONF_KEYS = ("batch", "direct", "fileRead", "fileWrite", "filePath",
"commonFiles", "osPwn", "osCmd", "osShell", "regRead",
"regAdd", "regDel", "tmpPath", "shLib", "encoding")
_KB_KEYS = ("bruteMode", "binaryField", "fileReadMode")
def setUp(self):
self._conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._kb = {k: kb.get(k) for k in self._KB_KEYS}
self._patched = [] # (obj, attr, original)
conf.batch = True
conf.direct = True
kb.bruteMode = False
def tearDown(self):
for obj, attr, orig in reversed(self._patched):
setattr(obj, attr, orig)
for k, v in self._conf.items():
conf[k] = v
for k, v in self._kb.items():
kb[k] = v
def patch(self, obj, attr, value):
self._patched.append((obj, attr, getattr(obj, attr)))
setattr(obj, attr, value)
return value
# --------------------------------------------------------------------------- #
# plugins/generic/filesystem.py #
# --------------------------------------------------------------------------- #
class TestGenericFilesystem(_FsBase):
import plugins.generic.filesystem as module
def _fs(self):
return self.module.Filesystem()
# -- fileContentEncode ------------------------------------------------- #
def test_fileContentEncode_hex_single(self):
# single=True -> one element, 0x-prefixed, exact lower-case hex of bytes
out = self._fs().fileContentEncode(b"ABC", "hex", True)
self.assertEqual(out, ["0x414243"])
def test_fileContentEncode_base64_single(self):
out = self._fs().fileContentEncode(b"ABC", "base64", True)
self.assertEqual(out, ["'QUJD'"])
def test_fileContentEncode_hex_chunked(self):
# 4 bytes -> 8 hex chars; chunkSize=4 -> two 0x-prefixed chunks of 4 chars
out = self._fs().fileContentEncode(b"ABCD", "hex", False, chunkSize=4)
self.assertEqual(out, ["0x4142", "0x4344"])
def test_fileContentEncode_base64_chunked(self):
# "ABCD" -> base64 "QUJDRA==" (8 chars); chunkSize=4 -> two quoted chunks
out = self._fs().fileContentEncode(b"ABCD", "base64", False, chunkSize=4)
self.assertEqual(out, ["'QUJD'", "'RA=='"])
def test_fileContentEncode_chunk_below_threshold_is_single(self):
# content shorter than chunkSize, single=False -> still one 0x chunk
out = self._fs().fileContentEncode(b"AB", "hex", False, chunkSize=256)
self.assertEqual(out, ["0x4142"])
def test_fileEncode_reads_then_encodes(self):
# fileEncode must read the file bytes and delegate to fileContentEncode
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_fe_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"hello")
try:
out = self._fs().fileEncode(path, "hex", True)
finally:
os.remove(path)
self.assertEqual(out, ["0x%s" % getText(encodeHex(b"hello"))])
self.assertEqual(out, ["0x68656c6c6f"])
# -- fileToSqlQueries -------------------------------------------------- #
def test_fileToSqlQueries_insert_then_concat_update(self):
# first chunk -> INSERT; subsequent -> UPDATE using the DBMS concatenate
# template (MySQL: CONCAT(field, chunk)).
set_dbms("MySQL")
fs = self._fs()
queries = fs.fileToSqlQueries(["0x4142", "0x4344", "0x4546"])
tbl, fld = fs.fileTblName, fs.tblField
self.assertEqual(queries[0],
"INSERT INTO %s(%s) VALUES (0x4142)" % (tbl, fld))
self.assertEqual(queries[1],
"UPDATE %s SET %s=CONCAT(%s,0x4344)" % (tbl, fld, fld))
self.assertEqual(queries[2],
"UPDATE %s SET %s=CONCAT(%s,0x4546)" % (tbl, fld, fld))
# -- _checkFileLength -------------------------------------------------- #
def test_checkFileLength_mysql_query_and_samefile(self):
# MySQL builds LENGTH(LOAD_FILE('<remote>')) and compares to local size.
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"12345") # 5 bytes
captured = {}
def getValue(query, *a, **k):
captured["query"] = query
return "5"
self.patch(self.module.inject, "getValue", getValue)
try:
same = self._fs()._checkFileLength(path, "/etc/passwd")
finally:
os.remove(path)
self.assertEqual(captured["query"],
"LENGTH(LOAD_FILE('/etc/passwd'))")
self.assertIs(same, True)
def test_checkFileLength_size_differs(self):
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl2_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"12345") # local 5
self.patch(self.module.inject, "getValue", lambda q, *a, **k: "9")
try:
same = self._fs()._checkFileLength(path, "/etc/passwd")
finally:
os.remove(path)
# remote 9 != local 5 -> not the same file
self.assertIs(same, False)
def test_checkFileLength_mssql_openrowset_stacked(self):
# MSSQL path issues an OPENROWSET BULK INSERT then DATALENGTH probe.
# createSupportTbl lives in the misc mixin; stub it on a subclass so the
# OPENROWSET-building branch runs in isolation.
set_dbms("Microsoft SQL Server")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl3_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"ABCD") # 4 bytes
stacked = []
class FS(self.module.Filesystem):
def createSupportTbl(self, *a, **k):
pass
self.patch(self.module.inject, "goStacked",
lambda q, *a, **k: stacked.append(q))
self.patch(self.module.inject, "getValue", lambda q, *a, **k: "4")
fs = FS()
try:
same = fs._checkFileLength(path, "C:\\boot.ini")
finally:
os.remove(path)
tbl, fld = fs.fileTblName, fs.tblField
# createSupportTbl DROP+CREATE, then the OPENROWSET insert
insert = ("INSERT INTO %s(%s) SELECT %s FROM OPENROWSET(BULK "
"'C:\\boot.ini', SINGLE_BLOB) AS %s(%s)"
% (tbl, fld, fld, tbl, fld))
self.assertIn(insert, stacked)
self.assertIs(same, True)
def test_checkFileLength_not_written_warns_false(self):
# non-positive remote size -> treated as "not written" -> sameFile False
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl4_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"x")
self.patch(self.module.inject, "getValue", lambda q, *a, **k: None)
try:
same = self._fs()._checkFileLength(path, "/etc/passwd")
finally:
os.remove(path)
self.assertIs(same, False)
# -- readFile ---------------------------------------------------------- #
def test_readFile_decodes_hex_and_writes(self):
# Drive the generic readFile orchestration with a stubbed stackedReadFile
# returning canned hex; assert the bytes handed to dataToOutFile are the
# decoded content (raw bytes), and the remote name is passed through.
set_dbms("MySQL")
written = {}
class FS(self.module.Filesystem):
def checkDbmsOs(self):
pass
def cleanup(self, *a, **k):
pass
def stackedReadFile(self, remoteFile):
return encodeHex(b"secret-data", binary=False)
def askCheckReadFile(self, localFile, remoteFile):
return None
def grab(name, data):
written["d"] = (name, data)
return "/out/path"
self.patch(self.module, "dataToOutFile", grab)
out = FS().readFile("/etc/shadow")
self.assertEqual(written["d"][0], "/etc/shadow")
self.assertEqual(written["d"][1], b"secret-data")
self.assertEqual(out, ["/out/path"])
def test_readFile_listlike_chunks_joined(self):
# list-of-chunks return value gets flattened before hex-decoding
set_dbms("MySQL")
written = {}
class FS(self.module.Filesystem):
def checkDbmsOs(self):
pass
def cleanup(self, *a, **k):
pass
def stackedReadFile(self, remoteFile):
# two chunks (each a 1-element list, as inject.getValue returns)
return [[encodeHex(b"AB", binary=False)],
[encodeHex(b"CD", binary=False)]]
def askCheckReadFile(self, localFile, remoteFile):
return True
def grab(name, data):
written["d"] = data
return "/out"
self.patch(self.module, "dataToOutFile", grab)
out = FS().readFile("/f")
self.assertEqual(written["d"], b"ABCD")
# askCheckReadFile True -> suffix annotation
self.assertEqual(out, ["/out (same file)"])
# -- writeFile dispatch ------------------------------------------------ #
def test_writeFile_dispatches_to_stacked(self):
# With stacking available (conf.direct True), writeFile must route to
# stackedWriteFile and return its result.
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_wf_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"data")
calls = {}
class FS(self.module.Filesystem):
def checkDbmsOs(self):
pass
def cleanup(self, *a, **k):
calls["cleanup"] = True
def stackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False):
calls["args"] = (localFile, remoteFile, fileType, forceCheck)
return True
try:
res = FS().writeFile(path, "/var/www/x", "text", forceCheck=True)
finally:
os.remove(path)
self.assertIs(res, True)
self.assertEqual(calls["args"], (path, "/var/www/x", "text", True))
self.assertTrue(calls["cleanup"])
# --------------------------------------------------------------------------- #
# plugins/dbms/mssqlserver/filesystem.py #
# --------------------------------------------------------------------------- #
class TestMSSQLFilesystem(_FsBase):
import plugins.dbms.mssqlserver.filesystem as module
def _handler(self):
from plugins.dbms.mssqlserver import MSSQLServerMap
set_dbms("Microsoft SQL Server")
return MSSQLServerMap()
# -- _dataToScr (debug.exe script) ------------------------------------- #
def test_dataToScr_header_and_hex_bytes(self):
fs = self._handler()
lines = fs._dataToScr(b"AB", "chunk1")
# header: name / rcx / size(hex) / fill
self.assertEqual(lines[0], "n chunk1")
self.assertEqual(lines[1], "rcx")
self.assertEqual(lines[2], "%x" % 2) # size = 2 bytes
self.assertEqual(lines[3], "f 0100 %x 00" % 2)
# the data 'e' line: base addr 0x100, hex of 'A'(41) and 'B'(42)
self.assertEqual(lines[4], "e 100 41 42")
self.assertEqual(lines[-2], "w")
self.assertEqual(lines[-1], "q")
def test_dataToScr_wraps_lines_and_advances_address(self):
# lineLen=20, so 21 bytes -> two 'e' lines; second starts at 0x100+20=0x114
fs = self._handler()
content = bytes(bytearray(range(21))) # 21 bytes 0x00..0x14
lines = fs._dataToScr(content, "c")
eLines = [ln for ln in lines if ln.startswith("e ")]
self.assertEqual(len(eLines), 2)
self.assertTrue(eLines[0].startswith("e 100 00 01 02"))
# 20 bytes consumed -> next address 0x100+0x14 = 0x114
self.assertTrue(eLines[1].startswith("e 114 14"))
# -- stackedReadFile (BULK INSERT + bin->hex extraction) --------------- #
def test_stackedReadFile_builds_bulk_insert_and_decodes(self):
fs = self._handler()
stacked = []
self.patch(self.module.inject, "goStacked",
lambda q, *a, **k: stacked.append(q))
# UNION available -> single getValue returns the hex content directly
def getValue(query, *a, **k):
return encodeHex(b"file-bytes", binary=False)
self.patch(self.module.inject, "getValue", getValue)
self.patch(self.module, "isTechniqueAvailable", lambda *a, **k: True)
result = fs.stackedReadFile("C:\\secret.txt")
# the BULK INSERT statement loading the file into the support table
bulk = [q for q in stacked if q.startswith("BULK INSERT ")]
self.assertEqual(len(bulk), 1)
self.assertIn("FROM 'C:\\secret.txt'", bulk[0])
self.assertIn("CODEPAGE='RAW'", bulk[0])
# the bin->hex conversion routine must reference the 0..F charset
binhex = [q for q in stacked if "0123456789ABCDEF" in q]
self.assertEqual(len(binhex), 1)
self.assertIn("DATALENGTH", binhex[0])
# result is the raw hex string returned by getValue
self.assertEqual(result, encodeHex(b"file-bytes", binary=False))
def test_stackedReadFile_chunked_when_no_union(self):
# No UNION technique -> COUNT(*) then per-row TOP-1 retrieval into a list
fs = self._handler()
self.patch(self.module.inject, "goStacked", lambda q, *a, **k: None)
self.patch(self.module, "isTechniqueAvailable", lambda *a, **k: False)
chunks = ["41", "42"]
def getValue(query, *a, **k):
if query.startswith("SELECT COUNT(*)"):
return "2"
# the per-index extraction query
if "NOT IN (SELECT TOP" in query:
return chunks.pop(0)
return None
self.patch(self.module.inject, "getValue", getValue)
result = fs.stackedReadFile("C:\\x")
self.assertEqual(result, ["41", "42"])
# -- unionWriteFile is explicitly unsupported -------------------------- #
def test_unionWriteFile_unsupported(self):
from lib.core.exception import SqlmapUnsupportedFeatureException
fs = self._handler()
self.assertRaises(SqlmapUnsupportedFeatureException,
fs.unionWriteFile, "a", "b", "binary")
# -- _stackedWriteFilePS (PowerShell base64) --------------------------- #
def test_stackedWriteFilePS_uploads_base64_and_builds_ps(self):
fs = self._handler()
writes = []
cmds = []
self.patch(fs, "xpCmdshellWriteFile",
lambda content, path, name: writes.append((content, name)))
self.patch(fs, "execCmd", lambda cmd: cmds.append(cmd))
fs._stackedWriteFilePS("C:\\Windows\\Temp", b"payload",
"C:\\out.exe", "binary")
expected_b64 = encodeBase64(b"payload", binary=False)
# the base64 payload goes to the .txt file; the .ps1 holds the decoder.
uploaded = "".join(c for c, name in writes if name.endswith(".txt"))
self.assertEqual(uploaded, expected_b64)
# the powershell command line: ByPass + reference to the .ps1 script
self.assertEqual(len(cmds), 1)
self.assertIn("powershell -ExecutionPolicy ByPass -File", cmds[0])
def test_stackedWriteFilePS_script_decodes_to_remote(self):
# Assert the PS script body contains the FromBase64String + Set-Content
# targeting the exact remote file path.
fs = self._handler()
script = {}
def grab(content, path, name):
if name.endswith(".ps1"):
script["body"] = content
self.patch(fs, "xpCmdshellWriteFile", grab)
self.patch(fs, "execCmd", lambda cmd: None)
fs._stackedWriteFilePS("C:\\T", b"abc", "C:\\target.dll", "binary")
self.assertIn("[System.Convert]::FromBase64String($Base64)", script["body"])
self.assertIn('Set-Content -Path "C:\\target.dll"', script["body"])
# -- _stackedWriteFileCertutilExe (certutil base64) -------------------- #
def test_stackedWriteFileCertutil_splits_b64_and_decodes(self):
fs = self._handler()
writes = []
cmds = []
self.patch(fs, "xpCmdshellWriteFile",
lambda content, path, name: writes.append(content))
self.patch(fs, "execCmd", lambda cmd: cmds.append(cmd))
# >500 chars of base64 so the splitter actually wraps lines
content = b"Z" * 600
fs._stackedWriteFileCertutilExe("C:\\T", "local", content,
"C:\\out.bin", "binary")
b64 = encodeBase64(content, binary=False)
# uploaded text == base64 rejoined on newline at 500-char boundaries
uploaded = writes[0]
self.assertEqual(uploaded.replace("\n", ""), b64)
self.assertEqual(uploaded.split("\n")[0], b64[:500])
# certutil -decode command targeting the remote file
self.assertEqual(len(cmds), 1)
self.assertIn("certutil -f -decode", cmds[0])
self.assertIn("C:\\out.bin", cmds[0])
# --------------------------------------------------------------------------- #
# lib/takeover/udf.py (+ MySQL/PostgreSQL CREATE FUNCTION overrides) #
# --------------------------------------------------------------------------- #
class TestUDF(_FsBase):
import lib.takeover.udf as module
def _udf(self):
u = self.module.UDF()
u.cmdTblName = "cmdtbl"
u.tblField = "data"
return u
# -- udfForgeCmd ------------------------------------------------------- #
def test_udfForgeCmd_wraps_quotes(self):
u = self._udf()
self.assertEqual(u.udfForgeCmd("whoami"), "'whoami'")
# already partially quoted -> not doubled
self.assertEqual(u.udfForgeCmd("'whoami"), "'whoami'")
self.assertEqual(u.udfForgeCmd("whoami'"), "'whoami'")
def _escaped(self, u, cmd):
# mirror udfExecCmd's argument preparation: forge then escape via the
# active DBMS unescaper. (The escaper may hex-encode the literal; we want
# to assert the SELECT wrapping/udf-name wiring, not re-test escaping.)
return self.module.unescaper.escape(u.udfForgeCmd(cmd))
# -- udfExecCmd -------------------------------------------------------- #
def test_udfExecCmd_builds_select_call(self):
set_dbms("MySQL")
u = self._udf()
captured = {}
self.patch(self.module.inject, "goStacked",
lambda q, silent=False: captured.setdefault("q", q))
u.udfExecCmd("id")
# default udfName is sys_exec; arg is the forged+escaped command
self.assertEqual(captured["q"],
"SELECT sys_exec(%s)" % self._escaped(u, "id"))
def test_udfExecCmd_custom_udf_name(self):
set_dbms("MySQL")
u = self._udf()
captured = {}
self.patch(self.module.inject, "goStacked",
lambda q, silent=False: captured.setdefault("q", q))
u.udfExecCmd("id", udfName="my_fn")
self.assertEqual(captured["q"],
"SELECT my_fn(%s)" % self._escaped(u, "id"))
# -- udfEvalCmd -------------------------------------------------------- #
def test_udfEvalCmd_direct_joins_lines(self):
# conf.direct -> uses udfExecCmd output, converting \r to \n
set_dbms("MySQL")
conf.direct = True
u = self._udf()
self.patch(self.module.inject, "goStacked",
lambda q, silent=False: ["foo\rbar", "baz"])
out = u.udfEvalCmd("id")
self.assertEqual(out, "foo\nbarbaz")
def test_udfEvalCmd_stacked_insert_select_delete(self):
# non-direct -> INSERT via UDF, SELECT back, then DELETE
set_dbms("MySQL")
conf.direct = False
u = self._udf()
stacked = []
self.patch(self.module.inject, "goStacked",
lambda q, *a, **k: stacked.append(q))
self.patch(self.module.inject, "getValue",
lambda q, *a, **k: "RESULT")
out = u.udfEvalCmd("id", udfName="sys_eval")
self.assertEqual(
stacked[0],
"INSERT INTO cmdtbl(data) VALUES (sys_eval(%s))"
% self._escaped(u, "id"))
self.assertEqual(stacked[1], "DELETE FROM cmdtbl")
self.assertEqual(out, "RESULT")
# -- udfCheckNeeded (pruning of the sys UDF set) ----------------------- #
def test_udfCheckNeeded_prunes_unrequested_udfs(self):
set_dbms("MySQL")
u = self._udf()
u.sysUdfs = {
"sys_fileread": {}, "sys_bineval": {},
"sys_eval": {}, "sys_exec": {},
}
# nothing requested -> everything irrelevant gets popped
conf.fileRead = conf.commonFiles = None
conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False
conf.regAdd = conf.regDel = False
u.udfCheckNeeded()
self.assertEqual(u.sysUdfs, {})
def test_udfCheckNeeded_keeps_exec_for_oscmd(self):
set_dbms("MySQL")
u = self._udf()
u.sysUdfs = {
"sys_fileread": {}, "sys_bineval": {},
"sys_eval": {}, "sys_exec": {},
}
conf.fileRead = conf.commonFiles = None
conf.osPwn = False
conf.osCmd = True # requests command exec
conf.osShell = conf.regRead = conf.regAdd = conf.regDel = False
u.udfCheckNeeded()
# sys_eval & sys_exec retained; fileread/bineval pruned
self.assertIn("sys_eval", u.sysUdfs)
self.assertIn("sys_exec", u.sysUdfs)
self.assertNotIn("sys_fileread", u.sysUdfs)
self.assertNotIn("sys_bineval", u.sysUdfs)
def test_udfCheckNeeded_keeps_fileread_for_pgsql_fileread(self):
# sys_fileread is retained ONLY when a file read is requested AND the
# back-end is PostgreSQL (per the explicit DBMS.PGSQL guard).
set_dbms("PostgreSQL")
u = self._udf()
u.sysUdfs = {"sys_fileread": {}, "sys_bineval": {},
"sys_eval": {}, "sys_exec": {}}
conf.fileRead = "/etc/passwd"
conf.commonFiles = None
conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False
conf.regAdd = conf.regDel = False
u.udfCheckNeeded()
self.assertIn("sys_fileread", u.sysUdfs)
def test_udfCheckNeeded_drops_fileread_for_mysql_fileread(self):
# On MySQL the same file-read request still prunes sys_fileread (the
# guard keeps it only for PostgreSQL).
set_dbms("MySQL")
u = self._udf()
u.sysUdfs = {"sys_fileread": {}, "sys_bineval": {},
"sys_eval": {}, "sys_exec": {}}
conf.fileRead = "/etc/passwd"
conf.commonFiles = None
conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False
conf.regAdd = conf.regDel = False
u.udfCheckNeeded()
self.assertNotIn("sys_fileread", u.sysUdfs)
# -- udfCheckAndOverwrite --------------------------------------------- #
def test_udfCheckAndOverwrite_new_udf_scheduled(self):
# UDF does not exist -> no overwrite prompt -> scheduled for creation
set_dbms("MySQL")
u = self._udf()
self.patch(self.module.inject, "getValue", lambda q, *a, **k: False)
u.udfCheckAndOverwrite("sys_eval")
self.assertIn("sys_eval", u.udfToCreate)
def test_udfCheckAndOverwrite_existing_no_overwrite(self):
# UDF exists and user declines overwrite -> NOT scheduled
set_dbms("MySQL")
u = self._udf()
self.patch(self.module.inject, "getValue", lambda q, *a, **k: True)
self.patch(u, "_askOverwriteUdf", lambda udf: False)
u.udfCheckAndOverwrite("sys_eval")
self.assertNotIn("sys_eval", u.udfToCreate)
# -- udfInjectCore ----------------------------------------------------- #
def test_udfInjectCore_uploads_and_creates(self):
# Drive the full inject orchestration with the file write succeeding:
# every requested UDF must end up created and the support table built.
set_dbms("MySQL")
calls = {"created": [], "supportType": None}
class U(self.module.UDF):
def __init__(self):
super(U, self).__init__()
self.cmdTblName = "cmdtbl"
self.tblField = "data"
self.udfLocalFile = __file__ # any existing file (checkFile passes)
self.udfRemoteFile = "/tmp/lib.so"
def udfSetRemotePath(self):
pass
def writeFile(self, localFile, remoteFile, fileType, forceCheck=False):
calls["write"] = (remoteFile, fileType, forceCheck)
return True
def udfCreateFromSharedLib(self, udf, inpRet):
calls["created"].append(udf)
self.createdUdf.add(udf)
def udfCreateSupportTbl(self, dataType):
calls["supportType"] = dataType
u = U()
self.patch(self.module.inject, "getValue", lambda q, *a, **k: False)
result = u.udfInjectCore({"sys_eval": {"return": "string"}})
self.assertIs(result, True)
# binary upload forced; remote path threaded through
self.assertEqual(calls["write"], ("/tmp/lib.so", "binary", True))
self.assertEqual(calls["created"], ["sys_eval"])
# MySQL support table uses longtext
self.assertEqual(calls["supportType"], "longtext")
def test_udfInjectCore_noop_when_all_already_created(self):
# If every UDF is already created, nothing is uploaded and it returns True
set_dbms("MySQL")
class U(self.module.UDF):
def writeFile(self, *a, **k):
raise AssertionError("writeFile must not be called")
u = U()
u.createdUdf = {"sys_eval"}
result = u.udfInjectCore({"sys_eval": {"return": "string"}})
self.assertIs(result, True)
self.assertEqual(u.udfToCreate, set())
# -- MySQL udfCreateFromSharedLib (CREATE FUNCTION ... SONAME) --------- #
def test_mysql_udfCreateFromSharedLib_sql(self):
import plugins.dbms.mysql.takeover as mod
set_dbms("MySQL")
t = mod.Takeover()
t.udfToCreate = {"sys_eval"}
t.createdUdf = set()
t.udfSharedLibName = "libsabc"
t.udfSharedLibExt = "so"
stacked = []
self.patch(mod.inject, "goStacked", lambda q, *a, **k: stacked.append(q))
t.udfCreateFromSharedLib("sys_eval", {"return": "string"})
self.assertEqual(stacked[0], "DROP FUNCTION sys_eval")
self.assertEqual(
stacked[1],
"CREATE FUNCTION sys_eval RETURNS string SONAME 'libsabc.so'")
self.assertIn("sys_eval", t.createdUdf)
# -- PostgreSQL udfCreateFromSharedLib (CREATE OR REPLACE FUNCTION) ---- #
def test_pgsql_udfCreateFromSharedLib_sql(self):
import plugins.dbms.postgresql.takeover as mod
set_dbms("PostgreSQL")
t = mod.Takeover()
t.udfToCreate = {"sys_eval"}
t.createdUdf = set()
t.udfRemoteFile = "/tmp/libsabc.so"
stacked = []
self.patch(mod.inject, "goStacked", lambda q, *a, **k: stacked.append(q))
t.udfCreateFromSharedLib(
"sys_eval", {"input": ["text"], "return": "text"})
self.assertEqual(stacked[0], "DROP FUNCTION sys_eval(text)")
self.assertEqual(
stacked[1],
"CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS "
"'/tmp/libsabc.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL "
"INPUT IMMUTABLE")
# -- PostgreSQL udfSetRemotePath (OS-dependent path) ------------------- #
def test_pgsql_udfSetRemotePath_linux_and_windows(self):
# Linux -> /tmp/<lib>; Windows -> bare <lib> (saved into the data dir).
# Set kb.os directly to avoid Backend.setOs()'s interactive OS-mismatch
# prompt when flipping the OS mid-test.
import plugins.dbms.postgresql.takeover as mod
from lib.core.enums import OS
set_dbms("PostgreSQL")
t = mod.Takeover()
t.udfSharedLibName = "libsxyz"
t.udfSharedLibExt = "so"
_os = kb.os
try:
kb.os = OS.LINUX
t.udfSetRemotePath()
self.assertEqual(t.udfRemoteFile, "/tmp/libsxyz.so")
kb.os = OS.WINDOWS
t.udfSharedLibExt = "dll"
t.udfSetRemotePath()
self.assertEqual(t.udfRemoteFile, "libsxyz.dll")
finally:
kb.os = _os
if __name__ == "__main__":
unittest.main()
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
DBMS version/fork fingerprinting (plugins/dbms/<dbms>/fingerprint.py). Each
plugin's getFingerprint()/checkDbms() probes the backend with a cascade of
boolean expressions (inject.checkBooleanExpression) and version reads
(inject.getValue). Those are the network seam: stubbing them lets the dialect's
whole detection cascade run offline. We drive every targeted plugin with the
oracle pinned both True and False so opposite branches of the cascade execute.
"""
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
# (display name, fingerprint module, handler package)
TARGETS = [
("MySQL", "plugins.dbms.mysql.fingerprint", "plugins.dbms.mysql"),
("PostgreSQL", "plugins.dbms.postgresql.fingerprint", "plugins.dbms.postgresql"),
("Microsoft SQL Server", "plugins.dbms.mssqlserver.fingerprint", "plugins.dbms.mssqlserver"),
("Oracle", "plugins.dbms.oracle.fingerprint", "plugins.dbms.oracle"),
("IBM DB2", "plugins.dbms.db2.fingerprint", "plugins.dbms.db2"),
("Microsoft Access", "plugins.dbms.access.fingerprint", "plugins.dbms.access"),
("Firebird", "plugins.dbms.firebird.fingerprint", "plugins.dbms.firebird"),
("Sybase", "plugins.dbms.sybase.fingerprint", "plugins.dbms.sybase"),
("SAP MaxDB", "plugins.dbms.maxdb.fingerprint", "plugins.dbms.maxdb"),
("HSQLDB", "plugins.dbms.hsqldb.fingerprint", "plugins.dbms.hsqldb"),
("H2", "plugins.dbms.h2.fingerprint", "plugins.dbms.h2"),
("Presto", "plugins.dbms.presto.fingerprint", "plugins.dbms.presto"),
("Vertica", "plugins.dbms.vertica.fingerprint", "plugins.dbms.vertica"),
("Informix", "plugins.dbms.informix.fingerprint", "plugins.dbms.informix"),
("InterSystems Cache", "plugins.dbms.cache.fingerprint", "plugins.dbms.cache"),
("MonetDB", "plugins.dbms.monetdb.fingerprint", "plugins.dbms.monetdb"),
("Altibase", "plugins.dbms.altibase.fingerprint", "plugins.dbms.altibase"),
("ClickHouse", "plugins.dbms.clickhouse.fingerprint", "plugins.dbms.clickhouse"),
("CrateDB", "plugins.dbms.cratedb.fingerprint", "plugins.dbms.cratedb"),
("Cubrid", "plugins.dbms.cubrid.fingerprint", "plugins.dbms.cubrid"),
("Mckoi", "plugins.dbms.mckoi.fingerprint", "plugins.dbms.mckoi"),
("Virtuoso", "plugins.dbms.virtuoso.fingerprint", "plugins.dbms.virtuoso"),
("Raima Database Manager", "plugins.dbms.raima.fingerprint", "plugins.dbms.raima"),
("eXtremeDB", "plugins.dbms.extremedb.fingerprint", "plugins.dbms.extremedb"),
("FrontBase", "plugins.dbms.frontbase.fingerprint", "plugins.dbms.frontbase"),
("Apache Derby", "plugins.dbms.derby.fingerprint", "plugins.dbms.derby"),
("MimerSQL", "plugins.dbms.mimersql.fingerprint", "plugins.dbms.mimersql"),
]
def _handler_cls(pkg):
main = importlib.import_module(pkg)
return [getattr(main, n) for n in dir(main) if n.endswith("Map")][0]
# Dialects whose non-extensive getFingerprint emits Format.getDbms() (i.e.
# "<name> <version>") rather than a hard-coded DBMS.* constant, so the version
# that flowed through (Backend.setVersionList(["1.0"])) actually appears in the
# output. (In the test harness Backend.getDbms() is None because set_dbms uses
# forceDbms, so for these the dialect NAME is absent but "1.0" is load-bearing.)
ACTVER_DBMS = frozenset((
"MySQL", "Microsoft SQL Server", "Firebird", "HSQLDB",
))
# Dialects whose getFingerprint has a fork concept: with the oracle pinned True
# the first fork-detection branch fires (MySQL->MariaDB, PostgreSQL->CockroachDB,
# Oracle->DM8, Cache->Iris, H2->Apache Ignite, Presto->Trino) and the output
# gains a " (... fork)" suffix. Pinned False, no fork is emitted.
FORK_DBMS = frozenset((
"MySQL", "PostgreSQL", "Oracle", "InterSystems Cache", "H2", "Presto",
))
# Dialects whose getFingerprint genuinely needs more extraction state under
# conf.extensiveFp and raises a narrow KeyError before completing.
EXTENSIVE_RAISERS = frozenset((
"SAP MaxDB",
))
class TestFingerprint(unittest.TestCase):
def setUp(self):
self._saved = {k: conf.get(k) for k in ("batch", "extensiveFp", "api", "dbms", "forceDbms")}
self._kb = {k: kb.get(k) for k in ("dbmsVersion", "forcedDbms", "dbms", "stickyDBMS",
"resolutionDbms", "os", "osVersion", "osSP")}
conf.batch = True
conf.extensiveFp = False
conf.api = False
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
for k, v in self._kb.items():
kb[k] = v
def _drive(self, name, modpath, pkg, oracle):
set_dbms(name)
Backend.setVersionList(["1.0"])
mod = importlib.import_module(modpath)
if hasattr(mod, "inject"):
mod.inject.checkBooleanExpression = lambda e, *a, **k: oracle
mod.inject.getValue = lambda q, *a, **k: "1.0"
handler = _handler_cls(pkg)()
fp = handler.getFingerprint()
self.assertIsInstance(fp, str)
# Real content: the dialect's own identity must have flowed into the
# output, not merely the constant "back-end DBMS: " prefix.
if name in ACTVER_DBMS:
# Format.getDbms() embedded the version list -> "1.0" must appear.
self.assertIn("1.0", fp,
"%s fp lost the version that flowed through: %r" % (name, fp))
else:
# the dialect name (DBMS.* constant) must appear verbatim.
self.assertIn(Backend.getForcedDbms(), fp,
"%s fp lost its dialect name: %r" % (name, fp))
# Fork detection: with the oracle pinned True the first fork branch
# fires for the fork-bearing dialects; pinned False none do. This is the
# only thing distinguishing the True/False runs for those dialects.
if name in FORK_DBMS:
if oracle:
self.assertIn("fork)", fp,
"%s did not emit a fork label with oracle=True: %r" % (name, fp))
else:
self.assertNotIn("fork)", fp,
"%s emitted a fork label with oracle=False: %r" % (name, fp))
else:
# dialects with no fork concept never emit a fork label
self.assertNotIn("fork)", fp)
# checkDbms walks the dialect's detection cascade end-to-end; it must
# return a real boolean verdict (True/False), never None or a raise.
verdict = handler.checkDbms()
self.assertIn(verdict, (True, False),
"%s checkDbms() returned a non-bool: %r" % (name, verdict))
return fp
def test_fingerprint_oracle_true(self):
for name, modpath, pkg in TARGETS:
self._drive(name, modpath, pkg, True)
def test_fingerprint_oracle_false(self):
for name, modpath, pkg in TARGETS:
self._drive(name, modpath, pkg, False)
def test_fingerprint_extensive(self):
# conf.extensiveFp drives the deeper comment-/version-/dbms-check cascades
# (getFingerprint past the early return) — much more code per dialect.
# In this mode every dialect's output is built around an
# "active fingerprint: <Format.getDbms()>" line, so that header is the
# real content proof; the version "1.0" rides along for the ACTVER set.
conf.extensiveFp = True
try:
for name, modpath, pkg in TARGETS:
for oracle in (True, False):
set_dbms(name)
Backend.setVersionList(["1.0"])
mod = importlib.import_module(modpath)
if hasattr(mod, "inject"):
mod.inject.checkBooleanExpression = lambda e, *a, **k: oracle
mod.inject.getValue = lambda q, *a, **k: "1.0"
handler = _handler_cls(pkg)()
if name in EXTENSIVE_RAISERS:
# this dialect genuinely needs extra extraction state under
# extensiveFp; assert it gets exactly that far and no further.
with self.assertRaises(KeyError):
handler.getFingerprint()
continue
fp = handler.getFingerprint()
self.assertIsInstance(fp, str)
self.assertIn("active fingerprint:", fp,
"%s extensiveFp produced no active-fingerprint line: %r" % (name, fp))
if name in ACTVER_DBMS:
self.assertIn("1.0", fp,
"%s extensiveFp lost the version: %r" % (name, fp))
finally:
conf.extensiveFp = False
def _make(name, modpath, pkg):
def _t(self):
# _drive already asserts real, dialect-specific content (version/name +
# fork label + a boolean checkDbms verdict) for both oracle states.
self._drive(name, modpath, pkg, True)
self._drive(name, modpath, pkg, False)
return _t
# one named test per DBMS for clearer reporting
for _name, _mod, _pkg in TARGETS:
setattr(TestFingerprint, "test_fp_%s" % _pkg.split(".")[-1], _make(_name, _mod, _pkg))
if __name__ == "__main__":
unittest.main()
+865
View File
@@ -0,0 +1,865 @@
#!/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()
+873
View File
@@ -0,0 +1,873 @@
#!/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 plugin mixins, driving branches NOT already
covered by tests/test_search_enum.py / tests/test_databases_enum.py:
* 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.
* plugins/generic/misc.py - getRemoteTempPath (posix / windows-direct / MSSQL
ErrorLog), getVersionFromBanner, delRemoteFile, createSupportTbl, likeOrExact.
* plugins/generic/takeover.py - the PURE helpers only: Takeover.__init__ table
naming and the regRead/regAdd/regDel/osBof/osSmb control flow with the process/
network collaborators stubbed out (no metasploit/icmpsh/UDF spawning).
The injection layer (lib.request.inject.{getValue,goStacked}) is patched per
module, conf.direct=True selects the simple inband branches, conf.batch=True keeps
prompts non-interactive, and conf.dumper is a recording stub. 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 DBMS, 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
class _RecordingDumper(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))
# --------------------------------------------------------------------------- #
# 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."""
_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 = _RecordingDumper()
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 = _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
# --------------------------------------------------------------------------- #
class TestCustomSqlQuery(_GenericBase):
def test_select_joins_listlike_rows(self):
set_dbms("MySQL")
c = Custom()
cmod.inject.getValue = lambda query, **k: [["1", "alice"], ["2", "bob"]]
out = c.sqlQuery("SELECT id, name FROM users;")
# SELECT + list-like rows => each row joined into a single scalar string.
self.assertEqual(len(out), 2)
self.assertTrue(all(isinstance(_, str) for _ in out))
def test_select_scalar_passthrough(self):
set_dbms("MySQL")
c = Custom()
captured = {}
def gv(query, **k):
captured["query"] = query
captured["fromUser"] = k.get("fromUser")
return "42"
cmod.inject.getValue = gv
out = c.sqlQuery("SELECT COUNT(*) FROM users")
self.assertEqual(out, "42")
self.assertTrue(captured["fromUser"])
def test_metadb_suffix_stripped(self):
from lib.core.settings import METADB_SUFFIX
set_dbms("MySQL")
c = Custom()
captured = {}
def gv(query, **k):
captured["query"] = query
return "x"
cmod.inject.getValue = gv
c.sqlQuery("SELECT * FROM foo%s.bar" % METADB_SUFFIX)
# the METADB-suffixed schema qualifier is stripped before injection
self.assertNotIn(METADB_SUFFIX, captured["query"])
def test_mssql_from_dbo_rewrite(self):
set_dbms("Microsoft SQL Server")
c = Custom()
captured = {}
def gv(query, **k):
captured["query"] = query
return "x"
cmod.inject.getValue = gv
c.sqlQuery("SELECT * FROM mydb.users")
# single-dot FROM target gets the .dbo. schema spliced in for MSSQL
self.assertIn("mydb.dbo.users", captured["query"])
def test_nonquery_without_stacking_warns_none(self):
set_dbms("MySQL")
conf.direct = False
kb.injection.data = {} # no stacking technique available
c = Custom()
cmod.inject.getValue = lambda *a, **k: self.fail("must not run a query")
out = c.sqlQuery("DELETE FROM users")
self.assertIsNone(out)
def test_nonquery_stacked_returns_null(self):
set_dbms("MySQL")
conf.direct = True # direct => stacked execution allowed
c = Custom()
calls = {}
def go(query, *a, **k):
calls["query"] = query
cmod.inject.goStacked = go
out = c.sqlQuery("DROP TABLE users")
self.assertEqual(out, NULL)
self.assertIn("DROP TABLE users", calls["query"])
def test_nonedata_exception_handled(self):
from lib.core.exception import SqlmapNoneDataException
set_dbms("MySQL")
c = Custom()
def boom(*a, **k):
raise SqlmapNoneDataException("no data")
cmod.inject.getValue = boom
# exception is swallowed and logged; output stays None
self.assertIsNone(c.sqlQuery("SELECT 1"))
class TestCustomSqlFile(_GenericBase):
def test_sqlfile_select_snippets(self):
set_dbms("MySQL")
c = Custom()
cmod.inject.getValue = lambda query, **k: "r"
# getSQLSnippet reads from disk; patch it to return inline SQL.
saved = cmod.getSQLSnippet
try:
cmod.getSQLSnippet = lambda dbms, filename, **kw: "SELECT 1;SELECT 2"
conf.sqlFile = "dummy.sql"
c.sqlFile()
# two SELECT statements => two recorded dumper.sqlQuery calls
self.assertEqual(len(conf.dumper.sqlQueries), 2)
finally:
cmod.getSQLSnippet = saved
def test_sqlfile_nonselect_snippet(self):
set_dbms("MySQL")
conf.direct = True
c = Custom()
cmod.inject.goStacked = lambda *a, **k: None
saved = cmod.getSQLSnippet
try:
cmod.getSQLSnippet = lambda dbms, filename, **kw: "DROP TABLE x"
conf.sqlFile = "dummy.sql"
c.sqlFile()
# non-SELECT => single recorded call with the whole snippet
self.assertEqual(len(conf.dumper.sqlQueries), 1)
self.assertEqual(conf.dumper.sqlQueries[0][0], "DROP TABLE x")
finally:
cmod.getSQLSnippet = saved
# --------------------------------------------------------------------------- #
# misc.py
# --------------------------------------------------------------------------- #
class _TestMisc(Miscellaneous):
"""Miscellaneous with the OS/exec collaborators stubbed."""
cmdTblName = "sqlmapoutput"
def __init__(self):
Miscellaneous.__init__(self)
self.checkDbmsOsCalls = 0
self.execCmdCalls = []
def checkDbmsOs(self, detailed=False, vatch=False):
self.checkDbmsOsCalls += 1
def execCmd(self, cmd, silent=False):
self.execCmdCalls.append((cmd, silent))
class TestMisc(_GenericBase):
def test_remote_temp_path_posix(self):
set_dbms("MySQL")
self._force_os(OS.LINUX)
conf.tmpPath = None
m = _TestMisc()
out = m.getRemoteTempPath()
self.assertEqual(out, "/tmp")
self.assertEqual(conf.tmpPath, "/tmp")
def test_remote_temp_path_windows_direct(self):
set_dbms("MySQL")
self._force_os(OS.WINDOWS)
conf.tmpPath = None
conf.direct = True
m = _TestMisc()
out = m.getRemoteTempPath()
self.assertEqual(out, "%TEMP%")
def test_remote_temp_path_explicit_windows_drive(self):
# An explicit Windows-style drive path flips Backend OS to Windows.
set_dbms("MySQL")
conf.tmpPath = "C:\\Temp"
m = _TestMisc()
out = m.getRemoteTempPath()
self.assertTrue(Backend.isOs(OS.WINDOWS))
self.assertIn("Temp", out)
self.assertNotIn("\\", out) # ntToPosixSlashes normalized the path
def test_remote_temp_path_mssql_errorlog(self):
set_dbms("Microsoft SQL Server")
conf.tmpPath = None
mmod.inject.getValue = lambda query, **k: "C:\\Logs\\ERRORLOG"
m = _TestMisc()
out = m.getRemoteTempPath()
# ntpath.dirname strips the ERRORLOG filename, then ntToPosixSlashes
# normalizes the slashes: the exact temp dir must be "C:/Logs". Asserting
# the full path (and that the filename is gone) proves dirname ran.
self.assertEqual(out, "C:/Logs")
self.assertNotIn("ERRORLOG", out)
def test_get_version_from_banner(self):
set_dbms("MySQL")
conf.direct = True
kb.bannerFp = {}
mmod.inject.getValue = lambda query, **k: "5.7.31-log"
m = _TestMisc()
m.getVersionFromBanner()
# regex \d[\d.-]* extracts the leading numeric-ish run (trailing '-' kept)
self.assertEqual(kb.bannerFp["dbmsVersion"], "5.7.31-")
def test_get_version_from_banner_cached(self):
set_dbms("MySQL")
kb.bannerFp = {"dbmsVersion": "8.0"}
mmod.inject.getValue = lambda *a, **k: self.fail("must not query when cached")
m = _TestMisc()
m.getVersionFromBanner()
self.assertEqual(kb.bannerFp["dbmsVersion"], "8.0")
def test_del_remote_file_posix(self):
set_dbms("MySQL")
self._force_os(OS.LINUX)
m = _TestMisc()
m.delRemoteFile("/tmp/foo")
self.assertEqual(m.execCmdCalls[-1], ("rm -f /tmp/foo", True))
def test_del_remote_file_windows(self):
set_dbms("MySQL")
self._force_os(OS.WINDOWS)
m = _TestMisc()
m.delRemoteFile("C:/tmp/foo")
cmd, silent = m.execCmdCalls[-1]
self.assertTrue(cmd.startswith("del /F /Q"))
self.assertTrue(silent)
def test_del_remote_file_empty_noop(self):
set_dbms("MySQL")
m = _TestMisc()
m.delRemoteFile(None)
self.assertEqual(m.execCmdCalls, [])
self.assertEqual(m.checkDbmsOsCalls, 0)
def test_create_support_tbl(self):
set_dbms("MySQL")
m = _TestMisc()
stacked = []
mmod.inject.goStacked = lambda query, **k: stacked.append(query)
m.createSupportTbl("mytbl", "data", "TEXT")
joined = " | ".join(stacked)
self.assertIn("DROP TABLE mytbl", joined)
self.assertIn("CREATE TABLE mytbl(data TEXT)", joined)
def test_create_support_tbl_mssql_cmdtbl(self):
set_dbms("Microsoft SQL Server")
m = _TestMisc()
stacked = []
mmod.inject.goStacked = lambda query, **k: stacked.append(query)
m.createSupportTbl(m.cmdTblName, "data", "NVARCHAR(4000)")
joined = " | ".join(stacked)
# MSSQL cmd output table gets an IDENTITY id column
self.assertIn("IDENTITY", joined)
def test_like_or_exact_default(self):
m = _TestMisc()
mmod.readInput = lambda *a, **k: '1'
choice, cond = m.likeOrExact("table")
self.assertEqual(choice, '1')
self.assertIn("LIKE", cond)
def test_like_or_exact_exact(self):
m = _TestMisc()
mmod.readInput = lambda *a, **k: '2'
choice, cond = m.likeOrExact("table")
self.assertEqual(choice, '2')
self.assertEqual(cond, "='%s'")
def test_like_or_exact_invalid(self):
from lib.core.exception import SqlmapNoneDataException
m = _TestMisc()
mmod.readInput = lambda *a, **k: '9'
self.assertRaises(SqlmapNoneDataException, m.likeOrExact, "table")
# --------------------------------------------------------------------------- #
# takeover.py (pure helpers only)
# --------------------------------------------------------------------------- #
class _TestTakeover(tmod.Takeover):
"""Takeover with all process/network collaborators stubbed.
Only the pure control-flow helpers (table naming, reg read/add/del dispatch,
osBof/osSmb guards) are exercised; metasploit/icmpsh/UDF spawning is replaced
with recorders so no external process or socket is ever created.
"""
def __init__(self):
tmod.Takeover.__init__(self)
self.regCalls = []
self.osVal = OS.WINDOWS
self.smbCalled = False
self.bofCalled = False
self._regInitCalled = 0
# neutralize environment setup / OS detection
def _regInit(self):
self._regInitCalled += 1
def checkDbmsOs(self, detailed=False, vatch=False):
pass
def initEnv(self, *a, **k):
pass
def getRemoteTempPath(self):
return "/tmp"
def createMsfShellcode(self, *a, **k):
pass
def readRegKey(self, regKey, regValue, parse=False):
self.regCalls.append(("read", regKey, regValue))
return "value"
def addRegKey(self, regKey, regValue, regType, regData):
self.regCalls.append(("add", regKey, regValue, regType, regData))
def delRegKey(self, regKey, regValue):
self.regCalls.append(("del", regKey, regValue))
def smb(self):
self.smbCalled = True
def bof(self):
self.bofCalled = True
class TestTakeover(_GenericBase):
def _saved_takeover_readInput(self):
return tmod.readInput
def setUp(self):
_GenericBase.setUp(self)
self._saved_t_readInput = tmod.readInput
def tearDown(self):
tmod.readInput = self._saved_t_readInput
_GenericBase.tearDown(self)
def test_init_cmd_table_name(self):
set_dbms("MySQL")
t = _TestTakeover()
self.assertEqual(t.cmdTblName, "%soutput" % conf.tablePrefix)
self.assertEqual(t.tblField, "data")
def test_reg_read_from_conf(self):
set_dbms("Microsoft SQL Server")
conf.regKey = "HKLM\\Soft"
conf.regVal = "Name"
t = _TestTakeover()
out = t.regRead()
self.assertEqual(out, "value")
self.assertEqual(t.regCalls[-1], ("read", "HKLM\\Soft", "Name"))
self.assertEqual(t._regInitCalled, 1)
def test_reg_read_defaults(self):
set_dbms("Microsoft SQL Server")
conf.regKey = None
conf.regVal = None
tmod.readInput = lambda message, default=None, **k: default
t = _TestTakeover()
t.regRead()
kind, regKey, regVal = t.regCalls[-1]
self.assertEqual(kind, "read")
self.assertIn("CurrentVersion", regKey)
self.assertEqual(regVal, "ProductName")
def test_reg_add_from_conf(self):
set_dbms("Microsoft SQL Server")
conf.regKey = "HKLM\\Soft"
conf.regVal = "Name"
conf.regData = "data"
conf.regType = "REG_SZ"
t = _TestTakeover()
t.regAdd()
self.assertEqual(t.regCalls[-1], ("add", "HKLM\\Soft", "Name", "REG_SZ", "data"))
def test_reg_add_missing_key_raises(self):
from lib.core.exception import SqlmapMissingMandatoryOptionException
set_dbms("Microsoft SQL Server")
conf.regKey = None
conf.regVal = None
conf.regData = None
conf.regType = None
tmod.readInput = lambda *a, **k: "" # empty -> missing mandatory option
t = _TestTakeover()
self.assertRaises(SqlmapMissingMandatoryOptionException, t.regAdd)
def test_reg_del_confirmed(self):
set_dbms("Microsoft SQL Server")
conf.regKey = "HKLM\\Soft"
conf.regVal = "Name"
tmod.readInput = lambda message, default=None, boolean=False, **k: True if boolean else default
t = _TestTakeover()
t.regDel()
self.assertEqual(t.regCalls[-1], ("del", "HKLM\\Soft", "Name"))
def test_reg_del_declined(self):
set_dbms("Microsoft SQL Server")
conf.regKey = "HKLM\\Soft"
conf.regVal = "Name"
tmod.readInput = lambda message, default=None, boolean=False, **k: False if boolean else default
t = _TestTakeover()
t.regDel()
# declined => no delRegKey call recorded
self.assertEqual([c for c in t.regCalls if c[0] == "del"], [])
def test_osbof_wrong_dbms_raises(self):
from lib.core.exception import SqlmapUnsupportedDBMSException
set_dbms("MySQL")
conf.direct = True
t = _TestTakeover()
self.assertRaises(SqlmapUnsupportedDBMSException, t.osBof)
def test_osbof_no_stacking_returns(self):
set_dbms("Microsoft SQL Server")
conf.direct = False
kb.injection.data = {} # no stacking, not direct => early return
t = _TestTakeover()
self.assertIsNone(t.osBof())
self.assertFalse(t.bofCalled)
def test_ossmb_non_windows_raises(self):
from lib.core.exception import SqlmapUnsupportedDBMSException
set_dbms("MySQL")
conf.direct = True
t = _TestTakeover()
# checkDbmsOs is a no-op here, so force the non-Windows OS explicitly
self._force_os(OS.LINUX)
self.assertRaises(SqlmapUnsupportedDBMSException, t.osSmb)
self.assertFalse(t.smbCalled)
def test_ossmb_windows_invokes_smb(self):
set_dbms("MySQL")
conf.direct = True
self._force_os(OS.WINDOWS)
t = _TestTakeover()
t.osSmb()
self.assertTrue(t.smbCalled)
if __name__ == "__main__":
unittest.main()
+55 -4
View File
@@ -448,16 +448,67 @@ class TestGraphqlDialects(unittest.TestCase):
self.assertIsNone(gi.DIALECTS["SQLite"].delay)
def _dbmsTruth(dbms):
"""A truth() oracle that behaves like a real `dbms` back-end: it answers each
dialect's fingerprint predicate by the SQL *semantics* a genuine instance would
exhibit, keyed on the function tokens the predicate emits - never on the
fingerprint constant itself. A predicate referencing a function the back-end does
not implement raises an error on a real server and is therefore falsy here."""
# Which vendor-specific tokens each back-end actually understands. A predicate is
# true only if every vendor token it mentions belongs to this back-end (mirroring
# an unknown function being a hard error rather than a false comparison).
knows = {
"SQLite": ("SQLITE_VERSION()",),
"Microsoft SQL Server": ("@@VERSION",),
"PostgreSQL": ("version()",),
"MySQL": ("@@VERSION_COMMENT", "@@VERSION"),
}
# @@VERSION exists on both MSSQL and MySQL; the distinguishing factor is the
# '%Microsoft%' banner match, which only an actual Microsoft server satisfies.
vendorTokens = ("SQLITE_VERSION()", "@@VERSION_COMMENT", "@@VERSION", "version()")
owned = knows[dbms]
def truth(cond):
# Any vendor token the predicate names must be implemented by this back-end,
# else the probe errors out (falsy).
for token in vendorTokens:
if token in cond and token not in owned:
# @@VERSION is shared; let the banner clause below decide instead.
if token == "@@VERSION" and "@@VERSION_COMMENT" not in cond:
continue
return False
if not any(token in cond for token in vendorTokens):
return False
# @@VERSION LIKE '%Microsoft%' is only true on a real Microsoft server.
if "@@VERSION" in cond and "Microsoft" in cond:
return dbms == "Microsoft SQL Server"
# version() LIKE 'PostgreSQL%' is only true on a real PostgreSQL server.
if "version()" in cond and "PostgreSQL" in cond:
return dbms == "PostgreSQL"
return True
return truth
class TestGraphqlFingerprint(unittest.TestCase):
"""DBMS fingerprinting drives off the universal truth() predicate"""
def test_identifies_sqlite(self):
truth = lambda cond: cond == gi.DIALECTS["SQLite"].fingerprint
self.assertEqual(gi._fingerprint(truth), "SQLite")
# A SQLite-modelled oracle answers only SQLite's own probe; _fingerprint must
# discriminate to land on SQLite rather than echo the asserted constant.
self.assertEqual(gi._fingerprint(_dbmsTruth("SQLite")), "SQLite")
def test_identifies_mysql(self):
truth = lambda cond: cond == gi.DIALECTS["MySQL"].fingerprint
self.assertEqual(gi._fingerprint(truth), "MySQL")
self.assertEqual(gi._fingerprint(_dbmsTruth("MySQL")), "MySQL")
def test_identifies_mssql(self):
# @@VERSION is shared with MySQL; only the '%Microsoft%' banner match resolves it.
self.assertEqual(gi._fingerprint(_dbmsTruth("Microsoft SQL Server")),
"Microsoft SQL Server")
def test_identifies_postgresql(self):
self.assertEqual(gi._fingerprint(_dbmsTruth("PostgreSQL")), "PostgreSQL")
def test_unknown_backend(self):
self.assertIsNone(gi._fingerprint(lambda cond: False))
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Parser-introspection helpers in lib/utils/gui.py. The GUI itself needs a live
display (Tk), so it is excluded from the smoke test and never imported there;
these module-level helpers, however, are pure and work on argparse/optparse
parser+option objects. We exercise BOTH backends (argparse natively, optparse
via a lightweight stand-in) so the compatibility branches are walked. Importing
the module also covers its (otherwise-uncovered) top-level definitions.
"""
import argparse
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils import gui
class _OptparseLikeOption(object):
"""Minimal optparse.Option stand-in (drives the non-argparse branches)."""
def __init__(self, short, long_, dest, help_, type_=None, takes=True):
self._short_opts = [short] if short else []
self._long_opts = [long_] if long_ else []
self.dest = dest
self.help = help_
self.type = type_
self._takes = takes
def takes_value(self):
return self._takes
class _OptparseLikeGroup(object):
def __init__(self, title, description, options):
self.title = title
self.description = description
self.option_list = options
def get_description(self):
return self.description
def _build_argparse():
p = argparse.ArgumentParser()
g = p.add_argument_group("Target", "options for the target")
g.add_argument("-u", "--url", dest="url", help="target url")
g.add_argument("--level", dest="level", type=int, help="level", choices=[1, 2, 3])
g.add_argument("--flag", dest="flag", action="store_true", help="a boolean")
return p, g
class TestArgparseBackend(unittest.TestCase):
def setUp(self):
self.parser, self.group = _build_argparse()
def test_parser_groups_found(self):
groups = gui._parserGroups(self.parser)
titles = [gui._groupTitle(g) for g in groups]
self.assertIn("Target", titles)
def test_group_options_and_metadata(self):
opts = gui._groupOptions(self.group)
self.assertTrue(opts)
self.assertEqual(gui._groupDescription(self.group), "options for the target")
def test_opt_accessors(self):
opts = gui._groupOptions(self.group)
by_dest = dict((gui._optDest(o), o) for o in opts)
url = by_dest["url"]
self.assertIn("--url", gui._optStrings(url))
self.assertEqual(gui._optHelp(url), "target url")
self.assertTrue(gui._optTakesValue(url))
self.assertEqual(gui._optValueType(url), "string")
self.assertIn("--url", gui._optionLabel(url))
def test_int_type_and_choices(self):
opts = gui._groupOptions(self.group)
by_dest = dict((gui._optDest(o), o) for o in opts)
level = by_dest["level"]
self.assertEqual(gui._optValueType(level), "int")
self.assertEqual(gui._optChoices(level), [1, 2, 3])
def test_store_true_takes_no_value(self):
opts = gui._groupOptions(self.group)
by_dest = dict((gui._optDest(o), o) for o in opts)
self.assertFalse(gui._optTakesValue(by_dest["flag"]))
class TestOptparseBackend(unittest.TestCase):
def setUp(self):
self.opt = _OptparseLikeOption("-u", "--url", "url", "target url", type_="string")
self.intopt = _OptparseLikeOption(None, "--level", "level", "level", type_="int")
self.boolopt = _OptparseLikeOption(None, "--flag", "flag", "flag", takes=False)
self.group = _OptparseLikeGroup("Target", "target opts", [self.opt, self.intopt, self.boolopt])
def test_opt_strings_from_short_long(self):
self.assertEqual(gui._optStrings(self.opt), ["-u", "--url"])
def test_value_type_and_takes(self):
self.assertEqual(gui._optValueType(self.intopt), "int")
self.assertTrue(gui._optTakesValue(self.opt))
self.assertFalse(gui._optTakesValue(self.boolopt))
def test_group_description_via_method(self):
self.assertEqual(gui._groupDescription(self.group), "target opts")
self.assertEqual(gui._groupOptions(self.group), [self.opt, self.intopt, self.boolopt])
if __name__ == "__main__":
unittest.main()
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Tests for lib/utils/har.py -- HAR (HTTP Archive) collector and HTTP
request/response parsing used by sqlmap's --har-file feature.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils import har as H
class TestFakeSocket(unittest.TestCase):
def test_makefile_returns_bytesio(self):
sock = H.FakeSocket(b"hello\r\n")
f = sock.makefile()
self.assertEqual(f.read(), b"hello\r\n")
class TestRawPair(unittest.TestCase):
def test_stores_fields(self):
pair = H.RawPair(b"GET / HTTP/1.0\r\n\r\n",
b"HTTP/1.0 200 OK\r\n\r\n",
startTime=1000, endTime=2000)
self.assertEqual(pair.request, b"GET / HTTP/1.0\r\n\r\n")
self.assertEqual(pair.response, b"HTTP/1.0 200 OK\r\n\r\n")
self.assertEqual(pair.startTime, 1000)
self.assertEqual(pair.endTime, 2000)
class TestHTTPCollector(unittest.TestCase):
def test_collect_and_obtain(self):
c = H.HTTPCollector()
c.collectRequest(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n",
b"HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\nbody",
startTime=1000, endTime=2000)
result = c.obtain()
log = result["log"]
self.assertEqual(log["version"], "1.2")
self.assertEqual(log["creator"]["name"], "sqlmap")
entries = log["entries"]
self.assertEqual(len(entries), 1)
self.assertEqual(entries[0]["request"]["method"], "GET")
self.assertEqual(entries[0]["response"]["status"], 200)
class TestHTTPCollectorFactory(unittest.TestCase):
def test_create_returns_collector(self):
f = H.HTTPCollectorFactory(harFile=True)
c = f.create()
self.assertIsInstance(c, H.HTTPCollector)
class TestEntry(unittest.TestCase):
def test_toDict(self):
req = H.Request("GET", "/path", "HTTP/1.1",
{"Host": "example.com"})
resp = H.Response("HTTP/1.1", 200, "OK",
{"Content-Type": "text/html"}, b"body")
entry = H.Entry(req, resp, startTime=1000, endTime=2000,
extendedArguments={})
d = entry.toDict()
self.assertEqual(d["request"]["method"], "GET")
self.assertEqual(d["response"]["status"], 200)
self.assertEqual(d["time"], 1000000)
self.assertIn("startedDateTime", d)
class TestRequest(unittest.TestCase):
def test_parse_simple_get(self):
raw = b"GET /path HTTP/1.1\r\nHost: example.com\r\n\r\n"
req = H.Request.parse(raw)
self.assertEqual(req.method, "GET")
self.assertEqual(req.path, "/path")
self.assertEqual(req.httpVersion, "HTTP/1.1")
self.assertEqual(req.headers.get("Host"), "example.com")
def test_parse_with_comment(self):
raw = (b"HTTP request [#1]:\r\n"
b"POST /submit HTTP/1.0\r\n"
b"Host: example.com\r\n"
b"Content-Type: text/plain\r\n"
b"Content-Length: 4\r\n"
b"\r\n"
b"body")
req = H.Request.parse(raw)
self.assertEqual(req.method, "POST")
self.assertEqual(req.path, "/submit")
self.assertEqual(req.comment, b"HTTP request [#1]:")
self.assertIn(b"body", req.postBody)
def test_toDict(self):
req = H.Request("GET", "/", "HTTP/1.0",
{"Host": "test.com", "Accept": "*/*"})
d = req.toDict()
self.assertEqual(d["method"], "GET")
self.assertEqual(d["url"], "http://test.com/")
self.assertEqual(len(d["headers"]), 2)
def test_toDict_with_postbody(self):
req = H.Request("POST", "/", "HTTP/1.1",
{"Host": "test.com", "Content-Type": "application/json"},
postBody=b'{"a":1}')
d = req.toDict()
self.assertEqual(d["postData"]["mimeType"], "application/json")
self.assertIn('{"a":1}', d["postData"]["text"])
def test_url_property(self):
req = H.Request("GET", "/path?q=1", "HTTP/1.0",
{"Host": "example.com"})
self.assertEqual(req.url, "http://example.com/path?q=1")
def test_url_no_host_header(self):
req = H.Request("GET", "/", "HTTP/1.0", {})
self.assertIn("unknown", req.url)
class TestResponse(unittest.TestCase):
def test_parse_simple(self):
raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 4\r\n\r\nbody"
resp = H.Response.parse(raw)
self.assertEqual(resp.status, 200)
self.assertEqual(resp.statusText, "OK")
self.assertEqual(resp.headers.get("Content-Type"), "text/html")
self.assertEqual(resp.content, b"body")
def test_parse_with_comment(self):
raw = (b"HTTP response [#1] (200 Fine):\r\n"
b"HTTP/1.0 200 Fine\r\n"
b"Content-Type: text/plain\r\n"
b"\r\n"
b"response body")
resp = H.Response.parse(raw)
self.assertEqual(resp.status, 200)
self.assertEqual(resp.statusText, "Fine")
self.assertIn(b"HTTP response", resp.comment)
def test_toDict(self):
resp = H.Response("HTTP/1.1", 404, "Not Found",
{"Content-Type": "text/html"}, b"not found")
d = resp.toDict()
self.assertEqual(d["status"], 404)
self.assertEqual(d["statusText"], "Not Found")
self.assertEqual(d["content"]["text"], "not found")
self.assertEqual(d["content"]["size"], 9)
def test_toDict_binary_content_encoded(self):
resp = H.Response("HTTP/1.1", 200, "OK",
{"Content-Type": "application/octet-stream"},
b"\x00\x01\xff")
d = resp.toDict()
self.assertEqual(d["content"]["encoding"], "base64")
def test_toDict_non_text_content(self):
resp = H.Response("HTTP/1.1", 200, "OK",
{"Content-Type": "text/plain"}, b"plain text")
d = resp.toDict()
self.assertEqual(d["content"]["text"], "plain text")
if __name__ == "__main__":
unittest.main(verbosity=2)
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Dictionary-attack machinery in lib/utils/hash.py (the cracking loop, hash-file
parsing, result storage and table/cache post-processing) - the part NOT covered
by tests/test_hash.py, which only exercises the pure hash-format functions.
These run the single-process cracking path (conf.disableMulti=True) against a
TINY temp wordlist that contains the known plaintext, so a known hash is cracked
deterministically in milliseconds without interactive prompts, multiprocessing
pools, network, or the real default dictionary. conf.hashDB is forced to None so
hashDBRetrieve/hashDBWrite become no-ops (no session DB side effects).
"""
import glob
import hashlib
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils import hash as H
from lib.core.data import conf, kb
from lib.core.enums import MKSTEMP_PREFIX
SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad"
# known plaintext / hashes shared across tests
PW = "testpass"
MD5_HASH = hashlib.md5(PW.encode("utf-8")).hexdigest()
class _CrackBase(unittest.TestCase):
"""Sets up a tiny wordlist and non-interactive, no-DB, single-process state."""
@classmethod
def setUpClass(cls):
cls._tmpfiles = []
# tiny wordlist containing the known plaintext (plus decoys)
cls.wordlist = os.path.join(SCRATCH, "test_hash_crack_wl.txt")
with open(cls.wordlist, "w") as f:
f.write("foo\nbar\n%s\nbaz\n" % PW)
cls._tmpfiles.append(cls.wordlist)
@classmethod
def tearDownClass(cls):
for path in cls._tmpfiles:
try:
os.remove(path)
except OSError:
pass
def setUp(self):
# snapshot global state we mutate
self._saved = {
"disableMulti": conf.disableMulti,
"hashDB": conf.hashDB,
"hashFile": conf.hashFile,
"wordlists": kb.wordlists,
"cachedUsersPasswords": kb.data.cachedUsersPasswords if "cachedUsersPasswords" in kb.data else None,
"storeHashes": kb.choices.storeHashes if "storeHashes" in kb.choices else None,
}
# deterministic, fast, side-effect-free cracking
conf.disableMulti = True
conf.hashDB = None
kb.wordlists = [self.wordlist]
def tearDown(self):
conf.disableMulti = self._saved["disableMulti"]
conf.hashDB = self._saved["hashDB"]
conf.hashFile = self._saved["hashFile"]
kb.wordlists = self._saved["wordlists"]
kb.data.cachedUsersPasswords = self._saved["cachedUsersPasswords"]
kb.choices.storeHashes = self._saved["storeHashes"]
class TestDictionaryAttack(_CrackBase):
def test_crack_md5_generic_variant_a(self):
# generic (no-salt) algorithms go through _bruteProcessVariantA
results = H.dictionaryAttack({"admin": [MD5_HASH]})
self.assertEqual(results, [("admin", MD5_HASH, PW)])
def test_crack_postgres_variant_b(self):
# username-dependent algorithm goes through _bruteProcessVariantB
h = H.postgres_passwd(PW, "testuser", uppercase=False)
results = H.dictionaryAttack({"testuser": [h]})
self.assertEqual(results, [("testuser", h, PW)])
def test_crack_django_md5_salted_variant_b(self):
# salted algorithm: salt is parsed out of the stored hash by dictionaryAttack
h = H.django_md5_passwd(PW, "salt")
results = H.dictionaryAttack({"u2": [h]})
self.assertEqual(results, [("u2", h, PW)])
def test_no_password_found_returns_empty(self):
# plaintext not in wordlist -> nothing cracked
h = hashlib.md5(b"not-in-wordlist-xyz").hexdigest()
results = H.dictionaryAttack({"admin": [h]})
self.assertEqual(results, [])
def test_unknown_hash_format_ignored(self):
# a value that hashRecognition rejects produces no hash_regexes and no results
results = H.dictionaryAttack({"admin": ["not_a_hash"]})
self.assertEqual(results, [])
def test_empty_attack_dict(self):
self.assertEqual(H.dictionaryAttack({}), [])
class TestCrackHashFile(_CrackBase):
def setUp(self):
super(TestCrackHashFile, self).setUp()
# capture the parsed attack_dict that crackHashFile feeds to dictionaryAttack
self._captured = {}
self._real_attack = H.dictionaryAttack
def _capture(attack_dict):
self._captured.clear()
self._captured.update(attack_dict)
return []
H.dictionaryAttack = _capture
def tearDown(self):
H.dictionaryAttack = self._real_attack
super(TestCrackHashFile, self).tearDown()
def test_user_colon_hash_file(self):
path = os.path.join(SCRATCH, "test_hash_crack_hashes.txt")
with open(path, "w") as f:
f.write("admin:%s\n" % MD5_HASH)
self._tmpfiles.append(path)
conf.hashFile = path
self.assertIsNone(H.crackHashFile(path))
# the "user:hash" line is parsed into {username: [hash]}
self.assertEqual(self._captured, {"admin": [MD5_HASH]})
def test_bare_hash_file(self):
# no "user:hash" structure -> a dummy user is synthesised per line
path = os.path.join(SCRATCH, "test_hash_crack_bare.txt")
with open(path, "w") as f:
f.write("%s\n" % MD5_HASH)
self._tmpfiles.append(path)
conf.hashFile = path
self.assertIsNone(H.crackHashFile(path))
from lib.core.settings import DUMMY_USER_PREFIX
self.assertEqual(len(self._captured), 1)
(key, value), = self._captured.items()
# the synthesised key uses the dummy-user prefix and maps to the bare hash
self.assertTrue(key.startswith(DUMMY_USER_PREFIX),
msg="bare line was not assigned a dummy user: %r" % key)
self.assertEqual(value, [MD5_HASH])
class TestAttackCachedUsersPasswords(_CrackBase):
def test_annotates_cleartext(self):
kb.data.cachedUsersPasswords = {"admin": [MD5_HASH]}
H.attackCachedUsersPasswords()
# the original value is augmented in place with the recovered clear-text
self.assertIn("clear-text password: %s" % PW, kb.data.cachedUsersPasswords["admin"][0])
def test_no_cached_data_is_noop(self):
kb.data.cachedUsersPasswords = {}
# must simply return without touching anything
self.assertIsNone(H.attackCachedUsersPasswords())
class TestStoreHashesToFile(_CrackBase):
def _hash_tempfiles(self):
pattern = os.path.join(tempfile.gettempdir(), MKSTEMP_PREFIX.HASHES + "*")
return set(glob.glob(pattern))
def test_store_disabled_writes_nothing(self):
kb.choices.storeHashes = False
before = self._hash_tempfiles()
H.storeHashesToFile({"admin": [MD5_HASH]})
self.assertEqual(self._hash_tempfiles(), before)
def test_store_enabled_writes_recognised_hash(self):
kb.choices.storeHashes = True
before = self._hash_tempfiles()
try:
H.storeHashesToFile({"admin": [MD5_HASH]})
new = self._hash_tempfiles() - before
self.assertEqual(len(new), 1)
with open(next(iter(new))) as fh:
written = fh.read()
self.assertIn(MD5_HASH, written)
self.assertIn("admin", written)
finally:
for path in self._hash_tempfiles() - before:
try:
os.remove(path)
except OSError:
pass
def test_empty_attack_dict_is_noop(self):
kb.choices.storeHashes = True
before = self._hash_tempfiles()
H.storeHashesToFile({})
self.assertEqual(self._hash_tempfiles(), before)
if __name__ == "__main__":
unittest.main(verbosity=2)
+12 -1
View File
@@ -109,10 +109,21 @@ class TestSerialized(_HashDBCase):
def test_bytes_containing_value_survives(self):
# REGRESSION (base64-pickle bytes fix): silently failed to restore on py3 before the fix.
# Must round-trip through SQLite, not the in-memory caches: write+flush here, then open a
# FRESH HashDB on the same file (empty read/write caches) so retrieve() hits the disk path.
value = {"raw": b"\x00\x01\xff", "items": [b"ab", "s", 1]}
self.db.write("bytesval", value, True)
self.db.flush()
self.assertEqual(self.db.retrieve("bytesval", True), value)
fresh = HashDB(self.path)
try:
# sanity: the value is genuinely not in the fresh in-memory caches
self.assertFalse(fresh._write_cache)
hash_ = HashDB.hashKey("bytesval")
self.assertIsNone(fresh._read_cache.get(hash_))
self.assertEqual(fresh.retrieve("bytesval", True), value)
finally:
fresh.closeAll()
class TestKeyHashing(_HashDBCase):
+293
View File
@@ -0,0 +1,293 @@
#!/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)
+65 -54
View File
@@ -102,32 +102,56 @@ class TestHelpers(unittest.TestCase):
class TestFingerprinting(unittest.TestCase):
# The mapping branches recognise a distinctive vendor substring *anywhere* inside
# a realistic error banner and normalise it to a canonical backend name. Feeding
# an embedded substring (not the bare canonical name) proves the source performs
# real substring discrimination rather than echoing its input.
def test_fingerprintByError_ad(self):
self.assertEqual(ldap._fingerprintByError("Microsoft Active Directory"),
"Microsoft Active Directory")
self.assertEqual(
ldap._fingerprintByError("LDAP error from Microsoft Active Directory server"),
"Microsoft Active Directory")
def test_fingerprintByError_openldap(self):
self.assertEqual(ldap._fingerprintByError("OpenLDAP"), "OpenLDAP")
self.assertEqual(ldap._fingerprintByError("OpenLDAP 2.4.57 SERVER_DOWN"),
"OpenLDAP")
def test_fingerprintByError_apacheds(self):
self.assertEqual(ldap._fingerprintByError("ApacheDS"), "ApacheDS")
self.assertEqual(ldap._fingerprintByError("org.apache.directory.ApacheDS 2.0"),
"ApacheDS")
def test_fingerprintByError_oracle(self):
self.assertEqual(ldap._fingerprintByError("Oracle Directory Server"),
self.assertEqual(ldap._fingerprintByError("Oracle Internet Directory / Oracle stack"),
"Oracle Directory Server")
def test_fingerprintByError_389(self):
self.assertEqual(ldap._fingerprintByError("389 Directory Server"),
self.assertEqual(ldap._fingerprintByError("Red Hat 389 ns-slapd"),
"389 Directory Server")
def test_fingerprintByError_generic(self):
self.assertEqual(ldap._fingerprintByError("Generic LDAP"), "Generic LDAP")
def test_fingerprintByError_precedence_ad_over_oracle(self):
# A banner carrying two recognised substrings resolves to the earlier branch
# (Active Directory), proving the result is driven by branch order, not by an
# echo of whichever name happens to appear.
self.assertEqual(
ldap._fingerprintByError("Microsoft Active Directory bridged to Oracle"),
"Microsoft Active Directory")
def test_fingerprintByError_jndi(self):
self.assertEqual(ldap._fingerprintByError("Java JNDI"), "Java JNDI")
def test_fingerprintByError_none_and_empty(self):
# The only real branch reachable by non-mapping banners: the falsy guard.
self.assertIsNone(ldap._fingerprintByError(None))
self.assertIsNone(ldap._fingerprintByError(""))
def test_fingerprintByError_pythonldap(self):
self.assertEqual(ldap._fingerprintByError("python-ldap"), "python-ldap")
def test_fingerprintByError_passthrough_when_unmatched(self):
# Banners that match no vendor branch (including the "python-ldap"/"Java JNDI"
# case, whose source branch is observationally identical to the catch-all) are
# returned verbatim. This single test documents that pass-through contract and,
# crucially, asserts such banners are NOT misclassified into a specific backend.
for banner in ("Generic LDAP", "python-ldap 3.4.0", "Caused by: Java JNDI",
"some unrecognised directory service"):
result = ldap._fingerprintByError(banner)
self.assertEqual(result, banner)
self.assertNotIn(result, ("Microsoft Active Directory", "OpenLDAP",
"ApacheDS", "Oracle Directory Server",
"389 Directory Server"))
class TestGrid(unittest.TestCase):
@@ -367,54 +391,41 @@ class TestCookiePlace(unittest.TestCase):
class TestNestedFilterParsing(unittest.TestCase):
def setUp(self):
# Import the REAL vulnserver parser (same technique as
# tests/test_graphql.py :: TestVulnserverGraphqlParser). `extra` and
# `extra/vulnserver` are packages, so a plain import works.
from extra.vulnserver import vulnserver
self.vs = vulnserver
def test_nested_compound_parses_all_siblings(self):
"""Blockers 3: nested (&) inside (|) must parse all siblings."""
# Inline copies of the vulnserver helpers so the test is self-contained
def _ldap_match(text, start):
depth = 0
i = start
while i < len(text):
ch = text[i]
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if depth == 0:
return i + 1
elif ch == '\\':
i += 1
i += 1
return len(text)
def _ldap_parse_value(text, start):
retVal = []
i = start
while i < len(text) and text[i] not in (')',):
if text[i] == '\\' and i + 2 < len(text):
retVal.append(chr(int(text[i+1:i+3], 16)))
i += 3
else:
retVal.append(text[i])
i += 1
return ''.join(retVal), i
# Minimum reproduction of the fixed _ldap_filter_to_sql
# (the real function is in extra/vulnserver/vulnserver.py)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'extra', 'vulnserver'))
# Can't cleanly import vulnserver because of the __main__ guard.
# Instead we verify the fixed _ldap_match returns the correct end
# position for a nested compound filter, which was the root cause.
f = '(|(&(uid=a)(cn=b))(mail=*))'
# The outer (| ... ) starts at 0 and should end at len(f)
outer_end = _ldap_match(f, 0)
# The REAL _ldap_match must balance brackets across nested compounds.
# Outer (| ... ) starts at 0 and ends at len(f).
outer_end = self.vs._ldap_match(f, 0)
self.assertEqual(outer_end, len(f))
# The inner (& ... ) compound's opening '(' is at position 2
# (f[2] == '('). _ldap_match must return the position after the
# matching ')' that closes the compound, i.e. right before (mail=*).
inner_end = _ldap_match(f, 2)
# Inner (& ... )'s opening '(' is at position 2; _ldap_match must
# return the position right before the (mail=*) sibling.
inner_end = self.vs._ldap_match(f, 2)
self.assertEqual(f[inner_end:inner_end+8], '(mail=*)')
# The REAL filter->SQL conversion must surface EVERY sibling condition:
# both members of the nested (&) AND the (mail=*) sibling of the (|).
clause, params, end = self.vs._ldap_filter_to_sql(f)
self.assertEqual(end, len(f))
self.assertIsNotNone(clause)
# nested-(&) siblings -> AND-joined, both columns present
self.assertIn(" AND ", clause)
self.assertIn("uid", clause)
self.assertIn("cn", clause)
# outer-(|) sibling must NOT be dropped
self.assertIn(" OR ", clause)
self.assertIn("mail", clause)
# the two equality values are parameterized in order
self.assertEqual(params, ["a", "b"])
if __name__ == "__main__":
unittest.main()
+663
View File
@@ -0,0 +1,663 @@
#!/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)
+739
View File
@@ -0,0 +1,739 @@
#!/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:[email protected]/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)
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Parsers under lib/parse/: DBMS banner fingerprinting (banner.py + the shared
FingerprintHandler in handler.py) and the .ini configuration-file reader
(configfile.py). These are pure: given a banner string (and the shipped XML
signature files) or a config file on disk, they populate kb/conf with no
network or DBMS. We drive each over realistic inputs and assert the extracted
fingerprint / parsed options.
"""
import os
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 kb, conf
from lib.core.enums import DBMS
from lib.parse.banner import bannerParser, MSSQLBannerHandler
from lib.parse.handler import FingerprintHandler
class TestFingerprintHandler(unittest.TestCase):
def test_feedinfo_dbms_version_scalar(self):
info = {}
h = FingerprintHandler("some banner", info)
h._feedInfo("dbmsVersion", "5.7.1")
self.assertEqual(info["dbmsVersion"], "5.7.1")
def test_feedinfo_set_valued_keys_split_on_pipe(self):
info = {}
h = FingerprintHandler("some banner", info)
h._feedInfo("type", "Linux|Debian")
self.assertIsInstance(info["type"], set)
self.assertEqual(info["type"], set(["Linux", "Debian"]))
def test_feedinfo_ignores_empty_and_none(self):
info = {}
h = FingerprintHandler("b", info)
h._feedInfo("type", "")
h._feedInfo("type", "None")
h._feedInfo("type", None)
self.assertNotIn("type", info)
class TestBannerParser(unittest.TestCase):
def setUp(self):
self._saved = kb.bannerFp
kb.bannerFp = {}
def tearDown(self):
kb.bannerFp = self._saved
def test_no_dbms_is_noop(self):
# without an identified DBMS bannerParser must bail out before touching kb.bannerFp
from lib.core.common import Backend
Backend.flushForcedDbms(force=True)
saved = (conf.get("forceDbms"), kb.get("dbms"))
conf.forceDbms = None
kb.dbms = None
try:
kb.bannerFp = {}
self.assertIsNone(bannerParser("PostgreSQL 9.5.3 on x86_64-pc-linux-gnu"))
# no back-end identified -> the early return leaves the fingerprint untouched
self.assertEqual(kb.bannerFp, {})
finally:
conf.forceDbms, kb.dbms = saved
def test_mysql_banner_populates_version(self):
set_dbms(DBMS.MYSQL)
kb.bannerFp = {}
bannerParser("5.0.51a-3ubuntu5.4")
# the generic signatures classify the OS/distrib from the banner tail
self.assertTrue(kb.bannerFp, msg="no fingerprint extracted")
self.assertIn("Ubuntu", kb.bannerFp.get("distrib", set()))
def test_oracle_banner_populates_version(self):
set_dbms(DBMS.ORACLE)
kb.bannerFp = {}
bannerParser("Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production")
self.assertIn("dbmsVersion", kb.bannerFp)
self.assertTrue(kb.bannerFp["dbmsVersion"].startswith("11.2.0"))
def test_pgsql_banner_populates_version(self):
set_dbms(DBMS.PGSQL)
kb.bannerFp = {}
# the shipped PostgreSQL signature 'PostgreSQL\s+([\w\.]+)' captures the version
bannerParser("PostgreSQL 9.5.3 on x86_64-pc-linux-gnu")
self.assertIn("dbmsVersion", kb.bannerFp)
self.assertEqual(kb.bannerFp["dbmsVersion"], "9.5.3")
def test_mssql_banner_populates_release_and_version(self):
set_dbms(DBMS.MSSQL)
kb.bannerFp = {}
# a real SQL Server 2008 RTM build present in data/xml/banner/mssql.xml,
# so the MSSQLBannerHandler resolves both the release year and the version
bannerParser("Microsoft SQL Server 2008 - 10.00.4311.00")
self.assertEqual(kb.bannerFp.get("dbmsRelease"), "2008")
self.assertEqual(kb.bannerFp.get("dbmsVersion"), "10.00.4311")
class TestMSSQLBannerHandler(unittest.TestCase):
def test_version_alt_built_for_dotzero_form(self):
info = {}
h = MSSQLBannerHandler("Microsoft SQL Server 10.00.1600.22", info)
h.startElement("version", {})
h.characters("10.00.1600")
h.endElement("version")
# endElement('version') derives the "<major>.0.<build>.0" alternate form
self.assertEqual(h._versionAlt, "10.0.1600.0")
class _Attrs(dict):
"""Minimal SAX-attrs stand-in (supports .get)."""
class TestMSSQLBannerHandlerServicePack(unittest.TestCase):
def test_servicepack_strips_spaces(self):
info = {}
h = MSSQLBannerHandler("banner", info)
h.startElement("servicepack", {})
h.characters(" 2 ")
h.endElement("servicepack")
self.assertEqual(h._servicePack, "2")
class TestConfigFileParser(unittest.TestCase):
def _write_cfg(self, body):
fd, path = tempfile.mkstemp(suffix=".ini", prefix="sqlmapcfg_")
os.close(fd)
with open(path, "w") as f:
f.write(body)
return path
def test_parses_target_and_typed_options(self):
from lib.parse.configfile import configFileParser
path = self._write_cfg(
"[Target]\n"
"url = http://config.invalid/?id=1\n"
"[Optimization]\n"
"threads = 4\n"
"[Injection]\n"
"tamper = space2comment\n"
)
saved = {k: conf.get(k) for k in ("url", "threads", "tamper")}
try:
configFileParser(path)
self.assertEqual(conf.url, "http://config.invalid/?id=1")
self.assertEqual(conf.threads, 4) # INTEGER datatype coerced
self.assertEqual(conf.tamper, "space2comment")
finally:
for k, v in saved.items():
conf[k] = v
os.remove(path)
def test_missing_target_section_raises(self):
from lib.parse.configfile import configFileParser
from lib.core.exception import SqlmapMissingMandatoryOptionException
path = self._write_cfg("[Request]\nthreads = 1\n")
try:
self.assertRaises(SqlmapMissingMandatoryOptionException,
configFileParser, path)
finally:
os.remove(path)
if __name__ == "__main__":
unittest.main()
+127 -36
View File
@@ -20,9 +20,13 @@ 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, DEFAULT_GET_POST_DELIMITER,
CUSTOM_INJECTION_MARK_CHAR)
MARK = "*"
# The real source marks injection points with kb.customInjectionMark, which defaults to
# CUSTOM_INJECTION_MARK_CHAR ('*'). Tie the test's mark char to the source constant so a
# change there is reflected here too.
MARK = CUSTOM_INJECTION_MARK_CHAR
def classify(d):
@@ -35,6 +39,60 @@ def classify(d):
return "PLAIN"
def _drive_request_marking(body):
"""Run sqlmap's REAL request-body injection-point marking on `body`.
Approach (a): drive the genuine code path in lib.core.target._setRequestParams()
(the same function the CLI uses) with a minimal conf/kb state, a POST body, and
readInput auto-answering 'Y'. The marking regexes (target.py:159-215) run against
`conf.data`; the fully-marked string is the snapshot of conf.data carrying the most
injection marks, captured BEFORE the later strip (target.py:~348) removes them.
Returns (fully_marked_data, kb.postHint). A regression in the source marking regexes
changes this output and breaks the asserting tests.
"""
import lib.core.target as target
from lib.core.data import conf, kb
from lib.core.enums import HTTPMETHOD
snapshots = []
base = type(conf)
orig_setitem = base.__setitem__
def _record(self, key, value):
if key == "data" and isinstance(value, str):
snapshots.append(value)
orig_setitem(self, key, value)
orig_readInput = target.readInput
target.readInput = lambda *a, **k: 'Y'
base.__setitem__ = _record
try:
conf.parameters = {}
conf.paramDict = {}
conf.direct = False
conf.method = HTTPMETHOD.POST
conf.url = "http://test.invalid/"
conf.cookie = None
conf.httpHeaders = []
conf.testParameter = None
conf.forms = None
conf.crawlDepth = None
kb.processUserMarks = None
kb.postHint = None
kb.customInjectionMark = MARK
kb.testOnlyCustom = False
conf.data = body
target._setRequestParams()
postHint = kb.postHint
finally:
base.__setitem__ = orig_setitem
target.readInput = orig_readInput
fully_marked = max(snapshots, key=lambda s: s.count(MARK))
return fully_marked, postHint
class TestRecognitionRegexes(unittest.TestCase):
CASES = [
('{"id":1}', "JSON"),
@@ -56,14 +114,14 @@ class TestRecognitionRegexes(unittest.TestCase):
class TestJsonMarking(unittest.TestCase):
# mirrors target.py:159-162 JSON injection-point marking
# Approach (a): exercises the REAL JSON injection-point marking in
# lib.core.target._setRequestParams() (target.py:159-162) via _drive_request_marking().
# No source logic is copied into the test; a regression in the source regexes fails it.
@staticmethod
def mark(data):
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*".*?)"(?<!\\")', r'\g<1>%s"' % MARK, data)
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*")"', r'\g<1>%s"' % MARK, data)
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*)(-?\d[\d\.]*)\b', r'\g<1>\g<3>%s' % MARK, data)
data = re.sub(r'("(?P<name>[^"]+)"\s*:\s*)((true|false|null))\b', r'\g<1>\g<3>%s' % MARK, data)
return data
marked, postHint = _drive_request_marking(data)
assert postHint == "JSON", "expected JSON postHint, got %r for %r" % (postHint, data)
return marked
CASES = [
('{"id":1}', '{"id":1*}'),
@@ -87,10 +145,15 @@ class TestJsonMarking(unittest.TestCase):
class TestXmlMarking(unittest.TestCase):
RX = r"(<(?P<name>[^>]+)( [^<]*)?>)([^<]+)(</\2)"
# Approach (a): exercises the REAL SOAP/XML injection-point marking in
# lib.core.target._setRequestParams() (target.py:215) via _drive_request_marking().
# A regression in the source XML regex fails this test.
def mark(self, data):
return re.sub(self.RX, r"\g<1>\g<4>%s\g<5>" % MARK, data)
from lib.core.enums import POST_HINT
marked, postHint = _drive_request_marking(data)
self.assertIn(postHint, (POST_HINT.XML, POST_HINT.SOAP),
msg="expected XML/SOAP postHint, got %r for %r" % (postHint, data))
return marked
CASES = [
("<a>x</a>", "<a>x*</a>"),
@@ -105,33 +168,61 @@ class TestXmlMarking(unittest.TestCase):
self.assertEqual(self.mark(inp), expected, msg="xmlmark(%r)" % inp)
class TestHppReconstruction(unittest.TestCase):
# mirrors connect.py:1171-1187 HPP splitting
def hpp(self, payload, name="id"):
from thirdparty.six.moves import urllib as _urllib # py2+py3
quote = _urllib.parse.quote
def _drive_hpp(payload, name="id"):
"""Run sqlmap's REAL HTTP-parameter-pollution payload reconstruction on `payload`.
def ue(s):
try:
return quote(s)
except Exception:
return s
Approach (a): drive the genuine HPP block inside lib.request.connect.Connect.queryPage()
(connect.py:1168-1192) -- the same method the engine uses to issue every request -- with
conf.hpp enabled and a GET value carrying the payload between PAYLOAD_DELIMITERs.
conf.skipUrlEncode is set so the unencoded splitter branch runs (matching the pinned
expected strings). queryPage's network call (agent.removePayloadDelimiters, invoked
immediately AFTER the HPP block) is hijacked to capture the transformed `value` and abort
before any I/O; the payload is then extracted from between the delimiters. A regression in
the source HPP logic changes this output and breaks the asserting tests.
"""
from lib.core.data import conf, kb
from lib.core.enums import PLACE
from lib.core.agent import agent
from lib.request.connect import Connect
class _Sentinel(Exception):
pass
captured = {}
def _capture(value):
captured["value"] = value
raise _Sentinel()
orig_remove = agent.removePayloadDelimiters
agent.removePayloadDelimiters = _capture
try:
conf.direct = False
conf.hpp = True
conf.method = "GET"
conf.paramDel = None
conf.skipUrlEncode = True
conf.url = "http://test.invalid/page.asp?%s=1" % name
kb.postUrlEncode = False
kb.tamperFunctions = []
kb.postSpaceToPlus = False
value = "%s=%s%s%s" % (name, PAYLOAD_DELIMITER, payload, PAYLOAD_DELIMITER)
_ = re.escape(PAYLOAD_DELIMITER)
match = re.search(r"(?P<name>\w+)=%s(?P<value>.+?)%s" % (_, _), value)
out = match.group("value")
for splitter in (ue(' '), ' '):
if splitter in out:
prefix, suffix = ("*/", "/*") if splitter == ' ' else (ue(x) for x in ("*/", "/*"))
parts = out.split(splitter)
parts[0] = "%s%s" % (parts[0], suffix)
parts[-1] = "%s%s=%s%s" % (DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[-1])
for i in range(1, len(parts) - 1):
parts[i] = "%s%s=%s%s%s" % (DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[i], suffix)
out = "".join(parts)
for splitter in (ue(','), ','):
out = out.replace(splitter, "%s%s=" % (DEFAULT_GET_POST_DELIMITER, match.group("name")))
return out
try:
Connect.queryPage(value=value, place=PLACE.GET, disableTampering=True)
except _Sentinel:
pass
finally:
agent.removePayloadDelimiters = orig_remove
_ = re.escape(PAYLOAD_DELIMITER)
return re.search(r"(?s)%s(?P<result>.*?)%s" % (_, _), captured["value"]).group("result")
class TestHppReconstruction(unittest.TestCase):
# Approach (a): drives the REAL HPP reconstruction (connect.py:1168-1192) via _drive_hpp().
def hpp(self, payload, name="id"):
return _drive_hpp(payload, name)
# Exact transform outputs (verified live against an ASP-style join). We pin the produced
# string rather than "reconstruct the SQL", because reconstruction depends on the SQL parser
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
The textual progress bar (lib/utils/progress.py) used during multi-item
extraction. Pure rendering/clamping logic plus ETA formatting.
"""
import os
import re
import sys
import time
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
import lib.utils.progress as progress_mod
from lib.utils.progress import ProgressBar
class TestProgressBar(unittest.TestCase):
def test_initial_is_zero_percent(self):
pb = ProgressBar(0, 100, 78)
self.assertTrue(str(pb).startswith("0%"), msg=str(pb))
def test_full_is_hundred_percent(self):
pb = ProgressBar(0, 100, 78)
pb.update(100)
self.assertTrue(str(pb).startswith("100%"), msg=str(pb))
def test_half_is_fifty_percent(self):
pb = ProgressBar(0, 100, 78)
pb.update(50)
self.assertIn("50%", str(pb))
def test_update_clamps_below_min(self):
pb = ProgressBar(10, 20, 78)
pb.update(-5)
self.assertTrue(str(pb).startswith("0%"))
def test_update_clamps_above_max(self):
pb = ProgressBar(0, 10, 78)
pb.update(999)
self.assertTrue(str(pb).startswith("100%"))
def test_convert_seconds(self):
pb = ProgressBar(0, 10, 78)
self.assertEqual(pb._convertSeconds(0), "00:00")
self.assertEqual(pb._convertSeconds(65), "01:05")
self.assertEqual(pb._convertSeconds(600), "10:00")
def test_progress_draws_eta_after_second_call(self):
captured = []
real = progress_mod.dataToStdout
progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data)
try:
pb = ProgressBar(0, 10, 78)
pb.progress(0) # first call only seeds the timer (eta None)
time.sleep(0.01) # let some wall-clock elapse so eta is computable
pb.progress(5) # second call computes and draws a real ETA
finally:
progress_mod.dataToStdout = real
self.assertTrue(captured, msg="progress() never wrote to stdout")
last = captured[-1]
# the drawn bar must carry an ETA token with an mm:ss timer (not the ??:?? placeholder)
self.assertIn("(ETA ", last, msg="no ETA token drawn: %r" % last)
self.assertNotIn("??:??", last, msg="ETA was not computed on the second call: %r" % last)
self.assertTrue(re.search(r"\(ETA \d{2}:\d{2}\)", last),
msg="ETA token missing an mm:ss timer: %r" % last)
if __name__ == "__main__":
unittest.main()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Secure directory purge (lib/utils/purge.py, the --purge feature): multi-pass
overwrite + truncation + removal of a directory's content. Driven against a
throwaway temp tree so the real output dir is never touched.
"""
import os
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
import lib.utils.purge as purge_mod
from lib.utils.purge import purge
class _RecordingLogger(object):
"""Captures every (level, message) emitted while installed as purge.logger."""
def __init__(self):
self.records = []
def _add(self, level):
return lambda msg, *a: self.records.append((level, msg % a if a else msg))
def __getattr__(self, name):
if name in ("warning", "info", "debug", "error", "critical"):
return self._add(name)
raise AttributeError(name)
def messages(self, level=None):
return [m for (lvl, m) in self.records if level is None or lvl == level]
class TestPurge(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="sqlmap_purge_")
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def test_overwrites_and_truncates_file_contents(self):
# a couple of files + a nested subdir with a (non-empty) file
plaintexts = {
os.path.join(self.tmp, "a.txt"): "secret data",
os.path.join(self.tmp, "sub", "b.txt"): "more secret data",
}
with open(os.path.join(self.tmp, "a.txt"), "w") as f:
f.write(plaintexts[os.path.join(self.tmp, "a.txt")])
with open(os.path.join(self.tmp, "empty.bin"), "w") as f:
pass
os.mkdir(os.path.join(self.tmp, "sub"))
with open(os.path.join(self.tmp, "sub", "b.txt"), "w") as f:
f.write(plaintexts[os.path.join(self.tmp, "sub", "b.txt")])
# neutralise the final rmtree so the overwrite/truncate work product remains
# observable on disk; the files are renamed, so locate them by walking the tree.
real_rmtree = purge_mod.shutil.rmtree
purge_mod.shutil.rmtree = lambda *a, **k: None
try:
purge(self.tmp)
finally:
purge_mod.shutil.rmtree = real_rmtree
# collect every surviving regular file (names are randomised by purge)
survivors = []
for root, _dirs, files in os.walk(self.tmp):
for name in files:
survivors.append(os.path.join(root, name))
# the originally non-empty files still exist (rmtree was a no-op) but the
# multi-pass overwrite + truncation reduced each to size 0 and the original
# plaintext is gone.
nonempty = [p for p in survivors if os.path.getsize(p) > 0]
self.assertEqual(nonempty, [], msg="files were not truncated to zero: %r" % nonempty)
blob = b"".join(open(p, "rb").read() for p in survivors)
for secret in plaintexts.values():
self.assertNotIn(secret.encode("utf-8"), blob,
msg="original plaintext %r survived the purge" % secret)
def test_purges_nested_content(self):
# full purge (including rmtree) wipes the whole tree
with open(os.path.join(self.tmp, "a.txt"), "w") as f:
f.write("secret data")
sub = os.path.join(self.tmp, "sub")
os.mkdir(sub)
with open(os.path.join(sub, "b.txt"), "w") as f:
f.write("more secret data")
purge(self.tmp)
self.assertFalse(os.path.exists(self.tmp))
def test_nonexistent_directory_is_noop(self):
missing = os.path.join(self.tmp, "does_not_exist")
real_logger = purge_mod.logger
rec = _RecordingLogger()
purge_mod.logger = rec
try:
# must not raise; the guard branch logs a skip warning and returns
purge(missing)
finally:
purge_mod.logger = real_logger
self.assertFalse(os.path.exists(missing))
self.assertTrue(
any("skipping purging" in w and "does not exist" in w for w in rec.messages("warning")),
msg="nonexistent-directory guard did not log its warning: %r" % rec.records,
)
if __name__ == "__main__":
unittest.main()
+475
View File
@@ -0,0 +1,475 @@
#!/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/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.
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).
"""
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
import plugins.generic.search as smod
import plugins.generic.entries as emod
from plugins.generic.search import Search
from plugins.generic.entries import Entries
class _RecordingDumper(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 _TestSearch(Search):
"""Search with the cross-mixin collaborators it relies on stubbed out.
The real Search lives in a multiple-inheritance hierarchy; in isolation we
must supply likeOrExact/forceDbmsEnum/getCurrentDb/getColumns/dumpFoundTables/
excludeDbsList, mirroring the inputs the production mixins would provide.
"""
excludeDbsList = ["information_schema", "mysql"]
def __init__(self):
Search.__init__(self)
self.like = ('1', " LIKE '%%%s%%'")
self.dumpFoundTablesCalls = []
self.dumpFoundColumnCalls = []
self.getColumnsCalls = []
self._cannedColumns = {}
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):
# Emulate column discovery by populating kb.data.cachedColumns for the
# currently-targeted conf.db/conf.tbl/conf.col, as the real plugin does.
self.getColumnsCalls.append((conf.db, conf.tbl, conf.col))
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 _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
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 = _RecordingDumper()
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 TestSearch(_SearchEnumBase):
# --- searchDb -----------------------------------------------------------
def test_search_db_found(self):
s = _TestSearch()
conf.db = "testdb"
# Feed identifiers that REQUIRE normalization: "select" is a reserved
# keyword and "weird db" contains a space; both force MySQL backtick
# quoting via safeSQLIdentificatorNaming. The plain "testdb" passes
# through unchanged. Asserting the quoted output proves the transform ran
# (a mock-echo would surface the raw, unquoted inputs instead).
smod.inject.getValue = lambda *a, **k: ["select", "weird db", "testdb"]
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][0], "found databases")
self.assertEqual(conf.dumper.listed[-1][1], ["`select`", "`weird db`", "testdb"])
def test_search_db_multiple_terms(self):
s = _TestSearch()
conf.db = "foo,bar"
# Return a DISTINCT value per search term by keying off the query string:
# each term is folded into the generated query (search_db inband query),
# so "foo" vs "bar" produce different queries. This proves the method
# actually iterates over both terms and records the right match for each,
# rather than appending the same constant twice.
def gv(query, *a, **k):
if "foo" in query:
return "foo_db"
elif "bar" in query:
return "bar_db"
return None
smod.inject.getValue = gv
s.searchDb()
# Two search terms => one distinct value found per term, in order.
self.assertEqual(conf.dumper.listed[-1][1], ["foo_db", "bar_db"])
def test_search_db_none_value(self):
s = _TestSearch()
conf.db = "nope"
smod.inject.getValue = lambda *a, **k: None
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][1], [])
def test_search_db_exclude_sys_dbs(self):
s = _TestSearch()
conf.db = "testdb"
conf.excludeSysDbs = True
seen = {}
def gv(query, *a, **k):
seen["query"] = query
return ["testdb"]
smod.inject.getValue = gv
s.searchDb()
# The exclusion clause for each system DB must be folded into the query.
self.assertIn("information_schema", seen["query"])
self.assertEqual(conf.dumper.listed[-1][1], ["testdb"])
# --- searchTable --------------------------------------------------------
def test_search_table_found_grouped_by_db(self):
s = _TestSearch()
conf.tbl = "users"
conf.db = None
smod.inject.getValue = lambda *a, **k: [["testdb", "users"], ["otherdb", "users"]]
s.searchTable()
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"], "otherdb": ["users"]})
# dumpFoundTables is invoked with the same mapping.
self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"], "otherdb": ["users"]})
def test_search_table_with_db_filter(self):
s = _TestSearch()
conf.tbl = "users"
conf.db = "testdb"
captured = {}
def gv(query, *a, **k):
captured["query"] = query
return [["testdb", "users"]]
smod.inject.getValue = gv
s.searchTable()
# conf.db present => a WHERE clause restricting to that db is appended.
self.assertIn("testdb", captured["query"])
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]})
def test_search_table_none_found(self):
s = _TestSearch()
conf.tbl = "ghost"
conf.db = None
smod.inject.getValue = lambda *a, **k: None
s.searchTable()
# No tables => dbTables/dumpFoundTables never called.
self.assertIsNone(conf.dumper.dbTablesArg)
self.assertEqual(s.dumpFoundTablesCalls, [])
# --- searchColumn -------------------------------------------------------
def test_search_column_db_and_tbl_provided(self):
s = _TestSearch()
conf.col = "password"
conf.db = "testdb"
conf.tbl = "users"
# With both db & tbl set, searchColumn does NOT call inject for table
# discovery; it assumes the provided db/tbl and calls getColumns.
smod.inject.getValue = lambda *a, **k: self.fail("getValue should not be called")
s.searchColumn()
self.assertEqual(conf.dumper.dbColumnsArg[1], '1') # colConsider
dbs = conf.dumper.dbColumnsArg[2]
self.assertIn("testdb", dbs)
self.assertIn("users", dbs["testdb"])
self.assertIn("password", dbs["testdb"]["users"])
self.assertEqual(s.dumpFoundColumnCalls[-1][2], '1')
# getColumns was consulted for the assumed db/tbl/col.
self.assertIn(("testdb", "users", "password"), s.getColumnsCalls)
def test_search_column_enumerate_tables(self):
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
# db & tbl missing => inject returns (db, table) pairs to scan.
smod.inject.getValue = lambda *a, **k: [["testdb", "users"]]
s.searchColumn()
dbs = conf.dumper.dbColumnsArg[2]
self.assertIn("testdb", dbs)
self.assertIn("users", dbs["testdb"])
# The requested column must have actually landed under the discovered
# db/table (getColumns populated kb.data.cachedColumns, which searchColumn
# folds into dbs); db/table presence alone wouldn't prove that.
self.assertIn("password", dbs["testdb"]["users"])
def test_search_column_none_found(self):
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
smod.inject.getValue = lambda *a, **k: None
s.searchColumn()
# Nothing discovered => dumper.dbColumns not called.
self.assertIsNone(conf.dumper.dbColumnsArg)
# --- search() dispatcher ------------------------------------------------
def test_search_dispatch_to_column(self):
s = _TestSearch()
conf.col = "password"
conf.tbl = "users"
conf.db = "testdb"
smod.inject.getValue = lambda *a, **k: None
# Should route to searchColumn (col takes precedence). With db & tbl both
# provided, searchColumn assumes them and consults getColumns for the
# requested db/tbl/col -> at least one recorded call, matching the request.
s.search()
self.assertGreaterEqual(len(s.getColumnsCalls), 1)
self.assertIn(("testdb", "users", "password"), s.getColumnsCalls)
def test_search_dispatch_missing_param(self):
s = _TestSearch()
conf.col = None
conf.tbl = None
conf.db = None
from lib.core.exception import SqlmapMissingMandatoryOptionException
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
# --- 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 = _TestEntries()
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 = _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"
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)
if __name__ == "__main__":
unittest.main()
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Tests for lib/utils/sgmllib.py -- the SGML/HTML parser used internally by
sqlmap for page content analysis. Exercises the parser with valid SGML/HTML
constructs and verifies the event stream.
"""
import contextlib
import io
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.utils.sgmllib import SGMLParser
class RecordingParser(SGMLParser):
"""SGMLParser subclass that records parse events AND delegates to parent."""
def __init__(self):
SGMLParser.__init__(self)
self.events = []
def _gather_data(self):
"""Extract concatenated text from data events."""
return "".join(body for ev in self.events if ev[0] == "data" for body in (ev[1],))
def handle_data(self, data):
self.events.append(("data", data))
def handle_comment(self, data):
self.events.append(("comment", data))
SGMLParser.handle_comment(self, data)
def handle_decl(self, decl):
self.events.append(("decl", decl))
def handle_pi(self, data):
self.events.append(("pi", data))
def handle_charref(self, name):
self.events.append(("charref", name))
SGMLParser.handle_charref(self, name) # do the actual conversion -> handle_data
def handle_entityref(self, name):
self.events.append(("entityref", name))
SGMLParser.handle_entityref(self, name) # do the actual conversion -> handle_data
def unknown_starttag(self, tag, attrs):
self.events.append(("start", tag, attrs))
def unknown_endtag(self, tag):
self.events.append(("end", tag))
def unknown_charref(self, ref):
self.events.append(("unknown_charref", ref))
def unknown_entityref(self, ref):
self.events.append(("unknown_entityref", ref))
class TestBasicParsing(unittest.TestCase):
def setUp(self):
self.p = RecordingParser()
def test_plain_text(self):
self.p.feed("hello world")
self.p.close()
self.assertEqual(self.p._gather_data(), "hello world")
def test_simple_start_and_end_tag(self):
self.p.feed("<p>text</p>")
self.p.close()
self.assertIn(("start", "p", []), self.p.events)
self.assertIn(("data", "text"), self.p.events)
self.assertIn(("end", "p"), self.p.events)
def test_nested_tags(self):
self.p.feed("<div><span>hello</span></div>")
self.p.close()
self.assertIn(("start", "div", []), self.p.events)
self.assertIn(("start", "span", []), self.p.events)
self.assertIn(("data", "hello"), self.p.events)
self.assertIn(("end", "span"), self.p.events)
self.assertIn(("end", "div"), self.p.events)
def test_sgml_shorttag(self):
# SGML shorthand: <tag/data/ == <tag>data</tag>
self.p.feed("<br/data/")
self.p.close()
self.assertIn(("start", "br", []), self.p.events)
self.assertIn(("data", "data"), self.p.events)
self.assertIn(("end", "br"), self.p.events)
def test_attributes(self):
self.p.feed('<a href="/page" class="link">click</a>')
self.p.close()
start_events = [e for e in self.p.events if e[0] == "start"]
self.assertEqual(len(start_events), 1)
tag, attrs = start_events[0][1], start_events[0][2]
self.assertEqual(tag, "a")
self.assertIn(("href", "/page"), attrs)
self.assertIn(("class", "link"), attrs)
def test_entity_reference(self):
self.p.feed("x &lt; y &amp; z")
self.p.close()
self.assertEqual(self.p._gather_data(), "x < y & z")
def test_known_entityref_event(self):
self.p.feed("&lt;")
self.p.close()
self.assertIn(("entityref", "lt"), self.p.events)
def test_numeric_charref(self):
self.p.feed("&#65;")
self.p.close()
self.assertEqual(self.p._gather_data(), "A")
def test_comment(self):
self.p.feed("a<!-- comment -->b")
self.p.close()
self.assertIn(("comment", " comment "), self.p.events)
self.assertEqual(self.p._gather_data(), "ab")
def test_doctype(self):
self.p.feed("<!DOCTYPE html>text")
self.p.close()
# The DOCTYPE must be reported as a declaration event (proving it was
# routed through parse_declaration, not mishandled as data) ...
self.assertIn(("decl", "DOCTYPE html"), self.p.events)
# ... and the trailing text must be the only data emitted.
self.assertEqual(self.p._gather_data(), "text")
def test_empty_input(self):
self.p.feed("")
self.p.close()
self.assertEqual(len(self.p.events), 0)
def test_feed_in_chunks(self):
for ch in "<p>abc</p>":
self.p.feed(ch)
self.p.close()
self.assertIn(("start", "p", []), self.p.events)
self.assertIn(("end", "p"), self.p.events)
self.assertEqual(self.p._gather_data(), "abc")
def test_multiple_feeds(self):
self.p.feed("<p>first</p>")
self.p.feed("<p>second</p>")
self.p.close()
starts = [e for e in self.p.events if e[0] == "start"]
self.assertEqual(len(starts), 2)
self.assertEqual(self.p._gather_data(), "firstsecond")
class TestEntityConversion(unittest.TestCase):
def test_convert_entityref_known(self):
p = SGMLParser()
self.assertEqual(p.convert_entityref("lt"), "<")
self.assertEqual(p.convert_entityref("gt"), ">")
self.assertEqual(p.convert_entityref("amp"), "&")
self.assertEqual(p.convert_entityref("quot"), '"')
self.assertEqual(p.convert_entityref("apos"), "'")
def test_convert_entityref_unknown(self):
p = SGMLParser()
self.assertIsNone(p.convert_entityref("unknown"))
def test_convert_charref_valid(self):
p = SGMLParser()
self.assertEqual(p.convert_charref("65"), "A")
self.assertEqual(p.convert_charref("97"), "a")
def test_convert_charref_invalid(self):
p = SGMLParser()
self.assertIsNone(p.convert_charref("notanumber"))
self.assertIsNone(p.convert_charref("9999")) # > 127
def test_convert_codepoint(self):
p = SGMLParser()
self.assertEqual(p.convert_codepoint(65), "A")
class TestCustomEntitydefs(unittest.TestCase):
def test_custom_entity(self):
p = RecordingParser()
p.entitydefs["copy"] = "\xa9"
p.feed("&copy;")
p.close()
self.assertEqual(p._gather_data(), "\xa9")
class TestGetStarttagText(unittest.TestCase):
def test_starttag_text(self):
p = RecordingParser()
p.feed("<div class='x'>text</div>")
p.close()
# get_starttag_text() must return the exact raw start-tag source,
# verbatim including the original quoting -- not a normalized form.
self.assertEqual(p.get_starttag_text(), "<div class='x'>")
class TestSetnomoretags(unittest.TestCase):
def test_nomoretags(self):
p = RecordingParser()
p.setnomoretags()
p.feed("<p>raw <b>text</b></p>")
p.close()
self.assertEqual(p._gather_data(), "<p>raw <b>text</b></p>")
class TestReset(unittest.TestCase):
def test_reset_clears_parser_state(self):
p = RecordingParser()
p.feed("<p>hello</p>")
# verify rawdata is cleared after close
self.assertEqual(p.rawdata, "")
p.reset()
self.assertEqual(p.stack, [])
self.assertEqual(p.lasttag, "???")
class TestVerbose(unittest.TestCase):
# In this parser, `verbose` only gates the debug printing emitted by
# report_unbalanced() (an unbalanced </tag> for which an end_<tag>
# handler exists). So a meaningful test must trigger that path and
# observe the difference on stdout.
class _Parser(SGMLParser):
def end_b(self):
pass
def _run(self, verbose):
p = self._Parser()
p.verbose = verbose
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
p.feed("</b>") # unbalanced end tag -> report_unbalanced()
p.close()
return buf.getvalue()
def test_verbose_mode_emits_debug(self):
out = self._run(1)
self.assertIn("*** Unbalanced </b>", out)
self.assertIn("*** Stack:", out)
def test_nonverbose_mode_is_silent(self):
self.assertEqual(self._run(0), "")
class TestSGMLParseError(unittest.TestCase):
def test_error_class(self):
from lib.utils.sgmllib import SGMLParseError
e = SGMLParseError("test")
self.assertIsInstance(e, RuntimeError)
if __name__ == "__main__":
unittest.main(verbosity=2)
+35 -5
View File
@@ -46,6 +46,20 @@ KNOWN_FRAGILE = set() # percentage/escapequotes empty/None crashes were FIXED b
# WAFs) -> ~6s/call. NOT a bug; excluded from execution to keep the unit suite fast.
HEAVY = {"luanginxmore"}
# Project contract for falsy input: tamper("") == "" and tamper(None) is None
# (the common idiom `if payload: ...; return payload` passes "" and None through unchanged).
# These tampers legitimately deviate for None: they initialize `retVal = ""` and only reassign
# it inside `if payload:`, so any falsy payload (both "" AND None) returns "" -- i.e. None is
# normalized to "" instead of being passed through. tamper("") == "" still holds for them.
NONE_RETURNS_EMPTY = {
"sp_password", # retVal = ""; reassigned only inside `if payload:`
"space2dash", # retVal = ""; reassigned only inside `if payload:`
"space2hash", # retVal = ""; reassigned only inside `if payload:`
"space2morehash", # retVal = ""; reassigned only inside `if payload:`
"space2mssqlhash", # retVal = ""; reassigned only inside `if payload:`
"space2mysqldash", # retVal = ""; reassigned only inside `if payload:`
}
class TestTamperRobustness(unittest.TestCase):
def test_no_crash_returns_string(self):
@@ -64,15 +78,31 @@ class TestTamperRobustness(unittest.TestCase):
class TestTamperEmptyNoneHandling(unittest.TestCase):
def test_graceful_on_empty_and_none(self):
# Assert the actual return contract on falsy input, not merely "does not raise":
# tamper("") == "" and tamper(None) is None
# (NONE_RETURNS_EMPTY tampers normalize None to "" -- see comment on that set.)
for name in TAMPERS:
if name in KNOWN_FRAGILE or name in HEAVY:
continue
mod = importlib.import_module("tamper.%s" % name)
for p in ("", None):
try:
mod.tamper(p)
except Exception as ex:
self.fail("tamper '%s' crashed on %r: %s" % (name, p, ex))
try:
empty_result = mod.tamper("")
except Exception as ex:
self.fail("tamper '%s' crashed on %r: %s" % (name, "", ex))
self.assertEqual(empty_result, "",
msg="tamper '%s' returned %r for '' (expected '')" % (name, empty_result))
try:
none_result = mod.tamper(None)
except Exception as ex:
self.fail("tamper '%s' crashed on %r: %s" % (name, None, ex))
if name in NONE_RETURNS_EMPTY:
self.assertEqual(none_result, "",
msg="tamper '%s' returned %r for None (expected '' per NONE_RETURNS_EMPTY)" % (name, none_result))
else:
self.assertIsNone(none_result,
msg="tamper '%s' returned %r for None (expected None)" % (name, none_result))
def test_previously_fragile_now_fixed(self):
# regression pin: percentage/escapequotes used to crash on empty/None; now must be graceful
+521
View File
@@ -0,0 +1,521 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Target-environment setup in lib/core/target.py.
target.py wires a single scan's per-host state together: it derives the output /
dump / files directories from the hostname, opens (and optionally flushes) the
HashDB session file, resumes a previously-fingerprinted DBMS/OS and stored kb
values out of that session, decides the custom injection marker, normalizes the
POST body (url-decode / base64), splits GET/POST/Cookie/header strings into the
testable paramDict, and restores the per-target merged options between targets.
None of that needs a live HTTP target or a real DBMS connection: every function
reads conf/kb globals (which are set up here per-test and restored in tearDown)
and at most touches the local filesystem (pointed at a private temp tree) or a
local SQLite session file. Those side-effecting paths are still pure with
respect to the network, so they are exercised here against real temp dirs.
All expected values below were probed from actual output, not assumed.
"""
import os
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import mergedOptions
from lib.core.data import paths
from lib.core.common import Backend
from lib.core.common import hashDBWrite
from lib.core.enums import HASHDB_KEYS
from lib.core.enums import HTTP_HEADER
from lib.core.enums import HTTPMETHOD
from lib.core.enums import PLACE
from lib.core.exception import SqlmapGenericException
from lib.core.exception import SqlmapNoneDataException
from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR
from lib.core.settings import RESTORE_MERGED_OPTIONS
from lib.core.settings import UNENCODED_ORIGINAL_VALUE
from lib.core.threads import getCurrentThreadData
from lib.utils.hashdb import HashDB
from lib.core.target import _createDumpDir
from lib.core.target import _createFilesDir
from lib.core.target import _createTargetDirs
from lib.core.target import _resumeDBMS
from lib.core.target import _resumeHashDBValues
from lib.core.target import _resumeOS
from lib.core.target import _restoreMergedOptions
from lib.core.target import _setAuxOptions
from lib.core.target import _setHashDB
from lib.core.target import _setRequestParams
from lib.core.target import _setResultsFile
from lib.core.target import initTargetEnv
SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad"
# conf/kb keys that the tests below mutate; saved in setUp, restored in tearDown so
# one test can never leak global state into another (or into the rest of the suite).
_CONF_KEYS = (
"direct", "parameters", "paramDict", "method", "data", "cookie", "httpHeaders",
"testParameter", "csrfToken", "url", "forms", "crawlDepth", "hostname", "path",
"port", "dbms", "os", "offline", "tmpPath", "technique", "dumpTable", "dumpAll",
"search", "fileRead", "commonFiles", "dumpPath", "filePath", "outputPath",
"hashDB", "hashDBFile", "sessionFile", "flushSession", "freshQueries",
"multipleTargets", "resultsFP", "resultsFile", "base64Parameter", "forceDbms",
)
_KB_KEYS = (
"processUserMarks", "postHint", "customInjectionMark", "testOnlyCustom",
"resumeValues", "aliasName", "errorChunkLength", "xpCmdshellAvailable",
"chars", "originalUrls", "postUrlEncode", "postSpaceToPlus",
)
_PATH_KEYS = ("SQLMAP_OUTPUT_PATH", "SQLMAP_DUMP_PATH", "SQLMAP_FILES_PATH")
class _TargetTestBase(unittest.TestCase):
"""Snapshot/restore conf, kb and paths globals around each test."""
def setUp(self):
self._conf = {k: conf.get(k) for k in _CONF_KEYS}
self._kb = {k: kb.get(k) for k in _KB_KEYS}
self._paths = {k: paths.get(k) for k in _PATH_KEYS}
# _resumeDBMS/_resumeOS mutate these kb fields via Backend.set* (not in _KB_KEYS)
self._dbms = kb.get("dbms")
self._forcedDbms = kb.get("forcedDbms")
self._tmpdirs = []
def tearDown(self):
# close any session DB we opened before restoring globals
if conf.get("hashDB"):
try:
conf.hashDB.close()
except Exception:
pass
getCurrentThreadData().hashDBCursor = None
if conf.get("resultsFP"):
try:
conf.resultsFP.close()
except Exception:
pass
for k, v in self._conf.items():
conf[k] = v
for k, v in self._kb.items():
kb[k] = v
for k, v in self._paths.items():
paths[k] = v
kb.dbms = self._dbms # _resumeDBMS may have set an identified DBMS
kb.forcedDbms = self._forcedDbms
for d in self._tmpdirs:
shutil.rmtree(d, ignore_errors=True)
def _outdir(self, name):
d = os.path.join(SCRATCH, name)
shutil.rmtree(d, ignore_errors=True)
self._tmpdirs.append(d)
paths.SQLMAP_OUTPUT_PATH = d
paths.SQLMAP_DUMP_PATH = os.path.join(d, "%s", "dump")
paths.SQLMAP_FILES_PATH = os.path.join(d, "%s", "files")
return d
def _new_hashdb(self):
handle, path = tempfile.mkstemp(suffix=".sqlite", dir=SCRATCH)
os.close(handle)
os.remove(path)
getCurrentThreadData().hashDBCursor = None
conf.hashDB = HashDB(path)
conf.hostname = "h"
conf.path = "/"
conf.port = 80
kb.resumeValues = True
conf.flushSession = False
conf.freshQueries = False
# another test file may have force-set a DBMS via set_dbms(); a leaked forcedDbms
# takes precedence in getIdentifiedDbms() and would mask what _resumeDBMS resolves
conf.forceDbms = None
kb.forcedDbms = None
kb.dbms = None
self.addCleanup(self._cleanup_hashdb, path)
return path
def _cleanup_hashdb(self, path):
for f in (path, path + "-wal", path + "-shm"):
if os.path.exists(f):
try:
os.remove(f)
except OSError:
pass
class TestRestoreMergedOptions(_TargetTestBase):
def test_restores_each_option_from_mergedOptions(self):
saved = {}
for opt in RESTORE_MERGED_OPTIONS:
saved[opt] = mergedOptions.get(opt)
mergedOptions[opt] = "VAL_%s" % opt
conf[opt] = "tampered"
try:
_restoreMergedOptions()
for opt in RESTORE_MERGED_OPTIONS:
self.assertEqual(conf[opt], "VAL_%s" % opt,
msg="option %r not restored from mergedOptions" % opt)
finally:
for opt, v in saved.items():
mergedOptions[opt] = v
class TestSetAuxOptions(_TargetTestBase):
def test_alias_is_nonempty_string(self):
conf.hostname = "example.com"
_setAuxOptions()
self.assertIsInstance(kb.aliasName, str)
self.assertTrue(kb.aliasName)
def test_alias_deterministic_for_same_host(self):
conf.hostname = "example.com"
_setAuxOptions()
first = kb.aliasName
_setAuxOptions()
self.assertEqual(kb.aliasName, first)
def test_alias_handles_none_host(self):
conf.hostname = None
_setAuxOptions() # seed=hash("") must not raise
self.assertIsInstance(kb.aliasName, str)
class TestInitTargetEnv(_TargetTestBase):
def _base(self):
conf.url = "http://h/?id=1"
conf.data = None
conf.httpHeaders = []
conf.base64Parameter = None
conf.multipleTargets = False
def test_default_injection_marker(self):
self._base()
initTargetEnv()
self.assertEqual(kb.customInjectionMark, CUSTOM_INJECTION_MARK_CHAR)
def test_inject_here_marker_detected(self):
self._base()
conf.url = "http://h/?id=%INJECT_HERE%"
initTargetEnv()
self.assertEqual(kb.customInjectionMark, "%INJECT_HERE%")
def test_urlencoded_post_body_is_decoded(self):
self._base()
conf.url = "http://h/"
conf.data = "id=a%20b"
conf.httpHeaders = [("Content-Type", "application/x-www-form-urlencoded")]
initTargetEnv()
self.assertTrue(kb.postUrlEncode)
self.assertEqual(str(conf.data), "id=a b")
# the raw (still-encoded) original is preserved as an attribute for later re-encoding
self.assertEqual(getattr(conf.data, UNENCODED_ORIGINAL_VALUE, None), "id=a%20b")
def test_non_urlencoded_content_type_skips_decode(self):
self._base()
conf.url = "http://h/"
conf.data = "id=a%20b"
conf.httpHeaders = [("Content-Type", "application/json")]
conf.base64Parameter = None
initTargetEnv()
self.assertFalse(kb.postUrlEncode)
def test_base64_post_body_is_decoded(self):
self._base()
conf.url = "http://h/"
conf.data = "aWQ9MQ==" # base64 of "id=1"
conf.httpHeaders = [("Content-Type", "application/json")]
conf.base64Parameter = "POST"
initTargetEnv()
self.assertEqual(str(conf.data), "id=1")
self.assertEqual(getattr(conf.data, UNENCODED_ORIGINAL_VALUE, None), "aWQ9MQ==")
class TestSetRequestParams(_TargetTestBase):
def _fresh(self):
conf.direct = None
conf.parameters = {}
conf.paramDict = {}
conf.method = HTTPMETHOD.GET
conf.data = None
conf.cookie = None
conf.httpHeaders = []
conf.testParameter = None
conf.csrfToken = None
conf.url = "http://h/"
conf.forms = False
conf.crawlDepth = None
kb.processUserMarks = None
kb.postHint = None
kb.customInjectionMark = "*"
kb.testOnlyCustom = False
def test_direct_connection_shortcut(self):
self._fresh()
conf.direct = "mysql://u:p@h/db"
conf.parameters = {}
_setRequestParams()
self.assertEqual(conf.parameters[None], "direct connection")
def test_get_parameters_split(self):
self._fresh()
conf.parameters = {PLACE.GET: "id=1&name=foo"}
conf.url = "http://h/?id=1&name=foo"
_setRequestParams()
self.assertEqual(dict(conf.paramDict[PLACE.GET]), {"id": "1", "name": "foo"})
def test_post_parameters_split(self):
self._fresh()
conf.method = HTTPMETHOD.POST
conf.data = "a=1&b=2"
_setRequestParams()
self.assertEqual(dict(conf.paramDict[PLACE.POST]), {"a": "1", "b": "2"})
def test_cookie_parameters_split(self):
self._fresh()
conf.parameters = {PLACE.GET: "id=1"}
conf.url = "http://h/?id=1"
conf.cookie = "sess=abc; uid=5"
_setRequestParams()
self.assertIn(PLACE.COOKIE, conf.paramDict)
self.assertEqual(dict(conf.paramDict[PLACE.COOKIE]), {"sess": "abc", "uid": "5"})
def test_user_agent_header_is_testable(self):
self._fresh()
conf.httpHeaders = [(HTTP_HEADER.USER_AGENT, "Mozilla")]
_setRequestParams()
self.assertIn(PLACE.USER_AGENT, conf.paramDict)
def test_referer_header_is_testable(self):
self._fresh()
conf.httpHeaders = [(HTTP_HEADER.REFERER, "http://ref/")]
_setRequestParams()
self.assertIn(PLACE.REFERER, conf.paramDict)
def test_no_parameters_raises(self):
self._fresh()
with self.assertRaises(SqlmapGenericException):
_setRequestParams()
def test_empty_post_body_defaults_to_empty_string(self):
self._fresh()
conf.method = HTTPMETHOD.POST
conf.data = None
conf.parameters = {PLACE.GET: "id=1"} # keep a testable param so it doesn't raise
conf.url = "http://h/?id=1"
_setRequestParams()
self.assertEqual(conf.data, "")
class TestResumeDBMS(_TargetTestBase):
def test_resumes_dbms_with_version(self):
self._new_hashdb()
conf.dbms = None
conf.offline = False
hashDBWrite(HASHDB_KEYS.DBMS, "MySQL 5.0")
_resumeDBMS()
self.assertEqual(Backend.getIdentifiedDbms(), "MySQL")
def test_no_stored_dbms_returns_quietly(self):
self._new_hashdb()
conf.dbms = None
conf.offline = False
_resumeDBMS() # nothing stored: must just return
# the quiet-return branch must leave NO DBMS identified (a real
# side-effect assertion, not merely "did not raise")
self.assertIsNone(Backend.getIdentifiedDbms())
def test_offline_without_session_raises(self):
self._new_hashdb()
conf.dbms = None
conf.offline = True
with self.assertRaises(SqlmapNoneDataException):
_resumeDBMS()
class TestResumeOS(_TargetTestBase):
def test_resumes_os(self):
self._new_hashdb()
conf.os = None
hashDBWrite(HASHDB_KEYS.OS, "Linux")
_resumeOS()
self.assertEqual(conf.os, "Linux")
def test_no_stored_os_returns_quietly(self):
self._new_hashdb()
conf.os = None
_resumeOS()
self.assertIsNone(conf.os)
def test_stored_none_string_is_ignored(self):
self._new_hashdb()
conf.os = None
hashDBWrite(HASHDB_KEYS.OS, "None")
_resumeOS()
self.assertIsNone(conf.os)
class TestResumeHashDBValues(_TargetTestBase):
def _base(self):
self._new_hashdb()
conf.dbms = None
conf.offline = False
conf.os = None
conf.tmpPath = None
conf.technique = None
conf.paramDict = {}
def test_resumes_serialized_chars(self):
self._base()
kb.chars = None
hashDBWrite(HASHDB_KEYS.KB_CHARS, {"a": 1}, serialize=True)
_resumeHashDBValues()
self.assertEqual(kb.chars, {"a": 1})
def test_resumes_numeric_error_chunk_length(self):
self._base()
kb.errorChunkLength = None
hashDBWrite(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH, "5")
_resumeHashDBValues()
self.assertEqual(kb.errorChunkLength, 5)
def test_non_numeric_chunk_length_becomes_none(self):
self._base()
kb.errorChunkLength = 99
hashDBWrite(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH, "notanumber")
_resumeHashDBValues()
self.assertIsNone(kb.errorChunkLength)
def test_xp_cmdshell_true_coerced_to_bool(self):
self._base()
kb.xpCmdshellAvailable = False
hashDBWrite(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE, str(True))
_resumeHashDBValues()
self.assertIs(kb.xpCmdshellAvailable, True)
class TestSetHashDB(_TargetTestBase):
def test_derives_session_file_under_output_path(self):
out = self._outdir("hdb_out")
os.makedirs(out)
getCurrentThreadData().hashDBCursor = None
conf.hashDBFile = None
conf.sessionFile = None
conf.outputPath = out
conf.flushSession = False
conf.hashDB = None
_setHashDB()
self.assertTrue(conf.hashDBFile.startswith(out))
self.assertIsInstance(conf.hashDB, HashDB)
def test_explicit_session_file_takes_precedence(self):
out = self._outdir("hdb_out2")
os.makedirs(out)
sess = os.path.join(out, "custom.sqlite")
getCurrentThreadData().hashDBCursor = None
conf.hashDBFile = None
conf.sessionFile = sess
conf.outputPath = out
conf.flushSession = False
conf.hashDB = None
_setHashDB()
self.assertEqual(conf.hashDBFile, sess)
class TestCreateDirs(_TargetTestBase):
def test_dump_dir_skipped_without_dump_flags(self):
self._outdir("d_out")
conf.hostname = "example.com"
conf.dumpPath = None
conf.dumpTable = False
conf.dumpAll = False
conf.search = False
_createDumpDir()
self.assertIsNone(conf.dumpPath)
def test_dump_dir_created_per_host(self):
self._outdir("d_out2")
conf.hostname = "example.com"
conf.dumpTable = True
conf.dumpAll = False
conf.search = False
_createDumpDir()
self.assertTrue(os.path.isdir(conf.dumpPath))
self.assertIn("example.com", conf.dumpPath)
def test_files_dir_skipped_without_file_flags(self):
self._outdir("f_out")
conf.hostname = "example.com"
conf.filePath = None
conf.fileRead = None
conf.commonFiles = None
_createFilesDir()
self.assertIsNone(conf.filePath)
def test_files_dir_created_per_host(self):
self._outdir("f_out2")
conf.hostname = "example.com"
conf.fileRead = "/etc/passwd"
conf.commonFiles = None
_createFilesDir()
self.assertTrue(os.path.isdir(conf.filePath))
self.assertIn("example.com", conf.filePath)
def test_target_dir_and_target_txt(self):
out = self._outdir("t_out")
conf.hostname = "example.com"
conf.url = "http://example.com/?id=1"
conf.data = None
conf.dumpTable = False
conf.dumpAll = False
conf.search = False
conf.fileRead = None
conf.commonFiles = None
kb.originalUrls = {}
_createTargetDirs()
self.assertTrue(os.path.isdir(conf.outputPath))
target = os.path.join(conf.outputPath, "target.txt")
self.assertTrue(os.path.exists(target))
with open(target) as f:
content = f.read()
self.assertIn("http://example.com/?id=1", content)
self.assertIn("(%s)" % HTTPMETHOD.GET, content)
class TestSetResultsFile(_TargetTestBase):
def test_skipped_when_not_multiple_targets(self):
self._outdir("r_out")
conf.multipleTargets = False
conf.resultsFP = None
_setResultsFile()
self.assertIsNone(conf.resultsFP)
def test_creates_csv_with_header_in_multiple_target_mode(self):
out = self._outdir("r_out2")
os.makedirs(out)
conf.multipleTargets = True
conf.resultsFile = os.path.join(out, "res.csv")
conf.resultsFP = None
_setResultsFile()
self.assertTrue(os.path.exists(conf.resultsFile))
conf.resultsFP.flush()
with open(conf.resultsFile) as f:
header = f.readline()
self.assertIn("Target URL", header)
self.assertIn("Parameter", header)
if __name__ == "__main__":
unittest.main(verbosity=2)
+769
View File
@@ -0,0 +1,769 @@
#!/usr/bin/env python
"""
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:
* lib/techniques/union/use.py - _oneShotUnionUse value extraction + unionUse
* lib/techniques/ldap/inject.py - boolean-blind LDAP oracle + blind char inference
* lib/techniques/graphql/inject.py - schema walk, query building, blind-SQLi inference
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,
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 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, PLACE
from lib.core.agent import agent
from lib.core.unescaper import unescaper
from lib.request.connect import Connect as Request
import lib.techniques.union.use as uu
import lib.techniques.ldap.inject as ldap
import lib.techniques.graphql.inject as gql
# ===========================================================================
# UNION: lib/techniques/union/use.py
# ===========================================================================
# A UNION injection vector is a tuple consumed positionally by _oneShotUnionUse /
# forgeUnionQuery (vector[0..10]). The exact contents do not matter here because the
# forge chain is stubbed to a pass-through; only the indexes the function itself reads
# (7=unionDuplicates, 8=forcePartialUnion, 9=tableFrom, 10=unionTemplate) carry meaning.
_UNION_VECTOR = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None)
_UU_CONF = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None,
"forcePartial": False, "disableJson": False, "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}
def _wrap(start, body, stop=None):
"""Wrap a value in the current UNION markers, exactly as the target page would."""
return "%s%s%s" % (start, body, stop if stop is not None else kb.chars.stop)
class _UnionCase(unittest.TestCase):
"""Base: stub the forge/escape/transport seam so _oneShotUnionUse's OWN parsing
(marker extraction, hashDB caching, json-agg, trimming, retry) is what is exercised."""
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._s_forge = agent.forgeUnionQuery
self._s_concat = agent.concatQuery
self._s_payload = agent.payload
self._s_escape = unescaper.escape
for k, v in _UU_CONF.items():
conf[k] = v
for k, v in _UU_KB.items():
kb[k] = v
kb.counters = {}
conf.hashDB = None # disable session resume in these tests
# minimal injection context the function reads
entry = AttribDict()
entry.vector = _UNION_VECTOR
entry.where = PAYLOAD.WHERE.ORIGINAL
kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry}
# pass-through forge chain: the produced payload text is irrelevant - the mock
# oracle answers from the EXPRESSION recorded out-of-band, not from the payload
agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED"
agent.concatQuery = lambda expression, unpack=True: expression
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD"
unescaper.escape = lambda expression, *a, **k: expression
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
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
def _install_page(self, page):
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)
class TestOneShotUnionUse(_UnionCase):
def test_single_value_extracted(self):
page = "<html>%s</html>" % _wrap(kb.chars.start, "hello")
self._install_page(page)
self.assertEqual(uu._oneShotUnionUse("SELECT a"), _wrap(kb.chars.start, "hello"))
def test_multi_column_delimited(self):
body = kb.chars.delimiter.join(("u", "p"))
page = "x %s y" % _wrap(kb.chars.start, body)
self._install_page(page)
retVal = uu._oneShotUnionUse("SELECT u,p")
self.assertIn("u%sp" % kb.chars.delimiter, retVal)
def test_no_markers_returns_none(self):
self._install_page("<html>nothing useful here</html>")
self.assertIsNone(uu._oneShotUnionUse("SELECT a"))
def test_counter_incremented(self):
self._install_page(_wrap(kb.chars.start, "v"))
uu._oneShotUnionUse("SELECT a")
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1)
def test_last_char_trim_patched(self):
# the page carries chars.stop with its final char trimmed; the engine repairs it
trimmed = kb.chars.stop[:-1]
page = "%s%s%s" % (kb.chars.start, "data", trimmed)
self._install_page(page)
retVal = uu._oneShotUnionUse("SELECT a")
self.assertEqual(retVal, _wrap(kb.chars.start, "data"))
def test_upper_cased_results_lowered(self):
# force-uppercased response: function lower-cases the whole page before parsing
page = ("PREFIX %s" % _wrap(kb.chars.start, "value")).upper()
self._install_page(page)
retVal = uu._oneShotUnionUse("SELECT a")
self.assertEqual(retVal, _wrap(kb.chars.start, "value").lower())
def test_order_by_retry_without_clause(self):
# first try (with ORDER BY) yields nothing; the engine retries stripping ORDER BY.
# both expressions feed the same stubbed oracle, so we vary the page by call count.
state = {"calls": 0}
def oracle(payload=None, content=False, raise404=False, **kwargs):
state["calls"] += 1
page = "" if state["calls"] == 1 else _wrap(kb.chars.start, "recovered")
return (page, AttribDict(), 200) if content else page
Request.queryPage = staticmethod(oracle)
uu.Request.queryPage = staticmethod(oracle)
retVal = uu._oneShotUnionUse("SELECT a ORDER BY 1")
self.assertEqual(retVal, _wrap(kb.chars.start, "recovered"))
self.assertEqual(state["calls"], 2)
def test_hashdb_resume_short_circuits(self):
# a cached value is returned without ever touching the oracle
import tempfile
from lib.utils.hashdb import HashDB
from lib.core.common import hashDBWrite
fd, path = tempfile.mkstemp(suffix=".sqlite")
os.close(fd)
os.remove(path)
saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port"))
try:
conf.hashDB = HashDB(path)
conf.hostname, conf.path, conf.port = "union.invalid", "/", 80
hashDBWrite("%s%s" % (conf.hexConvert or False, "SELECT cached"), "CACHED-UNION")
conf.hashDB.flush()
def boom(*a, **k):
raise AssertionError("oracle must not be called on a cache hit")
Request.queryPage = staticmethod(boom)
uu.Request.queryPage = staticmethod(boom)
self.assertEqual(uu._oneShotUnionUse("SELECT cached"), "CACHED-UNION")
finally:
conf.hostname, conf.path, conf.port = saved_loc
try:
conf.hashDB.closeAll()
except Exception:
pass
if os.path.exists(path):
os.remove(path)
class TestJsonAggExtraction(_UnionCase):
"""kb.jsonAggMode path: the page carries a JSON array between the markers (MySQL branch)."""
def setUp(self):
_UnionCase.setUp(self)
kb.jsonAggMode = True
def test_json_array_rows_wrapped(self):
# MySQL non-MSSQL/PGSQL branch: json.loads(output) over a JSON-array body, each row
# re-wrapped in start/stop markers so parseUnionPage can later split it
import json
body = json.dumps(["alice", "bob"])
page = "%s%s%s" % (kb.chars.start, body, kb.chars.stop)
self._install_page(page)
retVal = uu._oneShotUnionUse("SELECT name FROM users", False)
self.assertIn("alice", retVal)
self.assertIn("bob", retVal)
self.assertEqual(retVal.count(kb.chars.start), 2)
def test_truncated_aggregate_sets_flag(self):
# leading marker present but no trailing marker -> single-shot considered truncated
page = "%sincomplete-json-array-no-stop" % kb.chars.start
self._install_page(page)
retVal = uu._oneShotUnionUse("SELECT name FROM users", False)
self.assertIsNone(retVal)
self.assertTrue(kb.respTruncated)
class TestUnionUse(_UnionCase):
"""unionUse() orchestration over the (stubbed) one-shot path. set_dbms forced to a DBMS
NOT in FROM_DUMMY_TABLE and a scalar (no FROM) expression so the partial/limit/json-agg
branches are skipped and it falls through to the single one-shot extraction + parse."""
def setUp(self):
_UnionCase.setUp(self)
set_dbms("MySQL")
# initTechnique() only does session/template bookkeeping (page template, match ratio,
# resumed conf) irrelevant to the extraction under test, and needs a full injection
# session to run; stub it so unionUse()'s orchestration + parse is what is exercised.
self._s_initTechnique = uu.initTechnique
uu.initTechnique = lambda technique=None: None
# unionUse() calls getConsoleWidth(); with no tty (test runner) it falls back to
# curses.initscr(), which flips the terminal to the alternate screen. Pin COLUMNS
# so that path is never taken and the runner output stays clean.
self._s_columns = os.environ.get("COLUMNS")
os.environ["COLUMNS"] = "80"
def tearDown(self):
if self._s_columns is None:
os.environ.pop("COLUMNS", None)
else:
os.environ["COLUMNS"] = self._s_columns
uu.initTechnique = self._s_initTechnique
_UnionCase.tearDown(self)
def test_scalar_value(self):
self._install_page(_wrap(kb.chars.start, "scalar-result"))
value = uu.unionUse("SELECT 1")
self.assertEqual(value, "scalar-result")
def test_scalar_empty(self):
self._install_page("<html>no markers</html>")
value = uu.unionUse("SELECT 1")
self.assertIsNone(value)
# ===========================================================================
# LDAP: lib/techniques/ldap/inject.py
# ===========================================================================
class TestLdapPureHelpers(unittest.TestCase):
def test_ratio(self):
self.assertEqual(ldap._ratio("abc", "abc"), 1.0)
self.assertLess(ldap._ratio("hello", "zzzzz"), 0.5)
self.assertEqual(ldap._ratio(None, None), 1.0)
def test_ldap_literal_escapes_metachars(self):
self.assertEqual(ldap._ldapLiteral("a*b(c)"), "a\\2ab\\28c\\29")
def test_ldap_literal_backslash(self):
self.assertEqual(ldap._ldapLiteral("a\\b"), "a\\5cb")
def test_transport_encode(self):
self.assertEqual(ldap._transportEncode("a b&c=d"), "a%20b%26c%3Dd")
def test_is_password_param(self):
self.assertTrue(ldap._isPasswordParam("password"))
self.assertTrue(ldap._isPasswordParam("userPwd"))
self.assertTrue(ldap._isPasswordParam("auth_token"))
self.assertFalse(ldap._isPasswordParam("username"))
self.assertFalse(ldap._isPasswordParam(None))
def test_is_error(self):
self.assertTrue(ldap._isError("LdapErr: DSID-0123ABCD"))
self.assertTrue(ldap._isError("Invalid DN syntax (34)"))
self.assertFalse(ldap._isError("everything is fine"))
def test_backend_from_error(self):
self.assertEqual(ldap._backendFromError("LdapErr: DSID-0AB12345 problem"),
"Microsoft Active Directory")
# a generic LDAP error that matches the umbrella regex but no specific signature
self.assertEqual(ldap._backendFromError("Invalid DN syntax (34)"), "OpenLDAP")
self.assertIsNone(ldap._backendFromError("no error at all"))
def test_fingerprint_by_error(self):
self.assertEqual(ldap._fingerprintByError("Microsoft Active Directory"),
"Microsoft Active Directory")
self.assertEqual(ldap._fingerprintByError("OpenLDAP"), "OpenLDAP")
self.assertEqual(ldap._fingerprintByError("ApacheDS"), "ApacheDS")
self.assertEqual(ldap._fingerprintByError("389 Directory Server"),
"389 Directory Server")
self.assertIsNone(ldap._fingerprintByError(None))
def test_grid_renders_table(self):
grid = ldap._grid(["a", "bb"], [["1", "2"], ["33", "4"]])
self.assertIn("| a | bb |", grid)
self.assertIn("| 33 | 4 |", grid)
# header + 2 rows + 4 separators (top, under-header, ... actually 3 borders + n rows)
self.assertEqual(grid.count("+----+----+"), 3)
def test_charset_excludes_metachars(self):
for meta in ("*", "(", ")", "\\"):
self.assertNotIn(ord(meta), ldap._CHARSET)
self.assertIn(ord("a"), ldap._CHARSET)
self.assertIn(ord("0"), ldap._CHARSET)
def test_probe_builder_shapes(self):
b = ldap._ProbeBuilder("*)")
self.assertTrue(b.presence("uid").endswith("(uid=*"))
self.assertIn("(cn=adm*", b.prefix("cn", "adm"))
# compound probe closes its own (&...) and opens a suffix-eater
compound = b.presence("uid", constraint=("ou", "people"))
self.assertIn("(ou=people)", compound)
self.assertIn("(objectClass=", compound)
def test_probe_builder_default_breakout(self):
b = ldap._ProbeBuilder(None)
self.assertEqual(b.breakout, ")")
class _LdapOracleCase(unittest.TestCase):
"""Drive the real boolean oracle + blind inference against an in-process directory.
The _send seam is replaced by a function that simulates an LDAP-to-application filter
match: a payload's trailing assertion '(attr=value*' matches when the directory holds
`attr` whose value starts with `value`."""
DIRECTORY = {"uid": "admin", "mail": "bob", "cn": "Administrator"}
def setUp(self):
self._sparams = conf.get("parameters")
self._spdict = conf.get("paramDict")
self._scookiedel = conf.get("cookieDel")
self._ssend = ldap._send
conf.parameters = {PLACE.GET: "user=admin"}
conf.paramDict = {PLACE.GET: {"user": "admin"}}
conf.cookieDel = None
directory = self.DIRECTORY
sentinel = ldap.SENTINEL
def fake_send(place, parameter, value):
assertions = re.findall(r"\((\w+)=([^()]*)", value)
if not assertions:
return "FALSE-PAGE-baseline-content"
attr, pat = assertions[-1]
pat = pat.rstrip("*")
if attr in directory and directory[attr].startswith(pat):
return "TRUE-CONTENT-stable-match-%s" % attr
return "FALSE-PAGE-baseline-content"
ldap._send = fake_send
def tearDown(self):
conf.parameters = self._sparams
conf.paramDict = self._spdict
conf.cookieDel = self._scookiedel
ldap._send = self._ssend
class TestLdapParamSegment(_LdapOracleCase):
def test_original_value(self):
self.assertEqual(ldap._originalValue(PLACE.GET, "user"), "admin")
def test_original_value_from_paramdict_fallback(self):
self.assertEqual(ldap._originalValue(PLACE.GET, "missing"), "")
def test_replace_segment(self):
self.assertEqual(ldap._replaceSegment(PLACE.GET, "user", "XYZ"), "user=XYZ")
class TestLdapOracle(_LdapOracleCase):
def _oracle(self):
return ldap._makeOracle(PLACE.GET, "user", "TRUE-CONTENT-stable-match-uid")
def test_exists_true(self):
oracle, builder = self._oracle(), ldap._ProbeBuilder(")")
self.assertTrue(ldap._exists(oracle, builder, "uid"))
def test_exists_false(self):
oracle, builder = self._oracle(), ldap._ProbeBuilder(")")
self.assertFalse(ldap._exists(oracle, builder, "nonexistent"))
def test_infer_attribute_uid(self):
oracle, builder = self._oracle(), ldap._ProbeBuilder(")")
self.assertEqual(ldap._inferAttribute(oracle, builder, "uid"), "admin")
def test_infer_attribute_mail(self):
oracle, builder = self._oracle(), ldap._ProbeBuilder(")")
self.assertEqual(ldap._inferAttribute(oracle, builder, "mail"), "bob")
def test_infer_attribute_missing_none(self):
oracle, builder = self._oracle(), ldap._ProbeBuilder(")")
self.assertIsNone(ldap._inferAttribute(oracle, builder, "zzz"))
def test_enumerate_entry_keys(self):
oracle, builder = self._oracle(), ldap._ProbeBuilder(")")
keyAttr, values = ldap._enumerateEntryKeys(oracle, builder)
self.assertEqual(keyAttr, "uid")
self.assertEqual(values, ["admin"])
class TestLdapBoolean(_LdapOracleCase):
def test_boolean_divergent_returns_true_page(self):
page = ldap._boolean(lambda: "TRUE-STABLE-CONTENT-HERE",
lambda: "FALSE-DIFFERENT-PAGE-XX")
self.assertEqual(page, "TRUE-STABLE-CONTENT-HERE")
def test_boolean_identical_returns_none(self):
self.assertIsNone(ldap._boolean(lambda: "SAME-PAGE", lambda: "SAME-PAGE"))
def test_boolean_error_true_returns_none(self):
self.assertIsNone(ldap._boolean(lambda: "Invalid DN syntax (34)",
lambda: "anything"))
def test_detect_boolean_finds_tautology(self):
# the fake oracle returns a stable TRUE page for any tautology assertion
# '(objectClass=*' / '(uid=*' / '(cn=*' and a distinct FALSE page for SENTINEL
template, payload, breakout = ldap._detectBoolean(PLACE.GET, "user")
self.assertIsNotNone(template)
self.assertIsNotNone(breakout)
self.assertIn("=*", payload)
# ===========================================================================
# GraphQL: lib/techniques/graphql/inject.py
# ===========================================================================
class TestGraphqlPureHelpers(unittest.TestCase):
def test_unwrap_type_chain(self):
t = {"kind": "NON_NULL", "name": None,
"ofType": {"kind": "LIST", "name": None,
"ofType": {"kind": "SCALAR", "name": "String"}}}
self.assertEqual(gql._unwrapType(t),
[("NON_NULL", None), ("LIST", None), ("SCALAR", "String")])
def test_unwrap_type_depth_guard(self):
# malformed / non-dict terminates without recursion error
self.assertEqual(gql._unwrapType("notadict"), [])
def test_leaf_name(self):
chain = [("NON_NULL", None), ("SCALAR", "Int")]
self.assertEqual(gql._leafName(chain), "Int")
self.assertIsNone(gql._leafName([("LIST", None)]))
def test_classify_arg(self):
self.assertEqual(gql._classifyArg({"kind": "SCALAR", "name": "String"}), "string")
self.assertEqual(gql._classifyArg({"kind": "SCALAR", "name": "Int"}), "numeric")
self.assertEqual(gql._classifyArg({"kind": "SCALAR", "name": "ID"}), "id_dual")
self.assertIsNone(gql._classifyArg({"kind": "SCALAR", "name": "DateTime"}))
def test_escape_graphql_string(self):
self.assertEqual(gql._escapeGraphQLString('a"b\\c'), 'a\\"b\\\\c')
self.assertEqual(gql._escapeGraphQLString("a\nb"), "a\\nb")
def test_cell(self):
self.assertEqual(gql._cell(None), "NULL")
self.assertEqual(gql._cell({"b": 1, "a": 2}), '{"a": 2, "b": 1}')
self.assertEqual(gql._cell("plain"), "plain")
self.assertEqual(gql._cell(7), "7")
def test_chunks(self):
self.assertEqual(list(gql._chunks([1, 2, 3, 4, 5], 2)), [[1, 2], [3, 4], [5]])
def test_render_arg(self):
self.assertEqual(gql._renderArg("id", "5", "numeric"), "id:5")
self.assertEqual(gql._renderArg("n", "hi", "string"), 'n:"hi"')
self.assertEqual(gql._renderArg("id", "9", "id_dual"), "id:9") # digit -> bare
self.assertEqual(gql._renderArg("id", "ab", "id_dual"), 'id:"ab"') # non-digit -> quoted
def test_render_type_str(self):
self.assertEqual(gql._renderTypeStr(gql._unwrapType(
{"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String"}})), "String!")
self.assertEqual(gql._renderTypeStr(gql._unwrapType(
{"kind": "LIST", "name": None, "ofType": {"kind": "OBJECT", "name": "User"}})), "[User]")
def test_parse_json(self):
self.assertEqual(gql._parseJSON('{"a": 1}'), {"a": 1})
self.assertIsNone(gql._parseJSON("not json"))
self.assertIsNone(gql._parseJSON(""))
def test_is_graphql_response(self):
self.assertTrue(gql._isGraphQLResponse('{"data": {"__typename": "Query"}}'))
self.assertFalse(gql._isGraphQLResponse('{"data": {"id": 1}}'))
self.assertFalse(gql._isGraphQLResponse("[]"))
def test_error_text(self):
page = '{"errors": [{"message": "boom", "extensions": {"code": "BAD"}}]}'
text = gql._errorText(page)
self.assertIn("boom", text)
self.assertIn("BAD", text)
self.assertEqual(gql._errorText("{}"), "")
def test_slot_value(self):
self.assertEqual(gql._slotValue('{"data": {"f": {"x": 1}}}'), '{"x": 1}')
# non-graphql passes through unchanged
self.assertEqual(gql._slotValue("raw"), "raw")
def test_default_for_arg(self):
self.assertEqual(gql._defaultForArg({"kind": "SCALAR", "name": "Int"}, None), 0)
self.assertEqual(gql._defaultForArg({"kind": "SCALAR", "name": "String"}, None), "x")
self.assertEqual(gql._defaultForArg({"kind": "SCALAR", "name": "String"}, "given"), "given")
# A minimal but realistic introspection schema: query user(id: String, limit: Int): User
_GQL_SCHEMA = {
"queryType": {"name": "Query"},
"mutationType": {"name": "Mutation"},
"types": [
{"kind": "OBJECT", "name": "Query", "fields": [
{"name": "user", "args": [
{"name": "id", "type": {"kind": "SCALAR", "name": "String"}, "defaultValue": None},
{"name": "limit", "type": {"kind": "SCALAR", "name": "Int"}, "defaultValue": None},
], "type": {"kind": "OBJECT", "name": "User"}},
]},
{"kind": "OBJECT", "name": "Mutation", "fields": [
{"name": "addUser", "args": [
{"name": "name", "type": {"kind": "SCALAR", "name": "String"}, "defaultValue": None},
], "type": {"kind": "OBJECT", "name": "User"}},
]},
{"kind": "OBJECT", "name": "User", "fields": [
{"name": "name", "type": {"kind": "SCALAR", "name": "String"}, "args": []},
{"name": "uid", "type": {"kind": "SCALAR", "name": "ID"}, "args": []},
]},
],
}
class TestGraphqlSchemaWalk(unittest.TestCase):
def setUp(self):
self._sfields = dict(gql._inputFields)
def tearDown(self):
gql._inputFields.clear()
gql._inputFields.update(self._sfields)
def test_extract_slots(self):
slots = gql._extractSlots(_GQL_SCHEMA)
byArg = dict((s.targetArg, s) for s in slots)
self.assertIn("id", byArg)
self.assertEqual(byArg["id"].strategy, "string")
self.assertEqual(byArg["id"].operation, "query")
self.assertIn("limit", byArg)
self.assertEqual(byArg["limit"].strategy, "numeric")
# the mutation slot is harvested too (reported but not exercised by the scanner)
self.assertIn("name", byArg)
self.assertEqual(byArg["name"].operation, "mutation")
def test_return_selection_set(self):
slots = gql._extractSlots(_GQL_SCHEMA)
slot = next(s for s in slots if s.targetArg == "id")
self.assertEqual(slot.returnKind, "OBJECT")
self.assertIn("name", slot.returnSel)
self.assertIn("uid", slot.returnSel)
def test_scalar_fields(self):
typeByName = {"User": _GQL_SCHEMA["types"][2],
"String": {"kind": "SCALAR", "name": "String"},
"ID": {"kind": "SCALAR", "name": "ID"}}
names = gql._scalarFields(_GQL_SCHEMA["types"][2], typeByName)
self.assertEqual(set(names), {"name", "uid"})
def test_render_selection(self):
self.assertIsNone(gql._renderSelection("SCALAR", "String", [], {}))
sel = gql._renderSelection("OBJECT", "User", ["name", "uid"], {})
self.assertEqual(sel, "{ name uid }")
class TestGraphqlQueryBuilding(unittest.TestCase):
def setUp(self):
self._sfields = dict(gql._inputFields)
self.slots = gql._extractSlots(_GQL_SCHEMA)
self.strSlot = next(s for s in self.slots if s.targetArg == "id")
self.numSlot = next(s for s in self.slots if s.targetArg == "limit")
def tearDown(self):
gql._inputFields.clear()
gql._inputFields.update(self._sfields)
def test_build_query_string_arg(self):
q = gql._buildQuery(self.strSlot, "x' OR '1'='1")
self.assertTrue(q.startswith("{user:user("))
self.assertIn('id:"x\' OR \'1\'=\'1"', q)
self.assertIn("limit:0", q) # required-ish sibling defaulted
self.assertIn("{ name uid }", q)
def test_build_query_numeric_rejects_non_numeric(self):
self.assertEqual(gql._buildQuery(self.numSlot, "notanumber"), "")
def test_build_query_numeric_accepts_digit(self):
self.assertIn("limit:42", gql._buildQuery(self.numSlot, "42"))
def test_build_batch(self):
query, aliases = gql._buildBatch(self.strSlot, ["a", "b", "c"])
self.assertEqual(aliases, ["a0", "a1", "a2"])
self.assertIn("a0:user(", query)
self.assertIn("a2:user(", query)
def test_build_batch_aborts_on_unembeddable(self):
query, aliases = gql._buildBatch(self.numSlot, ["1", "notnum"])
self.assertEqual((query, aliases), ("", []))
def test_mutation_prefix(self):
mutSlot = next(s for s in self.slots if s.operation == "mutation")
self.assertTrue(gql._buildQuery(mutSlot, "x").startswith("mutation {"))
def _make_sql_truth(secret, dialect):
"""A generic boolean SQL oracle: evaluate the LENGTH / ASCII-SUBSTRING / bit predicates
that _inferExpr / _inferExprBatched emit, against a known `secret`, using `dialect`'s
rendering. Independent of the concrete expression text."""
def truth(cond):
m = re.search(r"(?:CHAR_LENGTH|LENGTH|LEN)\(\((.+?)\)\)\s*(>=|>|=)\s*(\d+)", cond)
if m:
op, n, L = m.group(2), int(m.group(3)), len(secret)
return (L >= n) if op == ">=" else (L > n) if op == ">" else (L == n)
m = re.search(r"\((?:ASCII|UNICODE)\((?:SUBSTRING|SUBSTR)\(\((.+?)\),(\d+),1\)\)\s*&\s*(\d+)\)>0", cond)
if m:
pos, bit = int(m.group(2)), int(m.group(3))
c = ord(secret[pos - 1]) if pos - 1 < len(secret) else 0
return (c & bit) > 0
m = re.search(r"(?:ASCII|UNICODE)\((?:SUBSTRING|SUBSTR)\(\((.+?)\),(\d+),1\)\)\s*(>=|>|=)\s*(\d+)", cond)
if m:
pos, op, n = int(m.group(2)), m.group(3), int(m.group(4))
c = ord(secret[pos - 1]) if pos - 1 < len(secret) else 0
return (c >= n) if op == ">=" else (c > n) if op == ">" else (c == n)
if cond == "1=1":
return True
if cond == "1=2":
return False
return False
return truth
class TestGraphqlBlindInference(unittest.TestCase):
DIALECT = gql.DIALECTS["MySQL"]
def test_infer_expr_recovers_string(self):
truth = _make_sql_truth("Hello", self.DIALECT)
self.assertEqual(gql._inferExpr(truth, self.DIALECT, "version()"), "Hello")
def test_infer_expr_recovers_with_symbols(self):
secret = "root@%"
truth = _make_sql_truth(secret, self.DIALECT)
self.assertEqual(gql._inferExpr(truth, self.DIALECT, "CURRENT_USER()"), secret)
def test_infer_expr_empty_value(self):
truth = _make_sql_truth("", self.DIALECT)
self.assertEqual(gql._inferExpr(truth, self.DIALECT, "expr"), "")
def test_infer_expr_batched_recovers_string(self):
secret = "MariaDB"
truth = _make_sql_truth(secret, self.DIALECT)
truthBatch = lambda conds: [truth(c) for c in conds]
self.assertEqual(gql._inferExprBatched(truthBatch, self.DIALECT, "version()"), secret)
def test_infer_expr_batched_empty(self):
truth = _make_sql_truth("", self.DIALECT)
truthBatch = lambda conds: [truth(c) for c in conds]
self.assertEqual(gql._inferExprBatched(truthBatch, self.DIALECT, "expr"), "")
def test_inferrer_picks_batched_when_supported(self):
secret = "abc"
truth = _make_sql_truth(secret, self.DIALECT)
truthBatch = lambda conds: [truth(c) for c in conds]
infer = gql._inferrer(truth, truthBatch, self.DIALECT)
self.assertEqual(infer("version()"), secret)
def test_inferrer_falls_back_to_sequential(self):
secret = "xyz"
truth = _make_sql_truth(secret, self.DIALECT)
infer = gql._inferrer(truth, None, self.DIALECT)
self.assertEqual(infer("version()"), secret)
def test_fingerprint(self):
for dbms, dialect in gql.DIALECTS.items():
truth = lambda cond, expected=dialect.fingerprint: cond == expected
self.assertEqual(gql._fingerprint(truth), dbms)
def test_fingerprint_unknown(self):
self.assertIsNone(gql._fingerprint(lambda cond: False))
class TestGraphqlDumpTable(unittest.TestCase):
DIALECT = gql.DIALECTS["MySQL"]
def test_dump_table_grid(self):
# infer() returns the column list for dialect.columns(table), then the concatenated
# rows scalar for dialect.rows(...). We map by which sub-expression is requested.
columns_expr = self.DIALECT.columns("users")
rows_value = gql.COL_SEP.join(("1", "alice")) + gql.ROW_SEP + gql.COL_SEP.join(("2", "bob"))
def infer(expr, maxLen=gql.MAX_LENGTH):
return "id,name" if expr == columns_expr else rows_value
columns, rows = gql._dumpTable(infer, self.DIALECT, "users")
self.assertEqual(columns, ["id", "name"])
self.assertEqual(rows, [["1", "alice"], ["2", "bob"]])
def test_dump_table_no_columns(self):
self.assertIsNone(gql._dumpTable(lambda e, maxLen=0: "", self.DIALECT, "users"))
class TestGraphqlParseRows(unittest.TestCase):
def test_parse_rows_list(self):
page = '{"data": {"users": [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]}}'
columns, rows = gql._parseRows(page, None)
self.assertEqual(columns, ["id", "name"])
self.assertEqual(rows, [["1", "a"], ["2", "b"]])
def test_parse_rows_single_object(self):
page = '{"data": {"user": {"id": 7, "name": "z"}}}'
columns, rows = gql._parseRows(page, None)
self.assertEqual(columns, ["id", "name"])
self.assertEqual(rows, [["7", "z"]])
def test_parse_rows_null_data(self):
self.assertIsNone(gql._parseRows('{"data": {"user": null}}', None))
def test_parse_rows_non_json(self):
self.assertIsNone(gql._parseRows("not json", None))
def test_grid_empty(self):
self.assertEqual(gql._grid([], []), "(empty)")
def test_grid_renders(self):
out = gql._grid(["a", "b"], [["1", "22"]])
self.assertIn("| a | b |", out)
self.assertIn("| 1 | 22 |", out)
if __name__ == "__main__":
unittest.main(verbosity=2)
+540
View File
@@ -0,0 +1,540 @@
#!/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)
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Threading helpers in lib/core/threads.py: the thread-local data model,
current-thread accessors, the exception-isolating wrapper, and runThreads()
(the worker-pool driver used throughout extraction). Exercised with trivial,
fast, network-free workers.
"""
import os
import sys
import threading
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.core import threads as T
from lib.core.data import conf, kb
from thirdparty.six.moves import queue as _queue
class TestThreadData(unittest.TestCase):
def test_reset_initializes_fields(self):
td = T.getCurrentThreadData()
td.retriesCount = 5
td.reset()
self.assertEqual(td.retriesCount, 0)
self.assertEqual(td.valueStack, [])
self.assertFalse(td.disableStdOut)
def test_get_current_thread_data_is_threadlocal(self):
# ThreadData subclasses threading.local: the wrapper object is shared, but its
# ATTRIBUTE STATE is per-thread. Verify both: same object, independent state.
main = T.getCurrentThreadData()
self.assertIs(main, T.getCurrentThreadData()) # stable within a thread
main.retriesCount = 111
other = {}
def worker():
td = T.getCurrentThreadData()
other["same_obj"] = (td is main)
# a fresh thread gets reset()-initialised state, NOT the main thread's 111
other["retries_seen"] = td.retriesCount
td.retriesCount = 222
t = threading.Thread(target=worker)
t.start()
t.join()
# the wrapper object identity is shared (threading.local semantics) ...
self.assertTrue(other["same_obj"])
# ... but the worker never saw the main thread's mutation (thread-local state) ...
self.assertEqual(other["retries_seen"], 0)
# ... and the worker's own mutation did not leak back into the main thread
self.assertEqual(main.retriesCount, 111)
def test_get_current_thread_name(self):
self.assertEqual(T.getCurrentThreadName(), threading.current_thread().name)
class TestExceptionHandledFunction(unittest.TestCase):
def test_success_runs_function(self):
calls = []
T.exceptionHandledFunction(lambda: calls.append(1))
self.assertEqual(calls, [1])
def _capture_errors(self, silent):
"""Run a raising worker, returning the list of logged error messages."""
errors = []
class _Rec(object):
def error(self, msg, *a):
errors.append(msg % a if a else msg)
def __getattr__(self, name):
return lambda *a, **k: None
saved_logger = T.logger
saved_continue = kb.get("threadContinue")
saved_multi = kb.get("multipleCtrlC")
T.logger = _Rec()
kb.threadContinue = True
kb.multipleCtrlC = False
try:
# must never propagate, regardless of the silent flag
T.exceptionHandledFunction(lambda: 1 / 0, silent=silent)
finally:
T.logger = saved_logger
kb.threadContinue = saved_continue
kb.multipleCtrlC = saved_multi
return errors
def test_non_silent_logs_error(self):
# silent=False (with threadContinue) routes the swallowed exception to logger.error
errors = self._capture_errors(silent=False)
self.assertTrue(errors, msg="non-silent mode logged no error")
self.assertTrue(any("ZeroDivisionError" in e for e in errors),
msg="error message did not name the exception: %r" % errors)
def test_silent_logs_nothing(self):
# silent=True gates the logging: the exception is swallowed without any error log
errors = self._capture_errors(silent=True)
self.assertEqual(errors, [], msg="silent mode unexpectedly logged: %r" % errors)
def test_keyboardinterrupt_propagates(self):
def boom():
raise KeyboardInterrupt
self.assertRaises(KeyboardInterrupt, T.exceptionHandledFunction, boom)
class TestSetDaemon(unittest.TestCase):
def test_sets_daemon_flag(self):
t = threading.Thread(target=lambda: None)
T.setDaemon(t)
self.assertTrue(t.daemon)
class TestRunThreads(unittest.TestCase):
def setUp(self):
self._saved = {k: conf.get(k) for k in ("threads", "hashDB")}
conf.hashDB = None
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
def test_workers_drain_shared_queue(self):
q = _queue.Queue()
total = 50
for i in range(total):
q.put(i)
seen = []
lock = threading.Lock()
def worker():
while True:
try:
item = q.get_nowait()
except _queue.Empty:
break
with lock:
seen.append(item)
conf.threads = 4
T.runThreads(4, worker, startThreadMsg=False)
self.assertEqual(sorted(seen), list(range(total)))
def test_single_thread_runs_worker(self):
calls = []
conf.threads = 1
T.runThreads(1, lambda: calls.append(1), startThreadMsg=False)
self.assertEqual(calls, [1])
def test_cleanup_function_invoked(self):
flags = []
conf.threads = 2
T.runThreads(2, lambda: None,
cleanupFunction=lambda: flags.append(1),
startThreadMsg=False)
self.assertTrue(flags)
if __name__ == "__main__":
unittest.main()
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests for the enumeration methods of plugins/generic/users.py.
The injection layer (lib.request.inject.getValue) is mocked so the methods can
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.
"""
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
import plugins.generic.users as umod
from plugins.generic.users import Users
from lib.core.settings import CURRENT_USER
class TestUsersEnum(unittest.TestCase):
def setUp(self):
# Snapshot the global state these tests mutate so tearDown can restore it
# exactly (other test files share conf / kb / the inject module).
self._direct = conf.direct
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")
set_dbms("MySQL")
conf.direct = True
conf.user = None
kb.data.has_information_schema = True
# Neutralize the side effects getPasswordHashes triggers once it has
# populated the cache (file write + interactive dictionary attack prompt).
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.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
if self._his is None:
kb.data.pop("has_information_schema", None)
else:
kb.data.has_information_schema = self._his
# --- getUsers -----------------------------------------------------------
def test_get_users_mysql(self):
umod.inject.getValue = lambda query, *a, **k: [["root"], ["guest"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertIn("root", res)
self.assertIn("guest", res)
self.assertIn("root", kb.data.cachedUsers)
def test_get_users_postgresql(self):
set_dbms("PostgreSQL")
umod.inject.getValue = lambda query, *a, **k: [["postgres"], ["app"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertEqual(sorted(res), ["app", "postgres"])
def test_get_users_mssql(self):
set_dbms("Microsoft SQL Server")
umod.inject.getValue = lambda query, *a, **k: [["sa"], ["dbo"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertIn("sa", res)
def test_get_users_oracle(self):
set_dbms("Oracle")
umod.inject.getValue = lambda query, *a, **k: [["SYS"], ["SYSTEM"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertIn("SYS", res)
def test_get_users_none_leaves_cache_empty(self):
# isNoneValue([]) -> cache stays empty; inband branch skips appends.
# Strengthen: prove getUsers actually QUERIED (no stale-cache short-circuit
# returning the constant []) by spying on getValue, then in the same test
# re-run with a non-empty result to prove the cache repopulates from the
# newly fetched rows.
calls = {"n": 0}
def gv_empty(query, *a, **k):
calls["n"] += 1
return []
umod.inject.getValue = gv_empty
users = Users()
kb.data.cachedUsers = []
res = users.getUsers()
self.assertEqual(res, [])
# The inband branch must have issued at least one query, not short-circuited.
self.assertGreaterEqual(calls["n"], 1)
# Paired non-empty case: same instance, fresh cache, real rows -> cache
# must repopulate with exactly those users.
umod.inject.getValue = lambda query, *a, **k: [["root"], ["guest"]]
kb.data.cachedUsers = []
res2 = users.getUsers()
self.assertEqual(sorted(res2), ["guest", "root"])
self.assertIn("root", kb.data.cachedUsers)
# --- getCurrentUser -----------------------------------------------------
def test_get_current_user(self):
umod.inject.getValue = lambda query, *a, **k: "root@localhost"
users = Users()
kb.data.currentUser = ""
self.assertEqual(users.getCurrentUser(), "root@localhost")
self.assertEqual(kb.data.currentUser, "root@localhost")
# --- isDba --------------------------------------------------------------
def test_is_dba_mysql(self):
umod.inject.getValue = lambda query, *a, **k: "root@localhost"
umod.inject.checkBooleanExpression = lambda query, *a, **k: True
users = Users()
kb.data.currentUser = ""
kb.data.isDba = None
self.assertTrue(users.isDba())
def test_is_dba_postgresql_false(self):
set_dbms("PostgreSQL")
umod.inject.checkBooleanExpression = lambda query, *a, **k: False
users = Users()
kb.data.isDba = None
self.assertFalse(users.isDba())
# --- getPasswordHashes --------------------------------------------------
def test_get_password_hashes_mysql(self):
# filterPairValues keeps length-2 rows -> {user: [hash]}
umod.inject.getValue = lambda query, *a, **k: [["root", "*ABC123"], ["guest", "*DEF456"]]
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("root", res)
self.assertIn("guest", res)
self.assertEqual(res["root"], ["*ABC123"])
def test_get_password_hashes_with_conf_user(self):
conf.user = "root@localhost"
umod.inject.getValue = lambda query, *a, **k: [["root", "*HASH"]]
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("root", res)
def test_get_password_hashes_oracle(self):
set_dbms("Oracle")
conf.user = "system"
umod.inject.getValue = lambda query, *a, **k: [["SYSTEM", "ABCDEF1234567890"]]
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("SYSTEM", res)
# conf.user upper-cased for Oracle
self.assertEqual(conf.user, "SYSTEM")
def test_get_password_hashes_current_user(self):
conf.user = CURRENT_USER
# First getValue resolves current user, subsequent ones return the rows.
def gv(query, *a, **k):
if "CURRENT_USER" in query.upper() or "current_user" in query:
return "root@localhost"
return [["root", "*HASH"]]
umod.inject.getValue = gv
users = Users()
kb.data.currentUser = ""
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("root", res)
# --- getPrivileges ------------------------------------------------------
def test_get_privileges_mysql(self):
# MySQL with information_schema: privilege column added verbatim.
umod.inject.getValue = lambda query, *a, **k: [["root", "SUPER"], ["guest", "SELECT"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("root", privileges)
self.assertIn("SUPER", privileges["root"])
self.assertIn("root", areAdmins)
self.assertNotIn("guest", areAdmins)
def test_get_privileges_postgresql(self):
set_dbms("PostgreSQL")
from lib.core.dicts import PGSQL_PRIVS
# PGSQL: digit columns map to PGSQL_PRIVS by column index; col 1 == True.
idx = sorted(PGSQL_PRIVS.keys())[0]
row = ["pguser"] + ["0"] * (max(PGSQL_PRIVS.keys()))
row[idx] = "1"
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("pguser", privileges)
self.assertIn(PGSQL_PRIVS[idx], privileges["pguser"])
def test_get_privileges_oracle(self):
set_dbms("Oracle")
umod.inject.getValue = lambda query, *a, **k: [["SYS", "DBA"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("SYS", privileges)
self.assertIn("DBA", privileges["SYS"])
self.assertIn("SYS", areAdmins)
def test_get_privileges_with_conf_user(self):
conf.user = "root"
umod.inject.getValue = lambda query, *a, **k: [["root", "SELECT"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("root", privileges)
# --- getRoles (delegates to getPrivileges) ------------------------------
def test_get_roles(self):
umod.inject.getValue = lambda query, *a, **k: [["root", "SUPER"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getRoles()
self.assertIn("root", privileges)
self.assertIn("root", areAdmins)
if __name__ == "__main__":
unittest.main()