mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-06-30 21:41:03 +00:00
Update of tests
This commit is contained in:
parent
cb20a446ae
commit
2297c81309
32 changed files with 7177 additions and 7304 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
|||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.6.186"
|
||||
VERSION = "1.10.6.187"
|
||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||
|
|
|
|||
|
|
@ -4,15 +4,32 @@
|
|||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Payload assembly helpers in lib/core/agent.py.
|
||||
Consolidated unit coverage for lib/core/agent.py.
|
||||
|
||||
These are the (mostly) DBMS-independent string transforms that wrap, fold and
|
||||
clean a payload on its way to the wire: prefix/suffix, payload delimiters,
|
||||
field extraction, CONCAT folding, and RAND-marker cleanup. All values below
|
||||
were probed from real output, not assumed.
|
||||
This file merges the agent.py tests previously spread across
|
||||
test_agent.py, test_agent_dialects.py, test_core_more.py and
|
||||
test_core_extra.py:
|
||||
|
||||
* Payload assembly helpers (DBMS-independent string transforms that wrap,
|
||||
fold and clean a payload on its way to the wire): prefix/suffix, payload
|
||||
delimiters, field extraction, CONCAT folding, RAND-marker cleanup.
|
||||
|
||||
* Cross-dialect exercise of the payload-assembly helpers. agent.py builds SQL
|
||||
payloads from per-DBMS dialect templates (queries.xml); the helpers are pure
|
||||
given the identified back-end DBMS, so driving each one across EVERY
|
||||
supported dialect walks the dialect-specific branches (CAST forms,
|
||||
concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target.
|
||||
|
||||
* Argument-combination / shape coverage for forgeUnionQuery, limitQuery,
|
||||
whereQuery, getComment, concatQuery(unpack=False), cleanupPayload markers,
|
||||
adjustLateValues, getFields shapes, prefix/suffix args, nullAndCastField
|
||||
noCast, plus the pure agent helpers (extractPayload/replacePayload, ...).
|
||||
|
||||
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
|
@ -21,9 +38,122 @@ from _testutils import bootstrap, set_dbms
|
|||
bootstrap()
|
||||
|
||||
from lib.core.agent import agent
|
||||
from lib.core.data import conf, kb, queries
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.settings import PAYLOAD_DELIMITER
|
||||
from lib.core.settings import (
|
||||
PAYLOAD_DELIMITER,
|
||||
SLEEP_TIME_MARKER,
|
||||
BOUNDED_BASE64_MARKER,
|
||||
)
|
||||
|
||||
DIALECTS = sorted(queries.keys())
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Per-dialect expectation maps (keyed by the DBMS display name == queries key).
|
||||
#
|
||||
# These were derived by inspecting the actual agent.py output for every dialect
|
||||
# (the queries.xml templates drive the branches). They pin the *distinctive*
|
||||
# dialect token so an assertion fails if the dialect branch collapses to the
|
||||
# wrong form (e.g. concat operator swapped, null-wrapper dropped).
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
# concatQuery / simpleConcatenate join operator per dialect.
|
||||
CONCAT_OPERATOR = {
|
||||
"ClickHouse": "CONCAT(",
|
||||
"Informix": "CONCAT(",
|
||||
"MySQL": "CONCAT(",
|
||||
"SAP MaxDB": "CONCAT(",
|
||||
"Microsoft SQL Server": "+",
|
||||
"Sybase": "+",
|
||||
"Microsoft Access": "&",
|
||||
}
|
||||
# everything not listed above uses the SQL standard "||"
|
||||
CONCAT_OPERATOR_DEFAULT = "||"
|
||||
|
||||
# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect.
|
||||
NULL_WRAPPER = {
|
||||
"Altibase": "NVL",
|
||||
"Apache Derby": "COALESCE",
|
||||
"ClickHouse": "ifNull",
|
||||
"CrateDB": "COALESCE",
|
||||
"Cubrid": "IFNULL",
|
||||
"Firebird": "COALESCE",
|
||||
"FrontBase": "COALESCE",
|
||||
"H2": "IFNULL",
|
||||
"HSQLDB": "IFNULL",
|
||||
"IBM DB2": "COALESCE",
|
||||
"Informix": "NVL",
|
||||
"InterSystems Cache": "COALESCE",
|
||||
"Mckoi": "IF(",
|
||||
"Microsoft Access": "IIF",
|
||||
"Microsoft SQL Server": "ISNULL",
|
||||
"MimerSQL": "COALESCE",
|
||||
"MonetDB": "COALESCE",
|
||||
"MySQL": "IFNULL",
|
||||
"Oracle": "NVL",
|
||||
"PostgreSQL": "COALESCE",
|
||||
"Presto": "COALESCE",
|
||||
"Raima Database Manager": "IFNULL",
|
||||
"SAP MaxDB": "VALUE",
|
||||
"SQLite": "COALESCE",
|
||||
"Snowflake": "NVL",
|
||||
"Spanner": "IFNULL",
|
||||
"Sybase": "ISNULL",
|
||||
"Vertica": "COALESCE",
|
||||
"Virtuoso": "__MAX_NOTNULL",
|
||||
"eXtremeDB": "IFNULL",
|
||||
}
|
||||
|
||||
# hexConvertField: dialects that DO have a hex function, mapped to its token.
|
||||
HEX_FUNCTION = {
|
||||
"Altibase": "HEX_ENCODE(",
|
||||
"Cubrid": "HEX(",
|
||||
"H2": "RAWTOHEX(",
|
||||
"IBM DB2": "HEX(",
|
||||
"Microsoft SQL Server": "fn_varbintohexstr",
|
||||
"MySQL": "HEX(",
|
||||
"Oracle": "RAWTOHEX(",
|
||||
"PostgreSQL": "ENCODE(",
|
||||
"Presto": "TO_HEX(",
|
||||
"SAP MaxDB": "HEX(",
|
||||
"SQLite": "HEX(",
|
||||
"Spanner": "TO_HEX(",
|
||||
"Sybase": "BINTOSTR",
|
||||
"Vertica": "TO_HEX(",
|
||||
}
|
||||
# dialects that intentionally do NOT support hex conversion and return the
|
||||
# field unchanged (a no-op the old "colname in out" check silently masked).
|
||||
HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION)
|
||||
|
||||
# limitQuery: dialects whose limit template is empty so the call legitimately
|
||||
# raises (no .limit.query). These are skipped by name in the limit-token test.
|
||||
LIMIT_RAISES = {"Mckoi", "Raima Database Manager"}
|
||||
# dialects with no special limitQuery branch: the query is returned unchanged
|
||||
# (no limit token is emitted).
|
||||
LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"}
|
||||
# broad set of dialect limit tokens; every running, non-passthrough dialect
|
||||
# emits at least one of these.
|
||||
LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER")
|
||||
|
||||
|
||||
class DbmsStateMixin(object):
|
||||
"""Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak."""
|
||||
|
||||
def setUp(self):
|
||||
self._forcedDbms = kb.forcedDbms
|
||||
self._sticky = kb.stickyDBMS
|
||||
self._batch = conf.batch
|
||||
conf.batch = True
|
||||
|
||||
def tearDown(self):
|
||||
kb.forcedDbms = self._forcedDbms
|
||||
kb.stickyDBMS = self._sticky
|
||||
conf.batch = self._batch
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Single-DBMS payload-assembly helpers (formerly test_agent.py)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class TestPayloadDelimiters(unittest.TestCase):
|
||||
def test_add(self):
|
||||
|
|
@ -82,5 +212,557 @@ class TestCleanupPayload(unittest.TestCase):
|
|||
self.assertTrue(out.split()[-1].isdigit(), msg=out) # ...and replaced with a concrete number
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Cross-dialect smoke coverage (formerly test_agent_dialects.py)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class TestNullCastConcatFields(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.nullCastConcatFields("user,password")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
# both column names survive the null/cast/concat rewrite
|
||||
self.assertIn("user", out, msg=dbms)
|
||||
self.assertIn("password", out, msg=dbms)
|
||||
# the dialect-specific NULL-wrapper must be present (the column-name
|
||||
# check above is always satisfied and so cannot catch a broken
|
||||
# branch); this fails if the wrapper collapses to the wrong form.
|
||||
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
|
||||
|
||||
def test_literal_passthrough(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
# a bare quoted literal is returned untouched
|
||||
self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms)
|
||||
|
||||
|
||||
class TestNullAndCastField(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.nullAndCastField("colname")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("colname", out, msg=dbms)
|
||||
# dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...)
|
||||
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestHexConvertField(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.hexConvertField("colname")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("colname", out, msg=dbms)
|
||||
if dbms in HEX_FUNCTION:
|
||||
# the dialect's hex function wraps the field
|
||||
self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out))
|
||||
else:
|
||||
# intentional no-op: the field is returned verbatim. The old
|
||||
# "colname in out" check masked this; pin the exact identity.
|
||||
self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestConcatQueryDialects(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.concatQuery("SELECT user FROM users")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
# concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/
|
||||
# SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and
|
||||
# the rest use the SQL-standard ||. Assert the right operator so the
|
||||
# test fails if the dialect collapses to the wrong concatenation.
|
||||
expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT)
|
||||
self.assertIn(expected, out, msg="%s: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestSimpleConcatenate(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.simpleConcatenate("a", "b")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("a", out, msg=dbms)
|
||||
self.assertIn("b", out, msg=dbms)
|
||||
|
||||
|
||||
class TestForgeUnionQueryDialects(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
count = 3
|
||||
out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None,
|
||||
None, None, "NULL", None)
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("UNION", out.upper(), msg=dbms)
|
||||
# position -1 with char NULL fills every one of the `count` columns
|
||||
# with the char, so the NULL char must appear exactly `count` times.
|
||||
# (a hardcoded "UNION in out" check could not catch a wrong column
|
||||
# count.) Match NULL as a whole token to avoid matching substrings.
|
||||
self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count,
|
||||
msg="%s expected %d NULLs: %s" % (dbms, count, out))
|
||||
|
||||
|
||||
class TestLimitQueryDialects(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
|
||||
# Only Mckoi/Raima have an empty limit template and legitimately
|
||||
# raise; skip exactly those by name rather than swallowing *any*
|
||||
# exception (which would hide a real regression in another dialect).
|
||||
if dbms in LIMIT_RAISES:
|
||||
with self.assertRaises(Exception, msg=dbms):
|
||||
agent.limitQuery(0, "SELECT user FROM users", "user")
|
||||
continue
|
||||
|
||||
out = agent.limitQuery(0, "SELECT user FROM users", "user")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
|
||||
if dbms in LIMIT_PASSTHROUGH:
|
||||
# these dialects have no dedicated limitQuery branch and return
|
||||
# the query unchanged (documented no-op).
|
||||
self.assertEqual(out, "SELECT user FROM users", msg=dbms)
|
||||
else:
|
||||
# every other running dialect emits a real limit construct
|
||||
self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS),
|
||||
msg="%s missing limit token: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestForgeCaseStatement(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.forgeCaseStatement("1=1")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
# dialects vary on the conditional form (CASE / IIF / IF); the
|
||||
# condition itself is always embedded
|
||||
self.assertIn("1=1", out, msg=dbms)
|
||||
# ...but the conditional construct itself must also be present,
|
||||
# otherwise the "1=1" check alone could pass on a degenerate output.
|
||||
self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out,
|
||||
msg="%s missing conditional construct: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestPrefixSuffixAcrossDialects(unittest.TestCase):
|
||||
def test_prefix_suffix(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
prefix = agent.prefixQuery("1=1")
|
||||
suffix = agent.suffixQuery("1=1")
|
||||
self.assertIsInstance(prefix, str, msg=dbms)
|
||||
self.assertIsInstance(suffix, str, msg=dbms)
|
||||
# prefixQuery pads a leading space ahead of the expression by default
|
||||
self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix))
|
||||
# suffixQuery returns the expression itself (no extra clause/comment)
|
||||
self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix))
|
||||
|
||||
|
||||
class TestRunAsDBMSUserAndWhere(unittest.TestCase):
|
||||
def test_run_as_user_noop_without_conf(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
# without conf.dbmsCred the query is returned unchanged
|
||||
self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Argument-combination / shape coverage (formerly test_core_more.py)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase):
|
||||
"""forgeUnionQuery arg combinations not reached by the dialect smoke test."""
|
||||
|
||||
def test_limited_subselect_wraps_query(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# limited=True wraps the payload as (SELECT ...) at `position`, fills the
|
||||
# rest with `char`, and appends the FROM/comment/suffix
|
||||
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None,
|
||||
None, None, "NULL", None, limited=True)
|
||||
self.assertIn("(SELECT user FROM mysql.user)", out)
|
||||
self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out)
|
||||
# position 1 of 3 => NULL,<payload>,NULL
|
||||
self.assertEqual(out.count("NULL"), 2, msg=out)
|
||||
|
||||
def test_multiple_unions_appends_second_select(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.forgeUnionQuery("SELECT a FROM t", 0, 2, None, None, None,
|
||||
"NULL", None, multipleUnions="b")
|
||||
# the multipleUnions payload produces a *second* UNION ALL SELECT
|
||||
self.assertEqual(out.upper().count("UNION ALL SELECT"), 2, msg=out)
|
||||
self.assertIn("b", out)
|
||||
|
||||
def test_from_table_override(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.forgeUnionQuery("SELECT 1", 0, 1, None, None, None, "NULL",
|
||||
None, fromTable=" FROM dummytable")
|
||||
self.assertIn("FROM dummytable", out, msg=out)
|
||||
|
||||
def test_into_outfile_forces_null_position(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# an INTO OUTFILE clause forces position 0 / char NULL and re-appends the file part
|
||||
out = agent.forgeUnionQuery("SELECT a INTO OUTFILE '/tmp/o.txt' FROM t",
|
||||
1, 2, None, None, None, "NULL", None)
|
||||
self.assertIn("INTO OUTFILE '/tmp/o.txt'", out, msg=out)
|
||||
|
||||
def test_collate_clause_on_mysql(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# collate=True on MySQL wraps a non-NULL, non-numeric value in the
|
||||
# MYSQL_UNION_VALUE_CAST collation wrapper
|
||||
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 0, 1, None,
|
||||
None, None, "NULL", None, collate=True)
|
||||
self.assertIn("CONVERT", out.upper(), msg=out)
|
||||
|
||||
|
||||
class TestLimitQuery(DbmsStateMixin, unittest.TestCase):
|
||||
"""limitQuery dialect shapes beyond the single limitQuery(0,...) smoke test."""
|
||||
|
||||
def test_no_from_returns_unchanged(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
self.assertEqual(agent.limitQuery(5, "SELECT 1", "1"), "SELECT 1")
|
||||
|
||||
def test_mysql_appends_limit_offset_one(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.limitQuery(7, "SELECT user FROM mysql.user", "user")
|
||||
self.assertTrue(out.endswith("LIMIT 7,1"), msg=out)
|
||||
|
||||
def test_pgsql_offset_form(self):
|
||||
set_dbms(DBMS.PGSQL)
|
||||
out = agent.limitQuery(4, "SELECT usename FROM pg_shadow", "usename")
|
||||
self.assertIn("OFFSET 4 LIMIT 1", out, msg=out)
|
||||
|
||||
def test_oracle_rownum_wrap(self):
|
||||
set_dbms(DBMS.ORACLE)
|
||||
out = agent.limitQuery(2, "SELECT banner FROM v$version", ["banner"])
|
||||
# Oracle wraps in a ROWNUM-bounded subselect ending with =<num+1>
|
||||
self.assertIn("ROWNUM", out.upper(), msg=out)
|
||||
self.assertTrue(out.rstrip().endswith("=3"), msg=out)
|
||||
|
||||
def test_firebird_first_skip(self):
|
||||
set_dbms(DBMS.FIREBIRD)
|
||||
out = agent.limitQuery(3, "SELECT foo FROM bar", "foo")
|
||||
self.assertIsInstance(out, str)
|
||||
self.assertIn("foo", out)
|
||||
# Firebird uses ROWS <num+1> TO <num+1> (the FIRST/SKIP emulation); pin
|
||||
# the exact shape so a broken offset arithmetic is caught.
|
||||
self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out)
|
||||
|
||||
def test_mssql_top_not_in(self):
|
||||
set_dbms(DBMS.MSSQL)
|
||||
out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name")
|
||||
# MSSQL emulates LIMIT via TOP + NOT IN
|
||||
self.assertIn("TOP", out.upper(), msg=out)
|
||||
self.assertIn("NOT IN", out.upper(), msg=out)
|
||||
|
||||
|
||||
class TestWhereQuery(DbmsStateMixin, unittest.TestCase):
|
||||
"""whereQuery only acts when conf.dumpWhere is set."""
|
||||
|
||||
def setUp(self):
|
||||
DbmsStateMixin.setUp(self)
|
||||
self._dumpWhere = conf.dumpWhere
|
||||
self._tbl = conf.tbl
|
||||
|
||||
def tearDown(self):
|
||||
conf.dumpWhere = self._dumpWhere
|
||||
conf.tbl = self._tbl
|
||||
DbmsStateMixin.tearDown(self)
|
||||
|
||||
def test_no_dumpwhere_is_identity(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = None
|
||||
self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t")
|
||||
|
||||
def test_appends_where_clause(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>10"
|
||||
conf.tbl = None
|
||||
out = agent.whereQuery("SELECT a FROM t")
|
||||
self.assertIn("WHERE id>10", out, msg=out)
|
||||
|
||||
def test_existing_where_gets_anded(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>10"
|
||||
conf.tbl = None
|
||||
out = agent.whereQuery("SELECT a FROM t WHERE b=1")
|
||||
self.assertIn("AND id>10", out, msg=out)
|
||||
|
||||
def test_order_by_suffix_preserved(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>10"
|
||||
conf.tbl = None
|
||||
out = agent.whereQuery("SELECT a FROM t ORDER BY a")
|
||||
# the genuine trailing ORDER BY is kept after the spliced WHERE
|
||||
self.assertIn("WHERE id>10", out, msg=out)
|
||||
# the ORDER BY must survive *after* the spliced WHERE clause; the
|
||||
# substring check alone could pass even if the suffix were dropped.
|
||||
self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out)
|
||||
|
||||
|
||||
class TestGetComment(unittest.TestCase):
|
||||
def test_present(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x")
|
||||
|
||||
def test_absent_returns_empty(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
self.assertEqual(agent.getComment(AttribDict()), "")
|
||||
|
||||
|
||||
class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase):
|
||||
def test_unpack_false_returns_input_unchanged(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False),
|
||||
"SELECT a FROM t")
|
||||
|
||||
def test_pgsql_unpack_uses_pipe_concat(self):
|
||||
set_dbms(DBMS.PGSQL)
|
||||
out = agent.concatQuery("SELECT usename FROM pg_shadow")
|
||||
self.assertIn("||", out, msg=out)
|
||||
self.assertIn(kb.chars.start, out, msg=out)
|
||||
self.assertIn(kb.chars.stop, out, msg=out)
|
||||
|
||||
|
||||
class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase):
|
||||
def test_origvalue_digit_inlined(self):
|
||||
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42")
|
||||
self.assertEqual(out, "x=42")
|
||||
|
||||
def test_origvalue_nondigit_quoted(self):
|
||||
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc")
|
||||
self.assertIn("'abc'", out, msg=out)
|
||||
|
||||
def test_original_marker_raw_substitution(self):
|
||||
out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw")
|
||||
self.assertEqual(out, "p=raw")
|
||||
|
||||
def test_space_replace_marker(self):
|
||||
out = agent.cleanupPayload("a[SPACE_REPLACE]b")
|
||||
self.assertEqual(out, "a%sb" % kb.chars.space)
|
||||
|
||||
def test_non_string_returns_none(self):
|
||||
self.assertIsNone(agent.cleanupPayload(None))
|
||||
|
||||
|
||||
class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase):
|
||||
def test_sleeptime_replaced_with_timesec(self):
|
||||
out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER)
|
||||
self.assertEqual(out, "SLEEP(%s)" % conf.timeSec)
|
||||
self.assertNotIn(SLEEP_TIME_MARKER, out)
|
||||
|
||||
def test_randnum_marker_substituted(self):
|
||||
out = agent.adjustLateValues("v=[RANDNUM]")
|
||||
self.assertNotIn("[RANDNUM]", out)
|
||||
self.assertTrue(out.split("=")[1].isdigit(), msg=out)
|
||||
|
||||
def test_bounded_base64_marker_encoded(self):
|
||||
payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER)
|
||||
out = agent.adjustLateValues(payload)
|
||||
# the marked region is base64-encoded and the markers are consumed
|
||||
self.assertNotIn(BOUNDED_BASE64_MARKER, out)
|
||||
self.assertEqual(out, "QUI=")
|
||||
|
||||
def test_empty_payload_passthrough(self):
|
||||
self.assertEqual(agent.adjustLateValues(""), "")
|
||||
|
||||
|
||||
class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase):
|
||||
def test_select_top(self):
|
||||
set_dbms(DBMS.MSSQL)
|
||||
res = agent.getFields("SELECT TOP 1 name FROM sysobjects")
|
||||
self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched")
|
||||
self.assertEqual(res[6], "name")
|
||||
|
||||
def test_distinct(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
res = agent.getFields("SELECT DISTINCT(name) FROM t")
|
||||
self.assertEqual(res[6], "name")
|
||||
|
||||
def test_function_is_single_element(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
res = agent.getFields("SELECT COUNT(*) FROM t")
|
||||
self.assertEqual(res[5], ["COUNT(*)"])
|
||||
|
||||
def test_no_from_keeps_whole_select_list(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
res = agent.getFields("SELECT a,b,c")
|
||||
self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM")
|
||||
self.assertEqual(res[5], ["a", "b", "c"])
|
||||
|
||||
|
||||
class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase):
|
||||
def test_prefix_with_explicit_prefix(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.prefixQuery("1=1", prefix="')")
|
||||
self.assertIn("')", out, msg=out)
|
||||
self.assertTrue(out.endswith("1=1"), msg=out)
|
||||
|
||||
def test_prefix_group_by_clause_uses_prefix_verbatim(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# clause == [2] (GROUP BY / ORDER BY) => no trailing space added
|
||||
out = agent.prefixQuery("1=1", prefix="X", clause=[2])
|
||||
self.assertEqual(out, "X1=1")
|
||||
|
||||
def test_suffix_appends_comment(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.suffixQuery("1=1", comment="-- -")
|
||||
self.assertTrue(out.startswith("1=1"), msg=out)
|
||||
self.assertIn("-", out)
|
||||
|
||||
def test_suffix_appends_suffix_no_comment(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.suffixQuery("1=1", suffix="')")
|
||||
self.assertIn("')", out, msg=out)
|
||||
|
||||
|
||||
class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase):
|
||||
def setUp(self):
|
||||
DbmsStateMixin.setUp(self)
|
||||
self._noCast = conf.noCast
|
||||
|
||||
def tearDown(self):
|
||||
conf.noCast = self._noCast
|
||||
DbmsStateMixin.tearDown(self)
|
||||
|
||||
def test_nocast_returns_field_unchanged(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.noCast = True
|
||||
self.assertEqual(agent.nullAndCastField("colname"), "colname")
|
||||
|
||||
def test_cast_present_when_nocast_off(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.noCast = False
|
||||
out = agent.nullAndCastField("colname")
|
||||
self.assertIn("CAST", out.upper(), msg=out)
|
||||
self.assertIn("colname", out)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Pure agent helpers (formerly test_core_extra.py)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class TestAgentPure(unittest.TestCase):
|
||||
"""Pure agent.py methods independent of full injection state."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from lib.core.agent import agent
|
||||
cls.agent = agent
|
||||
|
||||
def tearDown(self):
|
||||
set_dbms(None)
|
||||
|
||||
def test_get_comment_present(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
request = AttribDict()
|
||||
request.comment = "-- foo"
|
||||
self.assertEqual(self.agent.getComment(request), "-- foo")
|
||||
|
||||
def test_get_comment_absent(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
request = AttribDict()
|
||||
self.assertEqual(self.agent.getComment(request), "")
|
||||
|
||||
def test_add_payload_delimiters(self):
|
||||
from lib.core.settings import PAYLOAD_DELIMITER
|
||||
value = "1 AND 1=1"
|
||||
result = self.agent.addPayloadDelimiters(value)
|
||||
self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER))
|
||||
# falsy value returned unchanged
|
||||
self.assertEqual(self.agent.addPayloadDelimiters(""), "")
|
||||
|
||||
def test_remove_payload_delimiters_roundtrip(self):
|
||||
self.assertEqual(
|
||||
self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")),
|
||||
"1 AND 1=1",
|
||||
)
|
||||
|
||||
def test_extract_payload(self):
|
||||
wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix"
|
||||
self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1")
|
||||
|
||||
def test_replace_payload(self):
|
||||
wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix"
|
||||
replaced = self.agent.replacePayload(wrapped, "NEW")
|
||||
self.assertEqual(self.agent.extractPayload(replaced), "NEW")
|
||||
# surrounding text preserved
|
||||
self.assertTrue(replaced.startswith("prefix"))
|
||||
self.assertTrue(replaced.endswith("suffix"))
|
||||
|
||||
def test_simple_concatenate_mysql(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# MySQL concatenate query template is 'CONCAT(%s,%s)'
|
||||
self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)")
|
||||
|
||||
def test_hex_convert_field_mysql(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# MySQL hex template is 'HEX(%s)'
|
||||
self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)")
|
||||
|
||||
def test_get_fields_select_from(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
result = self.agent.getFields("SELECT a, b FROM users")
|
||||
fieldsToCastList = result[5]
|
||||
fieldsToCastStr = result[6]
|
||||
self.assertEqual(fieldsToCastStr, "a, b")
|
||||
self.assertEqual(fieldsToCastList, ["a", "b"])
|
||||
|
||||
def test_get_fields_no_from(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list
|
||||
result = self.agent.getFields("SELECT 1")
|
||||
fieldsSelectFrom = result[0]
|
||||
self.assertIsNone(fieldsSelectFrom)
|
||||
self.assertEqual(result[6], "1")
|
||||
|
||||
|
||||
class TestAgentWhereQuery(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from lib.core.agent import agent
|
||||
cls.agent = agent
|
||||
|
||||
def setUp(self):
|
||||
self._old_dumpWhere = conf.dumpWhere
|
||||
self._old_tbl = conf.tbl
|
||||
conf.tbl = None
|
||||
|
||||
def tearDown(self):
|
||||
conf.dumpWhere = self._old_dumpWhere
|
||||
conf.tbl = self._old_tbl
|
||||
set_dbms(None)
|
||||
|
||||
def test_no_dumpwhere_passthrough(self):
|
||||
conf.dumpWhere = None
|
||||
query = "SELECT a FROM t"
|
||||
self.assertEqual(self.agent.whereQuery(query), query)
|
||||
|
||||
def test_appends_where_clause(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>0"
|
||||
# no existing WHERE -> appends ' WHERE id>0'
|
||||
self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0")
|
||||
|
||||
def test_and_when_where_present(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>0"
|
||||
# existing WHERE -> appended with AND
|
||||
self.assertEqual(
|
||||
self.agent.whereQuery("SELECT a FROM t WHERE x=1"),
|
||||
"SELECT a FROM t WHERE x=1 AND id>0",
|
||||
)
|
||||
|
||||
def test_splices_before_order_by(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>0"
|
||||
# WHERE must be spliced before the trailing ORDER BY suffix
|
||||
self.assertEqual(
|
||||
self.agent.whereQuery("SELECT a FROM t ORDER BY a"),
|
||||
"SELECT a FROM t WHERE id>0 ORDER BY a",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
|
|||
|
|
@ -1,274 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Cross-dialect exercise of lib/core/agent.py payload-assembly helpers.
|
||||
|
||||
agent.py builds SQL payloads from per-DBMS dialect templates (queries.xml).
|
||||
The helpers are pure given the identified back-end DBMS, so driving each one
|
||||
across EVERY supported dialect walks the dialect-specific branches (CAST forms,
|
||||
concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target.
|
||||
|
||||
These are smoke-level assertions (right type, dialect tokens present) rather than
|
||||
golden strings: the goal is to traverse the dialect branches the single-DBMS
|
||||
tests in test_agent.py do not reach.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.agent import agent
|
||||
from lib.core.data import queries
|
||||
|
||||
DIALECTS = sorted(queries.keys())
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Per-dialect expectation maps (keyed by the DBMS display name == queries key).
|
||||
#
|
||||
# These were derived by inspecting the actual agent.py output for every dialect
|
||||
# (the queries.xml templates drive the branches). They pin the *distinctive*
|
||||
# dialect token so an assertion fails if the dialect branch collapses to the
|
||||
# wrong form (e.g. concat operator swapped, null-wrapper dropped).
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
# concatQuery / simpleConcatenate join operator per dialect.
|
||||
CONCAT_OPERATOR = {
|
||||
"ClickHouse": "CONCAT(",
|
||||
"Informix": "CONCAT(",
|
||||
"MySQL": "CONCAT(",
|
||||
"SAP MaxDB": "CONCAT(",
|
||||
"Microsoft SQL Server": "+",
|
||||
"Sybase": "+",
|
||||
"Microsoft Access": "&",
|
||||
}
|
||||
# everything not listed above uses the SQL standard "||"
|
||||
CONCAT_OPERATOR_DEFAULT = "||"
|
||||
|
||||
# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect.
|
||||
NULL_WRAPPER = {
|
||||
"Altibase": "NVL",
|
||||
"Apache Derby": "COALESCE",
|
||||
"ClickHouse": "ifNull",
|
||||
"CrateDB": "COALESCE",
|
||||
"Cubrid": "IFNULL",
|
||||
"Firebird": "COALESCE",
|
||||
"FrontBase": "COALESCE",
|
||||
"H2": "IFNULL",
|
||||
"HSQLDB": "IFNULL",
|
||||
"IBM DB2": "COALESCE",
|
||||
"Informix": "NVL",
|
||||
"InterSystems Cache": "COALESCE",
|
||||
"Mckoi": "IF(",
|
||||
"Microsoft Access": "IIF",
|
||||
"Microsoft SQL Server": "ISNULL",
|
||||
"MimerSQL": "COALESCE",
|
||||
"MonetDB": "COALESCE",
|
||||
"MySQL": "IFNULL",
|
||||
"Oracle": "NVL",
|
||||
"PostgreSQL": "COALESCE",
|
||||
"Presto": "COALESCE",
|
||||
"Raima Database Manager": "IFNULL",
|
||||
"SAP MaxDB": "VALUE",
|
||||
"SQLite": "COALESCE",
|
||||
"Snowflake": "NVL",
|
||||
"Spanner": "IFNULL",
|
||||
"Sybase": "ISNULL",
|
||||
"Vertica": "COALESCE",
|
||||
"Virtuoso": "__MAX_NOTNULL",
|
||||
"eXtremeDB": "IFNULL",
|
||||
}
|
||||
|
||||
# hexConvertField: dialects that DO have a hex function, mapped to its token.
|
||||
HEX_FUNCTION = {
|
||||
"Altibase": "HEX_ENCODE(",
|
||||
"Cubrid": "HEX(",
|
||||
"H2": "RAWTOHEX(",
|
||||
"IBM DB2": "HEX(",
|
||||
"Microsoft SQL Server": "fn_varbintohexstr",
|
||||
"MySQL": "HEX(",
|
||||
"Oracle": "RAWTOHEX(",
|
||||
"PostgreSQL": "ENCODE(",
|
||||
"Presto": "TO_HEX(",
|
||||
"SAP MaxDB": "HEX(",
|
||||
"SQLite": "HEX(",
|
||||
"Spanner": "TO_HEX(",
|
||||
"Sybase": "BINTOSTR",
|
||||
"Vertica": "TO_HEX(",
|
||||
}
|
||||
# dialects that intentionally do NOT support hex conversion and return the
|
||||
# field unchanged (a no-op the old "colname in out" check silently masked).
|
||||
HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION)
|
||||
|
||||
# limitQuery: dialects whose limit template is empty so the call legitimately
|
||||
# raises (no .limit.query). These are skipped by name in the limit-token test.
|
||||
LIMIT_RAISES = {"Mckoi", "Raima Database Manager"}
|
||||
# dialects with no special limitQuery branch: the query is returned unchanged
|
||||
# (no limit token is emitted).
|
||||
LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"}
|
||||
# broad set of dialect limit tokens; every running, non-passthrough dialect
|
||||
# emits at least one of these.
|
||||
LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER")
|
||||
|
||||
|
||||
class TestNullCastConcatFields(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.nullCastConcatFields("user,password")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
# both column names survive the null/cast/concat rewrite
|
||||
self.assertIn("user", out, msg=dbms)
|
||||
self.assertIn("password", out, msg=dbms)
|
||||
# the dialect-specific NULL-wrapper must be present (the column-name
|
||||
# check above is always satisfied and so cannot catch a broken
|
||||
# branch); this fails if the wrapper collapses to the wrong form.
|
||||
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
|
||||
|
||||
def test_literal_passthrough(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
# a bare quoted literal is returned untouched
|
||||
self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms)
|
||||
|
||||
|
||||
class TestNullAndCastField(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.nullAndCastField("colname")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("colname", out, msg=dbms)
|
||||
# dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...)
|
||||
self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestHexConvertField(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.hexConvertField("colname")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("colname", out, msg=dbms)
|
||||
if dbms in HEX_FUNCTION:
|
||||
# the dialect's hex function wraps the field
|
||||
self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out))
|
||||
else:
|
||||
# intentional no-op: the field is returned verbatim. The old
|
||||
# "colname in out" check masked this; pin the exact identity.
|
||||
self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestConcatQuery(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.concatQuery("SELECT user FROM users")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
# concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/
|
||||
# SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and
|
||||
# the rest use the SQL-standard ||. Assert the right operator so the
|
||||
# test fails if the dialect collapses to the wrong concatenation.
|
||||
expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT)
|
||||
self.assertIn(expected, out, msg="%s: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestSimpleConcatenate(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.simpleConcatenate("a", "b")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("a", out, msg=dbms)
|
||||
self.assertIn("b", out, msg=dbms)
|
||||
|
||||
|
||||
class TestForgeUnionQuery(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
count = 3
|
||||
out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None,
|
||||
None, None, "NULL", None)
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
self.assertIn("UNION", out.upper(), msg=dbms)
|
||||
# position -1 with char NULL fills every one of the `count` columns
|
||||
# with the char, so the NULL char must appear exactly `count` times.
|
||||
# (a hardcoded "UNION in out" check could not catch a wrong column
|
||||
# count.) Match NULL as a whole token to avoid matching substrings.
|
||||
self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count,
|
||||
msg="%s expected %d NULLs: %s" % (dbms, count, out))
|
||||
|
||||
|
||||
class TestLimitQuery(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
|
||||
# Only Mckoi/Raima have an empty limit template and legitimately
|
||||
# raise; skip exactly those by name rather than swallowing *any*
|
||||
# exception (which would hide a real regression in another dialect).
|
||||
if dbms in LIMIT_RAISES:
|
||||
with self.assertRaises(Exception, msg=dbms):
|
||||
agent.limitQuery(0, "SELECT user FROM users", "user")
|
||||
continue
|
||||
|
||||
out = agent.limitQuery(0, "SELECT user FROM users", "user")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
|
||||
if dbms in LIMIT_PASSTHROUGH:
|
||||
# these dialects have no dedicated limitQuery branch and return
|
||||
# the query unchanged (documented no-op).
|
||||
self.assertEqual(out, "SELECT user FROM users", msg=dbms)
|
||||
else:
|
||||
# every other running dialect emits a real limit construct
|
||||
self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS),
|
||||
msg="%s missing limit token: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestForgeCaseStatement(unittest.TestCase):
|
||||
def test_all_dialects(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
out = agent.forgeCaseStatement("1=1")
|
||||
self.assertIsInstance(out, str, msg=dbms)
|
||||
# dialects vary on the conditional form (CASE / IIF / IF); the
|
||||
# condition itself is always embedded
|
||||
self.assertIn("1=1", out, msg=dbms)
|
||||
# ...but the conditional construct itself must also be present,
|
||||
# otherwise the "1=1" check alone could pass on a degenerate output.
|
||||
self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out,
|
||||
msg="%s missing conditional construct: %s" % (dbms, out))
|
||||
|
||||
|
||||
class TestPrefixSuffixAcrossDialects(unittest.TestCase):
|
||||
def test_prefix_suffix(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
prefix = agent.prefixQuery("1=1")
|
||||
suffix = agent.suffixQuery("1=1")
|
||||
self.assertIsInstance(prefix, str, msg=dbms)
|
||||
self.assertIsInstance(suffix, str, msg=dbms)
|
||||
# prefixQuery pads a leading space ahead of the expression by default
|
||||
self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix))
|
||||
# suffixQuery returns the expression itself (no extra clause/comment)
|
||||
self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix))
|
||||
|
||||
|
||||
class TestRunAsDBMSUserAndWhere(unittest.TestCase):
|
||||
def test_run_as_user_noop_without_conf(self):
|
||||
for dbms in DIALECTS:
|
||||
set_dbms(dbms)
|
||||
# without conf.dbmsCred the query is returned unchanged
|
||||
self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
198
tests/test_brute.py
Normal file
198
tests/test_brute.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Unit coverage for lib/utils/brute.py.
|
||||
|
||||
tableExists / columnExists are driven with conf.direct=True and the external
|
||||
collaborators (inject.checkBooleanExpression, getFileItems, runThreads,
|
||||
getPageWordSet) monkeypatched so the check runs synchronously, deterministically
|
||||
and offline; plus _addPageTextWords.
|
||||
|
||||
Any global conf/kb/Backend state that a call reads or writes is snapshotted in
|
||||
setUp and restored in tearDown so test ordering is irrelevant.
|
||||
|
||||
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.enums import DBMS
|
||||
|
||||
import lib.utils.brute as brute
|
||||
from lib.request import inject
|
||||
|
||||
|
||||
class DbmsStateMixin(object):
|
||||
"""Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak."""
|
||||
|
||||
def setUp(self):
|
||||
self._forcedDbms = kb.forcedDbms
|
||||
self._sticky = kb.stickyDBMS
|
||||
self._batch = conf.batch
|
||||
conf.batch = True
|
||||
|
||||
def tearDown(self):
|
||||
kb.forcedDbms = self._forcedDbms
|
||||
kb.stickyDBMS = self._sticky
|
||||
conf.batch = self._batch
|
||||
|
||||
|
||||
class TestBrute(DbmsStateMixin, unittest.TestCase):
|
||||
"""Drive tableExists / columnExists with all external collaborators stubbed.
|
||||
|
||||
conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression,
|
||||
getFileItems and runThreads are monkeypatched so the check runs synchronously,
|
||||
deterministically and offline. getPageWordSet is neutralized so the wordlist is
|
||||
just what the stub returns.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
DbmsStateMixin.setUp(self)
|
||||
self._saved_conf = {k: conf.get(k) for k in
|
||||
("direct", "db", "tbl", "threads", "api", "verbose")}
|
||||
self._choices = kb.choices
|
||||
self._cachedTables = kb.data.get("cachedTables")
|
||||
self._cachedColumns = kb.data.get("cachedColumns")
|
||||
self._brute = kb.brute
|
||||
self._origPage = kb.originalPage
|
||||
|
||||
# stub the collaborators
|
||||
self._orig_cbe = inject.checkBooleanExpression
|
||||
self._orig_brute_cbe = brute.inject.checkBooleanExpression
|
||||
self._orig_getFileItems = brute.getFileItems
|
||||
self._orig_runThreads = brute.runThreads
|
||||
self._orig_getPageWordSet = brute.getPageWordSet
|
||||
|
||||
from lib.core.datatype import AttribDict
|
||||
kb.choices = AttribDict(keycheck=False)
|
||||
kb.choices.tableExists = None
|
||||
kb.choices.columnExists = None
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.cachedColumns = {}
|
||||
kb.brute = AttribDict({"tables": [], "columns": []})
|
||||
kb.originalPage = None
|
||||
|
||||
conf.direct = True
|
||||
conf.db = None
|
||||
conf.threads = 1
|
||||
conf.api = False
|
||||
conf.verbose = 0
|
||||
|
||||
# runThreads -> just call the worker once synchronously
|
||||
def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs):
|
||||
kb.threadContinue = True
|
||||
threadFunction()
|
||||
brute.runThreads = _fakeRunThreads
|
||||
# no page words injected into the wordlist
|
||||
brute.getPageWordSet = lambda page: set()
|
||||
# wordlist file -> small fixed list
|
||||
brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"]
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
kb.choices = self._choices
|
||||
if self._cachedTables is None:
|
||||
kb.data.pop("cachedTables", None)
|
||||
else:
|
||||
kb.data.cachedTables = self._cachedTables
|
||||
if self._cachedColumns is None:
|
||||
kb.data.pop("cachedColumns", None)
|
||||
else:
|
||||
kb.data.cachedColumns = self._cachedColumns
|
||||
kb.brute = self._brute
|
||||
kb.originalPage = self._origPage
|
||||
brute.inject.checkBooleanExpression = self._orig_brute_cbe
|
||||
brute.getFileItems = self._orig_getFileItems
|
||||
brute.runThreads = self._orig_runThreads
|
||||
brute.getPageWordSet = self._orig_getPageWordSet
|
||||
DbmsStateMixin.tearDown(self)
|
||||
|
||||
def test_table_exists_collects_true_results(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
|
||||
def _cbe(expression, expectingNone=True):
|
||||
# initial sanity probe (random table) -> must be False, otherwise the
|
||||
# function raises SqlmapDataException; then only "users" exists.
|
||||
return "users" in expression
|
||||
brute.inject.checkBooleanExpression = _cbe
|
||||
|
||||
result = brute.tableExists("/nonexistent/tables.txt")
|
||||
# cachedTables keyed by conf.db (None here) holds the discovered table
|
||||
self.assertIn(None, result)
|
||||
self.assertIn("users", result[None])
|
||||
self.assertNotIn("logs", result.get(None, []))
|
||||
# also recorded in kb.brute.tables as (db, table)
|
||||
self.assertIn((None, "users"), kb.brute.tables)
|
||||
|
||||
def test_table_exists_invalid_results_raises(self):
|
||||
from lib.core.exception import SqlmapDataException
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# the initial random-table probe returns True -> "invalid results" guard
|
||||
brute.inject.checkBooleanExpression = lambda *a, **k: True
|
||||
with self.assertRaises(SqlmapDataException):
|
||||
brute.tableExists("/nonexistent/tables.txt")
|
||||
|
||||
def test_column_exists_requires_table(self):
|
||||
from lib.core.exception import SqlmapMissingMandatoryOptionException
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.tbl = None
|
||||
# the sanity probe is False so we reach the missing-table guard
|
||||
brute.inject.checkBooleanExpression = lambda *a, **k: False
|
||||
with self.assertRaises(SqlmapMissingMandatoryOptionException):
|
||||
brute.columnExists("/nonexistent/columns.txt")
|
||||
|
||||
def test_column_exists_collects_and_types(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.tbl = "users"
|
||||
brute.getFileItems = lambda *a, **k: ["id", "name"]
|
||||
|
||||
calls = {"n": 0}
|
||||
|
||||
def _cbe(expression, expectingNone=True):
|
||||
calls["n"] += 1
|
||||
# initial sanity probe uses two random strings (no real column name)
|
||||
if "id" not in expression and "name" not in expression:
|
||||
return False
|
||||
# MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`.
|
||||
# 'id' is numeric (no non-digit chars => probe False => numeric);
|
||||
# 'name' is non-numeric (has non-digit chars => probe True => non-numeric).
|
||||
if "REGEXP" in expression:
|
||||
return "name" in expression
|
||||
# plain existence check (EXISTS(SELECT <col> FROM <tbl>)) => both columns exist
|
||||
return True
|
||||
brute.inject.checkBooleanExpression = _cbe
|
||||
|
||||
result = brute.columnExists("/nonexistent/columns.txt")
|
||||
self.assertIn(None, result)
|
||||
cols = result[None]["users"]
|
||||
# column names are run through safeSQLIdentificatorNaming, so the MySQL
|
||||
# reserved word "name" comes back backtick-quoted
|
||||
from lib.core.common import safeSQLIdentificatorNaming, getText
|
||||
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("id"))), "numeric")
|
||||
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("name"))), "non-numeric")
|
||||
|
||||
def test_add_page_text_words_filters(self):
|
||||
# restore the real getPageWordSet for this one and drive it directly
|
||||
brute.getPageWordSet = self._orig_getPageWordSet
|
||||
kb.originalPage = u"<html>admin password 1abc xy verylongword</html>"
|
||||
words = brute._addPageTextWords()
|
||||
# words <= 2 chars or starting with a digit are dropped
|
||||
self.assertIn("admin", words)
|
||||
self.assertIn("password", words)
|
||||
self.assertNotIn("xy", words)
|
||||
self.assertNotIn("1abc", words)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
1715
tests/test_common.py
Normal file
1715
tests/test_common.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,466 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Pure / near-pure parsers and state helpers in lib/core/common.py that are NOT
|
||||
already exercised by tests/test_common_utils.py.
|
||||
|
||||
Covered here:
|
||||
* proxy-log parsers reached through parseRequestFile()
|
||||
(_parseBurpLog plain log, _parseBurpLog Burp XML history, _parseWebScarabLog)
|
||||
* parseTargetDirect() non-smoke branch (driver resolution for SQLite)
|
||||
* removeReflectiveValues() reflected-payload masking
|
||||
* findPageForms() HTML <form> and inline JS POST discovery
|
||||
* saveConfig() .ini serialization
|
||||
* getSQLSnippet() proc-file loading + variable substitution
|
||||
* checkSystemEncoding() (no-op on a normal default encoding)
|
||||
* Format.getOs() fingerprint humanizer
|
||||
* Backend setters/getters (setOs/getOs, setOsVersion, setOsServicePack,
|
||||
setVersion/getVersion/setVersionList)
|
||||
* urlencode() extra branches (LIKE percent-encoding, convall, limit, direct)
|
||||
* safeStringFormat() extra branches (PAYLOAD_DELIMITER region, scalar percent)
|
||||
|
||||
Everything is run in isolation (no network, no DBMS). Any function that
|
||||
reads/writes global conf/kb/Backend state has that state saved and restored
|
||||
around the call so test ordering stays irrelevant. Temp files go to the
|
||||
session scratchpad and are removed.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
from lib.core.common import (
|
||||
parseRequestFile,
|
||||
parseTargetDirect,
|
||||
removeReflectiveValues,
|
||||
findPageForms,
|
||||
saveConfig,
|
||||
getSQLSnippet,
|
||||
checkSystemEncoding,
|
||||
urlencode,
|
||||
safeStringFormat,
|
||||
Format,
|
||||
Backend,
|
||||
)
|
||||
from lib.core.data import kb, conf
|
||||
from lib.core.enums import DBMS, HTTPMETHOD
|
||||
from lib.core.settings import REFLECTED_VALUE_MARKER, PAYLOAD_DELIMITER
|
||||
|
||||
SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad"
|
||||
|
||||
|
||||
def _write_temp(content, suffix):
|
||||
"""Write `content` (str) to a scratchpad temp file, return its path."""
|
||||
if not os.path.isdir(SCRATCH):
|
||||
os.makedirs(SCRATCH)
|
||||
handle, path = tempfile.mkstemp(suffix=suffix, dir=SCRATCH)
|
||||
os.write(handle, content.encode("utf-8") if isinstance(content, str) else content)
|
||||
os.close(handle)
|
||||
return path
|
||||
|
||||
|
||||
class TestParseRequestFileBurp(unittest.TestCase):
|
||||
"""_parseBurpLog via parseRequestFile (plain '=====' log + Burp XML history)."""
|
||||
|
||||
def setUp(self):
|
||||
self._scope = conf.scope
|
||||
self._method = conf.method
|
||||
self._headers = conf.headers
|
||||
conf.scope = None
|
||||
|
||||
def tearDown(self):
|
||||
conf.scope = self._scope
|
||||
conf.method = self._method
|
||||
conf.headers = self._headers
|
||||
|
||||
def test_plain_burp_log_get(self):
|
||||
content = (
|
||||
"======================================================\n"
|
||||
"GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n"
|
||||
"Host: www.target.com\n"
|
||||
"Cookie: PHPSESSID=abc\n"
|
||||
"======================================================\n"
|
||||
)
|
||||
path = _write_temp(content, ".log")
|
||||
try:
|
||||
targets = list(parseRequestFile(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
self.assertEqual(len(targets), 1)
|
||||
url, method, data, cookie, headers = targets[0]
|
||||
self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1")
|
||||
self.assertEqual(method, HTTPMETHOD.GET)
|
||||
self.assertIsNone(data)
|
||||
self.assertEqual(cookie, "PHPSESSID=abc")
|
||||
self.assertIn(("Host", "www.target.com"), headers)
|
||||
|
||||
def test_burp_xml_history_base64_request(self):
|
||||
req = "GET /vuln.php?id=1 HTTP/1.1\r\nHost: www.target.com\r\nCookie: SID=xyz\r\n\r\n"
|
||||
b64 = base64.b64encode(req.encode()).decode()
|
||||
xml = ('<items><item><port>80</port>'
|
||||
'<request base64="true"><![CDATA[%s]]></request>'
|
||||
'</item></items>' % b64)
|
||||
path = _write_temp(xml, ".xml")
|
||||
try:
|
||||
targets = list(parseRequestFile(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
self.assertEqual(len(targets), 1)
|
||||
url, method, data, cookie, headers = targets[0]
|
||||
self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1")
|
||||
self.assertEqual(method, HTTPMETHOD.GET)
|
||||
self.assertEqual(cookie, "SID=xyz")
|
||||
|
||||
def test_post_body_captured(self):
|
||||
content = (
|
||||
"======================================================\n"
|
||||
"POST http://www.target.com:80/login HTTP/1.1\n"
|
||||
"Host: www.target.com\n"
|
||||
"Content-Length: 17\n"
|
||||
"\n"
|
||||
"user=admin&pw=1\n"
|
||||
"======================================================\n"
|
||||
)
|
||||
path = _write_temp(content, ".log")
|
||||
try:
|
||||
targets = list(parseRequestFile(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
self.assertEqual(len(targets), 1)
|
||||
url, method, data, cookie, headers = targets[0]
|
||||
self.assertEqual(method, HTTPMETHOD.POST)
|
||||
self.assertEqual(data, "user=admin&pw=1")
|
||||
|
||||
def test_scope_filters_out_nonmatching(self):
|
||||
content = (
|
||||
"======================================================\n"
|
||||
"GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n"
|
||||
"Host: www.target.com\n"
|
||||
"======================================================\n"
|
||||
)
|
||||
path = _write_temp(content, ".log")
|
||||
try:
|
||||
conf.scope = r"example\.org" # does not match target.com
|
||||
targets = list(parseRequestFile(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
self.assertEqual(targets, [])
|
||||
|
||||
|
||||
class TestParseRequestFileWebScarab(unittest.TestCase):
|
||||
"""_parseWebScarabLog via parseRequestFile."""
|
||||
|
||||
def setUp(self):
|
||||
self._scope = conf.scope
|
||||
conf.scope = None
|
||||
|
||||
def tearDown(self):
|
||||
conf.scope = self._scope
|
||||
|
||||
def test_get_conversation(self):
|
||||
content = (
|
||||
"### Conversation : 1\n"
|
||||
"URL: http://www.target.com/vuln.php?id=1\n"
|
||||
"METHOD: GET\n"
|
||||
"COOKIE: SID=abc\n"
|
||||
)
|
||||
path = _write_temp(content, ".log")
|
||||
try:
|
||||
targets = list(parseRequestFile(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
self.assertEqual(len(targets), 1)
|
||||
url, method, data, cookie, headers = targets[0]
|
||||
self.assertEqual(url, "http://www.target.com/vuln.php?id=1")
|
||||
self.assertEqual(method, "GET")
|
||||
self.assertIsNone(data)
|
||||
self.assertEqual(cookie, "SID=abc")
|
||||
self.assertEqual(headers, tuple())
|
||||
|
||||
def test_post_conversation_skipped(self):
|
||||
# POST bodies live in separate files -> WebScarab POSTs are skipped
|
||||
content = (
|
||||
"### Conversation : 1\n"
|
||||
"URL: http://www.target.com/login\n"
|
||||
"METHOD: POST\n"
|
||||
)
|
||||
path = _write_temp(content, ".log")
|
||||
try:
|
||||
targets = list(parseRequestFile(path))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
self.assertEqual(targets, [])
|
||||
|
||||
|
||||
class TestParseTargetDirectNonSmoke(unittest.TestCase):
|
||||
"""parseTargetDirect() non-smoke branch: resolves the canonical DBMS name.
|
||||
|
||||
Uses SQLite because its driver (stdlib sqlite3) is always importable.
|
||||
"""
|
||||
|
||||
_KEYS = ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")
|
||||
|
||||
def setUp(self):
|
||||
self._saved = {k: conf.get(k) for k in self._KEYS}
|
||||
self._smoke = kb.smokeMode
|
||||
self._params_none = conf.parameters.get(None)
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved.items():
|
||||
conf[k] = v
|
||||
kb.smokeMode = self._smoke
|
||||
if self._params_none is None:
|
||||
conf.parameters.pop(None, None)
|
||||
else:
|
||||
conf.parameters[None] = self._params_none
|
||||
|
||||
def test_sqlite_local_dsn(self):
|
||||
kb.smokeMode = False
|
||||
conf.direct = "sqlite://%s" % os.path.join(SCRATCH, "test.db")
|
||||
parseTargetDirect()
|
||||
# non-smoke path canonicalizes the DBMS name via DBMS_DICT
|
||||
self.assertEqual(conf.dbms, DBMS.SQLITE)
|
||||
# local file DBMS: hostname forced to localhost, port 0
|
||||
self.assertEqual(conf.hostname, "localhost")
|
||||
self.assertEqual(conf.port, 0)
|
||||
self.assertEqual(conf.parameters[None], "direct connection")
|
||||
|
||||
|
||||
class TestRemoveReflectiveValues(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._mech = kb.reflectiveMechanism
|
||||
self._heur = kb.heuristicMode
|
||||
kb.reflectiveMechanism = True
|
||||
kb.heuristicMode = False
|
||||
|
||||
def tearDown(self):
|
||||
kb.reflectiveMechanism = self._mech
|
||||
kb.heuristicMode = self._heur
|
||||
|
||||
def test_reflected_payload_masked(self):
|
||||
content = u"<html>You searched for 1 AND 1=2 here</html>"
|
||||
out = removeReflectiveValues(content, "1 AND 1=2")
|
||||
self.assertIn(REFLECTED_VALUE_MARKER, out)
|
||||
self.assertNotIn("AND 1=2", out)
|
||||
|
||||
def test_no_reflection_returns_content_unchanged(self):
|
||||
content = u"<html>nothing interesting</html>"
|
||||
out = removeReflectiveValues(content, "1 AND 1=2")
|
||||
self.assertEqual(out, content)
|
||||
|
||||
def test_none_payload_returns_content(self):
|
||||
content = u"<html>x</html>"
|
||||
self.assertEqual(removeReflectiveValues(content, None), content)
|
||||
|
||||
def test_bytes_content_returned_as_is(self):
|
||||
# non-text content short-circuits (isinstance text_type check)
|
||||
content = b"<html>1 AND 1=2</html>"
|
||||
self.assertEqual(removeReflectiveValues(content, "1 AND 1=2"), content)
|
||||
|
||||
|
||||
class TestFindPageForms(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._scope = conf.scope
|
||||
self._crawlExclude = conf.crawlExclude
|
||||
self._cookie = conf.cookie
|
||||
conf.scope = None
|
||||
conf.crawlExclude = None
|
||||
conf.cookie = None
|
||||
|
||||
def tearDown(self):
|
||||
conf.scope = self._scope
|
||||
conf.crawlExclude = self._crawlExclude
|
||||
conf.cookie = self._cookie
|
||||
|
||||
def test_post_form_discovered(self):
|
||||
html = ('<html><form action="/input.php" method="POST">'
|
||||
'<input type="text" name="id" value="1">'
|
||||
'<input type="submit" value="Go"></form></html>')
|
||||
forms = findPageForms(html, "http://www.site.com")
|
||||
self.assertEqual(forms, set([("http://www.site.com/input.php", "POST", "id=1", None, None)]))
|
||||
|
||||
def test_get_form_discovered(self):
|
||||
html = ('<html><form action="/search" method="GET">'
|
||||
'<input type="text" name="q" value="x">'
|
||||
'<input type="submit" value="Go"></form></html>')
|
||||
forms = findPageForms(html, "http://www.site.com")
|
||||
self.assertEqual(len(forms), 1)
|
||||
url, method, data, _cookie, _ = list(forms)[0]
|
||||
self.assertEqual(method, "GET")
|
||||
self.assertIn("q=x", url)
|
||||
|
||||
def test_inline_js_post_discovered(self):
|
||||
# the `.post('url', {k: v})` regex branch (independent of HTML form parsing)
|
||||
html = "<script>$.post('/api/save', {name: 'foo', id: '1'});</script>"
|
||||
forms = findPageForms(html, "http://www.site.com")
|
||||
self.assertTrue(any(m == HTTPMETHOD.POST and u.endswith("/api/save") for (u, m, d, c, e) in forms))
|
||||
|
||||
def test_blank_content_returns_empty_set(self):
|
||||
self.assertEqual(findPageForms("", "http://www.site.com"), set())
|
||||
|
||||
|
||||
class TestSaveConfig(unittest.TestCase):
|
||||
def test_writes_ini_with_sections(self):
|
||||
path = _write_temp("", ".ini")
|
||||
try:
|
||||
saveConfig(conf, path)
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
# optDict families become [Section] headers
|
||||
self.assertIn("[Target]", data)
|
||||
self.assertIn("[Request]", data)
|
||||
self.assertIn("[Enumeration]", data)
|
||||
self.assertTrue(len(data) > 0)
|
||||
|
||||
|
||||
class TestGetSQLSnippet(unittest.TestCase):
|
||||
def test_mssql_proc_loaded(self):
|
||||
snippet = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate")
|
||||
self.assertIn("RECONFIGURE", snippet)
|
||||
|
||||
def test_variable_substitution(self):
|
||||
# %VAR% placeholders are substituted from kwargs (here %ENABLE%);
|
||||
# supplying it avoids the interactive "provide substitution values" prompt.
|
||||
snippet = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE="1")
|
||||
self.assertIn("xp_cmdshell", snippet)
|
||||
self.assertIn("RECONFIGURE", snippet)
|
||||
# comments (#...) are stripped and the placeholder is fully resolved
|
||||
self.assertNotIn("#", snippet)
|
||||
self.assertNotIn("%ENABLE%", snippet)
|
||||
|
||||
|
||||
class TestCheckSystemEncoding(unittest.TestCase):
|
||||
def test_noop_on_normal_encoding(self):
|
||||
# On a normal default encoding this is a no-op and must not raise.
|
||||
self.assertIsNone(checkSystemEncoding())
|
||||
|
||||
|
||||
class TestFormatGetOs(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._api = conf.api
|
||||
conf.api = False
|
||||
|
||||
def tearDown(self):
|
||||
conf.api = self._api
|
||||
|
||||
def test_humanizes_type_and_technology(self):
|
||||
info = {
|
||||
"type": set(["Linux"]),
|
||||
"distrib": set(["Ubuntu"]),
|
||||
"release": set(["8.10"]),
|
||||
"technology": set(["PHP 5.2.6", "Apache 2.2.9"]),
|
||||
}
|
||||
out = Format.getOs("back-end DBMS", info)
|
||||
self.assertTrue(out.startswith("back-end DBMS operating system: Linux"))
|
||||
self.assertIn("Ubuntu", out)
|
||||
self.assertIn("8.10", out)
|
||||
self.assertIn("web application technology:", out)
|
||||
|
||||
def test_api_mode_returns_dict(self):
|
||||
orig = conf.api
|
||||
try:
|
||||
conf.api = True
|
||||
info = {"type": set(["Windows"]), "technology": set(["IIS"])}
|
||||
out = Format.getOs("back-end DBMS", info)
|
||||
self.assertIsInstance(out, dict)
|
||||
self.assertIn("web application technology", out)
|
||||
finally:
|
||||
conf.api = orig
|
||||
|
||||
|
||||
class TestBackendSetters(unittest.TestCase):
|
||||
"""Backend OS/version setters write kb state; save and restore it."""
|
||||
|
||||
_KEYS = ("os", "osVersion", "osSP", "dbmsVersion")
|
||||
|
||||
def setUp(self):
|
||||
self._saved = {k: kb.get(k) for k in self._KEYS}
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved.items():
|
||||
kb[k] = v
|
||||
|
||||
def test_set_get_os(self):
|
||||
kb.os = None
|
||||
self.assertEqual(Backend.setOs("windows"), "Windows") # capitalized
|
||||
self.assertEqual(Backend.getOs(), "Windows")
|
||||
|
||||
def test_set_os_none_returns_none(self):
|
||||
self.assertIsNone(Backend.setOs(None))
|
||||
|
||||
def test_set_os_version(self):
|
||||
kb.osVersion = None
|
||||
Backend.setOsVersion("2008")
|
||||
self.assertEqual(Backend.getOsVersion(), "2008")
|
||||
|
||||
def test_set_os_service_pack(self):
|
||||
kb.osSP = None
|
||||
Backend.setOsServicePack(3)
|
||||
self.assertEqual(Backend.getOsServicePack(), 3)
|
||||
|
||||
def test_set_get_version(self):
|
||||
kb.dbmsVersion = []
|
||||
self.assertEqual(Backend.setVersion("5.7"), ["5.7"])
|
||||
self.assertEqual(Backend.getVersion(), "5.7")
|
||||
|
||||
def test_set_version_list(self):
|
||||
kb.dbmsVersion = []
|
||||
Backend.setVersionList(["8.0", "8.1"])
|
||||
self.assertEqual(Backend.getVersionList(), ["8.0", "8.1"])
|
||||
|
||||
|
||||
class TestUrlencodeExtraBranches(unittest.TestCase):
|
||||
def test_like_percent_encoded(self):
|
||||
# '%' inside a LIKE '...' literal is encoded to %25
|
||||
self.assertEqual(urlencode("AND name LIKE '%DBA%'"),
|
||||
"AND%20name%20LIKE%20%27%25DBA%25%27")
|
||||
|
||||
def test_convall_drops_safe_set(self):
|
||||
self.assertEqual(urlencode("a&b", convall=True), "a%26b")
|
||||
|
||||
def test_limit_does_not_crash_on_long_input(self):
|
||||
out = urlencode("x " * 4000, limit=True)
|
||||
self.assertTrue(len(out) > 0)
|
||||
|
||||
def test_direct_mode_returns_value_unchanged(self):
|
||||
orig = conf.direct
|
||||
try:
|
||||
conf.direct = "mysql://u:p@h:3306/d"
|
||||
self.assertEqual(urlencode("a b"), "a b")
|
||||
finally:
|
||||
conf.direct = orig
|
||||
|
||||
|
||||
class TestSafeStringFormatExtraBranches(unittest.TestCase):
|
||||
def test_percent_d_in_payload_region_becomes_string(self):
|
||||
fmt = "SELECT %s" + PAYLOAD_DELIMITER + " AND %d " + PAYLOAD_DELIMITER
|
||||
self.assertEqual(
|
||||
safeStringFormat(fmt, ("a", "5")),
|
||||
"SELECT a" + PAYLOAD_DELIMITER + " AND 5 " + PAYLOAD_DELIMITER)
|
||||
|
||||
def test_scalar_string_percent_preserved(self):
|
||||
# single-string param path: plain replace, embedded '%' survives
|
||||
self.assertEqual(safeStringFormat("LIKE %s", "100%done"), "LIKE 100%done")
|
||||
|
||||
def test_two_params_list(self):
|
||||
self.assertEqual(safeStringFormat("%s/%s", ("a", "b")), "a/b")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,676 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Additional REAL unit coverage for genuinely-uncovered PURE functions in:
|
||||
|
||||
* lib/core/common.py
|
||||
* lib/core/option.py
|
||||
* lib/core/agent.py
|
||||
* lib/request/basic.py
|
||||
|
||||
Every test asserts a concrete, independently-reasoned known-correct value that
|
||||
would FAIL if the function under test regressed. No isinstance-only checks, no
|
||||
tautologies, no swallowed exceptions.
|
||||
|
||||
Functions targeted here are deliberately DIFFERENT from those already exercised
|
||||
by tests/test_common_utils.py, test_common_parsers.py, test_core_more.py,
|
||||
test_core_final.py, test_option_setup.py, test_option_more.py, test_agent.py,
|
||||
test_agent_dialects.py, test_decodepage.py and test_charset.py.
|
||||
|
||||
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from tests._testutils import bootstrap, set_dbms
|
||||
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.defaults import defaults
|
||||
from lib.core.common import Backend
|
||||
from lib.core.enums import DBMS
|
||||
|
||||
|
||||
class TestCommonStringHelpers(unittest.TestCase):
|
||||
"""Small pure string/list/regex/encoding helpers in lib/core/common.py."""
|
||||
|
||||
def test_posix_to_nt_slashes(self):
|
||||
from lib.core.common import posixToNtSlashes
|
||||
self.assertEqual(posixToNtSlashes("C:/Windows"), "C:\\Windows")
|
||||
self.assertEqual(posixToNtSlashes("a/b/c"), "a\\b\\c")
|
||||
# falsy input returned unchanged
|
||||
self.assertEqual(posixToNtSlashes(""), "")
|
||||
self.assertIsNone(posixToNtSlashes(None))
|
||||
|
||||
def test_nt_to_posix_slashes(self):
|
||||
from lib.core.common import ntToPosixSlashes
|
||||
self.assertEqual(ntToPosixSlashes("C:\\Windows"), "C:/Windows")
|
||||
self.assertEqual(ntToPosixSlashes("a\\b\\c"), "a/b/c")
|
||||
self.assertEqual(ntToPosixSlashes(""), "")
|
||||
|
||||
def test_is_hex_encoded_string(self):
|
||||
from lib.core.common import isHexEncodedString
|
||||
self.assertTrue(isHexEncodedString("DEADBEEF"))
|
||||
self.assertTrue(isHexEncodedString("0x1234")) # 'x' is allowed by the regex
|
||||
self.assertFalse(isHexEncodedString("test"))
|
||||
self.assertFalse(isHexEncodedString("12 34")) # space breaks it
|
||||
|
||||
def test_is_digit(self):
|
||||
from lib.core.common import isDigit
|
||||
self.assertTrue(isDigit("123456"))
|
||||
self.assertFalse(isDigit("3b3"))
|
||||
self.assertFalse(isDigit(u"\xb2")) # superscript-2: str.isdigit() True, isDigit False
|
||||
self.assertFalse(isDigit("")) # empty -> no match
|
||||
self.assertFalse(isDigit(None))
|
||||
|
||||
def test_sanitize_str(self):
|
||||
from lib.core.common import sanitizeStr
|
||||
self.assertEqual(sanitizeStr("foo\n\rbar"), "foo bar")
|
||||
self.assertEqual(sanitizeStr("a\r\nb"), "a b")
|
||||
self.assertEqual(sanitizeStr(None), "None")
|
||||
|
||||
def test_filter_control_chars(self):
|
||||
from lib.core.common import filterControlChars
|
||||
self.assertEqual(filterControlChars("AND 1>(2+3)\n--"), "AND 1>(2+3) --")
|
||||
# custom replacement character
|
||||
self.assertEqual(filterControlChars("a\tb", replacement="_"), "a_b")
|
||||
|
||||
def test_normalize_path(self):
|
||||
from lib.core.common import normalizePath
|
||||
self.assertEqual(normalizePath("//var///log/apache.log"), "/var/log/apache.log")
|
||||
self.assertEqual(normalizePath("/a/b/../c"), "/a/c")
|
||||
|
||||
def test_directory_path(self):
|
||||
from lib.core.common import directoryPath
|
||||
self.assertEqual(directoryPath("/var/log/apache.log"), "/var/log")
|
||||
# no extension -> returned unchanged
|
||||
self.assertEqual(directoryPath("/var/log"), "/var/log")
|
||||
|
||||
def test_longest_common_prefix(self):
|
||||
from lib.core.common import longestCommonPrefix
|
||||
self.assertEqual(longestCommonPrefix("foobar", "fobar"), "fo")
|
||||
self.assertEqual(longestCommonPrefix("abc", "abd", "abe"), "ab")
|
||||
# single sequence returned verbatim
|
||||
self.assertEqual(longestCommonPrefix("only"), "only")
|
||||
|
||||
def test_first_not_none(self):
|
||||
from lib.core.common import firstNotNone
|
||||
self.assertEqual(firstNotNone(None, None, 1, 2, 3), 1)
|
||||
self.assertEqual(firstNotNone(None, 0), 0) # 0 is not None
|
||||
self.assertIsNone(firstNotNone(None, None))
|
||||
|
||||
def test_decode_string_escape(self):
|
||||
from lib.core.common import decodeStringEscape
|
||||
self.assertEqual(decodeStringEscape("a\\tb"), "a\tb")
|
||||
self.assertEqual(decodeStringEscape("a\\nb"), "a\nb")
|
||||
# no backslash -> unchanged
|
||||
self.assertEqual(decodeStringEscape("plain"), "plain")
|
||||
|
||||
def test_encode_string_escape(self):
|
||||
from lib.core.common import encodeStringEscape
|
||||
self.assertEqual(encodeStringEscape("a\tb"), "a\\tb")
|
||||
self.assertEqual(encodeStringEscape("a\nb"), "a\\nb")
|
||||
self.assertEqual(encodeStringEscape("plain"), "plain")
|
||||
|
||||
def test_decode_encode_string_escape_roundtrip(self):
|
||||
from lib.core.common import decodeStringEscape, encodeStringEscape
|
||||
self.assertEqual(decodeStringEscape(encodeStringEscape("x\ty\nz")), "x\ty\nz")
|
||||
|
||||
def test_escape_json_value(self):
|
||||
from lib.core.common import escapeJsonValue
|
||||
# newline gets escaped (literal '\n' becomes the two chars backslash+n)
|
||||
self.assertNotIn("\n", escapeJsonValue("foo\nbar"))
|
||||
self.assertIn("\\n", escapeJsonValue("foo\nbar"))
|
||||
# tab gets escaped to '\t'
|
||||
self.assertIn("\\t", escapeJsonValue("foo\tbar"))
|
||||
# quote and backslash escaped
|
||||
self.assertEqual(escapeJsonValue('a"b'), 'a\\"b')
|
||||
self.assertEqual(escapeJsonValue("a\\b"), "a\\\\b")
|
||||
# ordinary characters untouched
|
||||
self.assertEqual(escapeJsonValue("plain text"), "plain text")
|
||||
|
||||
def test_clean_query(self):
|
||||
from lib.core.common import cleanQuery
|
||||
self.assertEqual(cleanQuery("select id from users"), "SELECT id FROM users")
|
||||
# already-uppercase keywords stay; identifiers untouched
|
||||
self.assertEqual(cleanQuery("SELECT a FROM t"), "SELECT a FROM t")
|
||||
|
||||
def test_json_minimize_canonical(self):
|
||||
from lib.core.common import jsonMinimize
|
||||
# key order / whitespace independence
|
||||
self.assertEqual(jsonMinimize('{"b": 2, "a": 1}'), jsonMinimize('{"a":1, "b":2}'))
|
||||
# nested leaf path
|
||||
self.assertEqual(jsonMinimize('{"a": {"b": 1}}'), ".a.b=1")
|
||||
# empty object
|
||||
self.assertEqual(jsonMinimize("{}"), "")
|
||||
# not parseable -> None (and only None)
|
||||
self.assertIsNone(jsonMinimize("not json"))
|
||||
|
||||
def test_json_minimize_array_length_registers(self):
|
||||
from lib.core.common import jsonMinimize
|
||||
# array length change must perturb the projection
|
||||
self.assertNotEqual(jsonMinimize('{"a": [1, 2]}'), jsonMinimize('{"a": [1, 2, 3]}'))
|
||||
|
||||
def test_list_to_str_value(self):
|
||||
from lib.core.common import listToStrValue
|
||||
self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3")
|
||||
# set/tuple/generator normalized via list first
|
||||
self.assertEqual(listToStrValue((1, 2)), "1, 2")
|
||||
# non-list passes through
|
||||
self.assertEqual(listToStrValue("abc"), "abc")
|
||||
|
||||
def test_intersect(self):
|
||||
from lib.core.common import intersect
|
||||
self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3])
|
||||
# order follows containerA
|
||||
self.assertEqual(intersect([3, 2, 1], [1, 2]), [2, 1])
|
||||
# case-insensitive option
|
||||
self.assertEqual(intersect(["FOO", "bar"], ["foo"], lowerCase=True), ["foo"])
|
||||
|
||||
def test_priority_sort_columns(self):
|
||||
from lib.core.common import prioritySortColumns
|
||||
# 'id'-containing columns first, then by ascending length
|
||||
self.assertEqual(
|
||||
prioritySortColumns(["password", "userid", "name", "id"]),
|
||||
["id", "userid", "name", "password"],
|
||||
)
|
||||
|
||||
def test_safe_variable_naming(self):
|
||||
from lib.core.common import safeVariableNaming
|
||||
self.assertEqual(safeVariableNaming("class.id"), "EVAL_636c6173732e6964")
|
||||
# plain identifier left untouched
|
||||
self.assertEqual(safeVariableNaming("foobar"), "foobar")
|
||||
|
||||
def test_unsafe_variable_naming(self):
|
||||
from lib.core.common import unsafeVariableNaming
|
||||
self.assertEqual(unsafeVariableNaming("EVAL_636c6173732e6964"), "class.id")
|
||||
self.assertEqual(unsafeVariableNaming("foobar"), "foobar")
|
||||
|
||||
def test_variable_naming_roundtrip(self):
|
||||
from lib.core.common import safeVariableNaming, unsafeVariableNaming
|
||||
self.assertEqual(unsafeVariableNaming(safeVariableNaming("a-b")), "a-b")
|
||||
|
||||
def test_average(self):
|
||||
from lib.core.common import average
|
||||
self.assertAlmostEqual(average([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), 0.9, places=6)
|
||||
self.assertEqual(average([2, 4]), 3.0)
|
||||
self.assertIsNone(average([]))
|
||||
|
||||
def test_stdev(self):
|
||||
from lib.core.common import stdev
|
||||
self.assertEqual("%.3f" % stdev([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), "0.063")
|
||||
# fewer than 2 values -> None
|
||||
self.assertIsNone(stdev([1.0]))
|
||||
self.assertIsNone(stdev([]))
|
||||
|
||||
|
||||
class TestCommonSafeCompare(unittest.TestCase):
|
||||
"""Constant-time / checksum helpers."""
|
||||
|
||||
def test_safe_compare_strings(self):
|
||||
from lib.core.common import safeCompareStrings
|
||||
self.assertTrue(safeCompareStrings("test", "test"))
|
||||
self.assertFalse(safeCompareStrings("test1", "test2"))
|
||||
self.assertFalse(safeCompareStrings("test", None))
|
||||
# both None compares equal (a == b path)
|
||||
self.assertTrue(safeCompareStrings(None, None))
|
||||
|
||||
def test_safe_cs_value(self):
|
||||
from lib.core.common import safeCSValue
|
||||
# ensure deterministic delimiter
|
||||
old = conf.get("csvDel")
|
||||
conf.csvDel = defaults.csvDel
|
||||
try:
|
||||
self.assertEqual(safeCSValue("foo, bar"), '"foo, bar"')
|
||||
self.assertEqual(safeCSValue("foobar"), "foobar")
|
||||
self.assertEqual(safeCSValue("foo\rbar"), '"foo\rbar"')
|
||||
self.assertEqual(safeCSValue('foo"bar'), '"foo""bar"')
|
||||
finally:
|
||||
conf.csvDel = old
|
||||
|
||||
|
||||
class TestCommonSafeExString(unittest.TestCase):
|
||||
def test_sqlmap_exception_message(self):
|
||||
from lib.core.common import getSafeExString
|
||||
from lib.core.exception import SqlmapBaseException
|
||||
self.assertEqual(getSafeExString(SqlmapBaseException("foobar")), "foobar")
|
||||
|
||||
def test_oserror_prefixed_with_type(self):
|
||||
from lib.core.common import getSafeExString
|
||||
self.assertEqual(getSafeExString(OSError(0, "foobar")), "OSError: foobar")
|
||||
|
||||
def test_generic_value_error(self):
|
||||
from lib.core.common import getSafeExString
|
||||
self.assertEqual(getSafeExString(ValueError("bad input")), "ValueError: bad input")
|
||||
|
||||
|
||||
class TestCommonHostHeader(unittest.TestCase):
|
||||
def test_plain_host(self):
|
||||
from lib.core.common import getHostHeader
|
||||
self.assertEqual(getHostHeader("http://www.target.com/vuln.php?id=1"), "www.target.com")
|
||||
|
||||
def test_default_port_stripped(self):
|
||||
from lib.core.common import getHostHeader
|
||||
self.assertEqual(getHostHeader("http://www.target.com:80/x"), "www.target.com")
|
||||
self.assertEqual(getHostHeader("https://www.target.com:443/x"), "www.target.com")
|
||||
|
||||
def test_nondefault_port_kept(self):
|
||||
from lib.core.common import getHostHeader
|
||||
self.assertEqual(getHostHeader("http://www.target.com:8080/x"), "www.target.com:8080")
|
||||
|
||||
def test_ipv6_brackets(self):
|
||||
from lib.core.common import getHostHeader
|
||||
self.assertEqual(getHostHeader("http://[::1]:8080/vuln.php?id=1"), "[::1]:8080")
|
||||
self.assertEqual(getHostHeader("http://[::1]/vuln.php?id=1"), "[::1]")
|
||||
|
||||
|
||||
class TestCommonCheckSameHost(unittest.TestCase):
|
||||
def test_same_host(self):
|
||||
from lib.core.common import checkSameHost
|
||||
self.assertTrue(checkSameHost(
|
||||
"http://www.target.com/page1.php?id=1",
|
||||
"http://www.target.com/images/page2.php",
|
||||
))
|
||||
|
||||
def test_different_host(self):
|
||||
from lib.core.common import checkSameHost
|
||||
self.assertFalse(checkSameHost(
|
||||
"http://www.target.com/page1.php?id=1",
|
||||
"http://www.target2.com/images/page2.php",
|
||||
))
|
||||
|
||||
def test_www_prefix_ignored(self):
|
||||
from lib.core.common import checkSameHost
|
||||
# leading 'www.' is stripped before comparison
|
||||
self.assertTrue(checkSameHost("http://www.target.com/a", "http://target.com/b"))
|
||||
|
||||
def test_single_url_true_and_empty_none(self):
|
||||
from lib.core.common import checkSameHost
|
||||
self.assertTrue(checkSameHost("http://only.com/a"))
|
||||
self.assertIsNone(checkSameHost())
|
||||
|
||||
|
||||
class TestCommonUrldecode(unittest.TestCase):
|
||||
def test_convall_true(self):
|
||||
from lib.core.common import urldecode
|
||||
self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=True), "AND 1>(2+3)#")
|
||||
|
||||
def test_convall_false_keeps_unsafe(self):
|
||||
from lib.core.common import urldecode
|
||||
# %2B (plus) is in the default 'unsafe' set so it stays encoded when convall=False
|
||||
self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#")
|
||||
|
||||
def test_bytes_input(self):
|
||||
from lib.core.common import urldecode
|
||||
self.assertEqual(urldecode(b"AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#")
|
||||
|
||||
def test_spaceplus(self):
|
||||
from lib.core.common import urldecode
|
||||
# with spaceplus the '+' becomes a space
|
||||
self.assertEqual(urldecode("a+b", convall=False, spaceplus=True), "a b")
|
||||
# without spaceplus the '+' stays
|
||||
self.assertEqual(urldecode("a+b", convall=False, spaceplus=False), "a+b")
|
||||
|
||||
|
||||
class TestCommonChunkSplit(unittest.TestCase):
|
||||
def test_chunk_split_post_data(self):
|
||||
import random
|
||||
from lib.core.common import chunkSplitPostData
|
||||
from lib.core.patch import unisonRandom
|
||||
# The pinned docstring value is produced under sqlmap's cross-version PRNG; install it
|
||||
# (then restore the stdlib functions) so the expectation is deterministic here too.
|
||||
_saved = (random.choice, random.randint, random.sample, random.seed)
|
||||
unisonRandom()
|
||||
try:
|
||||
random.seed(0)
|
||||
expected = ('5;4Xe90\r\nSELEC\r\n3;irWlc\r\nT u\r\n1;eT4zO\r\ns\r\n'
|
||||
'5;YB4hM\r\nernam\r\n9;2pUD8\r\ne,passwor\r\n3;mp07y\r\nd F\r\n'
|
||||
'5;8RKXi\r\nROM u\r\n4;MvMhO\r\nsers\r\n0\r\n\r\n')
|
||||
self.assertEqual(chunkSplitPostData("SELECT username,password FROM users"), expected)
|
||||
finally:
|
||||
random.choice, random.randint, random.sample, random.seed = _saved
|
||||
|
||||
def test_chunk_split_terminator(self):
|
||||
import random
|
||||
from lib.core.common import chunkSplitPostData
|
||||
random.seed(123)
|
||||
# regardless of content, the chunked stream must end with the zero-length terminator
|
||||
self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n"))
|
||||
|
||||
|
||||
class TestCommonDecodeIntToUnicode(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
set_dbms(None)
|
||||
|
||||
def test_basic_ascii(self):
|
||||
from lib.core.common import decodeIntToUnicode
|
||||
self.assertEqual(decodeIntToUnicode(35), "#")
|
||||
self.assertEqual(decodeIntToUnicode(64), "@")
|
||||
self.assertEqual(decodeIntToUnicode(65), "A")
|
||||
|
||||
def test_non_int_passthrough(self):
|
||||
from lib.core.common import decodeIntToUnicode
|
||||
# non-int is returned unchanged
|
||||
self.assertEqual(decodeIntToUnicode("x"), "x")
|
||||
|
||||
def test_pgsql_high_codepoint(self):
|
||||
from lib.core.common import decodeIntToUnicode
|
||||
set_dbms(DBMS.PGSQL)
|
||||
# value > 255 on PGSQL takes the _unichr(value) branch
|
||||
self.assertEqual(decodeIntToUnicode(0x2122), u"™")
|
||||
|
||||
|
||||
class TestCommonDecodeDbmsHex(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._old_binary = kb.binaryField
|
||||
kb.binaryField = False
|
||||
|
||||
def tearDown(self):
|
||||
kb.binaryField = self._old_binary
|
||||
set_dbms(None)
|
||||
|
||||
def test_plain_hex(self):
|
||||
from lib.core.common import decodeDbmsHexValue
|
||||
self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1")
|
||||
|
||||
def test_odd_length_appends_question_mark(self):
|
||||
from lib.core.common import decodeDbmsHexValue
|
||||
self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?")
|
||||
|
||||
def test_list_input(self):
|
||||
from lib.core.common import decodeDbmsHexValue
|
||||
self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"])
|
||||
|
||||
def test_non_hex_passthrough(self):
|
||||
from lib.core.common import decodeDbmsHexValue
|
||||
self.assertEqual(decodeDbmsHexValue("5.1.41"), u"5.1.41")
|
||||
|
||||
|
||||
class TestCommonUnsafeSQLIdentificator(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
set_dbms(None)
|
||||
|
||||
def test_mssql_brackets(self):
|
||||
from lib.core.common import unsafeSQLIdentificatorNaming
|
||||
from lib.core.common import getText
|
||||
set_dbms(DBMS.MSSQL)
|
||||
self.assertEqual(getText(unsafeSQLIdentificatorNaming("[begin]")), "begin")
|
||||
self.assertEqual(getText(unsafeSQLIdentificatorNaming("foobar")), "foobar")
|
||||
|
||||
def test_mysql_backticks(self):
|
||||
from lib.core.common import unsafeSQLIdentificatorNaming, getText
|
||||
set_dbms(DBMS.MYSQL)
|
||||
self.assertEqual(getText(unsafeSQLIdentificatorNaming("`col`")), "col")
|
||||
|
||||
def test_oracle_uppercases(self):
|
||||
from lib.core.common import unsafeSQLIdentificatorNaming, getText
|
||||
set_dbms(DBMS.ORACLE)
|
||||
# Oracle strips double quotes and uppercases
|
||||
self.assertEqual(getText(unsafeSQLIdentificatorNaming('"name"')), "NAME")
|
||||
|
||||
|
||||
class TestCommonParseSqliteSchema(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._old_cached = kb.data.get("cachedColumns")
|
||||
self._old_db = conf.db
|
||||
self._old_tbl = conf.tbl
|
||||
kb.data.cachedColumns = {}
|
||||
conf.db = "SQLITE_MASTER"
|
||||
conf.tbl = "users"
|
||||
|
||||
def tearDown(self):
|
||||
kb.data.cachedColumns = self._old_cached
|
||||
conf.db = self._old_db
|
||||
conf.tbl = self._old_tbl
|
||||
|
||||
def test_simple_schema(self):
|
||||
from lib.core.common import parseSqliteTableSchema
|
||||
self.assertTrue(parseSqliteTableSchema(
|
||||
"CREATE TABLE users(\n\t\tid INTEGER,\n\t\tname TEXT\n);"))
|
||||
cols = kb.data.cachedColumns[conf.db][conf.tbl]
|
||||
self.assertEqual(tuple(cols.items()), (("id", "INTEGER"), ("name", "TEXT")))
|
||||
|
||||
def test_constraints_skipped(self):
|
||||
from lib.core.common import parseSqliteTableSchema
|
||||
self.assertTrue(parseSqliteTableSchema(
|
||||
"CREATE TABLE suppliers(\n\tsupplier_id INTEGER PRIMARY KEY DESC,\n\tname TEXT NOT NULL\n);"))
|
||||
cols = kb.data.cachedColumns[conf.db][conf.tbl]
|
||||
self.assertEqual(tuple(cols.items()), (("supplier_id", "INTEGER"), ("name", "TEXT")))
|
||||
|
||||
|
||||
class TestAgentPure(unittest.TestCase):
|
||||
"""Pure agent.py methods independent of full injection state."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from lib.core.agent import agent
|
||||
cls.agent = agent
|
||||
|
||||
def tearDown(self):
|
||||
set_dbms(None)
|
||||
|
||||
def test_get_comment_present(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
request = AttribDict()
|
||||
request.comment = "-- foo"
|
||||
self.assertEqual(self.agent.getComment(request), "-- foo")
|
||||
|
||||
def test_get_comment_absent(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
request = AttribDict()
|
||||
self.assertEqual(self.agent.getComment(request), "")
|
||||
|
||||
def test_add_payload_delimiters(self):
|
||||
from lib.core.settings import PAYLOAD_DELIMITER
|
||||
value = "1 AND 1=1"
|
||||
result = self.agent.addPayloadDelimiters(value)
|
||||
self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER))
|
||||
# falsy value returned unchanged
|
||||
self.assertEqual(self.agent.addPayloadDelimiters(""), "")
|
||||
|
||||
def test_remove_payload_delimiters_roundtrip(self):
|
||||
self.assertEqual(
|
||||
self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")),
|
||||
"1 AND 1=1",
|
||||
)
|
||||
|
||||
def test_extract_payload(self):
|
||||
wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix"
|
||||
self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1")
|
||||
|
||||
def test_replace_payload(self):
|
||||
wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix"
|
||||
replaced = self.agent.replacePayload(wrapped, "NEW")
|
||||
self.assertEqual(self.agent.extractPayload(replaced), "NEW")
|
||||
# surrounding text preserved
|
||||
self.assertTrue(replaced.startswith("prefix"))
|
||||
self.assertTrue(replaced.endswith("suffix"))
|
||||
|
||||
def test_simple_concatenate_mysql(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# MySQL concatenate query template is 'CONCAT(%s,%s)'
|
||||
self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)")
|
||||
|
||||
def test_hex_convert_field_mysql(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# MySQL hex template is 'HEX(%s)'
|
||||
self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)")
|
||||
|
||||
def test_get_fields_select_from(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
result = self.agent.getFields("SELECT a, b FROM users")
|
||||
fieldsToCastList = result[5]
|
||||
fieldsToCastStr = result[6]
|
||||
self.assertEqual(fieldsToCastStr, "a, b")
|
||||
self.assertEqual(fieldsToCastList, ["a", "b"])
|
||||
|
||||
def test_get_fields_no_from(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list
|
||||
result = self.agent.getFields("SELECT 1")
|
||||
fieldsSelectFrom = result[0]
|
||||
self.assertIsNone(fieldsSelectFrom)
|
||||
self.assertEqual(result[6], "1")
|
||||
|
||||
|
||||
class TestAgentWhereQuery(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from lib.core.agent import agent
|
||||
cls.agent = agent
|
||||
|
||||
def setUp(self):
|
||||
self._old_dumpWhere = conf.dumpWhere
|
||||
self._old_tbl = conf.tbl
|
||||
conf.tbl = None
|
||||
|
||||
def tearDown(self):
|
||||
conf.dumpWhere = self._old_dumpWhere
|
||||
conf.tbl = self._old_tbl
|
||||
set_dbms(None)
|
||||
|
||||
def test_no_dumpwhere_passthrough(self):
|
||||
conf.dumpWhere = None
|
||||
query = "SELECT a FROM t"
|
||||
self.assertEqual(self.agent.whereQuery(query), query)
|
||||
|
||||
def test_appends_where_clause(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>0"
|
||||
# no existing WHERE -> appends ' WHERE id>0'
|
||||
self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0")
|
||||
|
||||
def test_and_when_where_present(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>0"
|
||||
# existing WHERE -> appended with AND
|
||||
self.assertEqual(
|
||||
self.agent.whereQuery("SELECT a FROM t WHERE x=1"),
|
||||
"SELECT a FROM t WHERE x=1 AND id>0",
|
||||
)
|
||||
|
||||
def test_splices_before_order_by(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>0"
|
||||
# WHERE must be spliced before the trailing ORDER BY suffix
|
||||
self.assertEqual(
|
||||
self.agent.whereQuery("SELECT a FROM t ORDER BY a"),
|
||||
"SELECT a FROM t WHERE id>0 ORDER BY a",
|
||||
)
|
||||
|
||||
|
||||
class TestBasicHeuristicCharEncoding(unittest.TestCase):
|
||||
def test_ascii(self):
|
||||
from lib.request.basic import getHeuristicCharEncoding
|
||||
self.assertEqual(getHeuristicCharEncoding(b"<html></html>"), "ascii")
|
||||
|
||||
def test_cache_hit_returns_same(self):
|
||||
from lib.request.basic import getHeuristicCharEncoding
|
||||
page = b"<html>hello world</html>"
|
||||
first = getHeuristicCharEncoding(page)
|
||||
# second call for identical page must come back identical (and from cache)
|
||||
self.assertEqual(getHeuristicCharEncoding(page), first)
|
||||
key = (len(page), hash(page))
|
||||
self.assertEqual(kb.cache.encoding.get(key), first)
|
||||
|
||||
|
||||
class TestBasicDecodePage(unittest.TestCase):
|
||||
"""decodePage charset + HTML-entity decoding branches."""
|
||||
|
||||
def setUp(self):
|
||||
self._old_encoding = conf.encoding
|
||||
self._old_null = conf.nullConnection
|
||||
conf.nullConnection = False
|
||||
|
||||
def tearDown(self):
|
||||
conf.encoding = self._old_encoding
|
||||
conf.nullConnection = self._old_null
|
||||
|
||||
def test_html_entity_amp(self):
|
||||
from lib.request.basic import decodePage
|
||||
from lib.core.common import getText
|
||||
conf.encoding = None
|
||||
self.assertEqual(
|
||||
getText(decodePage(b"<html>foo&bar</html>", None, "text/html; charset=utf-8")),
|
||||
"<html>foo&bar</html>",
|
||||
)
|
||||
|
||||
def test_numeric_hex_entity_tab(self):
|
||||
from lib.request.basic import decodePage
|
||||
from lib.core.common import getText
|
||||
conf.encoding = None
|
||||
self.assertEqual(getText(decodePage(b"	", 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()
|
||||
|
|
@ -1,605 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Additional unit coverage for lib/core/common.py, lib/core/option.py and
|
||||
lib/core/target.py, targeting *pure* (or near-pure) functions and branches NOT
|
||||
already exercised by the existing test modules:
|
||||
|
||||
* tests/test_common_utils.py / test_common_parsers.py / test_core_more.py
|
||||
* tests/test_option_setup.py / test_option_more.py
|
||||
* tests/test_target_parsing.py
|
||||
|
||||
This file instead covers (common.py):
|
||||
|
||||
boldifyMessage, calculateDeltaSeconds, commonFinderOnly,
|
||||
enumValueToNameLookup, extractErrorMessage, filePathToSafeString,
|
||||
isWindowsDriveLetterPath, cleanReplaceUnicode, trimAlphaNum,
|
||||
removePostHintPrefix, safeExpandUser, safeFilepathEncode,
|
||||
serializeObject/unserializeObject, applyFunctionRecursively,
|
||||
extractExpectedValue, getHeader, getRequestHeader, parseJson,
|
||||
parsePasswordHash, findMultipartPostBoundary, setTechnique/getTechnique,
|
||||
extractRegexResult, extractTextTagContent, getFilteredPageContent,
|
||||
checkFile, listToStrValue, intersect, isZipFile, checkOldOptions.
|
||||
|
||||
(option.py):
|
||||
|
||||
_setHTTPAuthentication (basic/ntlm/bearer/pki + error branches),
|
||||
_setWriteFile, _setHTTPTimeout, _setAuthCred.
|
||||
|
||||
Everything runs in isolation: no network, no DBMS, no persistent filesystem
|
||||
mutation. All mutated conf/kb/Backend/socket state is snapshotted and restored.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
import lib.core.option as option
|
||||
from lib.core.data import conf, kb, paths
|
||||
from lib.core.enums import (
|
||||
AUTH_TYPE,
|
||||
DBMS,
|
||||
EXPECTED,
|
||||
HTTP_HEADER,
|
||||
SORT_ORDER,
|
||||
)
|
||||
from lib.core.exception import (
|
||||
SqlmapFilePathException,
|
||||
SqlmapMissingMandatoryOptionException,
|
||||
SqlmapMissingDependence,
|
||||
SqlmapSyntaxException,
|
||||
SqlmapSystemException,
|
||||
)
|
||||
from lib.core.settings import NULL
|
||||
from lib.core.common import (
|
||||
applyFunctionRecursively,
|
||||
boldifyMessage,
|
||||
calculateDeltaSeconds,
|
||||
checkFile,
|
||||
checkOldOptions,
|
||||
cleanReplaceUnicode,
|
||||
commonFinderOnly,
|
||||
enumValueToNameLookup,
|
||||
extractErrorMessage,
|
||||
extractExpectedValue,
|
||||
extractRegexResult,
|
||||
extractTextTagContent,
|
||||
filePathToSafeString,
|
||||
findMultipartPostBoundary,
|
||||
getFilteredPageContent,
|
||||
getHeader,
|
||||
getRequestHeader,
|
||||
getText,
|
||||
getTechnique,
|
||||
intersect,
|
||||
isWindowsDriveLetterPath,
|
||||
isZipFile,
|
||||
listToStrValue,
|
||||
parseJson,
|
||||
parsePasswordHash,
|
||||
removePostHintPrefix,
|
||||
safeExpandUser,
|
||||
safeFilepathEncode,
|
||||
serializeObject,
|
||||
setTechnique,
|
||||
trimAlphaNum,
|
||||
unserializeObject,
|
||||
)
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
|
||||
class _FakeRequest(object):
|
||||
"""Minimal stand-in for urllib2.Request used by getRequestHeader()."""
|
||||
|
||||
def __init__(self, headers):
|
||||
self.headers = headers
|
||||
|
||||
def header_items(self):
|
||||
return self.headers.items()
|
||||
|
||||
|
||||
class TestCommonPureHelpers(unittest.TestCase):
|
||||
"""Pure string/encoding/list/regex helpers from lib/core/common.py."""
|
||||
|
||||
def test_boldify_message_marks_known_pattern(self):
|
||||
self.assertEqual(
|
||||
boldifyMessage("GET parameter id is not injectable", istty=True),
|
||||
"\x1b[1mGET parameter id is not injectable\x1b[0m",
|
||||
)
|
||||
|
||||
def test_boldify_message_leaves_plain_unchanged(self):
|
||||
self.assertEqual(boldifyMessage("just a plain message", istty=True), "just a plain message")
|
||||
|
||||
def test_calculate_delta_seconds_from_epoch(self):
|
||||
self.assertGreater(calculateDeltaSeconds(0), 1151721660)
|
||||
|
||||
def test_calculate_delta_seconds_nonnegative(self):
|
||||
import time as _time
|
||||
self.assertGreaterEqual(calculateDeltaSeconds(_time.time()), 0.0)
|
||||
|
||||
def test_common_finder_only_returns_longest_common_prefix(self):
|
||||
self.assertEqual(commonFinderOnly("abcd", ["abcdefg", "foobar", "abcde"]), "abcde")
|
||||
|
||||
def test_enum_value_to_name_lookup_hit(self):
|
||||
self.assertEqual(enumValueToNameLookup(SORT_ORDER, SORT_ORDER.LAST), "LAST")
|
||||
|
||||
def test_enum_value_to_name_lookup_miss(self):
|
||||
self.assertIsNone(enumValueToNameLookup(SORT_ORDER, -987654321))
|
||||
|
||||
def test_file_path_to_safe_string(self):
|
||||
self.assertEqual(filePathToSafeString("C:/Windows/system32"), "C__Windows_system32")
|
||||
|
||||
def test_file_path_to_safe_string_spaces_backslashes(self):
|
||||
self.assertEqual(filePathToSafeString("a b\\c:d"), "a_b_c_d")
|
||||
|
||||
def test_is_windows_drive_letter_path_true(self):
|
||||
self.assertTrue(isWindowsDriveLetterPath("C:\\boot.ini"))
|
||||
|
||||
def test_is_windows_drive_letter_path_false(self):
|
||||
self.assertFalse(isWindowsDriveLetterPath("/var/log/apache.log"))
|
||||
|
||||
def test_clean_replace_unicode_list(self):
|
||||
self.assertEqual(cleanReplaceUnicode(["a", "b"]), ["a", "b"])
|
||||
|
||||
def test_clean_replace_unicode_scalar(self):
|
||||
self.assertEqual(cleanReplaceUnicode(u"plain"), u"plain")
|
||||
|
||||
def test_trim_alpha_num(self):
|
||||
self.assertEqual(trimAlphaNum("AND 1>(2+3)-- foobar"), " 1>(2+3)-- ")
|
||||
|
||||
def test_trim_alpha_num_all_alnum(self):
|
||||
self.assertEqual(trimAlphaNum("abc123"), "")
|
||||
|
||||
def test_trim_alpha_num_empty(self):
|
||||
self.assertEqual(trimAlphaNum(""), "")
|
||||
|
||||
def test_list_to_str_value_list(self):
|
||||
self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3")
|
||||
|
||||
def test_list_to_str_value_tuple(self):
|
||||
self.assertEqual(listToStrValue((4, 5)), "4, 5")
|
||||
|
||||
def test_list_to_str_value_scalar(self):
|
||||
self.assertEqual(listToStrValue("foo"), "foo")
|
||||
|
||||
def test_intersect_lists(self):
|
||||
self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3])
|
||||
|
||||
def test_intersect_lowercase(self):
|
||||
self.assertEqual(intersect(["A", "B"], ["a"], lowerCase=True), ["a"])
|
||||
|
||||
def test_intersect_empty(self):
|
||||
self.assertEqual(intersect([], [1, 2]), [])
|
||||
|
||||
def test_apply_function_recursively(self):
|
||||
self.assertEqual(
|
||||
applyFunctionRecursively([1, 2, [3, -9]], lambda _: _ > 0),
|
||||
[True, True, [True, False]],
|
||||
)
|
||||
|
||||
def test_apply_function_recursively_scalar(self):
|
||||
self.assertEqual(applyFunctionRecursively(5, lambda _: _ + 1), 6)
|
||||
|
||||
|
||||
class TestCommonRegexAndPage(unittest.TestCase):
|
||||
"""Regex / page-content extraction helpers."""
|
||||
|
||||
def test_extract_regex_result_hit(self):
|
||||
self.assertEqual(extractRegexResult(r"a(?P<result>[^g]+)g", "abcdefg"), "bcdef")
|
||||
|
||||
def test_extract_regex_result_no_match(self):
|
||||
self.assertIsNone(extractRegexResult(r"a(?P<result>[^g]+)g", "xyz"))
|
||||
|
||||
def test_extract_regex_result_no_result_group(self):
|
||||
self.assertIsNone(extractRegexResult(r"plain", "plain"))
|
||||
|
||||
def test_extract_regex_result_empty_content(self):
|
||||
self.assertIsNone(extractRegexResult(r"a(?P<result>.)b", ""))
|
||||
|
||||
def test_extract_text_tag_content(self):
|
||||
self.assertEqual(
|
||||
extractTextTagContent("<html><head><title>Title</title></head><body><pre>foobar</pre></body></html>"),
|
||||
["Title", "foobar"],
|
||||
)
|
||||
|
||||
def test_extract_text_tag_content_empty(self):
|
||||
self.assertEqual(extractTextTagContent(""), [])
|
||||
|
||||
def test_get_filtered_page_content(self):
|
||||
self.assertEqual(
|
||||
getFilteredPageContent(u"<html><title>foobar</title><body>test</body></html>"),
|
||||
"foobar test",
|
||||
)
|
||||
|
||||
def test_get_filtered_page_content_drops_script(self):
|
||||
page = u"<html><script>var x=1;</script><body>hello</body></html>"
|
||||
self.assertNotIn("var x", getFilteredPageContent(page))
|
||||
self.assertIn("hello", getFilteredPageContent(page))
|
||||
|
||||
def test_get_filtered_page_content_nonstring_passthrough(self):
|
||||
self.assertEqual(getFilteredPageContent(None), None)
|
||||
|
||||
def test_extract_error_message_oracle(self):
|
||||
page = (u"<html><title>Test</title>\n<b>Warning</b>: oci_parse() "
|
||||
u"[function.oci-parse]: ORA-01756: quoted string not properly "
|
||||
u"terminated<br><p>Only a test page</p></html>")
|
||||
self.assertEqual(
|
||||
getText(extractErrorMessage(page)),
|
||||
"oci_parse() [function.oci-parse]: ORA-01756: quoted string not properly terminated",
|
||||
)
|
||||
|
||||
def test_extract_error_message_none_for_plain(self):
|
||||
self.assertIsNone(extractErrorMessage("Warning: This is only a dummy foobar test"))
|
||||
|
||||
def test_extract_error_message_non_string(self):
|
||||
self.assertIsNone(extractErrorMessage(None))
|
||||
|
||||
def test_find_multipart_post_boundary(self):
|
||||
post = ("-----------------------------9051914041544843365972754266\n"
|
||||
"Content-Disposition: form-data; name=text\n\ndefault")
|
||||
self.assertEqual(findMultipartPostBoundary(post), "9051914041544843365972754266")
|
||||
|
||||
def test_find_multipart_post_boundary_none(self):
|
||||
self.assertIsNone(findMultipartPostBoundary(""))
|
||||
|
||||
|
||||
class TestCommonHeadersAndExpected(unittest.TestCase):
|
||||
|
||||
def test_get_header_case_insensitive(self):
|
||||
self.assertEqual(getHeader({"Foo": "bar"}, "foo"), "bar")
|
||||
|
||||
def test_get_header_missing(self):
|
||||
self.assertIsNone(getHeader({"Foo": "bar"}, "x"))
|
||||
|
||||
def test_get_header_empty_dict(self):
|
||||
self.assertIsNone(getHeader({}, "anything"))
|
||||
|
||||
def test_get_request_header_hit(self):
|
||||
self.assertEqual(getText(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "foo")), "BAR")
|
||||
|
||||
def test_get_request_header_miss(self):
|
||||
self.assertIsNone(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "missing"))
|
||||
|
||||
def test_extract_expected_value_bool_true(self):
|
||||
self.assertIs(extractExpectedValue(["1"], EXPECTED.BOOL), True)
|
||||
|
||||
def test_extract_expected_value_bool_false(self):
|
||||
self.assertIs(extractExpectedValue(["0"], EXPECTED.BOOL), False)
|
||||
|
||||
def test_extract_expected_value_bool_word(self):
|
||||
self.assertIs(extractExpectedValue(["true"], EXPECTED.BOOL), True)
|
||||
self.assertIs(extractExpectedValue(["false"], EXPECTED.BOOL), False)
|
||||
|
||||
def test_extract_expected_value_int(self):
|
||||
self.assertEqual(extractExpectedValue("5", EXPECTED.INT), 5)
|
||||
|
||||
def test_extract_expected_value_int_invalid(self):
|
||||
self.assertIsNone(extractExpectedValue(u"7\xb9645", EXPECTED.INT))
|
||||
|
||||
def test_extract_expected_value_no_expected(self):
|
||||
self.assertEqual(extractExpectedValue("foo", None), "foo")
|
||||
|
||||
|
||||
class TestParseJsonAndHash(unittest.TestCase):
|
||||
|
||||
def test_parse_json_double_quotes(self):
|
||||
self.assertEqual(parseJson('{"id":1}')["id"], 1)
|
||||
|
||||
def test_parse_json_single_quotes(self):
|
||||
self.assertEqual(parseJson("{'id':1, 'foo':[2,3,4]}")["id"], 1)
|
||||
|
||||
def test_parse_json_not_json(self):
|
||||
self.assertIsNone(parseJson("this is not json"))
|
||||
|
||||
def test_parse_password_hash_mssql(self):
|
||||
saved = kb.forcedDbms
|
||||
try:
|
||||
kb.forcedDbms = DBMS.MSSQL
|
||||
result = parsePasswordHash("0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a")
|
||||
self.assertIn("salt: 4086ceb6", result)
|
||||
self.assertIn("header: 0x0100", result)
|
||||
finally:
|
||||
kb.forcedDbms = saved
|
||||
|
||||
def test_parse_password_hash_none(self):
|
||||
self.assertEqual(parsePasswordHash(None), NULL)
|
||||
|
||||
def test_parse_password_hash_blank(self):
|
||||
self.assertEqual(parsePasswordHash(" "), NULL)
|
||||
|
||||
|
||||
class TestSerializeAndTechnique(unittest.TestCase):
|
||||
|
||||
def test_serialize_roundtrip(self):
|
||||
self.assertEqual(unserializeObject(serializeObject([1, 2, 3])), [1, 2, 3])
|
||||
|
||||
def test_serialize_object_is_str(self):
|
||||
self.assertIsInstance(serializeObject([1, 2, ("a", "b")]), str)
|
||||
|
||||
def test_unserialize_none(self):
|
||||
self.assertIsNone(unserializeObject(None))
|
||||
|
||||
def test_set_get_technique_thread_local(self):
|
||||
saved = getTechnique()
|
||||
try:
|
||||
setTechnique(5)
|
||||
self.assertEqual(getTechnique(), 5)
|
||||
finally:
|
||||
setTechnique(saved)
|
||||
|
||||
def test_get_technique_falls_back_to_kb(self):
|
||||
saved_thread = getTechnique()
|
||||
saved_kb = kb.get("technique")
|
||||
try:
|
||||
setTechnique(None)
|
||||
kb.technique = 7
|
||||
self.assertEqual(getTechnique(), 7)
|
||||
finally:
|
||||
setTechnique(saved_thread)
|
||||
kb.technique = saved_kb
|
||||
|
||||
|
||||
class TestRemovePostHint(unittest.TestCase):
|
||||
|
||||
def test_removes_known_prefix(self):
|
||||
self.assertEqual(removePostHintPrefix("JSON id"), "id")
|
||||
|
||||
def test_no_prefix_unchanged(self):
|
||||
self.assertEqual(removePostHintPrefix("id"), "id")
|
||||
|
||||
|
||||
class TestFileHelpers(unittest.TestCase):
|
||||
|
||||
def test_check_file_existing(self):
|
||||
self.assertTrue(checkFile(__file__))
|
||||
|
||||
def test_check_file_missing_no_raise(self):
|
||||
self.assertFalse(checkFile("/no/such/path_xyz_123", raiseOnError=False))
|
||||
|
||||
def test_check_file_missing_raises(self):
|
||||
with self.assertRaises(SqlmapSystemException):
|
||||
checkFile("/no/such/path_xyz_123", raiseOnError=True)
|
||||
|
||||
def test_is_zip_file_wordlist(self):
|
||||
# paths.WORDLIST is a zip-compressed wordlist shipped with sqlmap
|
||||
self.assertTrue(isZipFile(paths.WORDLIST))
|
||||
|
||||
def test_is_zip_file_plain_text(self):
|
||||
self.assertFalse(isZipFile(paths.SQL_KEYWORDS))
|
||||
|
||||
def test_safe_filepath_encode_ascii_passthrough(self):
|
||||
# On Python 3 the function returns the value unchanged for str input
|
||||
self.assertEqual(safeFilepathEncode("/tmp/x"), "/tmp/x")
|
||||
|
||||
def test_safe_expand_user_basename_preserved(self):
|
||||
self.assertIn(os.path.basename(__file__), safeExpandUser(__file__))
|
||||
|
||||
|
||||
class TestCheckOldOptions(unittest.TestCase):
|
||||
|
||||
def test_no_old_options_is_noop(self):
|
||||
# Returns None and does not raise when no deprecated options are present
|
||||
self.assertIsNone(checkOldOptions(["-u", "http://test.invalid/?id=1", "--banner"]))
|
||||
|
||||
|
||||
class TestOptionSetWriteFile(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._saved = (conf.fileWrite, conf.fileDest, conf.get("fileWriteType"))
|
||||
|
||||
def tearDown(self):
|
||||
conf.fileWrite, conf.fileDest, conf.fileWriteType = self._saved
|
||||
|
||||
def test_noop_when_no_filewrite(self):
|
||||
conf.fileWrite = None
|
||||
self.assertIsNone(option._setWriteFile())
|
||||
|
||||
def test_raises_on_missing_local_file(self):
|
||||
conf.fileWrite = "/no/such/local_file_xyz"
|
||||
conf.fileDest = "/var/www/x"
|
||||
with self.assertRaises(SqlmapFilePathException):
|
||||
option._setWriteFile()
|
||||
|
||||
def test_raises_on_missing_dest(self):
|
||||
fd, path = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
try:
|
||||
conf.fileWrite = path
|
||||
conf.fileDest = None
|
||||
with self.assertRaises(SqlmapMissingMandatoryOptionException):
|
||||
option._setWriteFile()
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_sets_file_write_type(self):
|
||||
fd, path = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
try:
|
||||
conf.fileWrite = path
|
||||
conf.fileDest = "/var/www/x"
|
||||
option._setWriteFile()
|
||||
self.assertIn(conf.fileWriteType, ("text", "binary"))
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class TestOptionSetHTTPTimeout(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._savedTimeout = conf.timeout
|
||||
self._savedSocket = socket.getdefaulttimeout()
|
||||
|
||||
def tearDown(self):
|
||||
conf.timeout = self._savedTimeout
|
||||
socket.setdefaulttimeout(self._savedSocket)
|
||||
|
||||
def test_explicit_timeout(self):
|
||||
conf.timeout = 10
|
||||
option._setHTTPTimeout()
|
||||
self.assertEqual(conf.timeout, 10.0)
|
||||
|
||||
def test_below_minimum_is_clamped(self):
|
||||
conf.timeout = 1
|
||||
option._setHTTPTimeout()
|
||||
self.assertEqual(conf.timeout, 3.0)
|
||||
|
||||
def test_default_when_unset(self):
|
||||
conf.timeout = None
|
||||
option._setHTTPTimeout()
|
||||
self.assertEqual(conf.timeout, 30.0)
|
||||
|
||||
|
||||
class TestOptionSetHTTPAuthentication(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._saved = {
|
||||
"authType": conf.authType,
|
||||
"authCred": conf.authCred,
|
||||
"authFile": conf.authFile,
|
||||
"authUsername": conf.authUsername,
|
||||
"authPassword": conf.authPassword,
|
||||
"httpHeaders": list(conf.httpHeaders),
|
||||
"passwordMgr": kb.passwordMgr,
|
||||
}
|
||||
# provide a real password manager so the basic/digest branches work
|
||||
kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
|
||||
def tearDown(self):
|
||||
conf.authType = self._saved["authType"]
|
||||
conf.authCred = self._saved["authCred"]
|
||||
conf.authFile = self._saved["authFile"]
|
||||
conf.authUsername = self._saved["authUsername"]
|
||||
conf.authPassword = self._saved["authPassword"]
|
||||
conf.httpHeaders = self._saved["httpHeaders"]
|
||||
kb.passwordMgr = self._saved["passwordMgr"]
|
||||
|
||||
def test_noop_when_nothing_set(self):
|
||||
conf.authType = None
|
||||
conf.authCred = None
|
||||
conf.authFile = None
|
||||
self.assertIsNone(option._setHTTPAuthentication())
|
||||
|
||||
def test_basic_credentials_parsed(self):
|
||||
conf.authType = "basic"
|
||||
conf.authCred = "admin:secret"
|
||||
conf.authFile = None
|
||||
option._setHTTPAuthentication()
|
||||
self.assertEqual(conf.authUsername, "admin")
|
||||
self.assertEqual(conf.authPassword, "secret")
|
||||
|
||||
def test_ntlm_credentials_parsed(self):
|
||||
conf.authType = "ntlm"
|
||||
conf.authCred = "DOMAIN\\user:pa:ss"
|
||||
conf.authFile = None
|
||||
conf.authUsername = None
|
||||
conf.authPassword = None
|
||||
# The python-ntlm handler module is optional; credential parsing happens
|
||||
# before the handler import, so the parsed creds are set regardless.
|
||||
try:
|
||||
option._setHTTPAuthentication()
|
||||
except SqlmapMissingDependence:
|
||||
pass
|
||||
self.assertEqual(conf.authUsername, "DOMAIN\\user")
|
||||
self.assertEqual(conf.authPassword, "pa:ss")
|
||||
|
||||
def test_ntlm_bad_format_raises(self):
|
||||
conf.authType = "ntlm"
|
||||
conf.authCred = "nobackslash:pass"
|
||||
conf.authFile = None
|
||||
with self.assertRaises(SqlmapSyntaxException):
|
||||
option._setHTTPAuthentication()
|
||||
|
||||
def test_bearer_appends_authorization_header(self):
|
||||
conf.authType = "bearer"
|
||||
conf.authCred = "tok123"
|
||||
conf.authFile = None
|
||||
conf.httpHeaders = []
|
||||
option._setHTTPAuthentication()
|
||||
self.assertIn((HTTP_HEADER.AUTHORIZATION, "Bearer tok123"), conf.httpHeaders)
|
||||
|
||||
def test_unsupported_type_raises(self):
|
||||
conf.authType = "wrongtype"
|
||||
conf.authCred = "a:b"
|
||||
conf.authFile = None
|
||||
with self.assertRaises(SqlmapSyntaxException):
|
||||
option._setHTTPAuthentication()
|
||||
|
||||
def test_type_without_credentials_raises(self):
|
||||
conf.authType = "basic"
|
||||
conf.authCred = None
|
||||
conf.authFile = None
|
||||
with self.assertRaises(SqlmapSyntaxException):
|
||||
option._setHTTPAuthentication()
|
||||
|
||||
def test_credentials_without_type_raises(self):
|
||||
conf.authType = None
|
||||
conf.authCred = "a:b"
|
||||
conf.authFile = None
|
||||
with self.assertRaises(SqlmapSyntaxException):
|
||||
option._setHTTPAuthentication()
|
||||
|
||||
def test_authfile_without_type_defaults_to_pki(self):
|
||||
conf.authType = None
|
||||
conf.authCred = None
|
||||
conf.authFile = __file__ # exists, so checkFile() inside PKI branch passes
|
||||
option._setHTTPAuthentication()
|
||||
self.assertEqual(conf.authType, AUTH_TYPE.PKI)
|
||||
|
||||
def test_pki_type_without_authfile_raises(self):
|
||||
conf.authType = "pki"
|
||||
conf.authCred = "x"
|
||||
conf.authFile = None
|
||||
with self.assertRaises(SqlmapSyntaxException):
|
||||
option._setHTTPAuthentication()
|
||||
|
||||
|
||||
class TestOptionSetAuthCred(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._saved = {
|
||||
"scheme": conf.scheme,
|
||||
"hostname": conf.hostname,
|
||||
"port": conf.port,
|
||||
"authUsername": conf.authUsername,
|
||||
"authPassword": conf.authPassword,
|
||||
"passwordMgr": kb.passwordMgr,
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
conf.scheme = self._saved["scheme"]
|
||||
conf.hostname = self._saved["hostname"]
|
||||
conf.port = self._saved["port"]
|
||||
conf.authUsername = self._saved["authUsername"]
|
||||
conf.authPassword = self._saved["authPassword"]
|
||||
kb.passwordMgr = self._saved["passwordMgr"]
|
||||
|
||||
def test_noop_without_password_manager(self):
|
||||
kb.passwordMgr = None
|
||||
# Must not raise when there is no password manager configured
|
||||
self.assertIsNone(option._setAuthCred())
|
||||
|
||||
def test_adds_credentials_to_manager(self):
|
||||
kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
conf.scheme = "http"
|
||||
conf.hostname = "host"
|
||||
conf.port = 80
|
||||
conf.authUsername = "u"
|
||||
conf.authPassword = "p"
|
||||
option._setAuthCred()
|
||||
self.assertEqual(
|
||||
kb.passwordMgr.find_user_password(None, "http://host:80"),
|
||||
("u", "p"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -1,706 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Additional unit coverage for lib/core/agent.py, lib/core/common.py and
|
||||
lib/utils/brute.py, targeting functions/branches NOT already exercised by:
|
||||
|
||||
* tests/test_agent.py (payload delimiters, prefix/suffix defaults,
|
||||
getFields(SELECT a,b), one MySQL concatQuery,
|
||||
cleanupPayload RANDNUM)
|
||||
* tests/test_agent_dialects.py (null/cast/concat, hexConvertField,
|
||||
nullAndCastField, simpleConcatenate,
|
||||
forgeUnionQuery(-1,3,...), limitQuery(0,...),
|
||||
forgeCaseStatement, runAsDBMSUser-noop)
|
||||
* tests/test_common_utils.py (paramToDict, getCharset, getLimitRange,
|
||||
parseUnionPage, safeStringFormat, urlencode,
|
||||
parseTargetUrl/Direct, safeSQLIdentificatorNaming)
|
||||
* tests/test_common_parsers.py (request-file parsers, reflective masking,
|
||||
findPageForms, saveConfig, getSQLSnippet,
|
||||
Backend setters, urlencode/safeStringFormat extras)
|
||||
|
||||
This file instead covers:
|
||||
|
||||
agent.py: forgeUnionQuery (limited / multipleUnions / fromTable / collate /
|
||||
INTO OUTFILE), limitQuery across several DBMS shapes (TOP/ROWNUM/
|
||||
OFFSET dialects + the " FROM "-less early return), whereQuery
|
||||
(dumpWhere splicing), getComment, concatQuery(unpack=False),
|
||||
cleanupPayload([ORIGVALUE]/[ORIGINAL]/[SPACE_REPLACE]),
|
||||
adjustLateValues (SLEEPTIME/base64/RANDNUM), getFields on TOP /
|
||||
DISTINCT / function / no-FROM shapes, prefixQuery/suffixQuery with
|
||||
explicit prefix/suffix/clause/comment args, nullAndCastField noCast.
|
||||
|
||||
common.py: isNoneValue, isNullValue, isNumPosStrValue, isNumber, isListLike,
|
||||
filterPairValues, filterListValue, filterNone, filterStringValue,
|
||||
zeroDepthSearch, splitFields, unArrayizeValue, flattenValue,
|
||||
arrayizeValue, joinValue, aliasToDbmsEnum, getPageWordSet,
|
||||
resetCookieJar (clear branch), normalizeUnicode.
|
||||
|
||||
brute.py: tableExists / columnExists driven with conf.direct=True and the
|
||||
external collaborators (inject.checkBooleanExpression, getFileItems,
|
||||
runThreads) monkeypatched, plus _addPageTextWords.
|
||||
|
||||
Everything runs in isolation (no network, no DBMS, no filesystem mutation of
|
||||
the project). Any global conf/kb/Backend state that a call reads or writes is
|
||||
snapshotted in setUp and restored in tearDown so test ordering is irrelevant.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.agent import agent
|
||||
from lib.core.data import conf, kb, queries
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.settings import (
|
||||
PAYLOAD_DELIMITER,
|
||||
SLEEP_TIME_MARKER,
|
||||
BOUNDED_BASE64_MARKER,
|
||||
NULL,
|
||||
)
|
||||
from lib.core.common import (
|
||||
Backend,
|
||||
isNoneValue,
|
||||
isNullValue,
|
||||
isNumPosStrValue,
|
||||
isNumber,
|
||||
isListLike,
|
||||
filterPairValues,
|
||||
filterListValue,
|
||||
filterNone,
|
||||
filterStringValue,
|
||||
zeroDepthSearch,
|
||||
splitFields,
|
||||
unArrayizeValue,
|
||||
flattenValue,
|
||||
arrayizeValue,
|
||||
joinValue,
|
||||
aliasToDbmsEnum,
|
||||
getPageWordSet,
|
||||
resetCookieJar,
|
||||
normalizeUnicode,
|
||||
)
|
||||
|
||||
|
||||
class DbmsStateMixin(object):
|
||||
"""Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak."""
|
||||
|
||||
def setUp(self):
|
||||
self._forcedDbms = kb.forcedDbms
|
||||
self._sticky = kb.stickyDBMS
|
||||
self._batch = conf.batch
|
||||
conf.batch = True
|
||||
|
||||
def tearDown(self):
|
||||
kb.forcedDbms = self._forcedDbms
|
||||
kb.stickyDBMS = self._sticky
|
||||
conf.batch = self._batch
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# lib/core/agent.py
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase):
|
||||
"""forgeUnionQuery arg combinations not reached by the dialect smoke test."""
|
||||
|
||||
def test_limited_subselect_wraps_query(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# limited=True wraps the payload as (SELECT ...) at `position`, fills the
|
||||
# rest with `char`, and appends the FROM/comment/suffix
|
||||
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None,
|
||||
None, None, "NULL", None, limited=True)
|
||||
self.assertIn("(SELECT user FROM mysql.user)", out)
|
||||
self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out)
|
||||
# position 1 of 3 => NULL,<payload>,NULL
|
||||
self.assertEqual(out.count("NULL"), 2, msg=out)
|
||||
|
||||
def test_multiple_unions_appends_second_select(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.forgeUnionQuery("SELECT a FROM t", 0, 2, None, None, None,
|
||||
"NULL", None, multipleUnions="b")
|
||||
# the multipleUnions payload produces a *second* UNION ALL SELECT
|
||||
self.assertEqual(out.upper().count("UNION ALL SELECT"), 2, msg=out)
|
||||
self.assertIn("b", out)
|
||||
|
||||
def test_from_table_override(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.forgeUnionQuery("SELECT 1", 0, 1, None, None, None, "NULL",
|
||||
None, fromTable=" FROM dummytable")
|
||||
self.assertIn("FROM dummytable", out, msg=out)
|
||||
|
||||
def test_into_outfile_forces_null_position(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# an INTO OUTFILE clause forces position 0 / char NULL and re-appends the file part
|
||||
out = agent.forgeUnionQuery("SELECT a INTO OUTFILE '/tmp/o.txt' FROM t",
|
||||
1, 2, None, None, None, "NULL", None)
|
||||
self.assertIn("INTO OUTFILE '/tmp/o.txt'", out, msg=out)
|
||||
|
||||
def test_collate_clause_on_mysql(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# collate=True on MySQL wraps a non-NULL, non-numeric value in the
|
||||
# MYSQL_UNION_VALUE_CAST collation wrapper
|
||||
out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 0, 1, None,
|
||||
None, None, "NULL", None, collate=True)
|
||||
self.assertIn("CONVERT", out.upper(), msg=out)
|
||||
|
||||
|
||||
class TestLimitQuery(DbmsStateMixin, unittest.TestCase):
|
||||
"""limitQuery dialect shapes beyond the single limitQuery(0,...) smoke test."""
|
||||
|
||||
def test_no_from_returns_unchanged(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
self.assertEqual(agent.limitQuery(5, "SELECT 1", "1"), "SELECT 1")
|
||||
|
||||
def test_mysql_appends_limit_offset_one(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.limitQuery(7, "SELECT user FROM mysql.user", "user")
|
||||
self.assertTrue(out.endswith("LIMIT 7,1"), msg=out)
|
||||
|
||||
def test_pgsql_offset_form(self):
|
||||
set_dbms(DBMS.PGSQL)
|
||||
out = agent.limitQuery(4, "SELECT usename FROM pg_shadow", "usename")
|
||||
self.assertIn("OFFSET 4 LIMIT 1", out, msg=out)
|
||||
|
||||
def test_oracle_rownum_wrap(self):
|
||||
set_dbms(DBMS.ORACLE)
|
||||
out = agent.limitQuery(2, "SELECT banner FROM v$version", ["banner"])
|
||||
# Oracle wraps in a ROWNUM-bounded subselect ending with =<num+1>
|
||||
self.assertIn("ROWNUM", out.upper(), msg=out)
|
||||
self.assertTrue(out.rstrip().endswith("=3"), msg=out)
|
||||
|
||||
def test_firebird_first_skip(self):
|
||||
set_dbms(DBMS.FIREBIRD)
|
||||
out = agent.limitQuery(3, "SELECT foo FROM bar", "foo")
|
||||
self.assertIsInstance(out, str)
|
||||
self.assertIn("foo", out)
|
||||
# Firebird uses ROWS <num+1> TO <num+1> (the FIRST/SKIP emulation); pin
|
||||
# the exact shape so a broken offset arithmetic is caught.
|
||||
self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out)
|
||||
|
||||
def test_mssql_top_not_in(self):
|
||||
set_dbms(DBMS.MSSQL)
|
||||
out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name")
|
||||
# MSSQL emulates LIMIT via TOP + NOT IN
|
||||
self.assertIn("TOP", out.upper(), msg=out)
|
||||
self.assertIn("NOT IN", out.upper(), msg=out)
|
||||
|
||||
|
||||
class TestWhereQuery(DbmsStateMixin, unittest.TestCase):
|
||||
"""whereQuery only acts when conf.dumpWhere is set."""
|
||||
|
||||
def setUp(self):
|
||||
DbmsStateMixin.setUp(self)
|
||||
self._dumpWhere = conf.dumpWhere
|
||||
self._tbl = conf.tbl
|
||||
|
||||
def tearDown(self):
|
||||
conf.dumpWhere = self._dumpWhere
|
||||
conf.tbl = self._tbl
|
||||
DbmsStateMixin.tearDown(self)
|
||||
|
||||
def test_no_dumpwhere_is_identity(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = None
|
||||
self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t")
|
||||
|
||||
def test_appends_where_clause(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>10"
|
||||
conf.tbl = None
|
||||
out = agent.whereQuery("SELECT a FROM t")
|
||||
self.assertIn("WHERE id>10", out, msg=out)
|
||||
|
||||
def test_existing_where_gets_anded(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>10"
|
||||
conf.tbl = None
|
||||
out = agent.whereQuery("SELECT a FROM t WHERE b=1")
|
||||
self.assertIn("AND id>10", out, msg=out)
|
||||
|
||||
def test_order_by_suffix_preserved(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.dumpWhere = "id>10"
|
||||
conf.tbl = None
|
||||
out = agent.whereQuery("SELECT a FROM t ORDER BY a")
|
||||
# the genuine trailing ORDER BY is kept after the spliced WHERE
|
||||
self.assertIn("WHERE id>10", out, msg=out)
|
||||
# the ORDER BY must survive *after* the spliced WHERE clause; the
|
||||
# substring check alone could pass even if the suffix were dropped.
|
||||
self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out)
|
||||
|
||||
|
||||
class TestGetComment(unittest.TestCase):
|
||||
def test_present(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x")
|
||||
|
||||
def test_absent_returns_empty(self):
|
||||
from lib.core.datatype import AttribDict
|
||||
self.assertEqual(agent.getComment(AttribDict()), "")
|
||||
|
||||
|
||||
class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase):
|
||||
def test_unpack_false_returns_input_unchanged(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False),
|
||||
"SELECT a FROM t")
|
||||
|
||||
def test_pgsql_unpack_uses_pipe_concat(self):
|
||||
set_dbms(DBMS.PGSQL)
|
||||
out = agent.concatQuery("SELECT usename FROM pg_shadow")
|
||||
self.assertIn("||", out, msg=out)
|
||||
self.assertIn(kb.chars.start, out, msg=out)
|
||||
self.assertIn(kb.chars.stop, out, msg=out)
|
||||
|
||||
|
||||
class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase):
|
||||
def test_origvalue_digit_inlined(self):
|
||||
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42")
|
||||
self.assertEqual(out, "x=42")
|
||||
|
||||
def test_origvalue_nondigit_quoted(self):
|
||||
out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc")
|
||||
self.assertIn("'abc'", out, msg=out)
|
||||
|
||||
def test_original_marker_raw_substitution(self):
|
||||
out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw")
|
||||
self.assertEqual(out, "p=raw")
|
||||
|
||||
def test_space_replace_marker(self):
|
||||
out = agent.cleanupPayload("a[SPACE_REPLACE]b")
|
||||
self.assertEqual(out, "a%sb" % kb.chars.space)
|
||||
|
||||
def test_non_string_returns_none(self):
|
||||
self.assertIsNone(agent.cleanupPayload(None))
|
||||
|
||||
|
||||
class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase):
|
||||
def test_sleeptime_replaced_with_timesec(self):
|
||||
out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER)
|
||||
self.assertEqual(out, "SLEEP(%s)" % conf.timeSec)
|
||||
self.assertNotIn(SLEEP_TIME_MARKER, out)
|
||||
|
||||
def test_randnum_marker_substituted(self):
|
||||
out = agent.adjustLateValues("v=[RANDNUM]")
|
||||
self.assertNotIn("[RANDNUM]", out)
|
||||
self.assertTrue(out.split("=")[1].isdigit(), msg=out)
|
||||
|
||||
def test_bounded_base64_marker_encoded(self):
|
||||
payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER)
|
||||
out = agent.adjustLateValues(payload)
|
||||
# the marked region is base64-encoded and the markers are consumed
|
||||
self.assertNotIn(BOUNDED_BASE64_MARKER, out)
|
||||
self.assertEqual(out, "QUI=")
|
||||
|
||||
def test_empty_payload_passthrough(self):
|
||||
self.assertEqual(agent.adjustLateValues(""), "")
|
||||
|
||||
|
||||
class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase):
|
||||
def test_select_top(self):
|
||||
set_dbms(DBMS.MSSQL)
|
||||
res = agent.getFields("SELECT TOP 1 name FROM sysobjects")
|
||||
self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched")
|
||||
self.assertEqual(res[6], "name")
|
||||
|
||||
def test_distinct(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
res = agent.getFields("SELECT DISTINCT(name) FROM t")
|
||||
self.assertEqual(res[6], "name")
|
||||
|
||||
def test_function_is_single_element(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
res = agent.getFields("SELECT COUNT(*) FROM t")
|
||||
self.assertEqual(res[5], ["COUNT(*)"])
|
||||
|
||||
def test_no_from_keeps_whole_select_list(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
res = agent.getFields("SELECT a,b,c")
|
||||
self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM")
|
||||
self.assertEqual(res[5], ["a", "b", "c"])
|
||||
|
||||
|
||||
class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase):
|
||||
def test_prefix_with_explicit_prefix(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.prefixQuery("1=1", prefix="')")
|
||||
self.assertIn("')", out, msg=out)
|
||||
self.assertTrue(out.endswith("1=1"), msg=out)
|
||||
|
||||
def test_prefix_group_by_clause_uses_prefix_verbatim(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# clause == [2] (GROUP BY / ORDER BY) => no trailing space added
|
||||
out = agent.prefixQuery("1=1", prefix="X", clause=[2])
|
||||
self.assertEqual(out, "X1=1")
|
||||
|
||||
def test_suffix_appends_comment(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.suffixQuery("1=1", comment="-- -")
|
||||
self.assertTrue(out.startswith("1=1"), msg=out)
|
||||
self.assertIn("-", out)
|
||||
|
||||
def test_suffix_appends_suffix_no_comment(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
out = agent.suffixQuery("1=1", suffix="')")
|
||||
self.assertIn("')", out, msg=out)
|
||||
|
||||
|
||||
class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase):
|
||||
def setUp(self):
|
||||
DbmsStateMixin.setUp(self)
|
||||
self._noCast = conf.noCast
|
||||
|
||||
def tearDown(self):
|
||||
conf.noCast = self._noCast
|
||||
DbmsStateMixin.tearDown(self)
|
||||
|
||||
def test_nocast_returns_field_unchanged(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.noCast = True
|
||||
self.assertEqual(agent.nullAndCastField("colname"), "colname")
|
||||
|
||||
def test_cast_present_when_nocast_off(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.noCast = False
|
||||
out = agent.nullAndCastField("colname")
|
||||
self.assertIn("CAST", out.upper(), msg=out)
|
||||
self.assertIn("colname", out)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# lib/core/common.py
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class TestSmallPredicates(unittest.TestCase):
|
||||
def test_is_none_value(self):
|
||||
self.assertTrue(isNoneValue(None))
|
||||
self.assertTrue(isNoneValue("None"))
|
||||
self.assertTrue(isNoneValue(""))
|
||||
self.assertTrue(isNoneValue([]))
|
||||
self.assertTrue(isNoneValue(["None", ""]))
|
||||
self.assertTrue(isNoneValue({}))
|
||||
self.assertFalse(isNoneValue([2]))
|
||||
self.assertFalse(isNoneValue("x"))
|
||||
|
||||
def test_is_null_value(self):
|
||||
self.assertTrue(isNullValue(u"NULL"))
|
||||
self.assertTrue(isNullValue(u"null"))
|
||||
self.assertFalse(isNullValue(u"foobar"))
|
||||
self.assertFalse(isNullValue(5))
|
||||
|
||||
def test_is_num_pos_str_value(self):
|
||||
self.assertTrue(isNumPosStrValue(1))
|
||||
self.assertTrue(isNumPosStrValue("1"))
|
||||
self.assertFalse(isNumPosStrValue(0))
|
||||
self.assertFalse(isNumPosStrValue("-2"))
|
||||
self.assertFalse(isNumPosStrValue("100000000000000000000"))
|
||||
self.assertFalse(isNumPosStrValue("abc"))
|
||||
|
||||
def test_is_number(self):
|
||||
self.assertTrue(isNumber(1))
|
||||
self.assertTrue(isNumber("0"))
|
||||
self.assertTrue(isNumber("3.14"))
|
||||
self.assertFalse(isNumber("foobar"))
|
||||
self.assertFalse(isNumber(None))
|
||||
|
||||
def test_is_list_like(self):
|
||||
self.assertTrue(isListLike([1]))
|
||||
self.assertTrue(isListLike((1,)))
|
||||
self.assertTrue(isListLike(set([1])))
|
||||
self.assertFalse(isListLike("x"))
|
||||
self.assertFalse(isListLike(5))
|
||||
|
||||
|
||||
class TestValueShaping(unittest.TestCase):
|
||||
def test_filter_pair_values(self):
|
||||
self.assertEqual(filterPairValues([[1, 2], [3], 1, [4, 5]]), [[1, 2], [4, 5]])
|
||||
self.assertEqual(filterPairValues(None), [])
|
||||
|
||||
def test_filter_list_value(self):
|
||||
self.assertEqual(filterListValue(["users", "admins", "logs"], r"(users|admins)"),
|
||||
["users", "admins"])
|
||||
# non-list input returned unchanged
|
||||
self.assertEqual(filterListValue("notlist", r"x"), "notlist")
|
||||
# no regex returns input
|
||||
self.assertEqual(filterListValue(["a"], None), ["a"])
|
||||
|
||||
def test_filter_none(self):
|
||||
self.assertEqual(filterNone([1, 2, "", None, 3, 0]), [1, 2, 3, 0])
|
||||
|
||||
def test_filter_string_value(self):
|
||||
self.assertEqual(filterStringValue("wzydeadbeef0123#", r"[0-9a-f]"), "deadbeef0123")
|
||||
|
||||
def test_un_arrayize_value(self):
|
||||
self.assertEqual(unArrayizeValue(["1"]), "1")
|
||||
self.assertEqual(unArrayizeValue("1"), "1")
|
||||
self.assertEqual(unArrayizeValue(["1", "2"]), "1")
|
||||
self.assertEqual(unArrayizeValue([["a", "b"], "c"]), "a")
|
||||
self.assertIsNone(unArrayizeValue([]))
|
||||
|
||||
def test_flatten_value(self):
|
||||
self.assertEqual(list(flattenValue([["1"], [["2"], "3"]])), ["1", "2", "3"])
|
||||
|
||||
def test_arrayize_value(self):
|
||||
self.assertEqual(arrayizeValue("1"), ["1"])
|
||||
self.assertEqual(arrayizeValue(["1"]), ["1"])
|
||||
|
||||
def test_join_value(self):
|
||||
self.assertEqual(joinValue(["1", "2"]), "1,2")
|
||||
self.assertEqual(joinValue("1"), "1")
|
||||
self.assertEqual(joinValue(["1", None]), "1,None")
|
||||
|
||||
|
||||
class TestZeroDepthAndSplit(unittest.TestCase):
|
||||
def test_zero_depth_search_skips_parens(self):
|
||||
expr = "SELECT (SELECT id FROM users WHERE 2>1) AS r FROM DUAL"
|
||||
idx = zeroDepthSearch(expr, " FROM ")
|
||||
# only the outer top-level FROM is found, not the one inside the subselect
|
||||
self.assertEqual(len(idx), 1)
|
||||
self.assertTrue(expr[idx[0]:].startswith(" FROM DUAL"))
|
||||
|
||||
def test_zero_depth_search_ignores_quoted(self):
|
||||
expr = "a , 'b , c' , d"
|
||||
# commas inside the quoted literal are not reported
|
||||
self.assertEqual(len(zeroDepthSearch(expr, ",")), 2)
|
||||
|
||||
def test_split_fields_basic(self):
|
||||
self.assertEqual(splitFields("foo, bar, max(foo, bar)"),
|
||||
["foo", "bar", "max(foo,bar)"])
|
||||
|
||||
def test_split_fields_quoted(self):
|
||||
self.assertEqual(splitFields("a, 'b, c', d"), ["a", "'b, c'", "d"])
|
||||
|
||||
def test_split_fields_custom_delimiter(self):
|
||||
self.assertEqual(splitFields("a; b; max(c; d)", delimiter=";"),
|
||||
["a", "b", "max(c;d)"])
|
||||
|
||||
|
||||
class TestAliasToDbmsEnum(unittest.TestCase):
|
||||
def test_known_aliases(self):
|
||||
self.assertEqual(aliasToDbmsEnum("mssql"), DBMS.MSSQL)
|
||||
self.assertEqual(aliasToDbmsEnum("mysql"), DBMS.MYSQL)
|
||||
self.assertEqual(aliasToDbmsEnum("postgres"), DBMS.PGSQL)
|
||||
|
||||
def test_unknown_alias_returns_none(self):
|
||||
self.assertIsNone(aliasToDbmsEnum("definitely_not_a_dbms"))
|
||||
|
||||
def test_empty_returns_none(self):
|
||||
self.assertIsNone(aliasToDbmsEnum(""))
|
||||
|
||||
|
||||
class TestGetPageWordSet(unittest.TestCase):
|
||||
def test_word_extraction(self):
|
||||
words = getPageWordSet(u"<html><title>foobar</title><body>test</body></html>")
|
||||
self.assertEqual(sorted(words), [u"foobar", u"test"])
|
||||
|
||||
def test_non_string_returns_empty(self):
|
||||
self.assertEqual(getPageWordSet(None), set())
|
||||
|
||||
|
||||
class TestNormalizeUnicode(unittest.TestCase):
|
||||
def test_accents_stripped(self):
|
||||
# normalizeUnicode collapses accented chars to their ASCII base
|
||||
self.assertEqual(normalizeUnicode(u"éè"), "ee")
|
||||
|
||||
def test_plain_ascii_unchanged(self):
|
||||
self.assertEqual(normalizeUnicode(u"abc123"), "abc123")
|
||||
|
||||
def test_none_returns_none(self):
|
||||
self.assertIsNone(normalizeUnicode(None))
|
||||
|
||||
|
||||
class TestResetCookieJar(unittest.TestCase):
|
||||
"""resetCookieJar's clear branch (conf.loadCookies falsy)."""
|
||||
|
||||
def setUp(self):
|
||||
self._loadCookies = conf.loadCookies
|
||||
conf.loadCookies = None
|
||||
|
||||
def tearDown(self):
|
||||
conf.loadCookies = self._loadCookies
|
||||
|
||||
def test_clear_branch(self):
|
||||
try:
|
||||
from http.cookiejar import CookieJar
|
||||
except ImportError: # Python 2
|
||||
from cookielib import CookieJar
|
||||
|
||||
jar = CookieJar()
|
||||
cleared = {"called": False}
|
||||
|
||||
class _Jar(object):
|
||||
def clear(self):
|
||||
cleared["called"] = True
|
||||
|
||||
resetCookieJar(_Jar())
|
||||
self.assertTrue(cleared["called"])
|
||||
# also accepts a real jar without raising
|
||||
self.assertIsNone(resetCookieJar(jar))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# lib/utils/brute.py
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
import lib.utils.brute as brute
|
||||
from lib.request import inject
|
||||
import lib.core.threads as threads_mod
|
||||
import lib.core.common as common_mod
|
||||
|
||||
|
||||
class TestBrute(DbmsStateMixin, unittest.TestCase):
|
||||
"""Drive tableExists / columnExists with all external collaborators stubbed.
|
||||
|
||||
conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression,
|
||||
getFileItems and runThreads are monkeypatched so the check runs synchronously,
|
||||
deterministically and offline. getPageWordSet is neutralized so the wordlist is
|
||||
just what the stub returns.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
DbmsStateMixin.setUp(self)
|
||||
self._saved_conf = {k: conf.get(k) for k in
|
||||
("direct", "db", "tbl", "threads", "api", "verbose")}
|
||||
self._choices = kb.choices
|
||||
self._cachedTables = kb.data.get("cachedTables")
|
||||
self._cachedColumns = kb.data.get("cachedColumns")
|
||||
self._brute = kb.brute
|
||||
self._origPage = kb.originalPage
|
||||
|
||||
# stub the collaborators
|
||||
self._orig_cbe = inject.checkBooleanExpression
|
||||
self._orig_brute_cbe = brute.inject.checkBooleanExpression
|
||||
self._orig_getFileItems = brute.getFileItems
|
||||
self._orig_runThreads = brute.runThreads
|
||||
self._orig_getPageWordSet = brute.getPageWordSet
|
||||
|
||||
from lib.core.datatype import AttribDict
|
||||
kb.choices = AttribDict(keycheck=False)
|
||||
kb.choices.tableExists = None
|
||||
kb.choices.columnExists = None
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.cachedColumns = {}
|
||||
kb.brute = AttribDict({"tables": [], "columns": []})
|
||||
kb.originalPage = None
|
||||
|
||||
conf.direct = True
|
||||
conf.db = None
|
||||
conf.threads = 1
|
||||
conf.api = False
|
||||
conf.verbose = 0
|
||||
|
||||
# runThreads -> just call the worker once synchronously
|
||||
def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs):
|
||||
kb.threadContinue = True
|
||||
threadFunction()
|
||||
brute.runThreads = _fakeRunThreads
|
||||
# no page words injected into the wordlist
|
||||
brute.getPageWordSet = lambda page: set()
|
||||
# wordlist file -> small fixed list
|
||||
brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"]
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
kb.choices = self._choices
|
||||
if self._cachedTables is None:
|
||||
kb.data.pop("cachedTables", None)
|
||||
else:
|
||||
kb.data.cachedTables = self._cachedTables
|
||||
if self._cachedColumns is None:
|
||||
kb.data.pop("cachedColumns", None)
|
||||
else:
|
||||
kb.data.cachedColumns = self._cachedColumns
|
||||
kb.brute = self._brute
|
||||
kb.originalPage = self._origPage
|
||||
brute.inject.checkBooleanExpression = self._orig_brute_cbe
|
||||
brute.getFileItems = self._orig_getFileItems
|
||||
brute.runThreads = self._orig_runThreads
|
||||
brute.getPageWordSet = self._orig_getPageWordSet
|
||||
DbmsStateMixin.tearDown(self)
|
||||
|
||||
def test_table_exists_collects_true_results(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
|
||||
def _cbe(expression, expectingNone=True):
|
||||
# initial sanity probe (random table) -> must be False, otherwise the
|
||||
# function raises SqlmapDataException; then only "users" exists.
|
||||
return "users" in expression
|
||||
brute.inject.checkBooleanExpression = _cbe
|
||||
|
||||
result = brute.tableExists("/nonexistent/tables.txt")
|
||||
# cachedTables keyed by conf.db (None here) holds the discovered table
|
||||
self.assertIn(None, result)
|
||||
self.assertIn("users", result[None])
|
||||
self.assertNotIn("logs", result.get(None, []))
|
||||
# also recorded in kb.brute.tables as (db, table)
|
||||
self.assertIn((None, "users"), kb.brute.tables)
|
||||
|
||||
def test_table_exists_invalid_results_raises(self):
|
||||
from lib.core.exception import SqlmapDataException
|
||||
set_dbms(DBMS.MYSQL)
|
||||
# the initial random-table probe returns True -> "invalid results" guard
|
||||
brute.inject.checkBooleanExpression = lambda *a, **k: True
|
||||
with self.assertRaises(SqlmapDataException):
|
||||
brute.tableExists("/nonexistent/tables.txt")
|
||||
|
||||
def test_column_exists_requires_table(self):
|
||||
from lib.core.exception import SqlmapMissingMandatoryOptionException
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.tbl = None
|
||||
# the sanity probe is False so we reach the missing-table guard
|
||||
brute.inject.checkBooleanExpression = lambda *a, **k: False
|
||||
with self.assertRaises(SqlmapMissingMandatoryOptionException):
|
||||
brute.columnExists("/nonexistent/columns.txt")
|
||||
|
||||
def test_column_exists_collects_and_types(self):
|
||||
set_dbms(DBMS.MYSQL)
|
||||
conf.tbl = "users"
|
||||
brute.getFileItems = lambda *a, **k: ["id", "name"]
|
||||
|
||||
calls = {"n": 0}
|
||||
|
||||
def _cbe(expression, expectingNone=True):
|
||||
calls["n"] += 1
|
||||
# initial sanity probe uses two random strings (no real column name)
|
||||
if "id" not in expression and "name" not in expression:
|
||||
return False
|
||||
# MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`.
|
||||
# 'id' is numeric (no non-digit chars => probe False => numeric);
|
||||
# 'name' is non-numeric (has non-digit chars => probe True => non-numeric).
|
||||
if "REGEXP" in expression:
|
||||
return "name" in expression
|
||||
# plain existence check (EXISTS(SELECT <col> FROM <tbl>)) => both columns exist
|
||||
return True
|
||||
brute.inject.checkBooleanExpression = _cbe
|
||||
|
||||
result = brute.columnExists("/nonexistent/columns.txt")
|
||||
self.assertIn(None, result)
|
||||
cols = result[None]["users"]
|
||||
# column names are run through safeSQLIdentificatorNaming, so the MySQL
|
||||
# reserved word "name" comes back backtick-quoted
|
||||
from lib.core.common import safeSQLIdentificatorNaming, getText
|
||||
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("id"))), "numeric")
|
||||
self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("name"))), "non-numeric")
|
||||
|
||||
def test_add_page_text_words_filters(self):
|
||||
# restore the real getPageWordSet for this one and drive it directly
|
||||
brute.getPageWordSet = self._orig_getPageWordSet
|
||||
kb.originalPage = u"<html>admin password 1abc xy verylongword</html>"
|
||||
words = brute._addPageTextWords()
|
||||
# words <= 2 chars or starting with a digit are dropped
|
||||
self.assertIn("admin", words)
|
||||
self.assertIn("password", words)
|
||||
self.assertNotIn("xy", words)
|
||||
self.assertNotIn("1abc", words)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -36,6 +36,26 @@ from plugins.generic.databases import Databases
|
|||
_NOOP = lambda self: None
|
||||
|
||||
|
||||
def _inference_gv(count, sequence):
|
||||
"""Build an inject.getValue stub for blind inference branches.
|
||||
|
||||
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
|
||||
yields the next item from `sequence` wrapped as a single-cell row ([value]),
|
||||
cycling if exhausted. This mirrors the count-then-per-row contract of every
|
||||
isInferenceAvailable() branch.
|
||||
"""
|
||||
state = {"i": 0}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return str(count)
|
||||
val = sequence[state["i"] % len(sequence)]
|
||||
state["i"] += 1
|
||||
return [val]
|
||||
|
||||
return gv
|
||||
|
||||
|
||||
class _BaseEnumTest(unittest.TestCase):
|
||||
"""Shared setup/teardown that snapshots and restores all touched global state."""
|
||||
|
||||
|
|
@ -507,5 +527,241 @@ class TestGetProcedures(_BaseEnumTest):
|
|||
self.assertEqual(sorted(result), sorted(procs))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Inference / brute-force branches (relocated from test_generic_enum_more.py)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _DbBase(unittest.TestCase):
|
||||
_CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude",
|
||||
"getComments", "excludeSysDbs", "search", "freshQueries")
|
||||
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
|
||||
self._saved_getValue = dbmod.inject.getValue
|
||||
self._saved_checkBool = dbmod.inject.checkBooleanExpression
|
||||
self._saved_injection_data = kb.injection.data
|
||||
self._saved_has_is = kb.data.get("has_information_schema")
|
||||
self._saved_hintValue = kb.get("hintValue")
|
||||
self._saved_choices = dict(kb.choices)
|
||||
self._saved_readInput = dbmod.readInput
|
||||
self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None)
|
||||
Databases.forceDbmsEnum = _NOOP
|
||||
|
||||
conf.getComments = False
|
||||
conf.excludeSysDbs = False
|
||||
conf.exclude = None
|
||||
conf.search = False
|
||||
conf.freshQueries = False
|
||||
conf.col = None
|
||||
kb.data.has_information_schema = True
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
dbmod.inject.getValue = self._saved_getValue
|
||||
dbmod.inject.checkBooleanExpression = self._saved_checkBool
|
||||
dbmod.readInput = self._saved_readInput
|
||||
kb.injection.data = self._saved_injection_data
|
||||
kb.data.has_information_schema = self._saved_has_is
|
||||
kb.hintValue = self._saved_hintValue
|
||||
kb.choices.clear()
|
||||
kb.choices.update(self._saved_choices)
|
||||
if self._saved_forceDbmsEnum is not None:
|
||||
Databases.forceDbmsEnum = self._saved_forceDbmsEnum
|
||||
else:
|
||||
try:
|
||||
del Databases.forceDbmsEnum
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def _fresh(self):
|
||||
d = Databases()
|
||||
kb.data.currentDb = ""
|
||||
kb.data.cachedDbs = []
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedCounts = {}
|
||||
kb.data.cachedStatements = []
|
||||
kb.data.cachedProcedures = []
|
||||
return d
|
||||
|
||||
def _inference(self):
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
|
||||
class TestDatabasesInference(_DbBase):
|
||||
def test_get_columns_inference_pgsql_types(self):
|
||||
# Blind column enumeration on PostgreSQL: a count, then for each index a
|
||||
# column name followed by its type. Assert the {db:{tbl:{col:type}}} parse.
|
||||
set_dbms("PostgreSQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
conf.db = "public"
|
||||
conf.tbl = "users"
|
||||
|
||||
names = ["id", "email"]
|
||||
state = {"i": 0, "name": True}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return str(len(names))
|
||||
if state["name"]:
|
||||
val = names[state["i"] % len(names)]
|
||||
state["i"] += 1
|
||||
state["name"] = False
|
||||
return [val]
|
||||
state["name"] = True
|
||||
return ["integer"]
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
result = d.getColumns()
|
||||
cols = result["public"]["users"]
|
||||
self.assertEqual(len(cols), 2)
|
||||
self.assertEqual(cols.get("id"), "integer")
|
||||
|
||||
def test_get_columns_inference_dump_mode_collist(self):
|
||||
# dumpMode with an explicit conf.col list: in the inference branch the
|
||||
# columns are taken straight from colList (no count/type queries at all)
|
||||
# and stored with value None. Asserting no getValue ran proves the
|
||||
# dump-mode shortcut, not a network round-trip.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = "id,name"
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("dumpMode+colList must not query in inference branch")
|
||||
|
||||
dbmod.inject.getValue = boom
|
||||
result = d.getColumns(dumpMode=True)
|
||||
cols = result["testdb"]["users"]
|
||||
# "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it;
|
||||
# both columns must be present (count, since exact key varies by quoting).
|
||||
self.assertEqual(len(cols), 2)
|
||||
self.assertIn("id", cols)
|
||||
self.assertIsNone(cols.get("id"))
|
||||
|
||||
def test_get_count_over_cached_tables_inference(self):
|
||||
# getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount.
|
||||
# Drive the inband table fetch + per-table count and assert the
|
||||
# {db:{count:[tables]}} grouping (tables sharing a count are grouped).
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = None
|
||||
kb.data.cachedTables = {"testdb": ["users", "posts"]}
|
||||
|
||||
counts = {"users": "5", "posts": "5"}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
for t, c in counts.items():
|
||||
if t in query:
|
||||
return c
|
||||
return "0"
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
result = d.getCount()
|
||||
# both tables have count 5 -> grouped under the same key
|
||||
self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"])
|
||||
|
||||
def test_get_statements_count_zero_returns_empty(self):
|
||||
# Inference path: a zero count short-circuits to the (empty) cache.
|
||||
set_dbms("PostgreSQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
# getStatements compares the count with the int literal 0 (count == 0), so
|
||||
# the count stub must return an int 0 (not "0") to take the empty branch.
|
||||
dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0")
|
||||
result = d.getStatements()
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_get_procedures_inference(self):
|
||||
set_dbms("PostgreSQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"])
|
||||
result = d.getProcedures()
|
||||
self.assertEqual(sorted(result), ["sp_a", "sp_b"])
|
||||
|
||||
def test_get_dbs_mssql_inband_paging(self):
|
||||
# MSSQL with no rows from the primary query falls into the query2 paging
|
||||
# loop (one indexed query per db until a blank value stops it).
|
||||
set_dbms("Microsoft SQL Server")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
dbs = ["master", "model"]
|
||||
|
||||
def gv(query, *a, **k):
|
||||
# The primary inband query is 'SELECT name FROM master..sysdatabases'
|
||||
# (no DB_NAME); make it return nothing so getDbs falls into the
|
||||
# 'SELECT DB_NAME(<index>)' paging loop (query2).
|
||||
if "DB_NAME" not in query:
|
||||
return None
|
||||
import re as _re
|
||||
idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0])
|
||||
return dbs[idx] if idx < len(dbs) else ""
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
result = d.getDbs()
|
||||
self.assertEqual(sorted(result), ["master", "model"])
|
||||
|
||||
def test_get_tables_inference_grouped_per_db(self):
|
||||
# Blind table enumeration: count for the db, then one table name per index.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
conf.db = "shop"
|
||||
conf.tbl = None
|
||||
dbmod.inject.getValue = _inference_gv(2, ["orders", "items"])
|
||||
result = d.getTables()
|
||||
self.assertIn("shop", result)
|
||||
self.assertEqual(sorted(result["shop"]), ["items", "orders"])
|
||||
|
||||
|
||||
class TestDatabasesBruteForce(_DbBase):
|
||||
def test_get_columns_mysql_lt5_bruteforce_decline(self):
|
||||
# MySQL < 5 (no information_schema) forces bruteForce in getColumns; with
|
||||
# the common-column-existence prompt answered 'N' it returns None without
|
||||
# issuing any column query.
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
kb.data.has_information_schema = False
|
||||
kb.choices.columnExists = None
|
||||
dbmod.readInput = lambda *a, **k: "N"
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("bruteForce decline must not query columns")
|
||||
|
||||
dbmod.inject.getValue = boom
|
||||
result = d.getColumns()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_columns_bruteforce_dumpmode_collist_on_decline(self):
|
||||
# bruteForce + decline + dumpMode + colList: the columns from colList are
|
||||
# stored with None type (the dump-mode salvage branch), not dropped.
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = "a,b"
|
||||
kb.data.has_information_schema = False
|
||||
kb.choices.columnExists = None
|
||||
dbmod.readInput = lambda *a, **k: "N"
|
||||
dbmod.inject.getValue = lambda *a, **k: None
|
||||
result = d.getColumns(dumpMode=True)
|
||||
cols = result["testdb"]["users"]
|
||||
self.assertEqual(sorted(cols.keys()), ["a", "b"])
|
||||
self.assertIsNone(cols.get("a"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -6,11 +6,19 @@ See the file 'LICENSE' for copying permission
|
|||
|
||||
DBMS-specific enumeration overrides (plugins/dbms/<dbms>/enumeration.py),
|
||||
driven through each full DBMS handler with the injection layer mocked, so the
|
||||
dialect-specific table/column discovery paths run without a live target. The
|
||||
in-band (UNION/error/direct) branch is taken via conf.direct=True and
|
||||
inject.getValue is stubbed with canned result rows.
|
||||
dialect-specific table/column/user/privilege discovery paths run without a live
|
||||
target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via
|
||||
conf.direct=True and inject.getValue is stubbed with canned result rows;
|
||||
conf.batch=True avoids interactive prompts.
|
||||
|
||||
Consolidated from former tests/test_dbms_enum.py (Microsoft SQL Server),
|
||||
tests/test_dbms_enum_a.py (Oracle/PostgreSQL/MySQL/SQLite) and
|
||||
tests/test_dbms_enum_b.py (Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB).
|
||||
|
||||
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
|
@ -19,11 +27,18 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.enums import EXPECTED
|
||||
from lib.core.exception import SqlmapUnsupportedFeatureException
|
||||
from lib.request import inject
|
||||
|
||||
|
||||
class _EnumBase(unittest.TestCase):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base for Microsoft SQL Server getTables (former test_dbms_enum.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _EnumBaseMSSQL(unittest.TestCase):
|
||||
"""Snapshot/restore the global state these enumerators mutate."""
|
||||
module = None # the enumeration module whose inject.getValue we patch
|
||||
|
||||
|
|
@ -45,7 +60,7 @@ class _EnumBase(unittest.TestCase):
|
|||
kb.data.cachedColumns = self._cachedColumns
|
||||
|
||||
|
||||
class TestMSSQLServerEnum(_EnumBase):
|
||||
class TestMSSQLServerEnum(_EnumBaseMSSQL):
|
||||
import plugins.dbms.mssqlserver.enumeration as module
|
||||
|
||||
def _handler(self):
|
||||
|
|
@ -94,5 +109,614 @@ class TestMSSQLServerEnum(_EnumBase):
|
|||
self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base for Oracle/PostgreSQL/MySQL/SQLite (former test_dbms_enum_a.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _EnumBaseA(unittest.TestCase):
|
||||
"""Snapshot/restore the global state these enumerators mutate.
|
||||
|
||||
Other tests in the suite depend on clean globals (a leaked kb.hintValue
|
||||
breaks test_inference_engine; a leaked forced DBMS breaks others), so every
|
||||
knob touched here is captured in setUp and put back in tearDown.
|
||||
"""
|
||||
|
||||
# the enumeration module whose inject.getValue we patch (overridden per DBMS)
|
||||
module = None
|
||||
|
||||
def setUp(self):
|
||||
# conf knobs
|
||||
self._direct = conf.direct
|
||||
self._batch = conf.batch
|
||||
self._user = conf.user
|
||||
self._db = conf.get("db")
|
||||
self._tbl = conf.get("tbl")
|
||||
self._exclude = conf.get("exclude")
|
||||
|
||||
# injection layer (some override modules - e.g. SQLite/PostgreSQL - do not
|
||||
# import inject because their overrides return constants without querying)
|
||||
self._has_inject = hasattr(self.module, "inject")
|
||||
if self._has_inject:
|
||||
self._gv = self.module.inject.getValue
|
||||
|
||||
# kb.data cached* containers
|
||||
self._cachedTables = kb.data.get("cachedTables")
|
||||
self._cachedColumns = kb.data.get("cachedColumns")
|
||||
self._cachedDbs = kb.data.get("cachedDbs")
|
||||
self._cachedUsers = kb.data.get("cachedUsers")
|
||||
self._cachedUsersRoles = kb.data.get("cachedUsersRoles")
|
||||
self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges")
|
||||
self._has_information_schema = kb.data.get("has_information_schema")
|
||||
|
||||
# state other tests are sensitive to
|
||||
self._hintValue = kb.hintValue
|
||||
self._injectionData = kb.injection.data
|
||||
self._forcedDbms = Backend.getForcedDbms()
|
||||
self._stickyDBMS = kb.stickyDBMS
|
||||
|
||||
# avoid readInput EOFError flakiness and interactive prompts
|
||||
conf.direct = True
|
||||
conf.batch = True
|
||||
|
||||
def tearDown(self):
|
||||
conf.direct = self._direct
|
||||
conf.batch = self._batch
|
||||
conf.user = self._user
|
||||
conf.db = self._db
|
||||
conf.tbl = self._tbl
|
||||
conf.exclude = self._exclude
|
||||
|
||||
if self._has_inject:
|
||||
self.module.inject.getValue = self._gv
|
||||
|
||||
kb.data.cachedTables = self._cachedTables
|
||||
kb.data.cachedColumns = self._cachedColumns
|
||||
kb.data.cachedDbs = self._cachedDbs
|
||||
kb.data.cachedUsers = self._cachedUsers
|
||||
kb.data.cachedUsersRoles = self._cachedUsersRoles
|
||||
kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges
|
||||
kb.data.has_information_schema = self._has_information_schema
|
||||
|
||||
kb.hintValue = self._hintValue
|
||||
kb.injection.data = self._injectionData
|
||||
kb.stickyDBMS = self._stickyDBMS
|
||||
if self._forcedDbms is not None:
|
||||
Backend.forceDbms(self._forcedDbms)
|
||||
else:
|
||||
kb.forcedDbms = None
|
||||
|
||||
|
||||
class TestOracleEnum(_EnumBaseA):
|
||||
module = importlib.import_module("plugins.dbms.oracle.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.oracle import OracleMap
|
||||
set_dbms("Oracle")
|
||||
return OracleMap()
|
||||
|
||||
def test_get_roles(self):
|
||||
# rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles
|
||||
conf.user = None
|
||||
kb.data.cachedUsersRoles = {}
|
||||
self.module.inject.getValue = lambda q, *a, **k: [
|
||||
["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"]
|
||||
]
|
||||
roles, areAdmins = self._handler().getRoles()
|
||||
self.assertIn("SYS", roles)
|
||||
self.assertIn("SCOTT", roles)
|
||||
self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"})
|
||||
# DBA implies administrator
|
||||
self.assertIn("SYS", areAdmins)
|
||||
|
||||
def test_get_roles_filtered_by_user(self):
|
||||
# conf.user populates a WHERE clause; canned rows still drive the parse
|
||||
conf.user = "SCOTT"
|
||||
kb.data.cachedUsersRoles = {}
|
||||
self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]]
|
||||
roles, _ = self._handler().getRoles()
|
||||
self.assertEqual(list(roles.keys()), ["SCOTT"])
|
||||
self.assertEqual(roles["SCOTT"], ["RESOURCE"])
|
||||
|
||||
def test_get_roles_multiple_roles_per_user(self):
|
||||
# a user appearing across several rows accumulates all granted roles
|
||||
conf.user = None
|
||||
kb.data.cachedUsersRoles = {}
|
||||
self.module.inject.getValue = lambda q, *a, **k: [
|
||||
["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"]
|
||||
]
|
||||
roles, _ = self._handler().getRoles()
|
||||
self.assertEqual(
|
||||
set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"}
|
||||
)
|
||||
|
||||
|
||||
class TestPostgreSQLEnum(_EnumBaseA):
|
||||
module = importlib.import_module("plugins.dbms.postgresql.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.postgresql import PostgreSQLMap
|
||||
set_dbms("PostgreSQL")
|
||||
return PostgreSQLMap()
|
||||
|
||||
def test_get_hostname_unsupported(self):
|
||||
# PostgreSQL overrides getHostname purely to warn; it returns None
|
||||
self.assertIsNone(self._handler().getHostname())
|
||||
|
||||
|
||||
class TestMySQLEnum(_EnumBaseA):
|
||||
# MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the
|
||||
# generic discovery path through the full MySQL handler instead.
|
||||
module = importlib.import_module("plugins.generic.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.mysql import MySQLMap
|
||||
set_dbms("MySQL")
|
||||
return MySQLMap()
|
||||
|
||||
def test_get_dbs(self):
|
||||
conf.db = None
|
||||
kb.data.cachedDbs = []
|
||||
kb.data.has_information_schema = True
|
||||
self.module.inject.getValue = lambda q, *a, **k: (
|
||||
3 if k.get("expected") == EXPECTED.INT
|
||||
else [["information_schema"], ["testdb"], ["mysql"]]
|
||||
)
|
||||
dbs = self._handler().getDbs()
|
||||
self.assertIn("testdb", dbs)
|
||||
self.assertEqual(set(kb.data.cachedDbs), set(dbs))
|
||||
|
||||
|
||||
class TestSQLiteEnum(_EnumBaseA):
|
||||
module = importlib.import_module("plugins.dbms.sqlite.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.sqlite import SQLiteMap
|
||||
set_dbms("SQLite")
|
||||
return SQLiteMap()
|
||||
|
||||
def test_unsupported_simple_overrides(self):
|
||||
# SQLite overrides these to a warning + an empty/neutral return value
|
||||
h = self._handler()
|
||||
self.assertIsNone(h.getCurrentUser())
|
||||
self.assertIsNone(h.getCurrentDb())
|
||||
self.assertIsNone(h.getHostname())
|
||||
self.assertEqual(h.getUsers(), [])
|
||||
self.assertEqual(h.getDbs(), [])
|
||||
self.assertEqual(h.searchDb(), [])
|
||||
self.assertEqual(h.getStatements(), [])
|
||||
self.assertEqual(h.getPasswordHashes(), {})
|
||||
self.assertEqual(h.getPrivileges(), {})
|
||||
|
||||
def test_is_dba_always_true(self):
|
||||
# on SQLite the current user is treated as having all privileges
|
||||
self.assertTrue(self._handler().isDba())
|
||||
|
||||
def test_search_column_raises(self):
|
||||
with self.assertRaises(SqlmapUnsupportedFeatureException):
|
||||
self._handler().searchColumn()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base + helpers for Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB
|
||||
# (former test_dbms_enum_b.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _fresh_cached():
|
||||
kb.data.cachedDbs = []
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedUsers = []
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
kb.data.cachedCounts = {}
|
||||
kb.data.cachedStatements = []
|
||||
kb.data.banner = None
|
||||
|
||||
|
||||
class _NoOpDumper(object):
|
||||
"""Swallow every dumper call so search methods don't emit/prompt."""
|
||||
|
||||
def __getattr__(self, name):
|
||||
return lambda *a, **k: None
|
||||
|
||||
|
||||
def _handler(display_name, dirname):
|
||||
"""Instantiate the full *Map handler for the given DBMS."""
|
||||
set_dbms(display_name)
|
||||
main = importlib.import_module("plugins.dbms.%s" % dirname)
|
||||
cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0]
|
||||
return cls()
|
||||
|
||||
|
||||
class _EnumBaseB(unittest.TestCase):
|
||||
"""Snapshot/restore every global these enumerators mutate."""
|
||||
|
||||
# subclasses set these
|
||||
display_name = None
|
||||
dirname = None
|
||||
|
||||
def setUp(self):
|
||||
# config snapshot
|
||||
self._direct = conf.direct
|
||||
self._batch = conf.batch
|
||||
self._db = conf.db
|
||||
self._tbl = conf.tbl
|
||||
self._col = conf.col
|
||||
self._user = conf.user
|
||||
self._exclude = conf.exclude
|
||||
self._search = conf.search
|
||||
self._getBanner = conf.getBanner
|
||||
self._excludeSysDbs = conf.excludeSysDbs
|
||||
self._dumper = conf.get("dumper")
|
||||
|
||||
# kb snapshot
|
||||
self._cached = {k: kb.data.get(k) for k in (
|
||||
"cachedDbs", "cachedTables", "cachedColumns", "cachedUsers",
|
||||
"cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner",
|
||||
)}
|
||||
self._hintValue = kb.hintValue
|
||||
self._injectionData = kb.injection.data
|
||||
self._currentDb = kb.data.get("currentDb")
|
||||
self._hasIS = kb.data.get("has_information_schema")
|
||||
|
||||
# injection layer snapshot
|
||||
self._gv = inject.getValue
|
||||
self._cbe = getattr(inject, "checkBooleanExpression", None)
|
||||
|
||||
# baseline config the in-band/non-interactive paths need
|
||||
conf.direct = True
|
||||
conf.batch = True
|
||||
kb.data.has_information_schema = True
|
||||
_fresh_cached()
|
||||
|
||||
# restore the chosen DBMS for every test
|
||||
self.handler = _handler(self.display_name, self.dirname)
|
||||
# the enumeration module whose pivotDumpTable some tests stub
|
||||
self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname)
|
||||
|
||||
def tearDown(self):
|
||||
conf.direct = self._direct
|
||||
conf.batch = self._batch
|
||||
conf.db = self._db
|
||||
conf.tbl = self._tbl
|
||||
conf.col = self._col
|
||||
conf.user = self._user
|
||||
conf.exclude = self._exclude
|
||||
conf.search = self._search
|
||||
conf.getBanner = self._getBanner
|
||||
conf.excludeSysDbs = self._excludeSysDbs
|
||||
conf.dumper = self._dumper
|
||||
|
||||
for k, v in self._cached.items():
|
||||
kb.data[k] = v
|
||||
kb.hintValue = self._hintValue
|
||||
kb.injection.data = self._injectionData
|
||||
kb.data.currentDb = self._currentDb
|
||||
kb.data.has_information_schema = self._hasIS
|
||||
|
||||
inject.getValue = self._gv
|
||||
if self._cbe is not None:
|
||||
inject.checkBooleanExpression = self._cbe
|
||||
if hasattr(self.em, "pivotDumpTable"):
|
||||
# restore the pristine reference from the wrapper module
|
||||
import lib.utils.pivotdumptable as _pdt
|
||||
self.em.pivotDumpTable = _pdt.pivotDumpTable
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sybase
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSybaseEnum(_EnumBaseB):
|
||||
display_name = "Sybase"
|
||||
dirname = "sybase"
|
||||
|
||||
def _pivot(self, *value_lists):
|
||||
"""Make em.pivotDumpTable return canned (entries, lengths) per call.
|
||||
|
||||
Each successive call pops the next mapping of {colName: [values]}.
|
||||
"""
|
||||
calls = list(value_lists)
|
||||
|
||||
def fake(table, colList, count=None, blind=True, alias=None):
|
||||
mapping = calls.pop(0) if calls else {}
|
||||
entries = {}
|
||||
lengths = {}
|
||||
for col in colList:
|
||||
vals = mapping.get(col.split(".")[-1], [])
|
||||
entries[col] = list(vals)
|
||||
lengths[col] = 0
|
||||
return entries, lengths
|
||||
|
||||
self.em.pivotDumpTable = fake
|
||||
|
||||
def test_get_users(self):
|
||||
self._pivot({"name": ["sa", "guest"]})
|
||||
users = self.handler.getUsers()
|
||||
self.assertIn("sa", users)
|
||||
self.assertIn("guest", users)
|
||||
|
||||
def test_get_dbs(self):
|
||||
self._pivot({"name": ["master", "model"]})
|
||||
dbs = self.handler.getDbs()
|
||||
self.assertEqual(sorted(dbs), ["master", "model"])
|
||||
|
||||
def test_get_tables(self):
|
||||
conf.db = "testdb"
|
||||
self._pivot({"name": ["users", "logs"]})
|
||||
tables = self.handler.getTables()
|
||||
self.assertIn("testdb", tables)
|
||||
self.assertEqual(sorted(tables["testdb"]), ["logs", "users"])
|
||||
|
||||
def test_get_columns(self):
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
# column pivot returns name + usertype: REAL Sybase numeric type ids that
|
||||
# getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar").
|
||||
from lib.core.dicts import SYBASE_TYPES
|
||||
self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]})
|
||||
cols = self.handler.getColumns()
|
||||
self.assertIn("testdb", cols)
|
||||
# table key is identifier-normalized (may be schema-qualified)
|
||||
tbls = cols["testdb"]
|
||||
self.assertTrue(any("users" in t for t in tbls))
|
||||
colset = list(tbls.values())[0]
|
||||
# the VALUE is the resolved type name, not the raw usertype number:
|
||||
# proves the SYBASE_TYPES numeric->name mapping actually ran.
|
||||
self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int"
|
||||
self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar"
|
||||
|
||||
def test_get_privileges(self):
|
||||
# getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression).
|
||||
# Drive the admin-set branch BOTH ways via the isDba oracle so the result
|
||||
# is not forced by a constant-True stub.
|
||||
conf.user = None
|
||||
|
||||
# oracle True: every user is flagged DBA -> admins == all users
|
||||
self._pivot({"name": ["sa", "guest"]})
|
||||
inject.checkBooleanExpression = lambda *a, **k: True
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs) # users still enumerated as privilege keys
|
||||
self.assertIn("guest", privs)
|
||||
self.assertEqual(admins, set(["sa", "guest"]))
|
||||
|
||||
# oracle False: nobody is a DBA -> admins is empty, but users still listed
|
||||
_fresh_cached()
|
||||
self._pivot({"name": ["sa", "guest"]})
|
||||
inject.checkBooleanExpression = lambda *a, **k: False
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs)
|
||||
self.assertEqual(admins, set())
|
||||
|
||||
def test_search_not_implemented(self):
|
||||
# these intentionally return [] with a warning on Sybase
|
||||
self.assertEqual(self.handler.searchDb(), [])
|
||||
self.assertEqual(self.handler.searchTable(), [])
|
||||
self.assertEqual(self.handler.searchColumn(), [])
|
||||
|
||||
def test_get_hostname(self):
|
||||
# not possible on Sybase; just must not raise
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SAP MaxDB
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMaxDBEnum(_EnumBaseB):
|
||||
display_name = "SAP MaxDB"
|
||||
dirname = "maxdb"
|
||||
|
||||
def _pivot(self, *value_lists):
|
||||
calls = list(value_lists)
|
||||
|
||||
def fake(table, colList, count=None, blind=True, alias=None):
|
||||
mapping = calls.pop(0) if calls else {}
|
||||
entries = {}
|
||||
lengths = {}
|
||||
for col in colList:
|
||||
vals = mapping.get(col.split(".")[-1], [])
|
||||
entries[col] = list(vals)
|
||||
lengths[col] = 0
|
||||
return entries, lengths
|
||||
|
||||
self.em.pivotDumpTable = fake
|
||||
|
||||
def test_get_dbs(self):
|
||||
self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]})
|
||||
dbs = self.handler.getDbs()
|
||||
self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"])
|
||||
|
||||
def test_get_tables(self):
|
||||
conf.db = "SYSTEM"
|
||||
self._pivot({"tablename": ["USERS", "TABLES"]})
|
||||
tables = self.handler.getTables()
|
||||
# db key is identifier-normalized (uppercase names get quoted)
|
||||
self.assertEqual(len(tables), 1)
|
||||
tbls = list(tables.values())[0]
|
||||
self.assertEqual(sorted(tbls), ["TABLES", "USERS"])
|
||||
|
||||
def test_get_columns(self):
|
||||
conf.db = "SYSTEM"
|
||||
conf.tbl = "USERS"
|
||||
self._pivot({
|
||||
"columnname": ["ID", "NAME"],
|
||||
"datatype": ["INTEGER", "CHAR"],
|
||||
"len": ["4", "32"],
|
||||
})
|
||||
cols = self.handler.getColumns()
|
||||
self.assertEqual(len(cols), 1)
|
||||
tbls = list(cols.values())[0]
|
||||
self.assertIn("USERS", tbls)
|
||||
self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)")
|
||||
|
||||
def test_get_privileges_empty(self):
|
||||
self.assertEqual(self.handler.getPrivileges(), {})
|
||||
|
||||
def test_get_password_hashes_empty(self):
|
||||
self.assertEqual(self.handler.getPasswordHashes(), {})
|
||||
|
||||
def test_get_hostname(self):
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Microsoft SQL Server (methods NOT covered by TestMSSQLServerEnum above)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMSSQLServerExtraEnum(_EnumBaseB):
|
||||
display_name = "Microsoft SQL Server"
|
||||
dirname = "mssqlserver"
|
||||
|
||||
def test_get_privileges(self):
|
||||
# getPrivileges -> getUsers (generic, inject.getValue) then isDba.
|
||||
# Exercise the admin-set branch BOTH ways via the isDba oracle.
|
||||
conf.user = None
|
||||
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
|
||||
|
||||
# oracle True: all users flagged DBA
|
||||
inject.checkBooleanExpression = lambda *a, **k: True
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs)
|
||||
self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"]))
|
||||
|
||||
# oracle False: none are DBA -> empty admin set, users still enumerated
|
||||
_fresh_cached()
|
||||
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
|
||||
inject.checkBooleanExpression = lambda *a, **k: False
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs)
|
||||
self.assertEqual(admins, set())
|
||||
|
||||
def test_search_table(self):
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
# in-band branch: getValue returns matching table name(s)
|
||||
inject.getValue = lambda q, *a, **k: ["users"]
|
||||
# capture the discovered tables instead of dumping them
|
||||
captured = {}
|
||||
conf.dumper = _NoOpDumper()
|
||||
self.handler.dumpFoundTables = lambda tables: captured.update(tables)
|
||||
self.handler.searchTable()
|
||||
# at least one database mapped to the matched table
|
||||
flat = set()
|
||||
for tbls in captured.values():
|
||||
flat.update(tbls)
|
||||
self.assertTrue(any("users" in t for t in flat))
|
||||
|
||||
def test_search_column(self):
|
||||
conf.db = "testdb"
|
||||
conf.tbl = None
|
||||
conf.col = "password"
|
||||
# exact match (no wildcard) so no recursive getColumns call;
|
||||
# getValue returns the tables that contain the column
|
||||
inject.getValue = lambda q, *a, **k: ["users"]
|
||||
captured = {}
|
||||
conf.dumper = _NoOpDumper()
|
||||
self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs)
|
||||
self.handler.searchColumn()
|
||||
# the searched column was located in at least one table
|
||||
flat = set()
|
||||
for tbls in captured.values():
|
||||
flat.update(tbls)
|
||||
self.assertTrue(any("users" in t for t in flat))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IBM DB2
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDB2Enum(_EnumBaseB):
|
||||
display_name = "IBM DB2"
|
||||
dirname = "db2"
|
||||
|
||||
def test_get_password_hashes_empty(self):
|
||||
self.assertEqual(self.handler.getPasswordHashes(), {})
|
||||
|
||||
def test_get_statements_empty(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Informix
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestInformixEnum(_EnumBaseB):
|
||||
display_name = "Informix"
|
||||
dirname = "informix"
|
||||
|
||||
def test_search_db(self):
|
||||
self.assertEqual(self.handler.searchDb(), [])
|
||||
|
||||
def test_search_table(self):
|
||||
self.assertEqual(self.handler.searchTable(), [])
|
||||
|
||||
def test_search_column(self):
|
||||
self.assertEqual(self.handler.searchColumn(), [])
|
||||
|
||||
def test_get_statements(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Firebird
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFirebirdEnum(_EnumBaseB):
|
||||
display_name = "Firebird"
|
||||
dirname = "firebird"
|
||||
|
||||
def test_get_dbs_empty(self):
|
||||
self.assertEqual(self.handler.getDbs(), [])
|
||||
|
||||
def test_get_password_hashes_empty(self):
|
||||
self.assertEqual(self.handler.getPasswordHashes(), {})
|
||||
|
||||
def test_search_db_empty(self):
|
||||
self.assertEqual(self.handler.searchDb(), [])
|
||||
|
||||
def test_get_hostname(self):
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements_empty(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HSQLDB
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHSQLDBEnum(_EnumBaseB):
|
||||
display_name = "HSQLDB"
|
||||
dirname = "hsqldb"
|
||||
|
||||
def test_get_banner(self):
|
||||
conf.getBanner = True
|
||||
kb.data.banner = None
|
||||
# getValue returns a single-element LIST; getBanner pipes it through
|
||||
# unArrayizeValue, which must unwrap it to the scalar banner string.
|
||||
inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"]
|
||||
banner = self.handler.getBanner()
|
||||
self.assertEqual(banner, "HSQLDB 2.5.1")
|
||||
|
||||
def test_get_privileges_empty(self):
|
||||
self.assertEqual(self.handler.getPrivileges(), {})
|
||||
|
||||
def test_get_hostname(self):
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements_empty(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
def test_get_current_db_default_schema(self):
|
||||
from lib.core.settings import HSQLDB_DEFAULT_SCHEMA
|
||||
self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
DBMS-specific enumeration overrides for Oracle, PostgreSQL, MySQL and SQLite
|
||||
(plugins/dbms/<dbms>/enumeration.py), driven through each full DBMS handler with
|
||||
the injection layer mocked, so the dialect-specific discovery paths run without a
|
||||
live target. The in-band (UNION/error/direct) branch is taken via conf.direct=True
|
||||
and inject.getValue is stubbed with canned result rows.
|
||||
|
||||
Companion to tests/test_dbms_enum.py (which covers Microsoft SQL Server).
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.enums import EXPECTED
|
||||
from lib.core.exception import SqlmapUnsupportedFeatureException
|
||||
|
||||
|
||||
class _EnumBase(unittest.TestCase):
|
||||
"""Snapshot/restore the global state these enumerators mutate.
|
||||
|
||||
Other tests in the suite depend on clean globals (a leaked kb.hintValue
|
||||
breaks test_inference_engine; a leaked forced DBMS breaks others), so every
|
||||
knob touched here is captured in setUp and put back in tearDown.
|
||||
"""
|
||||
|
||||
# the enumeration module whose inject.getValue we patch (overridden per DBMS)
|
||||
module = None
|
||||
|
||||
def setUp(self):
|
||||
# conf knobs
|
||||
self._direct = conf.direct
|
||||
self._batch = conf.batch
|
||||
self._user = conf.user
|
||||
self._db = conf.get("db")
|
||||
self._tbl = conf.get("tbl")
|
||||
self._exclude = conf.get("exclude")
|
||||
|
||||
# injection layer (some override modules - e.g. SQLite/PostgreSQL - do not
|
||||
# import inject because their overrides return constants without querying)
|
||||
self._has_inject = hasattr(self.module, "inject")
|
||||
if self._has_inject:
|
||||
self._gv = self.module.inject.getValue
|
||||
|
||||
# kb.data cached* containers
|
||||
self._cachedTables = kb.data.get("cachedTables")
|
||||
self._cachedColumns = kb.data.get("cachedColumns")
|
||||
self._cachedDbs = kb.data.get("cachedDbs")
|
||||
self._cachedUsers = kb.data.get("cachedUsers")
|
||||
self._cachedUsersRoles = kb.data.get("cachedUsersRoles")
|
||||
self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges")
|
||||
self._has_information_schema = kb.data.get("has_information_schema")
|
||||
|
||||
# state other tests are sensitive to
|
||||
self._hintValue = kb.hintValue
|
||||
self._injectionData = kb.injection.data
|
||||
self._forcedDbms = Backend.getForcedDbms()
|
||||
self._stickyDBMS = kb.stickyDBMS
|
||||
|
||||
# avoid readInput EOFError flakiness and interactive prompts
|
||||
conf.direct = True
|
||||
conf.batch = True
|
||||
|
||||
def tearDown(self):
|
||||
conf.direct = self._direct
|
||||
conf.batch = self._batch
|
||||
conf.user = self._user
|
||||
conf.db = self._db
|
||||
conf.tbl = self._tbl
|
||||
conf.exclude = self._exclude
|
||||
|
||||
if self._has_inject:
|
||||
self.module.inject.getValue = self._gv
|
||||
|
||||
kb.data.cachedTables = self._cachedTables
|
||||
kb.data.cachedColumns = self._cachedColumns
|
||||
kb.data.cachedDbs = self._cachedDbs
|
||||
kb.data.cachedUsers = self._cachedUsers
|
||||
kb.data.cachedUsersRoles = self._cachedUsersRoles
|
||||
kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges
|
||||
kb.data.has_information_schema = self._has_information_schema
|
||||
|
||||
kb.hintValue = self._hintValue
|
||||
kb.injection.data = self._injectionData
|
||||
kb.stickyDBMS = self._stickyDBMS
|
||||
if self._forcedDbms is not None:
|
||||
Backend.forceDbms(self._forcedDbms)
|
||||
else:
|
||||
kb.forcedDbms = None
|
||||
|
||||
|
||||
class TestOracleEnum(_EnumBase):
|
||||
module = importlib.import_module("plugins.dbms.oracle.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.oracle import OracleMap
|
||||
set_dbms("Oracle")
|
||||
return OracleMap()
|
||||
|
||||
def test_get_roles(self):
|
||||
# rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles
|
||||
conf.user = None
|
||||
kb.data.cachedUsersRoles = {}
|
||||
self.module.inject.getValue = lambda q, *a, **k: [
|
||||
["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"]
|
||||
]
|
||||
roles, areAdmins = self._handler().getRoles()
|
||||
self.assertIn("SYS", roles)
|
||||
self.assertIn("SCOTT", roles)
|
||||
self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"})
|
||||
# DBA implies administrator
|
||||
self.assertIn("SYS", areAdmins)
|
||||
|
||||
def test_get_roles_filtered_by_user(self):
|
||||
# conf.user populates a WHERE clause; canned rows still drive the parse
|
||||
conf.user = "SCOTT"
|
||||
kb.data.cachedUsersRoles = {}
|
||||
self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]]
|
||||
roles, _ = self._handler().getRoles()
|
||||
self.assertEqual(list(roles.keys()), ["SCOTT"])
|
||||
self.assertEqual(roles["SCOTT"], ["RESOURCE"])
|
||||
|
||||
def test_get_roles_multiple_roles_per_user(self):
|
||||
# a user appearing across several rows accumulates all granted roles
|
||||
conf.user = None
|
||||
kb.data.cachedUsersRoles = {}
|
||||
self.module.inject.getValue = lambda q, *a, **k: [
|
||||
["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"]
|
||||
]
|
||||
roles, _ = self._handler().getRoles()
|
||||
self.assertEqual(
|
||||
set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"}
|
||||
)
|
||||
|
||||
|
||||
class TestPostgreSQLEnum(_EnumBase):
|
||||
module = importlib.import_module("plugins.dbms.postgresql.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.postgresql import PostgreSQLMap
|
||||
set_dbms("PostgreSQL")
|
||||
return PostgreSQLMap()
|
||||
|
||||
def test_get_hostname_unsupported(self):
|
||||
# PostgreSQL overrides getHostname purely to warn; it returns None
|
||||
self.assertIsNone(self._handler().getHostname())
|
||||
|
||||
|
||||
class TestMySQLEnum(_EnumBase):
|
||||
# MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the
|
||||
# generic discovery path through the full MySQL handler instead.
|
||||
module = importlib.import_module("plugins.generic.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.mysql import MySQLMap
|
||||
set_dbms("MySQL")
|
||||
return MySQLMap()
|
||||
|
||||
def test_get_dbs(self):
|
||||
conf.db = None
|
||||
kb.data.cachedDbs = []
|
||||
kb.data.has_information_schema = True
|
||||
self.module.inject.getValue = lambda q, *a, **k: (
|
||||
3 if k.get("expected") == EXPECTED.INT
|
||||
else [["information_schema"], ["testdb"], ["mysql"]]
|
||||
)
|
||||
dbs = self._handler().getDbs()
|
||||
self.assertIn("testdb", dbs)
|
||||
self.assertEqual(set(kb.data.cachedDbs), set(dbs))
|
||||
|
||||
|
||||
class TestSQLiteEnum(_EnumBase):
|
||||
module = importlib.import_module("plugins.dbms.sqlite.enumeration")
|
||||
|
||||
def _handler(self):
|
||||
from plugins.dbms.sqlite import SQLiteMap
|
||||
set_dbms("SQLite")
|
||||
return SQLiteMap()
|
||||
|
||||
def test_unsupported_simple_overrides(self):
|
||||
# SQLite overrides these to a warning + an empty/neutral return value
|
||||
h = self._handler()
|
||||
self.assertIsNone(h.getCurrentUser())
|
||||
self.assertIsNone(h.getCurrentDb())
|
||||
self.assertIsNone(h.getHostname())
|
||||
self.assertEqual(h.getUsers(), [])
|
||||
self.assertEqual(h.getDbs(), [])
|
||||
self.assertEqual(h.searchDb(), [])
|
||||
self.assertEqual(h.getStatements(), [])
|
||||
self.assertEqual(h.getPasswordHashes(), {})
|
||||
self.assertEqual(h.getPrivileges(), {})
|
||||
|
||||
def test_is_dba_always_true(self):
|
||||
# on SQLite the current user is treated as having all privileges
|
||||
self.assertTrue(self._handler().isDba())
|
||||
|
||||
def test_search_column_raises(self):
|
||||
with self.assertRaises(SqlmapUnsupportedFeatureException):
|
||||
self._handler().searchColumn()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Second batch of DBMS-specific enumeration override tests (companion to
|
||||
tests/test_dbms_enum.py, which covers Microsoft SQL Server getTables).
|
||||
|
||||
Each test drives a FULL per-DBMS handler (the *Map class in
|
||||
plugins/dbms/<dbms>/__init__.py) with the injection layer mocked, so the
|
||||
dialect-specific table/column/user/privilege discovery paths run without a live
|
||||
target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via
|
||||
conf.direct=True; conf.batch=True avoids interactive prompts.
|
||||
|
||||
Covered here:
|
||||
* Sybase - getUsers, getDbs, getTables, getColumns, getPrivileges,
|
||||
searchDb/searchTable/searchColumn, getHostname, getStatements
|
||||
* SAP MaxDB - getDbs, getTables, getColumns, getPrivileges,
|
||||
getPasswordHashes, getHostname, getStatements
|
||||
* Microsoft SQL Server - getPrivileges, searchTable, searchColumn
|
||||
(getTables already covered by test_dbms_enum.py)
|
||||
* IBM DB2 - getPasswordHashes, getStatements
|
||||
* Informix - searchDb, searchTable, searchColumn, getStatements
|
||||
* Firebird - getDbs, getPasswordHashes, searchDb, getHostname, getStatements
|
||||
* HSQLDB - getBanner, getPrivileges, getHostname, getStatements,
|
||||
getCurrentDb
|
||||
|
||||
Sybase/MaxDB enumeration goes through lib.utils.pivotdumptable.pivotDumpTable
|
||||
(imported into the module namespace), so for those we mock that wrapper - it is
|
||||
part of the same data-retrieval layer - and mock inject.getValue elsewhere.
|
||||
|
||||
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.common import Backend
|
||||
from lib.core.enums import EXPECTED
|
||||
from lib.request import inject
|
||||
|
||||
|
||||
def _fresh_cached():
|
||||
kb.data.cachedDbs = []
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedUsers = []
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
kb.data.cachedCounts = {}
|
||||
kb.data.cachedStatements = []
|
||||
kb.data.banner = None
|
||||
|
||||
|
||||
class _NoOpDumper(object):
|
||||
"""Swallow every dumper call so search methods don't emit/prompt."""
|
||||
|
||||
def __getattr__(self, name):
|
||||
return lambda *a, **k: None
|
||||
|
||||
|
||||
def _handler(display_name, dirname):
|
||||
"""Instantiate the full *Map handler for the given DBMS."""
|
||||
set_dbms(display_name)
|
||||
main = importlib.import_module("plugins.dbms.%s" % dirname)
|
||||
cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0]
|
||||
return cls()
|
||||
|
||||
|
||||
class _EnumBase(unittest.TestCase):
|
||||
"""Snapshot/restore every global these enumerators mutate."""
|
||||
|
||||
# subclasses set these
|
||||
display_name = None
|
||||
dirname = None
|
||||
|
||||
def setUp(self):
|
||||
# config snapshot
|
||||
self._direct = conf.direct
|
||||
self._batch = conf.batch
|
||||
self._db = conf.db
|
||||
self._tbl = conf.tbl
|
||||
self._col = conf.col
|
||||
self._user = conf.user
|
||||
self._exclude = conf.exclude
|
||||
self._search = conf.search
|
||||
self._getBanner = conf.getBanner
|
||||
self._excludeSysDbs = conf.excludeSysDbs
|
||||
self._dumper = conf.get("dumper")
|
||||
|
||||
# kb snapshot
|
||||
self._cached = {k: kb.data.get(k) for k in (
|
||||
"cachedDbs", "cachedTables", "cachedColumns", "cachedUsers",
|
||||
"cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner",
|
||||
)}
|
||||
self._hintValue = kb.hintValue
|
||||
self._injectionData = kb.injection.data
|
||||
self._currentDb = kb.data.get("currentDb")
|
||||
self._hasIS = kb.data.get("has_information_schema")
|
||||
|
||||
# injection layer snapshot
|
||||
self._gv = inject.getValue
|
||||
self._cbe = getattr(inject, "checkBooleanExpression", None)
|
||||
|
||||
# baseline config the in-band/non-interactive paths need
|
||||
conf.direct = True
|
||||
conf.batch = True
|
||||
kb.data.has_information_schema = True
|
||||
_fresh_cached()
|
||||
|
||||
# restore the chosen DBMS for every test
|
||||
self.handler = _handler(self.display_name, self.dirname)
|
||||
# the enumeration module whose pivotDumpTable some tests stub
|
||||
self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname)
|
||||
|
||||
def tearDown(self):
|
||||
conf.direct = self._direct
|
||||
conf.batch = self._batch
|
||||
conf.db = self._db
|
||||
conf.tbl = self._tbl
|
||||
conf.col = self._col
|
||||
conf.user = self._user
|
||||
conf.exclude = self._exclude
|
||||
conf.search = self._search
|
||||
conf.getBanner = self._getBanner
|
||||
conf.excludeSysDbs = self._excludeSysDbs
|
||||
conf.dumper = self._dumper
|
||||
|
||||
for k, v in self._cached.items():
|
||||
kb.data[k] = v
|
||||
kb.hintValue = self._hintValue
|
||||
kb.injection.data = self._injectionData
|
||||
kb.data.currentDb = self._currentDb
|
||||
kb.data.has_information_schema = self._hasIS
|
||||
|
||||
inject.getValue = self._gv
|
||||
if self._cbe is not None:
|
||||
inject.checkBooleanExpression = self._cbe
|
||||
if hasattr(self.em, "pivotDumpTable"):
|
||||
# restore the pristine reference from the wrapper module
|
||||
import lib.utils.pivotdumptable as _pdt
|
||||
self.em.pivotDumpTable = _pdt.pivotDumpTable
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sybase
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSybaseEnum(_EnumBase):
|
||||
display_name = "Sybase"
|
||||
dirname = "sybase"
|
||||
|
||||
def _pivot(self, *value_lists):
|
||||
"""Make em.pivotDumpTable return canned (entries, lengths) per call.
|
||||
|
||||
Each successive call pops the next mapping of {colName: [values]}.
|
||||
"""
|
||||
calls = list(value_lists)
|
||||
|
||||
def fake(table, colList, count=None, blind=True, alias=None):
|
||||
mapping = calls.pop(0) if calls else {}
|
||||
entries = {}
|
||||
lengths = {}
|
||||
for col in colList:
|
||||
vals = mapping.get(col.split(".")[-1], [])
|
||||
entries[col] = list(vals)
|
||||
lengths[col] = 0
|
||||
return entries, lengths
|
||||
|
||||
self.em.pivotDumpTable = fake
|
||||
|
||||
def test_get_users(self):
|
||||
self._pivot({"name": ["sa", "guest"]})
|
||||
users = self.handler.getUsers()
|
||||
self.assertIn("sa", users)
|
||||
self.assertIn("guest", users)
|
||||
|
||||
def test_get_dbs(self):
|
||||
self._pivot({"name": ["master", "model"]})
|
||||
dbs = self.handler.getDbs()
|
||||
self.assertEqual(sorted(dbs), ["master", "model"])
|
||||
|
||||
def test_get_tables(self):
|
||||
conf.db = "testdb"
|
||||
self._pivot({"name": ["users", "logs"]})
|
||||
tables = self.handler.getTables()
|
||||
self.assertIn("testdb", tables)
|
||||
self.assertEqual(sorted(tables["testdb"]), ["logs", "users"])
|
||||
|
||||
def test_get_columns(self):
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
# column pivot returns name + usertype: REAL Sybase numeric type ids that
|
||||
# getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar").
|
||||
from lib.core.dicts import SYBASE_TYPES
|
||||
self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]})
|
||||
cols = self.handler.getColumns()
|
||||
self.assertIn("testdb", cols)
|
||||
# table key is identifier-normalized (may be schema-qualified)
|
||||
tbls = cols["testdb"]
|
||||
self.assertTrue(any("users" in t for t in tbls))
|
||||
colset = list(tbls.values())[0]
|
||||
# the VALUE is the resolved type name, not the raw usertype number:
|
||||
# proves the SYBASE_TYPES numeric->name mapping actually ran.
|
||||
self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int"
|
||||
self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar"
|
||||
|
||||
def test_get_privileges(self):
|
||||
# getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression).
|
||||
# Drive the admin-set branch BOTH ways via the isDba oracle so the result
|
||||
# is not forced by a constant-True stub.
|
||||
conf.user = None
|
||||
|
||||
# oracle True: every user is flagged DBA -> admins == all users
|
||||
self._pivot({"name": ["sa", "guest"]})
|
||||
inject.checkBooleanExpression = lambda *a, **k: True
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs) # users still enumerated as privilege keys
|
||||
self.assertIn("guest", privs)
|
||||
self.assertEqual(admins, set(["sa", "guest"]))
|
||||
|
||||
# oracle False: nobody is a DBA -> admins is empty, but users still listed
|
||||
_fresh_cached()
|
||||
self._pivot({"name": ["sa", "guest"]})
|
||||
inject.checkBooleanExpression = lambda *a, **k: False
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs)
|
||||
self.assertEqual(admins, set())
|
||||
|
||||
def test_search_not_implemented(self):
|
||||
# these intentionally return [] with a warning on Sybase
|
||||
self.assertEqual(self.handler.searchDb(), [])
|
||||
self.assertEqual(self.handler.searchTable(), [])
|
||||
self.assertEqual(self.handler.searchColumn(), [])
|
||||
|
||||
def test_get_hostname(self):
|
||||
# not possible on Sybase; just must not raise
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SAP MaxDB
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMaxDBEnum(_EnumBase):
|
||||
display_name = "SAP MaxDB"
|
||||
dirname = "maxdb"
|
||||
|
||||
def _pivot(self, *value_lists):
|
||||
calls = list(value_lists)
|
||||
|
||||
def fake(table, colList, count=None, blind=True, alias=None):
|
||||
mapping = calls.pop(0) if calls else {}
|
||||
entries = {}
|
||||
lengths = {}
|
||||
for col in colList:
|
||||
vals = mapping.get(col.split(".")[-1], [])
|
||||
entries[col] = list(vals)
|
||||
lengths[col] = 0
|
||||
return entries, lengths
|
||||
|
||||
self.em.pivotDumpTable = fake
|
||||
|
||||
def test_get_dbs(self):
|
||||
self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]})
|
||||
dbs = self.handler.getDbs()
|
||||
self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"])
|
||||
|
||||
def test_get_tables(self):
|
||||
conf.db = "SYSTEM"
|
||||
self._pivot({"tablename": ["USERS", "TABLES"]})
|
||||
tables = self.handler.getTables()
|
||||
# db key is identifier-normalized (uppercase names get quoted)
|
||||
self.assertEqual(len(tables), 1)
|
||||
tbls = list(tables.values())[0]
|
||||
self.assertEqual(sorted(tbls), ["TABLES", "USERS"])
|
||||
|
||||
def test_get_columns(self):
|
||||
conf.db = "SYSTEM"
|
||||
conf.tbl = "USERS"
|
||||
self._pivot({
|
||||
"columnname": ["ID", "NAME"],
|
||||
"datatype": ["INTEGER", "CHAR"],
|
||||
"len": ["4", "32"],
|
||||
})
|
||||
cols = self.handler.getColumns()
|
||||
self.assertEqual(len(cols), 1)
|
||||
tbls = list(cols.values())[0]
|
||||
self.assertIn("USERS", tbls)
|
||||
self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)")
|
||||
|
||||
def test_get_privileges_empty(self):
|
||||
self.assertEqual(self.handler.getPrivileges(), {})
|
||||
|
||||
def test_get_password_hashes_empty(self):
|
||||
self.assertEqual(self.handler.getPasswordHashes(), {})
|
||||
|
||||
def test_get_hostname(self):
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Microsoft SQL Server (methods NOT covered by test_dbms_enum.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestMSSQLServerExtraEnum(_EnumBase):
|
||||
display_name = "Microsoft SQL Server"
|
||||
dirname = "mssqlserver"
|
||||
|
||||
def test_get_privileges(self):
|
||||
# getPrivileges -> getUsers (generic, inject.getValue) then isDba.
|
||||
# Exercise the admin-set branch BOTH ways via the isDba oracle.
|
||||
conf.user = None
|
||||
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
|
||||
|
||||
# oracle True: all users flagged DBA
|
||||
inject.checkBooleanExpression = lambda *a, **k: True
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs)
|
||||
self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"]))
|
||||
|
||||
# oracle False: none are DBA -> empty admin set, users still enumerated
|
||||
_fresh_cached()
|
||||
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
|
||||
inject.checkBooleanExpression = lambda *a, **k: False
|
||||
privs, admins = self.handler.getPrivileges()
|
||||
self.assertIn("sa", privs)
|
||||
self.assertEqual(admins, set())
|
||||
|
||||
def test_search_table(self):
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
# in-band branch: getValue returns matching table name(s)
|
||||
inject.getValue = lambda q, *a, **k: ["users"]
|
||||
# capture the discovered tables instead of dumping them
|
||||
captured = {}
|
||||
conf.dumper = _NoOpDumper()
|
||||
self.handler.dumpFoundTables = lambda tables: captured.update(tables)
|
||||
self.handler.searchTable()
|
||||
# at least one database mapped to the matched table
|
||||
flat = set()
|
||||
for tbls in captured.values():
|
||||
flat.update(tbls)
|
||||
self.assertTrue(any("users" in t for t in flat))
|
||||
|
||||
def test_search_column(self):
|
||||
conf.db = "testdb"
|
||||
conf.tbl = None
|
||||
conf.col = "password"
|
||||
# exact match (no wildcard) so no recursive getColumns call;
|
||||
# getValue returns the tables that contain the column
|
||||
inject.getValue = lambda q, *a, **k: ["users"]
|
||||
captured = {}
|
||||
conf.dumper = _NoOpDumper()
|
||||
self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs)
|
||||
self.handler.searchColumn()
|
||||
# the searched column was located in at least one table
|
||||
flat = set()
|
||||
for tbls in captured.values():
|
||||
flat.update(tbls)
|
||||
self.assertTrue(any("users" in t for t in flat))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IBM DB2
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDB2Enum(_EnumBase):
|
||||
display_name = "IBM DB2"
|
||||
dirname = "db2"
|
||||
|
||||
def test_get_password_hashes_empty(self):
|
||||
self.assertEqual(self.handler.getPasswordHashes(), {})
|
||||
|
||||
def test_get_statements_empty(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Informix
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestInformixEnum(_EnumBase):
|
||||
display_name = "Informix"
|
||||
dirname = "informix"
|
||||
|
||||
def test_search_db(self):
|
||||
self.assertEqual(self.handler.searchDb(), [])
|
||||
|
||||
def test_search_table(self):
|
||||
self.assertEqual(self.handler.searchTable(), [])
|
||||
|
||||
def test_search_column(self):
|
||||
self.assertEqual(self.handler.searchColumn(), [])
|
||||
|
||||
def test_get_statements(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Firebird
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFirebirdEnum(_EnumBase):
|
||||
display_name = "Firebird"
|
||||
dirname = "firebird"
|
||||
|
||||
def test_get_dbs_empty(self):
|
||||
self.assertEqual(self.handler.getDbs(), [])
|
||||
|
||||
def test_get_password_hashes_empty(self):
|
||||
self.assertEqual(self.handler.getPasswordHashes(), {})
|
||||
|
||||
def test_search_db_empty(self):
|
||||
self.assertEqual(self.handler.searchDb(), [])
|
||||
|
||||
def test_get_hostname(self):
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements_empty(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HSQLDB
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHSQLDBEnum(_EnumBase):
|
||||
display_name = "HSQLDB"
|
||||
dirname = "hsqldb"
|
||||
|
||||
def test_get_banner(self):
|
||||
conf.getBanner = True
|
||||
kb.data.banner = None
|
||||
# getValue returns a single-element LIST; getBanner pipes it through
|
||||
# unArrayizeValue, which must unwrap it to the scalar banner string.
|
||||
inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"]
|
||||
banner = self.handler.getBanner()
|
||||
self.assertEqual(banner, "HSQLDB 2.5.1")
|
||||
|
||||
def test_get_privileges_empty(self):
|
||||
self.assertEqual(self.handler.getPrivileges(), {})
|
||||
|
||||
def test_get_hostname(self):
|
||||
self.assertIsNone(self.handler.getHostname())
|
||||
|
||||
def test_get_statements_empty(self):
|
||||
self.assertEqual(self.handler.getStatements(), [])
|
||||
|
||||
def test_get_current_db_default_schema(self):
|
||||
from lib.core.settings import HSQLDB_DEFAULT_SCHEMA
|
||||
self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -56,7 +56,7 @@ class TestCheckDependencies(unittest.TestCase):
|
|||
# 'kinterbasdb' (Firebird driver) is essentially never installed, so the
|
||||
# probe must hit the except branch and emit a warning naming the library.
|
||||
try:
|
||||
import kinterbasdb # noqa: F401
|
||||
__import__("kinterbasdb")
|
||||
self.skipTest("kinterbasdb is unexpectedly installed")
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
|||
802
tests/test_entries.py
Normal file
802
tests/test_entries.py
Normal file
|
|
@ -0,0 +1,802 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Unit tests for plugins/generic/entries.py (Entries), exercising dumpTable /
|
||||
dumpAll / dumpFoundTables / dumpFoundColumn by MOCKING the injection layer
|
||||
(lib.request.inject.getValue) and the dumper.
|
||||
|
||||
No network and no DBMS are involved: conf.direct=True selects the simple inband
|
||||
branches, or conf.direct=False with a BOOLEAN injection state selects the
|
||||
inference (blind) branches; inject.getValue is patched to return canned rows in
|
||||
the exact shape the methods parse, and conf.dumper is replaced with a recording
|
||||
stub so we can assert on what each method produced (kb.data caches / returned
|
||||
dicts). Every test restores all touched conf.* / kb.* / patched module attributes
|
||||
in tearDown so nothing leaks.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
|
||||
bootstrap()
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.enums import EXPECTED, PAYLOAD
|
||||
|
||||
import plugins.generic.search as smod
|
||||
import plugins.generic.entries as emod
|
||||
import plugins.generic.custom as cmod
|
||||
import plugins.generic.misc as mmod
|
||||
from plugins.generic.entries import Entries
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Helpers/base from tests/test_search_enum.py (inband TestEntries)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _RecordingDumperSE(object):
|
||||
"""Minimal stand-in for conf.dumper that records calls instead of printing/writing."""
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.listed = [] # (header, elements)
|
||||
self.dbTablesArg = None
|
||||
self.dbColumnsArg = None
|
||||
self.dbTableColumnsArg = None
|
||||
self.tableValues = []
|
||||
|
||||
def lister(self, header, elements, content_type=None, sort=True):
|
||||
self.listed.append((header, list(elements) if elements else []))
|
||||
|
||||
def dbTables(self, dbTables):
|
||||
self.dbTablesArg = dbTables
|
||||
|
||||
def dbColumns(self, dbColumnsDict, colConsider, dbs):
|
||||
self.dbColumnsArg = (dbColumnsDict, colConsider, dbs)
|
||||
|
||||
def dbTableColumns(self, tableColumns, content_type=None):
|
||||
self.dbTableColumnsArg = tableColumns
|
||||
|
||||
def dbTableValues(self, tableValues):
|
||||
self.tableValues.append(tableValues)
|
||||
|
||||
|
||||
class _TestEntriesSE(Entries):
|
||||
"""Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables)."""
|
||||
|
||||
def __init__(self):
|
||||
Entries.__init__(self)
|
||||
self.getColumnsResult = {} # {db: {tbl: {col: type}}}
|
||||
self.getTablesResult = {} # value assigned to kb.data.cachedTables
|
||||
self.getColumnsCalls = []
|
||||
|
||||
def forceDbmsEnum(self):
|
||||
pass
|
||||
|
||||
def getCurrentDb(self):
|
||||
return "testdb"
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
self.getColumnsCalls.append((conf.db, conf.tbl))
|
||||
kb.data.cachedColumns = dict(self.getColumnsResult)
|
||||
|
||||
def getTables(self, bruteForce=None):
|
||||
kb.data.cachedTables = dict(self.getTablesResult)
|
||||
|
||||
|
||||
class _SearchEnumBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Save mutated globals
|
||||
self._saved_conf = {k: conf.get(k) for k in (
|
||||
"db", "tbl", "col", "direct", "excludeSysDbs", "exclude", "search",
|
||||
"disableHashing", "noKeyset", "keyset", "forcePivoting",
|
||||
)}
|
||||
self._saved_dumper = conf.get("dumper")
|
||||
self._search_getValue = smod.inject.getValue
|
||||
self._entries_getValue = emod.inject.getValue
|
||||
self._search_readInput = smod.readInput
|
||||
self._entries_readInput = emod.readInput
|
||||
self._saved_has_is = kb.data.get("has_information_schema")
|
||||
self._saved_cachedColumns = kb.data.get("cachedColumns")
|
||||
self._saved_cachedTables = kb.data.get("cachedTables")
|
||||
self._saved_dumpedTable = kb.data.get("dumpedTable")
|
||||
self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt")
|
||||
self._saved_permissionFlag = kb.get("permissionFlag")
|
||||
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
conf.excludeSysDbs = False
|
||||
conf.exclude = None
|
||||
conf.search = True
|
||||
conf.disableHashing = True
|
||||
conf.noKeyset = True
|
||||
conf.keyset = False
|
||||
conf.forcePivoting = False
|
||||
conf.dumper = _RecordingDumperSE()
|
||||
|
||||
kb.data.has_information_schema = True
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.dumpedTable = {}
|
||||
kb.dumpKeyboardInterrupt = False
|
||||
kb.permissionFlag = False
|
||||
|
||||
# Non-interactive prompts: collapse readInput to its default.
|
||||
def _readInput(message, default=None, checkBatch=True, boolean=False):
|
||||
if boolean:
|
||||
return True if (default in (None, 'Y', 'y', True)) else False
|
||||
return default
|
||||
smod.readInput = _readInput
|
||||
emod.readInput = _readInput
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
conf.dumper = self._saved_dumper
|
||||
smod.inject.getValue = self._search_getValue
|
||||
emod.inject.getValue = self._entries_getValue
|
||||
smod.readInput = self._search_readInput
|
||||
emod.readInput = self._entries_readInput
|
||||
kb.data.has_information_schema = self._saved_has_is
|
||||
kb.data.cachedColumns = self._saved_cachedColumns
|
||||
kb.data.cachedTables = self._saved_cachedTables
|
||||
kb.data.dumpedTable = self._saved_dumpedTable
|
||||
kb.dumpKeyboardInterrupt = self._saved_dumpKbInt
|
||||
kb.permissionFlag = self._saved_permissionFlag
|
||||
|
||||
|
||||
class TestEntries(_SearchEnumBase):
|
||||
def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")):
|
||||
e = _TestEntriesSE()
|
||||
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
|
||||
return e
|
||||
|
||||
# --- dumpTable: inband (conf.direct) ------------------------------------
|
||||
|
||||
def test_dump_table_inband_rows(self):
|
||||
e = self._entries_with_cols(cols=("id", "name"))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
# MySQL inband dump returns a list of [colVal, colVal] rows.
|
||||
emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]]
|
||||
|
||||
e.dumpTable()
|
||||
|
||||
dumped = conf.dumper.tableValues[-1]
|
||||
self.assertEqual(dumped["__infos__"]["count"], 2)
|
||||
self.assertEqual(dumped["__infos__"]["table"], "users")
|
||||
self.assertEqual(dumped["__infos__"]["db"], "testdb")
|
||||
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
|
||||
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
|
||||
|
||||
def test_dump_table_uses_foundData(self):
|
||||
e = _TestEntriesSE()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["x"]]
|
||||
foundData = {"testdb": {"users": {"id": "int"}}}
|
||||
|
||||
e.dumpTable(foundData=foundData)
|
||||
|
||||
# foundData short-circuits column discovery: getColumns must not run.
|
||||
self.assertEqual(e.getColumnsCalls, [])
|
||||
self.assertIn("id", conf.dumper.tableValues[-1])
|
||||
|
||||
def test_dump_table_no_columns_skips(self):
|
||||
e = _TestEntriesSE()
|
||||
e.getColumnsResult = {} # discovery yields nothing
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "ghost"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
|
||||
|
||||
e.dumpTable()
|
||||
# No columns => no values dumped.
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dump_table_empty_entries(self):
|
||||
e = self._entries_with_cols(cols=("id",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: None # no rows
|
||||
|
||||
e.dumpTable()
|
||||
# Nothing retrieved => dumpedTable empty => dbTableValues not called.
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dump_table_current_db(self):
|
||||
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
|
||||
conf.db = None # triggers getCurrentDb() -> "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["7"]]
|
||||
|
||||
e.dumpTable()
|
||||
self.assertEqual(conf.db, "testdb")
|
||||
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"])
|
||||
|
||||
def test_dump_table_multiple_db_error(self):
|
||||
e = _TestEntriesSE()
|
||||
conf.db = "a,b"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
from lib.core.exception import SqlmapMissingMandatoryOptionException
|
||||
self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable)
|
||||
|
||||
def test_dump_table_get_tables_when_no_tbl(self):
|
||||
e = _TestEntriesSE()
|
||||
e.getTablesResult = {"testdb": ["users"]}
|
||||
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
|
||||
conf.db = "testdb"
|
||||
conf.tbl = None
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["42"]]
|
||||
|
||||
e.dumpTable()
|
||||
# Tables were discovered via getTables, then the row dumped.
|
||||
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"])
|
||||
|
||||
# --- dumpAll: single-db delegation --------------------------------------
|
||||
|
||||
def test_dump_all_single_db_delegates(self):
|
||||
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
|
||||
# dumpAll with db set & tbl None must delegate straight to dumpTable.
|
||||
conf.db = "testdb"
|
||||
conf.tbl = None
|
||||
conf.col = None
|
||||
e.getTablesResult = {"testdb": ["users"]}
|
||||
emod.inject.getValue = lambda *a, **k: [["9"]]
|
||||
|
||||
e.dumpAll()
|
||||
self.assertTrue(conf.dumper.tableValues)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Helpers/base from tests/test_generic_more.py (inband dump branches)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _RecordingDumperGM(object):
|
||||
"""Recording stand-in for conf.dumper (no printing / file writing)."""
|
||||
|
||||
def __init__(self):
|
||||
self.tableValues = []
|
||||
self.sqlQueries = []
|
||||
|
||||
def dbTableValues(self, tableValues):
|
||||
self.tableValues.append(tableValues)
|
||||
|
||||
def sqlQuery(self, query, queryRes):
|
||||
self.sqlQueries.append((query, queryRes))
|
||||
|
||||
|
||||
class _TestEntriesGM(Entries):
|
||||
"""Entries with cross-mixin collaborators stubbed.
|
||||
|
||||
forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by
|
||||
sibling mixins; we emulate column/table discovery by populating kb.data.cached*
|
||||
from canned attributes, exactly as the production plugins do.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Entries.__init__(self)
|
||||
self.getColumnsResult = {} # assigned to kb.data.cachedColumns
|
||||
self.getTablesResult = {} # assigned to kb.data.cachedTables
|
||||
self.getColumnsCalls = []
|
||||
self.getTablesCalls = 0
|
||||
|
||||
def forceDbmsEnum(self):
|
||||
pass
|
||||
|
||||
def getCurrentDb(self):
|
||||
return "testdb"
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
self.getColumnsCalls.append((conf.db, conf.tbl))
|
||||
kb.data.cachedColumns = dict(self.getColumnsResult)
|
||||
|
||||
def getTables(self, bruteForce=None):
|
||||
self.getTablesCalls += 1
|
||||
kb.data.cachedTables = dict(self.getTablesResult)
|
||||
|
||||
|
||||
class _GenericBase(unittest.TestCase):
|
||||
"""Snapshot/restore for everything the generic mixins touch."""
|
||||
|
||||
_CONF_KEYS = (
|
||||
"db", "tbl", "col", "direct", "batch", "exclude", "search",
|
||||
"disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere",
|
||||
"tmpPath", "sqlQuery", "sqlFile", "regKey", "regVal", "regData",
|
||||
"regType", "osPwn", "osShell", "cleanup", "privEsc",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
|
||||
self._saved_dumper = conf.get("dumper")
|
||||
|
||||
self._saved_getValue = {
|
||||
emod: emod.inject.getValue,
|
||||
cmod: cmod.inject.getValue,
|
||||
mmod: mmod.inject.getValue,
|
||||
}
|
||||
self._saved_goStacked = {
|
||||
cmod: cmod.inject.goStacked,
|
||||
mmod: mmod.inject.goStacked,
|
||||
}
|
||||
self._saved_emod_readInput = emod.readInput
|
||||
self._saved_mmod_readInput = mmod.readInput
|
||||
|
||||
self._saved_kb = {
|
||||
"cachedColumns": kb.data.get("cachedColumns"),
|
||||
"cachedTables": kb.data.get("cachedTables"),
|
||||
"dumpedTable": kb.data.get("dumpedTable"),
|
||||
"has_information_schema": kb.data.get("has_information_schema"),
|
||||
"dumpKeyboardInterrupt": kb.get("dumpKeyboardInterrupt"),
|
||||
"permissionFlag": kb.get("permissionFlag"),
|
||||
"hintValue": kb.get("hintValue"),
|
||||
"injection_data": kb.injection.data,
|
||||
"bannerFp": kb.get("bannerFp"),
|
||||
"os": kb.get("os"),
|
||||
}
|
||||
self._saved_forceDbms = kb.get("forcedDbms")
|
||||
|
||||
conf.direct = True
|
||||
conf.batch = True
|
||||
conf.exclude = None
|
||||
conf.search = False
|
||||
conf.disableHashing = True
|
||||
conf.noKeyset = True
|
||||
conf.keyset = False
|
||||
conf.forcePivoting = False
|
||||
conf.dumpWhere = None
|
||||
conf.dumper = _RecordingDumperGM()
|
||||
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.dumpedTable = {}
|
||||
kb.data.has_information_schema = True
|
||||
kb.dumpKeyboardInterrupt = False
|
||||
kb.permissionFlag = False
|
||||
|
||||
def _readInput(message, default=None, checkBatch=True, boolean=False):
|
||||
if boolean:
|
||||
return default in (None, 'Y', 'y', True)
|
||||
return default
|
||||
|
||||
emod.readInput = _readInput
|
||||
mmod.readInput = _readInput
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
conf.dumper = self._saved_dumper
|
||||
|
||||
for mod, fn in self._saved_getValue.items():
|
||||
mod.inject.getValue = fn
|
||||
for mod, fn in self._saved_goStacked.items():
|
||||
mod.inject.goStacked = fn
|
||||
emod.readInput = self._saved_emod_readInput
|
||||
mmod.readInput = self._saved_mmod_readInput
|
||||
|
||||
kb.data.cachedColumns = self._saved_kb["cachedColumns"]
|
||||
kb.data.cachedTables = self._saved_kb["cachedTables"]
|
||||
kb.data.dumpedTable = self._saved_kb["dumpedTable"]
|
||||
kb.data.has_information_schema = self._saved_kb["has_information_schema"]
|
||||
kb.dumpKeyboardInterrupt = self._saved_kb["dumpKeyboardInterrupt"]
|
||||
kb.permissionFlag = self._saved_kb["permissionFlag"]
|
||||
kb.hintValue = self._saved_kb["hintValue"]
|
||||
kb.injection.data = self._saved_kb["injection_data"]
|
||||
kb.bannerFp = self._saved_kb["bannerFp"]
|
||||
kb.os = self._saved_kb["os"]
|
||||
kb.forcedDbms = self._saved_forceDbms
|
||||
|
||||
@staticmethod
|
||||
def _force_os(os_name):
|
||||
# Backend.setOs only assigns when kb.os is currently None; reset first so
|
||||
# tests can deterministically pin the back-end OS.
|
||||
kb.os = None
|
||||
Backend.setOs(os_name)
|
||||
|
||||
|
||||
class TestEntriesDumpTable(_GenericBase):
|
||||
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
|
||||
e = _TestEntriesGM()
|
||||
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
|
||||
return e
|
||||
|
||||
def test_exclude_filters_columns(self):
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id", "secret"))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
conf.exclude = "secret"
|
||||
emod.inject.getValue = lambda *a, **k: [["1"]]
|
||||
|
||||
e.dumpTable()
|
||||
dumped = conf.dumper.tableValues[-1]
|
||||
self.assertIn("id", dumped)
|
||||
self.assertNotIn("secret", dumped)
|
||||
|
||||
def test_exclude_all_columns_skips(self):
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("secret",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
conf.exclude = "secret"
|
||||
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
|
||||
|
||||
e.dumpTable()
|
||||
# all columns excluded => "no usable column names" => nothing dumped
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dumpwhere_rewrites_query(self):
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
conf.dumpWhere = "id>5"
|
||||
captured = {}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
captured["query"] = query
|
||||
return [["9"]]
|
||||
|
||||
emod.inject.getValue = gv
|
||||
e.dumpTable()
|
||||
# agent.whereQuery folds conf.dumpWhere into the dump query
|
||||
self.assertIn("id>5", captured["query"])
|
||||
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"])
|
||||
|
||||
def test_disablehashing_false_path(self):
|
||||
# conf.disableHashing False => attackDumpedTable() is invoked; with no
|
||||
# hashes present it must complete without raising and still emit values.
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id", "name"))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
conf.disableHashing = False
|
||||
emod.inject.getValue = lambda *a, **k: [["1", "alice"]]
|
||||
|
||||
# Spy on attackDumpedTable: with disableHashing False it MUST be invoked
|
||||
# after the values are dumped. A recorder replaces it so we can assert the
|
||||
# call happened (and no real dictionary attack runs).
|
||||
saved_attack = emod.attackDumpedTable
|
||||
calls = {"n": 0}
|
||||
emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1)
|
||||
try:
|
||||
e.dumpTable()
|
||||
finally:
|
||||
emod.attackDumpedTable = saved_attack
|
||||
|
||||
self.assertEqual(calls["n"], 1)
|
||||
self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1)
|
||||
|
||||
def test_missing_columns_skips_table(self):
|
||||
# getColumns yields nothing for the targeted table => skip without fetching.
|
||||
set_dbms("MySQL")
|
||||
e = _TestEntriesGM()
|
||||
e.getColumnsResult = {"testdb": {"other": {"id": "int"}}}
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
|
||||
|
||||
e.dumpTable()
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_multiple_tables_one_dumped(self):
|
||||
set_dbms("MySQL")
|
||||
e = _TestEntriesGM()
|
||||
e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}}
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users,posts"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["1"]]
|
||||
|
||||
e.dumpTable()
|
||||
# both tables share the same cachedColumns dict => both dumped
|
||||
tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues]
|
||||
self.assertIn("users", tables)
|
||||
self.assertIn("posts", tables)
|
||||
|
||||
def test_metadb_suffix_db(self):
|
||||
# A db whose name carries the METADB_SUFFIX must not get a "db" prefix in
|
||||
# kb.dumpTable, and dumping still succeeds.
|
||||
from lib.core.settings import METADB_SUFFIX
|
||||
set_dbms("MySQL")
|
||||
metadb = "x%s" % METADB_SUFFIX
|
||||
e = self._entries(db=metadb, tbl="t", cols=("c",))
|
||||
conf.db = metadb
|
||||
conf.tbl = "t"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["v"]]
|
||||
|
||||
e.dumpTable()
|
||||
self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"])
|
||||
|
||||
|
||||
class TestEntriesDumpAll(_GenericBase):
|
||||
def test_dumpall_multiple_dbs_tables(self):
|
||||
set_dbms("MySQL")
|
||||
e = _TestEntriesGM()
|
||||
conf.db = None
|
||||
conf.tbl = None
|
||||
conf.col = None
|
||||
e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]}
|
||||
# dumpTable re-discovers columns per (db, tbl); supply both.
|
||||
e.getColumnsResult = {
|
||||
"db1": {"t1": {"a": "int"}},
|
||||
"db2": {"t2": {"b": "int"}},
|
||||
}
|
||||
emod.inject.getValue = lambda *a, **k: [["x"]]
|
||||
|
||||
e.dumpAll()
|
||||
# Every table contributed a values batch.
|
||||
self.assertEqual(len(conf.dumper.tableValues), 2)
|
||||
|
||||
def test_dumpall_list_cached_tables(self):
|
||||
# cachedTables as a bare list => wrapped under {None: [...]}.
|
||||
set_dbms("MySQL")
|
||||
e = _TestEntriesGM()
|
||||
conf.db = None
|
||||
conf.tbl = None
|
||||
conf.col = None
|
||||
|
||||
# getTables sets cachedTables; emulate the list shape directly.
|
||||
class _ListTables(_TestEntriesGM):
|
||||
def getTables(self_inner, bruteForce=None):
|
||||
kb.data.cachedTables = ["users"]
|
||||
|
||||
e = _ListTables()
|
||||
# dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the
|
||||
# None db via getCurrentDb() -> "testdb", so columns live under "testdb".
|
||||
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
|
||||
emod.inject.getValue = lambda *a, **k: [["1"]]
|
||||
|
||||
e.dumpAll()
|
||||
self.assertTrue(conf.dumper.tableValues)
|
||||
# The bare-list None db must be resolved via getCurrentDb() -> "testdb"
|
||||
# before the dump; assert the dumped __infos__ carries the real db (not
|
||||
# None) for the requested "users" table.
|
||||
infos = conf.dumper.tableValues[-1]["__infos__"]
|
||||
self.assertEqual(infos["db"], "testdb")
|
||||
self.assertEqual(infos["table"], "users")
|
||||
|
||||
def test_dumpall_exclude_skips_table(self):
|
||||
set_dbms("MySQL")
|
||||
e = _TestEntriesGM()
|
||||
conf.db = None
|
||||
conf.tbl = None
|
||||
conf.col = None
|
||||
conf.exclude = "secret"
|
||||
e.getTablesResult = {"db1": ["secret", "users"]}
|
||||
e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}}
|
||||
emod.inject.getValue = lambda *a, **k: [["1"]]
|
||||
|
||||
e.dumpAll()
|
||||
tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues]
|
||||
self.assertIn("users", tables)
|
||||
self.assertNotIn("secret", tables)
|
||||
|
||||
|
||||
class TestEntriesDumpFound(_GenericBase):
|
||||
def _entries(self):
|
||||
e = _TestEntriesGM()
|
||||
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
|
||||
return e
|
||||
|
||||
def test_dump_found_tables_yes_all(self):
|
||||
set_dbms("MySQL")
|
||||
e = self._entries()
|
||||
emod.inject.getValue = lambda *a, **k: [["1"]]
|
||||
# batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices.
|
||||
e.dumpFoundTables({"testdb": ["users"]})
|
||||
self.assertTrue(conf.dumper.tableValues)
|
||||
# The interactive selection must dump the REQUESTED db/table, not just
|
||||
# "something": assert the dumped __infos__ maps to testdb.users.
|
||||
infos = conf.dumper.tableValues[-1]["__infos__"]
|
||||
self.assertEqual(infos["db"], "testdb")
|
||||
self.assertEqual(infos["table"], "users")
|
||||
|
||||
def test_dump_found_tables_declined(self):
|
||||
set_dbms("MySQL")
|
||||
e = self._entries()
|
||||
|
||||
def _no(message, default=None, checkBatch=True, boolean=False):
|
||||
if boolean:
|
||||
return False
|
||||
return default
|
||||
|
||||
emod.readInput = _no
|
||||
emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined")
|
||||
e.dumpFoundTables({"testdb": ["users"]})
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dump_found_column_yes_all(self):
|
||||
set_dbms("MySQL")
|
||||
e = self._entries()
|
||||
emod.inject.getValue = lambda *a, **k: [["1"]]
|
||||
dbs = {"testdb": {"users": {"id": "int"}}}
|
||||
e.dumpFoundColumn(dbs, foundCols=None, colConsider='1')
|
||||
self.assertTrue(conf.dumper.tableValues)
|
||||
# The selection must dump the REQUESTED db/table mapping, not just
|
||||
# "something": assert the dumped __infos__ maps to testdb.users.
|
||||
infos = conf.dumper.tableValues[-1]["__infos__"]
|
||||
self.assertEqual(infos["db"], "testdb")
|
||||
self.assertEqual(infos["table"], "users")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Helpers/base from tests/test_generic_enum_more.py (inference branches)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _RecordingDumperInf(object):
|
||||
def __init__(self):
|
||||
self.tableValues = []
|
||||
|
||||
def dbTableValues(self, tableValues):
|
||||
self.tableValues.append(tableValues)
|
||||
|
||||
|
||||
class _TestEntriesInf(Entries):
|
||||
def __init__(self):
|
||||
Entries.__init__(self)
|
||||
self.getColumnsResult = {}
|
||||
self.getTablesResult = {}
|
||||
|
||||
def forceDbmsEnum(self):
|
||||
pass
|
||||
|
||||
def getCurrentDb(self):
|
||||
return "testdb"
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
kb.data.cachedColumns = dict(self.getColumnsResult)
|
||||
|
||||
def getTables(self, bruteForce=None):
|
||||
kb.data.cachedTables = dict(self.getTablesResult)
|
||||
|
||||
|
||||
class _EntriesBase(unittest.TestCase):
|
||||
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search",
|
||||
"disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere")
|
||||
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
|
||||
self._saved_dumper = conf.get("dumper")
|
||||
self._gv = emod.inject.getValue
|
||||
self._cbe = emod.inject.checkBooleanExpression
|
||||
self._readInput = emod.readInput
|
||||
self._saved_has_is = kb.data.get("has_information_schema")
|
||||
self._saved_cachedColumns = kb.data.get("cachedColumns")
|
||||
self._saved_cachedTables = kb.data.get("cachedTables")
|
||||
self._saved_dumpedTable = kb.data.get("dumpedTable")
|
||||
self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt")
|
||||
self._saved_permissionFlag = kb.get("permissionFlag")
|
||||
self._saved_injection_data = kb.injection.data
|
||||
|
||||
set_dbms("MySQL")
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
conf.exclude = None
|
||||
conf.search = False
|
||||
conf.disableHashing = True
|
||||
conf.noKeyset = True
|
||||
conf.keyset = False
|
||||
conf.forcePivoting = False
|
||||
conf.dumpWhere = None
|
||||
conf.dumper = _RecordingDumperInf()
|
||||
|
||||
kb.data.has_information_schema = True
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.dumpedTable = {}
|
||||
kb.dumpKeyboardInterrupt = False
|
||||
kb.permissionFlag = False
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None))
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
conf.dumper = self._saved_dumper
|
||||
emod.inject.getValue = self._gv
|
||||
emod.inject.checkBooleanExpression = self._cbe
|
||||
emod.readInput = self._readInput
|
||||
kb.data.has_information_schema = self._saved_has_is
|
||||
kb.data.cachedColumns = self._saved_cachedColumns
|
||||
kb.data.cachedTables = self._saved_cachedTables
|
||||
kb.data.dumpedTable = self._saved_dumpedTable
|
||||
kb.dumpKeyboardInterrupt = self._saved_dumpKbInt
|
||||
kb.permissionFlag = self._saved_permissionFlag
|
||||
kb.injection.data = self._saved_injection_data
|
||||
|
||||
|
||||
class TestEntriesInference(_EntriesBase):
|
||||
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
|
||||
e = _TestEntriesInf()
|
||||
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
|
||||
return e
|
||||
|
||||
def test_dump_table_inference_column_pivot(self):
|
||||
# Blind dump (conf.direct=False, BOOLEAN available): a row count, then one
|
||||
# value per (index, column). Assert the per-column pivoted values match.
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id", "name"))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
|
||||
# data[index][column] -> value. 2 rows, columns id/name.
|
||||
data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return "2" # row count
|
||||
# MySQL blind cell query: 'SELECT <col> FROM testdb.users ORDER BY ...
|
||||
# LIMIT <index>,1'. The row index is the LIMIT offset; the column is the
|
||||
# SELECT projection.
|
||||
import re as _re
|
||||
idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1))
|
||||
proj = query.split(" FROM ", 1)[0]
|
||||
col = "name" if "name" in proj else "id"
|
||||
return data[idx][col]
|
||||
|
||||
emod.inject.getValue = gv
|
||||
e.dumpTable()
|
||||
dumped = conf.dumper.tableValues[-1]
|
||||
self.assertEqual(dumped["__infos__"]["count"], 2)
|
||||
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
|
||||
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
|
||||
|
||||
def test_dump_table_inference_empty_table(self):
|
||||
# A zero row count in the inference path yields empty per-column value
|
||||
# lists and no dbTableValues emission (dumpedTable stays effectively empty).
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
|
||||
emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table"))
|
||||
e.dumpTable()
|
||||
# count 0 => empty entries => nothing dumped
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dump_table_inference_count_failure_skips(self):
|
||||
# A non-numeric count in the inference path => the table is skipped with a
|
||||
# warning, no values dumped.
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return None # count failed
|
||||
self.fail("must not fetch cells when count failed")
|
||||
|
||||
emod.inject.getValue = gv
|
||||
e.dumpTable()
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
|
|||
|
|
@ -1,865 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Additional unit tests for the generic enumeration mixins, deliberately targeting
|
||||
branches NOT already exercised by tests/test_databases_enum.py,
|
||||
tests/test_users_enum.py, tests/test_search_enum.py and tests/test_generic_more.py
|
||||
(which cover the conf.direct INBAND happy paths).
|
||||
|
||||
This file drives the OTHER branches:
|
||||
|
||||
* plugins/generic/databases.py - the INFERENCE paths (conf.direct=False +
|
||||
isInferenceAvailable via kb.injection BOOLEAN state: count -> per-row getValue),
|
||||
the MSSQL inband-paging fallback in getDbs(), getColumns onlyColNames / dumpMode,
|
||||
the getColumns MySQL<5 / ACCESS bruteforce fallback, getCount over cachedTables,
|
||||
and getStatements/getProcedures empty/none branches.
|
||||
* plugins/generic/users.py - getPrivileges role/grant parsing per DBMS in BOTH the
|
||||
inband path (PGSQL digit columns, MySQL<5 Y/N, Firebird letters, DB2 grant codes)
|
||||
and the INFERENCE path (count then per-index privilege), getPasswordHashes
|
||||
grouping/dedup in the inference path, getUsers inference, isDba MSSQL.
|
||||
* plugins/generic/entries.py - dumpTable INFERENCE path (count -> column-pivot via
|
||||
per-(index,column) getValue), the empty-table branch, the count-failure skip,
|
||||
and the resolveKeysetCursor disabling via conf.noKeyset.
|
||||
* plugins/generic/search.py - searchDb / searchTable / searchColumn INFERENCE
|
||||
paths (count then per-index getValue), and the MySQL<5 bruteforce branch of
|
||||
searchTable / searchColumn.
|
||||
|
||||
Recipe (proven in tests/test_databases_enum.py): patch the module's inject.getValue
|
||||
with canned rows in the EXACT shape the branch parses; for inference branches return
|
||||
a positive int for EXPECTED.INT count calls then the per-row/per-index values; set the
|
||||
needed kb.data flags; assert the exact resulting structure (sorted lists,
|
||||
{db:{tbl:{col:type}}} dicts, privilege sets, dumpedTable values).
|
||||
|
||||
CRITICAL STATE HYGIENE: every test snapshots and restores conf.*, the patched
|
||||
inject.getValue (per module), kb.data.cached*, kb.hintValue, kb.injection.data,
|
||||
Backend/forcedDbms in tearDown so nothing leaks into the rest of the suite.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.enums import EXPECTED, PAYLOAD
|
||||
|
||||
import plugins.generic.databases as dbmod
|
||||
import plugins.generic.users as umod
|
||||
import plugins.generic.search as smod
|
||||
import plugins.generic.entries as emod
|
||||
from plugins.generic.databases import Databases
|
||||
from plugins.generic.users import Users
|
||||
from plugins.generic.search import Search
|
||||
from plugins.generic.entries import Entries
|
||||
|
||||
_NOOP = lambda self: None
|
||||
|
||||
|
||||
def _inference_gv(count, sequence):
|
||||
"""Build an inject.getValue stub for blind inference branches.
|
||||
|
||||
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
|
||||
yields the next item from `sequence` wrapped as a single-cell row ([value]),
|
||||
cycling if exhausted. This mirrors the count-then-per-row contract of every
|
||||
isInferenceAvailable() branch.
|
||||
"""
|
||||
state = {"i": 0}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return str(count)
|
||||
val = sequence[state["i"] % len(sequence)]
|
||||
state["i"] += 1
|
||||
return [val]
|
||||
|
||||
return gv
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# databases.py
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _DbBase(unittest.TestCase):
|
||||
_CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude",
|
||||
"getComments", "excludeSysDbs", "search", "freshQueries")
|
||||
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
|
||||
self._saved_getValue = dbmod.inject.getValue
|
||||
self._saved_checkBool = dbmod.inject.checkBooleanExpression
|
||||
self._saved_injection_data = kb.injection.data
|
||||
self._saved_has_is = kb.data.get("has_information_schema")
|
||||
self._saved_hintValue = kb.get("hintValue")
|
||||
self._saved_choices = dict(kb.choices)
|
||||
self._saved_readInput = dbmod.readInput
|
||||
self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None)
|
||||
Databases.forceDbmsEnum = _NOOP
|
||||
|
||||
conf.getComments = False
|
||||
conf.excludeSysDbs = False
|
||||
conf.exclude = None
|
||||
conf.search = False
|
||||
conf.freshQueries = False
|
||||
conf.col = None
|
||||
kb.data.has_information_schema = True
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
dbmod.inject.getValue = self._saved_getValue
|
||||
dbmod.inject.checkBooleanExpression = self._saved_checkBool
|
||||
dbmod.readInput = self._saved_readInput
|
||||
kb.injection.data = self._saved_injection_data
|
||||
kb.data.has_information_schema = self._saved_has_is
|
||||
kb.hintValue = self._saved_hintValue
|
||||
kb.choices.clear()
|
||||
kb.choices.update(self._saved_choices)
|
||||
if self._saved_forceDbmsEnum is not None:
|
||||
Databases.forceDbmsEnum = self._saved_forceDbmsEnum
|
||||
else:
|
||||
try:
|
||||
del Databases.forceDbmsEnum
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def _fresh(self):
|
||||
d = Databases()
|
||||
kb.data.currentDb = ""
|
||||
kb.data.cachedDbs = []
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedCounts = {}
|
||||
kb.data.cachedStatements = []
|
||||
kb.data.cachedProcedures = []
|
||||
return d
|
||||
|
||||
def _inference(self):
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
|
||||
class TestDatabasesInference(_DbBase):
|
||||
def test_get_columns_inference_pgsql_types(self):
|
||||
# Blind column enumeration on PostgreSQL: a count, then for each index a
|
||||
# column name followed by its type. Assert the {db:{tbl:{col:type}}} parse.
|
||||
set_dbms("PostgreSQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
conf.db = "public"
|
||||
conf.tbl = "users"
|
||||
|
||||
names = ["id", "email"]
|
||||
state = {"i": 0, "name": True}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return str(len(names))
|
||||
if state["name"]:
|
||||
val = names[state["i"] % len(names)]
|
||||
state["i"] += 1
|
||||
state["name"] = False
|
||||
return [val]
|
||||
state["name"] = True
|
||||
return ["integer"]
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
result = d.getColumns()
|
||||
cols = result["public"]["users"]
|
||||
self.assertEqual(len(cols), 2)
|
||||
self.assertEqual(cols.get("id"), "integer")
|
||||
|
||||
def test_get_columns_inference_dump_mode_collist(self):
|
||||
# dumpMode with an explicit conf.col list: in the inference branch the
|
||||
# columns are taken straight from colList (no count/type queries at all)
|
||||
# and stored with value None. Asserting no getValue ran proves the
|
||||
# dump-mode shortcut, not a network round-trip.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = "id,name"
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("dumpMode+colList must not query in inference branch")
|
||||
|
||||
dbmod.inject.getValue = boom
|
||||
result = d.getColumns(dumpMode=True)
|
||||
cols = result["testdb"]["users"]
|
||||
# "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it;
|
||||
# both columns must be present (count, since exact key varies by quoting).
|
||||
self.assertEqual(len(cols), 2)
|
||||
self.assertIn("id", cols)
|
||||
self.assertIsNone(cols.get("id"))
|
||||
|
||||
def test_get_count_over_cached_tables_inference(self):
|
||||
# getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount.
|
||||
# Drive the inband table fetch + per-table count and assert the
|
||||
# {db:{count:[tables]}} grouping (tables sharing a count are grouped).
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = None
|
||||
kb.data.cachedTables = {"testdb": ["users", "posts"]}
|
||||
|
||||
counts = {"users": "5", "posts": "5"}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
for t, c in counts.items():
|
||||
if t in query:
|
||||
return c
|
||||
return "0"
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
result = d.getCount()
|
||||
# both tables have count 5 -> grouped under the same key
|
||||
self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"])
|
||||
|
||||
def test_get_statements_count_zero_returns_empty(self):
|
||||
# Inference path: a zero count short-circuits to the (empty) cache.
|
||||
set_dbms("PostgreSQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
# getStatements compares the count with the int literal 0 (count == 0), so
|
||||
# the count stub must return an int 0 (not "0") to take the empty branch.
|
||||
dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0")
|
||||
result = d.getStatements()
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_get_procedures_inference(self):
|
||||
set_dbms("PostgreSQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"])
|
||||
result = d.getProcedures()
|
||||
self.assertEqual(sorted(result), ["sp_a", "sp_b"])
|
||||
|
||||
def test_get_dbs_mssql_inband_paging(self):
|
||||
# MSSQL with no rows from the primary query falls into the query2 paging
|
||||
# loop (one indexed query per db until a blank value stops it).
|
||||
set_dbms("Microsoft SQL Server")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
dbs = ["master", "model"]
|
||||
|
||||
def gv(query, *a, **k):
|
||||
# The primary inband query is 'SELECT name FROM master..sysdatabases'
|
||||
# (no DB_NAME); make it return nothing so getDbs falls into the
|
||||
# 'SELECT DB_NAME(<index>)' paging loop (query2).
|
||||
if "DB_NAME" not in query:
|
||||
return None
|
||||
import re as _re
|
||||
idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0])
|
||||
return dbs[idx] if idx < len(dbs) else ""
|
||||
|
||||
dbmod.inject.getValue = gv
|
||||
result = d.getDbs()
|
||||
self.assertEqual(sorted(result), ["master", "model"])
|
||||
|
||||
def test_get_tables_inference_grouped_per_db(self):
|
||||
# Blind table enumeration: count for the db, then one table name per index.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
d = self._fresh()
|
||||
conf.db = "shop"
|
||||
conf.tbl = None
|
||||
dbmod.inject.getValue = _inference_gv(2, ["orders", "items"])
|
||||
result = d.getTables()
|
||||
self.assertIn("shop", result)
|
||||
self.assertEqual(sorted(result["shop"]), ["items", "orders"])
|
||||
|
||||
|
||||
class TestDatabasesBruteForce(_DbBase):
|
||||
def test_get_columns_mysql_lt5_bruteforce_decline(self):
|
||||
# MySQL < 5 (no information_schema) forces bruteForce in getColumns; with
|
||||
# the common-column-existence prompt answered 'N' it returns None without
|
||||
# issuing any column query.
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
kb.data.has_information_schema = False
|
||||
kb.choices.columnExists = None
|
||||
dbmod.readInput = lambda *a, **k: "N"
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("bruteForce decline must not query columns")
|
||||
|
||||
dbmod.inject.getValue = boom
|
||||
result = d.getColumns()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_columns_bruteforce_dumpmode_collist_on_decline(self):
|
||||
# bruteForce + decline + dumpMode + colList: the columns from colList are
|
||||
# stored with None type (the dump-mode salvage branch), not dropped.
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
d = self._fresh()
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = "a,b"
|
||||
kb.data.has_information_schema = False
|
||||
kb.choices.columnExists = None
|
||||
dbmod.readInput = lambda *a, **k: "N"
|
||||
dbmod.inject.getValue = lambda *a, **k: None
|
||||
result = d.getColumns(dumpMode=True)
|
||||
cols = result["testdb"]["users"]
|
||||
self.assertEqual(sorted(cols.keys()), ["a", "b"])
|
||||
self.assertIsNone(cols.get("a"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# users.py
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _UsersBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._direct = conf.direct
|
||||
self._technique = conf.technique
|
||||
self._user = conf.user
|
||||
self._gv = umod.inject.getValue
|
||||
self._cbe = umod.inject.checkBooleanExpression
|
||||
self._store = umod.storeHashesToFile
|
||||
self._attack = umod.attackCachedUsersPasswords
|
||||
self._readInput = umod.readInput
|
||||
self._his = kb.data.get("has_information_schema")
|
||||
self._injection_data = kb.injection.data
|
||||
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
conf.user = None
|
||||
kb.data.has_information_schema = True
|
||||
|
||||
umod.storeHashesToFile = lambda *a, **k: None
|
||||
umod.attackCachedUsersPasswords = lambda *a, **k: None
|
||||
umod.readInput = lambda *a, **k: "N"
|
||||
|
||||
def tearDown(self):
|
||||
conf.direct = self._direct
|
||||
conf.technique = self._technique
|
||||
conf.user = self._user
|
||||
umod.inject.getValue = self._gv
|
||||
umod.inject.checkBooleanExpression = self._cbe
|
||||
umod.storeHashesToFile = self._store
|
||||
umod.attackCachedUsersPasswords = self._attack
|
||||
umod.readInput = self._readInput
|
||||
kb.injection.data = self._injection_data
|
||||
if self._his is None:
|
||||
kb.data.pop("has_information_schema", None)
|
||||
else:
|
||||
kb.data.has_information_schema = self._his
|
||||
|
||||
def _inference(self):
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
|
||||
class TestUsersPrivilegesInband(_UsersBase):
|
||||
def test_privileges_pgsql_multiple_digit_columns(self):
|
||||
# PostgreSQL: privilege columns are digit flags; a column index maps to
|
||||
# PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1,
|
||||
# leave the rest 0; assert exactly those two privileges are parsed and that
|
||||
# "super" makes the user an admin.
|
||||
set_dbms("PostgreSQL")
|
||||
from lib.core.dicts import PGSQL_PRIVS
|
||||
ncols = max(PGSQL_PRIVS.keys())
|
||||
row = ["pguser"] + ["0"] * ncols
|
||||
row[1] = "1" # createdb
|
||||
row[2] = "1" # super
|
||||
umod.inject.getValue = lambda query, *a, **k: [row]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]})
|
||||
self.assertIn("pguser", areAdmins)
|
||||
|
||||
def test_privileges_mysql_lt5_yn_flags(self):
|
||||
# MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags
|
||||
# mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv.
|
||||
set_dbms("MySQL")
|
||||
from lib.core.dicts import MYSQL_PRIVS
|
||||
kb.data.has_information_schema = False
|
||||
ncols = max(MYSQL_PRIVS.keys())
|
||||
row = ["root"] + ["N"] * ncols
|
||||
row[1] = "Y" # select_priv
|
||||
row[3] = "Y" # update_priv
|
||||
umod.inject.getValue = lambda query, *a, **k: [row]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertIn(MYSQL_PRIVS[1], privileges["root"])
|
||||
self.assertIn(MYSQL_PRIVS[3], privileges["root"])
|
||||
self.assertNotIn(MYSQL_PRIVS[2], privileges["root"])
|
||||
|
||||
def test_privileges_firebird_letter_codes(self):
|
||||
# Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS.
|
||||
set_dbms("Firebird")
|
||||
from lib.core.dicts import FIREBIRD_PRIVS
|
||||
umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertEqual(set(privileges["fbuser"]),
|
||||
{FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]})
|
||||
|
||||
def test_privileges_db2_grant_codes(self):
|
||||
# DB2: privilege string is "<name>,<grant-letters>"; each 'Y'/'G' letter at
|
||||
# position i appends the DB2_PRIVS[i] name to the privilege.
|
||||
set_dbms("DB2")
|
||||
from lib.core.dicts import DB2_PRIVS
|
||||
conf.user = "db2admin"
|
||||
# "DBADM" plus a grant string whose first letter (position 1) is 'Y' ->
|
||||
# DB2_PRIVS[1] ("CONTROLAUTH") is appended.
|
||||
umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
joined = " ".join(privileges["DB2ADMIN"])
|
||||
self.assertIn("DBADM", joined)
|
||||
self.assertIn(DB2_PRIVS[1], joined)
|
||||
|
||||
|
||||
class TestUsersPrivilegesInference(_UsersBase):
|
||||
def test_privileges_inference_mysql(self):
|
||||
# Blind privilege enumeration for a named user: count, then one privilege
|
||||
# string per index. MySQL >= 5 adds each verbatim.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
conf.user = "root"
|
||||
privs = ["SELECT", "SUPER"]
|
||||
umod.inject.getValue = _inference_gv(2, privs)
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
# the user key is wildcard-wrapped for the MySQL information_schema LIKE
|
||||
key = [k for k in privileges if "root" in k][0]
|
||||
self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"})
|
||||
self.assertTrue(areAdmins) # SUPER => admin
|
||||
|
||||
def test_privileges_inference_oracle(self):
|
||||
set_dbms("Oracle")
|
||||
self._inference()
|
||||
conf.user = "system"
|
||||
umod.inject.getValue = _inference_gv(1, ["DBA"])
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertIn("SYSTEM", privileges)
|
||||
self.assertEqual(privileges["SYSTEM"], ["DBA"])
|
||||
self.assertIn("SYSTEM", areAdmins)
|
||||
|
||||
|
||||
class TestUsersPasswordHashesInference(_UsersBase):
|
||||
def test_password_hashes_inference_grouping(self):
|
||||
# Blind password-hash enumeration for two users: per-user count, then one
|
||||
# hash per index. Assert each user maps to its own hash list.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
conf.user = "root,guest"
|
||||
|
||||
# per-user single hash; count is 1 for every user
|
||||
hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return "1"
|
||||
for u, h in hashes.items():
|
||||
if u in query:
|
||||
return [h]
|
||||
return [None]
|
||||
|
||||
umod.inject.getValue = gv
|
||||
users = Users()
|
||||
kb.data.cachedUsersPasswords = {}
|
||||
res = users.getPasswordHashes()
|
||||
self.assertEqual(res["root"], ["*ROOTHASH"])
|
||||
self.assertEqual(res["guest"], ["*GUESTHASH"])
|
||||
|
||||
def test_password_hashes_inference_dedup(self):
|
||||
# The same hash returned twice for a user must be de-duplicated at the end
|
||||
# (kb.data.cachedUsersPasswords[user] = list(set(...))).
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
conf.user = "root"
|
||||
umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"])
|
||||
users = Users()
|
||||
kb.data.cachedUsersPasswords = {}
|
||||
res = users.getPasswordHashes()
|
||||
self.assertEqual(res["root"], ["*DUP"])
|
||||
|
||||
|
||||
class TestUsersGetUsersInference(_UsersBase):
|
||||
def test_get_users_inference(self):
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"])
|
||||
users = Users()
|
||||
kb.data.cachedUsers = []
|
||||
res = users.getUsers()
|
||||
self.assertEqual(sorted(res), ["guest@%", "root@localhost"])
|
||||
|
||||
def test_is_dba_mssql(self):
|
||||
# MSSQL isDba goes through the generic checkBooleanExpression branch.
|
||||
set_dbms("Microsoft SQL Server")
|
||||
umod.inject.checkBooleanExpression = lambda query, *a, **k: True
|
||||
users = Users()
|
||||
kb.data.isDba = None
|
||||
self.assertTrue(users.isDba())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# entries.py - inference (blind) dump path
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _RecordingDumper(object):
|
||||
def __init__(self):
|
||||
self.tableValues = []
|
||||
|
||||
def dbTableValues(self, tableValues):
|
||||
self.tableValues.append(tableValues)
|
||||
|
||||
|
||||
class _TestEntries(Entries):
|
||||
def __init__(self):
|
||||
Entries.__init__(self)
|
||||
self.getColumnsResult = {}
|
||||
self.getTablesResult = {}
|
||||
|
||||
def forceDbmsEnum(self):
|
||||
pass
|
||||
|
||||
def getCurrentDb(self):
|
||||
return "testdb"
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
kb.data.cachedColumns = dict(self.getColumnsResult)
|
||||
|
||||
def getTables(self, bruteForce=None):
|
||||
kb.data.cachedTables = dict(self.getTablesResult)
|
||||
|
||||
|
||||
class _EntriesBase(unittest.TestCase):
|
||||
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search",
|
||||
"disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere")
|
||||
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
|
||||
self._saved_dumper = conf.get("dumper")
|
||||
self._gv = emod.inject.getValue
|
||||
self._cbe = emod.inject.checkBooleanExpression
|
||||
self._readInput = emod.readInput
|
||||
self._saved_has_is = kb.data.get("has_information_schema")
|
||||
self._saved_cachedColumns = kb.data.get("cachedColumns")
|
||||
self._saved_cachedTables = kb.data.get("cachedTables")
|
||||
self._saved_dumpedTable = kb.data.get("dumpedTable")
|
||||
self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt")
|
||||
self._saved_permissionFlag = kb.get("permissionFlag")
|
||||
self._saved_injection_data = kb.injection.data
|
||||
|
||||
set_dbms("MySQL")
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
conf.exclude = None
|
||||
conf.search = False
|
||||
conf.disableHashing = True
|
||||
conf.noKeyset = True
|
||||
conf.keyset = False
|
||||
conf.forcePivoting = False
|
||||
conf.dumpWhere = None
|
||||
conf.dumper = _RecordingDumper()
|
||||
|
||||
kb.data.has_information_schema = True
|
||||
kb.data.cachedColumns = {}
|
||||
kb.data.cachedTables = {}
|
||||
kb.data.dumpedTable = {}
|
||||
kb.dumpKeyboardInterrupt = False
|
||||
kb.permissionFlag = False
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None))
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
conf.dumper = self._saved_dumper
|
||||
emod.inject.getValue = self._gv
|
||||
emod.inject.checkBooleanExpression = self._cbe
|
||||
emod.readInput = self._readInput
|
||||
kb.data.has_information_schema = self._saved_has_is
|
||||
kb.data.cachedColumns = self._saved_cachedColumns
|
||||
kb.data.cachedTables = self._saved_cachedTables
|
||||
kb.data.dumpedTable = self._saved_dumpedTable
|
||||
kb.dumpKeyboardInterrupt = self._saved_dumpKbInt
|
||||
kb.permissionFlag = self._saved_permissionFlag
|
||||
kb.injection.data = self._saved_injection_data
|
||||
|
||||
|
||||
class TestEntriesInference(_EntriesBase):
|
||||
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
|
||||
e = _TestEntries()
|
||||
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
|
||||
return e
|
||||
|
||||
def test_dump_table_inference_column_pivot(self):
|
||||
# Blind dump (conf.direct=False, BOOLEAN available): a row count, then one
|
||||
# value per (index, column). Assert the per-column pivoted values match.
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id", "name"))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
|
||||
# data[index][column] -> value. 2 rows, columns id/name.
|
||||
data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return "2" # row count
|
||||
# MySQL blind cell query: 'SELECT <col> FROM testdb.users ORDER BY ...
|
||||
# LIMIT <index>,1'. The row index is the LIMIT offset; the column is the
|
||||
# SELECT projection.
|
||||
import re as _re
|
||||
idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1))
|
||||
proj = query.split(" FROM ", 1)[0]
|
||||
col = "name" if "name" in proj else "id"
|
||||
return data[idx][col]
|
||||
|
||||
emod.inject.getValue = gv
|
||||
e.dumpTable()
|
||||
dumped = conf.dumper.tableValues[-1]
|
||||
self.assertEqual(dumped["__infos__"]["count"], 2)
|
||||
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
|
||||
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
|
||||
|
||||
def test_dump_table_inference_empty_table(self):
|
||||
# A zero row count in the inference path yields empty per-column value
|
||||
# lists and no dbTableValues emission (dumpedTable stays effectively empty).
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
|
||||
emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table"))
|
||||
e.dumpTable()
|
||||
# count 0 => empty entries => nothing dumped
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dump_table_inference_count_failure_skips(self):
|
||||
# A non-numeric count in the inference path => the table is skipped with a
|
||||
# warning, no values dumped.
|
||||
set_dbms("MySQL")
|
||||
e = self._entries(cols=("id",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return None # count failed
|
||||
self.fail("must not fetch cells when count failed")
|
||||
|
||||
emod.inject.getValue = gv
|
||||
e.dumpTable()
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# search.py - inference (blind) paths
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _TestSearch(Search):
|
||||
excludeDbsList = ["information_schema", "mysql"]
|
||||
|
||||
def __init__(self):
|
||||
Search.__init__(self)
|
||||
self.like = ('2', "='%s'") # exact match (colConsider '2')
|
||||
self.dumpFoundTablesCalls = []
|
||||
self.dumpFoundColumnCalls = []
|
||||
|
||||
def likeOrExact(self, what):
|
||||
return self.like
|
||||
|
||||
def forceDbmsEnum(self):
|
||||
pass
|
||||
|
||||
def getCurrentDb(self):
|
||||
return "testdb"
|
||||
|
||||
def dumpFoundTables(self, tables):
|
||||
self.dumpFoundTablesCalls.append(tables)
|
||||
|
||||
def dumpFoundColumn(self, dbs, foundCols, colConsider):
|
||||
self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider))
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
db, tbl, col = conf.db, conf.tbl, conf.col
|
||||
if db and tbl:
|
||||
kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {})
|
||||
kb.data.cachedColumns[db][tbl][col] = "varchar"
|
||||
|
||||
|
||||
class _RecDumper(object):
|
||||
def __init__(self):
|
||||
self.listed = []
|
||||
self.dbTablesArg = None
|
||||
self.dbColumnsArg = None
|
||||
|
||||
def lister(self, header, elements, content_type=None, sort=True):
|
||||
self.listed.append((header, list(elements) if elements else []))
|
||||
|
||||
def dbTables(self, dbTables):
|
||||
self.dbTablesArg = dbTables
|
||||
|
||||
def dbColumns(self, dbColumnsDict, colConsider, dbs):
|
||||
self.dbColumnsArg = (dbColumnsDict, colConsider, dbs)
|
||||
|
||||
|
||||
class _SearchBase(unittest.TestCase):
|
||||
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs",
|
||||
"exclude", "search")
|
||||
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
|
||||
self._saved_dumper = conf.get("dumper")
|
||||
self._gv = smod.inject.getValue
|
||||
self._readInput = smod.readInput
|
||||
self._saved_has_is = kb.data.get("has_information_schema")
|
||||
self._saved_cachedColumns = kb.data.get("cachedColumns")
|
||||
self._saved_hintValue = kb.get("hintValue")
|
||||
self._saved_injection_data = kb.injection.data
|
||||
|
||||
set_dbms("MySQL")
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
conf.excludeSysDbs = False
|
||||
conf.exclude = None
|
||||
conf.search = True
|
||||
conf.dumper = _RecDumper()
|
||||
|
||||
kb.data.has_information_schema = True
|
||||
kb.data.cachedColumns = {}
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
conf.dumper = self._saved_dumper
|
||||
smod.inject.getValue = self._gv
|
||||
smod.readInput = self._readInput
|
||||
kb.data.has_information_schema = self._saved_has_is
|
||||
kb.data.cachedColumns = self._saved_cachedColumns
|
||||
kb.hintValue = self._saved_hintValue
|
||||
kb.injection.data = self._saved_injection_data
|
||||
|
||||
|
||||
class TestSearchInference(_SearchBase):
|
||||
def test_search_db_inference(self):
|
||||
# Blind searchDb: count of matching dbs, then one db name per index.
|
||||
s = _TestSearch()
|
||||
conf.db = "testdb"
|
||||
smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"])
|
||||
s.searchDb()
|
||||
self.assertEqual(conf.dumper.listed[-1][0], "found databases")
|
||||
self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"])
|
||||
|
||||
def test_search_db_inference_no_match(self):
|
||||
# Count fails (non-numeric) => no databases appended, empty listing.
|
||||
s = _TestSearch()
|
||||
conf.db = "ghost"
|
||||
smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails"))
|
||||
s.searchDb()
|
||||
self.assertEqual(conf.dumper.listed[-1][1], [])
|
||||
|
||||
def test_search_table_inference_grouped(self):
|
||||
# Blind searchTable, no conf.db: outer count of dbs holding the table, then
|
||||
# per-db a name, then per-db a count of matching tables, then table names.
|
||||
s = _TestSearch()
|
||||
conf.tbl = "users"
|
||||
conf.db = None
|
||||
|
||||
# Sequencing by the EXPECTED.INT counts + the per-index string results.
|
||||
# 1st count: number of databases with the table -> 1
|
||||
# 1st db name -> "testdb"
|
||||
# 2nd count: number of tables in testdb -> 1
|
||||
# table name -> "users"
|
||||
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
v = seq["counts"][seq["ci"] % len(seq["counts"])]
|
||||
seq["ci"] += 1
|
||||
return v
|
||||
v = seq["vals"][seq["vi"] % len(seq["vals"])]
|
||||
seq["vi"] += 1
|
||||
return [v]
|
||||
|
||||
smod.inject.getValue = gv
|
||||
s.searchTable()
|
||||
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]})
|
||||
self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]})
|
||||
|
||||
def test_search_table_mysql_lt5_bruteforce_decline(self):
|
||||
# MySQL < 5 forces the bruteforce path; declining the prompt returns None
|
||||
# without any injection.
|
||||
s = _TestSearch()
|
||||
conf.tbl = "users"
|
||||
conf.db = None
|
||||
kb.data.has_information_schema = False
|
||||
smod.readInput = lambda *a, **k: "N"
|
||||
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
|
||||
self.assertIsNone(s.searchTable())
|
||||
|
||||
def test_search_column_inference(self):
|
||||
# Blind searchColumn, no db/tbl: count of dbs with the column, then db name;
|
||||
# then per-db count of tables with the column, then table name -> getColumns
|
||||
# folds the column into dbs.
|
||||
s = _TestSearch()
|
||||
conf.col = "password"
|
||||
conf.db = None
|
||||
conf.tbl = None
|
||||
|
||||
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
v = seq["counts"][seq["ci"] % len(seq["counts"])]
|
||||
seq["ci"] += 1
|
||||
return v
|
||||
v = seq["vals"][seq["vi"] % len(seq["vals"])]
|
||||
seq["vi"] += 1
|
||||
return [v]
|
||||
|
||||
smod.inject.getValue = gv
|
||||
s.searchColumn()
|
||||
dbs = conf.dumper.dbColumnsArg[2]
|
||||
self.assertIn("testdb", dbs)
|
||||
self.assertIn("users", dbs["testdb"])
|
||||
self.assertIn("password", dbs["testdb"]["users"])
|
||||
|
||||
def test_search_column_mysql_lt5_bruteforce_decline(self):
|
||||
s = _TestSearch()
|
||||
conf.col = "password"
|
||||
conf.db = None
|
||||
conf.tbl = None
|
||||
kb.data.has_information_schema = False
|
||||
smod.readInput = lambda *a, **k: "N"
|
||||
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
|
||||
# Declining returns None and never reaches dbColumns.
|
||||
self.assertIsNone(s.searchColumn())
|
||||
self.assertIsNone(conf.dumper.dbColumnsArg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -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
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Edge cases / control-flow branches of the blind-SQLi inference engine
|
||||
(lib/techniques/blind/inference.py) plus the pure UNION configuration helper
|
||||
(lib/techniques/union/use.py configUnion).
|
||||
|
||||
Complements tests/test_inference_engine.py (which covers the happy-path char-by-char
|
||||
extraction). Here we drive the REAL bisection() / queryOutputLength() against a mock
|
||||
oracle (Request.queryPage replaced by a parser of our own parseable payload template)
|
||||
to exercise the branches the engine test does not reach:
|
||||
|
||||
* trivial returns: payload is None, length == 0
|
||||
* --first-char / --last-char range limiting (both via the function args and via
|
||||
conf.firstChar / conf.lastChar)
|
||||
* --hex output decoding of the assembled value
|
||||
* kb.data.processChar post-processing hook
|
||||
* session resume from HashDB: a fully cached value, and a PARTIAL_VALUE_MARKER
|
||||
partial value that bisection continues from (against a REAL temp SQLite HashDB)
|
||||
* queryOutputLength() forging + DIGITS-charset length retrieval
|
||||
|
||||
No network, no live target, no real DBMS - exactly like the sibling engine test.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.common import decodeDbmsHexValue
|
||||
from lib.core.common import getCurrentThreadData
|
||||
from lib.core.common import hashDBWrite
|
||||
from lib.core.enums import CHARSET_TYPE
|
||||
from lib.core.exception import SqlmapSyntaxException
|
||||
from lib.core.settings import PARTIAL_VALUE_MARKER
|
||||
from lib.request.connect import Connect
|
||||
from lib.utils.hashdb import HashDB
|
||||
import lib.techniques.blind.inference as inf
|
||||
import lib.techniques.union.use as uu
|
||||
|
||||
# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
|
||||
# greater-char marker (swapped to '=' on the final equality check). A parseable template
|
||||
# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret.
|
||||
TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
|
||||
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
|
||||
|
||||
# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path
|
||||
_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False,
|
||||
"charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False,
|
||||
"repair": False, "flushSession": None, "freshQueries": None, "hashDB": None}
|
||||
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
|
||||
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False,
|
||||
"resumeValues": True, "inferenceMode": False}
|
||||
|
||||
|
||||
class _InferenceCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in _CONF}
|
||||
self._saved_kb = {k: kb.get(k) for k in _KB}
|
||||
self._saved_qp = Connect.queryPage
|
||||
self._saved_processChar = kb.data.get("processChar")
|
||||
for k, v in _CONF.items():
|
||||
conf[k] = v
|
||||
for k, v in _KB.items():
|
||||
kb[k] = v
|
||||
kb.data.processChar = None
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
for k, v in self._saved_kb.items():
|
||||
kb[k] = v
|
||||
kb.data.processChar = self._saved_processChar
|
||||
Connect.queryPage = self._saved_qp
|
||||
inf.Request.queryPage = self._saved_qp
|
||||
|
||||
def _install_oracle(self, secret):
|
||||
def oracle(payload=None, *args, **kwargs):
|
||||
m = _PARSE.search(payload)
|
||||
idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3))
|
||||
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
|
||||
return (ch > threshold) if op == ">" else (ch == threshold)
|
||||
|
||||
Connect.queryPage = staticmethod(oracle)
|
||||
inf.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
@staticmethod
|
||||
def _reset_thread():
|
||||
td = getCurrentThreadData()
|
||||
td.shared.value = ""
|
||||
td.shared.index = [0]
|
||||
td.shared.start = 0
|
||||
td.shared.count = 0
|
||||
|
||||
def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs):
|
||||
self._install_oracle(secret)
|
||||
self._reset_thread()
|
||||
if length is None:
|
||||
length = len(secret)
|
||||
return inf.bisection(TEMPLATE, expression, length=length, **kwargs)
|
||||
|
||||
|
||||
class TestTrivialReturns(_InferenceCase):
|
||||
def test_none_payload(self):
|
||||
# payload is None -> (0, None) without ever touching the oracle
|
||||
self.assertEqual(inf.bisection(None, "SELECT x"), (0, None))
|
||||
|
||||
def test_zero_length(self):
|
||||
# length == 0 -> (0, "") short-circuit
|
||||
self._install_oracle("ignored")
|
||||
self._reset_thread()
|
||||
self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, ""))
|
||||
|
||||
|
||||
class TestRangeLimiting(_InferenceCase):
|
||||
SECRET = "ABCDEFGH"
|
||||
|
||||
def test_first_char_arg(self):
|
||||
# firstChar=3 -> start from the 3rd character (1-based) -> drop "AB"
|
||||
_, value = self._bisect(self.SECRET, firstChar=3)
|
||||
self.assertEqual(value, "CDEFGH")
|
||||
|
||||
def test_last_char_arg(self):
|
||||
# lastChar=4 -> stop after the 4th character
|
||||
_, value = self._bisect(self.SECRET, lastChar=4)
|
||||
self.assertEqual(value, "ABCD")
|
||||
|
||||
def test_conf_first_char(self):
|
||||
conf.firstChar = 4
|
||||
_, value = self._bisect(self.SECRET)
|
||||
self.assertEqual(value, "DEFGH")
|
||||
|
||||
def test_conf_last_char(self):
|
||||
conf.lastChar = 3
|
||||
_, value = self._bisect(self.SECRET)
|
||||
self.assertEqual(value, "ABC")
|
||||
|
||||
def test_first_and_last_window(self):
|
||||
# combined window: chars 3..6 inclusive -> "CDEF"
|
||||
_, value = self._bisect(self.SECRET, firstChar=3, lastChar=6)
|
||||
self.assertEqual(value, "CDEF")
|
||||
|
||||
|
||||
class TestHexConvert(_InferenceCase):
|
||||
def test_hex_output_decoded(self):
|
||||
# --hex: the retrieved value is a hex string the engine decodes on the way out
|
||||
conf.hexConvert = True
|
||||
hexed = "48656C6C6F" # "Hello"
|
||||
_, value = self._bisect(hexed)
|
||||
self.assertEqual(value, "Hello")
|
||||
self.assertEqual(value, decodeDbmsHexValue(hexed))
|
||||
|
||||
|
||||
class TestProcessCharHook(_InferenceCase):
|
||||
def test_process_char_applied_to_each_char(self):
|
||||
# kb.data.processChar transforms every assembled character
|
||||
kb.data.processChar = lambda c: c.upper()
|
||||
_, value = self._bisect("abcde")
|
||||
self.assertEqual(value, "ABCDE")
|
||||
|
||||
|
||||
class TestResumeFromHashDB(_InferenceCase):
|
||||
"""bisection() consults the session store first (hashDBRetrieve(checkConf=True)).
|
||||
Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py)."""
|
||||
|
||||
def setUp(self):
|
||||
_InferenceCase.setUp(self)
|
||||
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
|
||||
os.close(fd)
|
||||
os.remove(self.path) # HashDB creates it lazily
|
||||
conf.hashDB = HashDB(self.path)
|
||||
# hashDBRetrieve/Write key off these
|
||||
self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port"))
|
||||
conf.hostname = "test.invalid"
|
||||
conf.path = "/"
|
||||
conf.port = 80
|
||||
|
||||
def tearDown(self):
|
||||
conf.hostname, conf.path, conf.port = self._saved_loc
|
||||
try:
|
||||
conf.hashDB.closeAll()
|
||||
except Exception:
|
||||
pass
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
_InferenceCase.tearDown(self)
|
||||
|
||||
def test_full_value_resumed(self):
|
||||
# a complete cached value short-circuits the whole bisection (0 queries)
|
||||
hashDBWrite("SELECT cached", "RESUMED")
|
||||
conf.hashDB.flush()
|
||||
count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7)
|
||||
self.assertEqual(value, "RESUMED")
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
def test_partial_value_continued(self):
|
||||
# a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix
|
||||
# and extracts only the remaining characters
|
||||
kb.inferenceMode = True # partial markers are honored only in inference mode
|
||||
hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER)
|
||||
conf.hashDB.flush()
|
||||
count, value = self._bisect("ABCDE", expression="SELECT partial", length=5)
|
||||
self.assertEqual(value, "ABCDE")
|
||||
self.assertGreater(count, 0) # it did real work for "CDE"
|
||||
|
||||
|
||||
class TestQueryOutputLength(_InferenceCase):
|
||||
def test_length_retrieved(self):
|
||||
# queryOutputLength forges a LENGTH() expression and runs bisection with the
|
||||
# DIGITS charset; the mock "secret" is the textual length itself
|
||||
self._install_oracle("42")
|
||||
self._reset_thread()
|
||||
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42)
|
||||
|
||||
def test_length_single_digit(self):
|
||||
self._install_oracle("7")
|
||||
self._reset_thread()
|
||||
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7)
|
||||
|
||||
def test_digits_charset_extracts_number(self):
|
||||
# direct bisection with the DIGITS charset (queryOutputLength's inner call)
|
||||
_, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS)
|
||||
self.assertEqual(value, "2026")
|
||||
|
||||
|
||||
class TestConfigUnion(unittest.TestCase):
|
||||
"""lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols."""
|
||||
|
||||
_CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50}
|
||||
|
||||
def setUp(self):
|
||||
self._saved = {k: conf.get(k) for k in self._CONF}
|
||||
self._saved_uchar = kb.get("uChar")
|
||||
for k, v in self._CONF.items():
|
||||
conf[k] = v
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved.items():
|
||||
conf[k] = v
|
||||
kb.uChar = self._saved_uchar
|
||||
|
||||
def test_char_and_range(self):
|
||||
uu.configUnion(char="NULL", columns="2-6")
|
||||
self.assertEqual(kb.uChar, "NULL")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6))
|
||||
|
||||
def test_single_column(self):
|
||||
uu.configUnion(char="NULL", columns="4")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4))
|
||||
|
||||
def test_uchar_substitution_quoted(self):
|
||||
# conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template
|
||||
conf.uChar = "test"
|
||||
uu.configUnion(char="x[CHAR]x", columns="1")
|
||||
self.assertEqual(kb.uChar, "x'test'x")
|
||||
|
||||
def test_uchar_substitution_digit(self):
|
||||
# a digit conf.uChar is substituted unquoted
|
||||
conf.uChar = "88"
|
||||
uu.configUnion(char="[CHAR]", columns="1")
|
||||
self.assertEqual(kb.uChar, "88")
|
||||
|
||||
def test_conf_ucols_overrides_columns_arg(self):
|
||||
# conf.uCols takes precedence over the columns argument
|
||||
conf.uCols = "3-9"
|
||||
uu.configUnion(char="NULL", columns="1-2")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9))
|
||||
|
||||
def test_non_integer_range_raises(self):
|
||||
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc")
|
||||
|
||||
def test_inverted_range_raises(self):
|
||||
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2")
|
||||
|
||||
def test_non_string_char_ignored(self):
|
||||
# a non-string char leaves kb.uChar untouched (early return)
|
||||
kb.uChar = "SENTINEL"
|
||||
uu.configUnion(char=None, columns="1")
|
||||
self.assertEqual(kb.uChar, "SENTINEL")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
1594
tests/test_option.py
Normal file
1594
tests/test_option.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,663 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Additional coverage for option setup / normalization helpers in
|
||||
lib/core/option.py, targeting functions and branches NOT already exercised by
|
||||
tests/test_option_setup.py:
|
||||
|
||||
* _setTamperingFunctions (loads real tamper modules into kb.tamperFunctions)
|
||||
* _setPreprocessFunctions (loads a preprocess(req) script into kb.preprocessFunctions)
|
||||
* _setPostprocessFunctions (loads a postprocess(page, headers, code) script)
|
||||
* _setSafeVisit (parses a safe request file into kb.safeReq)
|
||||
* _cleanupOptions (additional normalization branches: delay cast,
|
||||
csvDel/paramDel escape, col/binaryFields split,
|
||||
torType upper, abortCode, getAll, dummy->batch)
|
||||
* _basicOptionValidation (additional illegal option combinations / branches)
|
||||
* _normalizeOptions (string + boolean option coercion)
|
||||
* setVerbosity (eta clamp + high verbose)
|
||||
|
||||
As in test_option_setup.py, option.py mutates the global conf/kb singletons
|
||||
aggressively, so every test saves and restores the conf/kb fields it touches via
|
||||
the _preserve() context manager so the shared state stays pristine for the rest
|
||||
of the suite.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb, logger, paths
|
||||
from lib.core.exception import SqlmapSyntaxException
|
||||
from lib.core.exception import SqlmapSystemException
|
||||
from lib.core.exception import SqlmapGenericException
|
||||
from lib.core.exception import SqlmapFilePathException
|
||||
from lib.core.exception import SqlmapValueException
|
||||
from lib.core.settings import MAX_CONNECT_RETRIES
|
||||
|
||||
import lib.core.option as option
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
# scratchpad for the preprocess/postprocess/safe-req fixture files
|
||||
_SCRATCH = os.environ.get("CLAUDE_SCRATCH") or os.path.join(os.path.dirname(os.path.abspath(__file__)), "_option_more_tmp")
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
"""Remove the scratch fixture directory so it never lingers on disk (and so a
|
||||
stray __init__.py there can't shadow imports in a subsequent run)."""
|
||||
import shutil
|
||||
if os.path.isdir(_SCRATCH):
|
||||
shutil.rmtree(_SCRATCH, ignore_errors=True)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _preserve(target, *keys):
|
||||
"""Save the given keys of an AttribDict (conf/kb), then restore on exit.
|
||||
|
||||
Missing keys are restored to absent so a test can't leak a brand-new field.
|
||||
"""
|
||||
saved = {}
|
||||
for key in keys:
|
||||
saved[key] = target[key] if key in target else _SENTINEL
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for key in keys:
|
||||
if saved[key] is _SENTINEL:
|
||||
try:
|
||||
del target[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
target[key] = saved[key]
|
||||
|
||||
|
||||
class _ImportSandboxMixin(object):
|
||||
"""Loaders in option.py (tamper/preprocess/postprocess) permanently
|
||||
`sys.path.insert(0, <script dir>)` and import the script module, which would
|
||||
otherwise leak the scratch directory onto sys.path (shadowing later imports)
|
||||
and leave stray modules in sys.modules for the rest of the suite. Snapshot
|
||||
both around the test class and restore them so the shared interpreter state
|
||||
stays pristine for the other ~900 tests.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._saved_path = list(sys.path)
|
||||
cls._saved_modules = set(sys.modules)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
sys.path[:] = cls._saved_path
|
||||
for name in list(sys.modules):
|
||||
if name not in cls._saved_modules:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
class TestSetTamperingFunctions(_ImportSandboxMixin, unittest.TestCase):
|
||||
"""_setTamperingFunctions imports the named tamper modules and appends their
|
||||
tamper() callables to kb.tamperFunctions."""
|
||||
|
||||
def test_none_noop(self):
|
||||
with _preserve(conf, "tamper"), _preserve(kb, "tamperFunctions"):
|
||||
kb.tamperFunctions = []
|
||||
conf.tamper = None
|
||||
option._setTamperingFunctions()
|
||||
self.assertEqual(kb.tamperFunctions, [])
|
||||
|
||||
def test_loads_named_scripts(self):
|
||||
# 'between' (HIGHEST) before 'space2comment' (LOW) keeps priorities
|
||||
# non-increasing, so the interactive "mixed order" prompt is not triggered.
|
||||
with _preserve(conf, "tamper"), _preserve(kb, "tamperFunctions"):
|
||||
kb.tamperFunctions = []
|
||||
conf.tamper = "between,space2comment"
|
||||
option._setTamperingFunctions()
|
||||
self.assertEqual(len(kb.tamperFunctions), 2)
|
||||
names = sorted(f.__name__ for f in kb.tamperFunctions)
|
||||
self.assertEqual(names, ["between", "space2comment"])
|
||||
# each loaded entry is a callable tamper function
|
||||
self.assertTrue(all(callable(f) for f in kb.tamperFunctions))
|
||||
|
||||
def test_mixed_order_auto_resolved_in_batch(self):
|
||||
# 'space2comment' (LOW) before 'between' (HIGHEST) trips the priority
|
||||
# mixup; in batch mode readInput uses the 'Y' default and auto-resolves,
|
||||
# sorting kb.tamperFunctions by priority (descending).
|
||||
with _preserve(conf, "tamper", "batch"), _preserve(kb, "tamperFunctions"):
|
||||
kb.tamperFunctions = []
|
||||
conf.batch = True
|
||||
conf.tamper = "space2comment,between"
|
||||
option._setTamperingFunctions()
|
||||
self.assertEqual(len(kb.tamperFunctions), 2)
|
||||
# after auto-resolve, 'between' (HIGHEST) comes first
|
||||
self.assertEqual(kb.tamperFunctions[0].__name__, "between")
|
||||
|
||||
def test_missing_script_raises(self):
|
||||
with _preserve(conf, "tamper"), _preserve(kb, "tamperFunctions"):
|
||||
kb.tamperFunctions = []
|
||||
conf.tamper = "definitely_not_a_tamper_script_xyz"
|
||||
self.assertRaises(SqlmapFilePathException, option._setTamperingFunctions)
|
||||
|
||||
|
||||
class TestSetPreprocessFunctions(_ImportSandboxMixin, unittest.TestCase):
|
||||
"""_setPreprocessFunctions imports a preprocess(req) script and appends it to
|
||||
kb.preprocessFunctions (after a successful test-run against a dummy Request)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSetPreprocessFunctions, cls).setUpClass()
|
||||
if not os.path.isdir(_SCRATCH):
|
||||
os.makedirs(_SCRATCH)
|
||||
# an empty __init__.py is required next to the script
|
||||
with open(os.path.join(_SCRATCH, "__init__.py"), "w") as f:
|
||||
f.write("")
|
||||
cls.script = os.path.join(_SCRATCH, "pre_ok.py")
|
||||
with open(cls.script, "w") as f:
|
||||
f.write("#!/usr/bin/env\n\ndef preprocess(req):\n pass\n")
|
||||
cls.bad = os.path.join(_SCRATCH, "pre_no_func.py")
|
||||
with open(cls.bad, "w") as f:
|
||||
f.write("#!/usr/bin/env\n\ndef notpreprocess(req):\n pass\n")
|
||||
|
||||
def test_none_noop(self):
|
||||
with _preserve(conf, "preprocess"), _preserve(kb, "preprocessFunctions"):
|
||||
kb.preprocessFunctions = []
|
||||
conf.preprocess = None
|
||||
option._setPreprocessFunctions()
|
||||
self.assertEqual(kb.preprocessFunctions, [])
|
||||
|
||||
def test_loads_script(self):
|
||||
with _preserve(conf, "preprocess", "debug"), _preserve(kb, "preprocessFunctions"):
|
||||
kb.preprocessFunctions = []
|
||||
conf.debug = False
|
||||
conf.preprocess = self.script
|
||||
option._setPreprocessFunctions()
|
||||
self.assertEqual(len(kb.preprocessFunctions), 1)
|
||||
self.assertTrue(callable(kb.preprocessFunctions[0]))
|
||||
|
||||
def test_missing_function_raises(self):
|
||||
with _preserve(conf, "preprocess", "debug"), _preserve(kb, "preprocessFunctions"):
|
||||
kb.preprocessFunctions = []
|
||||
conf.debug = False
|
||||
conf.preprocess = self.bad
|
||||
self.assertRaises(SqlmapGenericException, option._setPreprocessFunctions)
|
||||
|
||||
def test_missing_file_raises(self):
|
||||
with _preserve(conf, "preprocess", "debug"), _preserve(kb, "preprocessFunctions"):
|
||||
kb.preprocessFunctions = []
|
||||
conf.debug = False
|
||||
conf.preprocess = os.path.join(_SCRATCH, "nope.py")
|
||||
self.assertRaises(SqlmapFilePathException, option._setPreprocessFunctions)
|
||||
|
||||
|
||||
class TestSetPostprocessFunctions(_ImportSandboxMixin, unittest.TestCase):
|
||||
"""_setPostprocessFunctions imports a postprocess(page, headers, code) script
|
||||
that must return a (page, headers, code) tuple."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSetPostprocessFunctions, cls).setUpClass()
|
||||
if not os.path.isdir(_SCRATCH):
|
||||
os.makedirs(_SCRATCH)
|
||||
with open(os.path.join(_SCRATCH, "__init__.py"), "w") as f:
|
||||
f.write("")
|
||||
cls.script = os.path.join(_SCRATCH, "post_ok.py")
|
||||
with open(cls.script, "w") as f:
|
||||
f.write("#!/usr/bin/env\n\ndef postprocess(page, headers=None, code=None):\n return page, headers, code\n")
|
||||
cls.bad = os.path.join(_SCRATCH, "post_no_func.py")
|
||||
with open(cls.bad, "w") as f:
|
||||
f.write("#!/usr/bin/env\n\ndef other(page, headers=None, code=None):\n return page, headers, code\n")
|
||||
|
||||
def test_none_noop(self):
|
||||
with _preserve(conf, "postprocess"), _preserve(kb, "postprocessFunctions"):
|
||||
kb.postprocessFunctions = []
|
||||
conf.postprocess = None
|
||||
option._setPostprocessFunctions()
|
||||
self.assertEqual(kb.postprocessFunctions, [])
|
||||
|
||||
def test_loads_script(self):
|
||||
with _preserve(conf, "postprocess"), _preserve(kb, "postprocessFunctions"):
|
||||
kb.postprocessFunctions = []
|
||||
conf.postprocess = self.script
|
||||
option._setPostprocessFunctions()
|
||||
self.assertEqual(len(kb.postprocessFunctions), 1)
|
||||
self.assertTrue(callable(kb.postprocessFunctions[0]))
|
||||
|
||||
def test_missing_function_raises(self):
|
||||
with _preserve(conf, "postprocess"), _preserve(kb, "postprocessFunctions"):
|
||||
kb.postprocessFunctions = []
|
||||
conf.postprocess = self.bad
|
||||
self.assertRaises(SqlmapGenericException, option._setPostprocessFunctions)
|
||||
|
||||
|
||||
class TestSetSafeVisit(unittest.TestCase):
|
||||
"""_setSafeVisit parses a raw HTTP request file into kb.safeReq, or normalizes
|
||||
a bare safeUrl, and enforces safeFreq > 0."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not os.path.isdir(_SCRATCH):
|
||||
os.makedirs(_SCRATCH)
|
||||
cls.reqfile = os.path.join(_SCRATCH, "safe_req.txt")
|
||||
with open(cls.reqfile, "w") as f:
|
||||
f.write("GET /safe?ping=1 HTTP/1.1\nHost: victim.example\nUser-Agent: t\n\n")
|
||||
cls.badfile = os.path.join(_SCRATCH, "safe_req_bad.txt")
|
||||
with open(cls.badfile, "w") as f:
|
||||
f.write("this is not a valid request line\n")
|
||||
|
||||
def _keys(self):
|
||||
return ("safeUrl", "safeReqFile", "safeFreq", "safePost")
|
||||
|
||||
def test_noop_when_unset(self):
|
||||
with _preserve(conf, *self._keys()):
|
||||
conf.safeUrl = None
|
||||
conf.safeReqFile = None
|
||||
conf.safeFreq = 0
|
||||
option._setSafeVisit() # must not raise
|
||||
|
||||
def test_safe_url_scheme_prepended(self):
|
||||
with _preserve(conf, *self._keys()):
|
||||
conf.safeUrl = "victim.example/keepalive"
|
||||
conf.safeReqFile = None
|
||||
conf.safeFreq = 5
|
||||
option._setSafeVisit()
|
||||
self.assertTrue(conf.safeUrl.startswith("http://"), msg=conf.safeUrl)
|
||||
|
||||
def test_safe_url_requires_positive_freq(self):
|
||||
with _preserve(conf, *self._keys()):
|
||||
conf.safeUrl = "http://victim.example/k"
|
||||
conf.safeReqFile = None
|
||||
conf.safeFreq = 0
|
||||
self.assertRaises(SqlmapSyntaxException, option._setSafeVisit)
|
||||
|
||||
def test_safe_req_file_parsed(self):
|
||||
with _preserve(conf, *self._keys()), _preserve(kb, "safeReq"):
|
||||
conf.safeUrl = None
|
||||
conf.safePost = None
|
||||
conf.safeReqFile = self.reqfile
|
||||
conf.safeFreq = 3
|
||||
option._setSafeVisit()
|
||||
self.assertEqual(kb.safeReq.method, "GET")
|
||||
self.assertIn("victim.example", kb.safeReq.url)
|
||||
self.assertEqual(kb.safeReq.headers.get("User-Agent"), "t")
|
||||
|
||||
def test_safe_req_file_invalid_format_raises(self):
|
||||
with _preserve(conf, *self._keys()), _preserve(kb, "safeReq"):
|
||||
conf.safeUrl = None
|
||||
conf.safePost = None
|
||||
conf.safeReqFile = self.badfile
|
||||
conf.safeFreq = 3
|
||||
self.assertRaises(SqlmapSyntaxException, option._setSafeVisit)
|
||||
|
||||
|
||||
class TestCleanupOptionsExtra(unittest.TestCase):
|
||||
"""Additional _cleanupOptions normalization branches not covered by
|
||||
test_option_setup.py."""
|
||||
|
||||
_KEYS = (
|
||||
"encoding", "eta", "testParameter", "ignoreCode", "abortCode",
|
||||
"paramFilter", "base64Parameter", "agent", "user", "rParam",
|
||||
"paramDel", "skip", "cookie", "delay", "url", "fileRead",
|
||||
"fileWrite", "fileDest", "msfPath", "tmpPath", "googleDork",
|
||||
"logFile", "bulkFile", "forms", "crawlDepth", "stdinPipe",
|
||||
"multipleTargets", "optimize", "os", "forceDbms", "dbms",
|
||||
"uValues", "uCols", "testFilter", "csrfToken", "testSkip",
|
||||
"tor", "timeSec", "retries", "code", "csvDel", "torPort",
|
||||
"torType", "outputDir", "string", "getAll", "noCast",
|
||||
"dumpFormat", "col", "exclude", "binaryFields", "proxy",
|
||||
"proxyFile", "dummy", "batch", "scope",
|
||||
)
|
||||
|
||||
def _base(self):
|
||||
for key in self._KEYS:
|
||||
conf[key] = None
|
||||
conf.eta = False
|
||||
conf.optimize = False
|
||||
conf.tor = False
|
||||
conf.getAll = False
|
||||
conf.noCast = False
|
||||
conf.dummy = False
|
||||
conf.batch = False
|
||||
conf.timeSec = 5
|
||||
conf.retries = 3
|
||||
conf.multipleTargets = False
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _ctx(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
yield
|
||||
|
||||
def test_delay_cast_to_float(self):
|
||||
with self._ctx():
|
||||
conf.delay = "2"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.delay, 2.0)
|
||||
self.assertIsInstance(conf.delay, float)
|
||||
|
||||
def test_csv_del_escape_decoded(self):
|
||||
with self._ctx():
|
||||
conf.csvDel = "\\t"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.csvDel, "\t")
|
||||
|
||||
def test_param_del_escape_decoded(self):
|
||||
with self._ctx():
|
||||
conf.paramDel = "\\n"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.paramDel, "\n")
|
||||
|
||||
def test_col_whitespace_normalized(self):
|
||||
with self._ctx():
|
||||
conf.col = "id , name , pass"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.col, "id,name,pass")
|
||||
|
||||
def test_binary_fields_split(self):
|
||||
with self._ctx():
|
||||
conf.binaryFields = "data, blob"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.binaryFields, ["data", "blob"])
|
||||
|
||||
def test_tor_type_uppercased(self):
|
||||
with self._ctx():
|
||||
conf.torType = "socks5"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.torType, "SOCKS5")
|
||||
|
||||
def test_abort_code_empty_becomes_list(self):
|
||||
with self._ctx():
|
||||
conf.abortCode = None
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.abortCode, [])
|
||||
|
||||
def test_abort_code_invalid_raises(self):
|
||||
with self._ctx():
|
||||
conf.abortCode = "notanumber"
|
||||
self.assertRaises(SqlmapSyntaxException, option._cleanupOptions)
|
||||
|
||||
def test_user_spaces_stripped(self):
|
||||
with self._ctx():
|
||||
conf.user = "ad min"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.user, "admin")
|
||||
|
||||
def test_dummy_forces_batch(self):
|
||||
with self._ctx():
|
||||
conf.dummy = True
|
||||
option._cleanupOptions()
|
||||
self.assertTrue(conf.batch)
|
||||
|
||||
def test_string_escape_decoded(self):
|
||||
with self._ctx():
|
||||
conf.string = "a\\tb"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.string, "a\tb")
|
||||
|
||||
def test_retries_clamped(self):
|
||||
with self._ctx():
|
||||
conf.retries = 9999
|
||||
option._cleanupOptions()
|
||||
# clamped to exactly MAX_CONNECT_RETRIES, not merely "less than 9999"
|
||||
self.assertEqual(conf.retries, MAX_CONNECT_RETRIES)
|
||||
|
||||
def test_unknown_encoding_raises(self):
|
||||
with self._ctx():
|
||||
conf.encoding = "definitely-not-an-encoding"
|
||||
self.assertRaises(SqlmapValueException, option._cleanupOptions)
|
||||
|
||||
|
||||
class TestBasicOptionValidationExtra(unittest.TestCase):
|
||||
"""Additional illegal option combinations / validation branches in
|
||||
_basicOptionValidation not covered by test_option_setup.py."""
|
||||
|
||||
_KEYS = (
|
||||
"limitStart", "limitStop", "level", "risk", "firstChar", "lastChar",
|
||||
"textOnly", "nullConnection", "uValues", "uChar", "base64Parameter",
|
||||
"tamper", "eta", "verbose", "direct", "url", "dbms", "tor", "proxy",
|
||||
"ignoreProxy", "regexp", "timeSec", "torPort", "torType", "dumpFormat",
|
||||
"technique", "threads", "predictOutput", "optimize", "csrfToken",
|
||||
"csrfUrl", "csrfMethod", "csrfData", "string", "notString", "noCast",
|
||||
"hexConvert", "titles", "dumpTable", "search", "dumpAll", "data",
|
||||
"requestFile", "forms", "googleDork", "bulkFile", "chunked",
|
||||
"cookieDel", "dbmsCred", "mobile", "agent", "crawlExclude",
|
||||
"crawlDepth", "safePost", "safeUrl", "safeReqFile", "safeFreq",
|
||||
"proxyFile", "proxyFreq", "checkTor", "alert", "secondUrl",
|
||||
"secondReq", "http2", "osPwn",
|
||||
)
|
||||
|
||||
def _base(self):
|
||||
for key in self._KEYS:
|
||||
conf[key] = None
|
||||
conf.textOnly = False
|
||||
conf.nullConnection = False
|
||||
conf.eta = False
|
||||
conf.direct = False
|
||||
conf.tor = False
|
||||
conf.ignoreProxy = False
|
||||
conf.predictOutput = False
|
||||
conf.optimize = False
|
||||
conf.noCast = False
|
||||
conf.hexConvert = False
|
||||
conf.titles = False
|
||||
conf.mobile = False
|
||||
conf.chunked = False
|
||||
conf.checkTor = False
|
||||
conf.http2 = False
|
||||
conf.osPwn = False
|
||||
conf.verbose = 1
|
||||
conf.timeSec = 5
|
||||
conf.torPort = None
|
||||
conf.torType = "SOCKS5"
|
||||
conf.dumpFormat = "CSV"
|
||||
conf.technique = [1, 2, 6, 4, 5]
|
||||
conf.threads = 1
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _ctx(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
yield
|
||||
|
||||
def test_bad_limit_start_raises(self):
|
||||
with self._ctx():
|
||||
conf.limitStart = -1
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_bad_limit_stop_raises(self):
|
||||
with self._ctx():
|
||||
conf.limitStop = 0
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_first_char_gt_last_char_raises(self):
|
||||
with self._ctx():
|
||||
conf.firstChar = 5
|
||||
conf.lastChar = 2
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_base64_tamper_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.base64Parameter = "id"
|
||||
conf.tamper = "space2comment"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_direct_dbms_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.direct = "mysql://u:p@h/db"
|
||||
conf.dbms = "MySQL"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_titles_nullconnection_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.titles = True
|
||||
conf.nullConnection = True
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_dump_search_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.dumpTable = True
|
||||
conf.search = True
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_string_notstring_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.string = "ok"
|
||||
conf.notString = "bad"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_chunked_requires_post(self):
|
||||
with self._ctx():
|
||||
conf.chunked = True
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_cookie_del_single_char(self):
|
||||
with self._ctx():
|
||||
conf.cookieDel = ";;"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_dbms_cred_format(self):
|
||||
with self._ctx():
|
||||
conf.dbmsCred = "rootnopassword"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_mobile_agent_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.mobile = True
|
||||
conf.agent = "UA/1.0"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_proxy_ignoreproxy_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.proxy = "http://127.0.0.1:8080"
|
||||
conf.ignoreProxy = True
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_csrf_url_requires_token(self):
|
||||
with self._ctx():
|
||||
conf.csrfUrl = "http://x/token"
|
||||
conf.csrfToken = None
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_csrf_token_threads_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.csrfToken = "tok"
|
||||
conf.threads = 4
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_time_sec_must_be_positive(self):
|
||||
with self._ctx():
|
||||
conf.timeSec = 0
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_forms_requires_target(self):
|
||||
with self._ctx():
|
||||
conf.forms = True
|
||||
conf.url = None
|
||||
conf.googleDork = None
|
||||
conf.bulkFile = None
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_safe_post_requires_safe_url(self):
|
||||
with self._ctx():
|
||||
conf.safePost = "x=1"
|
||||
conf.safeUrl = None
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_proxy_freq_requires_proxy_file(self):
|
||||
with self._ctx():
|
||||
conf.proxyFreq = 5
|
||||
conf.proxyFile = None
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_check_tor_requires_tor_or_proxy(self):
|
||||
with self._ctx():
|
||||
conf.checkTor = True
|
||||
conf.tor = False
|
||||
conf.proxy = None
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_second_url_req_incompatible(self):
|
||||
with self._ctx():
|
||||
conf.secondUrl = "http://x/2"
|
||||
conf.secondReq = "/path/req.txt"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_alert_unsafe_requires_env(self):
|
||||
# _basicOptionValidation raises SqlmapSystemException for --alert without env
|
||||
with self._ctx():
|
||||
saved = os.environ.pop("SQLMAP_UNSAFE_ALERT", None)
|
||||
try:
|
||||
conf.alert = "echo hi"
|
||||
self.assertRaises(SqlmapSystemException, option._basicOptionValidation)
|
||||
finally:
|
||||
if saved is not None:
|
||||
os.environ["SQLMAP_UNSAFE_ALERT"] = saved
|
||||
|
||||
|
||||
class TestNormalizeOptionsExtra(unittest.TestCase):
|
||||
"""_normalizeOptions coerces values by option type. test_option_setup covers
|
||||
INTEGER; here cover FLOAT and BOOLEAN coercion (STRING is left untouched)."""
|
||||
|
||||
def test_float_coercion(self):
|
||||
# 'delay' is a FLOAT option; a string value is coerced to float
|
||||
opts = {"delay": "2.5"}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertEqual(opts["delay"], 2.5)
|
||||
|
||||
def test_bad_float_becomes_zero(self):
|
||||
opts = {"delay": "notafloat"}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertEqual(opts["delay"], 0.0)
|
||||
|
||||
def test_boolean_coercion(self):
|
||||
# 'forms' is a BOOLEAN option; a truthy non-empty value -> True
|
||||
opts = {"forms": 1}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertIs(opts["forms"], True)
|
||||
|
||||
def test_boolean_empty_false(self):
|
||||
opts = {"forms": ""}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertIs(opts["forms"], False)
|
||||
|
||||
|
||||
class TestSetVerbosityExtra(unittest.TestCase):
|
||||
"""setVerbosity branches not covered by test_option_setup.py."""
|
||||
|
||||
def test_eta_clamps_verbose(self):
|
||||
saved_level = logger.level
|
||||
try:
|
||||
with _preserve(conf, "verbose", "eta"):
|
||||
conf.verbose = 5
|
||||
conf.eta = True
|
||||
option.setVerbosity()
|
||||
# with eta on and verbose > 2, verbose is clamped to 2 (DEBUG)
|
||||
self.assertEqual(conf.verbose, 2)
|
||||
self.assertEqual(logger.level, logging.DEBUG)
|
||||
finally:
|
||||
logger.setLevel(saved_level)
|
||||
|
||||
def test_string_verbose_coerced_to_int(self):
|
||||
saved_level = logger.level
|
||||
try:
|
||||
with _preserve(conf, "verbose", "eta"):
|
||||
conf.verbose = "1"
|
||||
conf.eta = False
|
||||
option.setVerbosity()
|
||||
self.assertEqual(conf.verbose, 1)
|
||||
self.assertEqual(logger.level, logging.INFO)
|
||||
finally:
|
||||
logger.setLevel(saved_level)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -1,739 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Option setup / normalization helpers in lib/core/option.py.
|
||||
|
||||
These exercise the (mostly) pure config-massaging functions that parse, validate
|
||||
and normalize user-supplied option values into the canonical conf.*/kb.* shapes
|
||||
that the rest of sqlmap relies on - WITHOUT touching the network, the DBMS, the
|
||||
filesystem (beyond what bootstrap already set up) or any interactive prompt.
|
||||
|
||||
option.py mutates the global conf/kb singletons aggressively, so every test that
|
||||
writes a conf/kb field saves and restores it via the _preserve() helper so the
|
||||
shared state stays pristine for the other test files in the suite.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb, logger
|
||||
from lib.core.common import Backend
|
||||
from lib.core.enums import HTTP_HEADER
|
||||
from lib.core.settings import DEFAULT_USER_AGENT
|
||||
from lib.core.settings import IGNORE_CODE_WILDCARD
|
||||
from lib.core.exception import SqlmapSyntaxException
|
||||
from lib.core.exception import SqlmapUnsupportedDBMSException
|
||||
|
||||
import lib.core.option as option
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
# conf/kb fields that Backend.getIdentifiedDbms()/getOs() consult; any test that
|
||||
# might touch DBMS/OS forcing snapshots ALL of them so no fingerprint state leaks
|
||||
# into sibling test files (e.g. test_target_parsing's resume tests).
|
||||
_BACKEND_CONF_KEYS = ("dbms", "forceDbms", "os")
|
||||
_BACKEND_KB_KEYS = ("dbms", "dbmsVersion", "forcedDbms", "dbmsFilter", "os", "osVersion", "osSP")
|
||||
|
||||
|
||||
class _BackendGuard(unittest.TestCase):
|
||||
"""Mixin: fully snapshot & restore Backend-relevant conf/kb state per test."""
|
||||
|
||||
def setUp(self):
|
||||
super(_BackendGuard, self).setUp()
|
||||
self._snap_conf = {k: (conf[k] if k in conf else _SENTINEL) for k in _BACKEND_CONF_KEYS}
|
||||
self._snap_kb = {k: (kb[k] if k in kb else _SENTINEL) for k in _BACKEND_KB_KEYS}
|
||||
|
||||
def tearDown(self):
|
||||
for store, snap, keys in ((conf, self._snap_conf, _BACKEND_CONF_KEYS),
|
||||
(kb, self._snap_kb, _BACKEND_KB_KEYS)):
|
||||
for k in keys:
|
||||
if snap[k] is _SENTINEL:
|
||||
try:
|
||||
del store[k]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
store[k] = snap[k]
|
||||
super(_BackendGuard, self).tearDown()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _preserve(target, *keys):
|
||||
"""Save the given keys of an AttribDict (conf/kb), then restore on exit.
|
||||
|
||||
Missing keys are restored to absent so a test can't leak a brand-new field.
|
||||
"""
|
||||
saved = {}
|
||||
for key in keys:
|
||||
saved[key] = target[key] if key in target else _SENTINEL
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for key in keys:
|
||||
if saved[key] is _SENTINEL:
|
||||
try:
|
||||
del target[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
target[key] = saved[key]
|
||||
|
||||
|
||||
class TestSetTechnique(unittest.TestCase):
|
||||
def test_letters_to_ints(self):
|
||||
# BEUST(Q) letters map to the PAYLOAD.TECHNIQUE ints (B=1,E=2,U=6,S=4,T=5)
|
||||
with _preserve(conf, "technique"):
|
||||
conf.technique = "BEUST"
|
||||
option._setTechnique()
|
||||
self.assertEqual(conf.technique, [1, 2, 6, 4, 5])
|
||||
|
||||
def test_lowercase_accepted(self):
|
||||
with _preserve(conf, "technique"):
|
||||
conf.technique = "bt"
|
||||
option._setTechnique()
|
||||
self.assertEqual(conf.technique, [1, 5])
|
||||
|
||||
def test_invalid_letter_raises(self):
|
||||
with _preserve(conf, "technique"):
|
||||
conf.technique = "X"
|
||||
self.assertRaises(SqlmapSyntaxException, option._setTechnique)
|
||||
|
||||
def test_already_list_left_alone(self):
|
||||
# non-string (already-normalized) value is a no-op
|
||||
with _preserve(conf, "technique"):
|
||||
conf.technique = [1, 6]
|
||||
option._setTechnique()
|
||||
self.assertEqual(conf.technique, [1, 6])
|
||||
|
||||
|
||||
class TestSetDBMS(_BackendGuard):
|
||||
def test_none_noop(self):
|
||||
with _preserve(conf, "dbms"):
|
||||
conf.dbms = None
|
||||
option._setDBMS()
|
||||
self.assertIsNone(conf.dbms)
|
||||
|
||||
def test_plain_canonicalized(self):
|
||||
# input is lowercased then mapped to the canonical DBMS name via DBMS_ALIASES
|
||||
with _preserve(conf, "dbms"):
|
||||
conf.dbms = "MySQL"
|
||||
option._setDBMS()
|
||||
self.assertEqual(conf.dbms, "MySQL")
|
||||
|
||||
# non-identity case: an all-caps spelling must be lowercased and run
|
||||
# through the alias map back to the canonical "MySQL" (proves the
|
||||
# lower -> alias-lookup -> canonical transform actually executes, rather
|
||||
# than the test passing because input already equals output)
|
||||
with _preserve(conf, "dbms"):
|
||||
conf.dbms = "MYSQL"
|
||||
option._setDBMS()
|
||||
self.assertEqual(conf.dbms, "MySQL")
|
||||
|
||||
def test_alias_canonicalized(self):
|
||||
# "pgsql" is an alias for PostgreSQL
|
||||
with _preserve(conf, "dbms"):
|
||||
conf.dbms = "pgsql"
|
||||
option._setDBMS()
|
||||
self.assertEqual(conf.dbms.lower(), "postgresql")
|
||||
|
||||
def test_version_extracted_into_backend(self):
|
||||
# _setDBMS calls Backend.setVersion -> mutates kb.dbmsVersion; preserve it too
|
||||
with _preserve(conf, "dbms"), _preserve(kb, "dbmsVersion"):
|
||||
conf.dbms = "mysql 5.7"
|
||||
option._setDBMS()
|
||||
self.assertEqual(conf.dbms, "MySQL")
|
||||
self.assertIn("5.7", Backend.getVersion())
|
||||
|
||||
def test_unsupported_raises(self):
|
||||
with _preserve(conf, "dbms"):
|
||||
conf.dbms = "totallynotadbms"
|
||||
self.assertRaises(SqlmapUnsupportedDBMSException, option._setDBMS)
|
||||
|
||||
|
||||
class TestSetOS(_BackendGuard):
|
||||
def test_none_noop(self):
|
||||
with _preserve(conf, "os"):
|
||||
conf.os = None
|
||||
option._setOS() # must not raise
|
||||
|
||||
def test_valid_os_sets_backend(self):
|
||||
# _setOS calls Backend.setOs -> mutates kb.os; preserve it too
|
||||
with _preserve(conf, "os"), _preserve(kb, "os"):
|
||||
conf.os = "Linux"
|
||||
option._setOS()
|
||||
self.assertEqual(Backend.getOs(), "Linux")
|
||||
|
||||
def test_unsupported_os_raises(self):
|
||||
with _preserve(conf, "os"):
|
||||
conf.os = "plan9"
|
||||
self.assertRaises(SqlmapUnsupportedDBMSException, option._setOS)
|
||||
|
||||
|
||||
class TestSetDBMSAuthentication(unittest.TestCase):
|
||||
def test_none_noop(self):
|
||||
with _preserve(conf, "dbmsCred", "dbmsUsername", "dbmsPassword"):
|
||||
conf.dbmsCred = None
|
||||
option._setDBMSAuthentication()
|
||||
# nothing populated
|
||||
self.assertIsNone(conf.get("dbmsUsername"))
|
||||
|
||||
def test_splits_user_password(self):
|
||||
with _preserve(conf, "dbmsCred", "dbmsUsername", "dbmsPassword"):
|
||||
conf.dbmsCred = "root:secret"
|
||||
option._setDBMSAuthentication()
|
||||
self.assertEqual(conf.dbmsUsername, "root")
|
||||
self.assertEqual(conf.dbmsPassword, "secret")
|
||||
|
||||
def test_empty_password_allowed(self):
|
||||
with _preserve(conf, "dbmsCred", "dbmsUsername", "dbmsPassword"):
|
||||
conf.dbmsCred = "sa:"
|
||||
option._setDBMSAuthentication()
|
||||
self.assertEqual(conf.dbmsUsername, "sa")
|
||||
self.assertEqual(conf.dbmsPassword, "")
|
||||
|
||||
|
||||
class TestSetThreads(unittest.TestCase):
|
||||
def test_zero_becomes_one(self):
|
||||
with _preserve(conf, "threads"):
|
||||
conf.threads = 0
|
||||
option._setThreads()
|
||||
self.assertEqual(conf.threads, 1)
|
||||
|
||||
def test_negative_becomes_one(self):
|
||||
with _preserve(conf, "threads"):
|
||||
conf.threads = -5
|
||||
option._setThreads()
|
||||
self.assertEqual(conf.threads, 1)
|
||||
|
||||
def test_non_int_becomes_one(self):
|
||||
with _preserve(conf, "threads"):
|
||||
conf.threads = None
|
||||
option._setThreads()
|
||||
self.assertEqual(conf.threads, 1)
|
||||
|
||||
def test_positive_int_preserved(self):
|
||||
with _preserve(conf, "threads"):
|
||||
conf.threads = 7
|
||||
option._setThreads()
|
||||
self.assertEqual(conf.threads, 7)
|
||||
|
||||
|
||||
class TestSetPrefixSuffix(unittest.TestCase):
|
||||
def test_no_prefix_suffix_noop(self):
|
||||
with _preserve(conf, "prefix", "suffix", "boundaries"):
|
||||
conf.prefix = None
|
||||
conf.suffix = None
|
||||
conf.boundaries = []
|
||||
option._setPrefixSuffix()
|
||||
self.assertEqual(conf.boundaries, [])
|
||||
|
||||
def test_builds_single_boundary(self):
|
||||
with _preserve(conf, "prefix", "suffix", "boundaries"):
|
||||
conf.prefix = "')"
|
||||
conf.suffix = "-- -"
|
||||
conf.boundaries = []
|
||||
option._setPrefixSuffix()
|
||||
self.assertEqual(len(conf.boundaries), 1)
|
||||
b = conf.boundaries[0]
|
||||
self.assertEqual(b.prefix, "')")
|
||||
self.assertEqual(b.suffix, "-- -")
|
||||
self.assertEqual(b.level, 1)
|
||||
|
||||
def test_ptype_single_quote(self):
|
||||
with _preserve(conf, "prefix", "suffix", "boundaries"):
|
||||
conf.prefix = "'"
|
||||
conf.suffix = "'"
|
||||
conf.boundaries = []
|
||||
option._setPrefixSuffix()
|
||||
self.assertEqual(conf.boundaries[0].ptype, 2)
|
||||
|
||||
def test_ptype_double_quote(self):
|
||||
with _preserve(conf, "prefix", "suffix", "boundaries"):
|
||||
conf.prefix = '"'
|
||||
conf.suffix = '"'
|
||||
conf.boundaries = []
|
||||
option._setPrefixSuffix()
|
||||
self.assertEqual(conf.boundaries[0].ptype, 4)
|
||||
|
||||
def test_ptype_plain(self):
|
||||
with _preserve(conf, "prefix", "suffix", "boundaries"):
|
||||
conf.prefix = " "
|
||||
conf.suffix = ""
|
||||
conf.boundaries = []
|
||||
option._setPrefixSuffix()
|
||||
self.assertEqual(conf.boundaries[0].ptype, 1)
|
||||
|
||||
|
||||
class TestSetHostname(unittest.TestCase):
|
||||
def test_extracts_host(self):
|
||||
with _preserve(conf, "url", "hostname"):
|
||||
conf.url = "http://www.example.com:8080/page?id=1"
|
||||
option._setHostname()
|
||||
self.assertEqual(conf.hostname, "www.example.com")
|
||||
|
||||
def test_no_url_noop(self):
|
||||
with _preserve(conf, "url", "hostname"):
|
||||
conf.url = None
|
||||
conf.hostname = "preexisting"
|
||||
option._setHostname()
|
||||
self.assertEqual(conf.hostname, "preexisting")
|
||||
|
||||
|
||||
class TestSetHTTPHeaderSetters(unittest.TestCase):
|
||||
def test_referer_appended(self):
|
||||
with _preserve(conf, "referer", "httpHeaders"):
|
||||
conf.httpHeaders = []
|
||||
conf.referer = "http://ref.example/"
|
||||
option._setHTTPReferer()
|
||||
self.assertIn((HTTP_HEADER.REFERER, "http://ref.example/"), conf.httpHeaders)
|
||||
|
||||
def test_referer_none_noop(self):
|
||||
with _preserve(conf, "referer", "httpHeaders"):
|
||||
conf.httpHeaders = []
|
||||
conf.referer = None
|
||||
option._setHTTPReferer()
|
||||
self.assertEqual(conf.httpHeaders, [])
|
||||
|
||||
def test_host_appended(self):
|
||||
with _preserve(conf, "host", "httpHeaders"):
|
||||
conf.httpHeaders = []
|
||||
conf.host = "victim.local"
|
||||
option._setHTTPHost()
|
||||
self.assertIn((HTTP_HEADER.HOST, "victim.local"), conf.httpHeaders)
|
||||
|
||||
def test_cookie_appended(self):
|
||||
with _preserve(conf, "cookie", "httpHeaders"):
|
||||
conf.httpHeaders = []
|
||||
conf.cookie = "SESSION=abc"
|
||||
option._setHTTPCookies()
|
||||
self.assertIn((HTTP_HEADER.COOKIE, "SESSION=abc"), conf.httpHeaders)
|
||||
|
||||
|
||||
class TestSetHTTPUserAgent(unittest.TestCase):
|
||||
def test_explicit_agent(self):
|
||||
with _preserve(conf, "agent", "mobile", "randomAgent", "httpHeaders"):
|
||||
conf.httpHeaders = []
|
||||
conf.mobile = False
|
||||
conf.randomAgent = False
|
||||
conf.agent = "MyCustomUA/1.0"
|
||||
option._setHTTPUserAgent()
|
||||
self.assertIn((HTTP_HEADER.USER_AGENT, "MyCustomUA/1.0"), conf.httpHeaders)
|
||||
|
||||
def test_default_agent_when_unset(self):
|
||||
with _preserve(conf, "agent", "mobile", "randomAgent", "httpHeaders"):
|
||||
conf.httpHeaders = []
|
||||
conf.mobile = False
|
||||
conf.randomAgent = False
|
||||
conf.agent = None
|
||||
option._setHTTPUserAgent()
|
||||
self.assertIn((HTTP_HEADER.USER_AGENT, DEFAULT_USER_AGENT), conf.httpHeaders)
|
||||
|
||||
def test_existing_ua_not_duplicated(self):
|
||||
with _preserve(conf, "agent", "mobile", "randomAgent", "httpHeaders"):
|
||||
conf.httpHeaders = [(HTTP_HEADER.USER_AGENT, "Already/1.0")]
|
||||
conf.mobile = False
|
||||
conf.randomAgent = False
|
||||
conf.agent = None
|
||||
option._setHTTPUserAgent()
|
||||
uas = [v for (h, v) in conf.httpHeaders if h.upper() == HTTP_HEADER.USER_AGENT.upper()]
|
||||
self.assertEqual(uas, ["Already/1.0"])
|
||||
|
||||
|
||||
class TestSetHTTPExtraHeaders(unittest.TestCase):
|
||||
def test_parses_newline_separated(self):
|
||||
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
|
||||
conf.httpHeaders = []
|
||||
conf.headers = "X-Foo: bar\nX-Baz: qux"
|
||||
option._setHTTPExtraHeaders()
|
||||
self.assertIn(("X-Foo", "bar"), conf.httpHeaders)
|
||||
self.assertIn(("X-Baz", "qux"), conf.httpHeaders)
|
||||
|
||||
def test_escaped_newline_form(self):
|
||||
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
|
||||
conf.httpHeaders = []
|
||||
conf.headers = "X-A: 1\\nX-B: 2"
|
||||
option._setHTTPExtraHeaders()
|
||||
self.assertIn(("X-A", "1"), conf.httpHeaders)
|
||||
self.assertIn(("X-B", "2"), conf.httpHeaders)
|
||||
|
||||
def test_invalid_header_raises(self):
|
||||
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
|
||||
conf.httpHeaders = []
|
||||
conf.headers = "no-colon-here"
|
||||
self.assertRaises(SqlmapSyntaxException, option._setHTTPExtraHeaders)
|
||||
|
||||
def test_no_headers_adds_cache_control(self):
|
||||
# with no explicit headers and no requestFile, a Cache-Control:no-cache is appended
|
||||
with _preserve(conf, "headers", "httpHeaders", "requestFile", "encoding"):
|
||||
conf.httpHeaders = []
|
||||
conf.headers = None
|
||||
conf.requestFile = None
|
||||
conf.encoding = None
|
||||
option._setHTTPExtraHeaders()
|
||||
self.assertIn((HTTP_HEADER.CACHE_CONTROL, "no-cache"), conf.httpHeaders)
|
||||
|
||||
|
||||
class TestNormalizeOptions(unittest.TestCase):
|
||||
def test_integer_coercion(self):
|
||||
# 'threads' is an INTEGER option; a string value is coerced to int in place
|
||||
opts = {"threads": "5"}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertEqual(opts["threads"], 5)
|
||||
|
||||
def test_bad_integer_becomes_zero(self):
|
||||
opts = {"threads": "notanumber"}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertEqual(opts["threads"], 0)
|
||||
|
||||
def test_none_left_alone(self):
|
||||
opts = {"threads": None}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertIsNone(opts["threads"])
|
||||
|
||||
def test_unknown_key_untouched(self):
|
||||
opts = {"definitelyNotAnOption": "value"}
|
||||
option._normalizeOptions(opts)
|
||||
self.assertEqual(opts["definitelyNotAnOption"], "value")
|
||||
|
||||
|
||||
class TestSetVerbosity(unittest.TestCase):
|
||||
def _restore_logger(self):
|
||||
return _preserve(conf, "verbose", "eta")
|
||||
|
||||
def test_none_becomes_one(self):
|
||||
saved_level = logger.level
|
||||
try:
|
||||
with self._restore_logger():
|
||||
conf.verbose = None
|
||||
conf.eta = False
|
||||
option.setVerbosity()
|
||||
self.assertEqual(conf.verbose, 1)
|
||||
self.assertEqual(logger.level, logging.INFO)
|
||||
finally:
|
||||
logger.setLevel(saved_level)
|
||||
|
||||
def test_zero_sets_error_level(self):
|
||||
saved_level = logger.level
|
||||
try:
|
||||
with self._restore_logger():
|
||||
conf.verbose = 0
|
||||
conf.eta = False
|
||||
option.setVerbosity()
|
||||
self.assertEqual(logger.level, logging.ERROR)
|
||||
finally:
|
||||
logger.setLevel(saved_level)
|
||||
|
||||
def test_two_sets_debug_level(self):
|
||||
saved_level = logger.level
|
||||
try:
|
||||
with self._restore_logger():
|
||||
conf.verbose = 2
|
||||
conf.eta = False
|
||||
option.setVerbosity()
|
||||
self.assertEqual(logger.level, logging.DEBUG)
|
||||
finally:
|
||||
logger.setLevel(saved_level)
|
||||
|
||||
|
||||
class TestCleanupOptions(_BackendGuard):
|
||||
"""_cleanupOptions touches a huge number of conf fields; preserve broadly."""
|
||||
|
||||
# the subset of conf keys these tests read or write
|
||||
_KEYS = (
|
||||
"encoding", "eta", "testParameter", "ignoreCode", "abortCode",
|
||||
"paramFilter", "base64Parameter", "agent", "user", "rParam",
|
||||
"paramDel", "skip", "cookie", "delay", "url", "fileRead",
|
||||
"fileWrite", "fileDest", "msfPath", "tmpPath", "googleDork",
|
||||
"logFile", "bulkFile", "forms", "crawlDepth", "stdinPipe",
|
||||
"multipleTargets", "optimize", "os", "forceDbms", "dbms",
|
||||
"uValues", "uCols", "testFilter", "csrfToken", "testSkip",
|
||||
"tor", "timeSec", "retries", "code", "csvDel", "torPort",
|
||||
"torType", "outputDir", "string", "getAll", "noCast",
|
||||
"dumpFormat", "col", "exclude", "binaryFields", "proxy",
|
||||
"proxyFile", "dummy", "batch", "scope",
|
||||
)
|
||||
|
||||
def _base(self):
|
||||
"""Set the cleanup-relevant conf fields to inert defaults, then let the
|
||||
individual test override the one(s) it cares about."""
|
||||
for key in self._KEYS:
|
||||
conf[key] = None
|
||||
conf.eta = False
|
||||
conf.optimize = False
|
||||
conf.tor = False
|
||||
conf.getAll = False
|
||||
conf.noCast = False
|
||||
conf.dummy = False
|
||||
conf.batch = False
|
||||
conf.timeSec = 5
|
||||
conf.retries = 3
|
||||
conf.multipleTargets = False
|
||||
|
||||
def test_test_parameter_split(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.testParameter = "id,name"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.testParameter, ["id", "name"])
|
||||
|
||||
def test_empty_test_parameter_becomes_list(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.testParameter = None
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.testParameter, [])
|
||||
|
||||
def test_ignore_code_wildcard(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.ignoreCode = IGNORE_CODE_WILDCARD
|
||||
option._cleanupOptions()
|
||||
self.assertIn(404, conf.ignoreCode)
|
||||
self.assertIn(0, conf.ignoreCode)
|
||||
|
||||
def test_ignore_code_list(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.ignoreCode = "401,403"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.ignoreCode, [401, 403])
|
||||
|
||||
def test_ignore_code_invalid_raises(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.ignoreCode = "abc"
|
||||
self.assertRaises(SqlmapSyntaxException, option._cleanupOptions)
|
||||
|
||||
def test_abort_code_list(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.abortCode = "500,502"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.abortCode, [500, 502])
|
||||
|
||||
def test_param_filter_uppercased_and_split(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.paramFilter = "get,post"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.paramFilter, ["GET", "POST"])
|
||||
|
||||
def test_skip_split(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.skip = "a, b ,c"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.skip, ["a", "b", "c"])
|
||||
|
||||
def test_url_scheme_prepended(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.url = "example.com/page?id=1"
|
||||
option._cleanupOptions()
|
||||
self.assertTrue(conf.url.startswith("http://"), msg=conf.url)
|
||||
|
||||
def test_url_credentials_extracted_to_basic_auth(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"), \
|
||||
_preserve(conf, "authType", "authCred"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.authType = None
|
||||
conf.authCred = None
|
||||
conf.url = "http://user:pass@example.com/page?id=1"
|
||||
option._cleanupOptions()
|
||||
self.assertNotIn("user:pass@", conf.url)
|
||||
self.assertEqual(conf.authCred, "user:pass")
|
||||
|
||||
def test_random_pool_from_rparam(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.rParam = "id=1,2,3"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.rParam, ["id"])
|
||||
self.assertEqual(kb.randomPool["id"], ["1", "2", "3"])
|
||||
|
||||
def test_code_cast_to_int(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.code = "200"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.code, 200)
|
||||
|
||||
def test_dump_format_uppercased(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.dumpFormat = "csv"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.dumpFormat, "CSV")
|
||||
|
||||
def test_uvalues_sets_ucols(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.uValues = "NULL,1,2"
|
||||
option._cleanupOptions()
|
||||
self.assertEqual(conf.uCols, "3-3")
|
||||
|
||||
def test_multiple_targets_flag(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.crawlDepth = 2
|
||||
option._cleanupOptions()
|
||||
self.assertTrue(conf.multipleTargets)
|
||||
|
||||
def test_proxy_disables_precon(self):
|
||||
with _preserve(conf, *self._KEYS), _preserve(kb, "explicitSettings", "randomPool", "dbmsFilter", "adjustTimeDelay"), \
|
||||
_preserve(conf, "disablePrecon"):
|
||||
kb.explicitSettings = set()
|
||||
kb.randomPool = {}
|
||||
self._base()
|
||||
conf.disablePrecon = False
|
||||
conf.proxy = "http://127.0.0.1:8080"
|
||||
option._cleanupOptions()
|
||||
self.assertTrue(conf.disablePrecon)
|
||||
|
||||
|
||||
class TestBasicOptionValidation(_BackendGuard):
|
||||
"""_basicOptionValidation reads a wide swathe of conf; set up a benign baseline
|
||||
and flip one offending pair per test."""
|
||||
|
||||
_KEYS = (
|
||||
"limitStart", "limitStop", "level", "risk", "firstChar", "lastChar",
|
||||
"textOnly", "nullConnection", "uValues", "uChar", "base64Parameter",
|
||||
"tamper", "eta", "verbose", "direct", "url", "dbms", "tor", "proxy",
|
||||
"ignoreProxy", "regexp", "timeSec", "torPort", "torType", "dumpFormat",
|
||||
"technique", "threads", "predictOutput", "optimize", "csrfToken",
|
||||
"csrfUrl", "string", "notString", "noCast", "hexConvert",
|
||||
)
|
||||
|
||||
def _base(self):
|
||||
for key in self._KEYS:
|
||||
conf[key] = None
|
||||
conf.textOnly = False
|
||||
conf.nullConnection = False
|
||||
conf.eta = False
|
||||
conf.direct = False
|
||||
conf.tor = False
|
||||
conf.ignoreProxy = False
|
||||
conf.predictOutput = False
|
||||
conf.optimize = False
|
||||
conf.noCast = False
|
||||
conf.hexConvert = False
|
||||
conf.verbose = 1
|
||||
conf.timeSec = 5
|
||||
conf.torPort = None
|
||||
conf.torType = "SOCKS5"
|
||||
conf.dumpFormat = "CSV"
|
||||
conf.technique = [1, 2, 6, 4, 5]
|
||||
conf.threads = 1
|
||||
|
||||
def test_clean_baseline_passes(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
option._basicOptionValidation() # must not raise
|
||||
|
||||
def test_bad_level_raises(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.level = 99
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_bad_risk_raises(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.risk = 9
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_textonly_nullconnection_incompatible(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.textOnly = True
|
||||
conf.nullConnection = True
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_direct_url_incompatible(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.direct = "mysql://u:p@h/db"
|
||||
conf.url = "http://x/?id=1"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_empty_technique_raises(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.technique = []
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_bad_regexp_raises(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.regexp = "("
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_bad_dump_format_raises(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.dumpFormat = "BOGUS"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_bad_tor_port_raises(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.torPort = 70000
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_uvalues_uchar_incompatible(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.uValues = "NULL,1"
|
||||
conf.uChar = "NULL"
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
def test_tor_ignoreproxy_incompatible(self):
|
||||
with _preserve(conf, *self._KEYS):
|
||||
self._base()
|
||||
conf.tor = True
|
||||
conf.ignoreProxy = True
|
||||
self.assertRaises(SqlmapSyntaxException, option._basicOptionValidation)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -20,7 +20,7 @@ from _testutils import bootstrap
|
|||
bootstrap()
|
||||
|
||||
from lib.core.settings import (JSON_RECOGNITION_REGEX, JSON_LIKE_RECOGNITION_REGEX,
|
||||
XML_RECOGNITION_REGEX, PAYLOAD_DELIMITER, DEFAULT_GET_POST_DELIMITER,
|
||||
XML_RECOGNITION_REGEX, PAYLOAD_DELIMITER,
|
||||
CUSTOM_INJECTION_MARK_CHAR)
|
||||
|
||||
# The real source marks injection points with kb.customInjectionMark, which defaults to
|
||||
|
|
|
|||
88
tests/test_request_basic.py
Normal file
88
tests/test_request_basic.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Unit coverage for PURE functions in lib/request/basic.py.
|
||||
|
||||
These exercise getHeuristicCharEncoding (with its kb.cache.encoding memoization)
|
||||
and decodePage's charset + HTML-entity decoding branches, in isolation - WITHOUT
|
||||
touching the network, the DBMS or any interactive prompt.
|
||||
|
||||
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
|
||||
|
||||
class TestBasicHeuristicCharEncoding(unittest.TestCase):
|
||||
def test_ascii(self):
|
||||
from lib.request.basic import getHeuristicCharEncoding
|
||||
self.assertEqual(getHeuristicCharEncoding(b"<html></html>"), "ascii")
|
||||
|
||||
def test_cache_hit_returns_same(self):
|
||||
from lib.request.basic import getHeuristicCharEncoding
|
||||
page = b"<html>hello world</html>"
|
||||
first = getHeuristicCharEncoding(page)
|
||||
# second call for identical page must come back identical (and from cache)
|
||||
self.assertEqual(getHeuristicCharEncoding(page), first)
|
||||
key = (len(page), hash(page))
|
||||
self.assertEqual(kb.cache.encoding.get(key), first)
|
||||
|
||||
|
||||
class TestBasicDecodePage(unittest.TestCase):
|
||||
"""decodePage charset + HTML-entity decoding branches."""
|
||||
|
||||
def setUp(self):
|
||||
self._old_encoding = conf.encoding
|
||||
self._old_null = conf.nullConnection
|
||||
conf.nullConnection = False
|
||||
|
||||
def tearDown(self):
|
||||
conf.encoding = self._old_encoding
|
||||
conf.nullConnection = self._old_null
|
||||
|
||||
def test_html_entity_amp(self):
|
||||
from lib.request.basic import decodePage
|
||||
from lib.core.common import getText
|
||||
conf.encoding = None
|
||||
self.assertEqual(
|
||||
getText(decodePage(b"<html>foo&bar</html>", None, "text/html; charset=utf-8")),
|
||||
"<html>foo&bar</html>",
|
||||
)
|
||||
|
||||
def test_numeric_hex_entity_tab(self):
|
||||
from lib.request.basic import decodePage
|
||||
from lib.core.common import getText
|
||||
conf.encoding = None
|
||||
self.assertEqual(getText(decodePage(b"	", 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")), "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -4,14 +4,15 @@
|
|||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Unit tests for plugins/generic/search.py (Search) and plugins/generic/entries.py
|
||||
(Entries), exercising searchDb / searchTable / searchColumn and dumpTable by
|
||||
MOCKING the injection layer (lib.request.inject.getValue) and the dumper.
|
||||
Unit tests for plugins/generic/search.py (Search), exercising searchDb /
|
||||
searchTable / searchColumn by MOCKING the injection layer
|
||||
(lib.request.inject.getValue) and the dumper.
|
||||
|
||||
No network and no DBMS are involved: conf.direct=True selects the simple inband
|
||||
branches, inject.getValue is patched to return canned rows in the exact shape the
|
||||
methods parse, and conf.dumper is replaced with a recording stub so we can assert
|
||||
on what each method produced (kb.data caches / returned dicts).
|
||||
branches (TestSearch), or conf.direct=False with a BOOLEAN injection state selects
|
||||
the inference branches (TestSearchInference); inject.getValue is patched to return
|
||||
canned rows in the exact shape the methods parse, and conf.dumper is replaced with
|
||||
a recording stub so we can assert on what each method produced.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -24,10 +25,30 @@ from _testutils import bootstrap, set_dbms
|
|||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.enums import EXPECTED, PAYLOAD
|
||||
import plugins.generic.search as smod
|
||||
import plugins.generic.entries as emod
|
||||
from plugins.generic.search import Search
|
||||
from plugins.generic.entries import Entries
|
||||
|
||||
|
||||
def _inference_gv(count, sequence):
|
||||
"""Build an inject.getValue stub for blind inference branches.
|
||||
|
||||
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
|
||||
yields the next item from `sequence` wrapped as a single-cell row ([value]),
|
||||
cycling if exhausted. This mirrors the count-then-per-row contract of every
|
||||
isInferenceAvailable() branch.
|
||||
"""
|
||||
state = {"i": 0}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return str(count)
|
||||
val = sequence[state["i"] % len(sequence)]
|
||||
state["i"] += 1
|
||||
return [val]
|
||||
|
||||
return gv
|
||||
|
||||
|
||||
class _RecordingDumper(object):
|
||||
|
|
@ -102,29 +123,6 @@ class _TestSearch(Search):
|
|||
kb.data.cachedColumns[db][tbl][col] = "varchar"
|
||||
|
||||
|
||||
class _TestEntries(Entries):
|
||||
"""Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables)."""
|
||||
|
||||
def __init__(self):
|
||||
Entries.__init__(self)
|
||||
self.getColumnsResult = {} # {db: {tbl: {col: type}}}
|
||||
self.getTablesResult = {} # value assigned to kb.data.cachedTables
|
||||
self.getColumnsCalls = []
|
||||
|
||||
def forceDbmsEnum(self):
|
||||
pass
|
||||
|
||||
def getCurrentDb(self):
|
||||
return "testdb"
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
self.getColumnsCalls.append((conf.db, conf.tbl))
|
||||
kb.data.cachedColumns = dict(self.getColumnsResult)
|
||||
|
||||
def getTables(self, bruteForce=None):
|
||||
kb.data.cachedTables = dict(self.getTablesResult)
|
||||
|
||||
|
||||
class _SearchEnumBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Save mutated globals
|
||||
|
|
@ -362,113 +360,190 @@ class TestSearch(_SearchEnumBase):
|
|||
self.assertRaises(SqlmapMissingMandatoryOptionException, s.search)
|
||||
|
||||
|
||||
class TestEntries(_SearchEnumBase):
|
||||
def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")):
|
||||
e = _TestEntries()
|
||||
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
|
||||
return e
|
||||
# --------------------------------------------------------------------------- #
|
||||
# search.py - inference (blind) paths
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
# --- dumpTable: inband (conf.direct) ------------------------------------
|
||||
class _TestSearchInf(Search):
|
||||
excludeDbsList = ["information_schema", "mysql"]
|
||||
|
||||
def test_dump_table_inband_rows(self):
|
||||
e = self._entries_with_cols(cols=("id", "name"))
|
||||
def __init__(self):
|
||||
Search.__init__(self)
|
||||
self.like = ('2', "='%s'") # exact match (colConsider '2')
|
||||
self.dumpFoundTablesCalls = []
|
||||
self.dumpFoundColumnCalls = []
|
||||
|
||||
def likeOrExact(self, what):
|
||||
return self.like
|
||||
|
||||
def forceDbmsEnum(self):
|
||||
pass
|
||||
|
||||
def getCurrentDb(self):
|
||||
return "testdb"
|
||||
|
||||
def dumpFoundTables(self, tables):
|
||||
self.dumpFoundTablesCalls.append(tables)
|
||||
|
||||
def dumpFoundColumn(self, dbs, foundCols, colConsider):
|
||||
self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider))
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
db, tbl, col = conf.db, conf.tbl, conf.col
|
||||
if db and tbl:
|
||||
kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {})
|
||||
kb.data.cachedColumns[db][tbl][col] = "varchar"
|
||||
|
||||
|
||||
class _RecDumper(object):
|
||||
def __init__(self):
|
||||
self.listed = []
|
||||
self.dbTablesArg = None
|
||||
self.dbColumnsArg = None
|
||||
|
||||
def lister(self, header, elements, content_type=None, sort=True):
|
||||
self.listed.append((header, list(elements) if elements else []))
|
||||
|
||||
def dbTables(self, dbTables):
|
||||
self.dbTablesArg = dbTables
|
||||
|
||||
def dbColumns(self, dbColumnsDict, colConsider, dbs):
|
||||
self.dbColumnsArg = (dbColumnsDict, colConsider, dbs)
|
||||
|
||||
|
||||
class _SearchBase(unittest.TestCase):
|
||||
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs",
|
||||
"exclude", "search")
|
||||
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
|
||||
self._saved_dumper = conf.get("dumper")
|
||||
self._gv = smod.inject.getValue
|
||||
self._readInput = smod.readInput
|
||||
self._saved_has_is = kb.data.get("has_information_schema")
|
||||
self._saved_cachedColumns = kb.data.get("cachedColumns")
|
||||
self._saved_hintValue = kb.get("hintValue")
|
||||
self._saved_injection_data = kb.injection.data
|
||||
|
||||
set_dbms("MySQL")
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
conf.excludeSysDbs = False
|
||||
conf.exclude = None
|
||||
conf.search = True
|
||||
conf.dumper = _RecDumper()
|
||||
|
||||
kb.data.has_information_schema = True
|
||||
kb.data.cachedColumns = {}
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
conf.dumper = self._saved_dumper
|
||||
smod.inject.getValue = self._gv
|
||||
smod.readInput = self._readInput
|
||||
kb.data.has_information_schema = self._saved_has_is
|
||||
kb.data.cachedColumns = self._saved_cachedColumns
|
||||
kb.hintValue = self._saved_hintValue
|
||||
kb.injection.data = self._saved_injection_data
|
||||
|
||||
|
||||
class TestSearchInference(_SearchBase):
|
||||
def test_search_db_inference(self):
|
||||
# Blind searchDb: count of matching dbs, then one db name per index.
|
||||
s = _TestSearchInf()
|
||||
conf.db = "testdb"
|
||||
smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"])
|
||||
s.searchDb()
|
||||
self.assertEqual(conf.dumper.listed[-1][0], "found databases")
|
||||
self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"])
|
||||
|
||||
def test_search_db_inference_no_match(self):
|
||||
# Count fails (non-numeric) => no databases appended, empty listing.
|
||||
s = _TestSearchInf()
|
||||
conf.db = "ghost"
|
||||
smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails"))
|
||||
s.searchDb()
|
||||
self.assertEqual(conf.dumper.listed[-1][1], [])
|
||||
|
||||
def test_search_table_inference_grouped(self):
|
||||
# Blind searchTable, no conf.db: outer count of dbs holding the table, then
|
||||
# per-db a name, then per-db a count of matching tables, then table names.
|
||||
s = _TestSearchInf()
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
# MySQL inband dump returns a list of [colVal, colVal] rows.
|
||||
emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]]
|
||||
conf.db = None
|
||||
|
||||
e.dumpTable()
|
||||
# Sequencing by the EXPECTED.INT counts + the per-index string results.
|
||||
# 1st count: number of databases with the table -> 1
|
||||
# 1st db name -> "testdb"
|
||||
# 2nd count: number of tables in testdb -> 1
|
||||
# table name -> "users"
|
||||
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
|
||||
|
||||
dumped = conf.dumper.tableValues[-1]
|
||||
self.assertEqual(dumped["__infos__"]["count"], 2)
|
||||
self.assertEqual(dumped["__infos__"]["table"], "users")
|
||||
self.assertEqual(dumped["__infos__"]["db"], "testdb")
|
||||
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
|
||||
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
v = seq["counts"][seq["ci"] % len(seq["counts"])]
|
||||
seq["ci"] += 1
|
||||
return v
|
||||
v = seq["vals"][seq["vi"] % len(seq["vals"])]
|
||||
seq["vi"] += 1
|
||||
return [v]
|
||||
|
||||
def test_dump_table_uses_foundData(self):
|
||||
e = _TestEntries()
|
||||
conf.db = "testdb"
|
||||
smod.inject.getValue = gv
|
||||
s.searchTable()
|
||||
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]})
|
||||
self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]})
|
||||
|
||||
def test_search_table_mysql_lt5_bruteforce_decline(self):
|
||||
# MySQL < 5 forces the bruteforce path; declining the prompt returns None
|
||||
# without any injection.
|
||||
s = _TestSearchInf()
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["x"]]
|
||||
foundData = {"testdb": {"users": {"id": "int"}}}
|
||||
conf.db = None
|
||||
kb.data.has_information_schema = False
|
||||
smod.readInput = lambda *a, **k: "N"
|
||||
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
|
||||
self.assertIsNone(s.searchTable())
|
||||
|
||||
e.dumpTable(foundData=foundData)
|
||||
|
||||
# foundData short-circuits column discovery: getColumns must not run.
|
||||
self.assertEqual(e.getColumnsCalls, [])
|
||||
self.assertIn("id", conf.dumper.tableValues[-1])
|
||||
|
||||
def test_dump_table_no_columns_skips(self):
|
||||
e = _TestEntries()
|
||||
e.getColumnsResult = {} # discovery yields nothing
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "ghost"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
|
||||
|
||||
e.dumpTable()
|
||||
# No columns => no values dumped.
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dump_table_empty_entries(self):
|
||||
e = self._entries_with_cols(cols=("id",))
|
||||
conf.db = "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: None # no rows
|
||||
|
||||
e.dumpTable()
|
||||
# Nothing retrieved => dumpedTable empty => dbTableValues not called.
|
||||
self.assertEqual(conf.dumper.tableValues, [])
|
||||
|
||||
def test_dump_table_current_db(self):
|
||||
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
|
||||
conf.db = None # triggers getCurrentDb() -> "testdb"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["7"]]
|
||||
|
||||
e.dumpTable()
|
||||
self.assertEqual(conf.db, "testdb")
|
||||
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"])
|
||||
|
||||
def test_dump_table_multiple_db_error(self):
|
||||
e = _TestEntries()
|
||||
conf.db = "a,b"
|
||||
conf.tbl = "users"
|
||||
conf.col = None
|
||||
from lib.core.exception import SqlmapMissingMandatoryOptionException
|
||||
self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable)
|
||||
|
||||
def test_dump_table_get_tables_when_no_tbl(self):
|
||||
e = _TestEntries()
|
||||
e.getTablesResult = {"testdb": ["users"]}
|
||||
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
|
||||
conf.db = "testdb"
|
||||
def test_search_column_inference(self):
|
||||
# Blind searchColumn, no db/tbl: count of dbs with the column, then db name;
|
||||
# then per-db count of tables with the column, then table name -> getColumns
|
||||
# folds the column into dbs.
|
||||
s = _TestSearchInf()
|
||||
conf.col = "password"
|
||||
conf.db = None
|
||||
conf.tbl = None
|
||||
conf.col = None
|
||||
emod.inject.getValue = lambda *a, **k: [["42"]]
|
||||
|
||||
e.dumpTable()
|
||||
# Tables were discovered via getTables, then the row dumped.
|
||||
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"])
|
||||
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
|
||||
|
||||
# --- dumpAll: single-db delegation --------------------------------------
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
v = seq["counts"][seq["ci"] % len(seq["counts"])]
|
||||
seq["ci"] += 1
|
||||
return v
|
||||
v = seq["vals"][seq["vi"] % len(seq["vals"])]
|
||||
seq["vi"] += 1
|
||||
return [v]
|
||||
|
||||
def test_dump_all_single_db_delegates(self):
|
||||
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
|
||||
# dumpAll with db set & tbl None must delegate straight to dumpTable.
|
||||
conf.db = "testdb"
|
||||
smod.inject.getValue = gv
|
||||
s.searchColumn()
|
||||
dbs = conf.dumper.dbColumnsArg[2]
|
||||
self.assertIn("testdb", dbs)
|
||||
self.assertIn("users", dbs["testdb"])
|
||||
self.assertIn("password", dbs["testdb"]["users"])
|
||||
|
||||
def test_search_column_mysql_lt5_bruteforce_decline(self):
|
||||
s = _TestSearchInf()
|
||||
conf.col = "password"
|
||||
conf.db = None
|
||||
conf.tbl = None
|
||||
conf.col = None
|
||||
e.getTablesResult = {"testdb": ["users"]}
|
||||
emod.inject.getValue = lambda *a, **k: [["9"]]
|
||||
|
||||
e.dumpAll()
|
||||
self.assertTrue(conf.dumper.tableValues)
|
||||
kb.data.has_information_schema = False
|
||||
smod.readInput = lambda *a, **k: "N"
|
||||
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
|
||||
# Declining returns None and never reaches dbColumns.
|
||||
self.assertIsNone(s.searchColumn())
|
||||
self.assertIsNone(conf.dumper.dbColumnsArg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ class TestCreateDirs(_TargetTestBase):
|
|||
self.assertIn("example.com", conf.filePath)
|
||||
|
||||
def test_target_dir_and_target_txt(self):
|
||||
out = self._outdir("t_out")
|
||||
self._outdir("t_out")
|
||||
conf.hostname = "example.com"
|
||||
conf.url = "http://example.com/?id=1"
|
||||
conf.data = None
|
||||
|
|
|
|||
|
|
@ -4,26 +4,29 @@
|
|||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Mocked-oracle / canned-input coverage for three self-contained injection engines:
|
||||
Mocked-oracle / canned-input coverage for the self-contained extraction /
|
||||
inference engines under lib/techniques/*:
|
||||
|
||||
* lib/techniques/union/use.py - _oneShotUnionUse value extraction + unionUse
|
||||
* lib/techniques/union/use.py - _oneShotUnionUse / unionUse / configUnion
|
||||
* lib/techniques/error/use.py - _oneShotErrorUse / _errorFields / errorUse
|
||||
* lib/techniques/ldap/inject.py - boolean-blind LDAP oracle + blind char inference
|
||||
* lib/techniques/graphql/inject.py - schema walk, query building, blind-SQLi inference
|
||||
* lib/techniques/blind/inference.py - bisection / queryOutputLength edge branches
|
||||
|
||||
The established pattern (see tests/test_inference.py, tests/test_union_engine.py) is
|
||||
followed: the network seam (Request.queryPage / Request.getPage / the per-module _send /
|
||||
_gqlSend) is replaced by a deterministic in-process oracle that answers against a known
|
||||
secret, so the REAL extraction / parsing / bisection logic runs with no live target,
|
||||
The established pattern (see tests/test_inference_engine.py,
|
||||
tests/test_union_engine.py) is followed: the network seam (Request.queryPage /
|
||||
Request.getPage / the per-module _send / _gqlSend) and the forge/escape chain are
|
||||
replaced by a deterministic in-process oracle that answers against a known secret,
|
||||
so the REAL extraction / parsing / bisection logic runs with no live target,
|
||||
no network and no DBMS.
|
||||
|
||||
configUnion is already covered by tests/test_inference.py - not duplicated here.
|
||||
|
||||
stdlib unittest only; works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
|
@ -32,14 +35,26 @@ bootstrap()
|
|||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.core.enums import PAYLOAD, PLACE
|
||||
from lib.core.common import decodeDbmsHexValue
|
||||
from lib.core.common import getCurrentThreadData
|
||||
from lib.core.common import hashDBWrite
|
||||
from lib.core.common import setTechnique
|
||||
from lib.core.enums import CHARSET_TYPE
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.enums import PLACE
|
||||
from lib.core.exception import SqlmapSyntaxException
|
||||
from lib.core.settings import PARTIAL_VALUE_MARKER
|
||||
from lib.core.agent import agent
|
||||
from lib.core.unescaper import unescaper
|
||||
from lib.request.connect import Connect
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.utils.hashdb import HashDB
|
||||
|
||||
import lib.techniques.union.use as uu
|
||||
import lib.techniques.error.use as eu
|
||||
import lib.techniques.ldap.inject as ldap
|
||||
import lib.techniques.graphql.inject as gql
|
||||
import lib.techniques.blind.inference as inf
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
|
|
@ -278,6 +293,497 @@ class TestUnionUse(_UnionCase):
|
|||
self.assertIsNone(value)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# UNION-based: lib/techniques/union/use.py (partial / LIMIT-loop branches)
|
||||
# ===========================================================================
|
||||
|
||||
# Distinct from the scalar _UNION_VECTOR / _UU_CONF / _UU_KB above: these drive the
|
||||
# partial / LIMIT-loop path (NEGATIVE where, forcePartial on, jsonAgg disabled).
|
||||
_UNION_VECTOR_LIMIT = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.NEGATIVE, False, False, None, None)
|
||||
|
||||
_UU_CONF_LIMIT = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None,
|
||||
"forcePartial": True, "disableJson": True, "binaryFields": None,
|
||||
"reportJson": False, "api": False, "threads": 1, "verbose": 0, "eta": False,
|
||||
"noTruncate": True, "uFrom": None}
|
||||
_UU_KB_LIMIT = {"jsonAggMode": False, "respTruncated": False, "unionDuplicates": False,
|
||||
"forcePartialUnion": False, "tableFrom": None, "unionTemplate": None,
|
||||
"nchar": False, "pageEncoding": None, "bruteMode": False, "partRun": None,
|
||||
"suppressResumeInfo": False, "threadContinue": True}
|
||||
|
||||
|
||||
class _UnionLimitCase(unittest.TestCase):
|
||||
"""Drive unionUse() down the partial / LIMIT-loop path (jsonAgg disabled, NEGATIVE where,
|
||||
forcePartial on). The forge chain is a pass-through; concatQuery records the per-row
|
||||
expression so the oracle can recover the LIMIT offset and answer from a known row set."""
|
||||
|
||||
def setUp(self):
|
||||
self._sc = {k: conf.get(k) for k in _UU_CONF_LIMIT}
|
||||
self._sk = {k: kb.get(k) for k in _UU_KB_LIMIT}
|
||||
self._sqp = Request.queryPage
|
||||
self._scounters = kb.get("counters")
|
||||
self._sinj_data = kb.injection.data
|
||||
self._shashdb = conf.get("hashDB")
|
||||
self._sbatch = conf.get("batch")
|
||||
self._s_forge = agent.forgeUnionQuery
|
||||
self._s_concat = agent.concatQuery
|
||||
self._s_payload = agent.payload
|
||||
self._s_escape = unescaper.escape
|
||||
self._s_lastexpr = getattr(agent, "_lastexpr", None)
|
||||
self._s_initTechnique = uu.initTechnique
|
||||
|
||||
for k, v in _UU_CONF_LIMIT.items():
|
||||
conf[k] = v
|
||||
for k, v in _UU_KB_LIMIT.items():
|
||||
kb[k] = v
|
||||
|
||||
conf.batch = True
|
||||
conf.hashDB = None
|
||||
kb.counters = {}
|
||||
|
||||
entry = AttribDict()
|
||||
entry.vector = _UNION_VECTOR_LIMIT
|
||||
entry.where = PAYLOAD.WHERE.NEGATIVE
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry}
|
||||
|
||||
# record the expression seen by each _oneShotUnionUse so the oracle can branch on it
|
||||
def rec_concat(expression, unpack=True):
|
||||
agent._lastexpr = expression
|
||||
return expression
|
||||
agent.concatQuery = rec_concat
|
||||
agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED"
|
||||
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD"
|
||||
unescaper.escape = lambda expression, *a, **k: expression
|
||||
uu.initTechnique = lambda technique=None: None
|
||||
|
||||
self._s_columns = os.environ.get("COLUMNS")
|
||||
os.environ["COLUMNS"] = "80"
|
||||
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._sc.items():
|
||||
conf[k] = v
|
||||
for k, v in self._sk.items():
|
||||
kb[k] = v
|
||||
conf.batch = self._sbatch
|
||||
Request.queryPage = self._sqp
|
||||
uu.Request.queryPage = self._sqp
|
||||
kb.counters = self._scounters
|
||||
kb.injection.data = self._sinj_data
|
||||
conf.hashDB = self._shashdb
|
||||
agent.forgeUnionQuery = self._s_forge
|
||||
agent.concatQuery = self._s_concat
|
||||
agent.payload = self._s_payload
|
||||
unescaper.escape = self._s_escape
|
||||
agent._lastexpr = self._s_lastexpr
|
||||
uu.initTechnique = self._s_initTechnique
|
||||
|
||||
if self._s_columns is None:
|
||||
os.environ.pop("COLUMNS", None)
|
||||
else:
|
||||
os.environ["COLUMNS"] = self._s_columns
|
||||
|
||||
def _install_row_oracle(self, rows, count=None):
|
||||
"""rows: list of tuples (per-row columns). Oracle answers COUNT and per-LIMIT rows
|
||||
from the recorded expression (agent._lastexpr), wrapping in real start/stop markers."""
|
||||
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
|
||||
total = count if count is not None else len(rows)
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
expr = getattr(agent, "_lastexpr", "") or ""
|
||||
if "COUNT" in expr.upper():
|
||||
body = str(total)
|
||||
else:
|
||||
m = re.search(r"LIMIT (\d+),1", expr)
|
||||
idx = int(m.group(1)) if m else 0
|
||||
row = rows[idx] if 0 <= idx < len(rows) else ("?",)
|
||||
body = delim.join(row)
|
||||
page = "%s%s%s" % (start, body, stop)
|
||||
return (page, AttribDict(), 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
uu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
|
||||
class TestUnionPartialDump(_UnionLimitCase):
|
||||
def test_multi_row_two_columns(self):
|
||||
rows = [("1", "alice"), ("2", "bob"), ("3", "carol")]
|
||||
self._install_row_oracle(rows)
|
||||
value = uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(list(value), [["1", "alice"], ["2", "bob"], ["3", "carol"]])
|
||||
|
||||
def test_multi_row_single_column(self):
|
||||
rows = [("alice",), ("bob",)]
|
||||
self._install_row_oracle(rows)
|
||||
value = uu.unionUse("SELECT name FROM users")
|
||||
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["alice", "bob"])
|
||||
|
||||
def test_query_count_matches_rows(self):
|
||||
# one COUNT query + one query per row = 4 UNION requests for 3 rows
|
||||
rows = [("1", "a"), ("2", "b"), ("3", "c")]
|
||||
self._install_row_oracle(rows)
|
||||
uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1 + len(rows))
|
||||
|
||||
def test_count_returns_zero_empty(self):
|
||||
# COUNT yields "0" -> empty-table sentinel (the function returns []), no row queries
|
||||
self._install_row_oracle([], count=0)
|
||||
value = uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(value, [])
|
||||
|
||||
def test_single_row_count_one(self):
|
||||
# COUNT yields "1": the multi-row thread loop is skipped, falls through to one one-shot
|
||||
rows = [("solo",)]
|
||||
self._install_row_oracle(rows, count=1)
|
||||
value = uu.unionUse("SELECT name FROM users")
|
||||
self.assertEqual(uu.unArrayizeValue(value), "solo")
|
||||
|
||||
def test_length_limited_window(self):
|
||||
# conf.limitStart/limitStop windowing (dump=True): only rows in [start, stop) survive.
|
||||
# With limitStart=2, limitStop=4 over a 5-row table the engine COUNTs then walks
|
||||
# offsets 1..3 -> rows index 1,2,3 -> "b","c","d".
|
||||
conf.forcePartial = False
|
||||
conf.limitStart = 2
|
||||
conf.limitStop = 4
|
||||
rows = [("a",), ("b",), ("c",), ("d",), ("e",)]
|
||||
self._install_row_oracle(rows, count=5)
|
||||
value = uu.unionUse("SELECT name FROM users", dump=True)
|
||||
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["b", "c", "d"])
|
||||
|
||||
|
||||
class TestOneShotUnionUseLimited(_UnionLimitCase):
|
||||
"""_oneShotUnionUse called directly with the `limited` flag set (the per-row caller's mode)."""
|
||||
|
||||
def test_limited_single_row(self):
|
||||
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
|
||||
body = delim.join(("7", "zed"))
|
||||
page = "%s%s%s" % (start, body, stop)
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
return (page, AttribDict(), 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
uu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
retVal = uu._oneShotUnionUse("SELECT id,name FROM t LIMIT 0,1", unpack=True, limited=True)
|
||||
self.assertEqual(retVal, page)
|
||||
# one wrapped multi-column entry -> one row of two columns
|
||||
self.assertEqual(list(uu.parseUnionPage(retVal)), [["7", "zed"]])
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# ERROR-based: lib/techniques/error/use.py
|
||||
# ===========================================================================
|
||||
|
||||
# An error injection vector is consumed by agent.prefixQuery/suffixQuery (here stubbed
|
||||
# to a pass-through that just yields the "[QUERY]" placeholder the engine substitutes into).
|
||||
_ERR_VECTOR = ("pref", "suff", 2, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None)
|
||||
|
||||
_ERR_CONF = {"hexConvert": False, "noEscape": None, "verbose": 0, "api": False,
|
||||
"reportJson": False, "limitStart": 0, "limitStop": 0, "noTruncate": True,
|
||||
"threads": 1, "eta": False}
|
||||
_ERR_KB = {"testMode": True, "safeCharEncode": False, "errorChunkLength": None,
|
||||
"fileReadMode": False, "bruteMode": False, "threadContinue": True,
|
||||
"suppressResumeInfo": False, "dumpTable": None}
|
||||
|
||||
|
||||
class _ErrorCase(unittest.TestCase):
|
||||
"""Stub the forge/escape/transport seam so _oneShotErrorUse's OWN parsing (marker
|
||||
extraction, trim repair, char restoration) is what is exercised."""
|
||||
|
||||
def setUp(self):
|
||||
self._sc = {k: conf.get(k) for k in _ERR_CONF}
|
||||
self._sk = {k: kb.get(k) for k in _ERR_KB}
|
||||
self._sqp = Request.queryPage
|
||||
self._scounters = kb.get("counters")
|
||||
self._stechnique = kb.get("technique")
|
||||
self._sinj_data = kb.injection.data
|
||||
self._shashdb = conf.get("hashDB")
|
||||
self._sbatch = conf.get("batch")
|
||||
|
||||
self._s_prefix = agent.prefixQuery
|
||||
self._s_suffix = agent.suffixQuery
|
||||
self._s_payload = agent.payload
|
||||
self._s_nullcast = agent.nullAndCastField
|
||||
self._s_escape = unescaper.escape
|
||||
|
||||
# restore thread state we touch
|
||||
td = getCurrentThreadData()
|
||||
self._s_td_uid = td.lastRequestUID
|
||||
self._s_td_httperr = td.lastHTTPError
|
||||
self._s_td_redirect = td.lastRedirectMsg
|
||||
|
||||
for k, v in _ERR_CONF.items():
|
||||
conf[k] = v
|
||||
for k, v in _ERR_KB.items():
|
||||
kb[k] = v
|
||||
|
||||
conf.batch = True
|
||||
conf.hashDB = None # disable session resume in these tests
|
||||
kb.counters = {}
|
||||
kb.technique = PAYLOAD.TECHNIQUE.ERROR
|
||||
setTechnique(PAYLOAD.TECHNIQUE.ERROR)
|
||||
|
||||
entry = AttribDict()
|
||||
entry.vector = _ERR_VECTOR
|
||||
entry.where = PAYLOAD.WHERE.ORIGINAL
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: entry}
|
||||
|
||||
# pass-through forge chain: the produced payload text carries the injExpression so
|
||||
# the oracle can (optionally) branch on the requested field; agent.payload returns
|
||||
# exactly the newValue it is handed.
|
||||
agent.prefixQuery = lambda vector, *a, **k: "[QUERY]"
|
||||
agent.suffixQuery = lambda query, *a, **k: query
|
||||
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue
|
||||
agent.nullAndCastField = lambda field: field
|
||||
unescaper.escape = lambda expression, *a, **k: expression
|
||||
|
||||
# getConsoleWidth() in _errorFields hits curses with no tty; pin COLUMNS so it doesn't
|
||||
self._s_columns = os.environ.get("COLUMNS")
|
||||
os.environ["COLUMNS"] = "80"
|
||||
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._sc.items():
|
||||
conf[k] = v
|
||||
for k, v in self._sk.items():
|
||||
kb[k] = v
|
||||
conf.batch = self._sbatch
|
||||
Request.queryPage = self._sqp
|
||||
eu.Request.queryPage = self._sqp
|
||||
kb.counters = self._scounters
|
||||
kb.technique = self._stechnique
|
||||
setTechnique(None)
|
||||
kb.injection.data = self._sinj_data
|
||||
conf.hashDB = self._shashdb
|
||||
|
||||
agent.prefixQuery = self._s_prefix
|
||||
agent.suffixQuery = self._s_suffix
|
||||
agent.payload = self._s_payload
|
||||
agent.nullAndCastField = self._s_nullcast
|
||||
unescaper.escape = self._s_escape
|
||||
|
||||
td = getCurrentThreadData()
|
||||
td.lastRequestUID = self._s_td_uid
|
||||
td.lastHTTPError = self._s_td_httperr
|
||||
td.lastRedirectMsg = self._s_td_redirect
|
||||
|
||||
if self._s_columns is None:
|
||||
os.environ.pop("COLUMNS", None)
|
||||
else:
|
||||
os.environ["COLUMNS"] = self._s_columns
|
||||
|
||||
@staticmethod
|
||||
def _wrap(body):
|
||||
return "%s%s%s" % (kb.chars.start, body, kb.chars.stop)
|
||||
|
||||
def _install_page(self, page):
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
return (page, {}, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
def _install_field_oracle(self, mapping):
|
||||
"""Oracle that branches on which field name appears in the forged payload (the
|
||||
injExpression is passed through agent.payload unchanged, so it is in `payload`)."""
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
body = "?"
|
||||
for field, value in mapping.items():
|
||||
if field in (payload or ""):
|
||||
body = value
|
||||
break
|
||||
page = "<html>%s</html>" % self._wrap(body)
|
||||
return (page, {}, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
|
||||
class TestOneShotErrorUse(_ErrorCase):
|
||||
def test_single_value_extracted(self):
|
||||
self._install_page("<html>%s</html>" % self._wrap("admin"))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT name"), "admin")
|
||||
|
||||
def test_space_char_restored(self):
|
||||
# the kb.chars.space placeholder (used to survive transport) is restored to a literal
|
||||
# space by _errorReplaceChars. NOTE: the other char tokens (dollar/at/hash) are random
|
||||
# per-run and may collide with the space token, so only space is asserted here.
|
||||
body = "hello%sworld" % kb.chars.space
|
||||
self._install_page(self._wrap(body))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hello world")
|
||||
|
||||
def test_no_markers_returns_none(self):
|
||||
self._install_page("<html>no useful markers here</html>")
|
||||
self.assertIsNone(eu._oneShotErrorUse("SELECT x"))
|
||||
|
||||
def test_html_entities_unescaped(self):
|
||||
# retVal goes through htmlUnescape() and <br> -> newline on the way out
|
||||
self._install_page(self._wrap("a & b<br>c"))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "a & b\nc")
|
||||
|
||||
def test_counter_incremented(self):
|
||||
self._install_page(self._wrap("v"))
|
||||
eu._oneShotErrorUse("SELECT x")
|
||||
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.ERROR), 1)
|
||||
|
||||
def test_field_substituted_into_expression(self):
|
||||
# field is replaced (once) by nullAndCastField(field) before forging; the oracle keys
|
||||
# on the field name in the resulting payload to prove the right column was requested
|
||||
self._install_field_oracle({"surname": "Smith"})
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT surname FROM users", field="surname"), "Smith")
|
||||
|
||||
def test_recovered_from_http_error_body(self):
|
||||
# page itself carries no markers; the delimited value lives in the 500-error body
|
||||
td = getCurrentThreadData()
|
||||
td.lastRequestUID = 4242
|
||||
td.lastHTTPError = (4242, 500, "<html>%s</html>" % self._wrap("from-error-page"))
|
||||
self._install_page("<html>regular page, no markers</html>")
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "from-error-page")
|
||||
|
||||
def test_recovered_from_response_header(self):
|
||||
# neither page nor error body has it; it is carried back in a response header value
|
||||
body = self._wrap("hdr-value")
|
||||
page = "<html>nothing</html>"
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
headers = {"X-Leak": body}
|
||||
return (page, headers, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hdr-value")
|
||||
|
||||
def test_hex_convert_decoded(self):
|
||||
# --hex: the delimited body is a hex string decoded by decodeDbmsHexValue
|
||||
conf.hexConvert = True
|
||||
self._install_page(self._wrap("48656C6C6F")) # "Hello"
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "Hello")
|
||||
|
||||
def test_empty_value_between_markers(self):
|
||||
self._install_page(self._wrap(""))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "")
|
||||
|
||||
|
||||
class TestOneShotErrorUseChunking(_ErrorCase):
|
||||
"""The MySQL multi-chunk reassembly loop: with kb.errorChunkLength set, output >= chunk
|
||||
length triggers another request at the next offset; the engine concatenates the pieces."""
|
||||
|
||||
def setUp(self):
|
||||
_ErrorCase.setUp(self)
|
||||
kb.testMode = False # honor the chunk-offset loop
|
||||
kb.errorChunkLength = 4 # pre-set so the length-probe search is skipped
|
||||
conf.verbose = 0
|
||||
|
||||
def test_multi_chunk_reassembled(self):
|
||||
# secret returned 4 chars at a time via SUBSTRING(expr, offset, 4); the loop walks offsets
|
||||
secret = "abcdefghij"
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
# MySQL substring is rendered as MID((field),offset,length)
|
||||
m = re.search(r"(?:MID|SUBSTRING)\(.*?,(\d+),(\d+)\)", payload or "")
|
||||
if m:
|
||||
off, length = int(m.group(1)), int(m.group(2))
|
||||
chunk = secret[off - 1:off - 1 + length]
|
||||
else:
|
||||
chunk = secret
|
||||
return ("%s%s%s" % (kb.chars.start, chunk, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
# a field is required for the SUBSTRING windowing branch to engage
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT data FROM t", field="data"), secret)
|
||||
|
||||
|
||||
class TestErrorFields(_ErrorCase):
|
||||
"""_errorFields iterates the field list, recovering one value per column."""
|
||||
|
||||
def test_multi_field_values(self):
|
||||
self._install_field_oracle({"user": "alice", "pass": "s3cr3t"})
|
||||
values = eu._errorFields("SELECT user,pass FROM t", "user,pass",
|
||||
["user", "pass"], suppressOutput=True)
|
||||
self.assertEqual(values, ["alice", "s3cr3t"])
|
||||
|
||||
def test_single_field_value(self):
|
||||
self._install_field_oracle({"email": "root@localhost"})
|
||||
values = eu._errorFields("SELECT email FROM t", "email", ["email"], suppressOutput=True)
|
||||
self.assertEqual(values, ["root@localhost"])
|
||||
|
||||
def test_empty_field_yields_null(self):
|
||||
# a field listed in emptyFields is short-circuited to the NULL sentinel (no oracle hit)
|
||||
from lib.core.settings import NULL
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("oracle must not be called for an empty field")
|
||||
Request.queryPage = staticmethod(boom)
|
||||
eu.Request.queryPage = staticmethod(boom)
|
||||
values = eu._errorFields("SELECT col FROM t", "col", ["col"],
|
||||
emptyFields=["col"], suppressOutput=True)
|
||||
self.assertEqual(values, [NULL])
|
||||
|
||||
def test_rownum_field_skipped(self):
|
||||
# a "ROWNUM " field is skipped entirely (Oracle limit artifact)
|
||||
self._install_field_oracle({"name": "bob"})
|
||||
values = eu._errorFields("SELECT name FROM t", "name",
|
||||
["ROWNUM x", "name"], suppressOutput=True)
|
||||
self.assertEqual(values, ["bob"])
|
||||
|
||||
|
||||
class TestErrorUse(_ErrorCase):
|
||||
"""errorUse() orchestration. initTechnique() needs a full injection session; stub it so
|
||||
the orchestration + _errorFields extraction + result shaping is what is exercised."""
|
||||
|
||||
def setUp(self):
|
||||
_ErrorCase.setUp(self)
|
||||
self._s_initTechnique = eu.initTechnique
|
||||
eu.initTechnique = lambda technique=None: None
|
||||
|
||||
def tearDown(self):
|
||||
eu.initTechnique = self._s_initTechnique
|
||||
_ErrorCase.tearDown(self)
|
||||
|
||||
def test_scalar_value(self):
|
||||
# scalar expression (no FROM): single one-shot extraction, unwrapped from the list
|
||||
self._install_page(self._wrap("5.7.40"))
|
||||
self.assertEqual(eu.errorUse("SELECT VERSION()"), "5.7.40")
|
||||
|
||||
def test_scalar_no_output_none(self):
|
||||
self._install_page("<html>no markers</html>")
|
||||
self.assertIsNone(eu.errorUse("SELECT VERSION()"))
|
||||
|
||||
def test_multi_row_dump(self):
|
||||
# dump=True over a FROM-table query: errorUse COUNTs the rows then LIMIT-walks them,
|
||||
# reconstructing each row's single column value in order
|
||||
conf.limitStart = 1
|
||||
conf.limitStop = 3
|
||||
rows = {0: "alice", 1: "bob", 2: "carol"}
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
nv = payload or ""
|
||||
if "COUNT" in nv.upper():
|
||||
body = "3"
|
||||
else:
|
||||
m = re.search(r"LIMIT (\d+),1", nv)
|
||||
idx = int(m.group(1)) if m else 0
|
||||
body = rows.get(idx, "?")
|
||||
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
value = eu.errorUse("SELECT name FROM users", dump=True)
|
||||
self.assertEqual([eu.unArrayizeValue(v) for v in value], ["alice", "bob", "carol"])
|
||||
|
||||
def test_dump_zero_count_returns_empty(self):
|
||||
# COUNT yields "0" (non-positive) -> the query returned no output -> None
|
||||
conf.limitStart = 1
|
||||
conf.limitStop = 10
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
nv = payload or ""
|
||||
body = "0" if "COUNT" in nv.upper() else "x"
|
||||
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
# a "0" count is truthy-but-not-positive -> empty-table sentinel (returns [])
|
||||
self.assertEqual(eu.errorUse("SELECT name FROM users", dump=True), [])
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# LDAP: lib/techniques/ldap/inject.py
|
||||
# ===========================================================================
|
||||
|
|
@ -371,7 +877,6 @@ class _LdapOracleCase(unittest.TestCase):
|
|||
conf.cookieDel = None
|
||||
|
||||
directory = self.DIRECTORY
|
||||
sentinel = ldap.SENTINEL
|
||||
|
||||
def fake_send(place, parameter, value):
|
||||
assertions = re.findall(r"\((\w+)=([^()]*)", value)
|
||||
|
|
@ -765,5 +1270,251 @@ class TestGraphqlParseRows(unittest.TestCase):
|
|||
self.assertIn("| 1 | 22 |", out)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Blind inference: lib/techniques/blind/inference.py
|
||||
# ===========================================================================
|
||||
|
||||
# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the
|
||||
# greater-char marker (swapped to '=' on the final equality check). A parseable template
|
||||
# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret.
|
||||
TEMPLATE = "EXPR=%s IDX=%d CMP>%d"
|
||||
_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)")
|
||||
|
||||
# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path
|
||||
_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False,
|
||||
"charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False,
|
||||
"repair": False, "flushSession": None, "freshQueries": None, "hashDB": None}
|
||||
_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False,
|
||||
"disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False,
|
||||
"resumeValues": True, "inferenceMode": False}
|
||||
|
||||
|
||||
class _InferenceCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._saved_conf = {k: conf.get(k) for k in _CONF}
|
||||
self._saved_kb = {k: kb.get(k) for k in _KB}
|
||||
self._saved_qp = Connect.queryPage
|
||||
self._saved_processChar = kb.data.get("processChar")
|
||||
for k, v in _CONF.items():
|
||||
conf[k] = v
|
||||
for k, v in _KB.items():
|
||||
kb[k] = v
|
||||
kb.data.processChar = None
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved_conf.items():
|
||||
conf[k] = v
|
||||
for k, v in self._saved_kb.items():
|
||||
kb[k] = v
|
||||
kb.data.processChar = self._saved_processChar
|
||||
Connect.queryPage = self._saved_qp
|
||||
inf.Request.queryPage = self._saved_qp
|
||||
|
||||
def _install_oracle(self, secret):
|
||||
def oracle(payload=None, *args, **kwargs):
|
||||
m = _PARSE.search(payload)
|
||||
idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3))
|
||||
ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0
|
||||
return (ch > threshold) if op == ">" else (ch == threshold)
|
||||
|
||||
Connect.queryPage = staticmethod(oracle)
|
||||
inf.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
@staticmethod
|
||||
def _reset_thread():
|
||||
td = getCurrentThreadData()
|
||||
td.shared.value = ""
|
||||
td.shared.index = [0]
|
||||
td.shared.start = 0
|
||||
td.shared.count = 0
|
||||
|
||||
def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs):
|
||||
self._install_oracle(secret)
|
||||
self._reset_thread()
|
||||
if length is None:
|
||||
length = len(secret)
|
||||
return inf.bisection(TEMPLATE, expression, length=length, **kwargs)
|
||||
|
||||
|
||||
class TestTrivialReturns(_InferenceCase):
|
||||
def test_none_payload(self):
|
||||
# payload is None -> (0, None) without ever touching the oracle
|
||||
self.assertEqual(inf.bisection(None, "SELECT x"), (0, None))
|
||||
|
||||
def test_zero_length(self):
|
||||
# length == 0 -> (0, "") short-circuit
|
||||
self._install_oracle("ignored")
|
||||
self._reset_thread()
|
||||
self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, ""))
|
||||
|
||||
|
||||
class TestRangeLimiting(_InferenceCase):
|
||||
SECRET = "ABCDEFGH"
|
||||
|
||||
def test_first_char_arg(self):
|
||||
# firstChar=3 -> start from the 3rd character (1-based) -> drop "AB"
|
||||
_, value = self._bisect(self.SECRET, firstChar=3)
|
||||
self.assertEqual(value, "CDEFGH")
|
||||
|
||||
def test_last_char_arg(self):
|
||||
# lastChar=4 -> stop after the 4th character
|
||||
_, value = self._bisect(self.SECRET, lastChar=4)
|
||||
self.assertEqual(value, "ABCD")
|
||||
|
||||
def test_conf_first_char(self):
|
||||
conf.firstChar = 4
|
||||
_, value = self._bisect(self.SECRET)
|
||||
self.assertEqual(value, "DEFGH")
|
||||
|
||||
def test_conf_last_char(self):
|
||||
conf.lastChar = 3
|
||||
_, value = self._bisect(self.SECRET)
|
||||
self.assertEqual(value, "ABC")
|
||||
|
||||
def test_first_and_last_window(self):
|
||||
# combined window: chars 3..6 inclusive -> "CDEF"
|
||||
_, value = self._bisect(self.SECRET, firstChar=3, lastChar=6)
|
||||
self.assertEqual(value, "CDEF")
|
||||
|
||||
|
||||
class TestHexConvert(_InferenceCase):
|
||||
def test_hex_output_decoded(self):
|
||||
# --hex: the retrieved value is a hex string the engine decodes on the way out
|
||||
conf.hexConvert = True
|
||||
hexed = "48656C6C6F" # "Hello"
|
||||
_, value = self._bisect(hexed)
|
||||
self.assertEqual(value, "Hello")
|
||||
self.assertEqual(value, decodeDbmsHexValue(hexed))
|
||||
|
||||
|
||||
class TestProcessCharHook(_InferenceCase):
|
||||
def test_process_char_applied_to_each_char(self):
|
||||
# kb.data.processChar transforms every assembled character
|
||||
kb.data.processChar = lambda c: c.upper()
|
||||
_, value = self._bisect("abcde")
|
||||
self.assertEqual(value, "ABCDE")
|
||||
|
||||
|
||||
class TestResumeFromHashDB(_InferenceCase):
|
||||
"""bisection() consults the session store first (hashDBRetrieve(checkConf=True)).
|
||||
Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py)."""
|
||||
|
||||
def setUp(self):
|
||||
_InferenceCase.setUp(self)
|
||||
fd, self.path = tempfile.mkstemp(suffix=".sqlite")
|
||||
os.close(fd)
|
||||
os.remove(self.path) # HashDB creates it lazily
|
||||
conf.hashDB = HashDB(self.path)
|
||||
# hashDBRetrieve/Write key off these
|
||||
self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port"))
|
||||
conf.hostname = "test.invalid"
|
||||
conf.path = "/"
|
||||
conf.port = 80
|
||||
|
||||
def tearDown(self):
|
||||
conf.hostname, conf.path, conf.port = self._saved_loc
|
||||
try:
|
||||
conf.hashDB.closeAll()
|
||||
except Exception:
|
||||
pass
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
_InferenceCase.tearDown(self)
|
||||
|
||||
def test_full_value_resumed(self):
|
||||
# a complete cached value short-circuits the whole bisection (0 queries)
|
||||
hashDBWrite("SELECT cached", "RESUMED")
|
||||
conf.hashDB.flush()
|
||||
count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7)
|
||||
self.assertEqual(value, "RESUMED")
|
||||
self.assertEqual(count, 0)
|
||||
|
||||
def test_partial_value_continued(self):
|
||||
# a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix
|
||||
# and extracts only the remaining characters
|
||||
kb.inferenceMode = True # partial markers are honored only in inference mode
|
||||
hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER)
|
||||
conf.hashDB.flush()
|
||||
count, value = self._bisect("ABCDE", expression="SELECT partial", length=5)
|
||||
self.assertEqual(value, "ABCDE")
|
||||
self.assertGreater(count, 0) # it did real work for "CDE"
|
||||
|
||||
|
||||
class TestQueryOutputLength(_InferenceCase):
|
||||
def test_length_retrieved(self):
|
||||
# queryOutputLength forges a LENGTH() expression and runs bisection with the
|
||||
# DIGITS charset; the mock "secret" is the textual length itself
|
||||
self._install_oracle("42")
|
||||
self._reset_thread()
|
||||
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42)
|
||||
|
||||
def test_length_single_digit(self):
|
||||
self._install_oracle("7")
|
||||
self._reset_thread()
|
||||
self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7)
|
||||
|
||||
def test_digits_charset_extracts_number(self):
|
||||
# direct bisection with the DIGITS charset (queryOutputLength's inner call)
|
||||
_, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS)
|
||||
self.assertEqual(value, "2026")
|
||||
|
||||
|
||||
class TestConfigUnion(unittest.TestCase):
|
||||
"""lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols."""
|
||||
|
||||
_CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50}
|
||||
|
||||
def setUp(self):
|
||||
self._saved = {k: conf.get(k) for k in self._CONF}
|
||||
self._saved_uchar = kb.get("uChar")
|
||||
for k, v in self._CONF.items():
|
||||
conf[k] = v
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._saved.items():
|
||||
conf[k] = v
|
||||
kb.uChar = self._saved_uchar
|
||||
|
||||
def test_char_and_range(self):
|
||||
uu.configUnion(char="NULL", columns="2-6")
|
||||
self.assertEqual(kb.uChar, "NULL")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6))
|
||||
|
||||
def test_single_column(self):
|
||||
uu.configUnion(char="NULL", columns="4")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4))
|
||||
|
||||
def test_uchar_substitution_quoted(self):
|
||||
# conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template
|
||||
conf.uChar = "test"
|
||||
uu.configUnion(char="x[CHAR]x", columns="1")
|
||||
self.assertEqual(kb.uChar, "x'test'x")
|
||||
|
||||
def test_uchar_substitution_digit(self):
|
||||
# a digit conf.uChar is substituted unquoted
|
||||
conf.uChar = "88"
|
||||
uu.configUnion(char="[CHAR]", columns="1")
|
||||
self.assertEqual(kb.uChar, "88")
|
||||
|
||||
def test_conf_ucols_overrides_columns_arg(self):
|
||||
# conf.uCols takes precedence over the columns argument
|
||||
conf.uCols = "3-9"
|
||||
uu.configUnion(char="NULL", columns="1-2")
|
||||
self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9))
|
||||
|
||||
def test_non_integer_range_raises(self):
|
||||
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc")
|
||||
|
||||
def test_inverted_range_raises(self):
|
||||
self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2")
|
||||
|
||||
def test_non_string_char_ignored(self):
|
||||
# a non-string char leaves kb.uChar untouched (early return)
|
||||
kb.uChar = "SENTINEL"
|
||||
uu.configUnion(char=None, columns="1")
|
||||
self.assertEqual(kb.uChar, "SENTINEL")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
|
|||
|
|
@ -1,540 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Additional mocked-oracle coverage for the two self-contained extraction engines
|
||||
whose value-recovery branches are NOT reached by tests/test_techniques.py /
|
||||
tests/test_inference.py:
|
||||
|
||||
* lib/techniques/error/use.py - _oneShotErrorUse / _errorFields / errorUse
|
||||
* lib/techniques/union/use.py - _oneShotUnionUse / unionUse (partial/LIMIT loop)
|
||||
|
||||
Same established harness as the sibling files (see tests/test_techniques.py,
|
||||
tests/test_inference.py): the network seam (Request.queryPage) and the forge/escape
|
||||
chain (agent.prefixQuery / suffixQuery / payload / forgeUnionQuery / concatQuery /
|
||||
unescaper.escape) are replaced by an in-process oracle that answers against a KNOWN
|
||||
secret wrapped in the REAL kb.chars.start/stop delimiters. The function's OWN regex
|
||||
extraction / multi-field iteration / counting / LIMIT windowing is what runs - no
|
||||
live target, no network, no DBMS.
|
||||
|
||||
Every test asserts the exact reconstructed value (known-secret oracle), so it fails
|
||||
if the extraction logic breaks.
|
||||
|
||||
stdlib unittest only; works on Python 2.7 and 3.x.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.agent import agent
|
||||
from lib.core.common import getCurrentThreadData
|
||||
from lib.core.common import setTechnique
|
||||
from lib.core.unescaper import unescaper
|
||||
from lib.request.connect import Connect as Request
|
||||
|
||||
import lib.techniques.error.use as eu
|
||||
import lib.techniques.union.use as uu
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# ERROR-based: lib/techniques/error/use.py
|
||||
# ===========================================================================
|
||||
|
||||
# An error injection vector is consumed by agent.prefixQuery/suffixQuery (here stubbed
|
||||
# to a pass-through that just yields the "[QUERY]" placeholder the engine substitutes into).
|
||||
_ERR_VECTOR = ("pref", "suff", 2, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None)
|
||||
|
||||
_ERR_CONF = {"hexConvert": False, "noEscape": None, "verbose": 0, "api": False,
|
||||
"reportJson": False, "limitStart": 0, "limitStop": 0, "noTruncate": True,
|
||||
"threads": 1, "eta": False}
|
||||
_ERR_KB = {"testMode": True, "safeCharEncode": False, "errorChunkLength": None,
|
||||
"fileReadMode": False, "bruteMode": False, "threadContinue": True,
|
||||
"suppressResumeInfo": False, "dumpTable": None}
|
||||
|
||||
|
||||
class _ErrorCase(unittest.TestCase):
|
||||
"""Stub the forge/escape/transport seam so _oneShotErrorUse's OWN parsing (marker
|
||||
extraction, trim repair, char restoration) is what is exercised."""
|
||||
|
||||
def setUp(self):
|
||||
self._sc = {k: conf.get(k) for k in _ERR_CONF}
|
||||
self._sk = {k: kb.get(k) for k in _ERR_KB}
|
||||
self._sqp = Request.queryPage
|
||||
self._scounters = kb.get("counters")
|
||||
self._stechnique = kb.get("technique")
|
||||
self._sinj_data = kb.injection.data
|
||||
self._shashdb = conf.get("hashDB")
|
||||
self._sbatch = conf.get("batch")
|
||||
|
||||
self._s_prefix = agent.prefixQuery
|
||||
self._s_suffix = agent.suffixQuery
|
||||
self._s_payload = agent.payload
|
||||
self._s_nullcast = agent.nullAndCastField
|
||||
self._s_escape = unescaper.escape
|
||||
|
||||
# restore thread state we touch
|
||||
td = getCurrentThreadData()
|
||||
self._s_td_uid = td.lastRequestUID
|
||||
self._s_td_httperr = td.lastHTTPError
|
||||
self._s_td_redirect = td.lastRedirectMsg
|
||||
|
||||
for k, v in _ERR_CONF.items():
|
||||
conf[k] = v
|
||||
for k, v in _ERR_KB.items():
|
||||
kb[k] = v
|
||||
|
||||
conf.batch = True
|
||||
conf.hashDB = None # disable session resume in these tests
|
||||
kb.counters = {}
|
||||
kb.technique = PAYLOAD.TECHNIQUE.ERROR
|
||||
setTechnique(PAYLOAD.TECHNIQUE.ERROR)
|
||||
|
||||
entry = AttribDict()
|
||||
entry.vector = _ERR_VECTOR
|
||||
entry.where = PAYLOAD.WHERE.ORIGINAL
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: entry}
|
||||
|
||||
# pass-through forge chain: the produced payload text carries the injExpression so
|
||||
# the oracle can (optionally) branch on the requested field; agent.payload returns
|
||||
# exactly the newValue it is handed.
|
||||
agent.prefixQuery = lambda vector, *a, **k: "[QUERY]"
|
||||
agent.suffixQuery = lambda query, *a, **k: query
|
||||
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue
|
||||
agent.nullAndCastField = lambda field: field
|
||||
unescaper.escape = lambda expression, *a, **k: expression
|
||||
|
||||
# getConsoleWidth() in _errorFields hits curses with no tty; pin COLUMNS so it doesn't
|
||||
self._s_columns = os.environ.get("COLUMNS")
|
||||
os.environ["COLUMNS"] = "80"
|
||||
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._sc.items():
|
||||
conf[k] = v
|
||||
for k, v in self._sk.items():
|
||||
kb[k] = v
|
||||
conf.batch = self._sbatch
|
||||
Request.queryPage = self._sqp
|
||||
eu.Request.queryPage = self._sqp
|
||||
kb.counters = self._scounters
|
||||
kb.technique = self._stechnique
|
||||
setTechnique(None)
|
||||
kb.injection.data = self._sinj_data
|
||||
conf.hashDB = self._shashdb
|
||||
|
||||
agent.prefixQuery = self._s_prefix
|
||||
agent.suffixQuery = self._s_suffix
|
||||
agent.payload = self._s_payload
|
||||
agent.nullAndCastField = self._s_nullcast
|
||||
unescaper.escape = self._s_escape
|
||||
|
||||
td = getCurrentThreadData()
|
||||
td.lastRequestUID = self._s_td_uid
|
||||
td.lastHTTPError = self._s_td_httperr
|
||||
td.lastRedirectMsg = self._s_td_redirect
|
||||
|
||||
if self._s_columns is None:
|
||||
os.environ.pop("COLUMNS", None)
|
||||
else:
|
||||
os.environ["COLUMNS"] = self._s_columns
|
||||
|
||||
@staticmethod
|
||||
def _wrap(body):
|
||||
return "%s%s%s" % (kb.chars.start, body, kb.chars.stop)
|
||||
|
||||
def _install_page(self, page):
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
return (page, {}, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
def _install_field_oracle(self, mapping):
|
||||
"""Oracle that branches on which field name appears in the forged payload (the
|
||||
injExpression is passed through agent.payload unchanged, so it is in `payload`)."""
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
body = "?"
|
||||
for field, value in mapping.items():
|
||||
if field in (payload or ""):
|
||||
body = value
|
||||
break
|
||||
page = "<html>%s</html>" % self._wrap(body)
|
||||
return (page, {}, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
|
||||
class TestOneShotErrorUse(_ErrorCase):
|
||||
def test_single_value_extracted(self):
|
||||
self._install_page("<html>%s</html>" % self._wrap("admin"))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT name"), "admin")
|
||||
|
||||
def test_space_char_restored(self):
|
||||
# the kb.chars.space placeholder (used to survive transport) is restored to a literal
|
||||
# space by _errorReplaceChars. NOTE: the other char tokens (dollar/at/hash) are random
|
||||
# per-run and may collide with the space token, so only space is asserted here.
|
||||
body = "hello%sworld" % kb.chars.space
|
||||
self._install_page(self._wrap(body))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hello world")
|
||||
|
||||
def test_no_markers_returns_none(self):
|
||||
self._install_page("<html>no useful markers here</html>")
|
||||
self.assertIsNone(eu._oneShotErrorUse("SELECT x"))
|
||||
|
||||
def test_html_entities_unescaped(self):
|
||||
# retVal goes through htmlUnescape() and <br> -> newline on the way out
|
||||
self._install_page(self._wrap("a & b<br>c"))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "a & b\nc")
|
||||
|
||||
def test_counter_incremented(self):
|
||||
self._install_page(self._wrap("v"))
|
||||
eu._oneShotErrorUse("SELECT x")
|
||||
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.ERROR), 1)
|
||||
|
||||
def test_field_substituted_into_expression(self):
|
||||
# field is replaced (once) by nullAndCastField(field) before forging; the oracle keys
|
||||
# on the field name in the resulting payload to prove the right column was requested
|
||||
self._install_field_oracle({"surname": "Smith"})
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT surname FROM users", field="surname"), "Smith")
|
||||
|
||||
def test_recovered_from_http_error_body(self):
|
||||
# page itself carries no markers; the delimited value lives in the 500-error body
|
||||
td = getCurrentThreadData()
|
||||
td.lastRequestUID = 4242
|
||||
td.lastHTTPError = (4242, 500, "<html>%s</html>" % self._wrap("from-error-page"))
|
||||
self._install_page("<html>regular page, no markers</html>")
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "from-error-page")
|
||||
|
||||
def test_recovered_from_response_header(self):
|
||||
# neither page nor error body has it; it is carried back in a response header value
|
||||
body = self._wrap("hdr-value")
|
||||
page = "<html>nothing</html>"
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
headers = {"X-Leak": body}
|
||||
return (page, headers, 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hdr-value")
|
||||
|
||||
def test_hex_convert_decoded(self):
|
||||
# --hex: the delimited body is a hex string decoded by decodeDbmsHexValue
|
||||
conf.hexConvert = True
|
||||
self._install_page(self._wrap("48656C6C6F")) # "Hello"
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "Hello")
|
||||
|
||||
def test_empty_value_between_markers(self):
|
||||
self._install_page(self._wrap(""))
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT x"), "")
|
||||
|
||||
|
||||
class TestOneShotErrorUseChunking(_ErrorCase):
|
||||
"""The MySQL multi-chunk reassembly loop: with kb.errorChunkLength set, output >= chunk
|
||||
length triggers another request at the next offset; the engine concatenates the pieces."""
|
||||
|
||||
def setUp(self):
|
||||
_ErrorCase.setUp(self)
|
||||
kb.testMode = False # honor the chunk-offset loop
|
||||
kb.errorChunkLength = 4 # pre-set so the length-probe search is skipped
|
||||
conf.verbose = 0
|
||||
|
||||
def test_multi_chunk_reassembled(self):
|
||||
# secret returned 4 chars at a time via SUBSTRING(expr, offset, 4); the loop walks offsets
|
||||
secret = "abcdefghij"
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
# MySQL substring is rendered as MID((field),offset,length)
|
||||
m = re.search(r"(?:MID|SUBSTRING)\(.*?,(\d+),(\d+)\)", payload or "")
|
||||
if m:
|
||||
off, length = int(m.group(1)), int(m.group(2))
|
||||
chunk = secret[off - 1:off - 1 + length]
|
||||
else:
|
||||
chunk = secret
|
||||
return ("%s%s%s" % (kb.chars.start, chunk, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
# a field is required for the SUBSTRING windowing branch to engage
|
||||
self.assertEqual(eu._oneShotErrorUse("SELECT data FROM t", field="data"), secret)
|
||||
|
||||
|
||||
class TestErrorFields(_ErrorCase):
|
||||
"""_errorFields iterates the field list, recovering one value per column."""
|
||||
|
||||
def test_multi_field_values(self):
|
||||
self._install_field_oracle({"user": "alice", "pass": "s3cr3t"})
|
||||
values = eu._errorFields("SELECT user,pass FROM t", "user,pass",
|
||||
["user", "pass"], suppressOutput=True)
|
||||
self.assertEqual(values, ["alice", "s3cr3t"])
|
||||
|
||||
def test_single_field_value(self):
|
||||
self._install_field_oracle({"email": "root@localhost"})
|
||||
values = eu._errorFields("SELECT email FROM t", "email", ["email"], suppressOutput=True)
|
||||
self.assertEqual(values, ["root@localhost"])
|
||||
|
||||
def test_empty_field_yields_null(self):
|
||||
# a field listed in emptyFields is short-circuited to the NULL sentinel (no oracle hit)
|
||||
from lib.core.settings import NULL
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("oracle must not be called for an empty field")
|
||||
Request.queryPage = staticmethod(boom)
|
||||
eu.Request.queryPage = staticmethod(boom)
|
||||
values = eu._errorFields("SELECT col FROM t", "col", ["col"],
|
||||
emptyFields=["col"], suppressOutput=True)
|
||||
self.assertEqual(values, [NULL])
|
||||
|
||||
def test_rownum_field_skipped(self):
|
||||
# a "ROWNUM " field is skipped entirely (Oracle limit artifact)
|
||||
self._install_field_oracle({"name": "bob"})
|
||||
values = eu._errorFields("SELECT name FROM t", "name",
|
||||
["ROWNUM x", "name"], suppressOutput=True)
|
||||
self.assertEqual(values, ["bob"])
|
||||
|
||||
|
||||
class TestErrorUse(_ErrorCase):
|
||||
"""errorUse() orchestration. initTechnique() needs a full injection session; stub it so
|
||||
the orchestration + _errorFields extraction + result shaping is what is exercised."""
|
||||
|
||||
def setUp(self):
|
||||
_ErrorCase.setUp(self)
|
||||
self._s_initTechnique = eu.initTechnique
|
||||
eu.initTechnique = lambda technique=None: None
|
||||
|
||||
def tearDown(self):
|
||||
eu.initTechnique = self._s_initTechnique
|
||||
_ErrorCase.tearDown(self)
|
||||
|
||||
def test_scalar_value(self):
|
||||
# scalar expression (no FROM): single one-shot extraction, unwrapped from the list
|
||||
self._install_page(self._wrap("5.7.40"))
|
||||
self.assertEqual(eu.errorUse("SELECT VERSION()"), "5.7.40")
|
||||
|
||||
def test_scalar_no_output_none(self):
|
||||
self._install_page("<html>no markers</html>")
|
||||
self.assertIsNone(eu.errorUse("SELECT VERSION()"))
|
||||
|
||||
def test_multi_row_dump(self):
|
||||
# dump=True over a FROM-table query: errorUse COUNTs the rows then LIMIT-walks them,
|
||||
# reconstructing each row's single column value in order
|
||||
conf.limitStart = 1
|
||||
conf.limitStop = 3
|
||||
rows = {0: "alice", 1: "bob", 2: "carol"}
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
nv = payload or ""
|
||||
if "COUNT" in nv.upper():
|
||||
body = "3"
|
||||
else:
|
||||
m = re.search(r"LIMIT (\d+),1", nv)
|
||||
idx = int(m.group(1)) if m else 0
|
||||
body = rows.get(idx, "?")
|
||||
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
value = eu.errorUse("SELECT name FROM users", dump=True)
|
||||
self.assertEqual([eu.unArrayizeValue(v) for v in value], ["alice", "bob", "carol"])
|
||||
|
||||
def test_dump_zero_count_returns_empty(self):
|
||||
# COUNT yields "0" (non-positive) -> the query returned no output -> None
|
||||
conf.limitStart = 1
|
||||
conf.limitStop = 10
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
nv = payload or ""
|
||||
body = "0" if "COUNT" in nv.upper() else "x"
|
||||
return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
eu.Request.queryPage = staticmethod(oracle)
|
||||
# a "0" count is truthy-but-not-positive -> empty-table sentinel (returns [])
|
||||
self.assertEqual(eu.errorUse("SELECT name FROM users", dump=True), [])
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# UNION-based: lib/techniques/union/use.py (branches not in test_techniques.py)
|
||||
# ===========================================================================
|
||||
|
||||
_UNION_VECTOR = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.NEGATIVE, False, False, None, None)
|
||||
|
||||
_UU_CONF = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None,
|
||||
"forcePartial": True, "disableJson": True, "binaryFields": None,
|
||||
"reportJson": False, "api": False, "threads": 1, "verbose": 0, "eta": False,
|
||||
"noTruncate": True, "uFrom": None}
|
||||
_UU_KB = {"jsonAggMode": False, "respTruncated": False, "unionDuplicates": False,
|
||||
"forcePartialUnion": False, "tableFrom": None, "unionTemplate": None,
|
||||
"nchar": False, "pageEncoding": None, "bruteMode": False, "partRun": None,
|
||||
"suppressResumeInfo": False, "threadContinue": True}
|
||||
|
||||
|
||||
class _UnionLimitCase(unittest.TestCase):
|
||||
"""Drive unionUse() down the partial / LIMIT-loop path (jsonAgg disabled, NEGATIVE where,
|
||||
forcePartial on). The forge chain is a pass-through; concatQuery records the per-row
|
||||
expression so the oracle can recover the LIMIT offset and answer from a known row set."""
|
||||
|
||||
def setUp(self):
|
||||
self._sc = {k: conf.get(k) for k in _UU_CONF}
|
||||
self._sk = {k: kb.get(k) for k in _UU_KB}
|
||||
self._sqp = Request.queryPage
|
||||
self._scounters = kb.get("counters")
|
||||
self._sinj_data = kb.injection.data
|
||||
self._shashdb = conf.get("hashDB")
|
||||
self._sbatch = conf.get("batch")
|
||||
self._s_forge = agent.forgeUnionQuery
|
||||
self._s_concat = agent.concatQuery
|
||||
self._s_payload = agent.payload
|
||||
self._s_escape = unescaper.escape
|
||||
self._s_lastexpr = getattr(agent, "_lastexpr", None)
|
||||
self._s_initTechnique = uu.initTechnique
|
||||
|
||||
for k, v in _UU_CONF.items():
|
||||
conf[k] = v
|
||||
for k, v in _UU_KB.items():
|
||||
kb[k] = v
|
||||
|
||||
conf.batch = True
|
||||
conf.hashDB = None
|
||||
kb.counters = {}
|
||||
|
||||
entry = AttribDict()
|
||||
entry.vector = _UNION_VECTOR
|
||||
entry.where = PAYLOAD.WHERE.NEGATIVE
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry}
|
||||
|
||||
# record the expression seen by each _oneShotUnionUse so the oracle can branch on it
|
||||
def rec_concat(expression, unpack=True):
|
||||
agent._lastexpr = expression
|
||||
return expression
|
||||
agent.concatQuery = rec_concat
|
||||
agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED"
|
||||
agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD"
|
||||
unescaper.escape = lambda expression, *a, **k: expression
|
||||
uu.initTechnique = lambda technique=None: None
|
||||
|
||||
self._s_columns = os.environ.get("COLUMNS")
|
||||
os.environ["COLUMNS"] = "80"
|
||||
|
||||
set_dbms("MySQL")
|
||||
|
||||
def tearDown(self):
|
||||
for k, v in self._sc.items():
|
||||
conf[k] = v
|
||||
for k, v in self._sk.items():
|
||||
kb[k] = v
|
||||
conf.batch = self._sbatch
|
||||
Request.queryPage = self._sqp
|
||||
uu.Request.queryPage = self._sqp
|
||||
kb.counters = self._scounters
|
||||
kb.injection.data = self._sinj_data
|
||||
conf.hashDB = self._shashdb
|
||||
agent.forgeUnionQuery = self._s_forge
|
||||
agent.concatQuery = self._s_concat
|
||||
agent.payload = self._s_payload
|
||||
unescaper.escape = self._s_escape
|
||||
agent._lastexpr = self._s_lastexpr
|
||||
uu.initTechnique = self._s_initTechnique
|
||||
|
||||
if self._s_columns is None:
|
||||
os.environ.pop("COLUMNS", None)
|
||||
else:
|
||||
os.environ["COLUMNS"] = self._s_columns
|
||||
|
||||
def _install_row_oracle(self, rows, count=None):
|
||||
"""rows: list of tuples (per-row columns). Oracle answers COUNT and per-LIMIT rows
|
||||
from the recorded expression (agent._lastexpr), wrapping in real start/stop markers."""
|
||||
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
|
||||
total = count if count is not None else len(rows)
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
expr = getattr(agent, "_lastexpr", "") or ""
|
||||
if "COUNT" in expr.upper():
|
||||
body = str(total)
|
||||
else:
|
||||
m = re.search(r"LIMIT (\d+),1", expr)
|
||||
idx = int(m.group(1)) if m else 0
|
||||
row = rows[idx] if 0 <= idx < len(rows) else ("?",)
|
||||
body = delim.join(row)
|
||||
page = "%s%s%s" % (start, body, stop)
|
||||
return (page, AttribDict(), 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
uu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
|
||||
class TestUnionPartialDump(_UnionLimitCase):
|
||||
def test_multi_row_two_columns(self):
|
||||
rows = [("1", "alice"), ("2", "bob"), ("3", "carol")]
|
||||
self._install_row_oracle(rows)
|
||||
value = uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(list(value), [["1", "alice"], ["2", "bob"], ["3", "carol"]])
|
||||
|
||||
def test_multi_row_single_column(self):
|
||||
rows = [("alice",), ("bob",)]
|
||||
self._install_row_oracle(rows)
|
||||
value = uu.unionUse("SELECT name FROM users")
|
||||
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["alice", "bob"])
|
||||
|
||||
def test_query_count_matches_rows(self):
|
||||
# one COUNT query + one query per row = 4 UNION requests for 3 rows
|
||||
rows = [("1", "a"), ("2", "b"), ("3", "c")]
|
||||
self._install_row_oracle(rows)
|
||||
uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1 + len(rows))
|
||||
|
||||
def test_count_returns_zero_empty(self):
|
||||
# COUNT yields "0" -> empty-table sentinel (the function returns []), no row queries
|
||||
self._install_row_oracle([], count=0)
|
||||
value = uu.unionUse("SELECT id,name FROM users")
|
||||
self.assertEqual(value, [])
|
||||
|
||||
def test_single_row_count_one(self):
|
||||
# COUNT yields "1": the multi-row thread loop is skipped, falls through to one one-shot
|
||||
rows = [("solo",)]
|
||||
self._install_row_oracle(rows, count=1)
|
||||
value = uu.unionUse("SELECT name FROM users")
|
||||
self.assertEqual(uu.unArrayizeValue(value), "solo")
|
||||
|
||||
def test_length_limited_window(self):
|
||||
# conf.limitStart/limitStop windowing (dump=True): only rows in [start, stop) survive.
|
||||
# With limitStart=2, limitStop=4 over a 5-row table the engine COUNTs then walks
|
||||
# offsets 1..3 -> rows index 1,2,3 -> "b","c","d".
|
||||
conf.forcePartial = False
|
||||
conf.limitStart = 2
|
||||
conf.limitStop = 4
|
||||
rows = [("a",), ("b",), ("c",), ("d",), ("e",)]
|
||||
self._install_row_oracle(rows, count=5)
|
||||
value = uu.unionUse("SELECT name FROM users", dump=True)
|
||||
self.assertEqual([uu.unArrayizeValue(v) for v in value], ["b", "c", "d"])
|
||||
|
||||
|
||||
class TestOneShotUnionUseLimited(_UnionLimitCase):
|
||||
"""_oneShotUnionUse called directly with the `limited` flag set (the per-row caller's mode)."""
|
||||
|
||||
def test_limited_single_row(self):
|
||||
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
|
||||
body = delim.join(("7", "zed"))
|
||||
page = "%s%s%s" % (start, body, stop)
|
||||
|
||||
def oracle(payload=None, content=False, raise404=False, **kwargs):
|
||||
return (page, AttribDict(), 200) if content else page
|
||||
Request.queryPage = staticmethod(oracle)
|
||||
uu.Request.queryPage = staticmethod(oracle)
|
||||
|
||||
retVal = uu._oneShotUnionUse("SELECT id,name FROM t LIMIT 0,1", unpack=True, limited=True)
|
||||
self.assertEqual(retVal, page)
|
||||
# one wrapped multi-column entry -> one row of two columns
|
||||
self.assertEqual(list(uu.parseUnionPage(retVal)), [["7", "zed"]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -11,7 +11,8 @@ be exercised against canned result rows without a live target, network, or DBMS.
|
|||
Each test sets conf.direct = True to drive the inband (union/error/query OR
|
||||
conf.direct) branch of the method under test, patches inject.getValue with rows
|
||||
matching the shape the method parses, then asserts the relevant kb.data.cached*
|
||||
container was populated.
|
||||
container was populated. Inference (blind) branches set conf.direct = False with a
|
||||
BOOLEAN technique present and follow the count-then-per-index contract.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -24,11 +25,32 @@ from _testutils import bootstrap, set_dbms
|
|||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.enums import EXPECTED, PAYLOAD
|
||||
import plugins.generic.users as umod
|
||||
from plugins.generic.users import Users
|
||||
from lib.core.settings import CURRENT_USER
|
||||
|
||||
|
||||
def _inference_gv(count, sequence):
|
||||
"""Build an inject.getValue stub for blind inference branches.
|
||||
|
||||
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
|
||||
yields the next item from `sequence` wrapped as a single-cell row ([value]),
|
||||
cycling if exhausted. This mirrors the count-then-per-row contract of every
|
||||
isInferenceAvailable() branch.
|
||||
"""
|
||||
state = {"i": 0}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return str(count)
|
||||
val = sequence[state["i"] % len(sequence)]
|
||||
state["i"] += 1
|
||||
return [val]
|
||||
|
||||
return gv
|
||||
|
||||
|
||||
class TestUsersEnum(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Snapshot the global state these tests mutate so tearDown can restore it
|
||||
|
|
@ -252,5 +274,205 @@ class TestUsersEnum(unittest.TestCase):
|
|||
self.assertIn("root", areAdmins)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Privilege parsing / inference branches (relocated from test_generic_enum_more.py)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class _UsersBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._direct = conf.direct
|
||||
self._technique = conf.technique
|
||||
self._user = conf.user
|
||||
self._gv = umod.inject.getValue
|
||||
self._cbe = umod.inject.checkBooleanExpression
|
||||
self._store = umod.storeHashesToFile
|
||||
self._attack = umod.attackCachedUsersPasswords
|
||||
self._readInput = umod.readInput
|
||||
self._his = kb.data.get("has_information_schema")
|
||||
self._injection_data = kb.injection.data
|
||||
|
||||
set_dbms("MySQL")
|
||||
conf.direct = True
|
||||
conf.user = None
|
||||
kb.data.has_information_schema = True
|
||||
|
||||
umod.storeHashesToFile = lambda *a, **k: None
|
||||
umod.attackCachedUsersPasswords = lambda *a, **k: None
|
||||
umod.readInput = lambda *a, **k: "N"
|
||||
|
||||
def tearDown(self):
|
||||
conf.direct = self._direct
|
||||
conf.technique = self._technique
|
||||
conf.user = self._user
|
||||
umod.inject.getValue = self._gv
|
||||
umod.inject.checkBooleanExpression = self._cbe
|
||||
umod.storeHashesToFile = self._store
|
||||
umod.attackCachedUsersPasswords = self._attack
|
||||
umod.readInput = self._readInput
|
||||
kb.injection.data = self._injection_data
|
||||
if self._his is None:
|
||||
kb.data.pop("has_information_schema", None)
|
||||
else:
|
||||
kb.data.has_information_schema = self._his
|
||||
|
||||
def _inference(self):
|
||||
conf.direct = False
|
||||
conf.technique = None
|
||||
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
|
||||
|
||||
|
||||
class TestUsersPrivilegesInband(_UsersBase):
|
||||
def test_privileges_pgsql_multiple_digit_columns(self):
|
||||
# PostgreSQL: privilege columns are digit flags; a column index maps to
|
||||
# PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1,
|
||||
# leave the rest 0; assert exactly those two privileges are parsed and that
|
||||
# "super" makes the user an admin.
|
||||
set_dbms("PostgreSQL")
|
||||
from lib.core.dicts import PGSQL_PRIVS
|
||||
ncols = max(PGSQL_PRIVS.keys())
|
||||
row = ["pguser"] + ["0"] * ncols
|
||||
row[1] = "1" # createdb
|
||||
row[2] = "1" # super
|
||||
umod.inject.getValue = lambda query, *a, **k: [row]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]})
|
||||
self.assertIn("pguser", areAdmins)
|
||||
|
||||
def test_privileges_mysql_lt5_yn_flags(self):
|
||||
# MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags
|
||||
# mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv.
|
||||
set_dbms("MySQL")
|
||||
from lib.core.dicts import MYSQL_PRIVS
|
||||
kb.data.has_information_schema = False
|
||||
ncols = max(MYSQL_PRIVS.keys())
|
||||
row = ["root"] + ["N"] * ncols
|
||||
row[1] = "Y" # select_priv
|
||||
row[3] = "Y" # update_priv
|
||||
umod.inject.getValue = lambda query, *a, **k: [row]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertIn(MYSQL_PRIVS[1], privileges["root"])
|
||||
self.assertIn(MYSQL_PRIVS[3], privileges["root"])
|
||||
self.assertNotIn(MYSQL_PRIVS[2], privileges["root"])
|
||||
|
||||
def test_privileges_firebird_letter_codes(self):
|
||||
# Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS.
|
||||
set_dbms("Firebird")
|
||||
from lib.core.dicts import FIREBIRD_PRIVS
|
||||
umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertEqual(set(privileges["fbuser"]),
|
||||
{FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]})
|
||||
|
||||
def test_privileges_db2_grant_codes(self):
|
||||
# DB2: privilege string is "<name>,<grant-letters>"; each 'Y'/'G' letter at
|
||||
# position i appends the DB2_PRIVS[i] name to the privilege.
|
||||
set_dbms("DB2")
|
||||
from lib.core.dicts import DB2_PRIVS
|
||||
conf.user = "db2admin"
|
||||
# "DBADM" plus a grant string whose first letter (position 1) is 'Y' ->
|
||||
# DB2_PRIVS[1] ("CONTROLAUTH") is appended.
|
||||
umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]]
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
joined = " ".join(privileges["DB2ADMIN"])
|
||||
self.assertIn("DBADM", joined)
|
||||
self.assertIn(DB2_PRIVS[1], joined)
|
||||
|
||||
|
||||
class TestUsersPrivilegesInference(_UsersBase):
|
||||
def test_privileges_inference_mysql(self):
|
||||
# Blind privilege enumeration for a named user: count, then one privilege
|
||||
# string per index. MySQL >= 5 adds each verbatim.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
conf.user = "root"
|
||||
privs = ["SELECT", "SUPER"]
|
||||
umod.inject.getValue = _inference_gv(2, privs)
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
# the user key is wildcard-wrapped for the MySQL information_schema LIKE
|
||||
key = [k for k in privileges if "root" in k][0]
|
||||
self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"})
|
||||
self.assertTrue(areAdmins) # SUPER => admin
|
||||
|
||||
def test_privileges_inference_oracle(self):
|
||||
set_dbms("Oracle")
|
||||
self._inference()
|
||||
conf.user = "system"
|
||||
umod.inject.getValue = _inference_gv(1, ["DBA"])
|
||||
users = Users()
|
||||
kb.data.cachedUsersPrivileges = {}
|
||||
privileges, areAdmins = users.getPrivileges()
|
||||
self.assertIn("SYSTEM", privileges)
|
||||
self.assertEqual(privileges["SYSTEM"], ["DBA"])
|
||||
self.assertIn("SYSTEM", areAdmins)
|
||||
|
||||
|
||||
class TestUsersPasswordHashesInference(_UsersBase):
|
||||
def test_password_hashes_inference_grouping(self):
|
||||
# Blind password-hash enumeration for two users: per-user count, then one
|
||||
# hash per index. Assert each user maps to its own hash list.
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
conf.user = "root,guest"
|
||||
|
||||
# per-user single hash; count is 1 for every user
|
||||
hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"}
|
||||
|
||||
def gv(query, *a, **k):
|
||||
if k.get("expected") == EXPECTED.INT:
|
||||
return "1"
|
||||
for u, h in hashes.items():
|
||||
if u in query:
|
||||
return [h]
|
||||
return [None]
|
||||
|
||||
umod.inject.getValue = gv
|
||||
users = Users()
|
||||
kb.data.cachedUsersPasswords = {}
|
||||
res = users.getPasswordHashes()
|
||||
self.assertEqual(res["root"], ["*ROOTHASH"])
|
||||
self.assertEqual(res["guest"], ["*GUESTHASH"])
|
||||
|
||||
def test_password_hashes_inference_dedup(self):
|
||||
# The same hash returned twice for a user must be de-duplicated at the end
|
||||
# (kb.data.cachedUsersPasswords[user] = list(set(...))).
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
conf.user = "root"
|
||||
umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"])
|
||||
users = Users()
|
||||
kb.data.cachedUsersPasswords = {}
|
||||
res = users.getPasswordHashes()
|
||||
self.assertEqual(res["root"], ["*DUP"])
|
||||
|
||||
|
||||
class TestUsersGetUsersInference(_UsersBase):
|
||||
def test_get_users_inference(self):
|
||||
set_dbms("MySQL")
|
||||
self._inference()
|
||||
umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"])
|
||||
users = Users()
|
||||
kb.data.cachedUsers = []
|
||||
res = users.getUsers()
|
||||
self.assertEqual(sorted(res), ["guest@%", "root@localhost"])
|
||||
|
||||
def test_is_dba_mssql(self):
|
||||
# MSSQL isDba goes through the generic checkBooleanExpression branch.
|
||||
set_dbms("Microsoft SQL Server")
|
||||
umod.inject.checkBooleanExpression = lambda query, *a, **k: True
|
||||
users = Users()
|
||||
kb.data.isDba = None
|
||||
self.assertTrue(users.isDba())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue