From 2297c8130993ea2fbcdce3615247ab3ea6ec282a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 18:27:59 +0200 Subject: [PATCH] Update of tests --- data/txt/sha256sums.txt | 44 +- lib/core/settings.py | 2 +- tests/test_agent.py | 694 ++++++- tests/test_agent_dialects.py | 274 --- tests/test_brute.py | 198 ++ tests/test_common.py | 1715 +++++++++++++++++ tests/test_common_parsers.py | 466 ----- tests/test_common_utils.py | 340 ---- tests/test_compat.py | 3 +- tests/test_core_extra.py | 676 ------- tests/test_core_final.py | 605 ------ tests/test_core_more.py | 706 ------- tests/test_databases_enum.py | 256 +++ tests/test_dbms_enum.py | 634 +++++- tests/test_dbms_enum_a.py | 215 --- tests/test_dbms_enum_b.py | 469 ----- tests/test_deps.py | 2 +- tests/test_entries.py | 802 ++++++++ tests/test_filesystem.py | 1 - tests/test_generic_enum_more.py | 865 --------- ...neric_more.py => test_generic_takeover.py} | 277 +-- tests/test_inference.py | 293 --- tests/test_option.py | 1594 +++++++++++++++ tests/test_option_more.py | 663 ------- tests/test_option_setup.py | 739 ------- tests/test_payload_marking.py | 2 +- tests/test_request_basic.py | 88 + tests/test_search_enum.py | 321 +-- tests/test_target_parsing.py | 2 +- tests/test_techniques.py | 771 +++++++- tests/test_techniques_more.py | 540 ------ tests/test_users_enum.py | 224 ++- 32 files changed, 7177 insertions(+), 7304 deletions(-) delete mode 100644 tests/test_agent_dialects.py create mode 100644 tests/test_brute.py create mode 100644 tests/test_common.py delete mode 100644 tests/test_common_parsers.py delete mode 100644 tests/test_common_utils.py delete mode 100644 tests/test_core_extra.py delete mode 100644 tests/test_core_final.py delete mode 100644 tests/test_core_more.py delete mode 100644 tests/test_dbms_enum_a.py delete mode 100644 tests/test_dbms_enum_b.py create mode 100644 tests/test_entries.py delete mode 100644 tests/test_generic_enum_more.py rename tests/{test_generic_more.py => test_generic_takeover.py} (66%) delete mode 100644 tests/test_inference.py create mode 100644 tests/test_option.py delete mode 100644 tests/test_option_more.py delete mode 100644 tests/test_option_setup.py create mode 100644 tests/test_request_basic.py delete mode 100644 tests/test_techniques_more.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index d8d8b6631..02cb904bc 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -5cbf5f4bc21f21873df79babd91da8f7fea5ec3c1999f108f005ca6fb4d453b6 lib/core/settings.py +bb908144ffaf055c67bb06da7f914d77cad00f84839c63ae2f83fea62cdacfe2 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -579,31 +579,25 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional 0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py -d2c27dff782dbe119a4cb5041f374d87b67e3da523ee3a7ad584d34721b6c564 tests/test_agent_dialects.py -bfb553602eb5d20b4ab5928dbcf8e6a3e7e5ff69f7d30d1f53ef6d323c237f6c tests/test_agent.py +d16977d057c28888aa41500f79a19789cadef693cb8b7d9a3bca55b983ce2266 tests/test_agent.py 138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py +36bcb68483d824db5d05870fab62f1907221bf256826b734302fbc15a9231c42 tests/test_brute.py 27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py c99b77cc5d85334f147a1a6d4b2867af396f70e9f2609f8587344e084910e893 tests/test_checks.py 9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py 2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py -c6338f74230b758cb41adacf4f04593e70b4b11e054ea0b35712607a781e0d55 tests/test_common_parsers.py -b1540c5f2be80ee3d870d7c373adfca23f33adb06724db00335adbd79bea4272 tests/test_common_utils.py +058c6b13f2f9ce7798f4106eb4f2a0eaf290eab6b3f9aff2f46553f80c872d29 tests/test_common.py 899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py -a0a29231acbbe6bec11400e28b39b76eaf812c03bf79d5f0dbdd68cd54a052f8 tests/test_compat.py +a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py 75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py -d2c52b1c9b0f31e2d30e1fc3942986692a815e76fa8e39903c3824d6d6d0ee71 tests/test_core_extra.py -7c6d542bf96e8962ecdf8607f93e84babe4820045533bded170955e95727d630 tests/test_core_final.py -e42f6dd46fa7f2d1e666116e2244fa02e7b9d930a005e2bbeea89cfe3f2215b6 tests/test_core_more.py -951822c0d6ea62dc91cc4a7614059788b256cac06167f4767721f2ad5d54a78b tests/test_databases_enum.py +2bd0faeaf7db1d73dd0caab3bde9900fdaa1f38fd736a6e238cd56ff9bc67b66 tests/test_databases_enum.py c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py -c9f7c5219e379b0242914f79f1e5d3b8b7d1a4c5e9f77cd05d0ec382d4fbed88 tests/test_dbms_enum_a.py -866978b7d5d0270a54465897932fe645c7e0360d73b0e4086540558c107e680d tests/test_dbms_enum_b.py -a3628b7f22dcc0ff4cd9ed8a1e70519a340f40fa4d73e9220c7d11f5088d9c01 tests/test_dbms_enum.py +8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py -8e469e4e29319bcb718803a9e109e742965875c985fa8e8d3bb5b18c922ec597 tests/test_deps.py +cf480e241746fdbbe071c2dfd25ec5fd186a79dcb4522f034f661b9f55a2c4ea tests/test_deps.py b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py @@ -612,11 +606,11 @@ ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns 4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py 9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py +fe1211ce43a51cd8ec7dd3395aafda8d7313ff60e2ef013072ce9fa49ca4a242 tests/test_entries.py bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py -31354d3cff0d26ecf3b42e949a2780ae3d286cdf206b59404e18a96e7a2cddd2 tests/test_filesystem.py +26730151abea598f193131c5d64ef92b531941972f3d6236f9951c3116030b1c tests/test_filesystem.py 6a9d95f64c7892957742534a14e8f094c6ed9ebc91b7059f4f1665049228a5a6 tests/test_fingerprint.py -4f3cfb830b323a3423b0f80985b9a0bbbe4ef77350b762f103dcd8936cca67c6 tests/test_generic_enum_more.py -9874920d18fc30736630df6b14a70b230504d2e4d0c035971a9aa285ee623839 tests/test_generic_more.py +de477d585396596556f7020d39bad3577f4d73336c19c1ee14e4158c45dbd924 tests/test_generic_takeover.py bde97a4781c4ee84e0fe86f7a33206f114167eb14b704013ecf1c26b838193d7 tests/test_graphql.py 50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py 92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py @@ -625,39 +619,37 @@ da2efd1b7457ff619d98a2ae5045f072fdd34be2aa1c18f17d74d7518eeb6707 tests/test_has c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py -280afe64cabac3a737d2574f4e2873760c3883eaac1b7ba0f8fed4b82b91c9c2 tests/test_inference.py 0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py -647d782395fe88dcda775808b9988a0809b208d1df9412d89dc8b6809bd15de6 tests/test_option_more.py -a5743989442de51b3689b30c27118249502bb462788abeeb1ddb27cd176cd363 tests/test_option_setup.py +6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py 7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py -064617c6a3d28ecd75136318b4f515ab1adefbf830da17667f105337b419c184 tests/test_payload_marking.py +57fa7b742aa0859ae166cea49dd6daac36d21aae05fe3ba6c73f42c4c56d7a3c tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py 2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py 67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py +f7478deabe9d117c60d597859510a168c81e71f60981503dcd798ef2311b30a1 tests/test_request_basic.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py -d4f6e60c23db67430cf68dc2d90317d69391a19feff0f842c08ae2443b481857 tests/test_search_enum.py +5b6ce95dddbd07d0126224f4f066643938476e536e18b700ea5d916e1052a715 tests/test_search_enum.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py d6bcba7232fff834737c094679c92e7a69cab5721bc87cb10bcab868c6a8115f tests/test_sgmllib.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py 8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py 8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py -44954b916f1e4a4bb217516a65cf330fca922600d484f732525e0e4a2a553167 tests/test_target_parsing.py +67472bd71c20782cc0f738e2c2e674c29d6985669e14d15b69baef7d0e33de62 tests/test_target_parsing.py b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py -d070a72ae9529182d6dfc0884f7720d42a5f0cd8cd865dd4c2d209389c3ade85 tests/test_techniques_more.py -f2e8b5b9799f4e591462f53a97bb643c6399acf703f33e119c03d991971274ab tests/test_techniques.py +0e644bb7b25c183d0d689ea7be542d7a2ce780cc68067f89afb2ee095a79f762 tests/test_techniques.py 639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py 708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py 48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py 4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py -e7793907ce4dad9034d61f2a3cdfec8af33b96f8e6f67138b09daf81a825c13f tests/test_users_enum.py +eca021208e388b4d14c53f1e9f8a6e7d685e54ba572fb2a8487e6b620a20bcb5 tests/test_users_enum.py 23ffd75b5aec33066e6d6aad01ab2c9c1b12ee20c1a0990f8f1be81f1ad16161 tests/_testutils.py 2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py 93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 79a4e7ea0..0600930ca 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.186" +VERSION = "1.10.6.187" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_agent.py b/tests/test_agent.py index 2fb7bc09e..d49a7d76f 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -4,15 +4,32 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission -Payload assembly helpers in lib/core/agent.py. +Consolidated unit coverage for lib/core/agent.py. -These are the (mostly) DBMS-independent string transforms that wrap, fold and -clean a payload on its way to the wire: prefix/suffix, payload delimiters, -field extraction, CONCAT folding, and RAND-marker cleanup. All values below -were probed from real output, not assumed. +This file merges the agent.py tests previously spread across +test_agent.py, test_agent_dialects.py, test_core_more.py and +test_core_extra.py: + + * Payload assembly helpers (DBMS-independent string transforms that wrap, + fold and clean a payload on its way to the wire): prefix/suffix, payload + delimiters, field extraction, CONCAT folding, RAND-marker cleanup. + + * Cross-dialect exercise of the payload-assembly helpers. agent.py builds SQL + payloads from per-DBMS dialect templates (queries.xml); the helpers are pure + given the identified back-end DBMS, so driving each one across EVERY + supported dialect walks the dialect-specific branches (CAST forms, + concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target. + + * Argument-combination / shape coverage for forgeUnionQuery, limitQuery, + whereQuery, getComment, concatQuery(unpack=False), cleanupPayload markers, + adjustLateValues, getFields shapes, prefix/suffix args, nullAndCastField + noCast, plus the pure agent helpers (extractPayload/replacePayload, ...). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. """ import os +import re import sys import unittest @@ -21,9 +38,122 @@ from _testutils import bootstrap, set_dbms bootstrap() from lib.core.agent import agent +from lib.core.data import conf, kb, queries from lib.core.enums import DBMS -from lib.core.settings import PAYLOAD_DELIMITER +from lib.core.settings import ( + PAYLOAD_DELIMITER, + SLEEP_TIME_MARKER, + BOUNDED_BASE64_MARKER, +) +DIALECTS = sorted(queries.keys()) + +# --------------------------------------------------------------------------- # +# Per-dialect expectation maps (keyed by the DBMS display name == queries key). +# +# These were derived by inspecting the actual agent.py output for every dialect +# (the queries.xml templates drive the branches). They pin the *distinctive* +# dialect token so an assertion fails if the dialect branch collapses to the +# wrong form (e.g. concat operator swapped, null-wrapper dropped). +# --------------------------------------------------------------------------- # + +# concatQuery / simpleConcatenate join operator per dialect. +CONCAT_OPERATOR = { + "ClickHouse": "CONCAT(", + "Informix": "CONCAT(", + "MySQL": "CONCAT(", + "SAP MaxDB": "CONCAT(", + "Microsoft SQL Server": "+", + "Sybase": "+", + "Microsoft Access": "&", +} +# everything not listed above uses the SQL standard "||" +CONCAT_OPERATOR_DEFAULT = "||" + +# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect. +NULL_WRAPPER = { + "Altibase": "NVL", + "Apache Derby": "COALESCE", + "ClickHouse": "ifNull", + "CrateDB": "COALESCE", + "Cubrid": "IFNULL", + "Firebird": "COALESCE", + "FrontBase": "COALESCE", + "H2": "IFNULL", + "HSQLDB": "IFNULL", + "IBM DB2": "COALESCE", + "Informix": "NVL", + "InterSystems Cache": "COALESCE", + "Mckoi": "IF(", + "Microsoft Access": "IIF", + "Microsoft SQL Server": "ISNULL", + "MimerSQL": "COALESCE", + "MonetDB": "COALESCE", + "MySQL": "IFNULL", + "Oracle": "NVL", + "PostgreSQL": "COALESCE", + "Presto": "COALESCE", + "Raima Database Manager": "IFNULL", + "SAP MaxDB": "VALUE", + "SQLite": "COALESCE", + "Snowflake": "NVL", + "Spanner": "IFNULL", + "Sybase": "ISNULL", + "Vertica": "COALESCE", + "Virtuoso": "__MAX_NOTNULL", + "eXtremeDB": "IFNULL", +} + +# hexConvertField: dialects that DO have a hex function, mapped to its token. +HEX_FUNCTION = { + "Altibase": "HEX_ENCODE(", + "Cubrid": "HEX(", + "H2": "RAWTOHEX(", + "IBM DB2": "HEX(", + "Microsoft SQL Server": "fn_varbintohexstr", + "MySQL": "HEX(", + "Oracle": "RAWTOHEX(", + "PostgreSQL": "ENCODE(", + "Presto": "TO_HEX(", + "SAP MaxDB": "HEX(", + "SQLite": "HEX(", + "Spanner": "TO_HEX(", + "Sybase": "BINTOSTR", + "Vertica": "TO_HEX(", +} +# dialects that intentionally do NOT support hex conversion and return the +# field unchanged (a no-op the old "colname in out" check silently masked). +HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION) + +# limitQuery: dialects whose limit template is empty so the call legitimately +# raises (no .limit.query). These are skipped by name in the limit-token test. +LIMIT_RAISES = {"Mckoi", "Raima Database Manager"} +# dialects with no special limitQuery branch: the query is returned unchanged +# (no limit token is emitted). +LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"} +# broad set of dialect limit tokens; every running, non-passthrough dialect +# emits at least one of these. +LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER") + + +class DbmsStateMixin(object): + """Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak.""" + + def setUp(self): + self._forcedDbms = kb.forcedDbms + self._sticky = kb.stickyDBMS + self._batch = conf.batch + conf.batch = True + + def tearDown(self): + kb.forcedDbms = self._forcedDbms + kb.stickyDBMS = self._sticky + conf.batch = self._batch + + +# --------------------------------------------------------------------------- # +# Single-DBMS payload-assembly helpers (formerly test_agent.py) +# --------------------------------------------------------------------------- # class TestPayloadDelimiters(unittest.TestCase): def test_add(self): @@ -82,5 +212,557 @@ class TestCleanupPayload(unittest.TestCase): self.assertTrue(out.split()[-1].isdigit(), msg=out) # ...and replaced with a concrete number +# --------------------------------------------------------------------------- # +# Cross-dialect smoke coverage (formerly test_agent_dialects.py) +# --------------------------------------------------------------------------- # + +class TestNullCastConcatFields(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.nullCastConcatFields("user,password") + self.assertIsInstance(out, str, msg=dbms) + # both column names survive the null/cast/concat rewrite + self.assertIn("user", out, msg=dbms) + self.assertIn("password", out, msg=dbms) + # the dialect-specific NULL-wrapper must be present (the column-name + # check above is always satisfied and so cannot catch a broken + # branch); this fails if the wrapper collapses to the wrong form. + self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) + + def test_literal_passthrough(self): + for dbms in DIALECTS: + set_dbms(dbms) + # a bare quoted literal is returned untouched + self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms) + + +class TestNullAndCastField(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.nullAndCastField("colname") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("colname", out, msg=dbms) + # dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...) + self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) + + +class TestHexConvertField(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.hexConvertField("colname") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("colname", out, msg=dbms) + if dbms in HEX_FUNCTION: + # the dialect's hex function wraps the field + self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out)) + else: + # intentional no-op: the field is returned verbatim. The old + # "colname in out" check masked this; pin the exact identity. + self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out)) + + +class TestConcatQueryDialects(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.concatQuery("SELECT user FROM users") + self.assertIsInstance(out, str, msg=dbms) + # concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/ + # SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and + # the rest use the SQL-standard ||. Assert the right operator so the + # test fails if the dialect collapses to the wrong concatenation. + expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT) + self.assertIn(expected, out, msg="%s: %s" % (dbms, out)) + + +class TestSimpleConcatenate(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.simpleConcatenate("a", "b") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("a", out, msg=dbms) + self.assertIn("b", out, msg=dbms) + + +class TestForgeUnionQueryDialects(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + count = 3 + out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None, + None, None, "NULL", None) + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("UNION", out.upper(), msg=dbms) + # position -1 with char NULL fills every one of the `count` columns + # with the char, so the NULL char must appear exactly `count` times. + # (a hardcoded "UNION in out" check could not catch a wrong column + # count.) Match NULL as a whole token to avoid matching substrings. + self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count, + msg="%s expected %d NULLs: %s" % (dbms, count, out)) + + +class TestLimitQueryDialects(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + + # Only Mckoi/Raima have an empty limit template and legitimately + # raise; skip exactly those by name rather than swallowing *any* + # exception (which would hide a real regression in another dialect). + if dbms in LIMIT_RAISES: + with self.assertRaises(Exception, msg=dbms): + agent.limitQuery(0, "SELECT user FROM users", "user") + continue + + out = agent.limitQuery(0, "SELECT user FROM users", "user") + self.assertIsInstance(out, str, msg=dbms) + + if dbms in LIMIT_PASSTHROUGH: + # these dialects have no dedicated limitQuery branch and return + # the query unchanged (documented no-op). + self.assertEqual(out, "SELECT user FROM users", msg=dbms) + else: + # every other running dialect emits a real limit construct + self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS), + msg="%s missing limit token: %s" % (dbms, out)) + + +class TestForgeCaseStatement(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.forgeCaseStatement("1=1") + self.assertIsInstance(out, str, msg=dbms) + # dialects vary on the conditional form (CASE / IIF / IF); the + # condition itself is always embedded + self.assertIn("1=1", out, msg=dbms) + # ...but the conditional construct itself must also be present, + # otherwise the "1=1" check alone could pass on a degenerate output. + self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out, + msg="%s missing conditional construct: %s" % (dbms, out)) + + +class TestPrefixSuffixAcrossDialects(unittest.TestCase): + def test_prefix_suffix(self): + for dbms in DIALECTS: + set_dbms(dbms) + prefix = agent.prefixQuery("1=1") + suffix = agent.suffixQuery("1=1") + self.assertIsInstance(prefix, str, msg=dbms) + self.assertIsInstance(suffix, str, msg=dbms) + # prefixQuery pads a leading space ahead of the expression by default + self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix)) + # suffixQuery returns the expression itself (no extra clause/comment) + self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix)) + + +class TestRunAsDBMSUserAndWhere(unittest.TestCase): + def test_run_as_user_noop_without_conf(self): + for dbms in DIALECTS: + set_dbms(dbms) + # without conf.dbmsCred the query is returned unchanged + self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms) + + +# --------------------------------------------------------------------------- # +# Argument-combination / shape coverage (formerly test_core_more.py) +# --------------------------------------------------------------------------- # + +class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase): + """forgeUnionQuery arg combinations not reached by the dialect smoke test.""" + + def test_limited_subselect_wraps_query(self): + set_dbms(DBMS.MYSQL) + # limited=True wraps the payload as (SELECT ...) at `position`, fills the + # rest with `char`, and appends the FROM/comment/suffix + out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None, + None, None, "NULL", None, limited=True) + self.assertIn("(SELECT user FROM mysql.user)", out) + self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out) + # position 1 of 3 => NULL,,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 = + 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 TO (the FIRST/SKIP emulation); pin + # the exact shape so a broken offset arithmetic is caught. + self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out) + + def test_mssql_top_not_in(self): + set_dbms(DBMS.MSSQL) + out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name") + # MSSQL emulates LIMIT via TOP + NOT IN + self.assertIn("TOP", out.upper(), msg=out) + self.assertIn("NOT IN", out.upper(), msg=out) + + +class TestWhereQuery(DbmsStateMixin, unittest.TestCase): + """whereQuery only acts when conf.dumpWhere is set.""" + + def setUp(self): + DbmsStateMixin.setUp(self) + self._dumpWhere = conf.dumpWhere + self._tbl = conf.tbl + + def tearDown(self): + conf.dumpWhere = self._dumpWhere + conf.tbl = self._tbl + DbmsStateMixin.tearDown(self) + + def test_no_dumpwhere_is_identity(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = None + self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t") + + def test_appends_where_clause(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t") + self.assertIn("WHERE id>10", out, msg=out) + + def test_existing_where_gets_anded(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t WHERE b=1") + self.assertIn("AND id>10", out, msg=out) + + def test_order_by_suffix_preserved(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t ORDER BY a") + # the genuine trailing ORDER BY is kept after the spliced WHERE + self.assertIn("WHERE id>10", out, msg=out) + # the ORDER BY must survive *after* the spliced WHERE clause; the + # substring check alone could pass even if the suffix were dropped. + self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out) + + +class TestGetComment(unittest.TestCase): + def test_present(self): + from lib.core.datatype import AttribDict + self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x") + + def test_absent_returns_empty(self): + from lib.core.datatype import AttribDict + self.assertEqual(agent.getComment(AttribDict()), "") + + +class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase): + def test_unpack_false_returns_input_unchanged(self): + set_dbms(DBMS.MYSQL) + self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False), + "SELECT a FROM t") + + def test_pgsql_unpack_uses_pipe_concat(self): + set_dbms(DBMS.PGSQL) + out = agent.concatQuery("SELECT usename FROM pg_shadow") + self.assertIn("||", out, msg=out) + self.assertIn(kb.chars.start, out, msg=out) + self.assertIn(kb.chars.stop, out, msg=out) + + +class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase): + def test_origvalue_digit_inlined(self): + out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42") + self.assertEqual(out, "x=42") + + def test_origvalue_nondigit_quoted(self): + out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc") + self.assertIn("'abc'", out, msg=out) + + def test_original_marker_raw_substitution(self): + out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw") + self.assertEqual(out, "p=raw") + + def test_space_replace_marker(self): + out = agent.cleanupPayload("a[SPACE_REPLACE]b") + self.assertEqual(out, "a%sb" % kb.chars.space) + + def test_non_string_returns_none(self): + self.assertIsNone(agent.cleanupPayload(None)) + + +class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase): + def test_sleeptime_replaced_with_timesec(self): + out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER) + self.assertEqual(out, "SLEEP(%s)" % conf.timeSec) + self.assertNotIn(SLEEP_TIME_MARKER, out) + + def test_randnum_marker_substituted(self): + out = agent.adjustLateValues("v=[RANDNUM]") + self.assertNotIn("[RANDNUM]", out) + self.assertTrue(out.split("=")[1].isdigit(), msg=out) + + def test_bounded_base64_marker_encoded(self): + payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER) + out = agent.adjustLateValues(payload) + # the marked region is base64-encoded and the markers are consumed + self.assertNotIn(BOUNDED_BASE64_MARKER, out) + self.assertEqual(out, "QUI=") + + def test_empty_payload_passthrough(self): + self.assertEqual(agent.adjustLateValues(""), "") + + +class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase): + def test_select_top(self): + set_dbms(DBMS.MSSQL) + res = agent.getFields("SELECT TOP 1 name FROM sysobjects") + self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched") + self.assertEqual(res[6], "name") + + def test_distinct(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT DISTINCT(name) FROM t") + self.assertEqual(res[6], "name") + + def test_function_is_single_element(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT COUNT(*) FROM t") + self.assertEqual(res[5], ["COUNT(*)"]) + + def test_no_from_keeps_whole_select_list(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT a,b,c") + self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM") + self.assertEqual(res[5], ["a", "b", "c"]) + + +class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase): + def test_prefix_with_explicit_prefix(self): + set_dbms(DBMS.MYSQL) + out = agent.prefixQuery("1=1", prefix="')") + self.assertIn("')", out, msg=out) + self.assertTrue(out.endswith("1=1"), msg=out) + + def test_prefix_group_by_clause_uses_prefix_verbatim(self): + set_dbms(DBMS.MYSQL) + # clause == [2] (GROUP BY / ORDER BY) => no trailing space added + out = agent.prefixQuery("1=1", prefix="X", clause=[2]) + self.assertEqual(out, "X1=1") + + def test_suffix_appends_comment(self): + set_dbms(DBMS.MYSQL) + out = agent.suffixQuery("1=1", comment="-- -") + self.assertTrue(out.startswith("1=1"), msg=out) + self.assertIn("-", out) + + def test_suffix_appends_suffix_no_comment(self): + set_dbms(DBMS.MYSQL) + out = agent.suffixQuery("1=1", suffix="')") + self.assertIn("')", out, msg=out) + + +class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase): + def setUp(self): + DbmsStateMixin.setUp(self) + self._noCast = conf.noCast + + def tearDown(self): + conf.noCast = self._noCast + DbmsStateMixin.tearDown(self) + + def test_nocast_returns_field_unchanged(self): + set_dbms(DBMS.MYSQL) + conf.noCast = True + self.assertEqual(agent.nullAndCastField("colname"), "colname") + + def test_cast_present_when_nocast_off(self): + set_dbms(DBMS.MYSQL) + conf.noCast = False + out = agent.nullAndCastField("colname") + self.assertIn("CAST", out.upper(), msg=out) + self.assertIn("colname", out) + + +# --------------------------------------------------------------------------- # +# Pure agent helpers (formerly test_core_extra.py) +# --------------------------------------------------------------------------- # + +class TestAgentPure(unittest.TestCase): + """Pure agent.py methods independent of full injection state.""" + + @classmethod + def setUpClass(cls): + from lib.core.agent import agent + cls.agent = agent + + def tearDown(self): + set_dbms(None) + + def test_get_comment_present(self): + from lib.core.datatype import AttribDict + request = AttribDict() + request.comment = "-- foo" + self.assertEqual(self.agent.getComment(request), "-- foo") + + def test_get_comment_absent(self): + from lib.core.datatype import AttribDict + request = AttribDict() + self.assertEqual(self.agent.getComment(request), "") + + def test_add_payload_delimiters(self): + from lib.core.settings import PAYLOAD_DELIMITER + value = "1 AND 1=1" + result = self.agent.addPayloadDelimiters(value) + self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER)) + # falsy value returned unchanged + self.assertEqual(self.agent.addPayloadDelimiters(""), "") + + def test_remove_payload_delimiters_roundtrip(self): + self.assertEqual( + self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")), + "1 AND 1=1", + ) + + def test_extract_payload(self): + wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix" + self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1") + + def test_replace_payload(self): + wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix" + replaced = self.agent.replacePayload(wrapped, "NEW") + self.assertEqual(self.agent.extractPayload(replaced), "NEW") + # surrounding text preserved + self.assertTrue(replaced.startswith("prefix")) + self.assertTrue(replaced.endswith("suffix")) + + def test_simple_concatenate_mysql(self): + set_dbms(DBMS.MYSQL) + # MySQL concatenate query template is 'CONCAT(%s,%s)' + self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)") + + def test_hex_convert_field_mysql(self): + set_dbms(DBMS.MYSQL) + # MySQL hex template is 'HEX(%s)' + self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)") + + def test_get_fields_select_from(self): + set_dbms(DBMS.MYSQL) + result = self.agent.getFields("SELECT a, b FROM users") + fieldsToCastList = result[5] + fieldsToCastStr = result[6] + self.assertEqual(fieldsToCastStr, "a, b") + self.assertEqual(fieldsToCastList, ["a", "b"]) + + def test_get_fields_no_from(self): + set_dbms(DBMS.MYSQL) + # a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list + result = self.agent.getFields("SELECT 1") + fieldsSelectFrom = result[0] + self.assertIsNone(fieldsSelectFrom) + self.assertEqual(result[6], "1") + + +class TestAgentWhereQuery(unittest.TestCase): + @classmethod + def setUpClass(cls): + from lib.core.agent import agent + cls.agent = agent + + def setUp(self): + self._old_dumpWhere = conf.dumpWhere + self._old_tbl = conf.tbl + conf.tbl = None + + def tearDown(self): + conf.dumpWhere = self._old_dumpWhere + conf.tbl = self._old_tbl + set_dbms(None) + + def test_no_dumpwhere_passthrough(self): + conf.dumpWhere = None + query = "SELECT a FROM t" + self.assertEqual(self.agent.whereQuery(query), query) + + def test_appends_where_clause(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # no existing WHERE -> appends ' WHERE id>0' + self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0") + + def test_and_when_where_present(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # existing WHERE -> appended with AND + self.assertEqual( + self.agent.whereQuery("SELECT a FROM t WHERE x=1"), + "SELECT a FROM t WHERE x=1 AND id>0", + ) + + def test_splices_before_order_by(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # WHERE must be spliced before the trailing ORDER BY suffix + self.assertEqual( + self.agent.whereQuery("SELECT a FROM t ORDER BY a"), + "SELECT a FROM t WHERE id>0 ORDER BY a", + ) + + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_agent_dialects.py b/tests/test_agent_dialects.py deleted file mode 100644 index 72b9007a5..000000000 --- a/tests/test_agent_dialects.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Cross-dialect exercise of lib/core/agent.py payload-assembly helpers. - -agent.py builds SQL payloads from per-DBMS dialect templates (queries.xml). -The helpers are pure given the identified back-end DBMS, so driving each one -across EVERY supported dialect walks the dialect-specific branches (CAST forms, -concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target. - -These are smoke-level assertions (right type, dialect tokens present) rather than -golden strings: the goal is to traverse the dialect branches the single-DBMS -tests in test_agent.py do not reach. -""" - -import os -import re -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.agent import agent -from lib.core.data import queries - -DIALECTS = sorted(queries.keys()) - -# --------------------------------------------------------------------------- # -# Per-dialect expectation maps (keyed by the DBMS display name == queries key). -# -# These were derived by inspecting the actual agent.py output for every dialect -# (the queries.xml templates drive the branches). They pin the *distinctive* -# dialect token so an assertion fails if the dialect branch collapses to the -# wrong form (e.g. concat operator swapped, null-wrapper dropped). -# --------------------------------------------------------------------------- # - -# concatQuery / simpleConcatenate join operator per dialect. -CONCAT_OPERATOR = { - "ClickHouse": "CONCAT(", - "Informix": "CONCAT(", - "MySQL": "CONCAT(", - "SAP MaxDB": "CONCAT(", - "Microsoft SQL Server": "+", - "Sybase": "+", - "Microsoft Access": "&", -} -# everything not listed above uses the SQL standard "||" -CONCAT_OPERATOR_DEFAULT = "||" - -# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect. -NULL_WRAPPER = { - "Altibase": "NVL", - "Apache Derby": "COALESCE", - "ClickHouse": "ifNull", - "CrateDB": "COALESCE", - "Cubrid": "IFNULL", - "Firebird": "COALESCE", - "FrontBase": "COALESCE", - "H2": "IFNULL", - "HSQLDB": "IFNULL", - "IBM DB2": "COALESCE", - "Informix": "NVL", - "InterSystems Cache": "COALESCE", - "Mckoi": "IF(", - "Microsoft Access": "IIF", - "Microsoft SQL Server": "ISNULL", - "MimerSQL": "COALESCE", - "MonetDB": "COALESCE", - "MySQL": "IFNULL", - "Oracle": "NVL", - "PostgreSQL": "COALESCE", - "Presto": "COALESCE", - "Raima Database Manager": "IFNULL", - "SAP MaxDB": "VALUE", - "SQLite": "COALESCE", - "Snowflake": "NVL", - "Spanner": "IFNULL", - "Sybase": "ISNULL", - "Vertica": "COALESCE", - "Virtuoso": "__MAX_NOTNULL", - "eXtremeDB": "IFNULL", -} - -# hexConvertField: dialects that DO have a hex function, mapped to its token. -HEX_FUNCTION = { - "Altibase": "HEX_ENCODE(", - "Cubrid": "HEX(", - "H2": "RAWTOHEX(", - "IBM DB2": "HEX(", - "Microsoft SQL Server": "fn_varbintohexstr", - "MySQL": "HEX(", - "Oracle": "RAWTOHEX(", - "PostgreSQL": "ENCODE(", - "Presto": "TO_HEX(", - "SAP MaxDB": "HEX(", - "SQLite": "HEX(", - "Spanner": "TO_HEX(", - "Sybase": "BINTOSTR", - "Vertica": "TO_HEX(", -} -# dialects that intentionally do NOT support hex conversion and return the -# field unchanged (a no-op the old "colname in out" check silently masked). -HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION) - -# limitQuery: dialects whose limit template is empty so the call legitimately -# raises (no .limit.query). These are skipped by name in the limit-token test. -LIMIT_RAISES = {"Mckoi", "Raima Database Manager"} -# dialects with no special limitQuery branch: the query is returned unchanged -# (no limit token is emitted). -LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"} -# broad set of dialect limit tokens; every running, non-passthrough dialect -# emits at least one of these. -LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER") - - -class TestNullCastConcatFields(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - out = agent.nullCastConcatFields("user,password") - self.assertIsInstance(out, str, msg=dbms) - # both column names survive the null/cast/concat rewrite - self.assertIn("user", out, msg=dbms) - self.assertIn("password", out, msg=dbms) - # the dialect-specific NULL-wrapper must be present (the column-name - # check above is always satisfied and so cannot catch a broken - # branch); this fails if the wrapper collapses to the wrong form. - self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) - - def test_literal_passthrough(self): - for dbms in DIALECTS: - set_dbms(dbms) - # a bare quoted literal is returned untouched - self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms) - - -class TestNullAndCastField(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - out = agent.nullAndCastField("colname") - self.assertIsInstance(out, str, msg=dbms) - self.assertIn("colname", out, msg=dbms) - # dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...) - self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) - - -class TestHexConvertField(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - out = agent.hexConvertField("colname") - self.assertIsInstance(out, str, msg=dbms) - self.assertIn("colname", out, msg=dbms) - if dbms in HEX_FUNCTION: - # the dialect's hex function wraps the field - self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out)) - else: - # intentional no-op: the field is returned verbatim. The old - # "colname in out" check masked this; pin the exact identity. - self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out)) - - -class TestConcatQuery(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - out = agent.concatQuery("SELECT user FROM users") - self.assertIsInstance(out, str, msg=dbms) - # concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/ - # SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and - # the rest use the SQL-standard ||. Assert the right operator so the - # test fails if the dialect collapses to the wrong concatenation. - expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT) - self.assertIn(expected, out, msg="%s: %s" % (dbms, out)) - - -class TestSimpleConcatenate(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - out = agent.simpleConcatenate("a", "b") - self.assertIsInstance(out, str, msg=dbms) - self.assertIn("a", out, msg=dbms) - self.assertIn("b", out, msg=dbms) - - -class TestForgeUnionQuery(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - count = 3 - out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None, - None, None, "NULL", None) - self.assertIsInstance(out, str, msg=dbms) - self.assertIn("UNION", out.upper(), msg=dbms) - # position -1 with char NULL fills every one of the `count` columns - # with the char, so the NULL char must appear exactly `count` times. - # (a hardcoded "UNION in out" check could not catch a wrong column - # count.) Match NULL as a whole token to avoid matching substrings. - self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count, - msg="%s expected %d NULLs: %s" % (dbms, count, out)) - - -class TestLimitQuery(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - - # Only Mckoi/Raima have an empty limit template and legitimately - # raise; skip exactly those by name rather than swallowing *any* - # exception (which would hide a real regression in another dialect). - if dbms in LIMIT_RAISES: - with self.assertRaises(Exception, msg=dbms): - agent.limitQuery(0, "SELECT user FROM users", "user") - continue - - out = agent.limitQuery(0, "SELECT user FROM users", "user") - self.assertIsInstance(out, str, msg=dbms) - - if dbms in LIMIT_PASSTHROUGH: - # these dialects have no dedicated limitQuery branch and return - # the query unchanged (documented no-op). - self.assertEqual(out, "SELECT user FROM users", msg=dbms) - else: - # every other running dialect emits a real limit construct - self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS), - msg="%s missing limit token: %s" % (dbms, out)) - - -class TestForgeCaseStatement(unittest.TestCase): - def test_all_dialects(self): - for dbms in DIALECTS: - set_dbms(dbms) - out = agent.forgeCaseStatement("1=1") - self.assertIsInstance(out, str, msg=dbms) - # dialects vary on the conditional form (CASE / IIF / IF); the - # condition itself is always embedded - self.assertIn("1=1", out, msg=dbms) - # ...but the conditional construct itself must also be present, - # otherwise the "1=1" check alone could pass on a degenerate output. - self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out, - msg="%s missing conditional construct: %s" % (dbms, out)) - - -class TestPrefixSuffixAcrossDialects(unittest.TestCase): - def test_prefix_suffix(self): - for dbms in DIALECTS: - set_dbms(dbms) - prefix = agent.prefixQuery("1=1") - suffix = agent.suffixQuery("1=1") - self.assertIsInstance(prefix, str, msg=dbms) - self.assertIsInstance(suffix, str, msg=dbms) - # prefixQuery pads a leading space ahead of the expression by default - self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix)) - # suffixQuery returns the expression itself (no extra clause/comment) - self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix)) - - -class TestRunAsDBMSUserAndWhere(unittest.TestCase): - def test_run_as_user_noop_without_conf(self): - for dbms in DIALECTS: - set_dbms(dbms) - # without conf.dbmsCred the query is returned unchanged - self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_brute.py b/tests/test_brute.py new file mode 100644 index 000000000..3d8143b91 --- /dev/null +++ b/tests/test_brute.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for lib/utils/brute.py. + +tableExists / columnExists are driven with conf.direct=True and the external +collaborators (inject.checkBooleanExpression, getFileItems, runThreads, +getPageWordSet) monkeypatched so the check runs synchronously, deterministically +and offline; plus _addPageTextWords. + +Any global conf/kb/Backend state that a call reads or writes is snapshotted in +setUp and restored in tearDown so test ordering is irrelevant. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import DBMS + +import lib.utils.brute as brute +from lib.request import inject + + +class DbmsStateMixin(object): + """Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak.""" + + def setUp(self): + self._forcedDbms = kb.forcedDbms + self._sticky = kb.stickyDBMS + self._batch = conf.batch + conf.batch = True + + def tearDown(self): + kb.forcedDbms = self._forcedDbms + kb.stickyDBMS = self._sticky + conf.batch = self._batch + + +class TestBrute(DbmsStateMixin, unittest.TestCase): + """Drive tableExists / columnExists with all external collaborators stubbed. + + conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression, + getFileItems and runThreads are monkeypatched so the check runs synchronously, + deterministically and offline. getPageWordSet is neutralized so the wordlist is + just what the stub returns. + """ + + def setUp(self): + DbmsStateMixin.setUp(self) + self._saved_conf = {k: conf.get(k) for k in + ("direct", "db", "tbl", "threads", "api", "verbose")} + self._choices = kb.choices + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + self._brute = kb.brute + self._origPage = kb.originalPage + + # stub the collaborators + self._orig_cbe = inject.checkBooleanExpression + self._orig_brute_cbe = brute.inject.checkBooleanExpression + self._orig_getFileItems = brute.getFileItems + self._orig_runThreads = brute.runThreads + self._orig_getPageWordSet = brute.getPageWordSet + + from lib.core.datatype import AttribDict + kb.choices = AttribDict(keycheck=False) + kb.choices.tableExists = None + kb.choices.columnExists = None + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.brute = AttribDict({"tables": [], "columns": []}) + kb.originalPage = None + + conf.direct = True + conf.db = None + conf.threads = 1 + conf.api = False + conf.verbose = 0 + + # runThreads -> just call the worker once synchronously + def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs): + kb.threadContinue = True + threadFunction() + brute.runThreads = _fakeRunThreads + # no page words injected into the wordlist + brute.getPageWordSet = lambda page: set() + # wordlist file -> small fixed list + brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"] + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + kb.choices = self._choices + if self._cachedTables is None: + kb.data.pop("cachedTables", None) + else: + kb.data.cachedTables = self._cachedTables + if self._cachedColumns is None: + kb.data.pop("cachedColumns", None) + else: + kb.data.cachedColumns = self._cachedColumns + kb.brute = self._brute + kb.originalPage = self._origPage + brute.inject.checkBooleanExpression = self._orig_brute_cbe + brute.getFileItems = self._orig_getFileItems + brute.runThreads = self._orig_runThreads + brute.getPageWordSet = self._orig_getPageWordSet + DbmsStateMixin.tearDown(self) + + def test_table_exists_collects_true_results(self): + set_dbms(DBMS.MYSQL) + + def _cbe(expression, expectingNone=True): + # initial sanity probe (random table) -> must be False, otherwise the + # function raises SqlmapDataException; then only "users" exists. + return "users" in expression + brute.inject.checkBooleanExpression = _cbe + + result = brute.tableExists("/nonexistent/tables.txt") + # cachedTables keyed by conf.db (None here) holds the discovered table + self.assertIn(None, result) + self.assertIn("users", result[None]) + self.assertNotIn("logs", result.get(None, [])) + # also recorded in kb.brute.tables as (db, table) + self.assertIn((None, "users"), kb.brute.tables) + + def test_table_exists_invalid_results_raises(self): + from lib.core.exception import SqlmapDataException + set_dbms(DBMS.MYSQL) + # the initial random-table probe returns True -> "invalid results" guard + brute.inject.checkBooleanExpression = lambda *a, **k: True + with self.assertRaises(SqlmapDataException): + brute.tableExists("/nonexistent/tables.txt") + + def test_column_exists_requires_table(self): + from lib.core.exception import SqlmapMissingMandatoryOptionException + set_dbms(DBMS.MYSQL) + conf.tbl = None + # the sanity probe is False so we reach the missing-table guard + brute.inject.checkBooleanExpression = lambda *a, **k: False + with self.assertRaises(SqlmapMissingMandatoryOptionException): + brute.columnExists("/nonexistent/columns.txt") + + def test_column_exists_collects_and_types(self): + set_dbms(DBMS.MYSQL) + conf.tbl = "users" + brute.getFileItems = lambda *a, **k: ["id", "name"] + + calls = {"n": 0} + + def _cbe(expression, expectingNone=True): + calls["n"] += 1 + # initial sanity probe uses two random strings (no real column name) + if "id" not in expression and "name" not in expression: + return False + # MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`. + # 'id' is numeric (no non-digit chars => probe False => numeric); + # 'name' is non-numeric (has non-digit chars => probe True => non-numeric). + if "REGEXP" in expression: + return "name" in expression + # plain existence check (EXISTS(SELECT FROM )) => 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"admin password 1abc xy verylongword" + 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) diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 000000000..40f578ec8 --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,1715 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Consolidated unit coverage for lib/core/common.py. + +This module merges the previously separate test_common_utils.py, +test_common_parsers.py and the common.py-specific classes from +test_core_more.py, test_core_extra.py and test_core_final.py into a single +file. Test logic is unchanged from those sources. + +Everything runs in isolation (no network, no DBMS, no persistent filesystem +mutation of the project). 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. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import base64 +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 conf, kb, paths +from lib.core.defaults import defaults +from lib.core.enums import ( + CHARSET_TYPE, + DBMS, + EXPECTED, + HTTPMETHOD, + PLACE, + SORT_ORDER, +) +from lib.core.exception import ( + SqlmapSystemException, +) +from lib.core.settings import ( + NULL, + PAYLOAD_DELIMITER, + REFLECTED_VALUE_MARKER, +) +from lib.core.common import ( + aliasToDbmsEnum, + applyFunctionRecursively, + arrayizeValue, + Backend, + boldifyMessage, + calculateDeltaSeconds, + checkFile, + checkOldOptions, + checkSystemEncoding, + cleanReplaceUnicode, + commonFinderOnly, + enumValueToNameLookup, + extractErrorMessage, + extractExpectedValue, + extractRegexResult, + extractTextTagContent, + filePathToSafeString, + filterListValue, + filterNone, + filterPairValues, + filterStringValue, + findMultipartPostBoundary, + findPageForms, + flattenValue, + Format, + getCharset, + getFilteredPageContent, + getHeader, + getLimitRange, + getPageWordSet, + getPartRun, + getRequestHeader, + getSQLSnippet, + getTechnique, + getText, + intersect, + isListLike, + isNoneValue, + isNullValue, + isNumber, + isNumPosStrValue, + isWindowsDriveLetterPath, + isZipFile, + joinValue, + listToStrValue, + normalizeUnicode, + paramToDict, + parseJson, + parsePasswordHash, + parseRequestFile, + parseTargetDirect, + parseTargetUrl, + parseUnionPage, + removePostHintPrefix, + removeReflectiveValues, + resetCookieJar, + safeExpandUser, + safeFilepathEncode, + safeStringFormat, + safeSQLIdentificatorNaming, + saveConfig, + serializeObject, + setTechnique, + splitFields, + trimAlphaNum, + unArrayizeValue, + unserializeObject, + urlencode, + zeroDepthSearch, +) + +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 _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() + + +# =========================================================================== # +# from tests/test_common_utils.py +# =========================================================================== # + +class TestParamToDict(unittest.TestCase): + """Parameter string -> OrderedDict for the various injection places.""" + + def test_get_two_params(self): + result = paramToDict(PLACE.GET, "id=1&name=foo") + self.assertEqual(list(result.items()), [("id", "1"), ("name", "foo")]) + + def test_get_preserves_order(self): + result = paramToDict(PLACE.GET, "c=3&a=1&b=2") + self.assertEqual(list(result.keys()), ["c", "a", "b"]) + + def test_post_place(self): + result = paramToDict(PLACE.POST, "user=admin&pass=secret") + self.assertEqual(result["user"], "admin") + self.assertEqual(result["pass"], "secret") + + def test_empty_value(self): + result = paramToDict(PLACE.GET, "id=&name=x") + self.assertEqual(result["id"], "") + self.assertEqual(result["name"], "x") + + def test_value_with_equal_signs(self): + # value is re-joined on '=' so embedded '=' survives + result = paramToDict(PLACE.GET, "token=a=b=c") + self.assertEqual(result["token"], "a=b=c") + + def test_cookie_delimiter(self): + # COOKIE place splits on ';' rather than '&' + result = paramToDict(PLACE.COOKIE, "foo=bar;baz=qux") + self.assertEqual(list(result.items()), [("foo", "bar"), ("baz", "qux")]) + + def test_param_without_equals_ignored(self): + # an element with no '=' has len(parts) < 2 and is skipped + result = paramToDict(PLACE.GET, "lonely&id=1") + self.assertEqual(list(result.items()), [("id", "1")]) + + +class TestGetCharset(unittest.TestCase): + """Inference charsets are fixed integer tables.""" + + def test_binary(self): + self.assertEqual(getCharset(CHARSET_TYPE.BINARY), [0, 1, 47, 48, 49]) + + def test_default_is_full_ascii(self): + self.assertEqual(getCharset(None), list(range(0, 128))) + + def test_digits(self): + result = getCharset(CHARSET_TYPE.DIGITS) + self.assertEqual(result, list(range(0, 10)) + list(range(47, 58))) + + def test_alpha_has_no_digits(self): + result = getCharset(CHARSET_TYPE.ALPHA) + # ASCII codes for '0'..'9' are 48..57; ALPHA must exclude them + self.assertFalse(any(48 <= _ <= 57 for _ in result)) + self.assertIn(ord("A"), result) + self.assertIn(ord("z"), result) + + def test_alphanum_superset_of_alpha(self): + alpha = set(getCharset(CHARSET_TYPE.ALPHA)) + alphanum = set(getCharset(CHARSET_TYPE.ALPHANUM)) + self.assertTrue(alpha.issubset(alphanum)) + self.assertIn(ord("5"), alphanum) + + def test_hexadecimal_contains_hex_letters(self): + result = getCharset(CHARSET_TYPE.HEXADECIMAL) + for ch in "0123456789abcdefABCDEF": + self.assertIn(ord(ch), result, msg="missing %r" % ch) + + +class TestGetLimitRange(unittest.TestCase): + def test_basic(self): + self.assertEqual(list(getLimitRange(10)), list(range(0, 10))) + + def test_plus_one(self): + self.assertEqual(list(getLimitRange(3, plusOne=True)), [1, 2, 3]) + + def test_string_count_coerced(self): + # count is int()-coerced internally + self.assertEqual(list(getLimitRange("4")), [0, 1, 2, 3]) + + def test_length(self): + self.assertEqual(len(getLimitRange(7)), 7) + + +class TestParseUnionPage(unittest.TestCase): + def test_none(self): + self.assertIsNone(parseUnionPage(None)) + + def test_two_entries(self): + page = "%sfoo%s%sbar%s" % (kb.chars.start, kb.chars.stop, kb.chars.start, kb.chars.stop) + # returns a BigArray; compare element-wise + self.assertEqual(list(parseUnionPage(page)), ["foo", "bar"]) + + def test_single_entry_unwrapped(self): + # a lone wrapped string is returned as the bare string, not a 1-element list + page = "%shello%s" % (kb.chars.start, kb.chars.stop) + self.assertEqual(parseUnionPage(page), "hello") + + def test_multi_column_row(self): + # a single row whose values are joined by kb.chars.delimiter becomes one + # nested list entry + page = "%sa%sb%s" % (kb.chars.start, kb.chars.delimiter, kb.chars.stop) + self.assertEqual(list(parseUnionPage(page)), [["a", "b"]]) + + def test_unmarked_page_returned_verbatim(self): + self.assertEqual(parseUnionPage("no markers here"), "no markers here") + + +class TestSafeStringFormat(unittest.TestCase): + def test_basic_tuple(self): + self.assertEqual(safeStringFormat("SELECT foo FROM %s LIMIT %d", ("bar", "1")), + "SELECT foo FROM bar LIMIT 1") + + def test_literal_percent_preserved(self): + self.assertEqual( + safeStringFormat("SELECT foo FROM %s WHERE name LIKE '%susan%' LIMIT %d", ("bar", "1")), + "SELECT foo FROM bar WHERE name LIKE '%susan%' LIMIT 1") + + def test_single_string_param(self): + self.assertEqual(safeStringFormat("a %s b", "X"), "a X b") + + def test_scalar_non_string(self): + self.assertEqual(safeStringFormat("n=%d", 5), "n=5") + + +class TestUrlencode(unittest.TestCase): + def test_basic(self): + self.assertEqual(urlencode("AND 1>(2+3)#"), "AND%201%3E%282%2B3%29%23") + + def test_none(self): + self.assertIsNone(urlencode(None)) + + def test_spaceplus(self): + self.assertEqual(urlencode("a b", spaceplus=True), "a+b") + + def test_convall_encodes_safe_chars(self): + # with convall the explicit 'safe' set is dropped, so '/' gets encoded + self.assertEqual(urlencode("a/b", convall=True), "a%2Fb") + + def test_safe_char_default_kept(self): + # by default '-' and '_' are in the safe set + self.assertEqual(urlencode("a-b_c"), "a-b_c") + + +class TestParseTargetUrl(unittest.TestCase): + """parseTargetUrl mutates conf.* in place; save and restore everything touched.""" + + def _save(self): + return {k: conf.get(k) for k in + ("url", "scheme", "path", "hostname", "port", "ipv6")} + + def _restore(self, saved): + for k, v in saved.items(): + conf[k] = v + + def test_https_url(self): + saved = self._save() + orig_params = conf.parameters.get(PLACE.GET) + try: + conf.url = "https://www.test.com/?id=1" + parseTargetUrl() + self.assertEqual(conf.hostname, "www.test.com") + self.assertEqual(conf.scheme, "https") + self.assertEqual(conf.port, 443) + self.assertEqual(conf.parameters[PLACE.GET], "id=1") + finally: + self._restore(saved) + if orig_params is None: + conf.parameters.pop(PLACE.GET, None) + else: + conf.parameters[PLACE.GET] = orig_params + + def test_scheme_defaulted_and_port(self): + saved = self._save() + try: + conf.url = "example.org:8080/app" + parseTargetUrl() + self.assertEqual(conf.hostname, "example.org") + self.assertEqual(conf.scheme, "http") + self.assertEqual(conf.port, 8080) + finally: + self._restore(saved) + + def test_empty_url_returns_none(self): + saved = self._save() + try: + conf.url = "" + self.assertIsNone(parseTargetUrl()) + finally: + self._restore(saved) + + +class TestParseTargetDirect(unittest.TestCase): + """parseTargetDirect under smokeMode (early-returns before driver imports).""" + + def _save(self): + return {k: conf.get(k) for k in + ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")} + + def _restore(self, saved): + for k, v in saved.items(): + conf[k] = v + + def test_full_mysql_dsn(self): + saved = self._save() + orig_smoke = kb.smokeMode + orig_none = conf.parameters.get(None) + try: + kb.smokeMode = True + conf.direct = "mysql://root:testpass@127.0.0.1:3306/testdb" + parseTargetDirect() + self.assertEqual(conf.dbms, "mysql") + self.assertEqual(conf.dbmsUser, "root") + self.assertEqual(conf.dbmsPass, "testpass") + self.assertEqual(conf.dbmsDb, "testdb") + self.assertEqual(conf.hostname, "127.0.0.1") + self.assertEqual(conf.port, 3306) + finally: + self._restore(saved) + kb.smokeMode = orig_smoke + if orig_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = orig_none + + def test_quoted_password(self): + saved = self._save() + orig_smoke = kb.smokeMode + orig_none = conf.parameters.get(None) + try: + kb.smokeMode = True + conf.direct = "mysql://user:'P@ssw0rd'@127.0.0.1:3306/test" + parseTargetDirect() + self.assertEqual(conf.dbmsPass, "P@ssw0rd") + self.assertEqual(conf.hostname, "127.0.0.1") + finally: + self._restore(saved) + kb.smokeMode = orig_smoke + if orig_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = orig_none + + def test_empty_direct_returns_none(self): + saved = self._save() + try: + conf.direct = None + self.assertIsNone(parseTargetDirect()) + finally: + self._restore(saved) + + +class TestSafeSQLIdentificatorNaming(unittest.TestCase): + """Quoting of identifiers is DBMS-specific; drive it via kb.forcedDbms.""" + + def _run(self, dbms, name, **kw): + orig = kb.forcedDbms + try: + kb.forcedDbms = dbms + return getText(safeSQLIdentificatorNaming(name, **kw)) + finally: + kb.forcedDbms = orig + + def test_mssql_keyword_bracketed(self): + self.assertEqual(self._run(DBMS.MSSQL, "begin"), "[begin]") + + def test_plain_name_unquoted(self): + self.assertEqual(self._run(DBMS.MSSQL, "foobar"), "foobar") + + def test_firebird_name_with_space_double_quoted(self): + self.assertEqual(self._run(DBMS.FIREBIRD, "foo bar"), '"foo bar"') + + def test_mysql_keyword_backticked(self): + self.assertEqual(self._run(DBMS.MYSQL, "select"), "`select`") + + def test_oracle_keyword_uppercased(self): + # Oracle quotes AND uppercases reserved words + self.assertEqual(self._run(DBMS.ORACLE, "table"), '"TABLE"') + + def test_unsafe_naming_passthrough(self): + orig = conf.unsafeNaming + try: + conf.unsafeNaming = True + self.assertEqual(self._run(DBMS.MYSQL, "select"), "select") + finally: + conf.unsafeNaming = orig + + +class TestGetPartRun(unittest.TestCase): + def test_no_dbms_handler_in_stack(self): + # called from a test (no conf.dbmsHandler.* on the stack) -> None + self.assertIsNone(getPartRun()) + + def test_non_alias_form_also_none(self): + self.assertIsNone(getPartRun(alias=False)) + + +# =========================================================================== # +# from tests/test_common_parsers.py +# =========================================================================== # + +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 = ('80' + '' + '' % 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"You searched for 1 AND 1=2 here" + 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"nothing interesting" + out = removeReflectiveValues(content, "1 AND 1=2") + self.assertEqual(out, content) + + def test_none_payload_returns_content(self): + content = u"x" + 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"1 AND 1=2" + 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 = ('
' + '' + '
') + 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 = ('
' + '' + '
') + 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 = "" + 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") + + +# =========================================================================== # +# from tests/test_core_more.py (common.py classes) +# =========================================================================== # + +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"foobartest") + 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)) + + +# =========================================================================== # +# from tests/test_core_extra.py (common.py classes) +# =========================================================================== # + +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"))) + + +# =========================================================================== # +# from tests/test_core_final.py (common.py classes) +# =========================================================================== # + +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[^g]+)g", "abcdefg"), "bcdef") + + def test_extract_regex_result_no_match(self): + self.assertIsNone(extractRegexResult(r"a(?P[^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.)b", "")) + + def test_extract_text_tag_content(self): + self.assertEqual( + extractTextTagContent("Title
foobar
"), + ["Title", "foobar"], + ) + + def test_extract_text_tag_content_empty(self): + self.assertEqual(extractTextTagContent(""), []) + + def test_get_filtered_page_content(self): + self.assertEqual( + getFilteredPageContent(u"foobartest"), + "foobar test", + ) + + def test_get_filtered_page_content_drops_script(self): + page = u"hello" + 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"Test\nWarning: oci_parse() " + u"[function.oci-parse]: ORA-01756: quoted string not properly " + u"terminated

Only a test page

") + 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"])) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_common_parsers.py b/tests/test_common_parsers.py deleted file mode 100644 index 4c2882990..000000000 --- a/tests/test_common_parsers.py +++ /dev/null @@ -1,466 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Pure / near-pure parsers and state helpers in lib/core/common.py that are NOT -already exercised by tests/test_common_utils.py. - -Covered here: - * proxy-log parsers reached through parseRequestFile() - (_parseBurpLog plain log, _parseBurpLog Burp XML history, _parseWebScarabLog) - * parseTargetDirect() non-smoke branch (driver resolution for SQLite) - * removeReflectiveValues() reflected-payload masking - * findPageForms() HTML
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 = ('80' - '' - '' % 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"You searched for 1 AND 1=2 here" - 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"nothing interesting" - out = removeReflectiveValues(content, "1 AND 1=2") - self.assertEqual(out, content) - - def test_none_payload_returns_content(self): - content = u"x" - 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"1 AND 1=2" - 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 = ('' - '' - '
') - 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 = ('
' - '' - '
') - 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 = "" - 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) diff --git a/tests/test_common_utils.py b/tests/test_common_utils.py deleted file mode 100644 index 9faa815f7..000000000 --- a/tests/test_common_utils.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Pure / near-pure helpers in lib/core/common.py. - -These cover the request/parameter parsing, charset construction, limit-range -generation, safe string formatting, URL encoding, UNION page parsing, target -URL/direct-connection parsing and SQL identifier quoting. They are exercised -in isolation (no network, no DBMS, no filesystem mutation); any function that -reads/writes global conf/kb state has that state saved and restored around the -call so test ordering stays irrelevant. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.common import ( - paramToDict, - getCharset, - getLimitRange, - parseUnionPage, - safeStringFormat, - urlencode, - parseTargetUrl, - parseTargetDirect, - safeSQLIdentificatorNaming, - getPartRun, - getText, -) -from lib.core.data import kb, conf -from lib.core.enums import PLACE, CHARSET_TYPE, DBMS - - -class TestParamToDict(unittest.TestCase): - """Parameter string -> OrderedDict for the various injection places.""" - - def test_get_two_params(self): - result = paramToDict(PLACE.GET, "id=1&name=foo") - self.assertEqual(list(result.items()), [("id", "1"), ("name", "foo")]) - - def test_get_preserves_order(self): - result = paramToDict(PLACE.GET, "c=3&a=1&b=2") - self.assertEqual(list(result.keys()), ["c", "a", "b"]) - - def test_post_place(self): - result = paramToDict(PLACE.POST, "user=admin&pass=secret") - self.assertEqual(result["user"], "admin") - self.assertEqual(result["pass"], "secret") - - def test_empty_value(self): - result = paramToDict(PLACE.GET, "id=&name=x") - self.assertEqual(result["id"], "") - self.assertEqual(result["name"], "x") - - def test_value_with_equal_signs(self): - # value is re-joined on '=' so embedded '=' survives - result = paramToDict(PLACE.GET, "token=a=b=c") - self.assertEqual(result["token"], "a=b=c") - - def test_cookie_delimiter(self): - # COOKIE place splits on ';' rather than '&' - result = paramToDict(PLACE.COOKIE, "foo=bar;baz=qux") - self.assertEqual(list(result.items()), [("foo", "bar"), ("baz", "qux")]) - - def test_param_without_equals_ignored(self): - # an element with no '=' has len(parts) < 2 and is skipped - result = paramToDict(PLACE.GET, "lonely&id=1") - self.assertEqual(list(result.items()), [("id", "1")]) - - -class TestGetCharset(unittest.TestCase): - """Inference charsets are fixed integer tables.""" - - def test_binary(self): - self.assertEqual(getCharset(CHARSET_TYPE.BINARY), [0, 1, 47, 48, 49]) - - def test_default_is_full_ascii(self): - self.assertEqual(getCharset(None), list(range(0, 128))) - - def test_digits(self): - result = getCharset(CHARSET_TYPE.DIGITS) - self.assertEqual(result, list(range(0, 10)) + list(range(47, 58))) - - def test_alpha_has_no_digits(self): - result = getCharset(CHARSET_TYPE.ALPHA) - # ASCII codes for '0'..'9' are 48..57; ALPHA must exclude them - self.assertFalse(any(48 <= _ <= 57 for _ in result)) - self.assertIn(ord("A"), result) - self.assertIn(ord("z"), result) - - def test_alphanum_superset_of_alpha(self): - alpha = set(getCharset(CHARSET_TYPE.ALPHA)) - alphanum = set(getCharset(CHARSET_TYPE.ALPHANUM)) - self.assertTrue(alpha.issubset(alphanum)) - self.assertIn(ord("5"), alphanum) - - def test_hexadecimal_contains_hex_letters(self): - result = getCharset(CHARSET_TYPE.HEXADECIMAL) - for ch in "0123456789abcdefABCDEF": - self.assertIn(ord(ch), result, msg="missing %r" % ch) - - -class TestGetLimitRange(unittest.TestCase): - def test_basic(self): - self.assertEqual(list(getLimitRange(10)), list(range(0, 10))) - - def test_plus_one(self): - self.assertEqual(list(getLimitRange(3, plusOne=True)), [1, 2, 3]) - - def test_string_count_coerced(self): - # count is int()-coerced internally - self.assertEqual(list(getLimitRange("4")), [0, 1, 2, 3]) - - def test_length(self): - self.assertEqual(len(getLimitRange(7)), 7) - - -class TestParseUnionPage(unittest.TestCase): - def test_none(self): - self.assertIsNone(parseUnionPage(None)) - - def test_two_entries(self): - page = "%sfoo%s%sbar%s" % (kb.chars.start, kb.chars.stop, kb.chars.start, kb.chars.stop) - # returns a BigArray; compare element-wise - self.assertEqual(list(parseUnionPage(page)), ["foo", "bar"]) - - def test_single_entry_unwrapped(self): - # a lone wrapped string is returned as the bare string, not a 1-element list - page = "%shello%s" % (kb.chars.start, kb.chars.stop) - self.assertEqual(parseUnionPage(page), "hello") - - def test_multi_column_row(self): - # a single row whose values are joined by kb.chars.delimiter becomes one - # nested list entry - page = "%sa%sb%s" % (kb.chars.start, kb.chars.delimiter, kb.chars.stop) - self.assertEqual(list(parseUnionPage(page)), [["a", "b"]]) - - def test_unmarked_page_returned_verbatim(self): - self.assertEqual(parseUnionPage("no markers here"), "no markers here") - - -class TestSafeStringFormat(unittest.TestCase): - def test_basic_tuple(self): - self.assertEqual(safeStringFormat("SELECT foo FROM %s LIMIT %d", ("bar", "1")), - "SELECT foo FROM bar LIMIT 1") - - def test_literal_percent_preserved(self): - self.assertEqual( - safeStringFormat("SELECT foo FROM %s WHERE name LIKE '%susan%' LIMIT %d", ("bar", "1")), - "SELECT foo FROM bar WHERE name LIKE '%susan%' LIMIT 1") - - def test_single_string_param(self): - self.assertEqual(safeStringFormat("a %s b", "X"), "a X b") - - def test_scalar_non_string(self): - self.assertEqual(safeStringFormat("n=%d", 5), "n=5") - - -class TestUrlencode(unittest.TestCase): - def test_basic(self): - self.assertEqual(urlencode("AND 1>(2+3)#"), "AND%201%3E%282%2B3%29%23") - - def test_none(self): - self.assertIsNone(urlencode(None)) - - def test_spaceplus(self): - self.assertEqual(urlencode("a b", spaceplus=True), "a+b") - - def test_convall_encodes_safe_chars(self): - # with convall the explicit 'safe' set is dropped, so '/' gets encoded - self.assertEqual(urlencode("a/b", convall=True), "a%2Fb") - - def test_safe_char_default_kept(self): - # by default '-' and '_' are in the safe set - self.assertEqual(urlencode("a-b_c"), "a-b_c") - - -class TestParseTargetUrl(unittest.TestCase): - """parseTargetUrl mutates conf.* in place; save and restore everything touched.""" - - def _save(self): - return {k: conf.get(k) for k in - ("url", "scheme", "path", "hostname", "port", "ipv6")} - - def _restore(self, saved): - for k, v in saved.items(): - conf[k] = v - - def test_https_url(self): - saved = self._save() - orig_params = conf.parameters.get(PLACE.GET) - try: - conf.url = "https://www.test.com/?id=1" - parseTargetUrl() - self.assertEqual(conf.hostname, "www.test.com") - self.assertEqual(conf.scheme, "https") - self.assertEqual(conf.port, 443) - self.assertEqual(conf.parameters[PLACE.GET], "id=1") - finally: - self._restore(saved) - if orig_params is None: - conf.parameters.pop(PLACE.GET, None) - else: - conf.parameters[PLACE.GET] = orig_params - - def test_scheme_defaulted_and_port(self): - saved = self._save() - try: - conf.url = "example.org:8080/app" - parseTargetUrl() - self.assertEqual(conf.hostname, "example.org") - self.assertEqual(conf.scheme, "http") - self.assertEqual(conf.port, 8080) - finally: - self._restore(saved) - - def test_empty_url_returns_none(self): - saved = self._save() - try: - conf.url = "" - self.assertIsNone(parseTargetUrl()) - finally: - self._restore(saved) - - -class TestParseTargetDirect(unittest.TestCase): - """parseTargetDirect under smokeMode (early-returns before driver imports).""" - - def _save(self): - return {k: conf.get(k) for k in - ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")} - - def _restore(self, saved): - for k, v in saved.items(): - conf[k] = v - - def test_full_mysql_dsn(self): - saved = self._save() - orig_smoke = kb.smokeMode - orig_none = conf.parameters.get(None) - try: - kb.smokeMode = True - conf.direct = "mysql://root:testpass@127.0.0.1:3306/testdb" - parseTargetDirect() - self.assertEqual(conf.dbms, "mysql") - self.assertEqual(conf.dbmsUser, "root") - self.assertEqual(conf.dbmsPass, "testpass") - self.assertEqual(conf.dbmsDb, "testdb") - self.assertEqual(conf.hostname, "127.0.0.1") - self.assertEqual(conf.port, 3306) - finally: - self._restore(saved) - kb.smokeMode = orig_smoke - if orig_none is None: - conf.parameters.pop(None, None) - else: - conf.parameters[None] = orig_none - - def test_quoted_password(self): - saved = self._save() - orig_smoke = kb.smokeMode - orig_none = conf.parameters.get(None) - try: - kb.smokeMode = True - conf.direct = "mysql://user:'P@ssw0rd'@127.0.0.1:3306/test" - parseTargetDirect() - self.assertEqual(conf.dbmsPass, "P@ssw0rd") - self.assertEqual(conf.hostname, "127.0.0.1") - finally: - self._restore(saved) - kb.smokeMode = orig_smoke - if orig_none is None: - conf.parameters.pop(None, None) - else: - conf.parameters[None] = orig_none - - def test_empty_direct_returns_none(self): - saved = self._save() - try: - conf.direct = None - self.assertIsNone(parseTargetDirect()) - finally: - self._restore(saved) - - -class TestSafeSQLIdentificatorNaming(unittest.TestCase): - """Quoting of identifiers is DBMS-specific; drive it via kb.forcedDbms.""" - - def _run(self, dbms, name, **kw): - orig = kb.forcedDbms - try: - kb.forcedDbms = dbms - return getText(safeSQLIdentificatorNaming(name, **kw)) - finally: - kb.forcedDbms = orig - - def test_mssql_keyword_bracketed(self): - self.assertEqual(self._run(DBMS.MSSQL, "begin"), "[begin]") - - def test_plain_name_unquoted(self): - self.assertEqual(self._run(DBMS.MSSQL, "foobar"), "foobar") - - def test_firebird_name_with_space_double_quoted(self): - self.assertEqual(self._run(DBMS.FIREBIRD, "foo bar"), '"foo bar"') - - def test_mysql_keyword_backticked(self): - self.assertEqual(self._run(DBMS.MYSQL, "select"), "`select`") - - def test_oracle_keyword_uppercased(self): - # Oracle quotes AND uppercases reserved words - self.assertEqual(self._run(DBMS.ORACLE, "table"), '"TABLE"') - - def test_unsafe_naming_passthrough(self): - orig = conf.unsafeNaming - try: - conf.unsafeNaming = True - self.assertEqual(self._run(DBMS.MYSQL, "select"), "select") - finally: - conf.unsafeNaming = orig - - -class TestGetPartRun(unittest.TestCase): - def test_no_dbms_handler_in_stack(self): - # called from a test (no conf.dbmsHandler.* on the stack) -> None - self.assertIsNone(getPartRun()) - - def test_non_alias_form_also_none(self): - self.assertIsNone(getPartRun(alias=False)) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_compat.py b/tests/test_compat.py index 69edf2e7a..98c544344 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -21,8 +21,7 @@ bootstrap() from lib.core.compat import (WichmannHill, patchHeaders, cmp, choose_boundary, round, cmp_to_key, LooseVersion, _is_write_mode, - MixedWriteTextIO, _codecs_open, codecs_open) -from lib.core.compat import xrange + MixedWriteTextIO, _codecs_open) class TestWichmannHill(unittest.TestCase): diff --git a/tests/test_core_extra.py b/tests/test_core_extra.py deleted file mode 100644 index 5c1a5a282..000000000 --- a/tests/test_core_extra.py +++ /dev/null @@ -1,676 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional REAL unit coverage for genuinely-uncovered PURE functions in: - - * lib/core/common.py - * lib/core/option.py - * lib/core/agent.py - * lib/request/basic.py - -Every test asserts a concrete, independently-reasoned known-correct value that -would FAIL if the function under test regressed. No isinstance-only checks, no -tautologies, no swallowed exceptions. - -Functions targeted here are deliberately DIFFERENT from those already exercised -by tests/test_common_utils.py, test_common_parsers.py, test_core_more.py, -test_core_final.py, test_option_setup.py, test_option_more.py, test_agent.py, -test_agent_dialects.py, test_decodepage.py and test_charset.py. - -stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from tests._testutils import bootstrap, set_dbms - -bootstrap() - -from lib.core.data import conf, kb -from lib.core.defaults import defaults -from lib.core.common import Backend -from lib.core.enums import DBMS - - -class TestCommonStringHelpers(unittest.TestCase): - """Small pure string/list/regex/encoding helpers in lib/core/common.py.""" - - def test_posix_to_nt_slashes(self): - from lib.core.common import posixToNtSlashes - self.assertEqual(posixToNtSlashes("C:/Windows"), "C:\\Windows") - self.assertEqual(posixToNtSlashes("a/b/c"), "a\\b\\c") - # falsy input returned unchanged - self.assertEqual(posixToNtSlashes(""), "") - self.assertIsNone(posixToNtSlashes(None)) - - def test_nt_to_posix_slashes(self): - from lib.core.common import ntToPosixSlashes - self.assertEqual(ntToPosixSlashes("C:\\Windows"), "C:/Windows") - self.assertEqual(ntToPosixSlashes("a\\b\\c"), "a/b/c") - self.assertEqual(ntToPosixSlashes(""), "") - - def test_is_hex_encoded_string(self): - from lib.core.common import isHexEncodedString - self.assertTrue(isHexEncodedString("DEADBEEF")) - self.assertTrue(isHexEncodedString("0x1234")) # 'x' is allowed by the regex - self.assertFalse(isHexEncodedString("test")) - self.assertFalse(isHexEncodedString("12 34")) # space breaks it - - def test_is_digit(self): - from lib.core.common import isDigit - self.assertTrue(isDigit("123456")) - self.assertFalse(isDigit("3b3")) - self.assertFalse(isDigit(u"\xb2")) # superscript-2: str.isdigit() True, isDigit False - self.assertFalse(isDigit("")) # empty -> no match - self.assertFalse(isDigit(None)) - - def test_sanitize_str(self): - from lib.core.common import sanitizeStr - self.assertEqual(sanitizeStr("foo\n\rbar"), "foo bar") - self.assertEqual(sanitizeStr("a\r\nb"), "a b") - self.assertEqual(sanitizeStr(None), "None") - - def test_filter_control_chars(self): - from lib.core.common import filterControlChars - self.assertEqual(filterControlChars("AND 1>(2+3)\n--"), "AND 1>(2+3) --") - # custom replacement character - self.assertEqual(filterControlChars("a\tb", replacement="_"), "a_b") - - def test_normalize_path(self): - from lib.core.common import normalizePath - self.assertEqual(normalizePath("//var///log/apache.log"), "/var/log/apache.log") - self.assertEqual(normalizePath("/a/b/../c"), "/a/c") - - def test_directory_path(self): - from lib.core.common import directoryPath - self.assertEqual(directoryPath("/var/log/apache.log"), "/var/log") - # no extension -> returned unchanged - self.assertEqual(directoryPath("/var/log"), "/var/log") - - def test_longest_common_prefix(self): - from lib.core.common import longestCommonPrefix - self.assertEqual(longestCommonPrefix("foobar", "fobar"), "fo") - self.assertEqual(longestCommonPrefix("abc", "abd", "abe"), "ab") - # single sequence returned verbatim - self.assertEqual(longestCommonPrefix("only"), "only") - - def test_first_not_none(self): - from lib.core.common import firstNotNone - self.assertEqual(firstNotNone(None, None, 1, 2, 3), 1) - self.assertEqual(firstNotNone(None, 0), 0) # 0 is not None - self.assertIsNone(firstNotNone(None, None)) - - def test_decode_string_escape(self): - from lib.core.common import decodeStringEscape - self.assertEqual(decodeStringEscape("a\\tb"), "a\tb") - self.assertEqual(decodeStringEscape("a\\nb"), "a\nb") - # no backslash -> unchanged - self.assertEqual(decodeStringEscape("plain"), "plain") - - def test_encode_string_escape(self): - from lib.core.common import encodeStringEscape - self.assertEqual(encodeStringEscape("a\tb"), "a\\tb") - self.assertEqual(encodeStringEscape("a\nb"), "a\\nb") - self.assertEqual(encodeStringEscape("plain"), "plain") - - def test_decode_encode_string_escape_roundtrip(self): - from lib.core.common import decodeStringEscape, encodeStringEscape - self.assertEqual(decodeStringEscape(encodeStringEscape("x\ty\nz")), "x\ty\nz") - - def test_escape_json_value(self): - from lib.core.common import escapeJsonValue - # newline gets escaped (literal '\n' becomes the two chars backslash+n) - self.assertNotIn("\n", escapeJsonValue("foo\nbar")) - self.assertIn("\\n", escapeJsonValue("foo\nbar")) - # tab gets escaped to '\t' - self.assertIn("\\t", escapeJsonValue("foo\tbar")) - # quote and backslash escaped - self.assertEqual(escapeJsonValue('a"b'), 'a\\"b') - self.assertEqual(escapeJsonValue("a\\b"), "a\\\\b") - # ordinary characters untouched - self.assertEqual(escapeJsonValue("plain text"), "plain text") - - def test_clean_query(self): - from lib.core.common import cleanQuery - self.assertEqual(cleanQuery("select id from users"), "SELECT id FROM users") - # already-uppercase keywords stay; identifiers untouched - self.assertEqual(cleanQuery("SELECT a FROM t"), "SELECT a FROM t") - - def test_json_minimize_canonical(self): - from lib.core.common import jsonMinimize - # key order / whitespace independence - self.assertEqual(jsonMinimize('{"b": 2, "a": 1}'), jsonMinimize('{"a":1, "b":2}')) - # nested leaf path - self.assertEqual(jsonMinimize('{"a": {"b": 1}}'), ".a.b=1") - # empty object - self.assertEqual(jsonMinimize("{}"), "") - # not parseable -> None (and only None) - self.assertIsNone(jsonMinimize("not json")) - - def test_json_minimize_array_length_registers(self): - from lib.core.common import jsonMinimize - # array length change must perturb the projection - self.assertNotEqual(jsonMinimize('{"a": [1, 2]}'), jsonMinimize('{"a": [1, 2, 3]}')) - - def test_list_to_str_value(self): - from lib.core.common import listToStrValue - self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") - # set/tuple/generator normalized via list first - self.assertEqual(listToStrValue((1, 2)), "1, 2") - # non-list passes through - self.assertEqual(listToStrValue("abc"), "abc") - - def test_intersect(self): - from lib.core.common import intersect - self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) - # order follows containerA - self.assertEqual(intersect([3, 2, 1], [1, 2]), [2, 1]) - # case-insensitive option - self.assertEqual(intersect(["FOO", "bar"], ["foo"], lowerCase=True), ["foo"]) - - def test_priority_sort_columns(self): - from lib.core.common import prioritySortColumns - # 'id'-containing columns first, then by ascending length - self.assertEqual( - prioritySortColumns(["password", "userid", "name", "id"]), - ["id", "userid", "name", "password"], - ) - - def test_safe_variable_naming(self): - from lib.core.common import safeVariableNaming - self.assertEqual(safeVariableNaming("class.id"), "EVAL_636c6173732e6964") - # plain identifier left untouched - self.assertEqual(safeVariableNaming("foobar"), "foobar") - - def test_unsafe_variable_naming(self): - from lib.core.common import unsafeVariableNaming - self.assertEqual(unsafeVariableNaming("EVAL_636c6173732e6964"), "class.id") - self.assertEqual(unsafeVariableNaming("foobar"), "foobar") - - def test_variable_naming_roundtrip(self): - from lib.core.common import safeVariableNaming, unsafeVariableNaming - self.assertEqual(unsafeVariableNaming(safeVariableNaming("a-b")), "a-b") - - def test_average(self): - from lib.core.common import average - self.assertAlmostEqual(average([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), 0.9, places=6) - self.assertEqual(average([2, 4]), 3.0) - self.assertIsNone(average([])) - - def test_stdev(self): - from lib.core.common import stdev - self.assertEqual("%.3f" % stdev([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), "0.063") - # fewer than 2 values -> None - self.assertIsNone(stdev([1.0])) - self.assertIsNone(stdev([])) - - -class TestCommonSafeCompare(unittest.TestCase): - """Constant-time / checksum helpers.""" - - def test_safe_compare_strings(self): - from lib.core.common import safeCompareStrings - self.assertTrue(safeCompareStrings("test", "test")) - self.assertFalse(safeCompareStrings("test1", "test2")) - self.assertFalse(safeCompareStrings("test", None)) - # both None compares equal (a == b path) - self.assertTrue(safeCompareStrings(None, None)) - - def test_safe_cs_value(self): - from lib.core.common import safeCSValue - # ensure deterministic delimiter - old = conf.get("csvDel") - conf.csvDel = defaults.csvDel - try: - self.assertEqual(safeCSValue("foo, bar"), '"foo, bar"') - self.assertEqual(safeCSValue("foobar"), "foobar") - self.assertEqual(safeCSValue("foo\rbar"), '"foo\rbar"') - self.assertEqual(safeCSValue('foo"bar'), '"foo""bar"') - finally: - conf.csvDel = old - - -class TestCommonSafeExString(unittest.TestCase): - def test_sqlmap_exception_message(self): - from lib.core.common import getSafeExString - from lib.core.exception import SqlmapBaseException - self.assertEqual(getSafeExString(SqlmapBaseException("foobar")), "foobar") - - def test_oserror_prefixed_with_type(self): - from lib.core.common import getSafeExString - self.assertEqual(getSafeExString(OSError(0, "foobar")), "OSError: foobar") - - def test_generic_value_error(self): - from lib.core.common import getSafeExString - self.assertEqual(getSafeExString(ValueError("bad input")), "ValueError: bad input") - - -class TestCommonHostHeader(unittest.TestCase): - def test_plain_host(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://www.target.com/vuln.php?id=1"), "www.target.com") - - def test_default_port_stripped(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://www.target.com:80/x"), "www.target.com") - self.assertEqual(getHostHeader("https://www.target.com:443/x"), "www.target.com") - - def test_nondefault_port_kept(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://www.target.com:8080/x"), "www.target.com:8080") - - def test_ipv6_brackets(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://[::1]:8080/vuln.php?id=1"), "[::1]:8080") - self.assertEqual(getHostHeader("http://[::1]/vuln.php?id=1"), "[::1]") - - -class TestCommonCheckSameHost(unittest.TestCase): - def test_same_host(self): - from lib.core.common import checkSameHost - self.assertTrue(checkSameHost( - "http://www.target.com/page1.php?id=1", - "http://www.target.com/images/page2.php", - )) - - def test_different_host(self): - from lib.core.common import checkSameHost - self.assertFalse(checkSameHost( - "http://www.target.com/page1.php?id=1", - "http://www.target2.com/images/page2.php", - )) - - def test_www_prefix_ignored(self): - from lib.core.common import checkSameHost - # leading 'www.' is stripped before comparison - self.assertTrue(checkSameHost("http://www.target.com/a", "http://target.com/b")) - - def test_single_url_true_and_empty_none(self): - from lib.core.common import checkSameHost - self.assertTrue(checkSameHost("http://only.com/a")) - self.assertIsNone(checkSameHost()) - - -class TestCommonUrldecode(unittest.TestCase): - def test_convall_true(self): - from lib.core.common import urldecode - self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=True), "AND 1>(2+3)#") - - def test_convall_false_keeps_unsafe(self): - from lib.core.common import urldecode - # %2B (plus) is in the default 'unsafe' set so it stays encoded when convall=False - self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") - - def test_bytes_input(self): - from lib.core.common import urldecode - self.assertEqual(urldecode(b"AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") - - def test_spaceplus(self): - from lib.core.common import urldecode - # with spaceplus the '+' becomes a space - self.assertEqual(urldecode("a+b", convall=False, spaceplus=True), "a b") - # without spaceplus the '+' stays - self.assertEqual(urldecode("a+b", convall=False, spaceplus=False), "a+b") - - -class TestCommonChunkSplit(unittest.TestCase): - def test_chunk_split_post_data(self): - import random - from lib.core.common import chunkSplitPostData - from lib.core.patch import unisonRandom - # The pinned docstring value is produced under sqlmap's cross-version PRNG; install it - # (then restore the stdlib functions) so the expectation is deterministic here too. - _saved = (random.choice, random.randint, random.sample, random.seed) - unisonRandom() - try: - random.seed(0) - expected = ('5;4Xe90\r\nSELEC\r\n3;irWlc\r\nT u\r\n1;eT4zO\r\ns\r\n' - '5;YB4hM\r\nernam\r\n9;2pUD8\r\ne,passwor\r\n3;mp07y\r\nd F\r\n' - '5;8RKXi\r\nROM u\r\n4;MvMhO\r\nsers\r\n0\r\n\r\n') - self.assertEqual(chunkSplitPostData("SELECT username,password FROM users"), expected) - finally: - random.choice, random.randint, random.sample, random.seed = _saved - - def test_chunk_split_terminator(self): - import random - from lib.core.common import chunkSplitPostData - random.seed(123) - # regardless of content, the chunked stream must end with the zero-length terminator - self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n")) - - -class TestCommonDecodeIntToUnicode(unittest.TestCase): - def tearDown(self): - set_dbms(None) - - def test_basic_ascii(self): - from lib.core.common import decodeIntToUnicode - self.assertEqual(decodeIntToUnicode(35), "#") - self.assertEqual(decodeIntToUnicode(64), "@") - self.assertEqual(decodeIntToUnicode(65), "A") - - def test_non_int_passthrough(self): - from lib.core.common import decodeIntToUnicode - # non-int is returned unchanged - self.assertEqual(decodeIntToUnicode("x"), "x") - - def test_pgsql_high_codepoint(self): - from lib.core.common import decodeIntToUnicode - set_dbms(DBMS.PGSQL) - # value > 255 on PGSQL takes the _unichr(value) branch - self.assertEqual(decodeIntToUnicode(0x2122), u"â„¢") - - -class TestCommonDecodeDbmsHex(unittest.TestCase): - def setUp(self): - self._old_binary = kb.binaryField - kb.binaryField = False - - def tearDown(self): - kb.binaryField = self._old_binary - set_dbms(None) - - def test_plain_hex(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1") - - def test_odd_length_appends_question_mark(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") - - def test_list_input(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) - - def test_non_hex_passthrough(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue("5.1.41"), u"5.1.41") - - -class TestCommonUnsafeSQLIdentificator(unittest.TestCase): - def tearDown(self): - set_dbms(None) - - def test_mssql_brackets(self): - from lib.core.common import unsafeSQLIdentificatorNaming - from lib.core.common import getText - set_dbms(DBMS.MSSQL) - self.assertEqual(getText(unsafeSQLIdentificatorNaming("[begin]")), "begin") - self.assertEqual(getText(unsafeSQLIdentificatorNaming("foobar")), "foobar") - - def test_mysql_backticks(self): - from lib.core.common import unsafeSQLIdentificatorNaming, getText - set_dbms(DBMS.MYSQL) - self.assertEqual(getText(unsafeSQLIdentificatorNaming("`col`")), "col") - - def test_oracle_uppercases(self): - from lib.core.common import unsafeSQLIdentificatorNaming, getText - set_dbms(DBMS.ORACLE) - # Oracle strips double quotes and uppercases - self.assertEqual(getText(unsafeSQLIdentificatorNaming('"name"')), "NAME") - - -class TestCommonParseSqliteSchema(unittest.TestCase): - def setUp(self): - self._old_cached = kb.data.get("cachedColumns") - self._old_db = conf.db - self._old_tbl = conf.tbl - kb.data.cachedColumns = {} - conf.db = "SQLITE_MASTER" - conf.tbl = "users" - - def tearDown(self): - kb.data.cachedColumns = self._old_cached - conf.db = self._old_db - conf.tbl = self._old_tbl - - def test_simple_schema(self): - from lib.core.common import parseSqliteTableSchema - self.assertTrue(parseSqliteTableSchema( - "CREATE TABLE users(\n\t\tid INTEGER,\n\t\tname TEXT\n);")) - cols = kb.data.cachedColumns[conf.db][conf.tbl] - self.assertEqual(tuple(cols.items()), (("id", "INTEGER"), ("name", "TEXT"))) - - def test_constraints_skipped(self): - from lib.core.common import parseSqliteTableSchema - self.assertTrue(parseSqliteTableSchema( - "CREATE TABLE suppliers(\n\tsupplier_id INTEGER PRIMARY KEY DESC,\n\tname TEXT NOT NULL\n);")) - cols = kb.data.cachedColumns[conf.db][conf.tbl] - self.assertEqual(tuple(cols.items()), (("supplier_id", "INTEGER"), ("name", "TEXT"))) - - -class TestAgentPure(unittest.TestCase): - """Pure agent.py methods independent of full injection state.""" - - @classmethod - def setUpClass(cls): - from lib.core.agent import agent - cls.agent = agent - - def tearDown(self): - set_dbms(None) - - def test_get_comment_present(self): - from lib.core.datatype import AttribDict - request = AttribDict() - request.comment = "-- foo" - self.assertEqual(self.agent.getComment(request), "-- foo") - - def test_get_comment_absent(self): - from lib.core.datatype import AttribDict - request = AttribDict() - self.assertEqual(self.agent.getComment(request), "") - - def test_add_payload_delimiters(self): - from lib.core.settings import PAYLOAD_DELIMITER - value = "1 AND 1=1" - result = self.agent.addPayloadDelimiters(value) - self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER)) - # falsy value returned unchanged - self.assertEqual(self.agent.addPayloadDelimiters(""), "") - - def test_remove_payload_delimiters_roundtrip(self): - self.assertEqual( - self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")), - "1 AND 1=1", - ) - - def test_extract_payload(self): - wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix" - self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1") - - def test_replace_payload(self): - wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix" - replaced = self.agent.replacePayload(wrapped, "NEW") - self.assertEqual(self.agent.extractPayload(replaced), "NEW") - # surrounding text preserved - self.assertTrue(replaced.startswith("prefix")) - self.assertTrue(replaced.endswith("suffix")) - - def test_simple_concatenate_mysql(self): - set_dbms(DBMS.MYSQL) - # MySQL concatenate query template is 'CONCAT(%s,%s)' - self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)") - - def test_hex_convert_field_mysql(self): - set_dbms(DBMS.MYSQL) - # MySQL hex template is 'HEX(%s)' - self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)") - - def test_get_fields_select_from(self): - set_dbms(DBMS.MYSQL) - result = self.agent.getFields("SELECT a, b FROM users") - fieldsToCastList = result[5] - fieldsToCastStr = result[6] - self.assertEqual(fieldsToCastStr, "a, b") - self.assertEqual(fieldsToCastList, ["a", "b"]) - - def test_get_fields_no_from(self): - set_dbms(DBMS.MYSQL) - # a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list - result = self.agent.getFields("SELECT 1") - fieldsSelectFrom = result[0] - self.assertIsNone(fieldsSelectFrom) - self.assertEqual(result[6], "1") - - -class TestAgentWhereQuery(unittest.TestCase): - @classmethod - def setUpClass(cls): - from lib.core.agent import agent - cls.agent = agent - - def setUp(self): - self._old_dumpWhere = conf.dumpWhere - self._old_tbl = conf.tbl - conf.tbl = None - - def tearDown(self): - conf.dumpWhere = self._old_dumpWhere - conf.tbl = self._old_tbl - set_dbms(None) - - def test_no_dumpwhere_passthrough(self): - conf.dumpWhere = None - query = "SELECT a FROM t" - self.assertEqual(self.agent.whereQuery(query), query) - - def test_appends_where_clause(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>0" - # no existing WHERE -> appends ' WHERE id>0' - self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0") - - def test_and_when_where_present(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>0" - # existing WHERE -> appended with AND - self.assertEqual( - self.agent.whereQuery("SELECT a FROM t WHERE x=1"), - "SELECT a FROM t WHERE x=1 AND id>0", - ) - - def test_splices_before_order_by(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>0" - # WHERE must be spliced before the trailing ORDER BY suffix - self.assertEqual( - self.agent.whereQuery("SELECT a FROM t ORDER BY a"), - "SELECT a FROM t WHERE id>0 ORDER BY a", - ) - - -class TestBasicHeuristicCharEncoding(unittest.TestCase): - def test_ascii(self): - from lib.request.basic import getHeuristicCharEncoding - self.assertEqual(getHeuristicCharEncoding(b""), "ascii") - - def test_cache_hit_returns_same(self): - from lib.request.basic import getHeuristicCharEncoding - page = b"hello world" - 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"foo&bar", None, "text/html; charset=utf-8")), - "foo&bar", - ) - - 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" ", 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"J", 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"™", 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() diff --git a/tests/test_core_final.py b/tests/test_core_final.py deleted file mode 100644 index 1e1119a48..000000000 --- a/tests/test_core_final.py +++ /dev/null @@ -1,605 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional unit coverage for lib/core/common.py, lib/core/option.py and -lib/core/target.py, targeting *pure* (or near-pure) functions and branches NOT -already exercised by the existing test modules: - - * tests/test_common_utils.py / test_common_parsers.py / test_core_more.py - * tests/test_option_setup.py / test_option_more.py - * tests/test_target_parsing.py - -This file instead covers (common.py): - - boldifyMessage, calculateDeltaSeconds, commonFinderOnly, - enumValueToNameLookup, extractErrorMessage, filePathToSafeString, - isWindowsDriveLetterPath, cleanReplaceUnicode, trimAlphaNum, - removePostHintPrefix, safeExpandUser, safeFilepathEncode, - serializeObject/unserializeObject, applyFunctionRecursively, - extractExpectedValue, getHeader, getRequestHeader, parseJson, - parsePasswordHash, findMultipartPostBoundary, setTechnique/getTechnique, - extractRegexResult, extractTextTagContent, getFilteredPageContent, - checkFile, listToStrValue, intersect, isZipFile, checkOldOptions. - -(option.py): - - _setHTTPAuthentication (basic/ntlm/bearer/pki + error branches), - _setWriteFile, _setHTTPTimeout, _setAuthCred. - -Everything runs in isolation: no network, no DBMS, no persistent filesystem -mutation. All mutated conf/kb/Backend/socket state is snapshotted and restored. -""" - -import os -import socket -import sys -import tempfile -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap -bootstrap() - -import lib.core.option as option -from lib.core.data import conf, kb, paths -from lib.core.enums import ( - AUTH_TYPE, - DBMS, - EXPECTED, - HTTP_HEADER, - SORT_ORDER, -) -from lib.core.exception import ( - SqlmapFilePathException, - SqlmapMissingMandatoryOptionException, - SqlmapMissingDependence, - SqlmapSyntaxException, - SqlmapSystemException, -) -from lib.core.settings import NULL -from lib.core.common import ( - applyFunctionRecursively, - boldifyMessage, - calculateDeltaSeconds, - checkFile, - checkOldOptions, - cleanReplaceUnicode, - commonFinderOnly, - enumValueToNameLookup, - extractErrorMessage, - extractExpectedValue, - extractRegexResult, - extractTextTagContent, - filePathToSafeString, - findMultipartPostBoundary, - getFilteredPageContent, - getHeader, - getRequestHeader, - getText, - getTechnique, - intersect, - isWindowsDriveLetterPath, - isZipFile, - listToStrValue, - parseJson, - parsePasswordHash, - removePostHintPrefix, - safeExpandUser, - safeFilepathEncode, - serializeObject, - setTechnique, - trimAlphaNum, - unserializeObject, -) -from thirdparty.six.moves import urllib as _urllib - - -class _FakeRequest(object): - """Minimal stand-in for urllib2.Request used by getRequestHeader().""" - - def __init__(self, headers): - self.headers = headers - - def header_items(self): - return self.headers.items() - - -class TestCommonPureHelpers(unittest.TestCase): - """Pure string/encoding/list/regex helpers from lib/core/common.py.""" - - def test_boldify_message_marks_known_pattern(self): - self.assertEqual( - boldifyMessage("GET parameter id is not injectable", istty=True), - "\x1b[1mGET parameter id is not injectable\x1b[0m", - ) - - def test_boldify_message_leaves_plain_unchanged(self): - self.assertEqual(boldifyMessage("just a plain message", istty=True), "just a plain message") - - def test_calculate_delta_seconds_from_epoch(self): - self.assertGreater(calculateDeltaSeconds(0), 1151721660) - - def test_calculate_delta_seconds_nonnegative(self): - import time as _time - self.assertGreaterEqual(calculateDeltaSeconds(_time.time()), 0.0) - - def test_common_finder_only_returns_longest_common_prefix(self): - self.assertEqual(commonFinderOnly("abcd", ["abcdefg", "foobar", "abcde"]), "abcde") - - def test_enum_value_to_name_lookup_hit(self): - self.assertEqual(enumValueToNameLookup(SORT_ORDER, SORT_ORDER.LAST), "LAST") - - def test_enum_value_to_name_lookup_miss(self): - self.assertIsNone(enumValueToNameLookup(SORT_ORDER, -987654321)) - - def test_file_path_to_safe_string(self): - self.assertEqual(filePathToSafeString("C:/Windows/system32"), "C__Windows_system32") - - def test_file_path_to_safe_string_spaces_backslashes(self): - self.assertEqual(filePathToSafeString("a b\\c:d"), "a_b_c_d") - - def test_is_windows_drive_letter_path_true(self): - self.assertTrue(isWindowsDriveLetterPath("C:\\boot.ini")) - - def test_is_windows_drive_letter_path_false(self): - self.assertFalse(isWindowsDriveLetterPath("/var/log/apache.log")) - - def test_clean_replace_unicode_list(self): - self.assertEqual(cleanReplaceUnicode(["a", "b"]), ["a", "b"]) - - def test_clean_replace_unicode_scalar(self): - self.assertEqual(cleanReplaceUnicode(u"plain"), u"plain") - - def test_trim_alpha_num(self): - self.assertEqual(trimAlphaNum("AND 1>(2+3)-- foobar"), " 1>(2+3)-- ") - - def test_trim_alpha_num_all_alnum(self): - self.assertEqual(trimAlphaNum("abc123"), "") - - def test_trim_alpha_num_empty(self): - self.assertEqual(trimAlphaNum(""), "") - - def test_list_to_str_value_list(self): - self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") - - def test_list_to_str_value_tuple(self): - self.assertEqual(listToStrValue((4, 5)), "4, 5") - - def test_list_to_str_value_scalar(self): - self.assertEqual(listToStrValue("foo"), "foo") - - def test_intersect_lists(self): - self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) - - def test_intersect_lowercase(self): - self.assertEqual(intersect(["A", "B"], ["a"], lowerCase=True), ["a"]) - - def test_intersect_empty(self): - self.assertEqual(intersect([], [1, 2]), []) - - def test_apply_function_recursively(self): - self.assertEqual( - applyFunctionRecursively([1, 2, [3, -9]], lambda _: _ > 0), - [True, True, [True, False]], - ) - - def test_apply_function_recursively_scalar(self): - self.assertEqual(applyFunctionRecursively(5, lambda _: _ + 1), 6) - - -class TestCommonRegexAndPage(unittest.TestCase): - """Regex / page-content extraction helpers.""" - - def test_extract_regex_result_hit(self): - self.assertEqual(extractRegexResult(r"a(?P[^g]+)g", "abcdefg"), "bcdef") - - def test_extract_regex_result_no_match(self): - self.assertIsNone(extractRegexResult(r"a(?P[^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.)b", "")) - - def test_extract_text_tag_content(self): - self.assertEqual( - extractTextTagContent("Title
foobar
"), - ["Title", "foobar"], - ) - - def test_extract_text_tag_content_empty(self): - self.assertEqual(extractTextTagContent(""), []) - - def test_get_filtered_page_content(self): - self.assertEqual( - getFilteredPageContent(u"foobartest"), - "foobar test", - ) - - def test_get_filtered_page_content_drops_script(self): - page = u"hello" - 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"Test\nWarning: oci_parse() " - u"[function.oci-parse]: ORA-01756: quoted string not properly " - u"terminated

Only a test page

") - 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() diff --git a/tests/test_core_more.py b/tests/test_core_more.py deleted file mode 100644 index 529415a8d..000000000 --- a/tests/test_core_more.py +++ /dev/null @@ -1,706 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional unit coverage for lib/core/agent.py, lib/core/common.py and -lib/utils/brute.py, targeting functions/branches NOT already exercised by: - - * tests/test_agent.py (payload delimiters, prefix/suffix defaults, - getFields(SELECT a,b), one MySQL concatQuery, - cleanupPayload RANDNUM) - * tests/test_agent_dialects.py (null/cast/concat, hexConvertField, - nullAndCastField, simpleConcatenate, - forgeUnionQuery(-1,3,...), limitQuery(0,...), - forgeCaseStatement, runAsDBMSUser-noop) - * tests/test_common_utils.py (paramToDict, getCharset, getLimitRange, - parseUnionPage, safeStringFormat, urlencode, - parseTargetUrl/Direct, safeSQLIdentificatorNaming) - * tests/test_common_parsers.py (request-file parsers, reflective masking, - findPageForms, saveConfig, getSQLSnippet, - Backend setters, urlencode/safeStringFormat extras) - -This file instead covers: - - agent.py: forgeUnionQuery (limited / multipleUnions / fromTable / collate / - INTO OUTFILE), limitQuery across several DBMS shapes (TOP/ROWNUM/ - OFFSET dialects + the " FROM "-less early return), whereQuery - (dumpWhere splicing), getComment, concatQuery(unpack=False), - cleanupPayload([ORIGVALUE]/[ORIGINAL]/[SPACE_REPLACE]), - adjustLateValues (SLEEPTIME/base64/RANDNUM), getFields on TOP / - DISTINCT / function / no-FROM shapes, prefixQuery/suffixQuery with - explicit prefix/suffix/clause/comment args, nullAndCastField noCast. - - common.py: isNoneValue, isNullValue, isNumPosStrValue, isNumber, isListLike, - filterPairValues, filterListValue, filterNone, filterStringValue, - zeroDepthSearch, splitFields, unArrayizeValue, flattenValue, - arrayizeValue, joinValue, aliasToDbmsEnum, getPageWordSet, - resetCookieJar (clear branch), normalizeUnicode. - - brute.py: tableExists / columnExists driven with conf.direct=True and the - external collaborators (inject.checkBooleanExpression, getFileItems, - runThreads) monkeypatched, plus _addPageTextWords. - -Everything runs in isolation (no network, no DBMS, no filesystem mutation of -the project). Any global conf/kb/Backend state that a call reads or writes is -snapshotted in setUp and restored in tearDown so test ordering is irrelevant. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.agent import agent -from lib.core.data import conf, kb, queries -from lib.core.enums import DBMS -from lib.core.settings import ( - PAYLOAD_DELIMITER, - SLEEP_TIME_MARKER, - BOUNDED_BASE64_MARKER, - NULL, -) -from lib.core.common import ( - Backend, - isNoneValue, - isNullValue, - isNumPosStrValue, - isNumber, - isListLike, - filterPairValues, - filterListValue, - filterNone, - filterStringValue, - zeroDepthSearch, - splitFields, - unArrayizeValue, - flattenValue, - arrayizeValue, - joinValue, - aliasToDbmsEnum, - getPageWordSet, - resetCookieJar, - normalizeUnicode, -) - - -class DbmsStateMixin(object): - """Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak.""" - - def setUp(self): - self._forcedDbms = kb.forcedDbms - self._sticky = kb.stickyDBMS - self._batch = conf.batch - conf.batch = True - - def tearDown(self): - kb.forcedDbms = self._forcedDbms - kb.stickyDBMS = self._sticky - conf.batch = self._batch - - -# --------------------------------------------------------------------------- # -# lib/core/agent.py -# --------------------------------------------------------------------------- # - -class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase): - """forgeUnionQuery arg combinations not reached by the dialect smoke test.""" - - def test_limited_subselect_wraps_query(self): - set_dbms(DBMS.MYSQL) - # limited=True wraps the payload as (SELECT ...) at `position`, fills the - # rest with `char`, and appends the FROM/comment/suffix - out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None, - None, None, "NULL", None, limited=True) - self.assertIn("(SELECT user FROM mysql.user)", out) - self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out) - # position 1 of 3 => NULL,,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 = - 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 TO (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"foobartest") - 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 FROM )) => 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"admin password 1abc xy verylongword" - 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) diff --git a/tests/test_databases_enum.py b/tests/test_databases_enum.py index 323a4a728..3bba88dde 100644 --- a/tests/test_databases_enum.py +++ b/tests/test_databases_enum.py @@ -36,6 +36,26 @@ from plugins.generic.databases import Databases _NOOP = lambda self: None +def _inference_gv(count, sequence): + """Build an inject.getValue stub for blind inference branches. + + Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise + yields the next item from `sequence` wrapped as a single-cell row ([value]), + cycling if exhausted. This mirrors the count-then-per-row contract of every + isInferenceAvailable() branch. + """ + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(count) + val = sequence[state["i"] % len(sequence)] + state["i"] += 1 + return [val] + + return gv + + class _BaseEnumTest(unittest.TestCase): """Shared setup/teardown that snapshots and restores all touched global state.""" @@ -507,5 +527,241 @@ class TestGetProcedures(_BaseEnumTest): self.assertEqual(sorted(result), sorted(procs)) +# --------------------------------------------------------------------------- # +# Inference / brute-force branches (relocated from test_generic_enum_more.py) +# --------------------------------------------------------------------------- # + +class _DbBase(unittest.TestCase): + _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", + "getComments", "excludeSysDbs", "search", "freshQueries") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_getValue = dbmod.inject.getValue + self._saved_checkBool = dbmod.inject.checkBooleanExpression + self._saved_injection_data = kb.injection.data + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_hintValue = kb.get("hintValue") + self._saved_choices = dict(kb.choices) + self._saved_readInput = dbmod.readInput + self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) + Databases.forceDbmsEnum = _NOOP + + conf.getComments = False + conf.excludeSysDbs = False + conf.exclude = None + conf.search = False + conf.freshQueries = False + conf.col = None + kb.data.has_information_schema = True + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + dbmod.inject.getValue = self._saved_getValue + dbmod.inject.checkBooleanExpression = self._saved_checkBool + dbmod.readInput = self._saved_readInput + kb.injection.data = self._saved_injection_data + kb.data.has_information_schema = self._saved_has_is + kb.hintValue = self._saved_hintValue + kb.choices.clear() + kb.choices.update(self._saved_choices) + if self._saved_forceDbmsEnum is not None: + Databases.forceDbmsEnum = self._saved_forceDbmsEnum + else: + try: + del Databases.forceDbmsEnum + except AttributeError: + pass + + def _fresh(self): + d = Databases() + kb.data.currentDb = "" + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] + return d + + def _inference(self): + conf.direct = False + conf.technique = None + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestDatabasesInference(_DbBase): + def test_get_columns_inference_pgsql_types(self): + # Blind column enumeration on PostgreSQL: a count, then for each index a + # column name followed by its type. Assert the {db:{tbl:{col:type}}} parse. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + conf.db = "public" + conf.tbl = "users" + + names = ["id", "email"] + state = {"i": 0, "name": True} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(names)) + if state["name"]: + val = names[state["i"] % len(names)] + state["i"] += 1 + state["name"] = False + return [val] + state["name"] = True + return ["integer"] + + dbmod.inject.getValue = gv + result = d.getColumns() + cols = result["public"]["users"] + self.assertEqual(len(cols), 2) + self.assertEqual(cols.get("id"), "integer") + + def test_get_columns_inference_dump_mode_collist(self): + # dumpMode with an explicit conf.col list: in the inference branch the + # columns are taken straight from colList (no count/type queries at all) + # and stored with value None. Asserting no getValue ran proves the + # dump-mode shortcut, not a network round-trip. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "id,name" + + def boom(*a, **k): + raise AssertionError("dumpMode+colList must not query in inference branch") + + dbmod.inject.getValue = boom + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + # "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it; + # both columns must be present (count, since exact key varies by quoting). + self.assertEqual(len(cols), 2) + self.assertIn("id", cols) + self.assertIsNone(cols.get("id")) + + def test_get_count_over_cached_tables_inference(self): + # getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount. + # Drive the inband table fetch + per-table count and assert the + # {db:{count:[tables]}} grouping (tables sharing a count are grouped). + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + kb.data.cachedTables = {"testdb": ["users", "posts"]} + + counts = {"users": "5", "posts": "5"} + + def gv(query, *a, **k): + for t, c in counts.items(): + if t in query: + return c + return "0" + + dbmod.inject.getValue = gv + result = d.getCount() + # both tables have count 5 -> grouped under the same key + self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"]) + + def test_get_statements_count_zero_returns_empty(self): + # Inference path: a zero count short-circuits to the (empty) cache. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + # getStatements compares the count with the int literal 0 (count == 0), so + # the count stub must return an int 0 (not "0") to take the empty branch. + dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0") + result = d.getStatements() + self.assertEqual(result, []) + + def test_get_procedures_inference(self): + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"]) + result = d.getProcedures() + self.assertEqual(sorted(result), ["sp_a", "sp_b"]) + + def test_get_dbs_mssql_inband_paging(self): + # MSSQL with no rows from the primary query falls into the query2 paging + # loop (one indexed query per db until a blank value stops it). + set_dbms("Microsoft SQL Server") + conf.direct = True + d = self._fresh() + dbs = ["master", "model"] + + def gv(query, *a, **k): + # The primary inband query is 'SELECT name FROM master..sysdatabases' + # (no DB_NAME); make it return nothing so getDbs falls into the + # 'SELECT DB_NAME()' paging loop (query2). + if "DB_NAME" not in query: + return None + import re as _re + idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0]) + return dbs[idx] if idx < len(dbs) else "" + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(sorted(result), ["master", "model"]) + + def test_get_tables_inference_grouped_per_db(self): + # Blind table enumeration: count for the db, then one table name per index. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "shop" + conf.tbl = None + dbmod.inject.getValue = _inference_gv(2, ["orders", "items"]) + result = d.getTables() + self.assertIn("shop", result) + self.assertEqual(sorted(result["shop"]), ["items", "orders"]) + + +class TestDatabasesBruteForce(_DbBase): + def test_get_columns_mysql_lt5_bruteforce_decline(self): + # MySQL < 5 (no information_schema) forces bruteForce in getColumns; with + # the common-column-existence prompt answered 'N' it returns None without + # issuing any column query. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + + def boom(*a, **k): + raise AssertionError("bruteForce decline must not query columns") + + dbmod.inject.getValue = boom + result = d.getColumns() + self.assertIsNone(result) + + def test_get_columns_bruteforce_dumpmode_collist_on_decline(self): + # bruteForce + decline + dumpMode + colList: the columns from colList are + # stored with None type (the dump-mode salvage branch), not dropped. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "a,b" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + dbmod.inject.getValue = lambda *a, **k: None + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + self.assertEqual(sorted(cols.keys()), ["a", "b"]) + self.assertIsNone(cols.get("a")) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_dbms_enum.py b/tests/test_dbms_enum.py index 8188f3c0e..dff6a0465 100644 --- a/tests/test_dbms_enum.py +++ b/tests/test_dbms_enum.py @@ -6,11 +6,19 @@ See the file 'LICENSE' for copying permission DBMS-specific enumeration overrides (plugins/dbms//enumeration.py), driven through each full DBMS handler with the injection layer mocked, so the -dialect-specific table/column discovery paths run without a live target. The -in-band (UNION/error/direct) branch is taken via conf.direct=True and -inject.getValue is stubbed with canned result rows. +dialect-specific table/column/user/privilege discovery paths run without a live +target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via +conf.direct=True and inject.getValue is stubbed with canned result rows; +conf.batch=True avoids interactive prompts. + +Consolidated from former tests/test_dbms_enum.py (Microsoft SQL Server), +tests/test_dbms_enum_a.py (Oracle/PostgreSQL/MySQL/SQLite) and +tests/test_dbms_enum_b.py (Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. """ +import importlib import os import sys import unittest @@ -19,11 +27,18 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _testutils import bootstrap, set_dbms bootstrap() +from lib.core.common import Backend from lib.core.data import conf, kb from lib.core.enums import EXPECTED +from lib.core.exception import SqlmapUnsupportedFeatureException +from lib.request import inject -class _EnumBase(unittest.TestCase): +# --------------------------------------------------------------------------- +# Base for Microsoft SQL Server getTables (former test_dbms_enum.py) +# --------------------------------------------------------------------------- + +class _EnumBaseMSSQL(unittest.TestCase): """Snapshot/restore the global state these enumerators mutate.""" module = None # the enumeration module whose inject.getValue we patch @@ -45,7 +60,7 @@ class _EnumBase(unittest.TestCase): kb.data.cachedColumns = self._cachedColumns -class TestMSSQLServerEnum(_EnumBase): +class TestMSSQLServerEnum(_EnumBaseMSSQL): import plugins.dbms.mssqlserver.enumeration as module def _handler(self): @@ -94,5 +109,614 @@ class TestMSSQLServerEnum(_EnumBase): self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"]) +# --------------------------------------------------------------------------- +# Base for Oracle/PostgreSQL/MySQL/SQLite (former test_dbms_enum_a.py) +# --------------------------------------------------------------------------- + +class _EnumBaseA(unittest.TestCase): + """Snapshot/restore the global state these enumerators mutate. + + Other tests in the suite depend on clean globals (a leaked kb.hintValue + breaks test_inference_engine; a leaked forced DBMS breaks others), so every + knob touched here is captured in setUp and put back in tearDown. + """ + + # the enumeration module whose inject.getValue we patch (overridden per DBMS) + module = None + + def setUp(self): + # conf knobs + self._direct = conf.direct + self._batch = conf.batch + self._user = conf.user + self._db = conf.get("db") + self._tbl = conf.get("tbl") + self._exclude = conf.get("exclude") + + # injection layer (some override modules - e.g. SQLite/PostgreSQL - do not + # import inject because their overrides return constants without querying) + self._has_inject = hasattr(self.module, "inject") + if self._has_inject: + self._gv = self.module.inject.getValue + + # kb.data cached* containers + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + self._cachedDbs = kb.data.get("cachedDbs") + self._cachedUsers = kb.data.get("cachedUsers") + self._cachedUsersRoles = kb.data.get("cachedUsersRoles") + self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges") + self._has_information_schema = kb.data.get("has_information_schema") + + # state other tests are sensitive to + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._forcedDbms = Backend.getForcedDbms() + self._stickyDBMS = kb.stickyDBMS + + # avoid readInput EOFError flakiness and interactive prompts + conf.direct = True + conf.batch = True + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.user = self._user + conf.db = self._db + conf.tbl = self._tbl + conf.exclude = self._exclude + + if self._has_inject: + self.module.inject.getValue = self._gv + + kb.data.cachedTables = self._cachedTables + kb.data.cachedColumns = self._cachedColumns + kb.data.cachedDbs = self._cachedDbs + kb.data.cachedUsers = self._cachedUsers + kb.data.cachedUsersRoles = self._cachedUsersRoles + kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges + kb.data.has_information_schema = self._has_information_schema + + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.stickyDBMS = self._stickyDBMS + if self._forcedDbms is not None: + Backend.forceDbms(self._forcedDbms) + else: + kb.forcedDbms = None + + +class TestOracleEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.oracle.enumeration") + + def _handler(self): + from plugins.dbms.oracle import OracleMap + set_dbms("Oracle") + return OracleMap() + + def test_get_roles(self): + # rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"] + ] + roles, areAdmins = self._handler().getRoles() + self.assertIn("SYS", roles) + self.assertIn("SCOTT", roles) + self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"}) + # DBA implies administrator + self.assertIn("SYS", areAdmins) + + def test_get_roles_filtered_by_user(self): + # conf.user populates a WHERE clause; canned rows still drive the parse + conf.user = "SCOTT" + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]] + roles, _ = self._handler().getRoles() + self.assertEqual(list(roles.keys()), ["SCOTT"]) + self.assertEqual(roles["SCOTT"], ["RESOURCE"]) + + def test_get_roles_multiple_roles_per_user(self): + # a user appearing across several rows accumulates all granted roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"] + ] + roles, _ = self._handler().getRoles() + self.assertEqual( + set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"} + ) + + +class TestPostgreSQLEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.postgresql.enumeration") + + def _handler(self): + from plugins.dbms.postgresql import PostgreSQLMap + set_dbms("PostgreSQL") + return PostgreSQLMap() + + def test_get_hostname_unsupported(self): + # PostgreSQL overrides getHostname purely to warn; it returns None + self.assertIsNone(self._handler().getHostname()) + + +class TestMySQLEnum(_EnumBaseA): + # MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the + # generic discovery path through the full MySQL handler instead. + module = importlib.import_module("plugins.generic.enumeration") + + def _handler(self): + from plugins.dbms.mysql import MySQLMap + set_dbms("MySQL") + return MySQLMap() + + def test_get_dbs(self): + conf.db = None + kb.data.cachedDbs = [] + kb.data.has_information_schema = True + self.module.inject.getValue = lambda q, *a, **k: ( + 3 if k.get("expected") == EXPECTED.INT + else [["information_schema"], ["testdb"], ["mysql"]] + ) + dbs = self._handler().getDbs() + self.assertIn("testdb", dbs) + self.assertEqual(set(kb.data.cachedDbs), set(dbs)) + + +class TestSQLiteEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.sqlite.enumeration") + + def _handler(self): + from plugins.dbms.sqlite import SQLiteMap + set_dbms("SQLite") + return SQLiteMap() + + def test_unsupported_simple_overrides(self): + # SQLite overrides these to a warning + an empty/neutral return value + h = self._handler() + self.assertIsNone(h.getCurrentUser()) + self.assertIsNone(h.getCurrentDb()) + self.assertIsNone(h.getHostname()) + self.assertEqual(h.getUsers(), []) + self.assertEqual(h.getDbs(), []) + self.assertEqual(h.searchDb(), []) + self.assertEqual(h.getStatements(), []) + self.assertEqual(h.getPasswordHashes(), {}) + self.assertEqual(h.getPrivileges(), {}) + + def test_is_dba_always_true(self): + # on SQLite the current user is treated as having all privileges + self.assertTrue(self._handler().isDba()) + + def test_search_column_raises(self): + with self.assertRaises(SqlmapUnsupportedFeatureException): + self._handler().searchColumn() + + +# --------------------------------------------------------------------------- +# Base + helpers for Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB +# (former test_dbms_enum_b.py) +# --------------------------------------------------------------------------- + +def _fresh_cached(): + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedUsers = [] + kb.data.cachedUsersPrivileges = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.banner = None + + +class _NoOpDumper(object): + """Swallow every dumper call so search methods don't emit/prompt.""" + + def __getattr__(self, name): + return lambda *a, **k: None + + +def _handler(display_name, dirname): + """Instantiate the full *Map handler for the given DBMS.""" + set_dbms(display_name) + main = importlib.import_module("plugins.dbms.%s" % dirname) + cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0] + return cls() + + +class _EnumBaseB(unittest.TestCase): + """Snapshot/restore every global these enumerators mutate.""" + + # subclasses set these + display_name = None + dirname = None + + def setUp(self): + # config snapshot + self._direct = conf.direct + self._batch = conf.batch + self._db = conf.db + self._tbl = conf.tbl + self._col = conf.col + self._user = conf.user + self._exclude = conf.exclude + self._search = conf.search + self._getBanner = conf.getBanner + self._excludeSysDbs = conf.excludeSysDbs + self._dumper = conf.get("dumper") + + # kb snapshot + self._cached = {k: kb.data.get(k) for k in ( + "cachedDbs", "cachedTables", "cachedColumns", "cachedUsers", + "cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner", + )} + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._currentDb = kb.data.get("currentDb") + self._hasIS = kb.data.get("has_information_schema") + + # injection layer snapshot + self._gv = inject.getValue + self._cbe = getattr(inject, "checkBooleanExpression", None) + + # baseline config the in-band/non-interactive paths need + conf.direct = True + conf.batch = True + kb.data.has_information_schema = True + _fresh_cached() + + # restore the chosen DBMS for every test + self.handler = _handler(self.display_name, self.dirname) + # the enumeration module whose pivotDumpTable some tests stub + self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname) + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.db = self._db + conf.tbl = self._tbl + conf.col = self._col + conf.user = self._user + conf.exclude = self._exclude + conf.search = self._search + conf.getBanner = self._getBanner + conf.excludeSysDbs = self._excludeSysDbs + conf.dumper = self._dumper + + for k, v in self._cached.items(): + kb.data[k] = v + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.data.currentDb = self._currentDb + kb.data.has_information_schema = self._hasIS + + inject.getValue = self._gv + if self._cbe is not None: + inject.checkBooleanExpression = self._cbe + if hasattr(self.em, "pivotDumpTable"): + # restore the pristine reference from the wrapper module + import lib.utils.pivotdumptable as _pdt + self.em.pivotDumpTable = _pdt.pivotDumpTable + + +# --------------------------------------------------------------------------- +# Sybase +# --------------------------------------------------------------------------- + +class TestSybaseEnum(_EnumBaseB): + display_name = "Sybase" + dirname = "sybase" + + def _pivot(self, *value_lists): + """Make em.pivotDumpTable return canned (entries, lengths) per call. + + Each successive call pops the next mapping of {colName: [values]}. + """ + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_users(self): + self._pivot({"name": ["sa", "guest"]}) + users = self.handler.getUsers() + self.assertIn("sa", users) + self.assertIn("guest", users) + + def test_get_dbs(self): + self._pivot({"name": ["master", "model"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["master", "model"]) + + def test_get_tables(self): + conf.db = "testdb" + self._pivot({"name": ["users", "logs"]}) + tables = self.handler.getTables() + self.assertIn("testdb", tables) + self.assertEqual(sorted(tables["testdb"]), ["logs", "users"]) + + def test_get_columns(self): + conf.db = "testdb" + conf.tbl = "users" + # column pivot returns name + usertype: REAL Sybase numeric type ids that + # getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar"). + from lib.core.dicts import SYBASE_TYPES + self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]}) + cols = self.handler.getColumns() + self.assertIn("testdb", cols) + # table key is identifier-normalized (may be schema-qualified) + tbls = cols["testdb"] + self.assertTrue(any("users" in t for t in tbls)) + colset = list(tbls.values())[0] + # the VALUE is the resolved type name, not the raw usertype number: + # proves the SYBASE_TYPES numeric->name mapping actually ran. + self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int" + self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar" + + def test_get_privileges(self): + # getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression). + # Drive the admin-set branch BOTH ways via the isDba oracle so the result + # is not forced by a constant-True stub. + conf.user = None + + # oracle True: every user is flagged DBA -> admins == all users + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) # users still enumerated as privilege keys + self.assertIn("guest", privs) + self.assertEqual(admins, set(["sa", "guest"])) + + # oracle False: nobody is a DBA -> admins is empty, but users still listed + _fresh_cached() + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_not_implemented(self): + # these intentionally return [] with a warning on Sybase + self.assertEqual(self.handler.searchDb(), []) + self.assertEqual(self.handler.searchTable(), []) + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_hostname(self): + # not possible on Sybase; just must not raise + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# SAP MaxDB +# --------------------------------------------------------------------------- + +class TestMaxDBEnum(_EnumBaseB): + display_name = "SAP MaxDB" + dirname = "maxdb" + + def _pivot(self, *value_lists): + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_dbs(self): + self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"]) + + def test_get_tables(self): + conf.db = "SYSTEM" + self._pivot({"tablename": ["USERS", "TABLES"]}) + tables = self.handler.getTables() + # db key is identifier-normalized (uppercase names get quoted) + self.assertEqual(len(tables), 1) + tbls = list(tables.values())[0] + self.assertEqual(sorted(tbls), ["TABLES", "USERS"]) + + def test_get_columns(self): + conf.db = "SYSTEM" + conf.tbl = "USERS" + self._pivot({ + "columnname": ["ID", "NAME"], + "datatype": ["INTEGER", "CHAR"], + "len": ["4", "32"], + }) + cols = self.handler.getColumns() + self.assertEqual(len(cols), 1) + tbls = list(cols.values())[0] + self.assertIn("USERS", tbls) + self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Microsoft SQL Server (methods NOT covered by TestMSSQLServerEnum above) +# --------------------------------------------------------------------------- + +class TestMSSQLServerExtraEnum(_EnumBaseB): + display_name = "Microsoft SQL Server" + dirname = "mssqlserver" + + def test_get_privileges(self): + # getPrivileges -> getUsers (generic, inject.getValue) then isDba. + # Exercise the admin-set branch BOTH ways via the isDba oracle. + conf.user = None + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + + # oracle True: all users flagged DBA + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"])) + + # oracle False: none are DBA -> empty admin set, users still enumerated + _fresh_cached() + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_table(self): + conf.db = "testdb" + conf.tbl = "users" + # in-band branch: getValue returns matching table name(s) + inject.getValue = lambda q, *a, **k: ["users"] + # capture the discovered tables instead of dumping them + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundTables = lambda tables: captured.update(tables) + self.handler.searchTable() + # at least one database mapped to the matched table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + def test_search_column(self): + conf.db = "testdb" + conf.tbl = None + conf.col = "password" + # exact match (no wildcard) so no recursive getColumns call; + # getValue returns the tables that contain the column + inject.getValue = lambda q, *a, **k: ["users"] + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs) + self.handler.searchColumn() + # the searched column was located in at least one table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + +# --------------------------------------------------------------------------- +# IBM DB2 +# --------------------------------------------------------------------------- + +class TestDB2Enum(_EnumBaseB): + display_name = "IBM DB2" + dirname = "db2" + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Informix +# --------------------------------------------------------------------------- + +class TestInformixEnum(_EnumBaseB): + display_name = "Informix" + dirname = "informix" + + def test_search_db(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_search_table(self): + self.assertEqual(self.handler.searchTable(), []) + + def test_search_column(self): + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Firebird +# --------------------------------------------------------------------------- + +class TestFirebirdEnum(_EnumBaseB): + display_name = "Firebird" + dirname = "firebird" + + def test_get_dbs_empty(self): + self.assertEqual(self.handler.getDbs(), []) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_search_db_empty(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# HSQLDB +# --------------------------------------------------------------------------- + +class TestHSQLDBEnum(_EnumBaseB): + display_name = "HSQLDB" + dirname = "hsqldb" + + def test_get_banner(self): + conf.getBanner = True + kb.data.banner = None + # getValue returns a single-element LIST; getBanner pipes it through + # unArrayizeValue, which must unwrap it to the scalar banner string. + inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"] + banner = self.handler.getBanner() + self.assertEqual(banner, "HSQLDB 2.5.1") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + def test_get_current_db_default_schema(self): + from lib.core.settings import HSQLDB_DEFAULT_SCHEMA + self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_dbms_enum_a.py b/tests/test_dbms_enum_a.py deleted file mode 100644 index 4c9948fd1..000000000 --- a/tests/test_dbms_enum_a.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -DBMS-specific enumeration overrides for Oracle, PostgreSQL, MySQL and SQLite -(plugins/dbms//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() diff --git a/tests/test_dbms_enum_b.py b/tests/test_dbms_enum_b.py deleted file mode 100644 index b0622366d..000000000 --- a/tests/test_dbms_enum_b.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Second batch of DBMS-specific enumeration override tests (companion to -tests/test_dbms_enum.py, which covers Microsoft SQL Server getTables). - -Each test drives a FULL per-DBMS handler (the *Map class in -plugins/dbms//__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() diff --git a/tests/test_deps.py b/tests/test_deps.py index 0f09e5cdd..91cabb5d6 100644 --- a/tests/test_deps.py +++ b/tests/test_deps.py @@ -56,7 +56,7 @@ class TestCheckDependencies(unittest.TestCase): # 'kinterbasdb' (Firebird driver) is essentially never installed, so the # probe must hit the except branch and emit a warning naming the library. try: - import kinterbasdb # noqa: F401 + __import__("kinterbasdb") self.skipTest("kinterbasdb is unexpectedly installed") except ImportError: pass diff --git a/tests/test_entries.py b/tests/test_entries.py new file mode 100644 index 000000000..d54a92bbc --- /dev/null +++ b/tests/test_entries.py @@ -0,0 +1,802 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for plugins/generic/entries.py (Entries), exercising dumpTable / +dumpAll / dumpFoundTables / dumpFoundColumn by MOCKING the injection layer +(lib.request.inject.getValue) and the dumper. + +No network and no DBMS are involved: conf.direct=True selects the simple inband +branches, or conf.direct=False with a BOOLEAN injection state selects the +inference (blind) branches; inject.getValue is patched to return canned rows in +the exact shape the methods parse, and conf.dumper is replaced with a recording +stub so we can assert on what each method produced (kb.data caches / returned +dicts). Every test restores all touched conf.* / kb.* / patched module attributes +in tearDown so nothing leaks. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms + +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD + +import plugins.generic.search as smod +import plugins.generic.entries as emod +import plugins.generic.custom as cmod +import plugins.generic.misc as mmod +from plugins.generic.entries import Entries + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_search_enum.py (inband TestEntries) +# --------------------------------------------------------------------------- # + +class _RecordingDumperSE(object): + """Minimal stand-in for conf.dumper that records calls instead of printing/writing.""" + + def __init__(self): + self.reset() + + def reset(self): + self.listed = [] # (header, elements) + self.dbTablesArg = None + self.dbColumnsArg = None + self.dbTableColumnsArg = None + self.tableValues = [] + + def lister(self, header, elements, content_type=None, sort=True): + self.listed.append((header, list(elements) if elements else [])) + + def dbTables(self, dbTables): + self.dbTablesArg = dbTables + + def dbColumns(self, dbColumnsDict, colConsider, dbs): + self.dbColumnsArg = (dbColumnsDict, colConsider, dbs) + + def dbTableColumns(self, tableColumns, content_type=None): + self.dbTableColumnsArg = tableColumns + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestEntriesSE(Entries): + """Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables).""" + + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} # {db: {tbl: {col: type}}} + self.getTablesResult = {} # value assigned to kb.data.cachedTables + self.getColumnsCalls = [] + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + self.getColumnsCalls.append((conf.db, conf.tbl)) + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + kb.data.cachedTables = dict(self.getTablesResult) + + +class _SearchEnumBase(unittest.TestCase): + def setUp(self): + # Save mutated globals + self._saved_conf = {k: conf.get(k) for k in ( + "db", "tbl", "col", "direct", "excludeSysDbs", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", + )} + self._saved_dumper = conf.get("dumper") + self._search_getValue = smod.inject.getValue + self._entries_getValue = emod.inject.getValue + self._search_readInput = smod.readInput + self._entries_readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + + set_dbms("MySQL") + conf.direct = True + conf.excludeSysDbs = False + conf.exclude = None + conf.search = True + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumper = _RecordingDumperSE() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + # Non-interactive prompts: collapse readInput to its default. + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return True if (default in (None, 'Y', 'y', True)) else False + return default + smod.readInput = _readInput + emod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + smod.inject.getValue = self._search_getValue + emod.inject.getValue = self._entries_getValue + smod.readInput = self._search_readInput + emod.readInput = self._entries_readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + + +class TestEntries(_SearchEnumBase): + def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesSE() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + # --- dumpTable: inband (conf.direct) ------------------------------------ + + def test_dump_table_inband_rows(self): + e = self._entries_with_cols(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + # MySQL inband dump returns a list of [colVal, colVal] rows. + emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]] + + e.dumpTable() + + dumped = conf.dumper.tableValues[-1] + self.assertEqual(dumped["__infos__"]["count"], 2) + self.assertEqual(dumped["__infos__"]["table"], "users") + self.assertEqual(dumped["__infos__"]["db"], "testdb") + self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) + self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) + + def test_dump_table_uses_foundData(self): + e = _TestEntriesSE() + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["x"]] + foundData = {"testdb": {"users": {"id": "int"}}} + + e.dumpTable(foundData=foundData) + + # foundData short-circuits column discovery: getColumns must not run. + self.assertEqual(e.getColumnsCalls, []) + self.assertIn("id", conf.dumper.tableValues[-1]) + + def test_dump_table_no_columns_skips(self): + e = _TestEntriesSE() + e.getColumnsResult = {} # discovery yields nothing + conf.db = "testdb" + conf.tbl = "ghost" + conf.col = None + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + # No columns => no values dumped. + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_empty_entries(self): + e = self._entries_with_cols(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: None # no rows + + e.dumpTable() + # Nothing retrieved => dumpedTable empty => dbTableValues not called. + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_current_db(self): + e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",)) + conf.db = None # triggers getCurrentDb() -> "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["7"]] + + e.dumpTable() + self.assertEqual(conf.db, "testdb") + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"]) + + def test_dump_table_multiple_db_error(self): + e = _TestEntriesSE() + conf.db = "a,b" + conf.tbl = "users" + conf.col = None + from lib.core.exception import SqlmapMissingMandatoryOptionException + self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable) + + def test_dump_table_get_tables_when_no_tbl(self): + e = _TestEntriesSE() + e.getTablesResult = {"testdb": ["users"]} + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + conf.db = "testdb" + conf.tbl = None + conf.col = None + emod.inject.getValue = lambda *a, **k: [["42"]] + + e.dumpTable() + # Tables were discovered via getTables, then the row dumped. + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"]) + + # --- dumpAll: single-db delegation -------------------------------------- + + def test_dump_all_single_db_delegates(self): + e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",)) + # dumpAll with db set & tbl None must delegate straight to dumpTable. + conf.db = "testdb" + conf.tbl = None + conf.col = None + e.getTablesResult = {"testdb": ["users"]} + emod.inject.getValue = lambda *a, **k: [["9"]] + + e.dumpAll() + self.assertTrue(conf.dumper.tableValues) + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_generic_more.py (inband dump branches) +# --------------------------------------------------------------------------- # + +class _RecordingDumperGM(object): + """Recording stand-in for conf.dumper (no printing / file writing).""" + + def __init__(self): + self.tableValues = [] + self.sqlQueries = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + def sqlQuery(self, query, queryRes): + self.sqlQueries.append((query, queryRes)) + + +class _TestEntriesGM(Entries): + """Entries with cross-mixin collaborators stubbed. + + forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by + sibling mixins; we emulate column/table discovery by populating kb.data.cached* + from canned attributes, exactly as the production plugins do. + """ + + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} # assigned to kb.data.cachedColumns + self.getTablesResult = {} # assigned to kb.data.cachedTables + self.getColumnsCalls = [] + self.getTablesCalls = 0 + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + self.getColumnsCalls.append((conf.db, conf.tbl)) + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + self.getTablesCalls += 1 + kb.data.cachedTables = dict(self.getTablesResult) + + +class _GenericBase(unittest.TestCase): + """Snapshot/restore for everything the generic mixins touch.""" + + _CONF_KEYS = ( + "db", "tbl", "col", "direct", "batch", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere", + "tmpPath", "sqlQuery", "sqlFile", "regKey", "regVal", "regData", + "regType", "osPwn", "osShell", "cleanup", "privEsc", + ) + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + + self._saved_getValue = { + emod: emod.inject.getValue, + cmod: cmod.inject.getValue, + mmod: mmod.inject.getValue, + } + self._saved_goStacked = { + cmod: cmod.inject.goStacked, + mmod: mmod.inject.goStacked, + } + self._saved_emod_readInput = emod.readInput + self._saved_mmod_readInput = mmod.readInput + + self._saved_kb = { + "cachedColumns": kb.data.get("cachedColumns"), + "cachedTables": kb.data.get("cachedTables"), + "dumpedTable": kb.data.get("dumpedTable"), + "has_information_schema": kb.data.get("has_information_schema"), + "dumpKeyboardInterrupt": kb.get("dumpKeyboardInterrupt"), + "permissionFlag": kb.get("permissionFlag"), + "hintValue": kb.get("hintValue"), + "injection_data": kb.injection.data, + "bannerFp": kb.get("bannerFp"), + "os": kb.get("os"), + } + self._saved_forceDbms = kb.get("forcedDbms") + + conf.direct = True + conf.batch = True + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumperGM() + + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.data.has_information_schema = True + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return default in (None, 'Y', 'y', True) + return default + + emod.readInput = _readInput + mmod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + + for mod, fn in self._saved_getValue.items(): + mod.inject.getValue = fn + for mod, fn in self._saved_goStacked.items(): + mod.inject.goStacked = fn + emod.readInput = self._saved_emod_readInput + mmod.readInput = self._saved_mmod_readInput + + kb.data.cachedColumns = self._saved_kb["cachedColumns"] + kb.data.cachedTables = self._saved_kb["cachedTables"] + kb.data.dumpedTable = self._saved_kb["dumpedTable"] + kb.data.has_information_schema = self._saved_kb["has_information_schema"] + kb.dumpKeyboardInterrupt = self._saved_kb["dumpKeyboardInterrupt"] + kb.permissionFlag = self._saved_kb["permissionFlag"] + kb.hintValue = self._saved_kb["hintValue"] + kb.injection.data = self._saved_kb["injection_data"] + kb.bannerFp = self._saved_kb["bannerFp"] + kb.os = self._saved_kb["os"] + kb.forcedDbms = self._saved_forceDbms + + @staticmethod + def _force_os(os_name): + # Backend.setOs only assigns when kb.os is currently None; reset first so + # tests can deterministically pin the back-end OS. + kb.os = None + Backend.setOs(os_name) + + +class TestEntriesDumpTable(_GenericBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesGM() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_exclude_filters_columns(self): + set_dbms("MySQL") + e = self._entries(cols=("id", "secret")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertIn("id", dumped) + self.assertNotIn("secret", dumped) + + def test_exclude_all_columns_skips(self): + set_dbms("MySQL") + e = self._entries(cols=("secret",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + # all columns excluded => "no usable column names" => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dumpwhere_rewrites_query(self): + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.dumpWhere = "id>5" + captured = {} + + def gv(query, *a, **k): + captured["query"] = query + return [["9"]] + + emod.inject.getValue = gv + e.dumpTable() + # agent.whereQuery folds conf.dumpWhere into the dump query + self.assertIn("id>5", captured["query"]) + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"]) + + def test_disablehashing_false_path(self): + # conf.disableHashing False => attackDumpedTable() is invoked; with no + # hashes present it must complete without raising and still emit values. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.disableHashing = False + emod.inject.getValue = lambda *a, **k: [["1", "alice"]] + + # Spy on attackDumpedTable: with disableHashing False it MUST be invoked + # after the values are dumped. A recorder replaces it so we can assert the + # call happened (and no real dictionary attack runs). + saved_attack = emod.attackDumpedTable + calls = {"n": 0} + emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) + try: + e.dumpTable() + finally: + emod.attackDumpedTable = saved_attack + + self.assertEqual(calls["n"], 1) + self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1) + + def test_missing_columns_skips_table(self): + # getColumns yields nothing for the targeted table => skip without fetching. + set_dbms("MySQL") + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"other": {"id": "int"}}} + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + def test_multiple_tables_one_dumped(self): + set_dbms("MySQL") + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}} + conf.db = "testdb" + conf.tbl = "users,posts" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + # both tables share the same cachedColumns dict => both dumped + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertIn("posts", tables) + + def test_metadb_suffix_db(self): + # A db whose name carries the METADB_SUFFIX must not get a "db" prefix in + # kb.dumpTable, and dumping still succeeds. + from lib.core.settings import METADB_SUFFIX + set_dbms("MySQL") + metadb = "x%s" % METADB_SUFFIX + e = self._entries(db=metadb, tbl="t", cols=("c",)) + conf.db = metadb + conf.tbl = "t" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["v"]] + + e.dumpTable() + self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"]) + + +class TestEntriesDumpAll(_GenericBase): + def test_dumpall_multiple_dbs_tables(self): + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]} + # dumpTable re-discovers columns per (db, tbl); supply both. + e.getColumnsResult = { + "db1": {"t1": {"a": "int"}}, + "db2": {"t2": {"b": "int"}}, + } + emod.inject.getValue = lambda *a, **k: [["x"]] + + e.dumpAll() + # Every table contributed a values batch. + self.assertEqual(len(conf.dumper.tableValues), 2) + + def test_dumpall_list_cached_tables(self): + # cachedTables as a bare list => wrapped under {None: [...]}. + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + + # getTables sets cachedTables; emulate the list shape directly. + class _ListTables(_TestEntriesGM): + def getTables(self_inner, bruteForce=None): + kb.data.cachedTables = ["users"] + + e = _ListTables() + # dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the + # None db via getCurrentDb() -> "testdb", so columns live under "testdb". + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + self.assertTrue(conf.dumper.tableValues) + # The bare-list None db must be resolved via getCurrentDb() -> "testdb" + # before the dump; assert the dumped __infos__ carries the real db (not + # None) for the requested "users" table. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dumpall_exclude_skips_table(self): + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + conf.exclude = "secret" + e.getTablesResult = {"db1": ["secret", "users"]} + e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertNotIn("secret", tables) + + +class TestEntriesDumpFound(_GenericBase): + def _entries(self): + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + return e + + def test_dump_found_tables_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + # batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices. + e.dumpFoundTables({"testdb": ["users"]}) + self.assertTrue(conf.dumper.tableValues) + # The interactive selection must dump the REQUESTED db/table, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dump_found_tables_declined(self): + set_dbms("MySQL") + e = self._entries() + + def _no(message, default=None, checkBatch=True, boolean=False): + if boolean: + return False + return default + + emod.readInput = _no + emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined") + e.dumpFoundTables({"testdb": ["users"]}) + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_found_column_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + dbs = {"testdb": {"users": {"id": "int"}}} + e.dumpFoundColumn(dbs, foundCols=None, colConsider='1') + self.assertTrue(conf.dumper.tableValues) + # The selection must dump the REQUESTED db/table mapping, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_generic_enum_more.py (inference branches) +# --------------------------------------------------------------------------- # + +class _RecordingDumperInf(object): + def __init__(self): + self.tableValues = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestEntriesInf(Entries): + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} + self.getTablesResult = {} + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + kb.data.cachedTables = dict(self.getTablesResult) + + +class _EntriesBase(unittest.TestCase): + _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + self._gv = emod.inject.getValue + self._cbe = emod.inject.checkBooleanExpression + self._readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + self._saved_injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = False + conf.technique = None + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumperInf() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None)) + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + emod.inject.getValue = self._gv + emod.inject.checkBooleanExpression = self._cbe + emod.readInput = self._readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + kb.injection.data = self._saved_injection_data + + +class TestEntriesInference(_EntriesBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesInf() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_dump_table_inference_column_pivot(self): + # Blind dump (conf.direct=False, BOOLEAN available): a row count, then one + # value per (index, column). Assert the per-column pivoted values match. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + # data[index][column] -> value. 2 rows, columns id/name. + data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return "2" # row count + # MySQL blind cell query: 'SELECT FROM testdb.users ORDER BY ... + # LIMIT ,1'. The row index is the LIMIT offset; the column is the + # SELECT projection. + import re as _re + idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1)) + proj = query.split(" FROM ", 1)[0] + col = "name" if "name" in proj else "id" + return data[idx][col] + + emod.inject.getValue = gv + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertEqual(dumped["__infos__"]["count"], 2) + self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) + self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) + + def test_dump_table_inference_empty_table(self): + # A zero row count in the inference path yields empty per-column value + # lists and no dbTableValues emission (dumpedTable stays effectively empty). + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table")) + e.dumpTable() + # count 0 => empty entries => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_inference_count_failure_skips(self): + # A non-numeric count in the inference path => the table is skipped with a + # warning, no values dumped. + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return None # count failed + self.fail("must not fetch cells when count failed") + + emod.inject.getValue = gv + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index bcf6da6a4..70b6192e1 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -33,7 +33,6 @@ bootstrap() from lib.core.data import conf, kb from lib.core.convert import encodeHex, encodeBase64, getText -from lib.core.enums import PAYLOAD # --------------------------------------------------------------------------- # diff --git a/tests/test_generic_enum_more.py b/tests/test_generic_enum_more.py deleted file mode 100644 index 683a459b7..000000000 --- a/tests/test_generic_enum_more.py +++ /dev/null @@ -1,865 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional unit tests for the generic enumeration mixins, deliberately targeting -branches NOT already exercised by tests/test_databases_enum.py, -tests/test_users_enum.py, tests/test_search_enum.py and tests/test_generic_more.py -(which cover the conf.direct INBAND happy paths). - -This file drives the OTHER branches: - - * plugins/generic/databases.py - the INFERENCE paths (conf.direct=False + - isInferenceAvailable via kb.injection BOOLEAN state: count -> per-row getValue), - the MSSQL inband-paging fallback in getDbs(), getColumns onlyColNames / dumpMode, - the getColumns MySQL<5 / ACCESS bruteforce fallback, getCount over cachedTables, - and getStatements/getProcedures empty/none branches. - * plugins/generic/users.py - getPrivileges role/grant parsing per DBMS in BOTH the - inband path (PGSQL digit columns, MySQL<5 Y/N, Firebird letters, DB2 grant codes) - and the INFERENCE path (count then per-index privilege), getPasswordHashes - grouping/dedup in the inference path, getUsers inference, isDba MSSQL. - * plugins/generic/entries.py - dumpTable INFERENCE path (count -> column-pivot via - per-(index,column) getValue), the empty-table branch, the count-failure skip, - and the resolveKeysetCursor disabling via conf.noKeyset. - * plugins/generic/search.py - searchDb / searchTable / searchColumn INFERENCE - paths (count then per-index getValue), and the MySQL<5 bruteforce branch of - searchTable / searchColumn. - -Recipe (proven in tests/test_databases_enum.py): patch the module's inject.getValue -with canned rows in the EXACT shape the branch parses; for inference branches return -a positive int for EXPECTED.INT count calls then the per-row/per-index values; set the -needed kb.data flags; assert the exact resulting structure (sorted lists, -{db:{tbl:{col:type}}} dicts, privilege sets, dumpedTable values). - -CRITICAL STATE HYGIENE: every test snapshots and restores conf.*, the patched -inject.getValue (per module), kb.data.cached*, kb.hintValue, kb.injection.data, -Backend/forcedDbms in tearDown so nothing leaks into the rest of the suite. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms - -bootstrap() - -from lib.core.data import conf, kb -from lib.core.enums import EXPECTED, PAYLOAD - -import plugins.generic.databases as dbmod -import plugins.generic.users as umod -import plugins.generic.search as smod -import plugins.generic.entries as emod -from plugins.generic.databases import Databases -from plugins.generic.users import Users -from plugins.generic.search import Search -from plugins.generic.entries import Entries - -_NOOP = lambda self: None - - -def _inference_gv(count, sequence): - """Build an inject.getValue stub for blind inference branches. - - Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise - yields the next item from `sequence` wrapped as a single-cell row ([value]), - cycling if exhausted. This mirrors the count-then-per-row contract of every - isInferenceAvailable() branch. - """ - state = {"i": 0} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - return str(count) - val = sequence[state["i"] % len(sequence)] - state["i"] += 1 - return [val] - - return gv - - -# --------------------------------------------------------------------------- # -# databases.py -# --------------------------------------------------------------------------- # - -class _DbBase(unittest.TestCase): - _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", - "getComments", "excludeSysDbs", "search", "freshQueries") - - def setUp(self): - self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} - self._saved_getValue = dbmod.inject.getValue - self._saved_checkBool = dbmod.inject.checkBooleanExpression - self._saved_injection_data = kb.injection.data - self._saved_has_is = kb.data.get("has_information_schema") - self._saved_hintValue = kb.get("hintValue") - self._saved_choices = dict(kb.choices) - self._saved_readInput = dbmod.readInput - self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) - Databases.forceDbmsEnum = _NOOP - - conf.getComments = False - conf.excludeSysDbs = False - conf.exclude = None - conf.search = False - conf.freshQueries = False - conf.col = None - kb.data.has_information_schema = True - - def tearDown(self): - for k, v in self._saved_conf.items(): - conf[k] = v - dbmod.inject.getValue = self._saved_getValue - dbmod.inject.checkBooleanExpression = self._saved_checkBool - dbmod.readInput = self._saved_readInput - kb.injection.data = self._saved_injection_data - kb.data.has_information_schema = self._saved_has_is - kb.hintValue = self._saved_hintValue - kb.choices.clear() - kb.choices.update(self._saved_choices) - if self._saved_forceDbmsEnum is not None: - Databases.forceDbmsEnum = self._saved_forceDbmsEnum - else: - try: - del Databases.forceDbmsEnum - except AttributeError: - pass - - def _fresh(self): - d = Databases() - kb.data.currentDb = "" - kb.data.cachedDbs = [] - kb.data.cachedTables = {} - kb.data.cachedColumns = {} - kb.data.cachedCounts = {} - kb.data.cachedStatements = [] - kb.data.cachedProcedures = [] - return d - - def _inference(self): - conf.direct = False - conf.technique = None - kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} - - -class TestDatabasesInference(_DbBase): - def test_get_columns_inference_pgsql_types(self): - # Blind column enumeration on PostgreSQL: a count, then for each index a - # column name followed by its type. Assert the {db:{tbl:{col:type}}} parse. - set_dbms("PostgreSQL") - self._inference() - d = self._fresh() - conf.db = "public" - conf.tbl = "users" - - names = ["id", "email"] - state = {"i": 0, "name": True} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - return str(len(names)) - if state["name"]: - val = names[state["i"] % len(names)] - state["i"] += 1 - state["name"] = False - return [val] - state["name"] = True - return ["integer"] - - dbmod.inject.getValue = gv - result = d.getColumns() - cols = result["public"]["users"] - self.assertEqual(len(cols), 2) - self.assertEqual(cols.get("id"), "integer") - - def test_get_columns_inference_dump_mode_collist(self): - # dumpMode with an explicit conf.col list: in the inference branch the - # columns are taken straight from colList (no count/type queries at all) - # and stored with value None. Asserting no getValue ran proves the - # dump-mode shortcut, not a network round-trip. - set_dbms("MySQL") - self._inference() - d = self._fresh() - conf.db = "testdb" - conf.tbl = "users" - conf.col = "id,name" - - def boom(*a, **k): - raise AssertionError("dumpMode+colList must not query in inference branch") - - dbmod.inject.getValue = boom - result = d.getColumns(dumpMode=True) - cols = result["testdb"]["users"] - # "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it; - # both columns must be present (count, since exact key varies by quoting). - self.assertEqual(len(cols), 2) - self.assertIn("id", cols) - self.assertIsNone(cols.get("id")) - - def test_get_count_over_cached_tables_inference(self): - # getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount. - # Drive the inband table fetch + per-table count and assert the - # {db:{count:[tables]}} grouping (tables sharing a count are grouped). - set_dbms("MySQL") - conf.direct = True - d = self._fresh() - conf.db = "testdb" - conf.tbl = None - kb.data.cachedTables = {"testdb": ["users", "posts"]} - - counts = {"users": "5", "posts": "5"} - - def gv(query, *a, **k): - for t, c in counts.items(): - if t in query: - return c - return "0" - - dbmod.inject.getValue = gv - result = d.getCount() - # both tables have count 5 -> grouped under the same key - self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"]) - - def test_get_statements_count_zero_returns_empty(self): - # Inference path: a zero count short-circuits to the (empty) cache. - set_dbms("PostgreSQL") - self._inference() - d = self._fresh() - # getStatements compares the count with the int literal 0 (count == 0), so - # the count stub must return an int 0 (not "0") to take the empty branch. - dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0") - result = d.getStatements() - self.assertEqual(result, []) - - def test_get_procedures_inference(self): - set_dbms("PostgreSQL") - self._inference() - d = self._fresh() - dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"]) - result = d.getProcedures() - self.assertEqual(sorted(result), ["sp_a", "sp_b"]) - - def test_get_dbs_mssql_inband_paging(self): - # MSSQL with no rows from the primary query falls into the query2 paging - # loop (one indexed query per db until a blank value stops it). - set_dbms("Microsoft SQL Server") - conf.direct = True - d = self._fresh() - dbs = ["master", "model"] - - def gv(query, *a, **k): - # The primary inband query is 'SELECT name FROM master..sysdatabases' - # (no DB_NAME); make it return nothing so getDbs falls into the - # 'SELECT DB_NAME()' 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 ","; 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 FROM testdb.users ORDER BY ... - # LIMIT ,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() diff --git a/tests/test_generic_more.py b/tests/test_generic_takeover.py similarity index 66% rename from tests/test_generic_more.py rename to tests/test_generic_takeover.py index 00bcd0c8d..f78aaa697 100644 --- a/tests/test_generic_more.py +++ b/tests/test_generic_takeover.py @@ -4,14 +4,8 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission -Additional unit tests for the generic plugin mixins, driving branches NOT already -covered by tests/test_search_enum.py / tests/test_databases_enum.py: +Unit tests for the generic plugin mixins covering: - * plugins/generic/entries.py - dumpTable column/table --exclude filtering, the - --where (conf.dumpWhere) query rewrite, disableHashing toggle, METADB suffix - db handling, the "no usable columns" / "missing columns" skip branches, and - dumpAll over multiple dbs/tables (dict and list shapes) plus dumpFoundTables / - dumpFoundColumn interactive flows. * plugins/generic/custom.py - sqlQuery SELECT/non-query/stacked branches, the MSSQL FROM rewrite, METADB suffix stripping, SqlmapNoneDataException handling, and sqlFile. @@ -38,14 +32,13 @@ bootstrap() from lib.core.common import Backend from lib.core.data import conf, kb -from lib.core.enums import DBMS, OS +from lib.core.enums import OS from lib.core.settings import NULL import plugins.generic.entries as emod import plugins.generic.custom as cmod import plugins.generic.misc as mmod import plugins.generic.takeover as tmod -from plugins.generic.entries import Entries from plugins.generic.custom import Custom from plugins.generic.misc import Miscellaneous @@ -64,40 +57,6 @@ class _RecordingDumper(object): self.sqlQueries.append((query, queryRes)) -# --------------------------------------------------------------------------- # -# entries.py -# --------------------------------------------------------------------------- # - -class _TestEntries(Entries): - """Entries with cross-mixin collaborators stubbed. - - forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by - sibling mixins; we emulate column/table discovery by populating kb.data.cached* - from canned attributes, exactly as the production plugins do. - """ - - def __init__(self): - Entries.__init__(self) - self.getColumnsResult = {} # assigned to kb.data.cachedColumns - self.getTablesResult = {} # assigned to kb.data.cachedTables - self.getColumnsCalls = [] - self.getTablesCalls = 0 - - def forceDbmsEnum(self): - pass - - def getCurrentDb(self): - return "testdb" - - def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): - self.getColumnsCalls.append((conf.db, conf.tbl)) - kb.data.cachedColumns = dict(self.getColumnsResult) - - def getTables(self, bruteForce=None): - self.getTablesCalls += 1 - kb.data.cachedTables = dict(self.getTablesResult) - - class _GenericBase(unittest.TestCase): """Snapshot/restore for everything the generic mixins touch.""" @@ -196,238 +155,6 @@ class _GenericBase(unittest.TestCase): Backend.setOs(os_name) -class TestEntriesDumpTable(_GenericBase): - def _entries(self, db="testdb", tbl="users", cols=("id", "name")): - e = _TestEntries() - e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} - return e - - def test_exclude_filters_columns(self): - set_dbms("MySQL") - e = self._entries(cols=("id", "secret")) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.exclude = "secret" - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpTable() - dumped = conf.dumper.tableValues[-1] - self.assertIn("id", dumped) - self.assertNotIn("secret", dumped) - - def test_exclude_all_columns_skips(self): - set_dbms("MySQL") - e = self._entries(cols=("secret",)) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.exclude = "secret" - emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") - - e.dumpTable() - # all columns excluded => "no usable column names" => nothing dumped - self.assertEqual(conf.dumper.tableValues, []) - - def test_dumpwhere_rewrites_query(self): - set_dbms("MySQL") - e = self._entries(cols=("id",)) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.dumpWhere = "id>5" - captured = {} - - def gv(query, *a, **k): - captured["query"] = query - return [["9"]] - - emod.inject.getValue = gv - e.dumpTable() - # agent.whereQuery folds conf.dumpWhere into the dump query - self.assertIn("id>5", captured["query"]) - self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"]) - - def test_disablehashing_false_path(self): - # conf.disableHashing False => attackDumpedTable() is invoked; with no - # hashes present it must complete without raising and still emit values. - set_dbms("MySQL") - e = self._entries(cols=("id", "name")) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.disableHashing = False - emod.inject.getValue = lambda *a, **k: [["1", "alice"]] - - # Spy on attackDumpedTable: with disableHashing False it MUST be invoked - # after the values are dumped. A recorder replaces it so we can assert the - # call happened (and no real dictionary attack runs). - saved_attack = emod.attackDumpedTable - calls = {"n": 0} - emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) - try: - e.dumpTable() - finally: - emod.attackDumpedTable = saved_attack - - self.assertEqual(calls["n"], 1) - self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1) - - def test_missing_columns_skips_table(self): - # getColumns yields nothing for the targeted table => skip without fetching. - set_dbms("MySQL") - e = _TestEntries() - e.getColumnsResult = {"testdb": {"other": {"id": "int"}}} - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") - - e.dumpTable() - self.assertEqual(conf.dumper.tableValues, []) - - def test_multiple_tables_one_dumped(self): - set_dbms("MySQL") - e = _TestEntries() - e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}} - conf.db = "testdb" - conf.tbl = "users,posts" - conf.col = None - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpTable() - # both tables share the same cachedColumns dict => both dumped - tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] - self.assertIn("users", tables) - self.assertIn("posts", tables) - - def test_metadb_suffix_db(self): - # A db whose name carries the METADB_SUFFIX must not get a "db" prefix in - # kb.dumpTable, and dumping still succeeds. - from lib.core.settings import METADB_SUFFIX - set_dbms("MySQL") - metadb = "x%s" % METADB_SUFFIX - e = self._entries(db=metadb, tbl="t", cols=("c",)) - conf.db = metadb - conf.tbl = "t" - conf.col = None - emod.inject.getValue = lambda *a, **k: [["v"]] - - e.dumpTable() - self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"]) - - -class TestEntriesDumpAll(_GenericBase): - def test_dumpall_multiple_dbs_tables(self): - set_dbms("MySQL") - e = _TestEntries() - conf.db = None - conf.tbl = None - conf.col = None - e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]} - # dumpTable re-discovers columns per (db, tbl); supply both. - e.getColumnsResult = { - "db1": {"t1": {"a": "int"}}, - "db2": {"t2": {"b": "int"}}, - } - emod.inject.getValue = lambda *a, **k: [["x"]] - - e.dumpAll() - # Every table contributed a values batch. - self.assertEqual(len(conf.dumper.tableValues), 2) - - def test_dumpall_list_cached_tables(self): - # cachedTables as a bare list => wrapped under {None: [...]}. - set_dbms("MySQL") - e = _TestEntries() - conf.db = None - conf.tbl = None - conf.col = None - - # getTables sets cachedTables; emulate the list shape directly. - class _ListTables(_TestEntries): - def getTables(self_inner, bruteForce=None): - kb.data.cachedTables = ["users"] - - e = _ListTables() - # dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the - # None db via getCurrentDb() -> "testdb", so columns live under "testdb". - e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpAll() - self.assertTrue(conf.dumper.tableValues) - # The bare-list None db must be resolved via getCurrentDb() -> "testdb" - # before the dump; assert the dumped __infos__ carries the real db (not - # None) for the requested "users" table. - infos = conf.dumper.tableValues[-1]["__infos__"] - self.assertEqual(infos["db"], "testdb") - self.assertEqual(infos["table"], "users") - - def test_dumpall_exclude_skips_table(self): - set_dbms("MySQL") - e = _TestEntries() - conf.db = None - conf.tbl = None - conf.col = None - conf.exclude = "secret" - e.getTablesResult = {"db1": ["secret", "users"]} - e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}} - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpAll() - tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] - self.assertIn("users", tables) - self.assertNotIn("secret", tables) - - -class TestEntriesDumpFound(_GenericBase): - def _entries(self): - e = _TestEntries() - e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} - return e - - def test_dump_found_tables_yes_all(self): - set_dbms("MySQL") - e = self._entries() - emod.inject.getValue = lambda *a, **k: [["1"]] - # batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices. - e.dumpFoundTables({"testdb": ["users"]}) - self.assertTrue(conf.dumper.tableValues) - # The interactive selection must dump the REQUESTED db/table, not just - # "something": assert the dumped __infos__ maps to testdb.users. - infos = conf.dumper.tableValues[-1]["__infos__"] - self.assertEqual(infos["db"], "testdb") - self.assertEqual(infos["table"], "users") - - def test_dump_found_tables_declined(self): - set_dbms("MySQL") - e = self._entries() - - def _no(message, default=None, checkBatch=True, boolean=False): - if boolean: - return False - return default - - emod.readInput = _no - emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined") - e.dumpFoundTables({"testdb": ["users"]}) - self.assertEqual(conf.dumper.tableValues, []) - - def test_dump_found_column_yes_all(self): - set_dbms("MySQL") - e = self._entries() - emod.inject.getValue = lambda *a, **k: [["1"]] - dbs = {"testdb": {"users": {"id": "int"}}} - e.dumpFoundColumn(dbs, foundCols=None, colConsider='1') - self.assertTrue(conf.dumper.tableValues) - # The selection must dump the REQUESTED db/table mapping, not just - # "something": assert the dumped __infos__ maps to testdb.users. - infos = conf.dumper.tableValues[-1]["__infos__"] - self.assertEqual(infos["db"], "testdb") - self.assertEqual(infos["table"], "users") - - # --------------------------------------------------------------------------- # # custom.py # --------------------------------------------------------------------------- # diff --git a/tests/test_inference.py b/tests/test_inference.py deleted file mode 100644 index adac33d58..000000000 --- a/tests/test_inference.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Edge cases / control-flow branches of the blind-SQLi inference engine -(lib/techniques/blind/inference.py) plus the pure UNION configuration helper -(lib/techniques/union/use.py configUnion). - -Complements tests/test_inference_engine.py (which covers the happy-path char-by-char -extraction). Here we drive the REAL bisection() / queryOutputLength() against a mock -oracle (Request.queryPage replaced by a parser of our own parseable payload template) -to exercise the branches the engine test does not reach: - - * trivial returns: payload is None, length == 0 - * --first-char / --last-char range limiting (both via the function args and via - conf.firstChar / conf.lastChar) - * --hex output decoding of the assembled value - * kb.data.processChar post-processing hook - * session resume from HashDB: a fully cached value, and a PARTIAL_VALUE_MARKER - partial value that bisection continues from (against a REAL temp SQLite HashDB) - * queryOutputLength() forging + DIGITS-charset length retrieval - -No network, no live target, no real DBMS - exactly like the sibling engine test. -""" - -import os -import re -import sys -import tempfile -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.data import conf, kb -from lib.core.common import decodeDbmsHexValue -from lib.core.common import getCurrentThreadData -from lib.core.common import hashDBWrite -from lib.core.enums import CHARSET_TYPE -from lib.core.exception import SqlmapSyntaxException -from lib.core.settings import PARTIAL_VALUE_MARKER -from lib.request.connect import Connect -from lib.utils.hashdb import HashDB -import lib.techniques.blind.inference as inf -import lib.techniques.union.use as uu - -# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the -# greater-char marker (swapped to '=' on the final equality check). A parseable template -# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret. -TEMPLATE = "EXPR=%s IDX=%d CMP>%d" -_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)") - -# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path -_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False, - "charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False, - "repair": False, "flushSession": None, "freshQueries": None, "hashDB": None} -_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False, - "disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False, - "resumeValues": True, "inferenceMode": False} - - -class _InferenceCase(unittest.TestCase): - def setUp(self): - self._saved_conf = {k: conf.get(k) for k in _CONF} - self._saved_kb = {k: kb.get(k) for k in _KB} - self._saved_qp = Connect.queryPage - self._saved_processChar = kb.data.get("processChar") - for k, v in _CONF.items(): - conf[k] = v - for k, v in _KB.items(): - kb[k] = v - kb.data.processChar = None - set_dbms("MySQL") - - def tearDown(self): - for k, v in self._saved_conf.items(): - conf[k] = v - for k, v in self._saved_kb.items(): - kb[k] = v - kb.data.processChar = self._saved_processChar - Connect.queryPage = self._saved_qp - inf.Request.queryPage = self._saved_qp - - def _install_oracle(self, secret): - def oracle(payload=None, *args, **kwargs): - m = _PARSE.search(payload) - idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3)) - ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0 - return (ch > threshold) if op == ">" else (ch == threshold) - - Connect.queryPage = staticmethod(oracle) - inf.Request.queryPage = staticmethod(oracle) - - @staticmethod - def _reset_thread(): - td = getCurrentThreadData() - td.shared.value = "" - td.shared.index = [0] - td.shared.start = 0 - td.shared.count = 0 - - def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs): - self._install_oracle(secret) - self._reset_thread() - if length is None: - length = len(secret) - return inf.bisection(TEMPLATE, expression, length=length, **kwargs) - - -class TestTrivialReturns(_InferenceCase): - def test_none_payload(self): - # payload is None -> (0, None) without ever touching the oracle - self.assertEqual(inf.bisection(None, "SELECT x"), (0, None)) - - def test_zero_length(self): - # length == 0 -> (0, "") short-circuit - self._install_oracle("ignored") - self._reset_thread() - self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, "")) - - -class TestRangeLimiting(_InferenceCase): - SECRET = "ABCDEFGH" - - def test_first_char_arg(self): - # firstChar=3 -> start from the 3rd character (1-based) -> drop "AB" - _, value = self._bisect(self.SECRET, firstChar=3) - self.assertEqual(value, "CDEFGH") - - def test_last_char_arg(self): - # lastChar=4 -> stop after the 4th character - _, value = self._bisect(self.SECRET, lastChar=4) - self.assertEqual(value, "ABCD") - - def test_conf_first_char(self): - conf.firstChar = 4 - _, value = self._bisect(self.SECRET) - self.assertEqual(value, "DEFGH") - - def test_conf_last_char(self): - conf.lastChar = 3 - _, value = self._bisect(self.SECRET) - self.assertEqual(value, "ABC") - - def test_first_and_last_window(self): - # combined window: chars 3..6 inclusive -> "CDEF" - _, value = self._bisect(self.SECRET, firstChar=3, lastChar=6) - self.assertEqual(value, "CDEF") - - -class TestHexConvert(_InferenceCase): - def test_hex_output_decoded(self): - # --hex: the retrieved value is a hex string the engine decodes on the way out - conf.hexConvert = True - hexed = "48656C6C6F" # "Hello" - _, value = self._bisect(hexed) - self.assertEqual(value, "Hello") - self.assertEqual(value, decodeDbmsHexValue(hexed)) - - -class TestProcessCharHook(_InferenceCase): - def test_process_char_applied_to_each_char(self): - # kb.data.processChar transforms every assembled character - kb.data.processChar = lambda c: c.upper() - _, value = self._bisect("abcde") - self.assertEqual(value, "ABCDE") - - -class TestResumeFromHashDB(_InferenceCase): - """bisection() consults the session store first (hashDBRetrieve(checkConf=True)). - Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py).""" - - def setUp(self): - _InferenceCase.setUp(self) - fd, self.path = tempfile.mkstemp(suffix=".sqlite") - os.close(fd) - os.remove(self.path) # HashDB creates it lazily - conf.hashDB = HashDB(self.path) - # hashDBRetrieve/Write key off these - self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port")) - conf.hostname = "test.invalid" - conf.path = "/" - conf.port = 80 - - def tearDown(self): - conf.hostname, conf.path, conf.port = self._saved_loc - try: - conf.hashDB.closeAll() - except Exception: - pass - if os.path.exists(self.path): - os.remove(self.path) - _InferenceCase.tearDown(self) - - def test_full_value_resumed(self): - # a complete cached value short-circuits the whole bisection (0 queries) - hashDBWrite("SELECT cached", "RESUMED") - conf.hashDB.flush() - count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7) - self.assertEqual(value, "RESUMED") - self.assertEqual(count, 0) - - def test_partial_value_continued(self): - # a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix - # and extracts only the remaining characters - kb.inferenceMode = True # partial markers are honored only in inference mode - hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER) - conf.hashDB.flush() - count, value = self._bisect("ABCDE", expression="SELECT partial", length=5) - self.assertEqual(value, "ABCDE") - self.assertGreater(count, 0) # it did real work for "CDE" - - -class TestQueryOutputLength(_InferenceCase): - def test_length_retrieved(self): - # queryOutputLength forges a LENGTH() expression and runs bisection with the - # DIGITS charset; the mock "secret" is the textual length itself - self._install_oracle("42") - self._reset_thread() - self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42) - - def test_length_single_digit(self): - self._install_oracle("7") - self._reset_thread() - self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7) - - def test_digits_charset_extracts_number(self): - # direct bisection with the DIGITS charset (queryOutputLength's inner call) - _, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS) - self.assertEqual(value, "2026") - - -class TestConfigUnion(unittest.TestCase): - """lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols.""" - - _CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50} - - def setUp(self): - self._saved = {k: conf.get(k) for k in self._CONF} - self._saved_uchar = kb.get("uChar") - for k, v in self._CONF.items(): - conf[k] = v - - def tearDown(self): - for k, v in self._saved.items(): - conf[k] = v - kb.uChar = self._saved_uchar - - def test_char_and_range(self): - uu.configUnion(char="NULL", columns="2-6") - self.assertEqual(kb.uChar, "NULL") - self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6)) - - def test_single_column(self): - uu.configUnion(char="NULL", columns="4") - self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4)) - - def test_uchar_substitution_quoted(self): - # conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template - conf.uChar = "test" - uu.configUnion(char="x[CHAR]x", columns="1") - self.assertEqual(kb.uChar, "x'test'x") - - def test_uchar_substitution_digit(self): - # a digit conf.uChar is substituted unquoted - conf.uChar = "88" - uu.configUnion(char="[CHAR]", columns="1") - self.assertEqual(kb.uChar, "88") - - def test_conf_ucols_overrides_columns_arg(self): - # conf.uCols takes precedence over the columns argument - conf.uCols = "3-9" - uu.configUnion(char="NULL", columns="1-2") - self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9)) - - def test_non_integer_range_raises(self): - self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc") - - def test_inverted_range_raises(self): - self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2") - - def test_non_string_char_ignored(self): - # a non-string char leaves kb.uChar untouched (early return) - kb.uChar = "SENTINEL" - uu.configUnion(char=None, columns="1") - self.assertEqual(kb.uChar, "SENTINEL") - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_option.py b/tests/test_option.py new file mode 100644 index 000000000..b869a83d2 --- /dev/null +++ b/tests/test_option.py @@ -0,0 +1,1594 @@ +#!/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 socket +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, kb, logger +from lib.core.common import Backend +from lib.core.enums import AUTH_TYPE +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.settings import MAX_CONNECT_RETRIES +from lib.core.exception import SqlmapFilePathException +from lib.core.exception import SqlmapGenericException +from lib.core.exception import SqlmapMissingDependence +from lib.core.exception import SqlmapMissingMandatoryOptionException +from lib.core.exception import SqlmapSyntaxException +from lib.core.exception import SqlmapSystemException +from lib.core.exception import SqlmapUnsupportedDBMSException +from lib.core.exception import SqlmapValueException +from thirdparty.six.moves import urllib as _urllib + +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") + +# 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") + + +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) + + +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 _ImportSandboxMixin(object): + """Loaders in option.py (tamper/preprocess/postprocess) permanently + `sys.path.insert(0,